Artigo Técnico

TIFF 12 bits, JPEG lossless e aritmético para PDF no Delphi

O PDFlibPas, a PDF Library for Delphi, importa imagens TIFF cuja compressão JPEG vai além do baseline de 8 bits: estendido sequencial de 12 bits (SOF1), Huffman lossless (SOF3), codificação aritmética (SOF9 e SOF10) e o JPEG old-style do TIFF 6.0 com offsets de tabelas. Como o DCTDecode do PDF não pode carregar esses processos, o PDFlibPas os decodifica em Pascal nativo e embute as samples sem perdas a 8 ou 16 bits por componente

O conjunto completo exige v3.724.0 ou posterior, e mira diretamente TIFFs de scanners, softwares de captura médicos e científicos e sistemas de arquivo congelados nos anos 1990. Abaixo estão por que essas variantes não podem ser copiadas para o PDF como estão, como o PDFlibPas as encaixa e as armadilhas Pascal encontradas pelo caminho

Por que um JPEG de 12 bits não entra direto num PDF?

Um JPEG de 12 bits não pode ser passado adiante porque o PDF não tem filtro algum que seja obrigado a decodificá-lo. A ISO 32000-1 §7.4.8 define o DCTDecode em torno do formato baseline do JPEG, com a extensão progressiva adicionada no PDF 1.3. Nada nessa cláusula cobre precisão de sample de 12 bits, o processo lossless do Anexo H da ITU-T T.81 ou o codificador de entropia aritmético do Anexo D. Um leitor que encontra um frame SOF1 de 12 bits ou um marker SOF9 dentro de um stream DCTDecode está no direito de não desenhar nada, e a maioria dos viewers faz exatamente isso

Para as strips de Huffman baseline e progressivas de 8 bits que formam a maioria dos arquivos JPEG-in-TIFF, o PDFlibPas ainda valida cada scan e passa os bytes comprimidos adiante como DCTDecode, o que mantém o PDF pequeno. Todo o resto é decodificado. O PDFlibPas lê o frame marker e a precisão de sample do primeiro bloco JPEG, e quando encontra SOF3, SOF9, SOF10 ou uma precisão diferente de 8, a página passa pelo decodificador de samples em vez do caminho de passagem direta. Detectar codificação aritmética pelo frame marker importa mais do que parece: uma checagem que olha só a precisão de sample deixa uma TIFF clássica esgueirar uma strip SOF9 de 8 bits para o caminho DCTDecode e produzir um PDF que os leitores não conseguem exibir

Fluxo de decisão para blocos JPEG de TIFF no PDFlibPas para Delphi: o frame marker encaminha scans de 8 bits ao DCTDecode e os outros processos à decodificação nativa
o frame marker, e não só a precisão de sample, decide se uma strip JPEG de TIFF passa adiante como DCTDecode ou é decodificada nativamente

O que o PDFlibPas escreve no PDF em vez disso

O PDFlibPas transforma blocos JPEG decodificados num image XObject comum com BitsPerComponent de 8 ou 16. As samples ficam armazenadas sem compressão por padrão; chame CompressImages(1) antes de adicionar a imagem e o PDFlibPas as grava com FlateDecode. A ISO 32000-1 §8.9.5.1 permite 16 bits por componente desde o PDF 1.5, então o PDFlibPas eleva a versão do documento para pelo menos 1.5 sempre que uma imagem com samples de 16 bits é adicionada. Isso inclui imagens multi-strip e tiled, cujo objeto raiz é um Form XObject sem BitsPerComponent próprio para inspecionar

O mapeamento é fixo por processo JPEG:

  • JPEG Huffman de 12 bits (sequencial SOF1, progressivo SOF2) é dequantizado, transformado inversamente e alargado para samples de 16 bits
  • JPEG codificado aritmeticamente (sequencial SOF9, progressivo SOF10) vira samples de 8 bits na precisão de 8 bits e samples de 16 bits na precisão de 12 bits
  • JPEG Huffman lossless (SOF3) vira samples de 8 ou 16 bits, batendo exatamente com a precisão declarada

O alargamento de 12 para 16 bits usa replicação de bits, (V shl 4) or (V shr 8), então 4095 mapeia para 65535 e o branco continua branco em vez de estourar em 65520. O PDFlibPas nunca derruba essas imagens para 8 bits às escondidas. Quando a biblioteca rasteriza tal página para a sua própria superfície de exibição, ela usa o byte alto de cada sample, mas a imagem PDF embutida mantém todos os dezesseis

Mapeamento de processos JPEG decodificados para larguras de sample no PDFlibPas para Delphi: Huffman de 12 bits alarga para 16 bits, aritmético e lossless mapeiam pela precisão declarada
cada processo JPEG tem uma largura de sample fixa no PDF, e dados de 12 bits são alargados por replicação de bits para o branco continuar branco

O preço é o tamanho do arquivo: mesmo com CompressImages(1), samples de 16 bits comprimidas com Flate são maiores que os dados DCT de onde vieram, e sem isso são maiores ainda. Para fluxos de diagnóstico e de arquivamento essa troca costuma ser a certa. Se o tamanho importa mais para um trabalho específico, o guia de redução do tamanho de arquivo PDF no Delphi mostra para onde os bytes realmente vão

Importando essas TIFFs com TPDFlib

O código chamador não muda em nada. TPDFlib.AddImageFromFile(Const FileName: WideString; Options: Integer): Integer recebe o número da página TIFF com base 1 em Options e devolve um ID de imagem, ou 0 quando a página não pode ser importada. Variantes JPEG não suportadas são rejeitadas antes de a imagem ser inserida, então um 0 significa que nada semidecodificado chegou ao documento

uses
  PDFlibrary;

function TiffToPdf(const TiffName, PdfName: WideString): Boolean;
var
  Lib: TPDFlib;
  Page, PageCount, ImageID: Integer;
  DPI, W, H: Double;
begin
  Result := False;
  Lib := TPDFlib.Create;
  try
    Lib.SetMeasurementUnits(0);   // user space padrão, 1/72 polegada
    Lib.SetOrigin(1);             // origem no topo-esquerda para DrawImage
    Lib.CompressImages(1);        // Flate para samples decodificadas
    PageCount := Lib.GetImagePageCount(TiffName);
    if PageCount = 0 then
      Exit;
    for Page := 1 to PageCount do
    begin
      ImageID := Lib.AddImageFromFile(TiffName, Page);  // Options = página TIFF
      if ImageID = 0 then
        Exit;                     // dados de imagem não suportados ou danificados
      Lib.SelectImage(ImageID);
      case Lib.ImageResolutionUnits of
        2: DPI := Lib.ImageHorizontalResolution;          // pontos por polegada
        3: DPI := Lib.ImageHorizontalResolution * 2.54;   // pontos por cm
      else
        DPI := 0;
      end;
      if DPI <= 0 then
        DPI := 300;
      W := Lib.ImageWidth * 72 / DPI;
      H := Lib.ImageHeight * 72 / DPI;
      if Page > 1 then
        Lib.NewPage;
      Lib.SetPageDimensions(W, H);
      Lib.DrawImage(0, 0, W, H);
    end;
    Result := Lib.SaveToFile(PdfName) = 1;
  finally
    Lib.Free;
  end;
end;

Não é preciso ramificação por formato, e AddImageFromStream e AddImageFromString seguem as mesmas regras para dados TIFF em memória

JPEG old-style: tabelas em offsets, strips sem headers

Desde a v3.713.0 o PDFlibPas importa TIFFs de JPEG old-style (Compression = 6) cujas tabelas vêm das tags de offset JPEGQTables, JPEGDCTables e JPEGACTables ou de um stream apenas de tabelas JPEGInterchangeFormat. A Seção 22 do TIFF 6.0 definiu esse layout, e a TIFF Technical Note 2 depois o substituiu por Compression = 7 e uma tag compartilhada JPEGTables, mas os scanners continuaram escrevendo a forma antiga por anos

O lado do contêiner, BigTIFF e layouts tiled, está coberto no write-up sobre o hardening do decodificador TIFF; aqui o foco é o payload JPEG. Cada tag de offset de tabela aponta para uma tabela de quantização bruta de 64 bytes, ou para uma tabela Huffman de 16 contagens seguida dos seus valores, uma entrada por componente. O PDFlibPas transforma isso em segmentos DQT e DHT num stream JPEGTables sintetizado. Cada strip de dados de entropia puros ganha então um header próprio: SOI, um frame marker carregando a contagem de linhas da strip, DRI quando JPEGRestartInterval está definido, SOS, os dados de entropia e EOI. As strips não podem ser coladas num único scan, porque o JPEG old-style reinicia a predição DC em cada fronteira de strip

Dois detalhes são fáceis de errar. Os três componentes de uma imagem YCbCr costumam apontar os offsets de tabela para os mesmos bytes, então o PDFlibPas deduplica tabelas por conteúdo antes de atribuir números de tabela. Se mais de duas tabelas Huffman de uma mesma classe restarem, o frame sintetizado usa SOF1 em vez de SOF0, já que um frame baseline pode referenciar só duas tabelas DC e duas AC. Quando o JPEGInterchangeFormat em vez disso aponta para um stream JPEG completo que contém scans, o PDFlibPas trata esse stream como a imagem inteira e o importa como uma única strip new-style. JPEG lossless, tiled e de planos separados old-style são recusados com um erro explícito em vez de chutados, então converta esses para JPEG new-style a montante

Por que um JPEG CMYK dentro de uma TIFF sai invertido?

Um JPEG CMYK dentro de uma TIFF sai invertido quando o importador aplica a convenção Adobe APP14 que pertence a arquivos JPEG avulsos. O Photoshop escreve CMYK invertido em JPEGs avulsos, então produtores de PDF tradicionalmente adicionam /Decode [1 0 1 0 1 0 1 0] para compensar. O PhotometricInterpretation 5 do TIFF define samples como quantidades de tinta, onde 0 significa sem tinta, e leitores TIFF mainstream tomam os samples JPEG decodificados ao pé da letra sem olhar o APP14

O PDFlibPas embute strips CMYK TIFF comprimidas em JPEG sem o array Decode invertido, com ou sem um marker APP14 presente. A flag de transform do APP14 continua decidindo se YCCK é convertido para CMYK, e o DCTDecode faz essa etapa por conta própria. Arquivos JPEG avulsos de quatro componentes mantêm a convenção Adobe invertida, então bytes JPEG idênticos são tratados de forma diferente conforme o contêiner, como os dois formatos exigem

Por que a decodificação JPEG lossless quebra num shift Pascal?

A decodificação JPEG lossless quebra em Pascal porque os preditores 5 e 6 precisam de um deslocamento aritmético para a direita e o shr de Pascal é lógico. O SOF3 prediz cada sample a partir do vizinho à esquerda (Ra), de cima (Rb) e da diagonal superior esquerda (Rc) e codifica só a diferença. Dos sete preditores na Tabela H.1 da T.81, o preditor 5 é Ra + ((Rb - Rc) >> 1) e o preditor 6 é Rb + ((Ra - Rc) >> 1), e a diferença entre colchetes fica negativa sempre que o vizinho superior esquerdo for o mais claro. Passe um Integer negativo para o shr e o bit de sinal vira um bit de magnitude, então a imagem se dissolve em ruído

Comportamento do shift para preditores JPEG lossless no Delphi e no PDFlibPas: shr lógico corrompe valores negativos, div 2 trunca rumo a zero, HalfDown faz floor como a referência
preditores 5 e 6 precisam de semântica floor, e um helper pequeno bate com o decodificador de referência no Delphi e no Free Pascal

Trocar o shift por div 2 também não resolve. O div trunca rumo a zero enquanto um deslocamento aritmético arredonda rumo ao infinito negativo, e os dois divergem em todo valor ímpar negativo. O Free Pascal tem SarLongint na unit System; o Delphi não tem equivalente, então o PDFlibPas usa um helper que compila de forma idêntica nos dois:

// Deslocamento aritmético à direita de um: floor(Value / 2)
// shr é um deslocamento lógico, e div 2 trunca rumo a zero
function HalfDown(Value: Integer): Integer;
begin
  if Value >= 0 then
    Result := Value shr 1
  else
    Result := -((1 - Value) shr 1);
end;

// Value = -3:  -3 shr 1 = 2147483646,  -3 div 2 = -1,  HalfDown(-3) = -2

Para o lado de largura de tipos da mesma classe de problema, veja armadilhas de Longint e ponteiro no Delphi Linux64

O resto do decodificador lossless tem regras de fronteira próprias. A primeira linha, e a primeira linha após cada restart marker, prediz só a partir do vizinho à esquerda e começa em 2^(P−Pt−1) para precisão P e point transform Pt; cada linha posterior começa a partir do sample de cima. A reconstrução dá a volta módulo 2^16, e o point transform escala só a saída, nunca a cadeia de predição. A categoria DC 16 de Huffman, uma diferença de 32768 sem bits extras, é legal só no modo lossless, então o parser de tabelas precisa aceitá-la antes de saber qual processo o frame vai declarar

O PDFlibPas suporta todos os sete preditores, point transforms e restart intervals que cobrem linhas inteiras. A precisão JPEG precisa igualar BitsPerSample e ser 8 ou 16, e todo componente precisa estar com sampling 1x1. Dados lossless YCbCr e JPEG lossless de 12 bits são recusados com um erro explícito

DCT de 12 bits e codificação aritmética em Pascal nativo

Os decodificadores de 12 bits e aritmético do PDFlibPas compartilham uma IDCT única e diferem só na decodificação de entropia. No JPEG Huffman de 12 bits, categorias DC chegam a 15 e categorias AC a 14, tabelas de quantização podem carregar entradas de 16 bits, e a predição DC reinicia a cada scan e a cada restart. Os coeficientes ficam em ordem zigzag até a dequantização, que precisa usar o valor de quantização no mesmo índice zigzag, e só então passam à ordem natural para a IDCT separável de duas passadas. Os samples decodificados caem dentro de ±1 de um decodificador de referência independente em ponto flutuante

A codificação aritmética troca Huffman pelo QM coder do Anexo D da T.81: uma tabela de estados de probabilidade com 114 entradas, uma área de estatísticas DC de 64 posições e uma área AC de 256 posições, dirigida por parâmetros de condicionamento do segmento DAC (defaults L = 0, U = 1, Kx = 5). Estatísticas, contexto DC e predição reiniciam a cada restart interval e no início de cada scan. É por isso que o PDFlibPas mantém segmentos DAC quando normaliza JPEGTables compartilhadas para imagens aritméticas; descarte-os e cada strip decodifica com o modelo de estatísticas errado

A v3.723.0 adicionou componentes subsampleados e conversão YCbCr-to-RGB com as equações JFIF a esses caminhos decodificados, e a v3.724.0 adicionou scans progressivos para dados Huffman de 12 bits (SOF2) e aritméticos (SOF10). O chroma subsampleado é replicado para a resolução cheia em vez de suavizado, que é o que um decodificador de referência produz com chroma smoothing desligado. JPEG hierárquico e JPEG lossless codificado aritmeticamente (SOF11) são recusados com um erro explícito

Testando fixtures JPEG binários no Delphi

Fixtures JPEG feitos à mão em testes de unit Delphi podem falhar de um jeito que parece exatamente um bug de decodificador. Uma constante string construída com caracteres #$xx que passa de 255 caracteres é tipada como UnicodeString, e atribuí-la a um AnsiString a converte pela code page ativa. Bytes $80 a $9F, rotineiros em dados codificados por entropia, são reescritos na entrada. Construir headers de markers em runtime tem a mesma armadilha: no Delphi, AnsiChar + AnsiChar produz um UnicodeString, então concatene bytes individuais num AnsiString ou escreva-os com SetLength e atribuição indexada. Para fixtures, armazene texto hex e decodifique em runtime:

uses
  SysUtils, PDFlibrary;

function HexToBytes(const Hex: AnsiString): AnsiString;
var
  I: Integer;
begin
  SetLength(Result, Length(Hex) div 2);
  for I := 1 to Length(Result) do
    Result[I] := AnsiChar(StrToInt('$' + string(Copy(Hex, I * 2 - 1, 2))));
end;

// O fixture fica em ASCII puro, então nenhuma code page pode tocá-lo
ImageID := Lib.AddImageFromString(HexToBytes(TiffHex), 1);

Ao gerar imagens de teste, note que jpegtran -arithmetic escreve SOF9 sequencial; SOF10 precisa também de -progressive. E um ID de imagem diferente de zero só prova que a importação foi aceita. Renderize a página e compare pixels, porque um erro de passagem direta produz um PDF bem formado que nenhum viewer consegue desenhar

Trazendo arquivos de digitalização legados para o PDF

Essas variantes JPEG costumam aparecer todas de uma vez durante uma migração de acervo. O PDFlibPas lê TIFFs de 12 bits, lossless, aritmético e JPEG old-style pela mesma chamada AddImageFromFile que todo o resto, mantém a precisão cheia no PDF e recusa os processos raros fora desse conjunto com um erro explícito. Para licenciamento, a lista completa de recursos e downloads de avaliação, veja a página do produto PDFlibPas PDF Library for Delphi