O PDFlibPas descodifica TIFF com um parser em Object Pascal escrito à mão, em vez de uma ligação à libtiff, e a versão 3.534.1 apertou precisamente os pontos em que esse parser recusa dados de entrada. O número mágico 43 do BigTIFF é agora rejeitado pelo nome, TileOffsets e TileByteCounts são recusados durante a análise das etiquetas, e cada buffer é dimensionado com aritmética Int64 sob um teto de descodificação de 256 MiB
O defeito que isto encerra nunca aparece num laboratório. Aparece num gateway de digitalização que corre em silêncio há três anos, até um cliente encaminhar por ele um arquivo geoespacial ou uma imagem médica de lâmina inteira. O ficheiro tem um cabeçalho TIFF legítimo. Analisa bem. O que sai é uma página de ruído às riscas, ou uma alocação de vários gigabytes que derruba o serviço, e nada ao longo do caminho declarou a entrada inválida. Essa é a forma de falha contra a qual vale a pena desenhar: não um crash, mas uma resposta errada entregue com confiança
Porque é que II ou MM não provam que tem um TIFF clássico?
Porque o marcador de ordem de bytes é partilhado pelos dois dialectos. O TIFF clássico e o BigTIFF abrem ambos com II ou MM, e o campo que efetivamente os distingue é o número mágico de 16 bits imediatamente a seguir: 42 para o TIFF clássico, conforme definido na especificação TIFF 6.0, e 43 para o BigTIFF com os seus offsets de 64 bits. Um loader escrito como FValidTIFF := PopWord = 42 não se engana quanto ao TIFF clássico, mas colapsa duas recusas muito diferentes num único booleano silencioso, pelo que um BigTIFF se torna indistinguível de um JPEG truncado a que alguém mudou o nome. O PDFlibPas separa agora os casos e regista cada um em TPDFTIFF.LastError: um cabeçalho com menos de quatro bytes, um marcador de ordem de bytes inválido, o número mágico 43 e qualquer outro valor mágico produzem todos textos distintos. A biblioteca continua sem descodificar BigTIFF, e dizê-lo com clareza é o essencial. O chamador obtém a diferença entre "isto não é um TIFF" e "isto é um TIFF cuja disposição de offsets de 64 bits o descodificador integrado não implementa", que é a diferença entre um ticket de suporte que se responde numa única resposta e um que se transforma numa semana de adivinhação
var
Tiff: TPDFTIFF;
Page: Integer;
begin
Tiff := TPDFTIFF.Create;
try
Tiff.LoadFromFile('inbox\scan-0417.tif');
if not Tiff.ValidTIFF then
raise Exception.Create('TIFF rejected: ' + Tiff.LastError);
if Tiff.PageCount < 1 then
raise Exception.Create('TIFF carries no decodable page');
for Page := 1 to Tiff.PageCount do
Writeln(Format('page %d: %dx%d, %d spp',
[Page,
Tiff.PageInfo[Page].Width,
Tiff.PageInfo[Page].Height,
Tiff.PageInfo[Page].SamplesPerPixel]));
finally
Tiff.Free;
end;
end;
Mosaicos são uma geometria diferente, não mais um array de offsets
O PDFlibPas rejeita TIFF em mosaico durante a análise das etiquetas, antes de qualquer dado de píxeis ser tocado. O atalho que convida ao erro é fácil de ver: a etiqueta 324 (TileOffsets) e a etiqueta 325 (TileByteCounts) são arrays de offsets de ficheiro e contagens de bytes, estruturalmente idênticas aos arrays de strips, pelo que apontar os campos de strip existentes para elas custa duas linhas e compila sem problemas. Também está errado. Os mosaicos formam uma grelha bidimensional com blocos de borda com preenchimento, o seu próprio passo de linha dentro de cada mosaico, e nenhuma semântica de RowsPerStrip, como a secção de imagens em mosaico da especificação TIFF 6.0 explica em pormenor. Alimentar um descodificador de strips com dados de mosaicos, portanto, não falha de forma ruidosa. SimpleExtract e CompDecode percorrem os dados com o passo errado e produzem uma imagem com as dimensões certas e os píxeis errados. O código antigo agravava isto ao manter StripsAreTiles, ColumnsPerTile e RowsPerTile em TTIFFPage: geometria de mosaicos registada por um descodificador sem montador de mosaicos por trás. Na 3.534.1, os handlers das etiquetas 324 e 325 lançam o erro de mosaico e abandonam o IFD imediatamente, pelo que a recusa transporta a palavra "tiled" em vez de aparecer semanas mais tarde como uma reclamação de renderização
Limitar uma dimensão não é um orçamento de memória
Limitar largura e altura a 65.535 cada uma é necessário e de longe insuficiente, porque a grandeza que determina a alocação é um produto. RowsPerStrip * Width * SamplesPerPixel pode transbordar a aritmética de 32 bits muito antes de qualquer dos lados atingir o seu próprio limite, e mesmo sem transbordo pode definir uma alocação que nenhum serviço deveria tentar. O PDFlibPas calcula os bytes de linha em Int64 e impõe três tetos em conjunto: 65.535 por dimensão, 32 componentes de cor e 256 MiB de bytes descodificados
const
PDFLIB_TIFF_MAX_IMAGE_DIM = 65535;
PDFLIB_TIFF_MAX_COLOR_COMPONENTS = 32;
PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES = 256 * 1024 * 1024;
// dentro de TPDFTIFF.ValidatePageForDecode
BitsPerPixel := Int64(P.BitsPerSample) * P.SamplesPerPixel;
RowBytes := (Int64(P.Width) * BitsPerPixel + 7) div 8;
if (RowBytes < 1) or
(RowBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
(Int64(P.Height) > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES div RowBytes) then
Exit(False);
DecodedBytes := RowBytes * P.RowsPerStrip;
if (DecodedBytes < 1) or
(DecodedBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
(DecodedBytes > MaxInt) then
Exit(False);
Três detalhes aí importam mais do que as próprias constantes. O teste da altura está escrito como uma divisão em vez de uma multiplicação, para que o produto excessivo nunca chegue a formar-se. Um RowsPerStrip abaixo de 1 ou acima da altura da imagem é normalizado primeiro para a altura, que é a leitura de strip única que o TIFF 6.0 já implica e que impede uma etiqueta hostil de inflar o buffer de strips. E a rotina é partilhada: ValidatePageForDecode corre no fim da análise das etiquetas e novamente à entrada de SimpleExtract e de CompDecode, pelo que código que chegue diretamente a um descodificador não contorna o orçamento. É a mesma regra que o PDFlibPas segue ao analisar grafos de objetos PDF não fidedignos, porque um limite imposto numa de três portas não é um limite
O que deve verificar quem chama antes de ler PageInfo?
Verifique primeiro ValidTIFF, depois PageCount, e só então indexe PageInfo. Um ficheiro recusado pode deixar PageCount a zero, e GetPageInfo responde a um índice fora do intervalo com um registo TTIFFPage não inicializado, pelo que um caminho de erro que leia resolução ou contagens de amostras no processo de comunicar a falha acaba por ler ruído. A versão 3.534.1 corrigiu os dois chamadores dentro da biblioteca: o caminho de importação de imagens lê XRes e YRes apenas dentro do ramo válido, e TPDFlib.GetImagePageCount exige ValidTIFF em vez de confiar por si num número de páginas não nulo. A jusante, o argumento Options de AddImageFromFile é o número da página, com base 1, num TIFF multipágina, pelo que GetImagePageCount tem de ser digno de confiança antes de o ciclo começar, e não depois. Zero páginas é agora uma resposta real com o significado "nada aqui é descodificável", e não um acidente de um early return, o que mais importa quando se está a intercalar e a entrelaçar lotes de digitalização duplex e uma folha mal descodificada em silêncio cairia na posição errada
var
Pdf: TPDFlib;
Pages, I, ImageID: Integer;
begin
Pdf := TPDFlib.Create;
try
Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
if Pages < 1 then
Exit; // cabeçalho inválido, BigTIFF, layout em mosaico ou acima do orçamento
Pdf.NewDocument;
for I := 1 to Pages do
begin
Pdf.NewPage;
ImageID := Pdf.AddImageFromFile('inbox\scan-0417.tif', I);
if ImageID > 0 then
begin
Pdf.SelectImage(ImageID);
Pdf.DrawImage(0, 0, 595, 842);
end;
end;
Pdf.SaveToFile('scan-0417.pdf');
finally
Pdf.Free;
end;
end;
Construir o descodificador ou ligar a libtiff?
O PDFlibPas mantém o descodificador integrado, e o fator decisivo é o alcance de plataformas e não a autoria. Cerca de 1.873 linhas de Object Pascal compilam onde quer que o compilador chegue: Win32, Win64, macOS, iOS, Android, e FPC em Linux. A libtiff 4.7.1 são cerca de 30.000 linhas de C espalhadas por 34 unidades de tradução tif_*.c, e os ficheiros objeto pré-compilados que existem hoje cobrem apenas Windows. Adotá-la trocaria a cobertura completa de TIFF por uma lista de plataformas suportadas que se reduz às máquinas capazes de correr a toolchain de C, mais uma passagem de linker que ninguém percorreu ainda
O preço disso vale a pena enunciar sem maquilhagem. O descodificador integrado trata o que o trabalho com documentos digitalizados realmente produz: CCITT Group 3 unidimensional e bidimensional, Group 4, LZW, Deflate, PackBits e JPEG-in-TIFF, nos modos fotométricos WhiteIsZero, BlackIsZero, RGB, paleta e CMYK com Predictor 1 e 2. Esses payloads alinham-se com os filtros PDF da ISO 32000-1 §7.4.4 e §7.4.6, razão pela qual o front-end TIFF pesa tanto num pipeline de digitalização. O que não trata é BigTIFF, mosaicos, Predictor 3 de vírgula flutuante, PixarLog e SGILog, compressão JPEG de estilo antigo 6, e pirâmides de sub-IFD. Desde a 3.534.1, cada um desses casos é uma recusa com nome em vez de uma imagem errada, e a biblioteca mantém uma lista escrita de gatilhos para reabrir a decisão da libtiff:
- um cliente reporta um ficheiro BigTIFF e precisa de suporte nativo em vez de um passo de conversão
- um cliente reporta TIFF em mosaico de fontes médicas, GIS ou industriais e precisa de o descodificar no local
- um cliente reporta TIFF com Predictor 3 de vírgula flutuante
- uma vulnerabilidade publicada atinge os caminhos de descodificação CCITT ou LZW integrados
- o argumento multiplataforma deixa de se aplicar, seja porque o suporte a macOS, iOS e Android é abandonado, seja porque uma integração reutilizável da libtiff já cobre macOS e Linux
A migração em si está delimitada em vez de ser hipotética: um condicional USE_LIBTIFF manteria a superfície pública de TPDFTIFF intacta, encaminharia LoadFromStream por TIFFClientOpen com callbacks de stream, e deixaria o parser Pascal como alternativa fora de Windows. Até que um desses gatilhos dispare de facto, manter dois descodificadores e uma matriz de testes duplicada não compra nada que um cliente sinta. Adiar um custo com a rota de fuga já escrita é uma coisa diferente de o ignorar
Onde isto deixa um pipeline de documentos digitalizados
Trate TPDFTIFF como um ponto de controlo e não como um conversor. Carregue o ficheiro, leia ValidTIFF, e registe LastError tal e qual sempre que for falso, porque essa cadeia é agora o caminho mais curto de um relatório de campo a um diagnóstico. Os ficheiros que falham nesse ponto de controlo continuam recuperáveis convertendo-os a montante, que é a resposta prática para fontes BigTIFF e em mosaico hoje. Para dados de entrada fora do TIFF, o PDFlibPas segue um caminho separado pela sua rota de entrada de imagens AVIF, HEIF e JPEG XL, para que a questão de saber qual descodificador é responsável por cada formato permaneça explícita em vez de emergente
Tudo isto fica atrás da API de imagens comum, pelo que um pipeline de documentos ganha a fronteira mais apertada sem alterar uma única linha do código que o chama, para além de verificar a contagem de páginas que já devia estar a verificar. Se está a ponderar um caminho nativo de TIFF para PDF em Delphi ou C++Builder, o componente completo e o seu tratamento de imagens estão documentados na página da PDF Library for Delphi