Abrufen von Bildabmessungen ohne Lesen der gesamten Datei

104

Gibt es eine günstige Möglichkeit, die Abmessungen eines Bildes zu ermitteln (jpg, png, ...)? Vorzugsweise möchte ich dies nur mit der Standardklassenbibliothek erreichen (aufgrund von Hosting-Einschränkungen). Ich weiß, dass es relativ einfach sein sollte, den Bildheader zu lesen und selbst zu analysieren, aber es scheint, dass so etwas bereits vorhanden sein sollte. Außerdem habe ich überprüft, dass der folgende Code das gesamte Bild liest (was ich nicht möchte):

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
quelle
Es wäre hilfreich, wenn Sie in der eigentlichen Frage etwas genauer wären. Die Tags haben mir .net und c # mitgeteilt, und Sie möchten eine Standardbibliothek, aber welche Hosting-Einschränkungen erwähnen Sie?
wnoise
Wenn Sie Zugriff auf den System.Windows.Media.Imaging-Namespace (in WPF) haben, lesen
Charlie

Antworten:

106

Wie immer ist es am besten, eine gut getestete Bibliothek zu finden. Sie sagten jedoch, dass dies schwierig ist. Hier ist ein zwielichtiger, weitgehend ungetesteter Code, der für eine ganze Reihe von Fällen funktionieren sollte:

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

Hoffentlich ist der Code ziemlich offensichtlich. Um ein neues Dateiformat hinzuzufügen, fügen Sie es hinzu, imageFormatDecoderswobei der Schlüssel ein Array der "magischen Bits" ist, die am Anfang jeder Datei des angegebenen Formats erscheinen, und der Wert eine Funktion ist, die die Größe aus dem Stream extrahiert. Die meisten Formate sind einfach genug, der einzige echte Stinker ist JPEG.

ICR
quelle
6
Einverstanden, JPEG ist scheiße. Übrigens - ein Hinweis für die Leute, die diesen Code in Zukunft verwenden möchten: Dies ist in der Tat ungetestet. Ich habe es mit einem feinen Kamm durchgearbeitet, und hier ist, was ich gefunden habe: Das BMP-Format hat eine andere (alte) Header-Variante, bei der die Abmessungen 16-Bit sind; Plushöhe kann negativ sein (dann das Vorzeichen fallen lassen). JPEG - 0xC0 ist nicht der einzige Header. Grundsätzlich sind alle 0xC0 bis 0xCF mit Ausnahme von 0xC4 und 0xCC gültige Header (Sie können sie problemlos in Interlaced-JPGs abrufen). Und damit die Dinge mehr Spaß machen, kann die Höhe 0 sein und später in einem 0xDC-Block angegeben werden. Siehe w3.org/Graphics/JPEG/itu-t81.pdf
22.
Die DecodeJfif-Methode oben wurde optimiert, um die ursprüngliche Prüfung (marker == 0xC0) so zu erweitern, dass auch 0xC1 und 0xC2 akzeptiert werden. Diese anderen Start-of-Frame-Header SOF1 und SOF2 codieren Breite / Höhe an denselben Bytepositionen. SOF2 ist ziemlich häufig.
Ryan Barton
4
Standardwarnung: Sie sollten niemals schreiben, throw e;sondern einfach throw;stattdessen. Ihre XML-Dokumentkommentare zum zweiten GetDimensionszeigen auch pathanstelle vonbinaryReader
Eregrith
1
Außerdem akzeptiert dieser Code anscheinend keine JPEGs, die im EXIF ​​/ TIFF-Format codiert sind, das von vielen Digitalkameras ausgegeben wird. Es unterstützt nur JFIF.
Cwills
2
System.Drawing.Image.FromStream (stream, false, false) gibt Ihnen die Abmessungen an, ohne das gesamte Bild zu laden, und funktioniert auf jedem Bild, das .Net laden kann. Warum diese unordentliche und unvollständige Lösung so viele positive Stimmen hat, ist unverständlich.
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;
     }
}

Die validateImageDataEinstellung falseverhindert , dass GDI + eine kostspielige Analyse der Bilddaten durchführt, wodurch die Ladezeit erheblich verkürzt wird. Diese Frage wirft mehr Licht auf das Thema.

Koray
quelle
1
Ich habe Ihre Lösung als letzte Ressource verwendet, die mit der oben genannten ICR-Lösung gemischt wurde. Hatte Probleme mit JPEG und löste damit.
Zorkind
2
Ich habe dies kürzlich in einem Projekt versucht, in dem ich die Größe von mehr als 2000 Bildern abfragen musste (hauptsächlich JPG und PNG, sehr gemischte Größen), und es war in der Tat viel schneller als die herkömmliche Verwendung new Bitmap().
AeonOfTime
1
Beste Antwort. Schnell, sauber und effektiv.
Dynamichael
1
Diese Funktion ist perfekt für Windows. aber es funktioniert nicht unter Linux, es liest immer noch die gesamte Datei unter Linux. (.net Kern 2.2)
Zhengchun
21

Haben Sie versucht, die WPF Imaging-Klassen zu verwenden? System.Windows.Media.Imaging.BitmapDecoder, etc.?

Ich glaube, es wurde versucht sicherzustellen, dass diese Codecs nur eine Teilmenge der Datei lesen, um die Header-Informationen zu ermitteln. Es ist einen Scheck wert.

Frank Krueger
quelle
Danke dir. Es scheint vernünftig, aber mein Hosting hat .NET 2.
Jan Zich
1
Hervorragende Antwort. Wenn Sie in Ihrem Projekt einen Verweis auf PresentationCore erhalten können, ist dies der richtige Weg.
Ojrac
In meinen Komponententests sind diese Klassen nicht besser als GDI. Zum Lesen der JPEG-Dimensionen sind noch ~ 32 KB erforderlich.
Nariman
Wie verwenden Sie den BitmapDecoder, um die Bildabmessungen des OP zu ermitteln?
Chuck Savage
1
Siehe diese SO-Frage: stackoverflow.com/questions/784734/…
Charlie
12

Ich habe vor ein paar Monaten nach etwas Ähnlichem gesucht. Ich wollte den Typ, die Version, die Höhe und die Breite eines GIF-Bildes lesen, konnte aber online nichts Nützliches finden.

Glücklicherweise befanden sich im Fall von GIF alle erforderlichen Informationen in den ersten 10 Bytes:

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

PNG sind etwas komplexer (Breite und Höhe betragen jeweils 4 Byte):

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

Wie oben erwähnt, ist wotsit eine gute Seite für detaillierte Spezifikationen zu Bild- und Datenformaten, obwohl die PNG-Spezifikationen bei pnglib viel detaillierter sind. Ich denke jedoch, dass der Wikipedia-Eintrag in den Formaten PNG und GIF der beste Ausgangspunkt ist.

Hier ist mein ursprünglicher Code zum Überprüfen von GIFs. Ich habe auch etwas für PNGs zusammengestellt:

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
quelle
8

Basierend auf den bisherigen Antworten und einigen zusätzlichen Suchanfragen scheint es, dass in der .NET 2-Klassenbibliothek keine Funktionalität dafür vorhanden ist. Also habe ich beschlossen, meine eigenen zu schreiben. Hier ist eine sehr grobe Version davon. Im Moment brauchte ich es nur für JPGs. Damit ist die Antwort von Abbas vervollständigt.

Es gibt keine Fehlerprüfung oder andere Überprüfung, aber ich benötige sie derzeit für eine begrenzte Aufgabe, und sie kann eventuell einfach hinzugefügt werden. Ich habe es an einigen Bildern getestet und es liest normalerweise nicht mehr als 6 KB von einem Bild. Ich denke, es hängt von der Menge der EXIF-Daten ab.

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
quelle
Breite und Höhe sind umgekehrt, wenn ich das versuche.
Jason Sturges
@JasonSturges Möglicherweise müssen Sie das Exif-Orientierungs-Tag berücksichtigen.
Andrew Morton
3

Ich habe das für die PNG-Datei gemacht

  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.
quelle
1

Ja, Sie können dies absolut tun und der Code hängt vom Dateiformat ab. Ich arbeite für einen Imaging-Anbieter ( Atalasoft ), und unser Produkt bietet GetImageInfo () für jeden Codec, der das Minimum tut, um Dimensionen und einige andere leicht abrufbare Daten herauszufinden.

Wenn Sie Ihre eigenen Rollen erstellen möchten , empfehle ich, mit wotsit.org zu beginnen , das detaillierte Spezifikationen für nahezu alle Bildformate enthält. Sie werden sehen, wie Sie die Datei identifizieren und wo sich Informationen darin befinden.

Wenn Sie mit C vertraut sind, können Sie diese Informationen auch mit der kostenlosen JPEGLIB abrufen. Ich würde wetten, dass Sie dies mit .NET-Bibliotheken tun können, aber ich weiß nicht wie.

Lou Franco
quelle
Ist es sicher anzunehmen, dass die Verwendung new AtalaImage(filepath).Widthetwas Ähnliches bewirkt?
Drzaus
1
Das erste (AtalaImage) liest das gesamte Bild - das zweite (GetImageInfo) liest die minimalen Metadaten, um die Elemente eines Bildinformationsobjekts abzurufen.
Lou Franco
0

Die Antwort von ICR wurde aktualisiert, um auch progressive jPegs und WebP zu unterstützen :)

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

}
Knall
quelle
-1

Es wird vom Dateiformat abhängen. Normalerweise geben sie es in den frühen Bytes der Datei an. In der Regel wird dies bei einer guten Implementierung zum Lesen von Bildern berücksichtigt. Ich kann Sie jedoch nicht auf eine für .NET verweisen.

Kevin Conner
quelle