Article technique

HotPDF sur Free Pascal et Lazarus : limites Win64

La réponse courte à ce ticket de support est oui, avec des limites. HotPDF 2.730.0 se construit avec Free Pascal 3.2.2 et Lazarus 4.6 pour Win64, et les chemins fondamentaux de création, de chargement et de sauvegarde fonctionnent. Ce qui ne suit pas, c’est tout ce qui repose sur un objet codec natif à édition de liens statique ou sur les méthodes anonymes de Delphi

La question arrive généralement de la même manière : une équipe standardise Lazarus pour un outil multiplateforme, ou hérite d’une base de code Free Pascal, et veut le même composant PDF qu’elle licencie déjà pour Delphi. Porter une bibliothèque Delphi mature est rarement une affaire de syntaxe. La partie intéressante est ce que le portage révèle sur les endroits où la bibliothèque était discrètement couplée à une seule chaîne d’outils, et en l’occurrence ce couplage se trouve à deux endroits très précis : l’ABI des fichiers objets des codecs embarqués, et les fonctionnalités du compilateur cachées derrière un symbole de version

Une matrice de capacités comparant la construction Delphi de HotPDF avec la construction Free Pascal 3.2.2 et Lazarus 4.6 Win64, montrant quels chemins documentaires sont partagés et quelles API de codec, de compression, de rendu parallèle et de méthodes anonymes aboutissent à un stub qui lève
Les chemins fondamentaux de création, de chargement et de sauvegarde sont identiques sur les deux constructions, et l’écart se situe entièrement dans les codecs à lien statique et les API à méthodes anonymes

Ce dont Free Pascal 3.2.2 a besoin avant que HPDFDoc ne compile

HotPDF compile sous Free Pascal uniquement en mode Delphi, et uniquement quand les répertoires d’unités LCL de Lazarus sont sur le chemin de recherche. Rien de tout cela n’est négociable. HotPDF.inc bascule le compilateur avec {$MODE DELPHI} et {$H+} à l’intérieur de son bloc {$IFDEF FPC} et refuse tout compilateur plus ancien avec un {$FATAL} quand FPC_FULLVERSION est inférieure à 30202, si bien qu’une installation 3.0.x échoue bruyamment au lieu de produire une unité cassée. Le paquet d’exécution Lazarus HotPDFLaz.lpk encode le reste : LCL comme paquet requis et -Mdelphi comme option personnalisée

L’exigence LCL surprend celles et ceux qui ne veulent qu’une sortie console, mais elle est structurelle. HPDFFPCCompat fournit les types VCL de Delphi pour lesquels Free Pascal n’a pas d’équivalent, en mappant TMetafile et TMetafileCanvas sur les classes bitmap et canvas de la LCL et en faisant un alias de TRichEdit vers TMemo, tandis que HPDFDoc crée un alias de TPNGObject vers Graphics.TPortableNetworkGraphic. Traitez-les comme des shims de compilation, pas comme une parité de fonctionnalités : une classe métafichier adossée à un bitmap garde l’unité compilable, elle ne rend pas les chemins métafichier identiques à leur comportement sur Delphi. Même le test de fumée sans GUI ajoute Interfaces, et le script de construction passe -Fu pour lcl\units\x86_64-win64 et le répertoire de sortie de lazutils

Pourquoi D2009+ ne peut pas servir de verrou de version

Il est tentant de considérer la construction Free Pascal comme celle d’un compilateur moderne et de définir simplement le symbole de fonctionnalité Delphi le plus récent. HotPDF ne le fait pas, et la raison mérite d’être énoncée clairement : D2009+ ne signifie pas uniquement les chaînes Unicode, il verrouille aussi les unités dont l’API publique est exprimée avec des méthodes anonymes. Free Pascal 3.2.2 ne prend en charge ni les méthodes anonymes de Delphi ni ces API, donc emprunter le symbole entraînerait du code qui ne peut pas compiler. La clause uses de HPDFDoc porte donc deux queues conditionnelles distinctes, et leur chevauchement est délibéré plutôt qu’accidentel

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

Pourquoi les codecs natifs s’arrêtent-ils au lieur ?

Parce que ce sont des objets COFF Win64 émis par une chaîne d’outils bien précise, et qu’aucun des deux lieurs de Free Pascal en Win64 ne les consommera : ni le lieur interne, ni la voie externe GNU ld. C’est un problème d’ABI de fichiers objets, pas un problème Pascal, et aucune quantité de source conditionnelle ne le corrige. La bibliothèque emprunte la seule voie honnête disponible. Chaque directive {$L} qui tire un objet codec statique est enveloppée dans {$IFNDEF FPC}, si bien que la construction Free Pascal les omet simplement, et HPDFFPCCodecStubs fournit alors chaque symbole externe manquant sous forme de stub qui lève au lieu de renvoyer

// 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;

Cette table de stubs est longue, et sa lecture vous dit exactement quelles capacités sont réservées à Delphi aujourd’hui : les points d’entrée deflate de zlib-ng et zopfli, la compression et la décompression libjpeg, le codec JPEG 2000 OpenJPEG, libtiff et ses initialiseurs par compression, l’encodage et le décodage JBIG2, les points d’entrée de transformation colorimétrique Little-CMS, et les primitives AES. Le choix de conception derrière les stubs compte plus que la liste. Un symbole manquant à l’édition de liens vous donne un mur de références indéfinies venant d’une unité que vous n’avez jamais touchée ; un stub qui lève ENotSupportedException vous donne une construction qui s’exécute, un message nommant la raison, et une trace de pile pointant vers le site d’appel. Cela signifie aussi qu’une construction Free Pascal ne produit jamais silencieusement des octets faux là où une construction Delphi produirait des octets corrects. Notez aussi l’effet de second ordre : exécuter les codecs d’images non fiables dans un processus isolé est une décision qui ne se pose que sur la construction Delphi, car une construction Free Pascal n’a aucun décodeur natif intra-processus à isoler au départ

Sous Delphi les objets codec statiques de HotPDF se lient et s’exécutent nativement, tandis que la construction Free Pascal Win64 saute les directives de lien et aiguille chaque symbole externe manquant vers un stub qui lève une exception nommée au site d’appel
Sauter les directives de lien et fournir un stub pour chaque symbole externe transforme un mur de références indéfinies en une construction qui s’exécute et nomme ses propres limites

Compression : la première ligne à changer est cmNone

Avant de porter quoi que ce soit d’autre, mettez Compression à cmNone. THPDFCompressionMethod offre exactement deux valeurs, cmNone et cmFlateDecode, et la seconde mène directement aux points d’entrée deflate qui sont des stubs dans une construction Free Pascal. Vérifiez d’abord le modèle d’objets fondamental avec la compression désactivée, puis décidez de ce dont vous avez besoin d’autre. C’est l’ordre que suit le test de fumée livré : créer un document d’une page non compressé, le recharger, et vérifier que le nombre de pages revient bien à un. Une sortie non compressée est plus volumineuse, et elle reste un PDF parfaitement valide

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 atteint un symbole remplacé par un 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.

Que devient le rendu parallèle des pages ?

Il compile toujours, renvoie toujours des bitmaps corrects, et cesse d’être parallèle. THotPDF.RenderLoadedPagesParallel et THotPDF.RenderLoadedPagesParallelOrdered sont bâties sur TThread.CreateAnonymousThread avec une fermeture procedure en ligne, ce que Free Pascal 3.2.2 ne sait pas exprimer, si bien que la branche Free Pascal exécute un repli séquentiel déterministe : elle parcourt les indices de pages dans l’ordre, appelle RenderLoadedPageToBitmap pour chacune, et compte les succès. La forme de l’API, la valeur de retour et le tableau de sortie restent inchangés, et c’est ce qui permet à une seule base de code de se construire des deux manières

Le même appel de rendu parallèle HotPDF tourne sur des threads ouvriers qui se chevauchent sous Delphi et parcourt les indices de pages en série sous Free Pascal, l’enregistrement d’info du pipeline signalant un nombre d’ouvriers de un plutôt que de cacher le repli
La branche Free Pascal garde la forme de l’API et le tableau de sortie tout en signalant un nombre d’ouvriers de un, si bien que le code qui lit déjà l’enregistrement d’info voit la vérité
var
  Bitmaps: THPDFBitmapArray;
  Info: THPDFParallelRenderPipelineInfo;
  Rendered: Integer;
begin
  Rendered := Pdf.RenderLoadedPagesParallel([0, 1, 2, 3], 150, 4,
    Bitmaps, Info);
  // Delphi : Info.WorkerCount vaut ce que le budget mémoire a permis
  // Free Pascal : Info.WorkerCount vaut toujours 1, pages dans l'ordre des indices
  if Info.WorkerCount = 1 then
    LogSerialFallback(Rendered, Info.RequestedWorkerCount);

Le repli n’est pas silencieux, et c’est la partie qui mérite d’être conçue autour. Il remplit THPDFParallelRenderPipelineInfo honnêtement : PageCount issu de la requête, RequestedWorkerCount faisant écho à ce que vous avez demandé, WorkerCount mis à 1, et les compteurs de pages complétées et livrées correspondant à ce qui est réellement revenu. Le code qui inspecte déjà Info pour dimensionner une barre de progression ou un budget mémoire continue de fonctionner et lit la vérité plutôt qu’une hypothèse. Si votre plan de débit dépend du pipeline de rendu parallèle et de son modèle de contre-pression, ce plan est un plan Delphi ; sur Free Pascal, budgétez le coût monothread de rendu d’une page en bitmap multiplié par le nombre de pages

Quelle construction faut-il réellement livrer ?

Choisissez selon les capacités, pas selon les préférences. Si votre flux de travail est l’assemblage de documents, le dessin de texte et de vecteurs, le remplissage de formulaires, le chargement et la sauvegarde, la construction Free Pascal en Win64 le couvre, et vous devriez valider avec la compression désactivée avant d’activer quoi que ce soit. Si cela implique des images JPEG ou JPEG 2000 ou TIFF ou JBIG2, des transformations colorimétriques ICC, une sortie compressée, ou un débit qui dépend de nombreux cœurs, restez pour l’instant sur Delphi ou C++Builder. La frontière est tracée par une ABI de fichiers objets et une fonctionnalité de langage manquante, toutes deux visibles dans la source plutôt qu’enfouies dans une matrice de support, et toutes deux en échec avec une erreur nommée plutôt qu’un résultat faux

Le paquet Free Pascal et Lazarus est livré dans la même distribution que les unités Delphi et C++Builder, si bien qu’une licence couvre les deux et que vous pouvez tester le chemin Lazarus contre vos propres documents avant de vous y engager ; la page produit du composant PDF HotPDF pour Delphi porte la matrice de support des compilateurs actuelle et la référence API complète