Search code examples
c#.netimageimage-manipulation

Getting image dimensions without reading the entire file


Is there a cheap way to get the dimensions of an image (jpg, png, ...)? Preferably, I would like to achieve this using only the standard class library (because of hosting restrictions). I know that it should be relatively easy to read the image header and parse it myself, but it seems that something like this should be already there. Also, I’ve verified that the following piece of code reads the entire image (which I don’t want):

using System;
using System.Drawing;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            Image img = new Bitmap("test.png");
            System.Console.WriteLine(img.Width + " x " + img.Height);
        }
    }
}

Solution

  • Your best bet as always is to find a well tested library. However, you said that is difficult, so here is some dodgy largely untested code that should work for a fair number of cases:

    using System;
    using System.Collections.Generic;
    using System.Drawing;
    using System.IO;
    using System.Linq;
    
    namespace ImageDimensions
    {
        public static class ImageHelper
        {
            const string errorMessage = "Could not recognize 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 unrecognized format.</exception>
            public static Size GetDimensions(string path)
            {
                using (BinaryReader binaryReader = new BinaryReader(File.OpenRead(path)))
                {
                    try
                    {
                        return GetDimensions(binaryReader);
                    }
                    catch (ArgumentException e)
                    {
                        if (e.Message.StartsWith(errorMessage))
                        {
                            throw new ArgumentException(errorMessage, "path", e);
                        }
                        else
                        {
                            throw e;
                        }
                    }
                }
            }
    
            /// <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 unrecognized format.</exception>    
            public static Size GetDimensions(BinaryReader binaryReader)
            {
                int maxMagicBytesLength = imageFormatDecoders.Keys.OrderByDescending(x => x.Length).First().Length;
    
                byte[] magicBytes = new byte[maxMagicBytesLength];
    
                for (int i = 0; i < maxMagicBytesLength; i += 1)
                {
                    magicBytes[i] = binaryReader.ReadByte();
    
                    foreach(var kvPair in imageFormatDecoders)
                    {
                        if (magicBytes.StartsWith(kvPair.Key))
                        {
                            return kvPair.Value(binaryReader);
                        }
                    }
                }
    
                throw new ArgumentException(errorMessage, "binaryReader");
            }
    
            private static bool StartsWith(this 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(this 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 int ReadLittleEndianInt32(this 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 = binaryReader.ReadLittleEndianInt32();
                int height = binaryReader.ReadLittleEndianInt32();
                return new Size(width, height);
            }
    
            private static Size DecodeJfif(BinaryReader binaryReader)
            {
                while (binaryReader.ReadByte() == 0xff)
                {
                    byte marker = binaryReader.ReadByte();
                    short chunkLength = binaryReader.ReadLittleEndianInt16();
    
                    if (marker == 0xc0)
                    {
                        binaryReader.ReadByte();
    
                        int height = binaryReader.ReadLittleEndianInt16();
                        int width = binaryReader.ReadLittleEndianInt16();
                        return new Size(width, height);
                    }
    
                    binaryReader.ReadBytes(chunkLength - 2);
                }
    
                throw new ArgumentException(errorMessage);
            }
        }
    }
    

    Hopefully the code is fairly obvious. To add a new file format you add it to imageFormatDecoders with the key being an array of the "magic bits" which appear at the beginning of every file of the given format and the value being a function which extracts the size from the stream. Most formats are simple enough, the only real stinker is jpeg.