Получение размеров изображения без чтения всего файла

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/…
Чарли

Ответы:

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.

ICR
источник
6
Согласен, JPEG - отстой. Кстати, примечание для людей, которые хотят использовать этот код в будущем: это действительно не проверено. Я прошел через это с помощью тонкой гребенки и вот что обнаружил: формат BMP имеет еще один (древний) вариант заголовка, размер которого составляет 16 бит; плюс высота может быть отрицательной (тогда опустите знак). Что касается JPEG - 0xC0 - не единственный заголовок. В основном все от 0xC0 до 0xCF, кроме 0xC4 и 0xCC, являются допустимыми заголовками (вы можете легко получить их в чересстрочных файлах JPG). И, чтобы было веселее, высота может быть равна 0 и указана позже в блоке 0xDC. См w3.org/Graphics/JPEG/itu-t81.pdf
Vilx-
Изменен метод DecodeJfif выше, чтобы развернуть исходную (marker == 0xC0) проверку, чтобы она также принимала 0xC1 и 0xC2. Эти другие заголовки начала кадра SOF1 и SOF2 кодируют ширину / высоту в одних и тех же позициях байтов. SOF2 довольно распространен.
Райан Бартон
4
Стандартное предупреждение: никогда не пишите, throw e;а просто throw;вместо этого. В вашем XML-документе комментарии ко второму изображению GetDimensionsтакже отображаются pathвместоbinaryReader
Эрегрит
1
Кроме того, похоже, этот код не принимает файлы JPEG, закодированные в формате EXIF ​​/ TIFF, который выводится многими цифровыми камерами. Он поддерживает только JFIF.
cwills 01
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
2
Недавно я попробовал это в проекте, где мне нужно было запросить размер более 2000 изображений (в основном jpg и png, очень разные размеры), и это действительно было намного быстрее, чем при использовании традиционного способа new Bitmap().
AeonOfTime
1
Лучший ответ. Быстро, чисто и эффективно.
dynamichael
1
Эта функция идеально подходит для окон. но он не работает в Linux, он все равно будет читать весь файл в Linux. (.net core 2.2)
zhengchun
21

Вы пробовали использовать классы WPF Imaging? System.Windows.Media.Imaging.BitmapDecoder, и т.д.?

Я считаю, что некоторые усилия были предприняты для того, чтобы эти кодеки считывали только подмножество файла, чтобы определить информацию заголовка. Стоит проверить.

Франк Крюгер
источник
Спасибо. Это кажется разумным, но на моем хостинге есть .NET 2.
Ян Зич
1
Отличный ответ. Если вы можете получить ссылку на PresentationCore в своем проекте, это правильный путь.
ojrac
В моих модульных тестах эти классы работают не лучше, чем GDI ... по-прежнему требуется ~ 32 КБ для чтения размеров JPEG.
Нариман
Итак, чтобы получить размеры изображения OP, как вы используете BitmapDecoder?
Чак Сэвидж
1
См. Этот вопрос SO: stackoverflow.com/questions/784734/…
Чарли
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 - хороший сайт для подробных спецификаций форматов изображений и данных, хотя спецификации PNG в pnglib гораздо более детализированы. Однако я думаю, что лучше всего начать с статьи в Википедии о форматах 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);  
    }
}
Аббаса
источник
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.
Эндрю Мортон
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
1
Первый (AtalaImage) считывает все изображение - второй (GetImageInfo) считывает минимальные метаданные для получения элементов информационного объекта изображения.
Лу Франко
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.

Кевин Коннер
источник