Obtener las dimensiones de la imagen sin leer el archivo completo

104

¿Existe una forma económica de obtener las dimensiones de una imagen (jpg, png, ...)? Preferiblemente, me gustaría lograr esto usando solo la biblioteca de clases estándar (debido a las restricciones de alojamiento). Sé que debería ser relativamente fácil leer el encabezado de la imagen y analizarlo yo mismo, pero parece que algo como esto ya debería estar allí. Además, he verificado que el siguiente código lee la imagen completa (que no quiero):

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
fuente
Sería útil que fuera un poco más específico en la pregunta propiamente dicha. Las etiquetas me han dicho .net y c #, y desea una biblioteca estándar, pero ¿cuáles son estas restricciones de alojamiento que menciona?
wnoise
Si tiene acceso al espacio de nombres System.Windows.Media.Imaging (en WPF), consulte esta pregunta SO: stackoverflow.com/questions/784734/…
Charlie

Respuestas:

106

Su mejor apuesta, como siempre, es encontrar una biblioteca bien probada. Sin embargo, dijiste que es difícil, por lo que aquí hay un código poco probado que debería funcionar para un buen número de casos:

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

Con suerte, el código es bastante obvio. Para agregar un nuevo formato de archivo, lo agrega imageFormatDecoderscon la clave como una matriz de los "bits mágicos" que aparecen al principio de cada archivo del formato dado y el valor es una función que extrae el tamaño de la secuencia. La mayoría de los formatos son bastante simples, el único apestoso real es jpeg.

ICR
fuente
6
De acuerdo, JPEG apesta. Por cierto, una nota para las personas que quieran usar este código en el futuro: esto de hecho no ha sido probado. Lo revisé con un peine fino, y esto es lo que encontré: el formato BMP tiene otra variación de encabezado (antigua) donde las dimensiones son de 16 bits; más la altura puede ser negativa (suelte el signo entonces). En cuanto a JPEG, 0xC0 no es el único encabezado. Básicamente, todos los de 0xC0 a 0xCF, excepto 0xC4 y 0xCC, son encabezados válidos (puede obtenerlos fácilmente en JPG entrelazados). Y, para hacer las cosas más divertidas, la altura puede ser 0 y especificarse más adelante en un bloque 0xDC. Véase w3.org/Graphics/JPEG/itu-t81.pdf
Vilx-
Se modificó el método DecodeJfif anterior para expandir la verificación original (marcador == 0xC0) para aceptar 0xC1 y 0xC2 también. Estos otros encabezados de inicio de trama SOF1 y SOF2 codifican ancho / alto en las mismas posiciones de bytes. SOF2 es bastante común.
Ryan Barton
4
Advertencia estándar: nunca debe escribir, throw e;sino simplemente throw;. Sus comentarios del documento XML en el segundo GetDimensionstambién se muestran en pathlugar debinaryReader
Eregrith
1
Además, parece que este código no acepta archivos JPEG codificados en formato EXIF ​​/ TIFF que emiten muchas cámaras digitales. Solo es compatible con JFIF.
cwills
2
System.Drawing.Image.FromStream (stream, false, false) le dará las dimensiones sin cargar la imagen completa, y funciona en cualquier imagen que .Net pueda cargar. Por qué esta solución desordenada e incompleta tiene tantos votos a favor está más allá de la comprensión.
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;
     }
}

el validateImageDataconjunto para falseevita que GDI + realice análisis costosos de los datos de la imagen, lo que reduce considerablemente el tiempo de carga. Esta pregunta arroja más luz sobre el tema.

Koray
fuente
1
Usé su solución como último recurso mezclado con la solución de ICR arriba. Tuve problemas con JPEG y los resolví.
Zorkind
2
Recientemente probé esto en un proyecto en el que tuve que consultar el tamaño de más de 2000 imágenes (jpg y png en su mayoría, tamaños muy mixtos), y de hecho fue mucho más rápido que la forma tradicional de usar new Bitmap().
AeonOfTime
1
La mejor respuesta. Rápido, limpio y eficaz.
dynamichael
1
Esta función es perfecta en Windows. pero no funciona en linux, aún leerá el archivo completo en linux. (.net core 2.2)
zhengchun
21

¿Ha intentado utilizar las clases de imágenes de WPF? System.Windows.Media.Imaging.BitmapDecoder, etc.?

Creo que se hizo un esfuerzo para asegurarse de que esos códecs solo leyeran un subconjunto del archivo para determinar la información del encabezado. Vale la pena comprobarlo.

Frank Krueger
fuente
Gracias. Parece razonable, pero mi alojamiento tiene .NET 2.
Jan Zich
1
Excelente respuesta. Si puede obtener una referencia a PresentationCore en su proyecto, este es el camino a seguir.
ojrac
En mis pruebas unitarias, estas clases no funcionan mejor que GDI ... todavía requieren ~ 32K para leer las dimensiones de JPEG.
Nariman
Entonces, para obtener las dimensiones de la imagen del OP, ¿cómo se usa BitmapDecoder?
Chuck Savage
1
Vea esta pregunta SO: stackoverflow.com/questions/784734/…
Charlie
12

Estaba buscando algo similar unos meses antes. Quería leer el tipo, la versión, la altura y el ancho de una imagen GIF, pero no pude encontrar nada útil en línea.

Afortunadamente, en el caso de GIF, toda la información requerida estaba en los primeros 10 bytes:

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

PNG son un poco más complejos (el ancho y el alto son de 4 bytes cada uno):

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

Como se mencionó anteriormente, wotsit es un buen sitio para especificaciones detalladas sobre formatos de imagen y datos, aunque las especificaciones PNG en pnglib son mucho más detalladas. Sin embargo, creo que la entrada de Wikipedia sobre PNG y GIF formatos es el mejor lugar para comenzar.

Aquí está mi código original para verificar GIF, también he juntado algo para 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
fuente
8

Según las respuestas hasta ahora y algunas búsquedas adicionales, parece que en la biblioteca de clases .NET 2 no hay ninguna funcionalidad para ello. Así que decidí escribir el mío. Aquí hay una versión muy aproximada. Por el momento, lo necesitaba solo para JPG. Así completa la respuesta publicada por Abbas.

No hay verificación de errores ni ninguna otra verificación, pero actualmente lo necesito para una tarea limitada y, eventualmente, se puede agregar fácilmente. Lo probé en varias imágenes y, por lo general, no lee más de 6K en una imagen. Supongo que depende de la cantidad de datos 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
fuente
El ancho y la altura se invierten cuando intento esto.
Jason Sturges
@JasonSturges Es posible que deba tener en cuenta la etiqueta de orientación Exif.
Andrew Morton
3

Hice esto para un archivo 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
fuente
1

Sí, absolutamente puede hacer esto y el código depende del formato del archivo. Trabajo para un proveedor de imágenes ( Atalasoft ), y nuestro producto proporciona un GetImageInfo () para cada códec que hace lo mínimo para averiguar las dimensiones y algunos otros datos fáciles de obtener.

Si desea lanzar el suyo, le sugiero que comience con wotsit.org , que tiene especificaciones detalladas para casi todos los formatos de imagen y verá cómo identificar el archivo y también dónde se puede encontrar la información.

Si se siente cómodo trabajando con C, entonces el jpeglib gratuito se puede utilizar para obtener esta información también. Apostaría a que puede hacer esto con bibliotecas .NET, pero no sé cómo.

Lou Franco
fuente
¿Es seguro asumir que el uso new AtalaImage(filepath).Widthhace algo similar?
drzaus
1
El primero (AtalaImage) lee la imagen completa; el segundo (GetImageInfo) lee los metadatos mínimos para obtener los elementos de un objeto de información de imagen.
Lou Franco
0

Se actualizó la respuesta de ICR para admitir jPegs progresivos y WebP también :)

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

}
explosión
fuente
-1

Dependerá del formato de archivo. Por lo general, lo indicarán en los primeros bytes del archivo. Y, por lo general, una buena implementación de lectura de imágenes lo tendrá en cuenta. Sin embargo, no puedo señalarle uno para .NET.

Kevin Conner
fuente