Artigo Técnico

HotPDF no Free Pascal: Deflate, AES e limites de codecs

O HotPDF compila e roda sob Free Pascal 3.2.2 com Lazarus, e o resumo honesto desse port são duas frases. Criação, carregamento, salvamento, compressão, descompressão, criptografia e descriptografia de documentos funcionam em backends só de Pascal, então um aplicativo Lazarus pode produzir e consumir PDF real sem qualquer dependência C. Os codecs nativos de imagem opcionais não, porque os objetos Win64 pré-construídos usam um sabor COFF que nenhum linker do Free Pascal consegue consumir, então nessa toolchain os pontos de entrada resolvem para stubs que falham fechados

Mapa de capacidades do HotPDF no Free Pascal: deflate, AES e backends de documento Pascal funcionando ao lado de stubs de codecs de imagem que falham fechados
Recursos de documento, compressão e criptografia rodam em backends só de Pascal, enquanto codecs nativos de imagem resolvem para stubs que falham fechados

Ir de "compila" para "funciona" levou um conjunto específico de correções, e cada uma delas é uma armadilha que vai encontrar qualquer outra base de código Delphi migrando para o Free Pascal. Vale registrá-las na ordem em que doeram

Por que uma unidade compilar não prova nada?

Porque uma unidade Pascal pode referenciar um símbolo que nunca fará nada de útil e ainda assim satisfazer o compilador. No ponto em que as 113 unidades da biblioteca compilaram limpas sob Free Pascal, os handlers de contêiner de archive genuinamente funcionavam, verificados por um smoke test que abria um CBZ e o convertia para PDF. O achatamento de formulários XFA não funcionava de forma alguma, porque achatar exige inflar o stream de pacotes comprimido /XFA e o ponto de entrada deflate ainda era um stub. Nada na saída do build distinguia esses dois casos

A regra que saiu disso é curta. Antes de escrever em uma nota de release que um recurso funciona em uma nova toolchain, escreva uma sonda de runtime que exerça o recurso de ponta a ponta nessa toolchain. Cobertura de compilação é um pré-requisito, nunca evidência. O quadro mais amplo do que o port cobre está em as notas de suporte a Win64 do Free Pascal e Lazarus

Um raise dentro de um stub cdecl não chega ao chamador

Este merece sua própria seção porque o sintoma é tão enganoso. As unidades de stub expõem pontos de entrada C como uma biblioteca estática faria, então um stub se parece com isto

// Parece razoável. Não é.
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
  public name 'inflate';
begin
  raise ENotSupportedException.Create('codec unavailable');
end;

No Free Pascal para Win64 essa exceção não se propaga ao chamador. Não há handler try..except que a veja, porque desenrolar através de uma fronteira cdecl declarada assim não carrega o frame de exceção Pascal; o processo termina com código de saída 217. Do lado do aplicativo não há erro, mensagem ou linha de log, apenas um programa que some. Isso é estritamente pior que uma resposta errada, porque uma resposta errada pode ser tratada

Por que uma exceção lançada dentro de um stub cdecl encerra um processo Free Pascal com código de saída 217 e como portar o gate no ponto de entrada Pascal corrige
O frame de exceção Pascal não pode desenrolar através da fronteira cdecl, então o processo morre em silêncio; a correção coloca o gate antes de o stub ser alcançado

A correção tentadora é fazer o stub retornar um código de falha em vez disso, e para o inflate isso está certo porque o zlib tem um retorno de erro bem definido. Está errado em geral: um stub de jpeg_read_header que retorna zero diz ao chamador para continuar com uma estrutura que ninguém inicializou. A correção duradoura é portar o gate no ponto de entrada Pascal em vez de dentro do stub em forma de C, usando a convenção de falha que essa API já tem

function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
  // Recuse antes de o stub ser alcançado, com a convenção de falha
  // desta própria API em vez de uma exceção através de cdecl
  Bitmap := nil;
  Result := False;
  Exit;
{$ENDIF}
  Result := DecodeJPEGNative(Data, Bitmap);
end;

paszlib não é zlib, e a diferença são duas classes de documento

A implementação Pascal de deflate disponível no Free Pascal lida com dois enquadramentos: o wrapper zlib e o deflate cru. Não lida com enquadramento gzip, que o zlib seleciona por valores de windowBits de 16 a 31, e não lida com o modo de detecção automática que os valores de 32 a 47 selecionam. O HotPDF precisa de ambos. O caminho seguro de importação de SVG pede 31, e o loader tem uma escada de fallback que pede 47 quando o enquadramento de um stream é ambíguo. Pule qualquer um deles e uma família inteira de documentos para de abrir, com um erro de decode que aponta para o stream em vez de para o enquadramento ausente

Cobertura de windowBits do paszlib versus zlib: faixas de enquadramento gzip e auto-detecção ausentes para a importação de SVG do HotPDF e o fallback do loader
O paszlib lida com o wrapper zlib e o deflate cru, mas o HotPDF também precisa de windowBits 31 e 47, então o shim precisa fornecer o enquadramento gzip ele mesmo

Há uma segunda incompatibilidade mais afiada. O record z_stream que o paszlib declara não tem o mesmo layout de memória que o C: seu campo msg é uma short string em vez de um ponteiro, e total_in e total_out são de 64 bits onde o ABI C tem machine words. Um record de chamador, portanto, não pode ser passado direto. O arranjo que funciona é manter o estado do paszlib atrás do ponteiro state que o record público já reserva, e copiar os campos públicos para dentro e para fora a cada chamada. O CRC do gzip e o trailer de comprimento de oito bytes são contabilizados nessa mesma camada de shim, que é o lugar natural para eles já que ela já é dona da decisão de enquadramento

Passar um dynamic array a um parâmetro var untyped

Este é o bug com maior probabilidade de estar no seu código agora. Quando você passa um dynamic array a um parâmetro var untyped, o que o callee recebe é o endereço da variável do array, que é o endereço de um ponteiro, não o endereço do payload. Então uma leitura para dentro dele sobrescreve a própria variável e o que estiver ao lado

var
  FBuffer: TBytes;
begin
  SetLength(FBuffer, 65536);

  // Errado: entrega o endereço da variável FBuffer
  FStream.Read(FBuffer, Length(FBuffer));

  // Certo: entrega o endereço do primeiro byte de payload
  FStream.Read(FBuffer[0], Length(FBuffer));
end;

No Delphi a forma errada frequentemente parece funcionar, porque o que ela corrompe é um slot de stack adjacente que nada lê depois. No Free Pascal a mesma linha causa segmentation fault no primeiro uso. O que a torna tão difícil de notar a olho é que arrays estáticos não têm esse problema, já que uma variável de array estático é seu próprio payload, então ambas as grafias estão corretas no mesmo arquivo dependendo da declaração a algumas centenas de linhas de distância

Contêineres ZIP sem System.Zip

O Free Pascal não tem equivalente da unidade zip da RTL, e a alternativa disponível tem tanto uma superfície de API diferente quanto nenhuma suporte para a criptografia legada que formatos de contêiner mais antigos ainda usam, então um pequeno leitor dentro da biblioteca acabou mais curto do que se adaptar a ela. Dois detalhes de formato custaram tempo e são fáceis de errar

O primeiro é o byte de verificação do header de criptografia. Seu décimo segundo byte é normalmente o byte alto do CRC, mas quando o bit 3 do flag de propósito geral está definido, significando que os tamanhos vivem em um data descriptor ao final e o CRC ainda não é conhecido, o byte de verificação vem do byte alto do tempo de modificação em vez disso. Implemente apenas a forma com CRC e todo archive escrito em modo streaming rejeita uma senha correta. O segundo é o campo extra ZIP64: seus três campos de 64 bits aparecem em uma ordem fixa mas só são escritos quando o campo de 32 bits correspondente está saturado, então lê-los em offsets fixos funciona nos archives que você testou e falha no próximo. Faça o parse deles posicionalmente contra quais campos de 32 bits estão saturados

Uma conveniência que vale conhecer: o stream de descompressão do Free Pascal recebe um segundo argumento de construtor que pula o header zlib, que é exatamente o que entradas ZIP precisam já que armazenam deflate cru. Esse caminho não toca no shim zlib da biblioteca de forma alguma, então não é afetado pelo backend C ausente

Transparência de glifos coloridos sob a LCL

Ler o canal alfa de um glifo colorido rasterizado é o único detalhe de gráficos sem tradução direta. A classe PNG da LCL não tem acessor de scanline que exponha o alfa, e atribuir um PNG a um bitmap o descarta, então um emoji colorido chega totalmente opaco e é compostado com uma caixa preta atrás. O caminho que funciona é a imagem de interface: crie-a a partir do PNG e depois leia pixels pelo acessor de cor, lembrando que seus componentes são de 16 bits e precisam ser deslocados oito para virar bytes. Essa superfície também usa ordem de linha natural de cima para baixo, então a inversão Height - 1 - Y que o código de scanline da VCL precisa deve ser removida em vez de portada

Duas notas de sistema de build antes de abrir um bug

Uma reconstrução completa ocasionalmente falha com um símbolo indefinido cujo nome termina em um sufixo $crc e um valor hexadecimal. Esse sufixo é calculado a partir dos tipos de parâmetro, e deixa de corresponder quando um build compila uma unidade contra duas versões diferentes de interface na mesma passada. Rerodar o build resolve; a assinatura não está errada

Segundo, o Free Pascal 3.2.2 não tem métodos anônimos, então em qualquer lugar em que a biblioteca usava closures para ligar um pipeline paralelo o build Free Pascal toma um fallback serial determinístico em vez disso. A saída é idêntica, o throughput não; se você depende de renderização de páginas em paralelo, essa é uma razão para ficar no Delphi por ora, e o design do pipeline é descrito em o artigo do pipeline de renderização paralela. A situação dos codecs de imagem é o outro lugar em que a escolha de toolchain muda capacidade e não apenas velocidade, então uma implantação Lazarus deve planejar seus formatos de imagem de acordo; a matriz atual por toolchain está na página de produto do HotPDF Delphi PDF component