using System; using System.Collections.Generic; using System.IO; namespace ImageUtility { public struct Dimensions { public int Width; public int Height; public static Dimensions ErrorValue { get { Dimensions returnValue; returnValue.Width = 0; returnValue.Height = 0; return returnValue; } } } /// /// Retrieves width and height of an image, without loading the entire image in memory. /// /// Based on https://stackoverflow.com/a/112711/1377948 /// (with edits from https://stackoverflow.com/a/60667939/1377948 /// for recognizing progressive jpeg and webp). /// /// Code will return (-1, -1) as an error value, instead of any exception throwing. /// public static class Dimension { static readonly Dictionary> ImageFormatDecoders = new Dictionary>() { { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }, DecodePng }, { new byte[] { 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 }, DecodeGif }, { new byte[] { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 }, DecodeGif }, { new byte[] { 0x52, 0x49, 0x46, 0x46 }, DecodeWebP }, { new byte[] { 0xFF, 0xD8 }, DecodeJfif }, { new byte[] { 0x42, 0x4D }, DecodeBitmap }, }; /// /// Currently the longest we have in is the png one, with 8 bytes. /// const int LONGEST_MAGIC_BYTES = 8; static byte[] _magicBytes; /// /// Retrieves width and height of an image, without loading the entire image in memory. /// /// Full path to the image to get the dimensions of. /// (width, height) tuple, or (-1, -1) if not found. public static Dimensions Get(string path) { using (var binaryReader = new BinaryReader(File.OpenRead(path))) { return Get(binaryReader); } } /// /// Retrieves width and height of an image opened in a BinaryReader. /// It will only read through the BinaryReader as least as it possibly /// can to get to the width and height. /// /// A BinaryReader with the image file opened in it. /// (width, height) tuple, or (-1, -1) if not found. public static Dimensions Get(BinaryReader binaryReader) { if (_magicBytes == null) { _magicBytes = new byte[LONGEST_MAGIC_BYTES]; } else { // reset the byte array checker to 0 value for (int i = 0; i < LONGEST_MAGIC_BYTES; i += 1) { _magicBytes[i] = 0; } } // Detect the image type not using the file type, // but via the bytes it has at the start of the file. // // We read up to n number of bytes at the start of the file (where n is LONGEST_MAGIC_BYTES), // and each time we've appended to the _magicBytes we're reading, // check if what we have so far matches any of the image types we know. for (int i = 0; i < LONGEST_MAGIC_BYTES; i += 1) { _magicBytes[i] = binaryReader.ReadByte(); foreach (var kvPair in ImageFormatDecoders) { if (_magicBytes.StartsWith(kvPair.Key)) { // The bytes have been recognized as one of our known image types, // now we use the Decode method assigned for that image type. return kvPair.Value(binaryReader); } } } return Dimensions.ErrorValue; } // ================================================================================= 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; } const int SIZE_OF_SHORT = sizeof(short); static readonly byte[] ShortBytes = new byte[SIZE_OF_SHORT]; static short ReadLittleEndianInt16(this BinaryReader binaryReader) { for (int i = 0; i < SIZE_OF_SHORT; i += 1) { ShortBytes[SIZE_OF_SHORT - 1 - i] = binaryReader.ReadByte(); } return BitConverter.ToInt16(ShortBytes, 0); } const int SIZE_OF_INT = sizeof(int); static readonly byte[] IntBytes = new byte[SIZE_OF_INT]; static int ReadLittleEndianInt32(this BinaryReader binaryReader) { for (int i = 0; i < SIZE_OF_INT; i += 1) { IntBytes[SIZE_OF_INT - 1 - i] = binaryReader.ReadByte(); } return BitConverter.ToInt32(IntBytes, 0); } // ================================================================================= static Dimensions DecodeBitmap(BinaryReader binaryReader) { binaryReader.ReadBytes(16); Dimensions returnValue; returnValue.Width = binaryReader.ReadInt32(); returnValue.Height = binaryReader.ReadInt32(); return returnValue; } static Dimensions DecodeGif(BinaryReader binaryReader) { Dimensions returnValue; returnValue.Width = binaryReader.ReadInt16(); returnValue.Height = binaryReader.ReadInt16(); return returnValue; } static Dimensions DecodePng(BinaryReader binaryReader) { binaryReader.ReadBytes(8); Dimensions returnValue; returnValue.Width = binaryReader.ReadLittleEndianInt32(); returnValue.Height = binaryReader.ReadLittleEndianInt32(); return returnValue; } static Dimensions DecodeJfif(BinaryReader binaryReader) { while (binaryReader.ReadByte() == 0xFF) // skipp FF { byte marker = binaryReader.ReadByte(); short chunkLength = binaryReader.ReadLittleEndianInt16(); // C2: progressive (from https://stackoverflow.com/a/60667939/1377948) if (marker == 0xC0 || marker == 0xC2) { binaryReader.ReadByte(); Dimensions returnValue; returnValue.Height = binaryReader.ReadLittleEndianInt16(); returnValue.Width = binaryReader.ReadLittleEndianInt16(); return returnValue; } if (chunkLength < 0) { ushort uChunkLength = (ushort)chunkLength; binaryReader.ReadBytes(uChunkLength - 2); } else { binaryReader.ReadBytes(chunkLength - 2); } } return Dimensions.ErrorValue; } // (from https://stackoverflow.com/a/60667939/1377948) static Dimensions DecodeWebP(BinaryReader binaryReader) { binaryReader.ReadUInt32(); // Size binaryReader.ReadBytes(15); // WEBP, VP8 + more binaryReader.ReadBytes(3); // SYNC Dimensions returnValue; // 14 bits (ignore last 2 bits of the 16 bit value) for width returnValue.Width = binaryReader.ReadUInt16() & 0x3FFF; // 14 bits (ignore last 2 bits of the 16 bit value) for height returnValue.Height = binaryReader.ReadUInt16() & 0x3FFF; return returnValue; } // ================================================================================= } }