在不读取整个文件的情况下获取图像尺寸

Posted

技术标签:

【中文标题】在不读取整个文件的情况下获取图像尺寸【英文标题】:Getting image dimensions without reading the entire file 【发布时间】:2010-09-11 19:11:07 【问题描述】:

有没有一种廉价的方法来获取图像的尺寸(jpg、png、...)?最好,我想只使用标准类库来实现这一点(因为托管限制)。我知道读取图像标题并自己解析它应该相对容易,但似乎这样的东西应该已经存在。另外,我已经验证了以下代码可以读取整个图像(我不想要):

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);
        
    

【问题讨论】:

如果您在问题上更具体一点会有所帮助。标签告诉我 .net 和 c#,你想要标准库,但你提到的这些托管限制是什么? 如果您有权访问 System.Windows.Media.Imaging 命名空间(在 WPF 中),请参阅这个 SO 问题:***.com/questions/784734/… 【参考方案1】:

与往常一样,最好的选择是找到一个经过良好测试的库。但是,您说这很困难,所以这里有一些大部分未经测试的狡猾代码,它们应该适用于相当多的情况:

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);
        
    

希望代码相当明显。要添加新的文件格式,请将其添加到imageFormatDecoders,键是出现在给定格式的每个文件开头的“魔术位”数组,值是从流中提取大小的函数.大多数格式都很简单,唯一真正令人讨厌的是 jpeg。

【讨论】:

同意,JPEG 很烂。顺便说一句 - 给未来想要使用此代码的人的说明:这确实是未经测试的。我仔细梳理了一下,发现如下: BMP 格式有另一个(古老的)标题变体,其中尺寸为 16 位;加上高度可以是负数(然后放下符号)。至于 JPEG - 0xC0 不是唯一的标题。基本上除了 0xC4 和 0xCC 之外的所有 0xC0 到 0xCF 都是有效的标题(你可以很容易地在隔行扫描的 JPG 中得到它们)。而且,为了让事情更有趣,高度可以为 0,稍后在 0xDC 块中指定。见w3.org/Graphics/JPEG/itu-t81.pdf 标准警告:你不应该写throw e;,而只是写throw;。您在第二个 GetDimensions 上的 XML doc cmets 也显示 path 而不是 binaryReader System.Drawing.Image.FromStream(stream, false, false) 将在不加载整个图像的情况下为您提供尺寸,并且它适用于 .Net 可以加载的任何图像。为什么这个混乱和不完整的解决方案有如此多的支持是无法理解的。 @dynamichael 可能会出现您无权访问该库的情况,因此需要这些解决方案。 @dynamichael System.Drawing 现在不是标准库;它依赖于 GDI+,并且有很多 c# 平台不可用。【参考方案2】:
using (FileStream file = new FileStream(this.ImageFileName, FileMode.Open, FileAccess.Read))

    using (Image tif = Image.FromStream(stream: file, 
                                        useEmbeddedColorManagement: false,
                                        validateImageData: false))
    
        float width = tif.PhysicalDimension.Width;
        float height = tif.PhysicalDimension.Height;
        float hresolution = tif.HorizontalResolution;
        float vresolution = tif.VerticalResolution;
     

validateImageData 设置为false 可防止GDI+ 对图像数据执行昂贵的分析,从而大大减少加载时间。 This question 对这个主题有更多的了解。

【讨论】:

我将您的解决方案用作最后一个资源,并与上面的 ICR 解决方案混合。 JPEG有问题,并解决了这个问题。 我最近在一个项目中尝试了这个,我必须查询 2000+ 图像的大小(主要是 jpg 和 png,大小非常混合),它确实比使用 @987654325 的传统方式快得多@. 最佳答案。快速、干净、有效。 这个功能在windows上很完美。但它不能在 linux 上运行,它仍然会在 linux 上读取整个文件。 (.net 核心 2.2)【参考方案3】:

您是否尝试过使用 WPF Imaging 类? System.Windows.Media.Imaging.BitmapDecoder等?

我相信一些努力是为了确保这些编解码器只读取文件的一个子集以确定标题信息。值得一试。

【讨论】:

谢谢。这似乎是合理的,但我的主机有 .NET 2。 优秀的答案。如果您可以在您的项目中获得对 PresentationCore 的引用,那么这就是您要走的路。 在我的单元测试中,这些类的性能并不比 GDI 好...仍然需要大约 32K 才能读取 JPEG 尺寸。 那么要得到OP的图片尺寸,BitmapDecoder怎么用? 查看这个 SO 问题:***.com/questions/784734/…【参考方案4】:

几个月前我一直在寻找类似的东西。我想了解 GIF 图片的类型、版本、高度和宽度,但在网上找不到任何有用的信息。

幸运的是,对于 GIF,所有必需的信息都在前 10 个字节中:

Type: Bytes 0-2
Version: Bytes 3-5
Height: Bytes 6-7
Width: Bytes 8-9

PNG 稍微复杂一些(宽度和高度各 4 字节):

Width: Bytes 16-19
Height: Bytes 20-23

如上所述,wotsit 是一个很好的网站,可以提供有关图像和数据格式的详细规范,尽管pnglib 的 PNG 规范要详细得多。但是,我认为PNG 和GIF 格式的***条目是最好的起点。

这是我检查 GIF 的原始代码,我还为 PNG 拼凑了一些东西:

using System;
using System.IO;
using System.Text;

public class ImageSizeTest

    public static void Main()
    
        byte[] bytes = new byte[10];

        string gifFile = @"D:\Personal\Images&Pics\iProduct.gif";
        using (FileStream fs = File.OpenRead(gifFile))
        
            fs.Read(bytes, 0, 10); // type (3 bytes), version (3 bytes), width (2 bytes), height (2 bytes)
        
        displayGifInfo(bytes);

        string pngFile = @"D:\Personal\Images&Pics\WaveletsGamma.png";
        using (FileStream fs = File.OpenRead(pngFile))
        
            fs.Seek(16, SeekOrigin.Begin); // jump to the 16th byte where width and height information is stored
            fs.Read(bytes, 0, 8); // width (4 bytes), height (4 bytes)
        
        displayPngInfo(bytes);
    

    public static void displayGifInfo(byte[] bytes)
    
        string type = Encoding.ASCII.GetString(bytes, 0, 3);
        string version = Encoding.ASCII.GetString(bytes, 3, 3);

        int width = bytes[6] | bytes[7] << 8; // byte 6 and 7 contain the width but in network byte order so byte 7 has to be left-shifted 8 places and bit-masked to byte 6
        int height = bytes[8] | bytes[9] << 8; // same for height

        Console.WriteLine("GIF\nType: 0\nVersion: 1\nWidth: 2\nHeight: 3\n", type, version, width, height);
    

    public static void displayPngInfo(byte[] bytes)
    
        int width = 0, height = 0;

        for (int i = 0; i <= 3; i++)
        
            width = bytes[i] | width << 8;
            height = bytes[i + 4] | height << 8;            
        

        Console.WriteLine("PNG\nWidth: 0\nHeight: 1\n", width, height);  
    

【讨论】:

【参考方案5】:

根据到目前为止的答案和一些额外的搜索,在 .NET 2 类库中似乎没有它的功能。所以我决定自己写。这是它的一个非常粗略的版本。目前,我只需要它来处理 JPG。所以它完成了阿巴斯发布的答案。

没有错误检查或任何其他验证,但我目前需要它来完成一项有限的任务,并且最终可以轻松添加。我在一些图像上对其进行了测试,它通常不会从图像中读取超过 6K 的内容。我想这取决于 EXIF 数据的数量。

using System;
using System.IO;

namespace Test


    class Program
    

        static bool GetJpegDimension(
            string fileName,
            out int width,
            out int height)
        

            width = height = 0;
            bool found = false;
            bool eof = false;

            FileStream stream = new FileStream(
                fileName,
                FileMode.Open,
                FileAccess.Read);

            BinaryReader reader = new BinaryReader(stream);

            while (!found || eof)
            

                // read 0xFF and the type
                reader.ReadByte();
                byte type = reader.ReadByte();

                // get length
                int len = 0;
                switch (type)
                
                    // start and end of the image
                    case 0xD8: 
                    case 0xD9: 
                        len = 0;
                        break;

                    // restart interval
                    case 0xDD: 
                        len = 2;
                        break;

                    // the next two bytes is the length
                    default: 
                        int lenHi = reader.ReadByte();
                        int lenLo = reader.ReadByte();
                        len = (lenHi << 8 | lenLo) - 2;
                        break;
                

                // EOF?
                if (type == 0xD9)
                    eof = true;

                // process the data
                if (len > 0)
                

                    // read the data
                    byte[] data = reader.ReadBytes(len);

                    // this is what we are looking for
                    if (type == 0xC0)
                    
                        width = data[1] << 8 | data[2];
                        height = data[3] << 8 | data[4];
                        found = true;
                    

                

            

            reader.Close();
            stream.Close();

            return found;

        

        static void Main(string[] args)
        
            foreach (string file in Directory.GetFiles(args[0]))
            
                int w, h;
                GetJpegDimension(file, out w, out h);
                System.Console.WriteLine(file + ": " + w + " x " + h);
            
        

    

【讨论】:

当我尝试这个时,宽度和高度是相反的。 @JasonSturges 您可能需要考虑 Exif Orientation 标签。【参考方案6】:

更新了 ICR 的答案以支持渐进式 jPegs 和 WebP :)

internal static class ImageHelper

    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 ,
         new byte[]  0x52, 0x49, 0x46, 0x46 , DecodeWebP ,
    ;

    /// <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 = 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(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 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 || marker == 0xc2) // c2: progressive
            
                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);
    

    private static Size DecodeWebP(BinaryReader binaryReader)
    
        binaryReader.ReadUInt32(); // Size
        binaryReader.ReadBytes(15); // WEBP, VP8 + more
        binaryReader.ReadBytes(3); // SYNC

        var width = binaryReader.ReadUInt16() & 0b00_11111111111111; // 14 bits width
        var height = binaryReader.ReadUInt16() & 0b00_11111111111111; // 14 bits height

        return new Size(width, height);
    


【讨论】:

感谢您开始使用 webp。 DecodeWebP 仅适用于 Webp 有损图像 - developers.google.com/speed/webp/gallery1 png 的内部结构都是大端的(我相信也是 gif)。并且BinaryReader 总是读取little-endian,不管系统字节序如何,所以现有的辅助函数是没用的。 我似乎无法使用此示例或其他示例尝试解码任何 jpeg;读取 0xff 后,下一个字节不是 C0 或 C2,因此它只是直接跳出或尝试读取超出流末尾的失败【参考方案7】:

我为 PNG 文件做了这个

  var buff = new byte[32];
        using (var d =  File.OpenRead(file))
                    
            d.Read(buff, 0, 32);
        
        const int wOff = 16;
        const int hOff = 20;            
        var Widht =BitConverter.ToInt32(new[] buff[wOff + 3], buff[wOff + 2], buff[wOff + 1], buff[wOff + 0],,0);
        var Height =BitConverter.ToInt32(new[] buff[hOff + 3], buff[hOff + 2], buff[hOff + 1], buff[hOff + 0],,0);

【讨论】:

【参考方案8】:

是的,您绝对可以这样做,代码取决于文件格式。我在一家图像供应商 (Atalasoft) 工作,我们的产品为每个编解码器提供了一个 GetImageInfo() 方法,它可以最少地找出尺寸和其他一些易于获取的数据。

如果你想自己动手,我建议从wotsit.org 开始,它对几乎所有图像格式都有详细的规范,你会看到如何识别文件以及在哪里可以找到其中的信息。

如果您习惯使用 C,那么免费的 jpeglib 也可用于获取此信息。我敢打赌,您可以使用 .NET 库来做到这一点,但我不知道该怎么做。

【讨论】:

可以安全地假设使用new AtalaImage(filepath).Width 会做类似的事情吗? 或只是Atalasoft.Imaging.Codec.RegisteredDecoders.GetImageInfo ( fullPath ) .Size 第一个 (AtalaImage) 读取整个图像 -- 第二个 (GetImageInfo) 读取最小元数据以获取图像信息对象的元素。【参考方案9】:

这将取决于文件格式。通常他们会在文件的早期字节中说明它。而且,通常,一个好的图像读取实现会考虑到这一点。不过,我无法为您指出 .NET 的一个。

【讨论】:

以上是关于在不读取整个文件的情况下获取图像尺寸的主要内容,如果未能解决你的问题,请参考以下文章

有没有办法在不读取整个文件的情况下推断文件是啥图像格式?

有没有办法在不读取整个文件的情况下推断文件是啥图像格式?

有没有办法在不读取整个文件的情况下推断文件是啥图像格式?

有没有办法在不读取整个文件的情况下推断文件是啥图像格式?

在不读取文件的情况下使用 parquet 文件统计信息

在不使用 java.util.Properties 的情况下读取文件并获取 key=value