Ottenere le dimensioni dell'immagine senza leggere l'intero file


104

Esiste un modo economico per ottenere le dimensioni di un'immagine (jpg, png, ...)? Preferibilmente, vorrei ottenere questo risultato utilizzando solo la libreria di classi standard (a causa delle restrizioni di hosting). So che dovrebbe essere relativamente facile leggere l'intestazione dell'immagine e analizzarla da solo, ma sembra che qualcosa del genere dovrebbe essere già lì. Inoltre, ho verificato che la seguente parte di codice legge l'intera immagine (cosa che non voglio):

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

Sarebbe utile se tu fossi un po 'più specifico nella domanda corretta. I tag mi hanno detto .net e c # e vuoi una libreria standard, ma quali sono queste restrizioni di hosting che hai menzionato?
wnoise

Se hai accesso allo spazio dei nomi System.Windows.Media.Imaging (in WPF), consulta questa domanda SO: stackoverflow.com/questions/784734/…
Charlie

Risposte:


106

La soluzione migliore come sempre è trovare una libreria ben collaudata. Tuttavia, hai detto che è difficile, quindi ecco un codice inaffidabile in gran parte non testato che dovrebbe funzionare per un discreto numero di casi:

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

Si spera che il codice sia abbastanza ovvio. Per aggiungere un nuovo formato di file, lo aggiungi imageFormatDecoderscon la chiave che è un array di "bit magici" che appaiono all'inizio di ogni file del formato dato e il valore è una funzione che estrae la dimensione dal flusso. La maggior parte dei formati sono abbastanza semplici, l'unico vero schifo è jpeg.


6
D'accordo, JPEG fa schifo. A proposito, una nota per le persone che vogliono utilizzare questo codice in futuro: questo è davvero non testato. L'ho provato con un pettine sottile, ed ecco cosa ho trovato: il formato BMP ha un'altra (antica) variazione dell'intestazione in cui le dimensioni sono a 16 bit; più l'altezza può essere negativa (rilascia il segno quindi). Per quanto riguarda JPEG, 0xC0 non è l'unica intestazione. Fondamentalmente tutti i valori da 0xC0 a 0xCF tranne 0xC4 e 0xCC sono intestazioni valide (puoi facilmente ottenerli in JPG interlacciati). E, per rendere le cose più divertenti, l'altezza può essere 0 e specificata in seguito in un blocco 0xDC. Vedere w3.org/Graphics/JPEG/itu-t81.pdf
Vilx-

Modificato il metodo DecodeJfif sopra per espandere il controllo originale (marker == 0xC0) per accettare anche 0xC1 e 0xC2. Queste altre intestazioni di inizio frame SOF1 e SOF2 codificano larghezza / altezza nelle stesse posizioni di byte. SOF2 è abbastanza comune.
Ryan Barton

4
Avviso standard: non dovresti mai scrivere throw e;ma semplicemente throw;invece. Anche i commenti del tuo documento XML sul secondo GetDimensionsmostrano pathinvece dibinaryReader
Eregrith

1
Inoltre, sembra che questo codice non accetti JPEG codificati in formato EXIF ​​/ TIFF che viene emesso da molte fotocamere digitali. Supporta solo JFIF.
cwills

2
System.Drawing.Image.FromStream (stream, false, false) ti darà le dimensioni senza caricare l'intera immagine e funziona su qualsiasi immagine .Net può caricare. Perché questa soluzione disordinata e incompleta ha così tanti voti positivi è al di là della comprensione.
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;
     }
}

l' validateImageDataimpostazione su falseimpedisce a GDI + di eseguire costose analisi dei dati dell'immagine, riducendo così notevolmente il tempo di caricamento. Questa domanda getta più luce sull'argomento.


1
Ho usato la tua soluzione come ultima risorsa mescolata con la soluzione di ICR sopra. Ha avuto problemi con JPEG e risolto con questo.
Zorkind,

2
Recentemente ho provato questo in un progetto in cui ho dovuto interrogare la dimensione di oltre 2000 immagini (jpg e png per lo più, dimensioni molto miste), ed era davvero molto più veloce del modo tradizionale di utilizzare new Bitmap().
AeonOfTime

1
Migliore risposta. Rapido, pulito ed efficace.
dynamichael

1
Questa funzione è perfetta su Windows. ma non funziona su Linux, leggerà comunque l'intero file su Linux. (.net core 2.2)
zhengchun

21

Hai provato a utilizzare le classi di imaging WPF? System.Windows.Media.Imaging.BitmapDecoder, eccetera.?

Credo che ci sia stato qualche sforzo per assicurarsi che quei codec leggessero solo un sottoinsieme del file per determinare le informazioni di intestazione. Vale la pena controllare.


Grazie. Sembra ragionevole, ma il mio hosting ha .NET 2.
Jan Zich

1
Ottima risposta. Se puoi ottenere un riferimento a PresentationCore nel tuo progetto, questa è la strada da percorrere.
ojrac

Nei miei unit test, queste classi non funzionano meglio di GDI ... richiedono ancora ~ 32K per leggere le dimensioni JPEG.
Nariman

Quindi, per ottenere le dimensioni dell'immagine dell'OP, come si utilizza BitmapDecoder?
Chuck Savage


12

Stavo cercando qualcosa di simile qualche mese prima. Volevo leggere il tipo, la versione, l'altezza e la larghezza di un'immagine GIF ma non sono riuscito a trovare nulla di utile online.

Fortunatamente in caso di GIF, tutte le informazioni richieste erano nei primi 10 byte:

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

I PNG sono leggermente più complessi (larghezza e altezza sono di 4 byte ciascuna):

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

Come accennato in precedenza, wotsit è un buon sito per specifiche dettagliate sui formati di immagini e dati sebbene le specifiche PNG su pnglib siano molto più dettagliate. Tuttavia, penso che la voce di Wikipedia sui formati PNG e GIF sia il miglior punto di partenza.

Ecco il mio codice originale per controllare le GIF, ho anche messo insieme qualcosa per i 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

Sulla base delle risposte finora e di alcune ricerche aggiuntive, sembra che nella libreria di classi .NET 2 non ci sia alcuna funzionalità per questo. Così ho deciso di scrivere il mio. Eccone una versione molto approssimativa. Al momento, ne avevo bisogno solo per i JPG. Quindi completa la risposta pubblicata da Abbas.

Non esiste alcun controllo degli errori o qualsiasi altra verifica, ma al momento ne ho bisogno per un'attività limitata e alla fine può essere facilmente aggiunto. L'ho testato su un certo numero di immagini e di solito non legge più di 6K da un'immagine. Immagino che dipenda dalla quantità di dati 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);
            }
        }

    }
}

Larghezza e altezza sono invertite quando provo questo.
Jason Sturges

@JasonSturges Potrebbe essere necessario prendere in considerazione il tag Exif Orientation.
Andrew Morton il

3

L'ho fatto per il 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);

1

Sì, puoi farlo assolutamente e il codice dipende dal formato del file. Lavoro per un fornitore di immagini ( Atalasoft ) e il nostro prodotto fornisce un GetImageInfo () per ogni codec che fa il minimo per scoprire le dimensioni e alcuni altri dati facili da ottenere.

Se vuoi creare il tuo, ti suggerisco di iniziare con wotsit.org , che ha specifiche dettagliate per quasi tutti i formati di immagine e vedrai come identificare il file e anche dove è possibile trovare le informazioni in esso.

Se ti senti a tuo agio a lavorare con C, allora il jpeglib gratuito può essere usato per ottenere anche queste informazioni. Scommetto che puoi farlo con le librerie .NET, ma non so come.


è lecito ritenere che l'uso new AtalaImage(filepath).Widthfaccia qualcosa di simile?
drzaus


1
Il primo (AtalaImage) legge l'intera immagine, il secondo (GetImageInfo) legge i metadati minimi per ottenere gli elementi di un oggetto info immagine.
Lou Franco

0

Aggiornata la risposta di ICR per supportare jPeg progressivi e 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

Dipenderà dal formato del file. Di solito lo dichiarano nei primi byte del file. E, di solito, una buona implementazione della lettura delle immagini ne terrà conto. Non posso indicarne uno per .NET però.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.