Remplacer la page 3 d'un contrat déjà validé ne devrait pas déplacer la table des matières. Supprimez l'ancienne page, insérez la nouvelle, et chaque signet qui pointait auparavant vers cet endroit atterrit désormais ailleurs. La bibliothèque PDFlibPas Delphi PDF évite cela en conservant l'objet page cible lui-même et en transférant uniquement les entrées qui portent le contenu visuel
Pourquoi les signets se cassent-ils après le remplacement d'une page PDF ?
Les signets se cassent parce qu'une destination PDF désigne une page par référence d'objet indirecte, pas par numéro de page. ISO 32000-1 §12.3.2.2 définit une destination explicite comme un tableau dont le premier élément est une référence indirecte vers l'objet page. Supprimez cet objet et ajoutez un remplaçant, et la référence devient pendante : la plupart des lecteurs réagissent en ramenant le lecteur à la page 1, ce qui est exactement le symptôme signalé après un remplacement de type suppression-puis-insertion. L'arbre des pages a l'air parfait, le nombre de pages est correct, le rendu est correct, et toute la couche de navigation est silencieusement erronée
Les destinations nommées ne vous sauvent pas non plus. §12.3.2.3 fait passer un nom par l'arbre de noms /Dests du catalogue de document, mais la feuille vers laquelle ce nom se résout est toujours un tableau de destination explicite contenant la même référence de page. Le nommage ajoute une couche d'indirection au-dessus de la référence de page, pas autour d'elle. Le même raisonnement couvre le reste de la couche interactive décrite au §12.5 : une annotation de lien porte un /Dest ou une action GoTo /A dont le /D est ce tableau, chaque annotation peut porter une entrée /P qui est une référence indirecte vers sa page, et un widget de champ de formulaire est une annotation exactement sur le même pied. Un échange de page naïf détache quatre sous-systèmes à la fois, et pour les voir énumérés sur un fichier réel, c'est le même graphe d'objets que parcourt l'introspection des signets, annotations et actions
Quelles entrées de page portent l'identité, lesquelles portent l'apparence
Un dictionnaire de page mélange deux sortes d'entrées, et un remplacement sur place réussit précisément lorsque vous les séparez. Le côté apparence est fini et énumérable : /Contents, /Resources, les cinq boîtes de page /MediaBox, /CropBox, /BleedBox, /TrimBox et /ArtBox, plus /Rotate, /Group, /UserUnit et /BoxColorInfo. Ces onze entrées décident de tout ce qu'un moteur de rendu produit pour la page, et rien d'autre dans le fichier ne les désigne par leur nom
Le côté identité est ce à quoi le reste du document s'est lié : le numéro d'objet et la génération de la page, le lien de retour /Parent vers l'arbre des pages, et /Annots. PDFlibPas conserve chacun d'eux intact. ReplacePageRanges purge les onze entrées visuelles du dictionnaire de la page cible et les rajoute à partir de la page source importée, si bien que l'objet page cible est modifié sur place plutôt que remplacé. La structure de l'arbre des pages exigée par §7.7.3 reste également identique octet pour octet dans sa forme : l'ordre de /Kids, /Count, et chaque /Parent survivant sont les mêmes avant et après, car aucun nœud n'a jamais été détaché
Comment PDFlibPas remplace-t-il une page sans renuméroter les objets ?
L'appel prend un document source, une page de début cible en base 1, une expression de plage source et un indicateur d'options. Les deux documents doivent être ouverts dans la même instance, et le document cible est celui sélectionné. Comme le nombre de pages cible ne change jamais, la plage demandée doit tenir dans le document en commençant à TargetStartPage, ce qui est vérifié avant même que quoi que ce soit soit créé
var
Lib: TPDFlib;
TargetDoc, SourceDoc: Integer;
begin
Lib := TPDFlib.Create;
try
// The document whose bookmarks and links must survive
if Lib.LoadFromFile('contract-final.pdf', '') <> 1 then
Exit;
TargetDoc := Lib.SelectedDocument;
// The revised clause page, rendered by whatever produced it
if Lib.LoadFromFile('clause-7-revised.pdf', '') <> 1 then
Exit;
SourceDoc := Lib.SelectedDocument;
Lib.SelectDocument(TargetDoc);
// Source page 1 overwrites the visuals of target page 3.
// Page count, page 3 object number, bookmarks and annotations are kept.
if Lib.ReplacePageRanges(SourceDoc, 3, '1', 0) = 1 then
Lib.SaveToFile('contract-final.pdf');
finally
Lib.Free;
end;
end;
En interne, les pages source ne peuvent pas simplement être lues à travers les frontières de documents, car chaque référence indirecte qu'elles contiennent appartient à la numérotation d'objets du document source. La plage source est donc d'abord importée de la façon habituelle, sous forme de pages temporaires ajoutées après la dernière page réelle, ce qui déclenche le remappage complet du graphe d'objets : flux de contenu, polices, XObjects, dégradés et espaces colorimétriques sont tous renumérotés dans le document cible. Ce n'est qu'ensuite que les onze entrées visuelles sont copiées de chaque page temporaire vers sa page cible, et ce n'est qu'ensuite que les pages temporaires sont détachées de l'arbre des pages. Le travail de remappage se produit là où il est peu coûteux et sûr, et la modification destructrice se réduit à un échange au niveau du dictionnaire sur des pages qui existent déjà
Le chemin de suppression qui détruirait ce que vous venez de transférer
Supprimer ces pages temporaires est l'étape qui paraît triviale et ne l'est pas. Le chemin ordinaire de suppression de page dans la bibliothèque fait plus que détacher un nœud : il fusionne les calques de chaque page supprimée, vide le premier flux de contenu, et récupère les ressources qu'aucune autre page ne partage. C'est un comportement correct pour une véritable suppression, et catastrophique ici, car au moment où les pages temporaires sont retirées, les pages cibles référencent déjà exactement ces flux de contenu et ces objets de ressources. Les vider viderait la page que vous venez de remplacer, et le balayage des ressources collecterait des polices et des images qui ont désormais un propriétaire bien vivant
La solution est un mode de préservation des objets référencés sur le chemin de suppression interne. Lorsqu'il est activé, la suppression saute à la fois le balayage des ressources non partagées et le vidage des flux de contenu, et ne fait rien d'autre que détacher les pages de l'arbre des pages et corriger la comptabilité de l'arbre. Les objets transférés survivent avec un nouveau propriétaire, et la propriété des objets après l'opération est exactement ce que vous dessineriez sur un tableau blanc : un flux de contenu, une page propriétaire, un numéro d'objet qui n'a jamais bougé. Les règles de cycle de vie associées pour créer, supprimer et réordonner des pages sont couvertes séparément dans les notes sur les opérations de cycle de vie du document et des pages
Ordre, doublons et échec tout-ou-rien
L'indicateur d'options sélectionne la façon dont la plage source est interprétée. 0 trie les numéros de page analysés et supprime les doublons, ce qui est le comportement par défaut sensé lorsque l'appelant passe quelque chose comme '4-6,2' et veut simplement dire ces quatre pages. 1 préserve l'ordre tel qu'écrit et autorise une page à se répéter, si bien que '2,1,2' signifie véritablement trois remplacements pris sur deux pages source. La validation s'exécute d'abord et s'exécute complètement : la syntaxe de la plage, chaque numéro de page par rapport au nombre de pages source, la valeur de l'option elle-même, et la capacité de la cible sont tous vérifiés avant qu'un seul objet ne soit créé. Un appel rejeté positionne LastErrorCode à 412, restaure la page précédemment sélectionnée, et laisse le document exactement comme il était
var
Replaced: Integer;
begin
Lib.SelectDocument(TargetDoc);
// Options = 1: source order is preserved and repeats are allowed, so
// target pages 5, 6 and 7 receive source pages 2, 1 and 2 respectively
Replaced := Lib.ReplacePageRanges(SourceDoc, 5, '2,1,2', 1);
if Replaced = 0 then
raise Exception.CreateFmt('Replacement rejected, LastErrorCode = %d',
[Lib.LastErrorCode]);
// On success the selection is the first replaced page
Assert(Lib.SelectedPage = 5);
end;
L'atomicité s'étend au-delà de la validation jusque dans le transfert lui-même. Avant que la première page source ne soit importée, les onze entrées visuelles de chaque page cible dans la plage sont capturées sous forme de valeurs encodées. Si l'import échoue, ou si le nombre de pages importées ne correspond pas à ce qui était demandé, les instantanés sont redécodés sur les pages cibles et les pages temporaires sont supprimées, si bien qu'un échec en cours d'exécution laisse quand même les visuels d'origine en place sur leurs objets d'origine. Cela compte plus qu'il n'y paraît : une plage de pages à moitié remplacée dans un contrat est pire qu'un appel échoué, car rien dans le fichier ne l'indique comme étant à moitié fait
// Post-conditions worth asserting in a regression test
Lib.SelectPage(3);
// Geometry now comes from the source page
WriteLn(Format('%.2f x %.2f', [Lib.PageWidth, Lib.PageHeight]));
// Annotations that were already on target page 3 are still attached
WriteLn(Lib.AnnotationCount);
// The bookmark created before the replacement still resolves to page 3
WriteLn(Lib.GetOutlinePage(OutlineID));
// And the document is still the same length
WriteLn(Lib.PageCount);
Ce que le remplacement sur place ne fait toujours pas pour vous
Les annotations source, les champs de formulaire source et les signets source ne sont délibérément pas importés. Faire passer un widget sans son entrée de champ /AcroForm, ou une annotation porteuse de contenu marqué sans sa propriété dans l'arbre de structure, produirait un objet interactif importé à moitié dont aucun lecteur ne peut faire quoi que ce soit, si bien que l'opération ne transfère que l'apparence. La conséquence pratique est que si la page de remplacement doit porter de nouveaux champs de formulaire ou de nouveaux liens, vous les ajoutez ensuite à la page cible, sur l'objet page cible qui est toujours là, à les attendre
Deux autres limites méritent d'être vérifiées sur vos propres fichiers. Premièrement, /Annots est préservé mais pas la géométrie de la page, si bien que remplacer une page de 220 mm par une page de 320 mm conserve les rectangles d'annotation à leurs anciennes coordonnées à l'intérieur d'une /MediaBox de taille différente ; si la géométrie change, repositionnez les annotations que vous avez conservées. Deuxièmement, les entrées en dehors des onze clés visuelles restent avec la page cible par conception, ce qui est correct pour /Trans ou /AA et périmé pour /Thumb, régénérez donc les vignettes après un remplacement. Les documents étiquetés méritent une réflexion supplémentaire : les éléments de structure pointent toujours vers le bon objet page via /Pg, mais leurs identifiants de contenu marqué décrivent un contenu qui n'est plus là, si bien qu'un échange de page dans un flux PDF/UA est autant une modification de l'arbre de structure qu'une modification de contenu. Si votre besoin réel est un compositage plutôt qu'un échange, superposer des éléments graphiques sur des pages que vous conservez, l'approche d'assemblage de pages et de modèles est l'outil le moins coûteux
Tout ce qui est décrit ici, y compris la syntaxe de l'expression de plage, les valeurs d'option et l'API de manipulation de page environnante, est livré dans la PDFlibPas Delphi PDF Library standard pour Delphi et C++Builder, dont la documentation de référence détaille intégralement l'appel de remplacement de page et ses codes d'erreur