Article technique

HotPDF sous Free Pascal : deflate, AES et limites de codecs

HotPDF se compile et s'exécute sous Free Pascal 3.2.2 avec Lazarus, et le résumé honnête de ce portage tient en deux phrases. La création, le chargement, la sauvegarde, la compression, la décompression, le chiffrement et le déchiffrement de documents fonctionnent tous sur des backends exclusivement Pascal, de sorte qu'une application Lazarus peut produire et consommer du vrai PDF sans aucune dépendance C. Les codecs d'images natifs optionnels non, car les objets Win64 préconstruits utilisent une saveur COFF qu'aucun des deux éditeurs de liens Free Pascal ne peut consommer, donc sur cette chaîne d'outils les points d'entrée se résolvent en stubs qui échouent fermés

Carte des capacités HotPDF sous Free Pascal : deflate, AES et backends de documents Pascal fonctionnels à côté de stubs de codecs d'images qui échouent fermés
Les fonctionnalités de document, de compression et de chiffrement tournent sur des backends exclusivement Pascal, tandis que les codecs d'images natifs se résolvent en stubs qui échouent fermés

Passer de « ça compile » à « ça marche » a pris un ensemble précis de corrections, et chacune est un piège qui rattrapera toute autre base de code Delphi migrant vers Free Pascal. Elles méritent d'être notées dans l'ordre où elles font mal

Pourquoi une unité qui compile ne prouve-t-elle rien ?

Parce qu'une unité Pascal peut référencer un symbole qui ne fera jamais rien d'utile et satisfaire quand même le compilateur. Au moment où les 113 unités de la bibliothèque construisaient proprement sous Free Pascal, les gestionnaires de conteneurs d'archives fonctionnaient réellement, vérifiés par un test de fumée qui ouvrait un CBZ et le convertissait en PDF. L'aplatissement de formulaires XFA ne fonctionnait pas du tout, car l'aplatissement doit gonfler le flux de paquets compressé /XFA et le point d'entrée deflate était encore un stub. Rien dans la sortie de construction ne distinguait ces deux cas

La règle qui en est sortie est courte. Avant d'écrire dans une note de version qu'une fonctionnalité fonctionne sur une nouvelle chaîne d'outils, écrivez une sonde d'exécution qui exerce la fonctionnalité de bout en bout sur cette chaîne d'outils. La couverture de compilation est un prérequis, jamais une preuve. Le tableau plus large de ce que couvre le portage se trouve dans les notes de support Win64 Free Pascal et Lazarus

Un raise dans un stub cdecl n'atteint pas l'appelant

Celle-ci mérite sa propre section car le symptôme est si trompeur. Les unités de stub exposent des points d'entrée C comme le ferait une bibliothèque statique, donc un stub ressemble à ceci

// Paraît raisonnable. Ne l'est pas.
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
  public name 'inflate';
begin
  raise ENotSupportedException.Create('codec unavailable');
end;

Sous Free Pascal pour Win64, cette exception ne se propage pas jusqu'à l'appelant. Il n'y a aucun gestionnaire try..except qui la voie, car le déroulement à travers une frontière cdecl déclarée de cette façon ne transporte pas la trame d'exception Pascal ; le processus se termine avec le code de sortie 217. Du côté de l'application, pas d'erreur, pas de message, pas de ligne de journal, juste un programme qui disparaît. C'est strictement pire qu'une réponse fausse, car une réponse fausse peut être gérée

Pourquoi une exception levée dans un stub cdecl termine un processus Free Pascal avec le code de sortie 217 et comment verrouiller le point d'entrée Pascal corrige cela
La trame d'exception Pascal ne peut pas se dérouler à travers la frontière cdecl, donc le processus meurt silencieusement ; la correction verrouille avant que le stub soit atteint

La correction tentante est de faire renvoyer au stub un code d'échec à la place, et pour inflate c'est juste car zlib a un retour d'erreur bien défini. C'est faux en général : un stub pour jpeg_read_header qui renvoie zéro dit à l'appelant de continuer avec une structure que personne n'a initialisée. La correction durable est de verrouiller au point d'entrée Pascal plutôt qu'à l'intérieur du stub en forme C, en utilisant la convention d'échec que cette API possède déjà

function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
  // Refuser avant que le stub soit atteint, avec la convention
  // d'échec de cette API plutôt qu'une exception à travers cdecl
  Bitmap := nil;
  Result := False;
  Exit;
{$ENDIF}
  Result := DecodeJPEGNative(Data, Bitmap);
end;

paszlib n'est pas zlib, et la différence est deux classes de documents

L'implémentation deflate Pascal disponible sous Free Pascal gère deux habillages : l'enveloppe zlib et le deflate brut. Elle ne gère pas l'habillage gzip, que zlib sélectionne par des valeurs windowBits de 16 à 31, et elle ne gère pas le mode de détection automatique que les valeurs 32 à 47 sélectionnent. HotPDF a besoin des deux. La voie d'import SVG sûre demande 31, et le chargeur a une échelle de repli qui demande 47 quand l'habillage d'un flux est ambigu. Sautez l'un ou l'autre et toute une famille de documents cesse de s'ouvrir, avec une erreur de décodage qui pointe vers le flux plutôt que vers l'habillage manquant

Couverture windowBits de paszlib face à zlib : plages gzip et auto-détection manquantes pour l'import SVG HotPDF et le repli du chargeur
paszlib gère l'enveloppe zlib et le deflate brut, mais HotPDF a aussi besoin de windowBits 31 et 47, donc la couche d'adaptation doit fournir elle-même l'habillage gzip

Il y a une seconde incompatibilité plus acérée. L'enregistrement z_stream que paszlib déclare n'a pas la même disposition mémoire que celui du C : son champ msg est une short string plutôt qu'un pointeur, et total_in et total_out sont sur 64 bits là où l'ABI C a des mots machine. Un enregistrement d'appelant ne peut donc pas être passé tel quel. L'arrangement qui fonctionne est de garder l'état paszlib derrière le pointeur state que l'enregistrement public réserve déjà, et de copier les champs publics en entrée et en sortie autour de chaque appel. Le CRC gzip et le suffixe de longueur de huit octets sont pris en compte dans cette même couche d'adaptation, qui est l'endroit naturel pour eux puisqu'elle possède déjà la décision d'habillage

Passer un tableau dynamique à un paramètre var non typé

C'est le bug le plus susceptible de se trouver dans votre code en ce moment. Quand vous passez un tableau dynamique à un paramètre var non typé, ce que l'appelé reçoit est l'adresse de la variable tableau, qui est l'adresse d'un pointeur, pas l'adresse de la charge utile. Donc une lecture dedans écrase la variable elle-même et tout ce qui est à côté

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

  // Faux : remet l'adresse de la variable FBuffer
  FStream.Read(FBuffer, Length(FBuffer));

  // Juste : remet l'adresse du premier octet de charge utile
  FStream.Read(FBuffer[0], Length(FBuffer));
end;

Sous Delphi, la forme fausse semble fréquemment fonctionner, car ce qu'elle corrompt est un emplacement de pile adjacent que rien ne lit ensuite. Sous Free Pascal, la même ligne provoque une erreur de segmentation dès la première utilisation. Ce qui la rend si difficile à repérer à l'œil est que les tableaux statiques n'ont pas ce problème, puisque une variable de tableau statique est sa propre charge utile, donc les deux orthographes sont correctes dans le même fichier selon la déclaration à quelques centaines de lignes de là

Conteneurs ZIP sans System.Zip

Free Pascal n'a pas d'équivalent de l'unité zip de la RTL, et l'alternative disponible a à la fois une surface d'API différente et aucun soutien du chiffrement hérité que d'anciens formats de conteneurs utilisent encore, donc un petit lecteur interne à la bibliothèque s'est révélé plus court que de s'y adapter. Deux détails de format ont coûté du temps et sont faciles à rater

Le premier est l'octet de contrôle de l'en-tête de chiffrement. Son douzième octet est normalement l'octet haut du CRC, mais quand le bit 3 du drapeau à usage général est activé, ce qui signifie que les tailles vivent dans un descripteur de données final et que le CRC n'est pas encore connu, l'octet de contrôle vient de l'octet haut de l'heure de modification à la place. N'implémentez que la forme CRC et chaque archive écrite en mode flux refuse un mot de passe correct. Le second est le champ extra ZIP64 : ses trois champs 64 bits apparaissent dans un ordre fixe mais ne sont écrits que lorsque le champ 32 bits correspondant est saturé, donc les lire à des décalages fixes fonctionne sur les archives que vous avez testées et échoue sur la suivante. Analysez-les positionnellement selon quels champs 32 bits sont saturés

Une commodité qui vaut la peine d'être connue : le flux de décompression Free Pascal prend un second argument de constructeur qui saute l'en-tête zlib, ce qui est exactement ce dont les entrées ZIP ont besoin puisqu'elles stockent du deflate brut. Cette voie ne touche pas du tout la couche d'adaptation zlib de la bibliothèque, donc elle n'est pas affectée par le backend C manquant

Transparence des glyphes couleur sous la LCL

Lire le canal alpha d'un glyphe couleur rastérisé est le seul détail graphique sans traduction directe. La classe PNG de la LCL n'a aucun accesseur de scanline qui expose l'alpha, et affecter un PNG à un bitmap le jette, donc un émoji couleur arrive entièrement opaque et se compose avec une boîte noire derrière lui. La voie qui fonctionne est l'image d'interface : la créer depuis le PNG, puis lire les pixels par l'accesseur de couleur, en se rappelant que ses composantes sont sur 16 bits et doivent être décalées de huit vers le bas pour devenir des octets. Cette surface utilise aussi l'ordre naturel de lignes de haut en bas, donc l'inversion Height - 1 - Y dont le code de scanline VCL a besoin doit être retirée plutôt que portée

Deux notes de système de construction avant de rapporter un bug

Une reconstruction complète échoue parfois avec un symbole non défini dont le nom se termine par un suffixe $crc et une valeur hexadécimale. Ce suffixe est calculé à partir des types de paramètres, et il ne correspond pas quand une construction compile une unité contre deux versions d'interface différentes dans la même passe. Relancer la construction le fait disparaître ; la signature n'est pas fausse

Deuxième point, Free Pascal 3.2.2 n'a pas de méthodes anonymes, donc partout où la bibliothèque utilisait des closures pour câbler un pipeline parallèle, la construction Free Pascal prend un repli série déterministe à la place. La sortie est identique, le débit non ; si vous dépendez du rendu de pages parallèle, c'est une raison de rester sous Delphi pour le moment, et la conception du pipeline est décrite dans l'article sur le pipeline de rendu parallèle. La situation des codecs d'images est l'autre endroit où le choix de la chaîne d'outils change les capacités plutôt que seulement la vitesse, donc un déploiement Lazarus doit planifier ses formats d'images en conséquence ; la matrice actuelle par chaîne d'outils est sur la page produit du HotPDF Delphi PDF component