A HotXLS lê a geometria real em pixels de uma imagem do próprio cabeçalho do arquivo antes de qualquer geometria de drawing ser escrita. GetImageSize em lxImage.pas despacha pela extensão do arquivo para GetJpegSize, GetGifSize, GetBmpSize, GetPngSize ou GetWmfEmfSize, converte pixels para pontos com um fator de 0.75, e retorna -1 quando genuinamente não consegue dizer
Essa última cláusula é a parte que levou mais tempo para acertar. Um leitor de tamanho que não pode falhar é um leitor de tamanho que mente, e uma mentira sobre geometria de imagem não aparece na hora da inserção. Ela aparece semanas depois, quando alguém abre a pasta de trabalho e o logo da empresa é um retângulo esmagado
Por que uma imagem precisa da geometria real antes de entrar no drawing?
Porque o formato de planilha guarda a extensão, não a imagem. Quando AddPicture ou Images.AddFromFile coloca uma imagem, a part de drawing registra uma largura e altura explícitas para a shape — em DrawingML são os atributos cx e cy do elemento a:ext, em English Metric Units, pela ECMA-376 Parte 1 §20.4. Nada nesse registro diz "use o tamanho natural da imagem", então o tamanho natural tem que vir de algum lugar, e o único lugar onde ele existe é nos bytes da imagem. Erre por um fator de três e a imagem fica esticada por um fator de três, em todo aplicativo que abre a pasta de trabalho, para sempre. A conversão de unidades dessas dimensões em pixels para EMU é uma preocupação separada, coberta no artigo sobre unidades EMU e escala; este texto é sobre o passo anterior, onde os números de pixels são obtidos. O fallback quando um leitor falha é 100 por 100, e esse número é deliberadamente, obviamente errado. Não é um palpite de tamanho plausível, é um marcador que significa "ninguém conseguiu ler esse arquivo", e é muito melhor que um número errado de cara plausível porque um usuário reporta na hora
GIF: o descriptor que nunca era analisado
GetGifSize é novo, e antes de ele existir toda GIF inserida numa planilha saía exatamente em 100 por 100 — não havia ramo de GIF nenhum, então a extensão caía no braço default de GetImageSize e retornava -1. É o cabeçalho mais barato do conjunto de analisar, o que torna a omissão levemente constrangedora e muito fácil de corrigir. A especificação GIF89a coloca o Logical Screen Descriptor imediatamente depois da assinatura de seis bytes, e a §18 define os dois primeiros campos dele como largura e altura da tela lógica, cada um um valor unsigned de 16 bits em little-endian. Em termos de arquivo, são os bytes 6 e 7 para largura, bytes 8 e 9 para altura, então uma leitura única de dez bytes cobre a checagem da assinatura e as duas dimensões
function GetGifSize(const AFileName: WideString; var AWidth, AHeight: Double): Integer;
var
f: TWFileStream;
Header: array [0.. 9] of Byte;
wWidth, wHeight: Word;
begin
Result:= - 1;
AWidth:= 100;
AHeight:= 100;
f:= TWFileStream.Create(AFileName, fmOpenRead);
try
// Assinatura "GIF8", depois o Logical Screen Descriptor com largura e
// altura como words little-endian nos offsets 6 e 8
if (f.Read(Header[0], SizeOf(Header))= SizeOf(Header))and
(Header[0]= Ord('G'))and (Header[1]= Ord('I'))and
(Header[2]= Ord('F'))and (Header[3]= Ord('8')) then
begin
wWidth:= Header[6]or (Header[7] shl 8);
wHeight:= Header[8]or (Header[9] shl 8);
if (wWidth> 0)and (wHeight> 0) then
begin
AWidth:= wWidth* 0.75;
AHeight:= wHeight* 0.75;
Result:= 1;
end;
end;
finally
f.Free;
end;
end;
Note a forma: os valores de falha são atribuídos primeiro, e o caminho de sucesso é a única coisa que os sobrescreve. Todo leitor em lxImage.pas segue essa ordem, porque ela torna "não consegui ler isso" o default em vez de algo que tem que ser lembrado em cada saída antecipada. Checar só GIF8 em vez da string completa GIF87a ou GIF89a é intencional — o layout do descriptor é idêntico nas duas revisões, e não há benefício em rejeitar um arquivo cuja geometria é legível
O que um tamanho de cabeçalho BMP de 12 muda de verdade?
GetBmpSize lê os quatro bytes no offset 14 como o tamanho do cabeçalho DIB e ramifica no valor, porque um BMP com tamanho de cabeçalho 12 guarda as dimensões numa largura de campo completamente diferente de todo outro BMP. O [MS-WMF] §2.2.2 detalha a família: BITMAPCOREHEADER tem 12 bytes e carrega largura e altura de 16 bits, enquanto BITMAPINFOHEADER, BITMAPV4HEADER e BITMAPV5HEADER todos começam com biWidth e biHeight signed de 32 bits. Ler quatro bytes onde o arquivo tem dois não falha alto; puxa a contagem de planos e a profundidade de bits como metade alta de um número e reporta um bitmap de várias centenas de milhares de pixels de largura. A segunda armadilha no mesmo cabeçalho é o sinal de biHeight: um valor negativo não significa altura negativa, significa que as linhas do DIB estão armazenadas de cima para baixo em vez de baixo para cima, que é uma afirmação de ordem de pixels sem relação nenhuma com geometria
f.Seek(14, soFromBeginning);
if f.Read(HeaderSize, 4)= 4 then
begin
if HeaderSize= 12 then
begin
// BITMAPCOREHEADER guarda dimensões de 16 bits
if (f.Read(wWidth, 2)= 2)and (f.Read(wHeight, 2)= 2) then
begin
lWidth:= wWidth;
lHeight:= wHeight;
Result:= 1;
end;
end
else if (f.Read(lWidth, 4)= 4)and (f.Read(lHeight, 4)= 4) then
Result:= 1;
end;
// ...
// Um biHeight negativo só marca um DIB top-down; a magnitude é o tamanho
if (Result= 1)and (lWidth> 0)and (lHeight<> 0) then
begin
AWidth:= lWidth* 0.75;
AHeight:= Abs(lHeight)* 0.75;
end
else
Result:= - 1;
A guarda final vale ler com atenção. A altura é testada contra <> 0 em vez de > 0, porque negativo é legal e zero não é, e Abs é aplicado só no ponto da conversão. A largura não tem caso equivalente — um biWidth negativo não é definido pelo formato, então é tratado como arquivo corrompido e rebaixado para -1. Passar um valor negativo cru para cima, em vez disso, produziria uma extensão negativa no registro de drawing, que é clampada, ignorada ou renderizada como shape espelhada dependendo de qual aplicativo abre a pasta de trabalho
JPEG: quais marcadores são realmente headers de frame?
GetJpegSize aceita o conjunto de marcadores [$C0..$C3, $C5..$C7, $C9..$CB, $CD..$CF], e as três lacunas nesse range são o ponto inteiro. A ITU-T T.81 §B.2.2 define o frame header como SOFn, e o frame header é onde vivem o número de linhas e o número de amostras por linha, em todo tipo de frame em vez de só nos comuns. Os marcadores que caem dentro do mesmo intervalo numérico mas não são frame headers são $C4 (DHT, define tabelas de Huffman), $C8 (JPG, reservado) e $CC (DAC, define o condicionamento da codificação aritmética). Um leitor que reconhece só baseline, extended sequential e progressive — $C0, $C1 e $C2 — funciona em quase todo JPEG que uma câmera ou uma ferramenta de screenshot produz, e então falha silenciosamente nos que importam para fluxos de documentos: JPEG lossless ($C3), as variantes codificadas aritmeticamente ($C9 a $CB) e os tipos de frame hierárquicos ($CD a $CF) todos varrem para além do frame header, saem do fim do arquivo, e caem no fallback. Imagens médicas e pipelines de documentos escaneados produzem exatamente esses arquivos
// Todo marcador SOFn carrega as dimensões do frame; $C4 (DHT), $C8 (JPG)
// e $CC (DAC) compartilham o range mas não são frame headers
if Seg in [$C0.. $C3, $C5.. $C7, $C9.. $CB, $CD.. $CF] then
begin
ReadLen:= f.Read(Dummy[0], 3);
ReadLen:= f.Read(wHeight, 2);
wHeight:= swapByte(wHeight);
ReadLen:= f.Read(wWidth, 2);
wWidth:= swapByte(wWidth);
found:= True;
Break;
end
Dois detalhes nessas sete linhas. Os três bytes pulados são a word de comprimento do segmento mais o byte de precisão de amostra, então a leitura cai na contagem de linhas; o JPEG guarda altura antes de largura, que é o inverso da maioria dos formatos e uma transposição fácil de cometer. E os valores passam por swapByte porque o JPEG é big-endian por inteiro, a mesma razão pela qual GetPngSize passa seus valores IHDR de 32 bits por lswapByte. A caminhada de segmentos tem a própria guarda: quando um marcador não é sem parâmetros, o leitor pega o comprimento de dois bytes, faz byte-swap, e se recusa a fazer seek se o valor for menor que 2 — um comprimento mais curto que o próprio campo se traduziria num seek para trás, e um seek para trás num loop de varredura é um loop infinito num arquivo que outra pessoa controla
Retornar menos um vence retornar um número plausível
O fio comum entre os cinco leitores é que GetImageSize distingue três resultados em vez de dois. 1 significa que a geometria foi lida do arquivo. -1 significa que não foi, e o chamador deve usar o default dele. 7 é retornado por GetWmfEmfSize e significa outra coisa inteira: diz ao AddPicture para tomar o tamanho do registro BSE do metafile, porque um WMF ou EMF carrega a extensão no próprio registro de imagem da pasta de trabalho em vez de num offset fixo no cabeçalho do arquivo. Tratamento de metafile em geral é coberto no artigo sobre o decoder EMF e WMF limitado. GetJpegSize mostra por que essa distinção precisa de uma flag explícita em vez de inferência: o loop de varredura pode terminar de três jeitos — achou um frame header, bateu no fim do arquivo, ou abortou por um comprimento de segmento corrompido — e só o primeiro atribui wWidth e wHeight. Sem o Boolean found, os outros dois caminhos converteriam o que esses locals por acaso contivessem e entregariam o resultado para cima como sucesso. Locals não inicializados são frequentemente zero, e zero é a pior resposta possível aqui: sobrevive a toda checagem de validade a jusante e produz uma imagem invisível que ninguém consegue selecionar nem deletar
var
W, H: Double;
begin
case GetImageSize(FileName, W, H) of
1: Sheet.AddPicture(FileName, Row, Col, W, H);
7: Sheet.AddPicture(FileName, Row, Col); // metafile: tamanho do registro BSE
else
// W e H são 100 x 100; decida a política em vez de aceitar um placeholder
LogWarning('unreadable image geometry: ' + FileName);
end;
end;
Onde ficam os limites honestos
Esses leitores deliberadamente analisam um cabeçalho e param. Eles não decodificam pixels, então reportam as dimensões armazenadas e nada mais, e há casos em que as dimensões armazenadas não são a história inteira. Uma GIF animada cujos frames são menores que a tela lógica ainda reporta a tela lógica, que é correto para layout e pode não bater com o que um viewer mostra. Um JPEG carregando uma tag de orientação EXIF que especifica rotação de 90 graus reporta as dimensões de frame não rotacionadas, então uma foto em retrato tirada num celular pode voltar como paisagem. Um DIB com razão de aspecto de pixel não quadrada reporta contagens cruas de pixels, e o fator 0.75 assume 96 DPI o tempo todo. Nenhum desses é bug na leitura de cabeçalho e nenhum é corrigível sem decodificar a imagem, que não é o propósito desta camada — são limites documentados, então se o seu pipeline lida com fotos de celular, leia a tag de orientação você mesmo e troque os valores antes de chamar AddPicture. Todo o resto sobre posicionamento de imagem, ancoragem e o modelo de objetos de drawing é coberto no artigo sobre gráficos, imagens e drawings
A HotXLS lê e escreve XLS, XLSX, ODS e CSV nativamente em Delphi e C++Builder sem instalação do Excel, e a inserção de imagem passa por esse caminho de cabeçalho em todos eles. Detalhes de suporte a formatos e licenciamento estão na página do produto HotXLS Delphi spreadsheet component