HotPDF ajoute des signets à un PDF existant dans Delphi via AddLoadedOutline, qui crée des entrées de plan à n'importe quel niveau d'imbrication sur un document chargé, et il lit et écrit des destinations nommées via ResolveLoadedNamedDestination et AddLoadedNamedDestination. Chargez le fichier, construisez l'arborescence, appelez SaveLoadedDocument, et la barre latérale du lecteur qui était auparavant vierge affiche désormais une table des matières fonctionnelle
Le scénario à l'origine de tout cela est malheureusement très courant. Vous fusionnez une douzaine de contrats dans un dossier de révision, ou assemblez trois manuels de produits en un seul fichier de distribution, et le résultat est structurellement correct : chaque page est présente, chaque police est intacte. Puis, quelqu'un l'ouvre dans Acrobat et le panneau des signets est vide. Un document de 400 pages sans navigation est techniquement complet mais pratiquement inutilisable. Jusqu'à la version 2.347.0, HotPDF pouvait lire, modifier et supprimer des signets sur des documents chargés, mais jamais en créer. Cette lacune est désormais comblée, et cet article détaille la nouvelle interface de l'API et l'élément d'arcane PDF, la clé /Count, qui peut poser problème si vous la traitez comme un simple compteur d'entrées
Qu'est-ce que l'arborescence du plan (outline tree) réellement ?
Un plan PDF est une arborescence de dictionnaires doublement liée, et non une liste plate, et cette structure explique chaque paramètre de l'API. La norme ISO 32000-1 §12.3.3 le définit : le catalogue du document pointe vers une racine /Outlines, la racine pointe vers ses premiers et derniers enfants via /First et /Last, et les frères se lient via /Next et /Prev tandis que chaque entrée pointe vers le haut via /Parent. Chaque entrée porte une chaîne /Title et, généralement, une destination /Dest indiquant où le clic doit mener le lecteur
Les destinations existent sous deux formes, et la norme ISO 32000-1 §12.3.2 les maintient délibérément séparées. Une destination explicite est un tableau en ligne comme [page /XYZ x y zoom] : une référence directe à un objet de page complétée par une spécification de vue. Une destination nommée stocke quant à elle une chaîne de nom, et la cible réelle réside dans le catalogue du document sous l'arborescence de noms /Names /Dests, un niveau d'indirection qui permet à de nombreux liens de partager une même cible et permet à la cible de se déplacer sans affecter les liens. HotPDF écrit des destinations explicites /XYZ lorsqu'il crée des signets, et fournit des appels distincts pour lire et étendre l'arborescence des noms
Comment ajouter des signets à un PDF existant dans Delphi ?
Appelez AddLoadedOutline(ParentIndex, DestPageIndex, Title, X, Y, Zoom) après LoadFromFile : passez ParentIndex = -1 pour une entrée de premier niveau, ou l'index (commençant à 0) d'une entrée de premier niveau existante pour l'imbriquer dessous. La fonction crée la racine /Outlines si le document n'en avait pas, construit le dictionnaire de l'entrée, écrit un tableau /Dest [page /XYZ x y zoom] pointant vers l'index de page DestPageIndex (basé sur 0), et lie la nouvelle entrée dans la chaîne des frères. Elle renvoie 0 en cas de succès et -1 en cas d'échec, par exemple lorsque DestPageIndex is hors limites
Un détail de liaison importe pour l'ordre : une nouvelle entrée est insérée en *tête* de la chaîne enfant de son parent, en tant que nouveau /First. Ainsi, si vous ajoutez « Chapitre 1 » puis « Chapitre 2 » comme entrées de premier niveau, le Chapitre 2 apparaît au-dessus du Chapitre 1 dans la barre latérale, et le Chapitre 1 passe à l'index de premier niveau 1. La méthode pratique consiste soit à ajouter les entrées dans l'ordre inverse de lecture, soit à ajouter chaque parent et à remplir immédiatement ses enfants pendant qu'il se trouve encore à l'index 0
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('merged-manual.pdf', '') > 0 then
begin
// Add chapters in reverse reading order: each new
// top-level entry becomes /First (index 0).
Pdf.AddLoadedOutline(-1, 40, 'Chapter 2: Configuration', 0, 792, 0);
Pdf.AddLoadedOutline(-1, 0, 'Chapter 1: Installation', 0, 792, 0);
// Chapter 1 is now top-level index 0; nest sections
// under it (children also prepend, so reverse order).
Pdf.AddLoadedOutline(0, 12, 'License activation', 0, 792, 0);
Pdf.AddLoadedOutline(0, 3, 'System requirements', 0, 792, 0);
Pdf.SaveLoadedDocument('merged-manual-toc.pdf');
end;
finally
Pdf.Free;
end;
end;
Les paramètres X, Y et Zoom correspondent directement à la destination /XYZ et valent tous 0 par défaut. Le format PDF place l'origine des coordonnées dans le coin inférieur gauche, de sorte que Y = 792 cible le haut d'une page US Letter, et selon la norme ISO 32000-1, une valeur nulle dans un emplacement /XYZ indique au lecteur de conserver son paramètre actuel, ce qui correspond exactement à ce que vous souhaitez pour le zoom dans presque tous les cas. Tout se déroule sur le graphe d'objets chargé en mémoire ; rien ne touche le disque avant l'exécution de SaveLoadedDocument, le même modèle d'édition-puis-sauvegarde utilisé par l'API pour réécrire le titre, l'auteur et les métadonnées XMP sur les PDF chargés
Redirection des signets : sauts de page versus actions URI
Les signets existants peuvent être redirigés sans avoir à les recréer, et HotPDF fournit deux appels distincts car les structures PDF sous-jacentes sont réellement différentes. SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom) remplace la destination /Dest de l'entrée de premier niveau à l'index Index par un nouveau tableau /XYZ, utile pour corriger l'erreur classique après fusion où chaque signet pointe toujours vers les numéros de page du fichier d'origine. SetLoadedOutlineURI(Index, URI) effectue une action structurellement différente : elle attache un dictionnaire d'action /A avec /S /URI, transformant le signet en un lien Web plutôt qu'en un saut interne au document
// The merge shifted everything by 10 pages: repoint the
// third top-level bookmark at page 12 (zero-based 11).
Pdf.SetLoadedOutlineDestination(2, 11, 0, 792, 0);
// Turn the last entry into an external link.
Pdf.SetLoadedOutlineURI(3, 'https://www.loslab.com/');
Pdf.SaveLoadedDocument('retargeted.pdf');
Gardez bien ces deux mécanismes à l'esprit lors de la conception de l'arborescence. Une destination /Dest est une pure navigation interne au document ; une action URI quitte complètement le document, et la norme ISO 32000-1 s'attend à ce qu'une entrée de plan navigue par l'un ou l'autre de ces mécanismes, pas les deux. Réservez donc SetLoadedOutlineURI aux entrées qui ne comportent pas déjà de destination de page, comme un lien « Page d'accueil du produit » ou « Signaler un problème » en fin d'arborescence plutôt qu'un en-tête de chapitre converti. L'action URI écrite ici correspond à la structure /S /URI utilisée pour les annotations de lien sur le contenu de page, traitée plus en détail dans le guide sur la création de hyperliens dans les documents PDF avec HotPDF
Comment fonctionnent les destinations nommées sur un PDF chargé ?
Les destinations nommées constituent le mécanisme d'ancrage stable de la norme ISO 32000-1 §12.3.2.3 : au lieu d'intégrer une référence de page dans chaque lien, le document conserve une carte de correspondance nom-destination dans l'arborescence de noms /Names /Dests du catalogue, et les liens font référence aux entrées par leur nom. ResolveLoadedNamedDestination(Name) recherche un nom dans cette arborescence et renvoie l'index de page (basé sur 0) vers lequel il pointe, ou -1 si the nom est absent. Le résolveur prend en charge les deux formes de stockage autorisées par la spécification pour la valeur mappée : un tableau de destination brut comme [page /XYZ x y zoom] et la forme dictionnaire qui enveloppe le tableau sous une clé /D
AddLoadedNamedDestination(Name, DestPageIndex, X, Y) fonctionne dans l'autre sens : elle enregistre une nouvelle ancre dans l'arborescence /Names /Dests, créant l'arborescence elle-même si le document n'en a pas, et renvoie True en cas de succès. Ensemble, ce duo couvre le flux de travail où un document fusionné doit respecter les ancres écrites par les outils en amont, et en publier de nouvelles pour les liens en aval
var
PageIdx: Integer;
begin
// Honor an anchor another tool wrote into /Names /Dests.
PageIdx := Pdf.ResolveLoadedNamedDestination('chapter.3.figures');
if PageIdx >= 0 then
Pdf.AddLoadedOutline(-1, PageIdx, 'Chapter 3: Figures', 0, 792, 0);
// Publish a new anchor for other links to target.
if Pdf.AddLoadedNamedDestination('appendix.glossary', 42, 0, 792) then
Pdf.SaveLoadedDocument('anchored.pdf');
end;
Les limites réelles : les destinations écrites par HotPDF, pour les signets comme pour les ancres nommées, sont de type /XYZ. Les autres types explicites définis par la spécification, comme /Fit, /FitH, /FitB et associés, ne sont pas émis par ces appels, bien que /XYZ avec un zoom nul couvre le cas d'utilisation principal « aller à cet endroit, conserver mon zoom ». De plus, GetLoadedBookmarkPageIndex(Title), la fonction pratique qui prend un titre de signet et renvoie sa page de destination, analyse le premier niveau de l'arborescence du plan, utilisez-la donc pour vérifier les entrées de niveau chapitre que vous venez de créer plutôt que les feuilles profondément imbriquées
Le piège de /Count : il compte les descendants, pas les entrées
La clé /Count sur la racine du plan ne signifie pas ce que la plupart des développeurs supposent, et une mauvaise interprétation produit des échecs de test ressemblant à de la corruption de données. La norme ISO 32000-1 §12.3.3 définit /Count comme le nombre total de *descendants visibles* à tous les niveaux, et non le nombre d'entrées de premier niveau. Sur une entrée individuelle, le signe a également une signification : une valeur positive indique que l'entrée est ouverte et que ce nombre de descendants s'affiche dans la barre latérale, tandis qu'une valeur négative signifie que l'entrée est repliée et que |N| descendants y sont masqués
Cela est apparu dans la suite de régression de HotPDF lors du développement des API de signets pour documents chargés. Un document de test contenait trois signets de premier niveau, dont un avec un enfant ; après la suppression d'une entrée de premier niveau, GetLoadedOutlineCount, qui lit la racine /Count, a renvoyé 1 au lieu de la valeur attendue 2. Rien n'était corrompu : la valeur initiale de 3 n'avait jamais représenté « trois entrées de premier niveau », et le recalcul correct doit parcourir la chaîne de premier niveau en accumulant chaque nœud plus son /Count positif, en ignorant les descendants des nœuds repliés. La leçon pour votre code est directe : n'effectuez jamais d'assertions sur les deltas de /Count après des modifications structurelles, car la valeur évolue de manière non linéaire avec les descendants visibles. Vérifiez plutôt la structure par son contenu
// After edits, verify by title lookup, not by /Count deltas.
if Pdf.GetLoadedBookmarkPageIndex('Chapter 1: Installation') = 0 then
ShowMessage('Outline verified');
Où cela s'intègre-t-il dans la boîte à outils pour documents chargés ?
La création de signets et de destinations nommées complète un tableau que l'API pour documents chargés dessine depuis un certain temps : le même cycle de chargement-édition-sauvegarde permet déjà de réécrire les métadonnées du document, d'ajouter des annotations de lien et d'effectuer des allers-retours d'annotations via l'importation et l'exportation XFDF. Le modèle est uniforme : LoadFromFile, une séquence d'appels *Loaded* sur le graphe d'objets en mémoire, puis un appel unique SaveLoadedDocument, ce qui facilite l'intégration de la construction de plans dans un pipeline existant de fusion ou de marquage comme étape supplémentaire avant la sauvegarde
Les API présentées ici, AddLoadedOutline, SetLoadedOutlineDestination, SetLoadedOutlineURI, ResolveLoadedNamedDestination, AddLoadedNamedDestination et GetLoadedBookmarkPageIndex, sont incluses dans la version standard de HotPDF Component pour Delphi et C++Builder, sans dépendance externe ni runtime de lecteur distinct requis