Last week, I showed you how to use the Custom Columns and ImageView Inspector to find unoptimized images with Telerik Fiddler. In today’s installment, I’ll show you how to extend Fiddler to annotate PNG images based on the bloat they contain, so that you can trivially recognize unoptimized images as you surf.

When you browse with the Show Image Bloat extension enabled, each image will be filled from the bottom based on the percentage of the file which consists of unnecessary bloat; an image with 2% overhead will show a small line at the bottom, while a product logo containing 90% bloat will be nearly completely obscured by the fill.

For example, here’s what you’ll see in your browser on several popular websites which use bloated PNG files:

Bloated images

As you’ll see in the extension’s source code below, detecting bloat in PNGs is quite easy because the image format specification makes parsing the chunks of the file very straightforward.

Using the Extension

Install the latest version of the extension from here.

To enable the extension, tick the Rules > Show Image Bloat menu item. Fiddler will subsequently rewrite PNG images as they download.

If you'd like to use a different fill color, type the following command in the QuickExec box:

    prefs set fiddler.ui.Colors.ImageBloat #FF4500

...where the color is a hex value or color name. Hit Enter to submit the command.

To see the original image, select the affected Session in the Fiddler Web Sessions list, and hit the R key to redownload the file; the extension does not modify reissued requests. You can then use the ImageView Inspector to see analysis of the original image.

Source Code

The following is the original version of the code, which analyzes PNG files only. The latest version of the code uses the "brick" style brush and also analyzes JPEG files.

ShowBloat.cs

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Text;
using System.Windows.Forms;
using Fiddler;
using System.Diagnostics;
using System.Reflection;
[assembly: Fiddler.RequiredVersion("2.4.9.4")]
[assembly: AssemblyVersion("2.4.9.4")]

public class ShowBloat : IAutoTamper3
{
    private bool bEnabled = false;
    MenuItem miMenuItem = new MenuItem("Show Image &Bloat");

    public void OnBeforeUnload() {
        bEnabled = false;
        miMenuItem.Dispose();
    }

    public void OnPeekAtRequestHeaders(Session oS) { }
    public void OnPeekAtResponseHeaders(Session oS) {
      if (bEnabled && oS.oResponse.MIMEType.OICStartsWith("image/png"))
      {
        oS.bBufferResponse = true;
      }
    }

    public void OnLoad() {
        // Add the Rules menu item
        miMenuItem.Click += delegate(object s, EventArgs e)
        {
            miMenuItem.Checked = !miMenuItem.Checked;
            bEnabled = miMenuItem.Checked;
        };
        FiddlerApplication.UI.mnuRules.MenuItems.AddRange(new[] { miMenuItem });
    }

 

    internal static Color GetColorFromString(string sColor) {
        if ((sColor.Length == 7) && (sColor[0] == '#'))
        {
            return Color.FromArgb(Convert.ToInt32(sColor.Substring(1, 2), 0x10),
                                    Convert.ToInt32(sColor.Substring(3, 2), 0x10),
                                    Convert.ToInt32(sColor.Substring(5, 2), 0x10));
        }
        else {
            return Color.FromName(sColor);
        }
    }

    // Recreates the image using a non-Indexed-Pixel format, which GDI+ cannot readily manipulate
    // (Specifically, Graphics.FromImage() will throw an exception)
    private Image UpgradeImageFormat(Image InImg) {
        Bitmap OutImg = new Bitmap(InImg.Width, InImg.Height, PixelFormat.Format32bppPArgb);
        using (Graphics g = Graphics.FromImage(OutImg))
        {
            g.DrawImage(InImg, new Rectangle(0, 0, InImg.Width, InImg.Height), 0, 0, InImg.Width, InImg.Height, GraphicsUnit.Pixel);
            InImg.Dispose();
        }
        return OutImg;
    }

    // Count the number of bytes considered bloat
    private static int GetPNGBloat(byte[] arrImg) {
        int iMaxLen = arrImg.Length;
        // Ensure PNG has MagicBytes and is > minimal length for a valid file.
        if ((iMaxLen < 28) || (0x89 != arrImg[0]) || (0x50 != arrImg[1]) || (0x4E != arrImg[2]) || (0x47 != arrImg[3]))
        {
            // Malformed
            return 0;
        }

        int iPtr = 8;
        bool bDone = false;
        int iBloat = 0;
        do
        {
            int iChunkSize = (arrImg[iPtr] << 24) + (arrImg[iPtr + 1] << 16) + (arrImg[iPtr + 2] << 8) + arrImg[iPtr + 3];
            if (iChunkSize < 0)
            {
                Debug.Assert(false, "Malformed chunk");
                break;
            }
            iPtr += 4;

            string sChunkType = new String(new char[]
                   { (char)arrImg[iPtr], (char)arrImg[iPtr + 1],
                     (char)arrImg[iPtr + 2], (char)arrImg[iPtr + 3] });
            iPtr += 4;

            switch (sChunkType)
            {
                case "IEND": // End of file
                    if ((iPtr + 4) < iMaxLen)
                    {
                        iBloat += (iMaxLen - iPtr - 4);
                    }
                    bDone = true;
                    break;

                case "iCCP": // Color Correction
                case "tIME":
                case "gAMA": // Gamma
                case "PLTE": // Palette
                case "acTL": //
Animation info
                case "IHDR": // Header
                case "cHRM": // Chromacity
                case "bKGD": // background color
                case "tRNS": // Transparency for paletted images
                case "sBIT": // Significant bits per sample
                case "sRGB": // color space
                case "pHYs": // pixelsize
                case "hIST": // histogram for paletted images
                case "vpAg": // Virtual page size
                case "oFFs": // Image offset
                case "fcTL": // AnimatedPNG Frame Control
                case "fdAT": // Animated frame data
                case "IDAT": // Image data
                // Do nothing, this is an Image Data chunk
                    break;

                case "tEXt":
                case "iTXt":
                case "zTXt":
                case "prVW":
                case "mkBF":
                case "mkTS":
                case "mkBS":
                case "mkBT":
                default:
                // This chunk should be considered "bloat"
                    iBloat += (12 + iChunkSize);
                    break;
            }

            // skip ReadChunkData
            iPtr += iChunkSize;

            // skip CheckSum
            iPtr += 4;

            // If we're out of space, we're done.
            if (iPtr > iMaxLen - 12) bDone = true;
        } while (!bDone);

        return iBloat;
    }

    public void AutoTamperRequestBefore(Session oS) { }
    public void AutoTamperRequestAfter(Session oS) { }
    public void AutoTamperResponseBefore(Session oS) { }
    public void OnBeforeReturningError(Session oSession){}

    public void AutoTamperResponseAfter(Session oS) {
        if (!bEnabled) return;

        // Skip Fiddler-generated reissues to allow for easy examination of originals
        if (oS.isAnyFlagSet(SessionFlags.RequestGeneratedByFiddler) &&
            !oS.oFlags.ContainsKey("X-FROM-BUILDER")) return;
           
        try
        {
            if (!oS.oResponse.MIMEType.OICStartsWith("image/png")) return;
            oS.utilDecodeResponse();

            byte[] arrImg = oS.responseBodyBytes;
            if (Utilities.IsNullOrEmpty(arrImg)) return;
            var iBloat = GetPNGBloat(arrImg);
            float flBloat = iBloat / (float)arrImg.Length;

            oS["X-ImageBloat"] = String.Format("{0:N0}/{1:N0} bytes ({2}%)",
                                                iBloat, arrImg.Length, flBloat*100);
            if (iBloat < 1) return;

            using (MemoryStream oStream = new MemoryStream(arrImg))
            {
                Bitmap oBMP = new Bitmap(oStream);

                if ((oBMP.PixelFormat & PixelFormat.Indexed) != PixelFormat.Undefined)
                {
                    oBMP = (Bitmap)UpgradeImageFormat(oBMP);
                }

                using (Graphics gfxImage = Graphics.FromImage(oBMP))
                {
                  Color clrBloat = GetColorFromString(FiddlerApplication.Prefs.GetStringPref("fiddler.ui.Colors.ImageBloat", "#FF4500"));
                  oS["ui-backcolor"] = FiddlerApplication.Prefs.GetStringPref("fiddler.ui.Colors.ImageBloat", "#FF4500");
                  Brush brsh = new SolidBrush(clrBloat);

                  int iTop = oBMP.Height - (int)(flBloat * oBMP.Height);

                  gfxImage.FillRectangle(brsh, 0, iTop, oBMP.Width, oBMP.Height);
                  brsh.Dispose();

                  using (MemoryStream oNewStream = new MemoryStream())
                  {
                    oBMP.Save(oNewStream, ImageFormat.Png);
                    oS.responseBodyBytes = oNewStream.ToArray();
                    oS.oResponse.headers["Cache-Control"] = "no-cache";
                    oS.oResponse.headers["Content-Length"] = oS.responseBodyBytes.Length.ToString();
                  }
                }
            }
        }
        catch (Exception eX)
        {
            FiddlerApplication.Log.LogFormat("[ImageBloat] Threw {0}", eX.Message);
        }
    }

}

Make.cmd

C:\windows\microsoft.net\framework64\v3.5\csc.exe /d:TRACE /target:library /out:"%USERPROFILE%\documents\fiddler2\scripts\ImageBloat.dll" ShowBloat.cs /reference:"C:\Program Files (x86)\fiddler2\fiddler.exe"


About the Author

Eric Lawrence

(@ericlaw) has built websites and web client software since the mid-1990s. After over a decade of working on the web for Microsoft, Eric joined Telerik in October 2012 to enhance the Fiddler Web Debugger on a full-time basis. With his recent move to Austin, Texas, Eric has now lived in the American South, North, West, and East.

Related Posts

Comments

Comments are disabled in preview mode.