Artigo Técnico

HotPDF no Free Pascal e Lazarus: limites no Win64

A resposta curta para aquele ticket de suporte é sim, com limites. O HotPDF 2.730.0 compila no Free Pascal 3.2.2 e Lazarus 4.6 para Win64, e os caminhos centrais de create, load e save funcionam. O que não vem junto é qualquer coisa que dependa de um objeto codec nativo com link estático ou dos anonymous methods do Delphi

A pergunta costuma chegar sempre do mesmo jeito: uma equipe padroniza no 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 é questão de sintaxe. A parte interessante é o que o port expõe sobre onde a biblioteca estava silenciosamente acoplada a uma única toolchain, e neste caso o acoplamento mora em dois lugares muito específicos: o ABI de arquivos de objeto dos codecs embutidos e os recursos do compilador escondidos atrás de um símbolo de versão

Uma matriz de capacidades comparando o build Delphi do HotPDF com o build Free Pascal 3.2.2 e Lazarus 4.6 para Win64, mostrando quais caminhos de documento são compartilhados e quais APIs de codec, compressão, renderização paralela e anonymous methods chegam a um stub que levanta exceção
Os caminhos centrais de create, load e save são idênticos nos dois builds, e a lacuna está inteiramente nos codecs com link estático e nas APIs de anonymous methods

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

O HotPDF compila sob Free Pascal apenas no modo Delphi, e apenas quando os diretórios de units da LCL do Lazarus estão no search path. Nada disso é negociável. O HotPDF.inc alterna o compilador com {$MODE DELPHI} e {$H+} dentro do bloco {$IFDEF FPC} e recusa qualquer coisa mais antiga com um {$FATAL} quando FPC_FULLVERSION está abaixo de 30202, então uma instalação 3.0.x falha de forma ruidosa em vez de produzir uma unit quebrada. O pacote de runtime do Lazarus HotPDFLaz.lpk codifica o resto: LCL como pacote obrigatório e -Mdelphi como opção customizada

A exigência da LCL surpreende quem só quer saída de console, mas ela é estrutural. O HPDFFPCCompat fornece os tipos VCL do Delphi para os quais o Free Pascal não tem equivalente, mapeando TMetafile e TMetafileCanvas para classes de bitmap e canvas da LCL e apelidando TRichEdit de TMemo, enquanto o HPDFDoc apelida TPNGObject de Graphics.TPortableNetworkGraphic. Trate esses como shims de compilação, não paridade de recursos: uma classe metafile sustentada por um bitmap mantém a unit compilando, não faz os caminhos de metafile se comportarem como no 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 do lazutils

Por que D2009+ não pode servir de version gate

É tentador tratar o build Free Pascal como um compilador moderno e simplesmente definir o símbolo de recursos mais novo do Delphi. O HotPDF não o faz, e o motivo vale ser dito com clareza: D2009+ não significa apenas strings Unicode, ele também filtra units cuja API pública é expressa com anonymous methods. O Free Pascal 3.2.2 não suporta nem os anonymous methods do Delphi nem essas APIs, então pegar o símbolo emprestado arrastaria código que não compila. A cláusula uses do HPDFDoc por isso carrega duas caudas condicionais separadas, e a sobreposição entre elas é deliberada, não acidental

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

Por que os codecs nativos param no linker?

Porque são objetos COFF Win64 emitidos por uma toolchain específica, e nenhum dos linkers do Free Pascal no Win64 os consome: nem o linker interno, nem o caminho externo GNU ld. Isso é um problema de ABI de arquivo de objeto, não um problema Pascal, e nenhuma quantidade de source condicional conserta isso. A biblioteca toma a única rota honesta disponível. Cada diretiva {$L} que puxa um objeto codec estático está embrulhada em {$IFNDEF FPC}, então o build Free Pascal simplesmente as omite, e o HPDFFPCCodecStubs então fornece cada símbolo externo faltante como um stub que levanta exceção em vez de retornar

// 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 é longa, e lê-la diz exatamente quais capacidades são exclusivas do Delphi hoje: os pontos de entrada deflate do zlib-ng e zopfli, compressão e descompressão libjpeg, o codec JPEG 2000 OpenJPEG, o libtiff e seus inicializadores por compressão, encode e decode JBIG2, os pontos de entrada de transformação de cor do Little-CMS e as primitivas AES. A escolha de design por trás dos stubs importa mais que a lista. Um símbolo faltante em tempo de link te dá uma parede de referências indefinidas vinda de uma unit que você nunca tocou; um stub que levanta ENotSupportedException te dá um build que roda, uma mensagem nomeando o motivo e um stack trace apontando para o call site. Isso também significa que um build Free Pascal nunca produz silenciosamente bytes errados onde um build Delphi produziria os corretos. Note também o efeito de segunda ordem: rodar codecs de imagem não confiáveis em um processo isolado é uma decisão que só surge no build Delphi, porque um build Free Pascal não tem decodificador nativo in-process para isolar em primeiro lugar

No Delphi os objetos codec estáticos do HotPDF linkam e rodam nativamente, enquanto o build Free Pascal Win64 pula as diretivas de link e encaminha cada símbolo externo faltante para um stub que levanta uma exceção nomeada no call site
Pular as diretivas de link e transformar cada símbolo externo em stub converte uma parede de referências indefinidas em um build que roda e nomeia seus próprios limites

Compressão: a primeira linha a mudar é cmNone

Antes de portar qualquer outra coisa, defina Compression como cmNone. O THPDFCompressionMethod oferece exatamente dois valores, cmNone e cmFlateDecode, e o segundo vai direto para os pontos de entrada deflate que são stubs em um build Free Pascal. Verifique primeiro o modelo central de objetos com a compressão desligada, depois decida o que mais você precisa. Essa é a ordem que o smoke test embarcado usa: criar um documento de uma página sem compressão, recarregá-lo e verificar que a contagem de páginas voltou como um. A saída sem compressão é maior, e ainda é 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 stubado
    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 com a renderização paralela de páginas?

Ainda compila, ainda retorna bitmaps corretos e deixa de ser paralela. O THotPDF.RenderLoadedPagesParallel e o THotPDF.RenderLoadedPagesParallelOrdered são construídos sobre TThread.CreateAnonymousThread com uma closure inline procedure, que o Free Pascal 3.2.2 não consegue expressar, então o ramo Free Pascal roda um fallback serial determinístico: percorre os índices de página em ordem, chama RenderLoadedPageToBitmap para cada um e conta os sucessos. O formato da API, o valor de retorno e o array de saída não mudam, e é isso que permite que uma única base de código compile dos dois jeitos

A mesma chamada de renderização paralela do HotPDF roda em worker threads sobrepostas sob o Delphi e percorre os índices de página serialmente sob o Free Pascal, com o record de info do pipeline reportando uma contagem de workers de um em vez de esconder o fallback
O ramo Free Pascal mantém o formato da API e o array de saída enquanto reporta contagem de workers de um, então código que já lê o record de info 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 em ordem de índice
  if Info.WorkerCount = 1 then
    LogSerialFallback(Rendered, Info.RequestedWorkerCount);

O fallback não é silencioso, e é essa a parte que vale projetar em volta. Ele preenche o THPDFParallelRenderPipelineInfo com honestidade: PageCount do pedido, RequestedWorkerCount ecoando o que você pediu, WorkerCount definido como 1 e as contagens completed e delivered correspondendo ao que realmente voltou. Código que já inspeciona Info para dimensionar uma barra de progresso ou um orçamento de memória continua funcionando e lê a verdade em vez de uma suposição. Se seu plano de throughput depende de do pipeline de renderização paralela e seu modelo de backpressure, esse plano é um plano Delphi; no Free Pascal, orce o custo single-threaded de renderizar uma página para bitmap multiplicado pela contagem de páginas

Qual build você realmente deve distribuir?

Escolha por capacidade, não por preferência. Se seu fluxo de trabalho é montagem de documentos, desenho de texto e vetores, preenchimento de formulários, load e save, o build Free Pascal no Win64 cobre, e você 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 throughput que depende de muitos cores, fique no Delphi ou C++Builder por enquanto. A fronteira é desenhada por um ABI de arquivo de objeto e um recurso de linguagem ausente, ambos visíveis no source em vez de enterrados em uma matriz de suporte, e ambos falham com um erro nomeado em vez de um resultado errado

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