Un rectangle dessiné autour d'un paragraphe pendant une relecture n'a pas besoin de devenir une marque à l'intérieur du PDF. Le THPDFViewerModel de HotPDF expose AddHighlightRegion, une méthode qui conserve chaque surlignage comme un enregistrement en mémoire plutôt que comme une modification du document chargé, de sorte qu'un relecteur peut annoter des dizaines de pages pendant que le fichier sur disque reste identique octet pour octet. Zoomez à 6400 %, faites pivoter la page de 90 degrés, passez de l'ajustement à la largeur à l'ajustement à la page, et le même rectangle continue d'atterrir sur le même paragraphe, car le calcul des coordonnées repasse par la géométrie de rendu réelle au moment où la marque a été dessinée
Les outils de relecture construits autour d'une visionneuse PDF rencontrent constamment ce problème. Un écran d'annotation, une passe de contrôle qualité sur des factures générées, un flux de validation interne : tous ont besoin de laisser quelqu'un attirer l'attention sur une zone d'une page sans que chaque marque provisoire ne devienne un changement permanent du fichier, et sans avoir à recourir à un sous-système d'annotation complet juste pour afficher un rectangle coloré pendant que quelqu'un décide encore si la marque est pertinente. HotPDF répond à cela avec une couche de surlignage dédiée qui réside entièrement du côté Modèle de la séparation décrite dans la construction d'une visionneuse PDF personnalisée avec une architecture MVC en Delphi, ce qui explique aussi pourquoi la même liste de surlignages peut être pilotée depuis un test unitaire sans le moindre handle de fenêtre en vue
Que stocke réellement AddHighlightRegion de HotPDF ?
AddHighlightRegion stocke exactement trois choses par marque : un index de page de base zéro, un THPDFRectangle en coordonnées d'espace utilisateur PDF, et une TColor, le tout emballé sous forme d'enregistrement THPDFViewerHighlight à l'intérieur de THPDFViewerModel. Appeler Viewer.HighlightRegion(PageIndex, PageRect, clYellow), ou l'équivalent Model.AddHighlightRegion, ajoute un de ces enregistrements à un tableau privé et renvoie son index, et cet index est le seul « handle » que l'appelant récupère : il n'y a aucun objet séparé, aucune interface à comptage de références, rien à libérer. Toutes les autres capacités décrites dans cet article, dessiner la marque, la remapper après un changement de zoom, la supprimer, sont construites au-dessus de ce petit enregistrement unique
Chaque rectangle est normalisé et rogné avant d'être accepté. AddHighlightRegion échange les bords gauche et droit si un relecteur glisse de droite à gauche, échange le haut et le bas pour un glissement vers le haut, puis rogne le résultat par rapport au MediaBox de la page récupéré via GetLoadedPageBox. Un rectangle qui se retrouve avec une largeur nulle, une hauteur nulle, ou entièrement hors de la page est purement et simplement rejeté : la méthode renvoie -1 et rien n'est ajouté à la liste. Cette valeur de retour n'est pas décorative : un lot de surlignages reconstruit à partir d'un fichier de relecture externe, ou à partir de coordonnées périmées après le remplacement d'une page, peut silencieusement perdre des entrées si l'appelant ne la vérifie pas
Comment un surlignage reste-t-il aligné après un zoom ou une rotation ?
Un surlignage reste aligné parce que HotPDF le stocke dans l'espace de page PDF et le reprojette dans l'espace écran à chaque redessin, plutôt que de stocker un rectangle écran qui deviendrait périmé dès que le niveau de zoom change. THPDFViewerModel.PagePointToView et son inverse, ViewPointToPage, effectuent cette projection en deux étapes : d'abord l'entrée /Rotate propre de la page, puis le ViewRotation indépendant de la visionneuse, qui n'est jamais réécrit dans le PDF et n'affecte que ce que la visionneuse affiche. Défaire la transformation au relâchement de la souris exécute les deux mêmes étapes en sens inverse, ce qui permet à un surlignage dessiné à fort zoom sur une page pivotée à 270 degrés d'atterrir exactement au bon endroit après que le relecteur a réinitialisé la vue vers l'ajustement à la page
Le DPI utilisé pour cette projection compte tout autant que la rotation. La visionneuse de HotPDF capture le DPI exact du bitmap actuellement à l'écran dans FRenderedDPI juste après chaque rendu, et ImageMouseUp transmet cette même valeur à ViewPointToPage afin qu'une coordonnée de souris soit toujours convertie en utilisant la résolution à laquelle elle a réellement été dessinée, et non une résolution recalculée à partir de la propriété de zoom actuelle. CreatePageSnapshot et ses méthodes apparentées plafonnent le DPI à une plage de 12 à 2400, mais le chemin de rendu interactif ne comporte aucun tel plafond : l'échelle de zoom standard culmine à 6400 %, ce qui correspond à bien plus de 2400 DPI sur la base par défaut de 96 DPI, de sorte que réutiliser une limite de type instantané pour le mappage de coordonnées décalerait chaque surlignage de plusieurs pixels au sommet de la plage de zoom. Deux petits réglages par défaut complètent l'interaction : un glissement de moins de deux pixels sur l'un ou l'autre axe est traité comme un clic et ne produit aucun surlignage, et le surlignage ne peut commencer qu'une fois qu'au moins une page a réellement été rendue, puisque FRenderedDPI démarre à zéro
Câbler le surlignage interactif dans un écran de relecture
Activer le surlignage interactif est une affaire de trois propriétés sur le contrôle THPDFViewer lui-même : régler InteractionMode sur vimHighlight au lieu de la valeur par défaut vimBrowse, choisir une HighlightColor, qui vaut clYellow par défaut, et gérer OnMarqueeSelect pour découvrir ce que le relecteur vient de dessiner. Tout le reste, capturer la souris, dessiner le rectangle de sélection en pointillé pendant que le relecteur glisse, reconvertir le point de relâchement en espace de page, appeler AddHighlightRegion, se produit à l'intérieur du contrôle avant que cet événement ne se déclenche
type
TReviewForm = class(TForm)
Viewer: THPDFViewer;
ReviewLog: TMemo;
procedure FormCreate(Sender: TObject);
private
procedure ViewerMarqueeSelect(Sender: TObject; Shift: TShiftState;
PageIndex: Integer; const PageRect: THPDFRectangle;
HighlightIndex: Integer);
end;
// PdfDoc is a THotPDF already loaded elsewhere on the form
procedure TReviewForm.FormCreate(Sender: TObject);
begin
Viewer.PDFDocument := PdfDoc;
Viewer.InteractionMode := vimHighlight;
Viewer.HighlightColor := clLime;
Viewer.OnMarqueeSelect := ViewerMarqueeSelect;
end;
procedure TReviewForm.ViewerMarqueeSelect(Sender: TObject; Shift: TShiftState;
PageIndex: Integer; const PageRect: THPDFRectangle; HighlightIndex: Integer);
begin
ReviewLog.Lines.Add(Format('page %d, mark #%d at (%.1f, %.1f)-(%.1f, %.1f)',
[PageIndex + 1, HighlightIndex, PageRect.Left, PageRect.Bottom,
PageRect.Right, PageRect.Top]));
end;
OnMarqueeSelect ne se déclenche que pour un glissement ayant réellement produit un surlignage : un clic trop petit pour compter comme un glissement efface immédiatement la superposition de sélection, et un glissement qui atterrit entièrement hors de la page atteint AddHighlightRegion mais y est rejeté de la même manière qu'un appel programmatique le serait, si bien que l'événement reste silencieux dans les deux cas. Un détail d'implémentation qui mérite d'être connu si le surlignage semble un jour cesser de répondre au bord du contrôle : la capture de la souris appartient au THPDFViewer lui-même, un descendant de TScrollBox, pas au TImage interne qui affiche le bitmap de la page, ce qui est précisément ce qui permet à un relecteur de glisser au-delà du bord de la page rendue tout en obtenant un relâchement propre
Ajouter, supprimer, et relire les surlignages depuis le code
Les surlignages n'ont pas du tout besoin de provenir d'un glissement de souris. Viewer.HighlightRegion(PageIndex, PageRect, Color), qui achemine vers le même Model.AddHighlightRegion que le glissement interactif appelle en interne, est public précisément pour qu'un écran de relecture puisse reconstruire des surlignages à partir de données qu'il possède déjà : des commentaires chargés depuis une base de données, des résultats d'une recherche de texte, ou des marques restaurées d'une session précédente. Comme les coordonnées sont de simples nombres en espace utilisateur PDF, rien dans ce chemin ne dépend du fait qu'une page ait été rendue au préalable, contrairement au glissement interactif, qui a besoin que FRenderedDPI contienne déjà une valeur réelle
var
I: Integer;
Item: TPriorComment; // your own record: PageIndex + PageRect
NewIndex: Integer;
begin
for I := 0 to PriorComments.Count - 1 do
begin
Item := TPriorComment(PriorComments[I]);
NewIndex := Viewer.HighlightRegion(Item.PageIndex, Item.PageRect, clAqua);
if NewIndex < 0 then
LogWarning('comment %d fell outside the page and was dropped', [I]);
end;
end;
Supprimer un seul surlignage est l'endroit où le stockage adossé à un tableau se révèle. RemoveHighlightRegion supprime un enregistrement et décale d'une position vers le bas tous les enregistrements suivants pour combler l'écart, ce qui signifie que tout index capturé antérieurement, à partir d'un événement OnMarqueeSelect ou d'une énumération précédente, n'est plus fiable dès qu'un élément qui le précède dans la liste est supprimé. OnHighlightChange se déclenche à chaque ajout, suppression, et appel à ClearHighlightRegions, mais il ne transporte aucune information sur ce qui a changé, si bien que le schéma sûr consiste à le traiter comme un signal pour reconstruire toute liste qu'un panneau de relecture affiche à partir de HighlightCount et TryGetHighlightRegion, plutôt que de corriger un index mis en cache sur place
procedure TReviewForm.ViewerHighlightChange(Sender: TObject);
var
I: Integer;
Mark: THPDFViewerHighlight;
begin
MarkList.Items.Clear;
for I := 0 to Viewer.Model.HighlightCount - 1 do
if Viewer.Model.TryGetHighlightRegion(I, Mark) then
MarkList.Items.AddObject(Format('page %d', [Mark.PageIndex + 1]),
TObject(I));
end;
Quand une marque devrait-elle plutôt devenir une véritable annotation Highlight ?
Une zone de surlignage devrait devenir une véritable annotation dès l'instant où elle doit survivre en dehors de cette seule instance de THPDFViewer. HotPDF expose également AddHighlightAnnotation pour une nouvelle page et AddLoadedHighlightAnnotation pour un document déjà chargé, et malgré un nom presque identique, il s'agit d'un mécanisme complètement différent : les deux écrivent une véritable annotation de balisage de texte selon ISO 32000-1 §12.5.6.10, un PDF /Subtype /Highlight, dans le tableau /Annots de la page, avec des /QuadPoints marquant précisément la suite de glyphes concernée, et toute visionneuse PDF conforme la rend une fois le fichier enregistré, pas seulement HotPDF lui-même. La même limite de mécanisme détermine si une marque fait l'aller-retour via XFDF : une annotation créée avec AddLoadedHighlightAnnotation est un objet PDF normal que ExportLoadedAnnotationsToXFDF récupère et transmet à Acrobat ou à un autre outil de relecture sous forme de balisage ISO 19444-1, couvert dans l'import et l'export d'annotations PDF au format XFDF en Delphi, tandis qu'une zone ajoutée via AddHighlightRegion est invisible pour cet export car elle n'a jamais été écrite dans le graphe d'objets : elle n'existe qu'aussi longtemps que le THPDFViewerModel qui l'a créée existe lui-même. La famille complète des types d'annotations de balisage et géométriques disponibles sur une page, et la manière dont un rectangle place chacune d'elles, est couverte dans l'article sur les annotations PDF en Delphi avec HotPDF, et la règle pratique est simple : garder une marque jetable tant qu'un document est encore en discussion, et ne la valider en annotation qu'une fois la décision définitive
Où s'arrête la couche de surlignage
La couche de surlignage, pour sa part, ne tente pas de ressembler à un feutre surligneur translucide : RefreshDocument dessine chaque zone comme un rectangle de contour de deux pixels dans sa propre couleur par-dessus le bitmap de page mis en cache, de la même manière qu'elle dessine les résultats de recherche, plutôt que de fondre un remplissage coloré sur le texte en dessous, si bien qu'un aspect classique de lavis jaune doit être peint dans le code applicatif ou reporté sur le flux d'apparence propre d'une annotation promue. Une capacité qui mérite d'être réutilisée une fois qu'une zone existe est CreateCurrentPageRegionSnapshot, qui prend le même THPDFRectangle qu'un surlignage porte déjà et ne rend que cette zone dans un bitmap, utile pour joindre une petite image d'aperçu à un commentaire de relecture sans exporter la page entière. Une application de relecture n'a pas besoin de choisir entre les deux mécanismes à l'avance : faire de chaque nouvelle marque par défaut une zone THPDFViewerHighlight jetable tant qu'un fil de commentaires reste ouvert, et n'appeler AddLoadedHighlightAnnotation qu'une fois qu'un relecteur le résout, ce qui garde le PDF chargé intact pendant les allers-retours qui produisent le plus de changements. Le contrôle de visionneuse décrit ici fait partie du composant HotPDF standard pour Delphi et C++Builder, aux côtés du reste des API d'annotation et de formulaire référencées ci-dessus