Une annotation PDF est un dictionnaire attaché à une page, pas une marque dessinée dessus. ISO 32000-1 §12.5 définit environ deux douzaines de sous-types, et chacun porte un /Subtype, un rectangle en coordonnées de page, un jeu d'indicateurs et généralement un flux d'apparence qui décide de ce qu'un visualiseur peint réellement. Les sous-types ne signifient pas tous la même chose pour une personne qui relit un document. Un Highlight et un tracé Ink sont des commentaires ; un Link est de la navigation ; un Popup est la petite fenêtre qui s'ouvre quand vous cliquez sur une note, stockée comme objet à part et désignée par un parent. Les réponses sont de véritables annotations Text qui référencent le commentaire auquel elles répondent par une entrée in-reply-to. Le tableau d'annotations au niveau de la page n'est donc pas la liste de commentaires du relecteur. C'est un sac plat contenant des commentaires, la tuyauterie qui les relie, et plusieurs choses qu'aucun relecteur n'appellerait un commentaire. Un panneau qui traite ce tableau comme la liste de commentaires sera en désaccord avec tous les autres visualiseurs qu'utilise le client
Bâtir un flux de revue d'annotations sur PDFium Component, le composant VCL/LCL fondé sur PDFium pour Delphi, C++Builder et Lazarus, revient à se concentrer sur les points où cet écart entre le tableau brut et la vue humaine cause des ennuis : compter, indexer, recolorer des marques que le moteur a déjà figées, supprimer sans laisser de fantômes, et ajouter vos propres marques
Pourquoi votre compte ne correspond jamais au volet de commentaires d'Acrobat
Ouvrez un contrat annoté côte à côte dans votre visualiseur et dans Acrobat et les totaux s'accordent rarement. Acrobat montre une vue soignée : le balisage regroupé en fils de réponses, les popups repliés dans les notes auxquelles ils appartiennent, les liens et les widgets de formulaire laissés de côté. Le tableau brut contient tout cela sans distinction, si bien qu'un comptage naïf surestime d'un côté et sous-estime de l'autre en même temps
Les popups gonflent le total, car chaque note adhésive arrive avec un objet Popup distinct et compter les deux double la note. Les réponses le dégonflent si vous filtrez sur les marques visibles, puisqu'une réponse est une annotation Text dont rien n'est peint tant que quelqu'un n'a pas déplié le fil, et l'écarter fait perdre la discussion. Les indicateurs Hidden et NoView retirent une annotation de l'écran sans la retirer du tableau, si bien qu'un comptage aveugle aux indicateurs inclut des marques que l'utilisateur ne peut pas voir. Les annotations Link se trouvent dans le même tableau que les commentaires et n'ont leur place ni dans le compte ni dans la liste. Décidez de la règle de comptage avant d'écrire la boucle, et notez la décision, car « pourquoi votre panneau affiche-t-il un nombre différent d'Acrobat » est le premier ticket que gagne une fonction de revue
Indexez tout une fois, puis ne réanalysez plus jamais une page
Une règle de conception commande tout ce qui suit : filtrer par auteur, par type ou par page ne doit jamais réanalyser les objets de page. Sur un document de 300 pages fortement annoté, réanalyser à chaque changement de liste déroulante transforme le panneau en quelque chose qui bégaie pendant des secondes. Le composant expose AnnotationCount et la propriété indexée Annotation[], toutes deux portées sur la page actuellement chargée, et l'enregistrement TPdfAnnotation qu'elles rendent porte ce dont une liste a besoin : Subtype, Flags, Color, Rectangle, ContentsText, AuthorText. Le bon geste est de balayer chaque page une fois à l'ouverture et de tenir votre propre index plat :
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];
// Ne garder que les sous-types utiles au relecteur ; noter le couple
// page et index car toutes les éditions ultérieures y sont adressées
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 qui mérite d'être souligné est (PageNo, i). Toute mutation ultérieure, recoloration ou suppression, est adressée par numéro de page plus index d'annotation, et l'index est fragile : retirer une annotation renumérote tout ce qui la suit sur cette page. Prévoyez donc de reconstruire les entrées de la page concernée après toute suppression au lieu de rapiécer les numéros d'index sur place. La reconstruction coûte une milliseconde. Un index périmé, en revanche, supprime le commentaire du mauvais relecteur, ce qui est le genre de bug qui érode la confiance dans toute la fonction
Le fil de discussion mérite une place dans l'index même si votre première version se contente de compter les réponses au lieu de les afficher. Groupez les éléments par leur référence parente pendant que vous tenez la page ouverte, pour que le panneau puisse plus tard replier un fil comme le fait Acrobat. Reconstruire ce regroupement paresseusement pendant le défilement ruine tout l'intérêt d'indexer une fois, car cela rouvre des pages que vous avez déjà payé pour analyser. La géométrie demande la même discipline. Le Rectangle de chaque enregistrement est en espace page, et sa conversion en coordonnées de vue a sa place dans une seule aide partagée, pas éparpillée dans le code. Les panneaux développent des bugs de coordonnées quand la sélection, la détection de clic et le dessin inventent chacun leurs propres calculs de zoom et de rotation ; faites passer les trois par une conversion unique et un surlignage, sa ligne dans la liste et sa cible de clic restent épinglés sur la même encre
Recolorer le balisage et le veto du flux d'apparence
Faire passer un surlignage du jaune à l'ambre semble tenir en une ligne, et parfois c'est le cas. Le piège est ISO 32000-1 §12.5.5. Quand une annotation porte un flux d'apparence /AP, un visualiseur conforme peint ce flux préconstruit et traite l'entrée de couleur du dictionnaire comme une métadonnée morte. Acrobat écrit des flux d'apparence pour à peu près tout ce qu'il crée, si bien que la plupart des annotations qui arrivent des clients sont déjà dans cet état, et la couleur que vous avez si sûrement posée n'atteint jamais l'écran. La recoloration est un lire-modifier-écrire à travers la propriété Annotation[], et le composant est honnête sur le conflit : quand le moteur refuse qu'une couleur de dictionnaire prime sur une apparence cuite, l'écriture lève EPdfError
A := Pdf.Annotation[Item.Index];
A.HasColor := True;
A.Color := $0000B0FF; // ambre
A.ColorAlpha := 160;
try
Pdf.Annotation[Item.Index] := A;
except
on EPdfError do
begin
// L'annotation possède un flux /AP prérendu ; la couleur du
// dictionnaire seule ne peut pas changer ce que peignent les visualiseurs
Item.AppearanceLocked := True;
StatusBar.SimpleText := 'Color is fixed by the annotation appearance';
end;
end;
Interceptez cette exception à chaque fois, et traitez-la comme une information plutôt que comme un échec. Sautez la garde et votre panneau affiche joyeusement de l'ambre dans sa propre liste tandis que la page continue de peindre du jaune ; l'utilisateur le signale des semaines plus tard sous la forme « votre visualiseur ignore mes modifications », et vous passez un après-midi à ne pas réussir à le reproduire sur un fichier qui se trouve n'avoir aucun flux d'apparence. Une fois que vous savez que l'apparence est verrouillée, vous avez deux réponses honnêtes : recolorer votre propre surcouche de sélection au lieu de l'annotation, pour que le relecteur voie au moins le surlignage qu'il a choisi, ou marquer la ligne comme verrouillée par son apparence pour que personne n'attende que le changement tienne
Supprimer des annotations sans laisser de fantômes
DeleteAnnotation retire l'objet de l'arbre d'annotations de la page courante, mais il laisse tranquille la trame de page en cache. Peignez juste après l'appel et le surlignage supprimé est encore à l'écran, assis dans un bitmap qui ne correspond plus au modèle de document derrière lui. Le correctif consiste à traiter le nouveau rendu comme une partie de la suppression, pas comme une étape que l'appelant pourrait oublier :
Pdf.PageNumber := Item.PageNo;
Pdf.DeleteAnnotation(Item.Index); // lève EPdfError en cas d'échec
Bmp := Pdf.RenderPage(0, 0, ViewWidth, ViewHeight, ro0, [reAnnotations]);
try
PaintPageBitmap(Bmp);
finally
Bmp.Free; // RenderPage cède la propriété du bitmap à l'appelant
end;
RebuildPageEntries(Item.PageNo); // les index après Item.Index ont glissé
Deux détails de ce bloc se ratent facilement. L'option reAnnotations doit être présente, sinon la nouvelle trame abandonne toutes les annotations restantes et la page donne l'impression que vous avez effacé tout le jeu de commentaires au lieu d'une seule marque. Et le Bmp.Free n'est pas facultatif : la surcharge fonction de RenderPage cède la propriété du bitmap à l'appelant, si bien qu'une libération manquante fait fuir une trame pleine page à chaque suppression, ce qu'un relecteur parcourant un long document transformera en véritable pression mémoire en quelques minutes
Ajouter des marques de relecteur depuis votre propre interface
La création d'annotations passe par CreateAnnotation, qui prend un enregistrement TPdfAnnotation rempli (sous-type, rectangle, couleur, contenu, auteur) et l'attache à la page courante. Une note adhésive, de sous-type anText, est le cas facile : posez la position, le contenu et l'auteur et c'est fini. Les annotations Ink sont là où l'on se fait prendre. Le rectangle de l'enregistrement ne fait que borner le dessin ; les tracés eux-mêmes sont des tableaux de points qu'il faut attacher à part via l'appel de tracé du moteur, FPDFAnnot_AddInkStroke alimenté par des données FS_POINTF, capturées depuis la souris ou le stylet un tracé à la fois. Construisez une annotation Ink à partir d'un rectangle et de rien d'autre et vous obtenez un gribouillage vide qui se rend comme un espace blanc, ce qui ressemble à un bug du moteur et n'est en réalité qu'une annotation à moitié finie
Réglez la politique de paternité dans la foulée. Chaque marque que crée votre interface devrait porter un AuthorText cohérent, car le filtre par relecteur que vous bâtirez le mois prochain ne vaudra que ce que valent les noms que vous estampez sur les commentaires aujourd'hui. Des chaînes d'auteur vides ou incohérentes ne se réparent pas rétroactivement sans rouvrir chaque fichier
Faire sortir la revue du visualiseur
Les données de revue gagnent leur place dès qu'elles peuvent quitter le visualiseur, sous forme de synthèse que le chef de projet lit sans ouvrir le fichier ou de CSV qui alimente un tableau de suivi. Exportez depuis l'index que vous avez déjà bâti, jamais depuis une analyse fraîche, et choisissez une façon stable de renvoyer à chaque marque. Un numéro de page associé au rectangle de l'annotation survit à des allers-retours auxquels un index de tableau ne survit pas, car la suppression suivante renumérote discrètement les index et votre CSV se met à désigner les mauvais commentaires
Une ligne qui mérite d'être gardée porte la page, le sous-type, l'auteur, l'horodatage de création quand le fichier en enregistre un, le texte du contenu, et une colonne de statut qui vous appartient plutôt qu'une que le PDF fournit. La même passe d'indexation sert plus tôt, à la réception, quand un document arrive de l'extérieur de l'équipe et que vous voulez savoir ce qu'il contient avant que quiconque le relise. L'article sur l'atelier de réception des PDF parcourt ce tri, et la navigation dans les champs de formulaire couvre le problème miroir : relire des documents construits pour collecter des données plutôt que des commentaires
Un cas que le tableau ne vous montrera pas
Un mode de défaillance mérite un signalement parce qu'il ressemble à un défaut de votre code et n'en est pas un. Un client signale des surlignages visibles partout sur une page, mais votre panneau n'en liste aucun et AnnotationCount revient à zéro. L'explication habituelle est que les marques ont été aplaties quelque part en amont. L'aplatissement cuit les apparences d'annotation dans le contenu ordinaire de la page, si bien que les surlignages deviennent une partie des graphiques de page et cessent complètement d'exister comme objets d'annotation. Il ne reste rien qu'une API d'annotations puisse énumérer, recolorer ou supprimer. Quand vous voyez du balisage peint avec un compte à zéro, arrêtez de chercher le bug dans votre boucle d'énumération et demandez comment le fichier a été produit
La surface d'annotation utilisée ici, de l'énumération et de la création jusqu'à la recoloration, la suppression et les options de rendu qui gardent l'affichage honnête, est livrée avec PDFium Component pour Delphi, C++Builder et Lazarus/FPC