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


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), дивіться це питання ТАК: 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.


6
Погоджено, JPEG смокче. Btw - примітка для людей, які хочуть використовувати цей код у майбутньому: це справді не перевірено. Я пережив це за допомогою тонкої гребінця, і ось що я знайшов: формат BMP має ще одну (старовинну) варіацію заголовка, де розміри - 16-бітні; плюс висота може бути негативною (тоді знак опустити). Що стосується JPEG - 0xC0 - не єдиний заголовок. В основному всі 0xC0 до 0xCF, за винятком 0xC4 та 0xCC, є дійсними заголовками (їх можна легко отримати у переплетених JPG). А щоб зробити речі веселішими, висота може бути 0 і вказана пізніше в блоці 0xDC. Дивіться w3.org/Graphics/JPEG/itu-t81.pdf
Vilx-

Налаштовано вище метод DecodeJfif, щоб розширити вихідний (маркер == 0xC0) прапор, щоб також прийняти 0xC1 та 0xC2. Ці інші заголовки початку кадру SOF1 та SOF2 кодують ширину / висоту в однакових позиціях байтів. SOF2 досить поширений.
Райан Бартон

4
Стандартне попередження: Ніколи не слід писати, throw e;а просто throw;замість цього. Ваші коментарі документа XML щодо другого GetDimensionsтакож показують pathзамістьbinaryReader
Eregrith

1
Крім того, схоже, цей код не приймає JPEG, закодовані у форматі EXIF ​​/ TIFF, які виводяться багатьма цифровими камерами. Він підтримує лише JFIF.
cwills

2
System.Drawing.Image.FromStream (stream, false, false) надасть вам розміри без завантаження всього зображення, і він працює на будь-якому зображенні. Net може завантажуватися. Чому в цьому безладному та неповному рішенні є стільки результатів, не зрозуміло.
dynamhael

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
Найкраща відповідь. Швидкий, чистий та ефективний.
dynamhael

1
Ця функція ідеально підходить для Windows. але він не працює на Linux, він все одно буде читати весь файл на Linux. (.net core 2.2)
zhengchun

21

Ви спробували використовувати класи WPF Imaging? System.Windows.Media.Imaging.BitmapDecoderтощо?

Я вважаю, що певні зусилля були спрямовані на те, щоб ці кодеки читали лише підмножину файлу, щоб визначити інформацію заголовка. Це варто перевірити.


Дякую. Це здається розумним, але мій хостинг має .NET 2.
Ян Зіч,

1
Відмінна відповідь. Якщо ви можете отримати посилання на PresentationCore у своєму проекті, це шлях.
ojrac

У моїх одиничних тестах ці класи не працюють краще, ніж GDI ... все-таки потрібно ~ 32K для зчитування розмірів JPEG.
Наріман

Отже, щоб отримати розміри зображення OP, як ви використовуєте BitmapDecoder?
Чак Савідж

1
Дивіться це питання ТАК: 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. Так воно завершує відповідь, опубліковану Абасом.

Немає перевірки помилок чи будь-якої іншої перевірки, але в даний час я потрібна для обмеженого завдання, і її можна з часом легко додати. Я тестував це на деякій кількості зображень, і зазвичай це не зчитує більше, ніж 6К із зображення. Я думаю, це залежить від кількості даних 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);
            }
        }

    }
}

Коли я спробую це, ширина і висота змінюються на зворотному напрямку.
Jason Sturges

@JasonSturges Можливо, вам знадобиться врахувати тег орієнтації Exif.
Ендрю Мортон

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, хоча.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.