Obter dimensões da imagem sem ler o arquivo inteiro

104

Existe uma maneira barata de obter as dimensões de uma imagem (jpg, png, ...)? De preferência, gostaria de fazer isso usando apenas a biblioteca de classes padrão (devido às restrições de hospedagem). Eu sei que deve ser relativamente fácil ler o cabeçalho da imagem e analisá-lo sozinho, mas parece que algo assim já deve estar lá. Além disso, verifiquei que o código a seguir lê a imagem inteira (o que não quero):

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
fonte
Ajudaria se você fosse um pouco mais específico na pergunta adequada. As tags indicaram .net e c #, e você deseja uma biblioteca padrão, mas quais são essas restrições de hospedagem que você menciona?
wnoise
Se você tiver acesso ao namespace System.Windows.Media.Imaging (em WPF), consulte esta pergunta do SO: stackoverflow.com/questions/784734/…
Charlie

Respostas:

106

Sua melhor aposta, como sempre, é encontrar uma biblioteca bem testada. No entanto, você disse que isso é difícil, então aqui está um código duvidoso em grande parte não testado que deve funcionar para um bom 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);
        }
    }
}

Esperançosamente, o código é bastante óbvio. Para adicionar um novo formato de arquivo, você o adiciona imageFormatDecoderscom a chave sendo um array dos "bits mágicos" que aparecem no início de cada arquivo do formato fornecido e o valor sendo uma função que extrai o tamanho do fluxo. A maioria dos formatos são bastante simples, o único verdadeiro fedorento é o JPEG.

ICR
fonte
6
Concordo, JPEG é uma merda. Btw - uma nota para as pessoas que querem usar este código no futuro: isto realmente não foi testado. Passei por isso com um pente fino e aqui está o que descobri: o formato BMP tem outra variação de cabeçalho (antiga) em que as dimensões são de 16 bits; mais a altura pode ser negativa (solte o sinal então). Quanto ao JPEG - 0xC0 não é o único cabeçalho. Basicamente, todos de 0xC0 a 0xCF, exceto 0xC4 e 0xCC, são cabeçalhos válidos (você pode obtê-los facilmente em JPGs entrelaçados). E, para tornar as coisas mais divertidas, a altura pode ser 0 e especificada posteriormente em um bloco 0xDC. Consulte w3.org/Graphics/JPEG/itu-t81.pdf
Vilx-
Ajustou o método DecodeJfif acima para expandir a verificação original (marcador == 0xC0) para aceitar 0xC1 e 0xC2 também. Esses outros cabeçalhos de início de quadro SOF1 e SOF2 codificam largura / altura nas mesmas posições de byte. SOF2 é bastante comum.
Ryan Barton
4
Aviso padrão: você nunca deve escrever, throw e;mas simplesmente throw;. Seus comentários de documento XML no segundo GetDimensionstambém aparecem em pathvez debinaryReader
Eregrith
1
Além disso, parece que este código não aceita JPEGs codificados no formato EXIF ​​/ TIFF, que é gerado por muitas câmeras digitais. Suporta apenas JFIF.
cwills de
2
System.Drawing.Image.FromStream (stream, false, false) fornecerá as dimensões sem carregar a imagem inteira, e funciona em qualquer imagem .Net pode carregar. Por que essa solução confusa e incompleta tem tantos votos positivos está além da compreensão.
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;
     }
}

o validateImageDataconjunto para falseevitar que o GDI + execute análises caras dos dados da imagem, reduzindo drasticamente o tempo de carregamento. Esta questão lança mais luz sobre o assunto.

Koray
fonte
1
Usei sua solução como último recurso misturada com a solução do ICR acima. Teve problemas com JPEG, e resolvi com isso.
Zorkind
2
Recentemente tentei fazer isso em um projeto em que tive que consultar o tamanho de mais de 2.000 imagens (principalmente jpg e png, tamanhos muito mistos) e foi realmente muito mais rápido do que a forma tradicional de usar new Bitmap().
AeonOfTime
1
Melhor resposta. Rápido, limpo e eficaz.
dynamichael
1
Esta função é perfeita para windows. mas não funciona no linux, ele ainda lerá o arquivo inteiro no linux. (.net core 2.2)
zhengchun de
21

Você já tentou usar as classes WPF Imaging? System.Windows.Media.Imaging.BitmapDecoder, etc.?

Acredito que algum esforço foi feito para garantir que esses codecs leiam apenas um subconjunto do arquivo para determinar as informações do cabeçalho. Vale a pena verificar.

Frank Krueger
fonte
Obrigado. Parece razoável, mas minha hospedagem tem .NET 2.
Jan Zich
1
Excelente resposta. Se você conseguir uma referência ao PresentationCore em seu projeto, este é o caminho a percorrer.
ojrac
Em meus testes de unidade, essas classes não têm desempenho melhor do que GDI ... ainda requerem ~ 32K para ler as dimensões de JPEGs.
Nariman de
Portanto, para obter as dimensões da imagem do OP, como você usa o BitmapDecoder?
Chuck Savage,
1
Veja esta pergunta do SO: stackoverflow.com/questions/784734/…
Charlie
12

Eu estava procurando algo semelhante alguns meses antes. Eu queria ler o tipo, versão, altura e largura de uma imagem GIF, mas não consegui encontrar nada útil online.

Felizmente, no caso do GIF, todas as informações necessárias estavam nos primeiros 10 bytes:

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

PNG são um pouco mais complexos (largura e altura têm 4 bytes cada):

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

Como mencionado acima, wotsit é um bom site para especificações detalhadas sobre formatos de imagem e dados, embora as especificações PNG em pnglib sejam muito mais detalhadas. No entanto, acho que a entrada da Wikipedia nos formatos PNG e GIF é o melhor lugar para começar.

Este é meu código original para verificar GIFs. Também criei algo para PNGs:

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

Com base nas respostas até agora e em algumas pesquisas adicionais, parece que na biblioteca de classes do .NET 2 não há funcionalidade para ela. Então decidi escrever o meu próprio. Aqui está uma versão muito aproximada disso. No momento, eu precisava apenas para JPG's. Portanto, completa a resposta postada por Abbas.

Não há verificação de erro ou qualquer outra verificação, mas atualmente preciso dela para uma tarefa limitada e pode ser facilmente adicionada. Eu testei em algumas imagens, e geralmente não lê mais do que 6K de uma imagem. Acho que depende da quantidade de dados 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
fonte
A largura e a altura são invertidas quando tento fazer isso.
Jason Sturges,
@JasonSturges Você pode precisar levar em consideração a etiqueta de orientação Exif.
Andrew Morton,
3

Eu fiz isso para o arquivo 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
fonte
1

Sim, você pode fazer isso com certeza e o código depende do formato do arquivo. Eu trabalho para um fornecedor de imagens ( Atalasoft ), e nosso produto fornece um GetImageInfo () para cada codec que faz o mínimo para descobrir as dimensões e alguns outros dados fáceis de obter.

Se você quiser fazer o seu próprio, sugiro começar com wotsit.org , que tem especificações detalhadas para praticamente todos os formatos de imagem e você verá como identificar o arquivo e também onde as informações podem ser encontradas.

Se você se sente confortável em trabalhar com C, o jpeglib gratuito também pode ser usado para obter essas informações. Aposto que você pode fazer isso com bibliotecas .NET, mas não sei como.

Lou franco
fonte
é seguro presumir que usar new AtalaImage(filepath).Widthfaz algo semelhante?
drzaus de
1
O primeiro (AtalaImage) lê a imagem inteira - o segundo (GetImageInfo) lê os metadados mínimos para obter os elementos de um objeto de informação da imagem.
Lou Franco
0

Resposta do ICR atualizada para suportar jPegs e WebP progressivos também :)

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

}
estrondo
fonte
-1

Vai depender do formato do arquivo. Normalmente, eles o declararão nos primeiros bytes do arquivo. E, normalmente, uma boa implementação de leitura de imagem levará isso em consideração. Eu não posso apontar para um para .NET embora.

Kevin Conner
fonte