Artigo Técnico

Backends de encoder JBIG2 e o linker do Free Pascal

O PDFlibPas pode codificar imagens bilevel como JBIG2 por meio de dois backends diferentes. Um é um encoder MMR em Object Pascal nativo que está sempre presente. O outro é um encoder externo de symbol dictionary que produz saída substancialmente menor em texto digitalizado, e é opcional: um projeto precisa linkar a unidade de backend para que ele exista. Essa distinção é a origem da maior surpresa comum com este recurso, então vale enunciar primeiro: o DefaultJBIG2EncodeOptions solicita o encoder externo por padrão, e quando a unidade de backend não está linkada a solicitação silenciosamente recua para o caminho MMR em Pascal

No Delphi e no C++Builder o backend externo é um conjunto de objetos estáticos pré-construídos. No Free Pascal ele precisou se tornar uma DLL, e o caminho até essa conclusão é uma história de linker útil para qualquer um que já tentou linkar objetos C++ em um programa Free Pascal

O registro é o contrato

A unidade de backend se registra a partir de sua seção de inicialização chamando RegisterJBIG2EncoderBackend. Os chamadores a solicitam pelo bit de opções, PDF_JBIG2_OPTION_EXTERNAL_ENCODER, que tem o valor 4, ou pelo parâmetro UseExternalEncoder dos pontos de entrada estendidos de imagem. O guarda-chuva da biblioteca deliberadamente não puxa a unidade de backend, porque carregar um grande conjunto de objetos deve ser decisão de cada projeto; na árvore do C++Builder, por exemplo, ele é incluído explicitamente pelos projetos que o querem

A consequência para os chamadores é que solicitar o encoder externo é uma preferência, não uma garantia, e um build que esquece a unidade produz arquivos maiores em vez de um erro. Se o tamanho da saída importa o suficiente para pedir o encoder melhor, importa o suficiente para verificar que você o obteve

Fluxo de solicitação de encode JBIG2 do PDFlibPas em que a preferência pelo encoder externo recua em silêncio para o caminho MMR Pascal nativo sem a unidade de backend
Solicitar o encoder externo de symbol dictionary é uma preferência: linkado, a saída encolhe; sem link, o caminho MMR Pascal roda em silêncio com arquivos maiores
uses
  PDFlibrary,
{$IFDEF FPC}
  PDFlibJBIG2EncDLL;    // backend dinâmico para Free Pascal
{$ELSE}
  PDFlibJBIG2EncC;      // conjunto de objetos estáticos para Delphi / C++Builder
{$ENDIF}

var
  Pdf: TPDFlib;
  ImageId: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.NewDocument;
    Pdf.NewPage;
    // Interpolate, SymbolExtract, UseExternalEncoder, SkipBlackDots,
    // BlackDotSize, LossyLevel
    ImageId := Pdf.AddImageJBIG2FromFileEx('scan-page-1.tif',
      0, 1, 1, 0, 0, 0);
    if ImageId = 0 then
      raise Exception.Create('JBIG2 encoding failed');
    Pdf.SaveToFile('archive.pdf');
  finally
    Pdf.Free;
  end;
end;

Compilar a unidade foram duas linhas. Símbolos era o trabalho

Fazer a própria unidade de backend compilar sob Free Pascal levou exatamente duas mudanças: definir o dialeto do assembler e substituir um construtor de configurações de formato baseado em record pela variável global padrão. Isso reflete bem o quanto Pascal direto é portável entre os dois compiladores

O lado dos símbolos era o trabalho de verdade. O conjunto de objetos referencia 176 símbolos C. Desses, 128 já tinham implementações Pascal dentro da unidade e só precisavam de nomes de exportação anexados, porque o Delphi usa o nome da função como nome de símbolo enquanto o Free Pascal exige uma declaração de nome público explícita. Vinte e sete eram compartilhados com o codec JPEG 2000 e precisavam ser exportados de exatamente um lugar, já que defini-los duas vezes quebra qualquer programa que linka ambos. Os 21 restantes eram entradas de plataforma e runtime C, dezesseis funções de arquivo Win32 mais algumas chamadas de biblioteca padrão, e foram para uma nova unidade de compatibilidade

Nada disso é conceitualmente difícil, e tudo é necessário antes de o linker sequer tentar. O linker foi onde parou

Três rotas de link, três becos sem saída

O linker interno do Free Pascal não consegue ler os arquivos objeto, porque foram produzidos por um compilador que emite seções COMDAT associativas e o linker interno informa que não as suporta. Isso é uma recusa seca, não um aviso

Trocar por um linker externo parecia a resposta. O linker binutils empacotado com o Free Pascal crasha de imediato ao aplicar section garbage collection a este archive, e esse flag faz parte do conjunto fixo de parâmetros que o Free Pascal passa para o alvo Windows de 64 bits, então não pode ser removido da linha de comando; as chaves documentadas para suprimi-lo são ignoradas nesse caminho. Fornecer um binutils muito mais novo falha de outro jeito: ele não consegue processar de forma alguma o link script do Free Pascal, produzindo uma saída vazia sem o script e uma parede de erros de relocação com ele

Um limite descoberto no caminho vale a pena conhecer mesmo que você nunca encontre o problema do linker. O linker externo resolve os caminhos de arquivos objeto relativos ao diretório de saída do executável em vez da árvore de código-fonte, então uma diretiva relativa de include de objeto só funciona quando o diretório de saída por acaso coincide com o diretório de trabalho em tempo de compilação. Uma biblioteca não pode presumir isso sobre o projeto de um consumidor, o que por si só é uma razão para preferir uma biblioteca linkada a objetos soltos

Três rotas de linker fracassadas para objetos C++ do encoder JBIG2 sob Free Pascal e a DLL expondo dois pontos de entrada C flat que as resolveram
Seções COMDAT derrotam o linker interno e ambos os linkers externos falham, então o encoder C++ é distribuído como uma DLL vinculada dinamicamente pela unidade de backend

Por que um compilador C++ diferente não ajuda

A próxima ideia óbvia é reconstruir o lado C++ com um compilador cujos objetos o Free Pascal consiga ler. Também não funciona, e a razão é fundamental em vez de uma questão de chaves. Uma unidade de tradução C++ mínima contendo um template, compilada com todo recurso de geração de código desativado, ainda emite weak external symbols, porque a instanciação de template e inline os produz por construção. O Free Pascal rejeita essa classe de símbolo de forma categórica. O sentido inverso também falha: um linker C++ mainstream não consegue consumir objetos do outro compilador por causa do mesmo tratamento de seções COMDAT

Então o código C++ não pode ser entregue como objetos ao Free Pascal por nenhuma rota disponível. Pode ser entregue como uma DLL, que foi o que aconteceu: o encoder e sua dependência de processamento de imagem são construídos em uma biblioteca expondo dois pontos de entrada C flat, e a unidade de backend do Free Pascal os vincula dinamicamente e se registra exatamente como o backend estático faz. O caminho Delphi e C++Builder não foi tocado de forma alguma, que é o desfecho certo; um problema de portabilidade em uma toolchain não deve perturbar a toolchain que já funcionava

A polaridade é a única coisa que vai te morder

Entre um bitmap bilevel do Windows e um encoder JBIG2 há uma incompatibilidade de convenção que nenhum sistema de tipos vai capturar. Uma scanline de device-independent bitmap de um bit por pixel trata um bit definido como branco. O encoder trata um bit definido como preto. Passe as scanlines sem alterar e você obtém um stream JBIG2 perfeitamente válido do negativo fotográfico da sua página

Convenções de polaridade de DIB de um bit e JBIG2 em que um bit definido é branco na scanline e preto no encoder, corrigidas invertendo cada byte
Os mesmos bytes, significado oposto: sem inverter cada byte o encoder produz um stream JBIG2 válido do negativo fotográfico
// DIB de um bit: bit definido significa branco. Encoder JBIG2: bit
// definido significa preto. Inverta cada byte na entrada
for I := 0 to RowBytes - 1 do
  Row[I] := Row[I] xor $FF;

O método de verificação importa tanto quanto a correção. Comparar comprimentos de stream comprimido não diz nada, porque uma imagem negativa comprime para um tamanho parecido. Olhar a página prova apenas que não está visivelmente invertida. A verificação confiável é renderizar a saída dos dois caminhos de codificação, Pascal nativo e externo, para PNG e compará-los byte a byte: ambos os encoders são lossless na mesma imagem de origem, então qualquer coisa que não seja uma correspondência exata é um bug em um deles. Essa comparação agora é um teste de regressão permanente, e é o tipo de asserção que vale construir sempre que duas implementações devem concordar exatamente

Qual backend usar

Para conteúdo bilevel geral, meios-tons com dithering, line art, gráficos mistos, o encoder MMR Pascal nativo é adequado e não tem custo de implantação. Para texto digitalizado, que é o caso para o qual o JBIG2 foi projetado, o encoder externo de symbol dictionary é onde mora a redução de tamanho, porque ele fatora formas de glifos repetidas em um dicionário em vez de recodificar cada ocorrência. Se você produz arquivos de documentos digitalizados, essa diferença é grande o suficiente para mudar o planejamento de armazenamento

A questão a montante, como a imagem bilevel é produzida em primeiro lugar, importa tanto quanto para o tamanho da saída; a renderização monocromática baseada em regiões é coberta em o artigo de renderização de regiões monocromáticas, e a estratégia de tamanho de documento inteiro em otimização de tamanho de arquivo PDF e subsetting de fontes. Para conjuntos de varreduras com páginas repetidas, a deduplicação muitas vezes supera uma compressão melhor, que é o assunto de deduplicação perceptual de imagens. A disponibilidade de toolchain e backend por plataforma está listada na página de produto da losLab PDF Developer Library