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


104

是否有一种便宜的方法来获取图像的尺寸(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#,并且您想要标准库,但是您提到的这些托管限制是什么?
wnoise

如果您有权访问System.Windows.Media.Imaging命名空间(在WPF中),请参见以下SO问题:stackoverflow.com/questions/784734/…–
Charlie

Answers:


106

与往常一样,最好的选择是找到一个经过良好测试的库。但是,您说这很困难,因此下面是一些易于测试的代码,它们在很多情况下都适用:

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。


6
同意,JPEG很烂。顺便说一句-给以后想要使用此代码的人的注释:确实未经测试。我已经用一把细梳仔细地研究了一下,这就是我发现的:BMP格式还有另一个(古老的)报头变体,其尺寸为16位。加高可以为负(然后放下符号)。至于JPEG-0xC0不是唯一的标头。基本上,除了0xC4和0xCC之外,所有0xC0至0xCF都是有效的标头(您可以在隔行JPG中轻松获取它们)。并且,为了使事情变得更有趣,可以将height设置为0,然后在0xDC块中指定它。参见w3.org/Graphics/JPEG/itu-t81.pdf
Vilx-2011年

调整了上面的DecodeJfif方法以扩展原始(标记== 0xC0)检查,以同时接受0xC1和0xC2。这些其他帧开始标头SOF1和SOF2在相同的字节位置编码宽度/高度。SOF2很常见。
Ryan Barton

4
标准警告:您永远不要写,throw e;而只是写throw;。第二你的XML文档注释GetDimensions也显示path代替binaryReader
Eregrith

1
同样,此代码似乎不接受许多数码相机输出的以EXIF / TIFF格式编码的JPEG。它仅支持JFIF。
遗嘱'18

2
System.Drawing.Image.FromStream(stream,false,false)将为您提供尺寸而无需加载整个图像,并且它可以在.Net可以加载的任何图像上使用。为什么这个凌乱和不完整的解决方案会有如此多的反对,这是无法理解的。
dynamichael

25
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 +对图像数据进行昂贵的分析,从而大大减少了加载时间。 这个问题使这个问题更加清晰。


1
我将您的解决方案用作上面与ICR解决方案混合的最后资源。JPEG出现问题,并以此解决。
Zorkind 2013年

2
我最近在一个项目中尝试了此操作,该项目中我不得不查询2000多个图像的大小(大多数是jpg和png,非常混合的大小),并且确实比使用传统方式快得多new Bitmap()
AeonOfTime

1
最佳答案。快速,干净,有效。
dynamichael

1
此功能在Windows上非常完美。但它在Linux上不起作用,它将仍然在Linux上读取整个文件。(.net core 2.2)
zhengchun

21

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

我相信需要做出一些努力来确保这些编解码器仅读取文件的一个子集,以便确定头信息。值得一查。


谢谢。这似乎是合理的,但我的主机具有.NET 2
扬Zich

1
极好的答案。如果您可以在项目中获得对PresentationCore的引用,那么这就是方法。
ojrac 2010年

在我的单元测试中,这些类的性能没有比GDI好。...仍然需要约32K来读取JPEG尺寸。
Nariman

因此,要获取OP的图像尺寸,如何使用BitmapDecoder?
Chuck Savage 2013年


12

几个月前,我一直在寻找类似的东西。我想阅读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规范更为详细。但是,我认为Wikipedia上的PNGGIF格式条目是最好的起点。

这是我检查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);  
    }
}

8

根据到目前为止的答案和其他一些搜索,似乎在.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标记。
Andrew Morton

3

我这样做是为了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);

1

是的,您绝对可以这样做,并且代码取决于文件格式。我为影像供应商工作(Atalasoft)工作,我们的产品为每个编解码器都提供了一个GetImageInfo(),该编解码器会尽力找出尺寸和其他易于获取的数据。

如果您想自己动手,建议从wotsit.org开始,该文件具有适用于几乎所有图像格式的详细规格,您将看到如何识别文件以及在何处可以找到信息。

如果您喜欢使用C,那么免费的jpeglib也可以用于获取此信息。我敢打赌,您可以使用.NET库执行此操作,但是我不知道如何操作。


是否可以安全地假设使用new AtalaImage(filepath).Width类似的东西?
drzaus 2014年


1
第一个(AtalaImage)读取整个图像-第二个(GetImageInfo)读取最少的元数据以获取图像信息对象的元素。
Lou Franco 2014年

0

更新了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);
    }

}

-1

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.