Mendapatkan dimensi gambar tanpa membaca seluruh file

104

Adakah cara yang murah untuk mendapatkan dimensi sebuah gambar (jpg, png, ...)? Lebih disukai, saya ingin mencapai ini hanya dengan menggunakan perpustakaan kelas standar (karena pembatasan hosting). Saya tahu bahwa seharusnya relatif mudah untuk membaca header gambar dan menguraikannya sendiri, tetapi tampaknya sesuatu seperti ini seharusnya sudah ada. Juga, saya telah memverifikasi bahwa potongan kode berikut membaca seluruh gambar (yang tidak saya inginkan):

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);
        }
    }
}
Jan Zich
sumber
Akan membantu jika Anda sedikit lebih spesifik dalam pertanyaan yang tepat. Tag telah memberi tahu saya .net dan c #, dan Anda menginginkan pustaka standar, tetapi apa batasan hosting yang Anda sebutkan ini?
wnoise
Jika Anda memiliki akses ke namespace System.Windows.Media.Imaging (di WPF), lihat pertanyaan SO ini: stackoverflow.com/questions/784734/…
Charlie

Jawaban:

106

Taruhan terbaik Anda seperti biasa adalah menemukan perpustakaan yang teruji dengan baik. Namun, Anda mengatakan itu sulit, jadi berikut adalah beberapa kode cerdik yang sebagian besar belum teruji yang seharusnya berfungsi untuk sejumlah kasus:

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

Semoga kodenya cukup jelas. Untuk menambahkan format file baru, Anda menambahkannya ke imageFormatDecodersdengan kunci berupa array "bit ajaib" yang muncul di awal setiap file dari format yang diberikan dan nilainya menjadi fungsi yang mengekstrak ukuran dari aliran. Sebagian besar format cukup sederhana, satu-satunya hal yang buruk adalah jpeg.

ICR
sumber
6
Setuju, JPEG menyebalkan. Btw - catatan untuk orang-orang yang ingin menggunakan kode ini di masa mendatang: ini memang belum teruji. Saya telah melewatinya dengan sisir yang bagus, dan inilah yang saya temukan: Format BMP memiliki variasi tajuk (kuno) lain di mana dimensinya 16-bit; tinggi plus bisa negatif (hilangkan tanda itu). Adapun JPEG - 0xC0 bukan satu-satunya header. Pada dasarnya semua 0xC0 hingga 0xCF kecuali 0xC4 dan 0xCC adalah header yang valid (Anda dapat dengan mudah mendapatkannya dalam JPG yang saling bertautan). Dan, untuk membuat segalanya lebih menyenangkan, ketinggian bisa menjadi 0 dan ditentukan nanti dalam blok 0xDC. Lihat w3.org/Graphics/JPEG/itu-t81.pdf
Vilx-
Tweak metode DecodeJfif di atas untuk memperluas cek asli (marker == 0xC0) untuk menerima 0xC1 dan 0xC2 juga. Header start-of-frame lainnya SOF1 dan SOF2 ini menyandikan lebar / tinggi dalam posisi byte yang sama. SOF2 cukup umum.
Ryan Barton
4
Peringatan standar: Anda tidak boleh menulis, throw e;tetapi cukup throw;. Komentar dokumen XML Anda pada yang kedua GetDimensionsjuga ditampilkan pathalih-alihbinaryReader
Eregrith
1
Juga, sepertinya kode ini tidak menerima JPEG yang dikodekan dalam format EXIF ​​/ TIFF yang dihasilkan oleh banyak kamera digital. Ini hanya mendukung JFIF.
cwills
2
System.Drawing.Image.FromStream (stream, false, false) akan memberi Anda dimensi tanpa memuat seluruh gambar, dan berfungsi pada gambar apa pun .Net dapat memuat. Mengapa solusi yang berantakan dan tidak lengkap ini memiliki begitu banyak suara positif adalah di luar pemahaman.
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;
     }
}

yang validateImageDataditetapkan untuk falsemencegah GDI + dari melakukan analisis mahal dari data gambar, sehingga sangat mengurangi waktu buka. Pertanyaan ini menjelaskan lebih banyak tentang subjek ini.

Koray
sumber
1
Saya menggunakan solusi Anda sebagai sumber daya terakhir yang dicampur dengan solusi ICR di atas. Punya masalah dengan JPEG, dan diselesaikan dengan ini.
Zorkind
2
Saya baru-baru ini mencoba ini dalam sebuah proyek di mana saya harus menanyakan ukuran 2000+ gambar (kebanyakan jpg dan png, ukuran sangat beragam), dan memang jauh lebih cepat daripada cara penggunaan tradisional new Bitmap().
AeonOfTime
1
Jawaban Terbaik. Cepat, bersih, dan efektif.
dynamichael
1
Fungsi ini sempurna di windows. tetapi tidak berfungsi di linux, ia masih akan membaca seluruh file di linux. (.net core 2.2)
zhengchun
21

Sudahkah Anda mencoba menggunakan kelas Pencitraan WPF? System.Windows.Media.Imaging.BitmapDecoder, dll.?

Saya yakin beberapa upaya dilakukan untuk memastikan codec tersebut hanya membaca sebagian file untuk menentukan informasi header. Ini layak untuk dicek.

Frank Krueger
sumber
Terima kasih. Tampaknya masuk akal, tetapi hosting saya memiliki .NET 2.
Jan Zich
1
Jawaban yang sangat bagus. Jika Anda bisa mendapatkan referensi ke PresentationCore dalam proyek Anda, inilah caranya.
ojrac
Dalam pengujian unit saya, kelas-kelas ini tidak berkinerja lebih baik dari GDI ... masih membutuhkan ~ 32K untuk membaca dimensi JPEG.
Nariman
Jadi untuk mendapatkan dimensi gambar OP, bagaimana Anda menggunakan BitmapDecoder?
Chuck Savage
1
Lihat pertanyaan SO ini: stackoverflow.com/questions/784734/…
Charlie
12

Saya mencari sesuatu yang serupa beberapa bulan sebelumnya. Saya ingin membaca jenis, versi, tinggi dan lebar gambar GIF tetapi tidak dapat menemukan sesuatu yang berguna secara online.

Untungnya dalam kasus GIF, semua informasi yang diperlukan ada di 10 byte pertama:

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

PNG sedikit lebih kompleks (lebar dan tinggi masing-masing 4 byte):

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

Seperti disebutkan di atas, wotsit adalah situs yang bagus untuk spesifikasi rinci tentang gambar dan format data meskipun spesifikasi PNG di pnglib jauh lebih rinci. Namun, menurut saya entri Wikipedia tentang format PNG dan GIF adalah tempat terbaik untuk memulai.

Inilah kode asli saya untuk memeriksa GIF, saya juga telah menyatukan sesuatu untuk 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);  
    }
}
Abbas
sumber
8

Berdasarkan jawaban sejauh ini dan beberapa pencarian tambahan, tampaknya di perpustakaan kelas .NET 2 tidak ada fungsi untuk itu. Jadi saya memutuskan untuk menulis sendiri. Ini adalah versi yang sangat kasar. Saat ini, saya hanya membutuhkannya untuk JPG. Jadi itu melengkapi jawaban yang diposting oleh Abbas.

Tidak ada pemeriksaan kesalahan atau verifikasi lainnya, tetapi saat ini saya membutuhkannya untuk tugas terbatas, dan pada akhirnya dapat dengan mudah ditambahkan. Saya mengujinya pada beberapa gambar, dan biasanya tidak membaca lebih dari 6K dari sebuah gambar. Saya kira itu tergantung pada jumlah data 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);
            }
        }

    }
}
Jan Zich
sumber
Lebar dan tinggi dibalik saat saya mencoba ini.
Jason Sturges
@JasonSturges Anda mungkin perlu mempertimbangkan tag Orientasi Exif.
Andrew Morton
3

Saya melakukan ini untuk file 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);
Danny D
sumber
1

Ya, Anda benar-benar dapat melakukan ini dan kodenya tergantung pada format file. Saya bekerja untuk vendor pencitraan ( Atalasoft ), dan produk kami menyediakan GetImageInfo () untuk setiap codec yang melakukan minimum untuk mengetahui dimensi dan beberapa data lain yang mudah diperoleh.

Jika Anda ingin menggulung sendiri, saya sarankan memulai dengan wotsit.org , yang memiliki spesifikasi terperinci untuk hampir semua format gambar dan Anda akan melihat bagaimana mengidentifikasi file dan juga di mana informasi di dalamnya dapat ditemukan.

Jika Anda merasa nyaman bekerja dengan C, maka jpeglib gratis dapat digunakan untuk mendapatkan informasi ini juga. Saya berani bertaruh bahwa Anda dapat melakukan ini dengan pustaka .NET, tetapi saya tidak tahu caranya.

Lou Franco
sumber
apakah aman untuk mengasumsikan bahwa menggunakan new AtalaImage(filepath).Widthmelakukan sesuatu yang serupa?
drzaus
1
Yang pertama (AtalaImage) membaca seluruh gambar - yang kedua (GetImageInfo) membaca metadata minimal untuk mendapatkan elemen objek info gambar.
Lou Franco
0

Jawaban ICR yang diperbarui untuk mendukung jPegs & WebP progresif juga :)

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

}
bang
sumber
-1

Ini akan tergantung pada format file. Biasanya mereka akan menyatakannya dalam byte awal file. Dan, biasanya, implementasi pembacaan gambar yang baik akan memperhitungkannya. Saya tidak bisa mengarahkan Anda ke satu untuk .NET sekalipun.

Kevin Conner
sumber