Obtenir les dimensions de l'image sans lire le fichier entier

104

Existe-t-il un moyen peu coûteux d'obtenir les dimensions d'une image (jpg, png, ...)? De préférence, j'aimerais y parvenir en utilisant uniquement la bibliothèque de classes standard (en raison des restrictions d'hébergement). Je sais qu'il devrait être relativement facile de lire l'en-tête de l'image et de l'analyser moi-même, mais il semble que quelque chose comme ça devrait déjà être là. De plus, j'ai vérifié que le morceau de code suivant lit l'image entière (ce que je ne veux pas):

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
la source
Il serait utile que vous soyez un peu plus précis dans la question proprement dite. Les balises m'ont indiqué .net et c #, et vous voulez une bibliothèque standard, mais quelles sont ces restrictions d'hébergement que vous mentionnez?
wnoise du
Si vous avez accès à l'espace de noms System.Windows.Media.Imaging (dans WPF), consultez cette question SO: stackoverflow.com/questions/784734/...
Charlie

Réponses:

106

Votre meilleur pari, comme toujours, est de trouver une bibliothèque bien testée. Cependant, vous avez dit que c'était difficile, alors voici un code douteux en grande partie non testé qui devrait fonctionner dans un bon nombre de cas:

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

Espérons que le code est assez évident. Pour ajouter un nouveau format de fichier, vous l'ajoutez imageFormatDecodersavec la clé étant un tableau des "bits magiques" qui apparaissent au début de chaque fichier du format donné et la valeur étant une fonction qui extrait la taille du flux. La plupart des formats sont assez simples, le seul vrai stinker est jpeg.

ICR
la source
6
D'accord, JPEG est nul. Btw - une note pour les personnes qui veulent utiliser ce code à l'avenir: cela n'a en effet pas été testé. Je l'ai parcouru avec un peigne fin, et voici ce que j'ai trouvé: le format BMP a une autre variante d'en-tête (ancienne) où les dimensions sont 16 bits; plus la hauteur peut être négative (laissez tomber le signe alors). Quant au JPEG - 0xC0 n'est pas le seul en-tête. Fondamentalement, tous les 0xC0 à 0xCF sauf 0xC4 et 0xCC sont des en-têtes valides (vous pouvez facilement les obtenir dans des JPG entrelacés). Et, pour rendre les choses plus amusantes, la hauteur peut être égale à 0 et spécifiée plus tard dans un bloc 0xDC. Voir w3.org/Graphics/JPEG/itu-t81.pdf
2011
Ajustement de la méthode DecodeJfif ci-dessus pour étendre la vérification d'origine (marker == 0xC0) pour accepter également 0xC1 et 0xC2. Ces autres en-têtes de début de trame SOF1 et SOF2 codent la largeur / hauteur dans les mêmes positions d'octet. SOF2 est assez courant.
Ryan Barton
4
Avertissement standard: vous ne devez jamais écrire throw e;mais simplement à la throw;place. Vos commentaires de doc XML sur le second GetDimensionsmontrent également à la pathplace debinaryReader
Eregrith
1
De plus, il semble que ce code n'accepte pas les JPEG encodés au format EXIF ​​/ TIFF qui sont émis par de nombreux appareils photo numériques. Il prend uniquement en charge JFIF.
cwills
2
System.Drawing.Image.FromStream (stream, false, false) vous donnera les dimensions sans charger l'image entière, et cela fonctionne sur n'importe quelle image que .Net peut charger. Pourquoi cette solution désordonnée et incomplète a tant de votes positifs est incompréhensible.
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;
     }
}

le validateImageDatajeu falseempêche GDI + d'effectuer une analyse coûteuse des données d'image, réduisant ainsi considérablement le temps de chargement. Cette question éclaire davantage le sujet.

Koray
la source
1
J'ai utilisé votre solution comme dernière ressource mélangée à la solution ICR ci-dessus. Eu des problèmes avec JPEG et résolus avec cela.
Zorkind du
2
J'ai récemment essayé cela dans un projet où je devais interroger la taille de plus de 2000 images (jpg et png principalement, tailles très mélangées), et c'était en effet beaucoup plus rapide que la manière traditionnelle d'utiliser new Bitmap().
AeonOfTime
1
Meilleure réponse. Rapide, propre et efficace.
dynamichael
1
Cette fonction est parfaite sur Windows. mais cela ne fonctionne pas sous Linux, il lira toujours le fichier entier sous Linux. (.net core 2.2)
zhengchun
21

Avez-vous essayé d'utiliser les classes WPF Imaging? System.Windows.Media.Imaging.BitmapDecoder, etc.?

Je pense qu'un effort a été fait pour s'assurer que ces codecs lisent uniquement un sous-ensemble du fichier afin de déterminer les informations d'en-tête. Ça vaut un chèque.

Frank Krueger
la source
Je vous remercie. Cela semble raisonnable, mais mon hébergement a .NET 2.
Jan Zich
1
Excellente réponse. Si vous pouvez obtenir une référence à PresentationCore dans votre projet, c'est la voie à suivre.
ojrac
Dans mes tests unitaires, ces classes ne fonctionnent pas mieux que GDI ... nécessitent encore ~ ​​32K pour lire les dimensions JPEG.
Nariman
Alors, pour obtenir les dimensions de l'image de l'OP, comment utilisez-vous le BitmapDecoder?
Chuck Savage
1
Voir cette question SO: stackoverflow.com/questions/784734/…
Charlie
12

Je cherchais quelque chose de similaire quelques mois plus tôt. Je voulais lire le type, la version, la hauteur et la largeur d'une image GIF mais je n'ai rien trouvé d'utile en ligne.

Heureusement dans le cas du GIF, toutes les informations requises se trouvaient dans les 10 premiers octets:

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

Les PNG sont légèrement plus complexes (la largeur et la hauteur sont de 4 octets chacune):

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

Comme mentionné ci-dessus, wotsit est un bon site pour des spécifications détaillées sur les formats d'image et de données, bien que les spécifications PNG de pnglib soient beaucoup plus détaillées. Cependant, je pense que l'entrée Wikipedia sur PNG et GIF formats est le meilleur point de départ.

Voici mon code original pour vérifier les GIF, j'ai également giflé quelque chose pour les 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
la source
8

Sur la base des réponses jusqu'à présent et de quelques recherches supplémentaires, il semble que dans la bibliothèque de classes .NET 2, il n'y ait aucune fonctionnalité pour cela. J'ai donc décidé d'écrire le mien. En voici une version très approximative. Pour le moment, je n'en avais besoin que pour les JPG. Donc, il complète la réponse publiée par Abbas.

Il n'y a pas de vérification d'erreur ni aucune autre vérification, mais j'en ai actuellement besoin pour une tâche limitée, et elle peut être éventuellement facilement ajoutée. Je l'ai testé sur un certain nombre d'images, et il ne lit généralement pas plus de 6K d'une image. Je suppose que cela dépend de la quantité de données 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
la source
La largeur et la hauteur sont inversées lorsque j'essaye ceci.
Jason Sturges
@JasonSturges Vous devrez peut-être prendre en compte la balise d'orientation Exif.
Andrew Morton le
3

Je l'ai fait pour le fichier 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
la source
1

Oui, vous pouvez absolument le faire et le code dépend du format de fichier. Je travaille pour un fournisseur d'imagerie ( Atalasoft ), et notre produit fournit un GetImageInfo () pour chaque codec qui fait le minimum pour connaître les dimensions et d'autres données faciles à obtenir.

Si vous voulez rouler le vôtre, je vous suggère de commencer par wotsit.org , qui contient des spécifications détaillées pour à peu près tous les formats d'image et vous verrez comment identifier le fichier et où les informations qu'il peuvent être trouvées.

Si vous êtes à l'aise avec C, alors le jpeglib gratuit peut également être utilisé pour obtenir ces informations. Je parierais que vous pouvez le faire avec les bibliothèques .NET, mais je ne sais pas comment.

Lou Franco
la source
est-il sûr de supposer que l'utilisation new AtalaImage(filepath).Widthfait quelque chose de similaire?
drzaus
1
Le premier (AtalaImage) lit l'image entière - le second (GetImageInfo) lit les métadonnées minimales pour obtenir les éléments d'un objet d'informations d'image.
Lou Franco
0

Mise à jour de la réponse d'ICR pour prendre en charge les jPeg et WebP progressifs également :)

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

}
coup
la source
-1

Cela dépendra du format de fichier. Habituellement, ils l'indiquent dans les premiers octets du fichier. Et, généralement, une bonne implémentation de lecture d'image en tiendra compte. Cependant, je ne peux pas vous en indiquer un pour .NET.

Kevin Conner
la source