Artigo Técnico

Reforço do decodificador TIFF em Delphi: BigTIFF e tiles

O PDFlibPas decodifica TIFF com um parser Object Pascal escrito à mão em vez de uma ligação à libtiff, e a versão 3.534.1 apertou exatamente os pontos em que esse parser recusa entrada. O magic 43 do BigTIFF agora é rejeitado pelo nome, TileOffsets e TileByteCounts são recusados durante a análise das tags, e cada buffer é dimensionado com aritmética Int64 sob um teto de decodificação de 256 MiB

O defeito que isso fecha nunca aparece em laboratório. Ele aparece como um gateway de digitalização que rodou silenciosamente por três anos, até que um cliente roteia por ele um arquivo geoespacial ou uma imagem médica de lâmina inteira. O arquivo tem um cabeçalho TIFF legítimo. Ele é analisado. O que sai é uma página de ruído listrado, ou uma alocação de vários gigabytes que derruba o serviço, e nada ao longo do caminho declarou a entrada inválida. Esse é o modo de falha contra o qual vale a pena projetar: não uma queda abrupta, mas uma resposta errada entregue com confiança

Por que II ou MM não prova que você tem um TIFF clássico?

Porque o marcador de ordem de bytes é compartilhado pelos dois dialetos. O TIFF clássico e o BigTIFF abrem com II ou MM, e o campo que de fato os distingue é o magic de 16 bits imediatamente posterior: 42 para o TIFF clássico conforme definido na especificação TIFF 6.0, 43 para o BigTIFF com seus offsets de 64 bits. Um loader escrito como FValidTIFF := PopWord = 42 não está errado quanto ao TIFF clássico, mas colapsa duas recusas muito diferentes em um booleano silencioso, de modo que um BigTIFF se torna indistinguível de um JPEG truncado que alguém renomeou. O PDFlibPas agora separa os casos e registra cada um em TPDFTIFF.LastError: um cabeçalho com menos de quatro bytes, um marcador de ordem de bytes inválido, o magic 43 e qualquer outro valor de magic produzem textos distintos. A biblioteca ainda não decodifica BigTIFF, e dizer isso com clareza é o ponto. Quem chama recebe a diferença entre "isto não é um TIFF" e "isto é um TIFF cujo layout de offsets de 64 bits o decodificador integrado não implementa", que é a diferença entre um chamado de suporte que você responde em uma mensagem e um que vira uma semana de adivinhação

O loader TIFF do PDFlibPas lê separadamente o marcador de ordem de bytes e o magic de 16 bits, de modo que um cabeçalho curto, um marcador inválido, o magic 43 do BigTIFF e qualquer outro valor de magic produzem textos LastError distintos em vez de um booleano silencioso
O TIFF clássico e o BigTIFF abrem com o mesmo marcador de ordem de bytes, então o PDFlibPas separa quatro casos de recusa e nomeia cada um no LastError
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;

Tiles são uma geometria diferente, não outro array de offsets

O PDFlibPas rejeita TIFF tiled durante a análise das tags, antes que qualquer dado de pixel seja tocado. O atalho que convida o defeito é fácil de enxergar: a tag 324 (TileOffsets) e a tag 325 (TileByteCounts) são arrays de offsets de arquivo e contagens de bytes, estruturalmente idênticos aos arrays de strips, de modo que apontar os campos de strip existentes para eles custa duas linhas e compila limpo. Também está errado. Tiles formam uma grade bidimensional com blocos de borda preenchidos, seu próprio passo de linha dentro de cada tile e nenhuma semântica de RowsPerStrip, como a seção de imagens tiled do TIFF 6.0 explica. Alimentar um decodificador de strips com payloads de tiles, portanto, não falha de forma evidente. SimpleExtract e CompDecode percorrem os dados com o passo errado e emitem uma imagem com as dimensões certas e os pixels errados. O código antigo agravava isso mantendo StripsAreTiles, ColumnsPerTile e RowsPerTile em TTIFFPage: geometria de tile registrada por um decodificador sem montador de tiles por trás. Na 3.534.1 os manipuladores das tags 324 e 325 levantam o erro de tile e abandonam o IFD imediatamente, de modo que a recusa carrega a palavra "tiled" em vez de aparecer semanas depois como uma reclamação de renderização

O PDFlibPas compara o layout de strips, em que faixas de largura total compartilham um passo de linha, com o layout tiled, uma grade bidimensional de blocos de borda preenchidos, e recusa as tags 324 e 325 durante a análise das tags antes que qualquer dado de pixel seja tocado
Arrays de tiles parecem estruturalmente idênticos a arrays de strips, e é por isso que alimentá-los em um decodificador de strips produz as dimensões certas e os pixels errados

Um limite de dimensão não é um orçamento de memória

Limitar largura e altura a 65.535 cada é necessário e está longe de ser suficiente, porque a quantidade que dirige a alocação é um produto. RowsPerStrip * Width * SamplesPerPixel pode estourar a aritmética de 32 bits muito antes de qualquer lado atingir seu próprio limite, e mesmo sem estouro pode nomear uma alocação que nenhum serviço deveria tentar. O PDFlibPas calcula os bytes de linha em Int64 e impõe três tetos juntos: 65.535 por dimensão, 32 componentes de cor e 256 MiB de bytes decodificados

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 de altura é escrito como uma divisão em vez de uma multiplicação, de modo que o produto superdimensionado nunca chega a ser formado. Um RowsPerStrip abaixo de 1 ou acima da altura da imagem é normalizado para a altura primeiro, o que é a leitura de strip única que o TIFF 6.0 já implica e impede que uma tag hostil infle o buffer de strip. E a rotina é compartilhada: ValidatePageForDecode roda ao final da análise das tags e novamente na entrada de SimpleExtract e CompDecode, de modo que código que chega direto a um decodificador não pode contornar o orçamento. Essa é a mesma regra que o PDFlibPas segue ao analisar grafos de objetos PDF não confiáveis, porque um limite imposto em uma porta de três não é um limite

O PDFlibPas dimensiona cada buffer TIFF com aritmética Int64, testa a altura da imagem com uma divisão para que o produto superdimensionado nunca se forme e roda a mesma rotina ValidatePageForDecode na análise das tags e em ambas as entradas dos decodificadores
Três tetos, aritmética Int64 para bytes de linha e uma rotina de validação compartilhada alcançada pelas três portas, porque um limite imposto em uma porta de três não é um limite

O que quem chama deve verificar antes de ler PageInfo?

Verifique ValidTIFF primeiro, depois PageCount, e só então indexe PageInfo. Um arquivo rejeitado pode deixar PageCount em zero, e GetPageInfo responde a um índice fora da faixa com um registro TTIFFPage não inicializado, de modo que um caminho de erro que lê resolução ou contagens de amostras a caminho de relatar a falha acaba lendo ruído. A versão 3.534.1 corrigiu ambos os 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 só em uma contagem de páginas diferente de zero. Mais adiante, o argumento Options de AddImageFromFile é o número de página de base 1 de um TIFF multipágina, de modo que GetImagePageCount precisa ser confiável antes de o loop começar, e não depois. Zero páginas agora é uma resposta real significando "nada aqui é decodificável", não um acidente de um retorno antecipado, o que importa mais quando você está colacionando e intercalando lotes de digitalização duplex e uma folha silenciosamente mal decodificada 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 tiled 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 decodificador ou vincular a libtiff?

O PDFlibPas mantém o decodificador integrado, e o fator decisivo é o alcance de plataformas, não a autoria. Cerca de 1.873 linhas de Object Pascal compilam em qualquer lugar onde o compilador chega: Win32, Win64, macOS, iOS, Android e FPC no Linux. A libtiff 4.7.1 tem cerca de 30.000 linhas de C espalhadas por 34 unidades de tradução tif_*.c, e os arquivos de 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 rodar a toolchain C, além de uma passagem de linker que ninguém percorreu ainda

O que isso custa vale a pena declarar sem edulcoração. O decodificador integrado dá conta do que o trabalho com documentos digitalizados realmente produz: CCITT Group 3 unidimensional e bidimensional, Group 4, LZW, Deflate, PackBits e JPEG-em-TIFF, nos fotométricos WhiteIsZero, BlackIsZero, RGB, paleta e CMYK com Predictor 1 e 2. Esses payloads coincidem com os filtros PDF do ISO 32000-1 §7.4.4 e §7.4.6, e é por isso que o front-end TIFF carrega tanto peso em um pipeline de digitalização. O que ele não trata é BigTIFF, tiles, Predictor 3 de ponto flutuante, PixarLog e SGILog, compressão JPEG 6 no estilo antigo e pirâmides de sub-IFD. Desde a 3.534.1 cada um desses casos é uma recusa nomeada em vez de uma imagem errada, e a biblioteca mantém uma lista escrita de gatilhos para reabrir a decisão sobre a libtiff:

  • um cliente relata um arquivo BigTIFF e precisa de suporte nativo em vez de uma etapa de conversão
  • um cliente relata TIFF tiled de fontes médicas, GIS ou industriais e precisa que ele seja decodificado no lugar
  • um cliente relata TIFF com Predictor 3 de ponto flutuante
  • uma vulnerabilidade publicada atinge os caminhos de decodificação CCITT ou LZW integrados
  • o argumento multiplataforma deixa de valer, 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 tem escopo definido, não é hipotética: um condicional USE_LIBTIFF manteria a superfície pública de TPDFTIFF intacta, rotearia LoadFromStream por TIFFClientOpen com callbacks de stream e deixaria o parser Pascal como fallback para fora do Windows. Até que um desses gatilhos realmente dispare, manter dois decodificadores e uma matriz de testes dobrada não compra nada que o cliente sinta. Adiar um custo com a rota de fuga já escrita é uma coisa diferente de ignorá-lo

Onde isso deixa um pipeline de documentos digitalizados

Trate TPDFTIFF como um portão em vez de um conversor. Carregue o arquivo, leia ValidTIFF e registre LastError verbatim sempre que for falso, porque essa string agora é a rota mais curta de um relato de campo a um diagnóstico. Arquivos que falham no portão continuam recuperáveis ao serem convertidos a montante, que é a resposta prática para fontes BigTIFF e tiled hoje. Para entrada totalmente fora do TIFF, o PDFlibPas segue uma rota separada por seu caminho de entrada de imagens AVIF, HEIF e JPEG XL, de modo que a questão de qual decodificador é dono de qual formato permanece explícita em vez de emergente

Tudo isso fica atrás da API de imagens ordinária, de modo que um pipeline de documentos ganha a fronteira mais rígida sem mudar uma linha de código de chamada além de verificar a contagem de páginas que já deveria estar verificando. Se você está avaliando um caminho nativo de TIFF para PDF em Delphi ou C++Builder, o componente completo e seu tratamento de imagens estão documentados na página do PDF Library para Delphi