Click here to Skip to main content
Click here to Skip to main content

Reading Image Headers to Get Width and Height

By , 28 Apr 2009
 

Introduction

I had a requirement to cache the orientation of JPEGs within a set of folders. The easiest way is just to load each image and work out whether it is landscape or portrait based on the width and height, possibly like this:

public bool IsLandscape(string path)
{
  using (Bitmap b = new Bitmap(path))
  {
    return b.Width > b.Height;
  }
}

This is great when there are only a few images but it is incredibly slow as the framework has to load the image into GDI and then marshal it over to .NET.

Improvement One - Multi-Threading

Performance could be improved by creating a queue of image paths and then loading them on multiple threads potentially using the ThreadPool bound to the number of physical core on the machine.

foreach (string path in paths)
{
  // add each path as a task in the thread pool
  ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadCallback), path);
}

// wait until all images have been loaded
lock (this.Landscapes)
{
  Monitor.Wait(this.Landscapes);
}
private void ThreadCallback(object stateInfo)
{
  string path = (string)stateInfo;
  bool isLandscape = this.IsLandscape(path);
           
  lock (this.Landscapes)
  {
    if (isLandscape)
    {
      this.Landscapes.Add(path);
    }

    imagesRemaining--;

    if (imagesRemaining == 0)
    {
      // all images loaded, signal the main thread
      Monitor.Pulse(this.Landscapes);
    }
  }
}

Ok so performance is improved however new issues arise:

  1. The main performance bottleneck is IO bound as loading an image from disk and converting it to a usable bitmap takes phenomenally longer than getting the size once it is in memory.
  2. Bitmaps required a lot of memory, we can be talking about upwards of ten megabytes depending on the total number of pixels.
  3. Most computers only have a couple of core so threading is of limited benefit.

Improvement Two – Reading the Headers

It occurred to me that there were a number of applications that read width and height information remarkably quickly, too fast to have read the whole file; turns out there are headers in image files which contain width and height – bingo.

After some searching, I came across this buried forum post which gives a great example of how to read not only JPEG headers but also GIF, PNG and BMP:

The post is great although I found it couldn't read all JPEG file headers for some reason. Firstly I modified DecodeJfif so that the chunk length could be an unsigned 16 bit integer (ushort in C#):

private static Size DecodeJfif(BinaryReader binaryReader)
{
  while (binaryReader.ReadByte() == 0xff)
  {
    byte marker = binaryReader.ReadByte();
    short chunkLength = ReadLittleEndianInt16(binaryReader);
    if (marker == 0xc0)
    {
      binaryReader.ReadByte();
      int height = ReadLittleEndianInt16(binaryReader);
      int width = ReadLittleEndianInt16(binaryReader);
      return new Size(width, height);
    }

    if (chunkLength < 0)
    {
      ushort uchunkLength = (ushort)chunkLength;
      binaryReader.ReadBytes(uchunkLength - 2);
    }
    else
    {
      binaryReader.ReadBytes(chunkLength - 2);
    }
  }

  throw new ArgumentException(errorMessage);
}

Secondly, I added a try/catch block around getting the dimensions so that if the header isn't present, it falls back to the slow way:

public static Size GetDimensions(string path)
{
  try
  {
    using (BinaryReader binaryReader = new BinaryReader(File.OpenRead(path)))
    {
      try
      {
        return GetDimensions(binaryReader);
      }
      catch (ArgumentException e)
      {
        string newMessage = string.Format("{0} file: '{1}' ", errorMessage, path);

        throw new ArgumentException(newMessage, "path", e);
      }
    }
  }
  catch (ArgumentException)
  {
    //do it the old fashioned way

    using (Bitmap b = new Bitmap(path))
    {
      return b.Size;
    }              
  }
}

Reading just the headers produced such a massive performance improvement that I removed the multi-threading and just used one thread to process each image sequentially.

Putting It All Together

To further increase performance, I created an XML cache file with width, height and date modified information so that only images that had changed would have their headers checked. I didn't want the XML file to be saved every time an image was cached as that would be a new bottleneck. So I added a timer which saved the data to XML 5 seconds after the save method was called. I used Linq-To-XML to save the list of ImageFileAttributes to disk:

class ImageListToXml
{
  private const string XmlRoot = "Cache";
  private const string XmlImagePath = "ImagePath";
  private const string XmlWidth = "Width";
  private const string XmlHeight = "Height";
  private const string XmlImageCached = "ImageCached";
  private const string XmlLastModified = "LastModified";

  public static void LoadFromXml(string filePath, ImageList list)
  {
    list.Clear();
    XDocument xdoc = XDocument.Load(filePath);
    list.AddRange(
      from d in xdoc.Root.Elements()
      select new ImageFileAttributes(
        (string)d.Attribute(XmlImagePath),
        new Size(
          (int)d.Attribute(XmlWidth),
          (int)d.Attribute(XmlHeight)),
        (DateTime)d.Attribute(XmlLastModified)));
  }

  public static void SaveAsXml(string filePath, ImageList list)
  {
    XElement xml = new XElement(XmlRoot,
      from d in list
      select new XElement(XmlRoot,
        new XAttribute(XmlImagePath, d.Path),
        new XAttribute(XmlWidth, d.Size.Width),
        new XAttribute(XmlHeight, d.Size.Height),
        new XAttribute(XmlLastModified, d.LastModified ?? DateTime.MinValue)));

    xml.Save(filePath);
  }
}

Results

Using header produces tremendous performance improvements and caching the dimension results from the images takes the process from seconds to milliseconds.

Performance improvements for 563 images, from 198257ms to 69ms.

This console output gives an indication of the orders of magnitude that can be gained; we are talking about improving the total time taken from more than 3 minutes to less than one-tenth of a second.

History

  • Version 1.0 - Initial release

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

andywilsonuk
Software Developer (Senior) Play.com
United Kingdom United Kingdom
Member
Hi, my name's Andy Wilson and I live in Cambridge, UK where I work as a Senior C# Software Developer.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5mentorMd. Marufuzzaman14 Mar '12 - 23:29 
It's outstanding & helpful for developers. Thanks for sharing.
QuestionVery helpful!memberGTatham25 Jun '11 - 4:20 
Was looking for a faster way to get vertical/horizontal image dimensions than loading the image, and this fit the bill perfectly. Thanks!
QuestionBrilliant articlememberborgy337723 Jun '11 - 21:00 
Well done, helped me out heaps, Thanks.
GeneralMy vote of 5membertoddsecond13 Feb '11 - 1:38 
Gave me just the information I needed in a convenient form. Thanks very much. Stephen
GeneralSupport for JPEGs with progressive encoding (etc.)memberUli Hutzler25 Jan '10 - 4:28 
Frist, thank you very much for your informative article. If I may make one small suggestion:
 
The "start-of-frame" (SOF) marker in a JPEG file that precedes the width and height information doesn't necessarily have to be 0xFFC0.
Depending on the type of encoding used it may, as I understand it, be any of the "SOFn" values (0xFFCn) listed, for instance, here:
 
http://home.elka.pw.edu.pl/~mmanowie/psap/neue/1%20JPEG%20Overview.htm[^]
 
Especially the value 0xFFC2 used with progressively encoded JPEGs seems to be quite common. (Based on a very non-representative personal survey Smile | :) ) So if you check at least for this value in the DecodeJfif method, you should be able to extract size information from a wider range of JPEG images.
 
Again, thank you very much and apologies for my poor English.
GeneralTranslation to .Net 2.0memberAnkit Rajpoot4 Jun '09 - 13:16 
Hello devwilson,
 
First of all, congratulations for a well written article on a well researched topic.
 
I'm developing a windows application that reads image width and height. So naturally, I was looking for something like this. Well, your article and code really helped me out. But just one thing, that I cannot figure out how to do is to translate the following line:
 
int maxMagicBytesLength = imageFormatDecoders.Keys.OrderByDescending(x => x.Length).First().Length;

 
into equivalent .Net 2.0 (C# 2.0) code. As my rest of the application is .net 2.0. I just can't figure out the use of the OrderByDescending() method. I've no experience of .Net 3.0 or later versions. Please help me.
 
Thanks in advance. Smile | :)
 
Excuse me for buttin' in, but I'm interrupt driven.

AnswerRe: Translation to .Net 2.0memberdevwilson4 Jun '09 - 22:43 
Hi Ankit,
 
Thanks for the feedback I'm glad you found the article useful!
 
I see what you mean about converting to .net 2.0, my original idea was to just sort the Keys but of course you can't, and in .net 2.0 you can't convert the Keys to a List and sort that either D'Oh! | :doh: . I think this should do what you want however:
 
int maxMagicBytesLength = 0;
 
foreach (byte[] bytes in imageFormatDecoders.Keys)
{
    if (bytes.Length > maxMagicBytesLength)
    {
        maxMagicBytesLength = bytes.Length;
    }
}
 
The Linq to Object extensions really do make for more compact code (9 lines to 1).
 
Good luck with your project.
 
- devwilson

GeneralRe: Translation to .Net 2.0memberAnkit Rajpoot5 Jun '09 - 1:52 
Thanks a lot!!! Smile | :) Smile | :) Smile | :) Thumbs Up | :thumbsup:
 
Excuse me for buttin' in, but I'm interrupt driven.

GeneralRe: Translation to .Net 2.0membereonic10 Jun '11 - 19:59 
Thanks for a great post really useful.
 
Could anyone post the .Net 2 version of the imageFormatDecoders Dictionary bit not using Linq?
AnswerRe: Translation to .Net 2.0memberheadkaze15 Aug '12 - 18:30 
Here is a .NET 2.0 version.
 
    /// <summary>
    /// Taken from http://stackoverflow.com/questions/111345/getting-image-dimensions-without-reading-the-entire-file/111349
    /// Minor improvements including supporting unsigned 16-bit integers when decoding Jfif and added logic
    /// to load the image using new Bitmap if reading the headers fails
    /// </summary>
    public static class ImageHeader
    {
        private delegate TResult Func<T, TResult>(T arg);
        private const string errorMessage = "Could not recognise image format.";
 
        private static Dictionary<byte[], Func<BinaryReader, Size>> imageFormatDecoders = new Dictionary<byte[], Func<BinaryReader, Size>>()
        { 
            { new byte[] { 0x42, 0x4D }, DecodeBitmap }, 
            { new byte[] { 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 }, DecodeGif }, 
            { new byte[] { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 }, DecodeGif }, 
            { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }, DecodePng },
            { new byte[] { 0xff, 0xd8 }, DecodeJfif }, 
        };
 
        /// <summary>        
        /// Gets the dimensions of an image.        
        /// </summary>        
        /// <param name="path">The path of the image to get the dimensions of.</param>        
        /// <returns>The dimensions of the specified image.</returns>        
        /// <exception cref="ArgumentException">The image was of an unrecognised format.</exception>        
        public static Size GetDimensions(string path)
        {
            try
            {
                using (BinaryReader binaryReader = new BinaryReader(File.OpenRead(path)))
                {
                    try
                    {
                        return GetDimensions(binaryReader);
                    }
                    catch (ArgumentException e)
                    {
                        string newMessage = string.Format("{0} file: '{1}' ", errorMessage, path);
 
                        throw new ArgumentException(newMessage, "path", e);
                    }
                }
            }
            catch (ArgumentException)
            {
                //do it the old fashioned way
 
                using (Bitmap b = new Bitmap(path))
                {
                    return b.Size;
                }
            }
        }
 
        public static int GetMaxMagicBytesLength()
        {
            int maxMagicBytesLength = 0;
 
            foreach (byte[] magicBytes in imageFormatDecoders.Keys)
                maxMagicBytesLength = Math.Max(maxMagicBytesLength, magicBytes.Length);
 
            return maxMagicBytesLength;
        }
 
        /// <summary>        
        /// Gets the dimensions of an image.        
        /// </summary>        
        /// <param name="path">The path of the image to get the dimensions of.</param>        
        /// <returns>The dimensions of the specified image.</returns>        
        /// <exception cref="ArgumentException">The image was of an unrecognised format.</exception>            
        public static Size GetDimensions(BinaryReader binaryReader)
        {
            int maxMagicBytesLength = GetMaxMagicBytesLength();
 
            byte[] magicBytes = new byte[maxMagicBytesLength];
 
            for (int i = 0; i < maxMagicBytesLength; i += 1)
            {
                magicBytes[i] = binaryReader.ReadByte();
 
                foreach (var kvPair in imageFormatDecoders)
                {
                    if (StartsWith(magicBytes, kvPair.Key))
                    {
                        return kvPair.Value(binaryReader);
                    }
                }
            }
 
            throw new ArgumentException(errorMessage, "binaryReader");
        }
 
        private static bool StartsWith(byte[] thisBytes, byte[] thatBytes)
        {
            for (int i = 0; i < thatBytes.Length; i += 1)
            {
                if (thisBytes[i] != thatBytes[i])
                {
                    return false;
                }
            }
 
            return true;
        }
 
        private static short ReadLittleEndianInt16(BinaryReader binaryReader)
        {
            byte[] bytes = new byte[sizeof(short)];
 
            for (int i = 0; i < sizeof(short); i += 1)
            {
                bytes[sizeof(short) - 1 - i] = binaryReader.ReadByte();
            }
            return BitConverter.ToInt16(bytes, 0);
        }
 
        private static ushort ReadLittleEndianUInt16(BinaryReader binaryReader)
        {
            byte[] bytes = new byte[sizeof(ushort)];
 
            for (int i = 0; i < sizeof(ushort); i += 1)
            {
                bytes[sizeof(ushort) - 1 - i] = binaryReader.ReadByte();
            }
            return BitConverter.ToUInt16(bytes, 0);
        }
 
        private static int ReadLittleEndianInt32(BinaryReader binaryReader)
        {
            byte[] bytes = new byte[sizeof(int)];
            for (int i = 0; i < sizeof(int); i += 1)
            {
                bytes[sizeof(int) - 1 - i] = binaryReader.ReadByte();
            }
            return BitConverter.ToInt32(bytes, 0);
        }
 
        private static Size DecodeBitmap(BinaryReader binaryReader)
        {
            binaryReader.ReadBytes(16);
            int width = binaryReader.ReadInt32();
            int height = binaryReader.ReadInt32();
            return new Size(width, height);
        }
 
        private static Size DecodeGif(BinaryReader binaryReader)
        {
            int width = binaryReader.ReadInt16();
            int height = binaryReader.ReadInt16();
            return new Size(width, height);
        }
 
        private static Size DecodePng(BinaryReader binaryReader)
        {
            binaryReader.ReadBytes(8);
            int width = ReadLittleEndianInt32(binaryReader);
            int height = ReadLittleEndianInt32(binaryReader);
            return new Size(width, height);
        }
 
        private static Size DecodeJfif(BinaryReader binaryReader)
        {
            while (binaryReader.ReadByte() == 0xff)
            {
                byte marker = binaryReader.ReadByte();
                short chunkLength = ReadLittleEndianInt16(binaryReader);
                if (marker == 0xc0)
                {
                    binaryReader.ReadByte();
                    int height = ReadLittleEndianInt16(binaryReader);
                    int width = ReadLittleEndianInt16(binaryReader);
                    return new Size(width, height);
                }
 
                if (chunkLength < 0)
                {
                    ushort uchunkLength = (ushort)chunkLength;
                    binaryReader.ReadBytes(uchunkLength - 2);
                }
                else
                {
                    binaryReader.ReadBytes(chunkLength - 2);
                }
            }
 
            throw new ArgumentException(errorMessage);
        }
    }

GeneralModifitication? [modified]memberskogenGump7 May '09 - 6:02 
Hi Dev,
Interesting article, I was looking at backporting this to .net 2.0 and I noticed this optimisation (I have to use a byte[] not a BinaryReader but I implemented it here for brevity)
 
public static Size GetSize(byte[] source)
{
BinaryReader br = new BinaryReader(new MemoryStream(source));
switch (source[0])
{
case 0x42:
return DecodeBitmap(br);
case 0x47:
return DecodeGif(br);
case 0x89:
return DecodePng(br);
case 0xff:
return DecodeJfif(br);
}
throw new ArgumentException(errorMessage, "source");
}
 
Of course, its makes it more interesting if you need to add other types, but assuming you don't all that often, does this speed it up for you ?
 
Edit: Not forgetting to increment the counter by the size of the magic bytes Big Grin | :-D
 
modified on Friday, May 8, 2009 5:15 AM

GeneralRe: Modifitication?memberdevwilson8 May '09 - 1:32 
Hi skogenGump,
 
I suppose the point of matching against the complete magic bytes is to avoid false positives; for example if a new format came around that used 0xff, 0xd9 (as opposed to JPG 0xff, 0xd8), the code you've suggested would just use the first listed entry. This isn't to say your approach won't work just that you need to be careful what you're passing in your BinaryReader Smile | :)
 
My guess is that performance improvements would be negligible but give it a go and let me know how you get on with the 2.0 version and whether you spy any other performance improvements.
 
Cheers
 
- devwilson

GeneralUsing BitmapDecodermembergordonwatts5 May '09 - 17:30 
I think the same thing can be done using some built-in classes in the frameworK:
 
                BitmapDecoder dc = BitmapDecoder.Create(imageStream, BitmapCreateOptions.DelayCreation, BitmapCacheOption.None);
                if (dc.Frames.Count == 0)
                {
                    return null;
                }
                double xsize = dc.Frames[0].Width / 96.0 * dc.Frames[0].DpiX;
                double ysize = dc.Frames[0].Height / 96.0 * dc.Frames[0].DpiY;
 
                imageStream.Close();
 
                return new int[] { (int)xsize, (int)ysize };
 
At least, that is what I'm using to do very fast readin of header info in my images.
 
I really like the multi-threaded approach.
 
Cheers,
Gordon.
 
Cheers,
Gordon.

GeneralRe: Using BitmapDecodermemberdevwilson6 May '09 - 0:39 
Hi Gordon,
 
Interesting feedback!
 
Yeah it seems that this will give you the correct width and height however when I quickly baked it into the sample code, the performance seemed slower than reading the headers directly, ~800ms single thread BitmapDecoder, ~600ms multi-thread BitmapDecoder vs ~200ms reading headers directly - did you get similar results?
 
I'm tempted to add this as a fallback method if reading the headers directly fails and move the current logic for opening the full bitmap to a third fallback position.
 
As for the multi-threaded approach in my sample, I think you could probably tune some of the locking to give more performance.
 
- devwilson

GeneralRe: Using BitmapDecodermembergordonwatts10 May '09 - 12:32 
Hi,
I'm sure your timings are correct - I never tested it. When the speed when from the amazingly slow full image load to the much faster time (it wasn't 800ms on my machine...) the performance was good enough for the images I was loading. I can't help but wonder if 800ms was a bit slow on my system - I was loading 1000's of jpegs and extracting their information and it wasn't taking 800 or so seconds. At any rate, I'm sure your method is a bit faster!
 
Cheers,
Gordon.
 
Cheers,
Gordon.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130523.1 | Last Updated 28 Apr 2009
Article Copyright 2009 by andywilsonuk
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid