ITHCWY: Robert Ellison's Blog

Crushing PNGs in .NET

Crushing PNGs in .NET

I'm working on page speed and Google PageSpeed Insights is telling me that my PNGs are just way too large. Sadly .NET does not provide any way to optimize PNG images so there is no easy fix - just unmanaged libraries and command line tools.

I have an allergy to manual processes so I've lashed up some code to automatically find and optimize PNGs in my App_Data folder using PNGCRUSH. I can call CrushAllImages() to fix up everything or CrushImage() when I need to fix up a specific PNG. Code below:

public static void CrushAllImages()
{
    try
    {
        string appDataRoot = HostingEnvironment.MapPath("~/App_Data");
        if (appDataRoot == null)
        {
            return;
        }

        DirectoryInfo directoryInfo = new DirectoryInfo(appDataRoot);
        FileInfo[] pngs = directoryInfo.GetFiles("*.png", SearchOption.AllDirectories);
        foreach (FileInfo png in pngs)
        {
            CrushImage(png.FullName);
        }
    }
    catch (Exception ex)
    {
        //...
    }
}

public static void CrushImage(string fullPath)
{
    if (string.IsNullOrEmpty(fullPath))
    {
        return;
    }

    try
    {
        string markerPath = Path.ChangeExtension(fullPath, ".cng");
        if (File.Exists(markerPath))
        {
            return;
        }

        string crushExe = HostingEnvironment.MapPath("~/App_Data/pngcrush_1_7_77_w32.exe");

        ProcessStartInfo psi = new ProcessStartInfo(crushExe, string.Format(CultureInfo.InvariantCulture, "\"{0}\" \"{1}\"", fullPath, markerPath));
        psi.UseShellExecute = false;
        psi.CreateNoWindow = true;
        psi.LoadUserProfile = false;
        psi.WorkingDirectory = HostingEnvironment.MapPath("~/App_Data");

        Process p = Process.Start(psi);
        if (p == null)
        {
            throw new InvalidOperationException("No Process!");
        }
        p.WaitForExit();

        if (File.Exists(markerPath))
        {
            if (p.ExitCode == 0)
            {
                File.Copy(markerPath, fullPath, true);
                File.WriteAllText(markerPath, "Processed");
            }
            else
            {
                SiteLog.Log.Add(LogSeverity.Error, "CrushImage Failed (non-0 exit code) for " + fullPath);
                File.Delete(markerPath);
            }
        }
    }
    catch (Exception ex)
    {
       // ...
    }
}

Minify and inline CSS for ASP.NET MVC

ASP.NET has a CssMinify class (and a JavaScript variant as well) designed for use in the bundling pipeline. But what if you want to have your CSS minified and inline? Here is an action that is working for me (rendered into a style tag on my _Layout.cshtml using @Html.Action("InlineCss", "Home")).

public ActionResult InlineCss()
{
    BundleContext context = new BundleContext(
        new HttpContextWrapper(System.Web.HttpContext.Current), 
        BundleTable.Bundles, 
        "~/Content/css");
            
    Bundle cssBundle = BundleTable.Bundles.GetBundleFor("~/Content/css");
    BundleResponse response = cssBundle.GenerateBundleResponse(context);
           
    CssMinify cssMinify = new CssMinify();
    cssMinify.Process(context, response);

    return Content(response.Content);
}

Note that I'm using this to inline CSS for this blog. The pages are cached so I'm not worried about how well this action performs. My blog is also basically all landing pages so I'm also not worried about caching a non-inline version for later use, I just drop all the CSS on every page.

Debugging Treasure Trove

Mark Jackson, my co-founder at Cucku, is blogging re-mastered debugging tips from StackHash at infopurge.tumblr.com. StackHash is now an open source project on CodePlex and all of the great content from the original site has been taken offline. This new project is a great resource for debugging on the Windows platform, especially post-mortem crash dump analysis. If that’s your thing do yourself a favor and subscribe to Mark’s blog.

Migrating from Blogger to BlogEngine.NET

(Update June 22, 2010: I've released a tool, Blogger2BlogML, that converts Blogger's ATOM export file to BlogML. I ended up doing this because of problems with comments when I migrated this blog — I had to fix these up manually which was painful. I'm now working on some larger blogs where this would be impossible…)

In January I got an email from Blogger announcing that they're killing FTP support. Apparently only 0.5% of Blogger blogs are published using FTP and it's a huge pain to support, mainly because many hosting providers restrict FTP access to certain IP addresses and if the Google servers running Blogger that moment aren't listed it's technical support time. 

Fair enough, but a bit painful for me as I have five blogs running on top of Blogger. I need FTP publishing as the templates I use end up running as ASP or ASP.NET pages. I Thought He Came With You is the first to move - if you're reading this post then it's up and running on BlogEngine.NET. This is an open source ASP.NET blogging platform. If it works out for this blog over the next month I'll start migrating the others. 

Getting up and running with BlogEngine.NET is easy enough - download the latest release and follow the getting started guide. I added the default install to a new Visual Studio web site project and was able to run it fine in the development server, no need to configure IIS. 

The challenge was moving posts and comments from Blogger into BlogEngine.NET. BlogEngine.NET happily imports and exports BlogML, Blogger spits out it's own Atom export format

Luckily Aaron Lerch has knocked up a PowerShell script to export Blogger to BlogML. This takes your Blogger ID as a parameter and exports all the blogs associated with that ID. You can just pull the <blog> element you need out of the export if necessary. If you use this tool follow the syntax closely from the example on the post. If you're new to PowerShell run this as administrator to start with and enter "Set-ExecutionPolicy RemoteSigned" to give permission to run the script. Then exit and run a non-admin instance of PowerShell to run the script.

After getting the export BlogEngine.NET refused to import it. I kept getting an invalid username or password error from the BlogML importer. Digging into the code a bit I found that a crash was occurring in api/BlogImporter.asmx when a blank category was passed to AddCategories(). I patched this function to skip blank/null categories and was then able to import successfully. I added a bug report (11789) for the problem so please vote for it if you hit the same issue.

The blog was in pretty good shape after the import. Categories were lost so I had to create them again in BlogEngine.NET and then apply them to each post. I also found that the date for comments had been set to the date they were imported. This can be fixed by editing each post directly (the files in App_Data\posts). I fixed the timestamps and also added back the URL for each comment where I had one from Blogger. I also edited my own comments to match my new BlogEngine.NET account so they got decorated as "by the author".

At this point the blog was good to go, but using the default template.

I've now whipped up a new template to match the old blog - I was pleasantly surprised by how easy this was to do. At this point I Thought He Came With You is going live on BlogEngine.NET. 

Almost everything is working at this point. I had a couple of user controls I need to move over - one shows the number of people who haven't visited the blog, the other figures out the most popular posts using the Google Analytics API. I'll see if I can implement at least the latter as a Widget so that other people can use it - watch this space (Update - now available here). 

Overall, I'm impressed. I resisted the urge to just write my own blogging platform which is a testament to how easy it is to both get BlogEngine.NET running and to customize the look and feel. Now I just need to work up the enthusiasm to do the same for my remaining four Blogger based blogs...