Vous ajoutez une nouvelle plage d'étiquettes à un manuel d'un éditeur pour que l'appendice affiche App-a, App-b au lieu de A-1, A-2, vous sauvegardez, et vous rouvrez le fichier. La boîte de page dit toujours A-1. Ouvrez le catalogue dans un éditeur de texte et le dictionnaire /PageLabels montre la raison : il porte un tableau /Kids hérité du producteur et, juste à côté, un tableau /Nums tout neuf contenant votre plage. ISO 32000-1 laisse la racine d'un arbre numérique porter l'une de ces clés ou l'autre, et un lecteur qui trouve /Kids sur la racine y descend sans chercher plus loin
Comment un PDF stocke les plages d'étiquettes de pages
Les étiquettes de pages sont les chaînes qu'un lecteur affiche dans sa boîte de page au lieu de l'index physique de la page, et ISO 32000-1 §12.4.2 les garde dans un arbre numérique sous la clé de catalogue /PageLabels. Chaque clé est un index de page à base zéro où commence une plage d'étiquetage. Chaque valeur est un dictionnaire d'étiquette avec au plus trois entrées : /S pour le style de numérotation (D, R, r, A ou a), /P pour une chaîne de préfixe, et /St pour le numéro de la première page de la plage, qui vaut 1 par défaut. Une plage court jusqu'à la clé suivante. La spécification exige aussi une entrée pour l'index de page 0, si bien que chaque page du document tombe dans quelque plage
Dans PDF Library for Delphi l'écrivain est TPDFlib.AddPageLabels(Start, Style, Offset, Prefix), et il se projette sur ce dictionnaire avec trois règles à mémoriser. Start est à base 1 comme tout autre argument de page de la bibliothèque, et il entre dans l'arbre comme Start - 1. Style va de 0 à 5 : 0 n'écrit aucun /S, si bien que l'étiquette est le préfixe seul (pratique pour une seule page étiquetée Cover), et 1 à 5 deviennent D, R, r, A et a. Un style hors de 0..5 fait renvoyer 0 à l'appel et laisse le document intact. Offset devient /St seulement s'il est supérieur à zéro ; passez 0 et la clé est omise, ce que chaque lecteur interprète comme 1
Un livre typique venant d'un outil de mise en page demande trois plages : chiffres romains minuscules pour les pages liminaires, numéros arabes pour le corps, et un appendice préfixé
uses
PDFlibrary;
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('handbook.pdf', '') <> 1 then
Exit;
// Pages 1-4 : i, ii, iii, iv (romain minuscule)
Lib.AddPageLabels(1, 3, 1, '');
// Pages 5-120 : 1, 2, 3 ... (décimal, repartant à 1)
Lib.AddPageLabels(5, 1, 1, '');
// Pages 121 et suivantes : A-1, A-2 ... (décimal avec préfixe)
Lib.AddPageLabels(121, 1, 1, 'A-');
WriteLn(Lib.GetPageLabel(5)); // 1
WriteLn(Lib.GetPageLabel(122)); // A-2
Lib.SaveToFile('handbook-labeled.pdf');
finally
Lib.Free;
end;
end;
Les étiquettes de pages sont arrivées en PDF 1.3, si bien que chaque appel demande aussi au document de porter sa version de sortie à 1.3 au moins. Si vous avez verrouillé la version avec LockSaveVersion, cette demande est ignorée et le verrou tient, car la bibliothèque ne passe pas outre une version que vous avez choisie à dessein. Si vous verrouillez, verrouillez à 1.3 ou plus dans tout travail qui écrit des étiquettes
Pourquoi une nouvelle plage disparaît-elle quand la racine a des /Kids ?
La plage disparaît parce qu'ISO 32000-1 §7.9.7 (Table 37) donne à la racine d'un arbre numérique soit /Kids soit /Nums, jamais les deux, et que les lecteurs choisissent /Kids quand il est présent. Les producteurs qui écrivent de longs documents découpent souvent l'arbre en nœuds intermédiaires, chacun avec une paire /Limits, et les suspendent à une racine qui n'a que des /Kids. Une routine d'insertion qui ne connaît que /Nums regarde cette racine, n'y trouve pas de tableau, en crée un à côté des /Kids existants, et y dépose la nouvelle plage. L'écriture réussit. Le fichier reste analysable. Chaque lecteur descend ensuite les /Kids, voit les trois plages originales du producteur, et ne touche jamais au tableau égaré — exactement le symptôme du manuel d'éditeur du paragraphe d'ouverture
La sortie consiste à cesser de traiter la racine comme un conteneur qui pourrait se doter d'un second point d'entrée. Quand AddPageLabels trouve des /Kids sur la racine, il parcourt d'abord chaque feuille de l'arbre existant de gauche à droite et collecte chaque clé avec son dictionnaire d'étiquette, puis bâtit un unique tableau plat /Nums à partir de cette liste. /Kids, /Limits et tout /Nums résiduel sont retirés de la racine avant que le tableau plat ne soit attaché, et seule alors la nouvelle plage entre par une insertion triée ordinaire. Retirer /Limits compte aussi : la Table 37 ne l'autorise que sur les nœuds intermédiaires et les feuilles, et une racine qui la garde est malformée même si elle porte désormais /Nums
Un effet de bord est facile à manquer. Si un fichier vous arrive déjà cassé, avec les deux clés sur la racine parce qu'un outil antérieur a fait l'insertion naïve, la marche des feuilles suit les /Kids comme le ferait un lecteur, si bien que les plages du tableau égaré ne sont pas reportées. Un lecteur n'a jamais affiché ces plages non plus, si bien que les étiquettes après l'édition correspondent à ce que les lecteurs voyaient déjà
L'arbre aplati n'est pas rebâti ensuite en nœuds /Kids équilibrés, et nous pensons que c'est le bon choix pour cet arbre particulier. L'équilibrage paie pour des arbres de noms avec des milliers de destinations ou de fichiers intégrés. Un arbre d'étiquettes de pages a une entrée par plage d'étiquetage, et même un gros manuel de référence en demande rarement plus de quelques douzaines ; une feuille unique est aussi ce qu'écrivent la plupart des producteurs quand ils ne découpent pas pour le principe. Une passe de rééquilibrage ajouterait du code qui peut se tromper à chaque sauvegarde, pour un gain de recherche que personne ne pourrait mesurer sur un arbre aussi petit
// Réétiqueter l'appendice dans un fichier dont la racine /PageLabels utilise /Kids
if Lib.LoadFromFile('vendor-manual.pdf', '') = 1 then
begin
WriteLn('Before: ', Lib.GetPageLabel(121)); // p. ex. A-1
// Même page de départ que la plage existante, si bien que sa valeur est remplacée
if Lib.AddPageLabels(121, 5, 1, 'App-') = 1 then
begin
WriteLn('After: ', Lib.GetPageLabel(121)); // App-a
WriteLn('Front: ', Lib.GetPageLabel(2)); // ii, inchangé
Lib.SaveToFile('vendor-manual-relabeled.pdf');
end;
end;
Comme la nouvelle plage démarre sur le même index de page qu'une clé existante, l'insertion remplace le dictionnaire de cette clé au lieu d'ajouter une seconde entrée pour l'index de page 120. Les plages romaine et décimales des feuilles du producteur passent avec leurs dictionnaires originaux, y compris les valeurs /St
Marcher sur les feuilles doit aussi survivre aux arbres malformés. Une entrée /Kids qui pointe en arrière vers l'un de ses propres ancêtres, ou deux branches qui partagent une feuille, se rencontrent dans des fichiers venus de pipelines endommagés. Le parcours se souvient des nœuds qu'il a déjà visités, si bien qu'un cycle est marché une fois puis laissé derrière, et une feuille partagée contribue ses plages une fois. Les mêmes soucis pour les arbres de noms sont traités dans la gestion des cycles d'arbres de noms, des /Limits erronés et des grandes feuilles
Parcourir un tableau /Nums par paires
Un tableau /Nums est une suite plate d'entrées alternées, [key0 value0 key1 value1 ...], et seules les positions paires sont des clés. Cette disposition invite à l'erreur classique. Une boucle qui avance d'un élément à la fois en demandant "est-ce un nombre ?" comparera les valeurs comme si c'étaient des clés chaque fois qu'une valeur arrive être numérique. Si cette comparaison gagne un test inférieur, le point d'insertion atterrit sur un index impair, la nouvelle paire entre au milieu d'une paire existante, et chaque clé et valeur suivantes échangent leurs places
Les valeurs des étiquettes de pages sont des dictionnaires, si bien que le bug se cache sur /PageLabels lui-même. Il fait surface le jour où le même helper est réutilisé pour un arbre numérique dont les valeurs sont de simples nombres, et rien dans la syntaxe du tableau ne vous avertit de quel genre vous tenez. PDF Library for Delphi lit les clés à l'index X * 2 et les valeurs à X * 2 + 1 dans la marche des feuilles comme dans l'insertion, et une correspondance exacte de clé remplace la valeur et arrête le scan. Si vous écrivez votre propre code d'arbre numérique sur des objets bruts, copiez ce pas avant toute autre chose
Relire les étiquettes avec GetPageLabel
TPDFlib.GetPageLabel(Page) renvoie l'étiquette d'une page à base 1, et ses deux replis se comportent différemment à dessein. Un document sans aucune entrée /PageLabels renvoie le numéro de page décimal, si bien qu'un appelant peut utiliser la fonction sans condition pour l'affichage. Un document qui a un arbre mais aucune plage couvrant la page demandée renvoie une chaîne vide. Ce second cas est ce que produit un fichier qui saute l'entrée obligatoire pour l'index de page 0 : les premières pages n'ont pas d'étiquette au sens de la spécification, et une chaîne vide le rapporte plus honnêtement qu'un numéro inventé. Commencez toujours votre jeu d'étiquettes à la page 1
Les styles de lettres suivent la spécification PDF plutôt que les colonnes de tableurs. Après Z vient AA, puis BB, puis CC ; la lettre se répète au lieu de retenir, si bien que la page 53 d'une plage A est AAA. Les chiffres romains sont produits en majuscules pour R et en minuscules pour r, et le préfixe est préposé tel quel dans les deux cas
var
P: Integer;
Data: WideString;
begin
// Audit rapide de ce qu'un lecteur affichera dans sa boîte de page
for P := 1 to Lib.PageCount do
WriteLn(P, ' -> ', Lib.GetPageLabel(P));
// La valeur d'option 4 n'exporte que les plages d'étiquettes, un bloc
// PageLabelBegin par plage
Data := Lib.ExportDocumentData(4);
// ... éditez le texte, ou portez-le vers un autre fichier ...
// Le premier enregistrement d'étiquette efface les plages existantes, puis chaque
// enregistrement est rejoué via AddPageLabels ; renvoie les enregistrements appliqués
WriteLn(Lib.ImportDocumentData(Data, 4), ' label ranges applied');
end;
Pour les éditions en masse, ExportDocumentData avec la valeur d'option 4 écrit chaque plage en un bloc PageLabelBegin suivi de lignes PageLabelNewIndex (la page de départ à base 1), PageLabelStart, un PageLabelPrefix optionnel et PageLabelNumStyle, avec des noms tels que LowercaseRomanNumerals et DecimalArabicNumerals. ImportDocumentData traite le premier enregistrement d'étiquette qu'il rencontre comme un remplacement complet : il appelle ClearPageLabels une fois, ce qui retire l'entrée /PageLabels entière du catalogue, puis remet chaque enregistrement à AddPageLabels. L'arbre rebâti est une feuille unique dès le départ, si bien qu'un aller-retour textuel à travers un original en forme de /Kids revient dans la même forme plate que produit la voie de réétiquetage
Ordre des feuilles et déplacements de pages après le réétiquetage
L'aplatissement garde les plages dans l'ordre où il les trouve. §7.9.7 exige des clés en ordre croissant, mais des fichiers aux feuilles en désordre circulent, et GetPageLabel applique la dernière plage dans cet ordre dont la clé est inférieure ou égale à l'index de la page. Pour un fichier qui déclare ses plages comme [0 roman, 120 A-, 4 decimal], la plage décimale démarrant à 1, la page 122 se résout vers la plage décimale et se lit 118 là où vous attendiez A-2. Si l'audit page par page ci-dessus montre des étiquettes de ce genre, ne bricolez pas autour avec un AddPageLabels de plus. Appelez ClearPageLabels et réécrivez le jeu complet dans l'ordre croissant ; quatre appels coûtent moins cher que de raisonner sur quelle plage gagne
Les étiquettes sont aussi liées à des index de pages, pas à des objets pages. Tout ce qui change le nombre ou l'ordre des pages laisse les plages exactement où elles étaient, pointant vers les pages qui se trouvent désormais à ces index. Un échange sur place tel que remplacer des pages en préservant les numéros d'objets garde le compte, si bien que les étiquettes restent alignées. Une fusion comme la réunion de scans duplex entrelacés produit une nouvelle séquence de pages, et cette sortie devrait recevoir un jeu de plages fraîchement écrit après la fusion, en commençant à la page 1
Les appels d'étiquettes, l'aplatissement des /Kids et l'export et l'import de données de document montrés ici font partie de PDF Library for Delphi, qui vise Delphi, C++Builder et Lazarus ; son entrée de référence pour AddPageLabels liste les valeurs de style et les codes de retour en une table