I Thought He Came With You is Robert Ellison’s blog about software, marketing, politics, photography, time lapse and the occasional well deserved rant. Follow along with a monthly email, RSS or on Facebook. About 7,250,102,760 people have not visited yet so it might be your first time here. Suggested reading: Got It, or roll the dice.

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.