Artigo Técnico

HotPDF em Free Pascal e Lazarus: limites do suporte Win64

A resposta curta a esse ticket de suporte é sim, com limites. O HotPDF 2.730.0 compila em Free Pascal 3.2.2 e Lazarus 4.6 para Win64, e os caminhos centrais de criação, carregamento e gravação funcionam. O que não acompanha é tudo o que assente num objeto de codec nativo ligado estaticamente ou nos anonymous methods de Delphi

A pergunta chega quase sempre da mesma forma: uma equipa padroniza em Lazarus para uma ferramenta multiplataforma, ou herda uma base de código Free Pascal, e quer o mesmo componente PDF que já licencia para Delphi. Portar uma biblioteca Delphi madura raramente é uma questão de sintaxe. A parte interessante é o que o porting expõe sobre onde a biblioteca estava discretamente acoplada a uma única toolchain, e neste caso o acoplamento está em dois lugares muito específicos: o ABI de ficheiros de objeto dos codecs incluídos, e as funcionalidades do compilador escondidas atrás de um símbolo de versão

Uma matriz de capacidades que compara a build Delphi do HotPDF com a build Free Pascal 3.2.2 e Lazarus 4.6 para Win64, mostrando que caminhos de documentos são partilhados e que APIs de codec, compressão, renderização paralela e anonymous methods chegam a um stub que levanta exceção
Os caminhos centrais de criação, carregamento e gravação são idênticos em ambas as builds, e a lacuna está inteiramente nos codecs ligados estaticamente e nas APIs de anonymous methods

O que o Free Pascal 3.2.2 precisa antes de o HPDFDoc compilar

O HotPDF compila em Free Pascal apenas em modo Delphi, e apenas quando os diretórios de units LCL do Lazarus estão no caminho de pesquisa. Nada disto é negociável. O HotPDF.inc comuta o compilador com {$MODE DELPHI} e {$H+} dentro do seu bloco {$IFDEF FPC} e recusa qualquer versão mais antiga com um {$FATAL} quando FPC_FULLVERSION está abaixo de 30202, pelo que uma instalação 3.0.x falha de forma ruidosa em vez de produzir uma unit partida. O pacote de runtime Lazarus HotPDFLaz.lpk codifica o resto: LCL como pacote obrigatório e -Mdelphi como opção personalizada

O requisito da LCL surpreende quem só quer saída em consola, mas é estrutural. O HPDFFPCCompat fornece os tipos VCL de Delphi para que o Free Pascal não tem equivalente, mapeando TMetafile e TMetafileCanvas para classes de bitmap e canvas da LCL e aliasando TRichEdit a TMemo, enquanto o HPDFDoc faz alias de TPNGObject a Graphics.TPortableNetworkGraphic. Trate-os como shims de compilação, não como paridade de funcionalidades: uma classe de metafile suportada por um bitmap mantém a unit a compilar, não faz os caminhos de metafile comportarem-se como se comportam em Delphi. Até o smoke test sem GUI puxa Interfaces, e o script de build passa -Fu para lcl\units\x86_64-win64 e o diretório de saída de lazutils

Porque o D2009+ não pode servir também de gate de versão

É tentador tratar a build de Free Pascal como um compilador moderno e simplesmente definir o símbolo de funcionalidades mais recente de Delphi. O HotPDF não o faz, e a razão merece ser dita sem rodeios: D2009+ não significa apenas strings Unicode, também controla units cuja API pública é expressa com anonymous methods. O Free Pascal 3.2.2 não suporta nem os anonymous methods de Delphi nem essas APIs, pelo que pedir emprestado o símbolo arrastaria código que não consegue compilar. A cláusula uses do HPDFDoc traz por isso duas caudas condicionais separadas, e a sobreposição entre elas é deliberada e não acidental

uses
  // ...
  HPDFJavaScript,
  HPDFFormCalcGraph
{$IFDEF FPC}
  , HPDFFPCCodecStubs,
  HPDFCMS,
  HPDFWinCertSigner
{$ENDIF}
{$IFDEF D2009+}
  , HPDFXFARuntime,
  HPDFCMS,
  HPDFWinCertSigner,
  HPDFSignVerify,
  HPDFSignatureBatch
{$ENDIF};

Porque é que os codecs nativos param no linker?

Porque são objetos COFF Win64 emitidos por uma toolchain específica, e nenhum dos linkers de Free Pascal em Win64 os consome: nem o linker interno, nem o caminho externo com GNU ld. Isto é um problema de ABI de ficheiros de objeto, não um problema de Pascal, e nenhuma quantidade de código condicional o resolve. A biblioteca toma o único caminho honesto disponível. Cada diretiva {$L} que puxa um objeto de codec estático está embrulhada em {$IFNDEF FPC}, pelo que a build de Free Pascal simplesmente as omite, e o HPDFFPCCodecStubs fornece então cada símbolo externo em falta como um stub que levanta exceção em vez de devolver

// HPDFFPCCodecStubs.pas
function HPDFFPCNativeCodecUnavailable: PtrUInt;
begin
  raise ENotSupportedException.Create(
    'This native codec is not available in the Free Pascal build');
end;

function HPDFFPCStub_deflate: PtrUInt; cdecl;
  public name 'deflate';
begin
  Result := HPDFFPCNativeCodecUnavailable;
end;

Essa tabela de stubs é extensa, e lê-la diz-lhe exatamente que capacidades são hoje só de Delphi: os pontos de entrada deflate do zlib-ng e do zopfli, a compressão e descompressão libjpeg, o codec JPEG 2000 OpenJPEG, o libtiff e os seus inicializadores por compressão, o encode e decode JBIG2, os pontos de entrada de transformação de cor do Little-CMS, e as primitivas AES. A decisão de design por trás dos stubs importa mais do que a lista. Um símbolo em falta ao ligar dá-lhe uma parede de referências indefinidas vinda de uma unit que nunca tocou; um stub que levanta ENotSupportedException dá-lhe uma build que corre, uma mensagem que nomeia o motivo, e um stack trace que aponta para o local da chamada. Significa também que uma build de Free Pascal nunca produz em silêncio bytes errados onde uma build de Delphi produziria bytes corretos. Note ainda o efeito de segunda ordem: correr codecs de imagens não fidedignas num processo isolado é uma decisão que só surge na build de Delphi, porque uma build de Free Pascal não tem decoder nativo em processo para isolar em primeiro lugar

Em Delphi os objetos de codec estático do HotPDF ligam-se e correm nativamente, enquanto a build Free Pascal Win64 salta as diretivas de ligação e encaminha cada símbolo externo em falta para um stub que levanta uma exceção nomeada no local da chamada
Saltar as diretivas de ligação e criar stubs para todos os símbolos externos transforma uma parede de referências indefinidas numa build que corre e nomeia os seus próprios limites

Compressão: a primeira linha a mudar é cmNone

Antes de portar qualquer outra coisa, ponha Compression em cmNone. O THPDFCompressionMethod oferece exatamente dois valores, cmNone e cmFlateDecode, e o segundo vai diretamente para os pontos de entrada deflate que são stubs numa build de Free Pascal. Verifique primeiro o modelo de objetos central com a compressão desligada, e depois decida o mais de que precisa. É essa a ordem que o smoke test incluído usa: criar um documento de uma página sem compressão, recarregá-lo, e verificar que a contagem de páginas devolveu um. A saída sem compressão é maior, e continua a ser um PDF perfeitamente válido

program HotPDFLazarusSmoke;

{$mode delphi}
{$H+}

uses
  Interfaces, SysUtils, HPDFDoc;

var
  Pdf, Reloaded: THotPDF;
  OutputFile: string;
  PageCount: Integer;
begin
  OutputFile := IncludeTrailingPathDelimiter(GetTempDir) +
    'HotPDF-FPC-Smoke.pdf';
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := OutputFile;
    Pdf.Compression := cmNone;   // cmFlateDecode chega a um símbolo com stub
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(72, 72, 0, 'HotPDF Free Pascal smoke test');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;

  Reloaded := THotPDF.Create(nil);
  try
    PageCount := Reloaded.LoadFromFile(OutputFile);
    if PageCount <> 1 then
      raise Exception.CreateFmt('Expected one page, got %d', [PageCount]);
  finally
    Reloaded.Free;
  end;
end.

O que acontece à renderização paralela de páginas?

Continua a compilar, continua a devolver bitmaps corretos, e deixa de ser paralelo. O THotPDF.RenderLoadedPagesParallel e o THotPDF.RenderLoadedPagesParallelOrdered assentam em TThread.CreateAnonymousThread com uma closure de procedure inline, que o Free Pascal 3.2.2 não sabe expressar, pelo que o ramo de Free Pascal corre um fallback serial determinista: percorre os índices de páginas por ordem, chama RenderLoadedPageToBitmap a cada um, e conta os sucessos. A forma da API, o valor de retorno e a matriz de saída ficam iguais, e é isso que permite a uma única base de código compilar das duas maneiras

A mesma chamada de renderização paralela do HotPDF corre em threads de trabalho sobrepostas em Delphi e percorre os índices de páginas em série no Free Pascal, com o registo de informações do pipeline a reportar uma contagem de workers de um em vez de esconder o fallback
O ramo de Free Pascal mantém a forma da API e a matriz de saída enquanto reporta uma contagem de workers de um, pelo que o código que já lê o registo de informações vê a verdade
var
  Bitmaps: THPDFBitmapArray;
  Info: THPDFParallelRenderPipelineInfo;
  Rendered: Integer;
begin
  Rendered := Pdf.RenderLoadedPagesParallel([0, 1, 2, 3], 150, 4,
    Bitmaps, Info);
  // Delphi: Info.WorkerCount é o que o orçamento de memória permitiu
  // Free Pascal: Info.WorkerCount é sempre 1, páginas pela ordem dos índices
  if Info.WorkerCount = 1 then
    LogSerialFallback(Rendered, Info.RequestedWorkerCount);

O fallback não é silencioso, e é essa a parte que vale a pena ter em conta ao desenhar. Preenche o THPDFParallelRenderPipelineInfo com honestidade: PageCount do pedido, RequestedWorkerCount a ecoar o que pediu, WorkerCount fixado em 1, e as contagens de concluídas e entregues a coincidir com o que realmente voltou. O código que já inspeciona Info para dimensionar uma barra de progresso ou um orçamento de memória continua a funcionar e lê a verdade em vez de uma suposição. Se o seu plano de débito depende do pipeline de renderização paralela e do seu modelo de backpressure, esse plano é um plano de Delphi; no Free Pascal, considere no orçamento o custo single-threaded de renderizar uma página para bitmap multiplicado pela contagem de páginas

Que build deve realmente distribuir?

Escolha por capacidade, não por preferência. Se o seu fluxo de trabalho é montagem de documentos, desenho de texto e vetores, preenchimento de formulários, carregamento e gravação, a build de Free Pascal em Win64 cobre isso, e deve validar com a compressão desligada antes de ligar qualquer coisa. Se envolve imagens JPEG ou JPEG 2000 ou TIFF ou JBIG2, transformações de cor ICC, saída comprimida, ou débito que dependa de muitos núcleos, fique em Delphi ou C++Builder por agora. A fronteira é traçada por um ABI de ficheiros de objeto e por uma funcionalidade de linguagem em falta, ambos visíveis no código-fonte em vez de enterrados numa matriz de suporte, e ambos a falhar com um erro nomeado em vez de um resultado errado

O pacote de Free Pascal e Lazarus vem na mesma distribuição que as units de Delphi e C++Builder, pelo que uma licença cobre ambos e pode testar o caminho Lazarus contra os seus próprios documentos antes de se comprometer; a página de produto do componente Delphi PDF HotPDF traz a matriz de suporte de compiladores atual e a referência completa da API