Article technique

Revue des annotations PDF dans Delphi avec PDFium Component

Un réviseur ouvre le même contrat dans votre visualiseur Delphi et dans Adobe Acrobat. Le panneau de commentaires d'Acrobat liste quatorze éléments ; votre panneau de revue en montre onze. Votre boucle n'est pas fautive. Les trois manquants sont deux réponses, objets annotation complets liés à leurs parents par des références in-reply-to, et une fenêtre popup appartenant à une note que vous avez déjà comptée. Les annotations PDF ne sont pas une liste plate de rectangles colorés : ISO 32000-1 §12.5 définit un réseau de dictionnaires avec sous-types, flags, flux d'apparence et relations parent-enfant, et un panneau de revue qui ignore ces relations continuera de diverger de tous les autres lecteurs du client. Ce parcours construit un workflow de revue d'annotations avec PDFium Component, composant VCL/LCL fondé sur PDFium pour Delphi, C++Builder et Lazarus, autour des endroits où les documents réellement annotés résistent.

Pourquoi votre compteur ne correspond jamais au panneau d'Acrobat

Acrobat présente une vue éditorialisée : annotations de markup groupées en fils de réponses, popups repliés dans leurs parents. Le tableau brut des annotations de chaque page contient à la fois plus et moins que cette vue ne le suggère.

  • Les annotations popup sont des objets séparés attachés à une note parent : les compter double chaque note collante.
  • Les réponses sont des annotations Text complètes qui référencent un parent ; filtrer seulement les marques visibles supprime silencieusement le fil de discussion.
  • Les flags Hidden et NoView retirent une annotation de l'affichage, pas du tableau ; les tests de flags appartiennent donc à l'indexation.
  • Les annotations Link vivent dans le même tableau, et aucun réviseur ne considère un lien hypertexte comme un commentaire.

Fixez la règle de comptage avant d'écrire le code et placez-la dans la spécification, car « pourquoi votre panneau affiche-t-il un nombre différent d'Acrobat » est le premier ticket de support que génère une fonction de revue.

Indexer tout une fois, puis ne jamais reparser une page

Filtrer par auteur, type ou page ne doit pas relancer un parse des objets de page : sur un document de 300 pages fortement annoté, chaque changement de liste déroulante deviendrait plusieurs secondes de latence. Le composant expose AnnotationCount et la propriété indexée Annotation[] sur la page actuellement chargée, et l'enregistrement TPdfAnnotation retourné contient tout ce dont une liste a besoin : Subtype, Flags, Color, Rectangle, ContentsText et AuthorText. Construisez l'index en une passe à l'ouverture :

procedure TReviewPanel.BuildIndex;
var
  PageNo, i: Integer;
  A: TPdfAnnotation;
begin
  FItems.Clear;
  for PageNo := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := PageNo;
    for i := 0 to Pdf.AnnotationCount - 1 do
    begin
      A := Pdf.Annotation[i];
      // Keep reviewer-relevant subtypes only; record the page and
      // index pair because all later edits are addressed by it
      if A.Subtype in [anText, anHighlight, anInk] then
        FItems.Add(TReviewItem.Create(PageNo, i,
          A.AuthorText, A.ContentsText, A.Rectangle, A.Color));
    end;
  end;
end;

Le couple à souligner est (PageNo, i). Chaque mutation ultérieure, recolorer ou supprimer, est adressée par numéro de page plus index d'annotation, et les index se décalent quand une annotation est supprimée. Prévoyez de reconstruire les entrées de la page affectée après toute suppression plutôt que de patcher les index en place ; la reconstruction coûte des millisecondes, tandis qu'un index obsolète supprime le commentaire du mauvais réviseur.

Le threading des réponses mérite une place dans le design d'index même si la première version ne fait que compter les réponses. Groupez les éléments par référence parent dès la construction afin que le panneau puisse plus tard replier un fil comme Acrobat ; reconstruire le regroupement paresseusement pendant le défilement réouvre des pages que vous avez déjà payées à parser. Le même raisonnement vaut pour la géométrie : le Rectangle de chaque enregistrement est en espace page, convertissez-le en coordonnées de vue dans un helper unique. Les panneaux de revue accumulent des bugs de coordonnées quand sélection, hit-testing et peinture portent chacun leur propre arithmétique zoom-rotation ; un seul chemin de conversion garde surbrillance, entrée de liste et cible de clic sur la même encre.

Recolorer le markup et le veto du flux d'apparence

Changer un surligneur de jaune à ambre semble être une ligne, et parfois ça l'est. La complication est ISO 32000-1 §12.5.5 : lorsqu'une annotation porte un flux d'apparence /AP, les lecteurs conformes rendent ce flux préconstruit, et l'entrée couleur du dictionnaire devient décorative. Comme Acrobat écrit des flux d'apparence pour presque tout ce qu'il crée, la plupart des annotations provenant des clients sont dans cet état. La recoloration est un read-modify-write par Annotation[], et le composant rapporte honnêtement un veto moteur en levant EPdfError :

A := Pdf.Annotation[Item.Index];
A.HasColor := True;
A.Color := $0000B0FF;       // amber
A.ColorAlpha := 160;
try
  Pdf.Annotation[Item.Index] := A;
except
  on EPdfError do
  begin
    // The annotation owns a pre-rendered /AP stream; the dictionary
    // color alone cannot change what viewers paint
    Item.AppearanceLocked := True;
    StatusBar.SimpleText := 'Color is fixed by the annotation appearance';
  end;
end;

Gérez cette exception à chaque fois. Sans garde, votre panneau affiche la nouvelle couleur dans sa liste alors que la page continue de peindre l'ancienne, et la divergence ressort des semaines plus tard comme « votre visualiseur ignore mes modifications ». Quand l'apparence est verrouillée, les options honnêtes consistent à recolorer votre overlay de sélection plutôt que l'annotation, ou à signaler l'élément comme verrouillé par apparence dans l'UI.

Supprimer des annotations sans laisser de fantômes

DeleteAnnotation détache l'objet de la page courante, mais ne reconstruit pas l'apparence de page mise en cache ; repeignez immédiatement et le surlignage supprimé reste visible. Re-rendez le raster de page dans la même opération :

Pdf.PageNumber := Item.PageNo;
Pdf.DeleteAnnotation(Item.Index);   // raises EPdfError on failure
Bmp := Pdf.RenderPage(0, 0, ViewWidth, ViewHeight, ro0, [reAnnotations]);
try
  PaintPageBitmap(Bmp);
finally
  Bmp.Free;  // RenderPage hands bitmap ownership to the caller
end;
RebuildPageEntries(Item.PageNo);  // indices after Item.Index shifted

Notez l'option reAnnotations dans l'appel de rendu : sans elle, le raster exclut toutes les annotations restantes, ce qui ressemble à une suppression massive pour l'utilisateur. Notez aussi Bmp.Free : la surcharge fonction de RenderPage transfère la propriété du bitmap à l'appelant ; ne pas libérer fuit un raster pleine page à chaque suppression.

Ajouter les marques de revue depuis votre UI

La création d'annotations passe par CreateAnnotation, qui prend un enregistrement TPdfAnnotation rempli : sous-type, rectangle, couleur, contenu, auteur, puis l'ajoute à la page courante. Les notes collantes (anText) sont le cas simple : position, contenu, auteur, terminé. Les annotations ink sont le piège : le rectangle de l'enregistrement ne fait que borner le dessin, et les traits réels sont des tableaux de points attachés séparément par l'appel ink du moteur (FPDFAnnot_AddInkStroke avec données FS_POINTF), capturés souris ou stylet trait par trait. Créer une annotation ink à partir d'un rectangle seul produit un gribouillis vide qui ne rend rien.

Décidez en même temps la politique d'auteur : chaque marque créée par votre UI doit porter un AuthorText cohérent, car le filtrage aval par réviseur n'est fiable que si les noms écrits aujourd'hui le sont.

Faire sortir la revue du visualiseur

Les données de revue valent lorsqu'elles quittent le visualiseur : résumé lu par le chef de projet sans ouvrir le fichier, ou CSV alimentant une feuille de suivi. Exportez depuis l'index, pas depuis un reparse, et gardez des références stables : numéro de page plus rectangle d'annotation survivent mieux aux allers-retours qu'un index de tableau que la prochaine suppression invalide.

Une ligne d'export défendable contient page, sous-type, auteur, information de création si présente, texte du contenu et votre propre colonne de statut. Pour les documents arrivant de l'extérieur, il est utile de lancer la même indexation durant le triage d'ingestion ; l'article sur le workbench d'ingestion PDF montre ce modèle, et la navigation dans les champs de formulaire couvre le problème voisin des documents qui collectent des données plutôt que des commentaires.

FAQ

Pourquoi un surlignage que j'ai recoloré garde-t-il son ancienne couleur ?

L'annotation porte presque certainement un flux d'apparence /AP, que les lecteurs conformes peignent avant la couleur du dictionnaire (ISO 32000-1 §12.5.5). L'écriture de l'enregistrement par Annotation[] lève alors EPdfError : traitez l'exception comme la source de vérité, pas la couleur que vous vouliez définir.

Pourquoi la page montre-t-elle encore une annotation supprimée ?

DeleteAnnotation met à jour le modèle documentaire, pas le raster en cache. Re-rendez la page avec RenderPage après une suppression réussie, et reconstruisez les entrées d'index de cette page car les indices d'annotation se décalent vers le bas.

Les annotations aplaties apparaissent-elles dans le tableau d'annotations ?

Non. L'aplatissement convertit les apparences d'annotation en contenu de page ordinaire ; elles cessent donc d'être des objets annotation. Si un fichier client montre du markup visible mais AnnotationCount vaut zéro, l'explication habituelle est un flattening en amont : il ne reste rien à revoir programmatiquement.

La surface d'API annotation utilisée ici, énumération, création, recoloration, suppression et options de rendu qui gardent l'affichage honnête, est livrée avec PDFium Component pour Delphi, C++Builder et Lazarus/FPC.