Artigo Técnico

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

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

O conjunto completo exige a v3.724.0 ou posterior, e visa de frente TIFFs de scanners, software de captura médico e científico e sistemas de arquivo congelados nos anos 90. Seguem-se as razões pelas quais estas variantes não podem ser copiadas para o PDF tal como estão, como o PDFlibPas as aterra, e as armadilhas de Pascal encontradas no caminho

Porque é que um JPEG de 12 bits não entra diretamente num PDF?

Um JPEG de 12 bits não pode passar direto porque o PDF não tem filtro nenhum que seja obrigado a descodificá-lo. O ISO 32000-1 §7.4.8 define o DCTDecode em torno do formato baseline do JPEG, com a extensão progressiva acrescentada no PDF 1.3. Nada nessa cláusula cobre precisão de amostras de 12 bits, o processo lossless do Anexo H do ITU-T T.81, ou o codificador de entropia aritmética do Anexo D. Um leitor que encontre um frame SOF1 de 12 bits ou um marcador SOF9 dentro de um stream DCTDecode tem todo o direito de não desenhar nada, e a maioria dos viewers faz exatamente isso

Para as strips baseline de Huffman de 8 bits e progressivas que formam a maioria dos ficheiros JPEG-em-TIFF, o PDFlibPas ainda valida cada scan e passa os bytes comprimidos como DCTDecode, o que mantém o PDF pequeno. Todo o resto é descodificado. O PDFlibPas lê o marcador de frame e a precisão de amostras do primeiro bloco JPEG, e quando encontra SOF3, SOF9, SOF10 ou uma precisão diferente de 8, a página passa pelo descodificador de amostras em vez do caminho de passagem direta. Detetar a codificação aritmética pelo marcador de frame importa mais do que parece: uma verificação que só olhe para a precisão de amostras deixa um TIFF clássico esgueirar uma strip SOF9 de 8 bits no caminho DCTDecode e produzir um PDF que os leitores não conseguem mostrar

Fluxo de decisão para blocos TIFF JPEG no PDFlibPas para Delphi: o marcador de frame encaminha scans de 8 bits para o DCTDecode e os outros processos para descodificação nativa
o marcador de frame, e não só a precisão de amostras, decide se uma strip TIFF JPEG passa direta como DCTDecode ou é descodificada nativamente

O que o PDFlibPas escreve no PDF em alternativa

O PDFlibPas transforma blocos JPEG descodificados num XObject de imagem comum com BitsPerComponent 8 ou 16. As amostras ficam guardadas sem compressão por omissão; chame CompressImages(1) antes de adicionar a imagem e o PDFlibPas escreve-as com FlateDecode. O ISO 32000-1 §8.9.5.1 permite 16 bits por componente a partir do PDF 1.5, por isso o PDFlibPas sobe a versão do documento para pelo menos 1.5 sempre que uma imagem com amostras de 16 bits é adicionada. Isso inclui imagens multi-strip e em mosaico, 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) é desquantizado, transformado inversamente e alargado a amostras de 16 bits
  • JPEG com codificação aritmética (sequencial SOF9, progressivo SOF10) passa a amostras de 8 bits na precisão de 8 bits e a amostras de 16 bits na precisão de 12 bits
  • JPEG Huffman lossless (SOF3) passa a amostras de 8 ou 16 bits, a casar 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), por isso 4095 mapeia para 65535 e o branco continua branco em vez de estacar em 65520. O PDFlibPas nunca deixa estas imagens cair para 8 bits às suas costas. Quando a biblioteca rasteriza tal página para a sua própria superfície de visualização usa o byte alto de cada amostra, mas a imagem PDF embutida mantém os dezasseis

Mapeamento dos processos JPEG descodificados para larguras de amostra no PDFlibPas para Delphi: Huffman de 12 bits alarga a 16 bits, aritmético e lossless mapeiam pela precisão declarada
cada processo JPEG tem uma largura de amostra fixa no PDF, e os dados de 12 bits alargam-se por replicação de bits para o branco continuar branco

O preço é o tamanho do ficheiro: mesmo com CompressImages(1), amostras de 16 bits comprimidas com Flate são maiores do que os dados DCT de onde vieram, e sem isso são maiores ainda. Para fluxos de diagnóstico e arquivo, essa troca costuma ser a certa. Se o tamanho pesa mais num dado trabalho, o guia para reduzir o tamanho de ficheiros PDF em Delphi mostra para onde os bytes vão realmente

Importar estes TIFFs com o TPDFlib

O código de chamada não muda nada. O TPDFlib.AddImageFromFile(Const FileName: WideString; Options: Integer): Integer recebe o número da página TIFF, base 1, em Options e devolve um ID de imagem, ou 0 quando a página não pode ser importada. As variantes JPEG não suportadas são rejeitadas antes de a imagem ser inserida, por isso um 0 significa que nada meio descodificado 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 por omissão, 1/72 polegada
    Lib.SetOrigin(1);             // origem no topo esquerdo para o DrawImage
    Lib.CompressImages(1);        // Flate para amostras descodificadas
    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 é precisa ramificação por formato, e o AddImageFromStream e o AddImageFromString seguem as mesmas regras para dados TIFF em memória

JPEG old-style: tabelas em offsets, strips sem cabeçalhos

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

O lado do contentor, BigTIFF e layouts em mosaico, está coberto no write-up sobre o endurecimento do descodificador TIFF; aqui o foco é o payload JPEG. Cada tag de offset de tabela aponta para uma tabela de quantização crua 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 entropia-codificados nus recebe depois um cabeçalho próprio: SOI, um marcador de frame com a contagem de linhas dessa 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 a cada fronteira de strip

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

Porque é que um JPEG CMYK dentro de um TIFF sai invertido?

Um JPEG CMYK dentro de um TIFF sai invertido quando o importador aplica a convenção Adobe APP14 que pertence aos ficheiros JPEG autónomos. O Photoshop escreve CMYK invertido em JPEGs autónomos, por isso os produtores de PDF tradicionalmente acrescentam /Decode [1 0 1 0 1 0 1 0] para compensar. O PhotometricInterpretation 5 do TIFF define as amostras como quantidades de tinta em que 0 significa sem tinta, e os leitores TIFF mainstream tomam as amostras JPEG descodificadas pelo valor facial sem olhar para o APP14

O PDFlibPas embute strips CMYK TIFF comprimidas em JPEG sem o array Decode invertido, haja ou não um marcador APP14. A flag de transformação APP14 continua a decidir se o YCCK é convertido em CMYK, e o DCTDecode trata desse passo sozinho. Os ficheiros JPEG autónomos de quatro componentes mantêm a convenção Adobe invertida, por isso bytes JPEG idênticos são tratados de forma diferente conforme o contentor, como os dois formatos exigem

Porque é que a descodificação JPEG lossless parte num shift de Pascal?

A descodificação JPEG lossless parte em Pascal porque os preditores 5 e 6 precisam de um shift aritmético para a direita e o shr de Pascal é um lógico. O SOF3 prediz cada amostra a partir do vizinho à esquerda (Ra), de cima (Rb) e de cima à esquerda (Rc) e codifica só a diferença. Dos sete preditores na Tabela H.1 do T.81, o preditor 5 é Ra + ((Rb - Rc) >> 1) e o preditor 6 é Rb + ((Ra - Rc) >> 1), e a diferença entre parênteses fica negativa sempre que o vizinho de cima à esquerda for o mais claro. Dê um Integer negativo ao shr e o bit de sinal torna-se um bit de magnitude, por isso a imagem dissolve-se em ruído

Comportamento de shift para preditores JPEG lossless no Delphi e no PDFlibPas: shr lógico corrompe valores negativos, div 2 trunca para zero, HalfDown arredonda para baixo como a referência
os preditores 5 e 6 precisam de semântica de piso, e um pequeno helper casa com o descodificador de referência no Delphi e no Free Pascal

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

// Shift aritmético para a direita de um: floor(Value / 2)
// shr é um shift lógico, e div 2 trunca para 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 das larguras de tipos dessa mesma classe de problema, veja armadilhas de Longint e apontadores no Delphi Linux64

O resto do descodificador lossless tem regras de fronteira próprias. A primeira linha, e a primeira linha após cada marcador de restart, prediz a partir do vizinho da esquerda apenas e começa em 2^(P−Pt−1) para precisão P e point transform Pt; cada linha posterior começa a partir da amostra 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 extra, é legal só em modo lossless, por isso o parser de tabelas tem de a aceitar antes de saber que processo o frame vai declarar

O PDFlibPas suporta todos os sete preditores, point transforms e intervalos de restart que cubram linhas completas. A precisão JPEG tem de ser igual ao BitsPerSample e ser 8 ou 16, e todos os componentes têm de estar amostrados 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 descodificadores de 12 bits e aritmético do PDFlibPas partilham uma IDCT e diferem só na descodificação de entropia. No JPEG Huffman de 12 bits, as categorias DC chegam a 15 e as AC a 14, as tabelas de quantização podem ter entradas de 16 bits, e a predição DC reinicia em cada scan e em cada restart. Os coeficientes ficam em ordem zigzag até à desquantização, que tem de 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. As amostras descodificadas aterram a ±1 de um descodificador de referência independente em vírgula flutuante

A codificação aritmética substitui Huffman pelo codificador QM do Anexo D do T.81: uma tabela de estados de probabilidade de 114 entradas, uma área de estatísticas DC de 64 posições e uma área AC de 256, guiada 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 intervalo de restart e ao início de cada scan. É por isso que o PDFlibPas mantém segmentos DAC quando normaliza JPEGTables partilhados para imagens aritméticas; largue-os e cada strip descodifica com o modelo de estatísticas errado

A v3.723.0 acrescentou componentes com subsampling e conversão YCbCr-para-RGB com as equações JFIF a estes caminhos descodificados, e a v3.724.0 acrescentou scans progressivos tanto para dados Huffman de 12 bits (SOF2) como aritméticos (SOF10). A croma com subsampling é replicada para resolução total em vez de suavizada, que é o que um descodificador de referência produz com a suavização de croma desligada. JPEG hierárquico e JPEG lossless com codificação aritmética (SOF11) são recusados com um erro explícito

Testar fixtures JPEG binárias em Delphi

Fixtures JPEG construídas à mão em testes unitários Delphi podem falhar de uma forma que parece exatamente um bug de descodificador. Uma constante string construída com caracteres #$xx que passa de 255 caracteres é tipada como UnicodeString, e atribuí-la a um AnsiString converte-a através da página de código ativa. Os bytes $80 a $9F, rotina em dados entropia-codificados, são reescritos na entrada. Construir cabeçalhos de marcadores em runtime tem a mesma armadilha: em Delphi, AnsiChar + AnsiChar produz um UnicodeString, por isso acrescente bytes individuais a um AnsiString ou escreva-os com SetLength e atribuição indexada. Para fixtures, guarde texto hexadecimal e descodifique 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;

// A fixture continua ASCII puro, por isso nenhuma página de código a toca
ImageID := Lib.AddImageFromString(HexToBytes(TiffHex), 1);

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

Trazer arquivos de digitalização legados para o PDF

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