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:
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.
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.
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.
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);
}
}
}
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"
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.