Article technique

Annotations de balisage de texte avec les QuadPoints de PDFium en Delphi

Le composant PDFium permet de créer des annotations de balisage de texte — c'est-à-dire de surbrillance, de soulignement, de rature et de soulignement ondulé — via la méthode TPdf.CreateAnnotation : il suffit de définir HasAttachmentPoints := True sur l'enregistrement TPdfAnnotation et de renseigner son quadrilatère AttachmentPoints, le composant écrivant alors l'entrée QuadPoints définie dans la norme ISO 32000-1 §12.5.6.10. C'est là toute l'API. La raison d'être de cet article tient à ce qui se déroule en coulisses, car la chaîne d'appels bruts de PDFium présente un mode d'échec particulièrement déroutant : la fonction FPDFAnnot_SetAttachmentPoints renvoie faux sur une annotation nouvellement créée, systématiquement, sans code d'erreur ni explication. C'est le pendant, côté création, de notre article sur la lecture et la révision des annotations existantes, qui traite du sens inverse sur les mêmes structures

Le scénario de débogage est toujours identique. Vous créez une annotation de surbrillance, vous appelez le modificateur de points d'attache avec l'index 0, la fonction renvoie faux, et vous commencez à douter de vos coordonnées. Vous permutez les points, inversez l'axe Y, passez des coordonnées de page à celles du périphérique. Rien n'y fait, car les coordonnées n'ont jamais été le problème. La difficulté vient de la sémantique des index de l'API C, et une fois comprise, la correction tient en deux lignes

Ce que signifient les QuadPoints dans l'ISO 32000-1

QuadPoints est un tableau de 8x n nombres décrivant n quadrilatères, et la norme ISO 32000-1 §12.5.6.10 l'exige pour chaque annotation de balisage de texte : chaque quadrilatère délimite un mot ou un groupe de mots contigus auquel s'applique la surbrillance, le soulignement ou la rature. L'entrée Rect de l'annotation existe toujours, mais pour les sous-types de balisage, elle ne fait que borner la région ; les quadrilatères (quads) sont ce que le moteur de rendu dessine réellement. On utilise un quadrilatère plutôt qu'un rectangle car le texte peut être pivoté ou incliné, ainsi les quatre coins sont stockés sous forme de quatre points indépendants : x1 y1 x2 y2 x3 y3 x4 y4

L'ordre de ces quatre points est le point de divergence entre la spécification et les implémentations réelles. Le texte de la spécification décrit les points en traçant le quadrilatère dans le sens inverse des aiguilles d'une montre, mais le moteur de rendu d'Adobe l'a toujours interprété selon un tracé en Z : d'abord le bord supérieur de gauche à droite, puis le bord inférieur de gauche à droite. Comme tous les auteurs ont testé leurs fichiers par rapport à Acrobat, pratiquement tous les moteurs de rendu, y compris PDFium, suivent ce tracé en Z, et les fichiers qui suivraient la formulation littérale de la spécification s'affichent sous forme de surbrillances écrasées ou déformées dans certains lecteurs. La structure FS_QUADPOINTSF de PDFium code précisément cette convention : (x1,y1) is the top-left corner, (x2,y2) top-right, (x3,y3) bottom-left, (x4,y4) bottom-right, in page coordinates where Y grows upward. Follow that order and be done; renderers are lenient about many things, but a scrambled quad is not one of them

Pourquoi FPDFAnnot_SetAttachmentPoints renvoie-t-elle faux ?

La fonction FPDFAnnot_SetAttachmentPoints échoue sur une nouvelle annotation car son rôle est de remplacer le quadrilatère à un index donné, et une annotation nouvellement créée ne possède aucun quadrilatère à remplacer. Sa signature prend un handle d'annotation, un quad_index et les points ; l'index 0 ne signifie pas « le premier emplacement, à créer si nécessaire », mais « le quadrilatère existant numéro 0 ». Lorsque FPDFAnnot_CountAttachmentPoints renvoie 0, il n'y a pas de tel quadrilatère et l'appel renvoie faux. La fonction qui crée un emplacement est FPDFAnnot_AppendAttachmentPoints. Toute annotation créée via FPDFPage_CreateAnnot démarre avec un compteur à zéro, ainsi le code de création doit d'abord appeler Append, et seules les mises à jour suivantes peuvent appeler Set

// Inside the component's annotation writer (v1.79.1+):
// a new annotation has no quad slots yet, so Append creates
// the first one; Set only replaces a slot that already exists
if FPDFAnnot_CountAttachmentPoints(Annotation) = 0 then
  Check(FPDFAnnot_AppendAttachmentPoints(Annotation, QuadPoints) <> 0,
    'Cannot set attachment points')
else
  Check(FPDFAnnot_SetAttachmentPoints(Annotation, 0, QuadPoints) <> 0,
    'Cannot set attachment points');

Le même schéma s'applique si vous appelez les fonctions C exportées directement, ce que le composant vous permet de faire puisque tous les points d'entrée FPDFAnnot_* sont disponibles dans PDFium.pas. Chaque fois que vous détenez un handle FPDF_ANNOTATION et souhaitez écrire des quadrilatères, interrogez d'abord FPDFAnnot_CountAttachmentPoints et orientez le traitement en conséquence. Si vous cherchez pourquoi « FPDFAnnot_SetAttachmentPoints renvoie faux », ce branchement basé sur le décompte est très probablement la solution

Créer une surbrillance avec TPdf.CreateAnnotation

Le composant prenant en charge pour vous le choix entre Append et Set, la création d'une surbrillance se résume à renseigner un enregistrement. L'exemple ci-dessous crée une page A4 et place une surbrillance jaune semi-transparente sur une région de 200 par 20 points ; notez que le quadrilatère suit l'ordre en Z décrit plus haut, et que le Rectangle est configuré pour englober le quadrilatère, garantissant ainsi le bon comportement des lecteurs qui effectuent des tests de collision par rapport au rectangle Rect

var
  Pdf: TPdf;
  A: TPdfAnnotation;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;
    Pdf.AddPage(0, 595, 842);

    FillChar(A, SizeOf(A), 0);
    A.Subtype := anHighlight;
    A.HasColor := True;
    A.Color := clYellow;
    A.ColorAlpha := $80;                     // 50% opacity
    A.HasAttachmentPoints := True;
    A.AttachmentPoints[1].X := 50;  A.AttachmentPoints[1].Y := 700; // top-left
    A.AttachmentPoints[2].X := 250; A.AttachmentPoints[2].Y := 700; // top-right
    A.AttachmentPoints[3].X := 50;  A.AttachmentPoints[3].Y := 680; // bottom-left
    A.AttachmentPoints[4].X := 250; A.AttachmentPoints[4].Y := 680; // bottom-right
    A.Rectangle.Left := 50;  A.Rectangle.Top := 700;
    A.Rectangle.Right := 250; A.Rectangle.Bottom := 680;
    A.ContentsText := 'Highlighted region';
    Pdf.CreateAnnotation(A);

    Pdf.SaveAs('highlighted.pdf');
  finally
    Pdf.Free;
  end;
end;

Changer de sous-type ne prend qu'une ligne. anUnderline, anStrikeout et anSquiggly acceptent la même structure d'enregistrement, quadrilatères compris, car l'ISO 32000-1 traite ces trois types comme une même famille d'annotations, qui ne diffèrent que par la manière dont la zone du quadrilatère est décorée. Les sous-types qui ne correspondent pas à du balisage de texte, comme anSquare, anCircle et anText, se positionnent uniquement d'après le rectangle Rectangle ; laissez HasAttachmentPoints à False pour ceux-là, et la logique des quadrilatères ne s'exécutera pas

Pourquoi AttachmentPoints[0] compile-t-il sous Delphi mais échoue-t-il sous FPC ?

La structure TQuadrilateralPoint est déclarée sous la forme array [1..4] of TPdfPoint, un tableau indexé à partir de 1, ce qui piège quiconque a l'habitude d'indexer à partir de 0. Si vous écrivez A.AttachmentPoints[0], le compilateur dcc32 de Delphi compilera le code sans se plaindre, car la vérification des limites est désactivée par défaut ; à l'exécution, l'expression lira ou écrira silencieusement dans la mémoire située juste avant le tableau, qui correspond à un champ adjacent dans l'enregistrement TPdfAnnotation. Votre surbrillance se retrouve avec un coin erroné, ou un champ voisin est corrompu, sans qu'aucune exception ne soit levée. Free Pascal a détecté ce bogue exact dans nos propres exemples de code lors du portage vers Lazarus : fpc réalise une vérification des limites à la compilation pour les index constants et a rejeté d'emblée l'utilisation de AttachmentPoints[0..3], ce qui a permis de découvrir conjointement l'erreur d'index et le bogue de la bibliothèque entre Set et Append

Deux réflexes en découlent. Indexez le quadrilatère de 1 à 4, en respectant l'ordre des coins de l'exemple ci-dessus, et compilez votre code d'annotation au moins une fois avec la vérification des limites activée — soit avec {$R+} sous Delphi, soit sous n'importe quelle configuration fpc — avant de le valider. Une compilation dcc32 réussie par défaut n'est pas une preuve de la justesse des index ; elle montre simplement que rien n'a planté sur la mémoire présente à cet endroit

Obtenir les coordonnées du quadrilatère à partir du texte réel

Les rectangles codés en dur conviennent pour une démonstration, mais les surbrillances réelles ciblent de vrais caractères, et les coordonnées doivent provenir de la géométrie de la page de texte de PDFium plutôt que d'estimations. Les routines décrites dans notre guide d'extraction de texte avec le composant PDFium vous fournissent des rectangles de délimitation par caractère dans le même espace de coordonnées de page que les quadrilatères, de sorte qu'une correspondance de recherche se convertit directement en points d'angle : gauche du premier caractère, droite du dernier, haut et bas à partir des limites de la ligne. Si vous générez le texte vous-même et devez savoir où les lignes vont se positionner avant qu'elles n'existent, l'article sur la mesure du texte et le retour à la ligne présente la méthode pour calculer ces limites en amont

Une limite honnête : l'enregistrement TPdfAnnotation porte un seul TQuadrilateralPoint, ainsi un appel à CreateAnnotation écrit un seul quadrilatère. Une sélection s'étendant sur trois lignes requiert trois quadrilatères (un par ligne), conformément au §12.5.6.10, et vous disposez de deux approches pour y parvenir. La méthode simple consiste à créer une annotation par ligne, ce qui s'affiche correctement partout et préserve l'API du composant. La méthode compacte, soit une seule annotation portant trois quadrilatères, nécessite de créer l'annotation via le composant puis d'appeler vous-même la fonction exportée FPDFAnnot_AppendAttachmentPoints pour les deuxième et troisième quadrilatères, ce qui fonctionne précisément car Append crée des emplacements au lieu de les remplacer. Ne tentez pas de définir plusieurs quadrilatères en répétant des appels à SetAttachmentPoints ; tout index dépassant le décompte actuel renverra simplement faux, pour la même raison que l'index 0 sur une nouvelle annotation

Après l'écriture, validez le résultat dans un vrai lecteur plutôt que de faire confiance aux codes de retour : ouvrez le fichier dans Acrobat ou tout lecteur basé sur PDFium et confirmez que le balisage se positionne sur le texte, respecte l'opacité ciblée et survit à un cycle d'enregistrement et de réouverture. Les types d'annotations, la gestion des quadrilatères et la logique d'écriture présentés ici font tous partie du composant standard PDFium Component pour Delphi, C++Builder et Lazarus ; la page produit héberge la référence complète de l'API d'annotation aux côtés des autres fonctionnalités de la bibliothèque