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

118

有没有一种便宜的方法来获取图像(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问题:https://dev59.com/nXRA5IYBdhLWcg3w6SRH?lq=1 - Charlie
9个回答

117

像往常一样,您最好找到一个经过充分测试的库。然而,您说这很困难,因此以下是一些不太可靠且未经充分测试的代码,它应该适用于相当数量的情况:

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 中,其键是给定格式的每个文件开头出现的“magic bits”的数组,值是从流中提取大小的函数。 大多数格式都足够简单,唯一真正棘手的是jpeg。


8
同意,JPEG 格式很糟糕。顺便提醒那些想在将来使用此代码的人:这确实没有经过测试。我已经仔细检查了一遍,以下是我的发现:BMP 格式有另一种(古老的)标题变体,其中尺寸为 16 位;此外高度可以为负数(然后去掉符号)。至于 JPEG - 0xC0 不是唯一的头文件。基本上所有 0xC0 到 0xCF 中除了 0xC4 和 0xCC 的都是有效头文件(你可以很容易地在交错的 JPG 文件中获取它们)。还有,为了让事情更有趣,高度可以为零,并在 0xDC 块中稍后指定。请参阅 http://www.w3.org/Graphics/JPEG/itu-t81.pdf。 - Vilx-
6
标准警告:您不应该写 throw e; 而是应该直接写 throw;。您在第二个 GetDimensions 的 XML 文档注释中也显示了 path 而非 binaryReader - Eregrith
4
System.Drawing.Image.FromStream(stream, false, false)会给你图片的尺寸而不必加载整个图片,而且它适用于任何.Net可以加载的图片。为什么这个混乱而不完整的解决方案有这么多的赞是无法理解的。 - dynamichael
2
@dynamichael,有时候你可能无法访问那个库,所以这些解决方案是必要的。 - MattyMatt
4
@dynamichael System.Drawing现在已经不是标准库了;它依赖于GDI+,并且有很多C#平台上不可用。 - Nyerguds
显示剩余7条评论

37
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
2
最近我在一个项目中尝试了这个方法,需要查询2000多张图片的大小(主要是jpg和png格式,大小不一),结果比传统的使用new Bitmap()的方式快得多。 - AeonOfTime
2
最佳答案。快速,简洁,有效。 - dynamichael
1
这个函数在Windows上完美运行。但是在Linux上它不起作用,它仍然会读取整个文件。(.net core 2.2) - zhengchun

21

你尝试过使用WPF图像处理类吗?例如System.Windows.Media.Imaging.BitmapDecoder等。

我相信有些工作已经投入到确保这些编解码器只读取文件的子集以确定标头信息。值得检查一下。


谢谢。看起来很合理,但我的托管服务只支持.NET 2。 - Jan Zich
1
非常好的答案。如果您可以在项目中获取PresentationCore的引用,那么这就是正确的方法。 - ojrac
在我的单元测试中,这些类的性能并不比GDI更好...仍然需要约32K才能读取JPEG的尺寸。 - Nariman
那么,要获取OP的图像尺寸,如何使用BitmapDecoder? - Chuck Savage
1
请参见此SO问题:https://dev59.com/nXRA5IYBdhLWcg3w6SRH?lq=1 - Charlie

14

几个月前,我在寻找类似的东西。 我想读取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规范更为详细。然而,我认为维基百科关于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);  
    }
}

9
根据目前的回答和一些额外的搜索结果,似乎在.NET 2类库中没有此功能。因此,我决定自己编写。这是一个非常初步的版本。目前,我只需要用于JPG文件。因此,它完善了Abbas发布的答案。
没有错误检查或其他验证,但目前我只需要它完成一个有限的任务,并且最终可以轻松添加。我测试了一些图像,通常不会从图像中读取超过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);
            }
        }

    }
}

1
当我尝试这个时,宽度和高度被颠倒了。 - Jason Sturges
@JasonSturges 你可能需要考虑Exif方向标签。 - Andrew Morton

6

已更新ICR的答案,支持渐进式JPEG和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有损图像-https://developers.google.com/speed/webp/gallery1 - Markus
PNG的内部都是大端字节序(我相信GIF也是如此)。而且,无论系统字节序如何,BinaryReader始终读取小端字节序,因此现有的辅助函数是无用的。 - Nyerguds
我似乎无法使用此示例或其他示例解码任何JPEG图像;在读取0xff之后,下一个字节不是C0或C2,因此它会直接跳出或尝试读取超出流的末尾而失败。 - Samuel Johnson
@SamuelJohnson 可能的原因:很可能你的JPEG只有SOF1和SOF2段(需要在标记检查中0xC1和0xC2相等 - 编辑你的代码,这里缺失了)。可能是你做了一些更改,或者你的副本不完整,这可能导致逻辑过早退出循环。或者,你的系统是bigEndian(BE):这里的littleEndian(LE)函数不是读取LE数据,而是将jpeg中的BE数据转换为LE,并且仅在像Windows PC这样的LE系统上工作。在BE系统上,你很可能永远找不到SOF段并超出文件长度。 - Karl Stephen
@Nyerguds 您是正确的,但问题在于该代码假定您正在使用小端系统(它可能会在大端系统上失败,我没有检查)。每个函数的含义,比如ReadLittleEndianInt16,并不是“我要读取一个小端二进制数据并在内存中获取值”,而是“我假设这些数据是大端的,并且无论我所在的系统的字节顺序如何,我都会将字节交换为小端配置”。 - Karl Stephen
@KarlStephen 那又怎样?当处理具有特定字节顺序的文件规范时,系统的字节顺序完全无关紧要。使用BinaryReader从png头部读取值,并且结果将在任何情况下始终被字节交换损坏,不管它所运行的系统如何。在一个不同字节顺序的系统上读取它并不会神奇地改变png文件中的原始字节,而且由于BinaryReader明确规定在不同字节顺序的系统上不改变其行为,那一侧也不会发生变化。 - Nyerguds

5
我针对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);

它给出了错误的结果。 - Cylian
@Cylian 你是用什么文件类型尝试的?PNG? - undefined

1

是的,你绝对可以做到这个,代码取决于文件格式。我为一家图像供应商(Atalasoft)工作,我们的产品为每个编解码器提供了一个GetImageInfo()函数,该函数最少执行并获取尺寸和其他易于获取的数据。

如果您想自己开发,请从 wotsit.org 开始,该网站有几乎所有图像格式的详细规范,您将看到如何识别文件以及可以在其中找到信息的位置。

如果您熟悉使用 C 语言,那么也可以使用免费的jpeglib来获取此信息。我敢打赌,使用.NET库也可以实现此功能,但我不知道如何做。


使用 new AtalaImage(filepath).Width 可以做类似的事情,这样做是否安全呢? - drzaus
Atalasoft.Imaging.Codec.RegisteredDecoders.GetImageInfo(fullPath).Size - drzaus
1
第一个函数(AtalaImage)读取整个图像 -- 第二个函数(GetImageInfo)仅读取最小的元数据以获取图像信息对象的元素。 - Lou Franco

-2

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接