Tamponner un filigrane (watermark) ou un logo sur chaque page d'un document ressemble à un travail de cinq minutes jusqu'à ce que vous ouvriez le résultat dans un inspecteur de taille de fichier. L'approche évidente consiste à parcourir les pages et, sur chacune d'elles, à reconstruire les mêmes objets texte ou image. Cela fonctionne visuellement, et c'est un gaspillage qui s'accumule. Un filigrane diagonal "BROUILLON" dessiné directement sur un rapport de cent pages représente cent copies du même chemin et des mêmes données textuelles dans les flux de contenu, et le fichier sauvegardé les transporte tous
Un Form XObject est la construction que le PDF fournit pour éviter exactement cela. Il enveloppe (wraps) un morceau de contenu réutilisable, une page entière ou un petit modèle, dans un seul objet nommé qui peut être peint de nombreuses fois à de nombreuses positions. Le contenu vit dans le fichier une seule fois. Chaque page qui veut le tampon contient une courte instruction qui dit "peins le XObject N ici, avec cette transformation." Un filigrane de cent pages ajoute alors un seul objet de contenu au fichier plutôt que cent, et c'est la différence entre un document qui croît de manière linéaire avec son nombre de pages et un autre qui ne le fait pas. Les filigranes, les tampons de logo, les modèles de numéros de page et les sceaux (seals) sont tous le même type de problème, et le Form XObject est le bon outil pour chacun d'entre eux
Pourquoi un objet stocké bat cent redessins
L'économie est structurelle, pas cosmétique. Une page PDF s'affiche en exécutant son flux de contenu, une séquence d'opérateurs de dessin. Lorsque vous redessinez un tampon par page, vous ajoutez (appending) la séquence complète d'opérateurs pour ce tampon au flux de chaque page, et les octets sont dupliqués autant de fois que vous avez de pages. Un Form XObject déplace ces opérateurs dans un seul flux stocké une fois dans le document. La référence qu'une page individuelle conserve est petite : elle pousse (pushes) une matrice de transformation, appelle le XObject et restaure l'état. Le nombre de pages ne multiplie plus le coût de l'illustration (artwork)
Cela importe le plus lorsque le tampon est lourd. Un sceau vectoriel avec des centaines de segments de chemin, ou un logo bitmap, est coûteux à stocker. Stockée une fois et référencée, la partie lourde est payée une seule fois et la surcharge (overhead) par page est de quelques octets d'invocation. Le résultat visuel sur la page est identique à un redessin direct, ce qui est le but. Le lecteur ne peut pas voir la différence ; la taille du fichier, elle, le peut tout à fait
Capturer une page dans un XObject
PDFium construit l'objet réutilisable à partir d'une page existante. La source est une page d'un document que vous avez ouvert, un petit PDF d'une page qui ne contient rien d'autre que l'illustration de votre filigrane, ou une page particulière d'un fichier plus volumineux. CreateXObjectFromPage capture le contenu de cette page source dans un handle réutilisable qui appartient au document de destination, celui que vous tamponnez
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
begin
Dest := TPdf.Create(nil);
Stamp := TPdf.Create(nil);
try
Dest.FileName := 'Report.pdf';
Dest.Active := True;
Stamp.FileName := 'Watermark.pdf'; // une page d'illustration (artwork)
Stamp.Active := True;
if not (Dest.Active and Stamp.Active) then
raise Exception.Create('Impossible d''ouvrir les documents d''entrée');
// Capturer la page 0 du document de tampon dans un handle réutilisable qui
// appartient à Dest. La source doit être Active ; l'index est basé sur zéro.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Impossible de construire le XObject de tampon');
// ... le placer, puis le libérer avant de fermer Stamp (voir ci-dessous) ...
La signature est CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. La méthode lève une exception (raises) si le document source n'est pas Active, et elle renvoie nil plutôt que de lever une exception lorsque PDFium ne peut pas construire l'objet, donc la vérification explicite ci-dessus n'est pas optionnelle. Le handle qui revient est un TPdfXObject que vous possédez, et les deux contraintes de durée de vie (lifetime constraints) qui y sont attachées sont la partie de tout cet exercice qui piège les gens, c'est pourquoi elles ont leur propre section ci-dessous
Placer le tampon sur une page
Un XObject capturé ne fait rien tout seul. Pour le faire apparaître, vous insérez une copie de celui-ci sur la page courante du document, celle sélectionnée par la propriété PageNumber en base 1, avec InsertFormObjectFromXObject. Cet appel renvoie l'objet de page sous-jacent, un FPDF_PAGEOBJECT, et le handle retourné est la façon dont vous positionnez le placement. Sans transformation, le tampon atterrit à l'origine dans les propres coordonnées de la page source, ce qui est rarement l'endroit où vous le souhaitez
Étant donné qu'InsertFormObjectFromXObject insère une copie par appel et renvoie un nouvel objet de page à chaque fois, vous pouvez peindre le même XObject plusieurs fois sur une page avec des transformations différentes, et le contenu stocké est toujours compté une seule fois dans le fichier. Un logo dans un coin (corner logo) et un léger filigrane pleine page peuvent provenir du même objet capturé
var
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
RawM: FS_MATRIX;
begin
// La page courante de Dest reçoit une copie du XObject.
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
raise Exception.Create('L''insertion a échoué sur cette page');
// Le positionner : déplacer de 200 unités vers la droite, 500 vers le haut, à l'échelle de 70 %.
M := TPdfMatrix.Create;
try
M.Scale(0.7, 0.7);
M.Translate(200, 500);
RawM := M.Handle;
if FPDFPageObj_SetMatrix(PageObj, RawM) = 0 then
raise Exception.Create('Impossible d''affecter la matrice de tampon');
finally
M.Free;
end;
Dest.UpdatePage; // valider (commit) les modifications de cette page dans son flux de contenu
// if not Dest.SaveAs(...) then ... lorsque chaque page est terminée.
end;
Deux détails d'intendance rendent cela sûr. Premièrement, une fois inséré, l'objet page appartient à la page, pas au XObject. Libérer le XObject plus tard n'invalide pas les placements que vous avez déjà faits. C'est ce qui permet au séquencement (ordering) créer-placer-libérer décrit ci-dessous de fonctionner. Deuxièmement, l'insertion et le positionnement ne modifient que la liste d'objets de la page en mémoire ; UpdatePage est ce qui sérialise (serialises) cette liste dans le flux de contenu de la page, donc une page que vous modifiez sans l'appeler est sauvegardée comme si le tampon n'avait jamais été placé
La règle de durée de vie du handle qui mord les gens
Deux contraintes régissent le handle XObject, et ignorer l'une ou l'autre produit une défaillance qui semble sans rapport avec sa cause. Premièrement, le document source doit être actif au moment où vous appelez CreateXObjectFromPage. La capture lit le contenu de la page source à partir du document source actif (live), de sorte que ce document et sa page doivent être ouverts et valides lorsque le handle est construit. Deuxièmement, et c'est celle qui surprend les gens, le handle doit être libéré avant que la page source ne soit fermée, et en pratique avant de fermer ou de libérer le document source d'où il provient
La raison est que le XObject est une référence à une structure que le document source possède toujours. Ce n'est pas une copie détachée et autonome que vous pouvez transporter une fois la source disparue. Fermez la source en premier et le handle reste pointé vers un contenu qui a été détruit (torn down), donc le libérer plus tard, ou toute autre utilisation, opère sur une mémoire qui n'est plus valide. Le symptôme est celui, classique, d'un handle ballant (dangling handle) : une violation d'accès à l'arrêt, ou une corruption intermittente qui se déplace selon l'ordre d'allocation, avec une pile (stack) qui pointe vers le code de nettoyage (cleanup code) plutôt que vers la ligne qui a réellement causé le problème. La solution est le séquencement (ordering), et non le codage défensif. Construisez le XObject, insérez-le sur chaque page qui en a besoin, libérez le XObject, et fermez seulement ensuite le document source. Le destructeur TPdfXObject libère le handle PDFium sous-jacent pour vous, par conséquent, libérer le wrapper au bon moment est toute votre responsabilité
La matrice et la signification de ses six nombres
Le placement est une transformation affine 2D, la même que celle utilisée partout par le PDF pour positionner le contenu (ISO 32000-1, section 8.3.4). Il s'agit de six nombres, écrits a, b, c, d, e, f, et PDFium les expose en tant qu'enregistrement (record) FS_MATRIX. Ils mappent un point de l'espace de l'objet (object's own space) à l'espace de la page (page space) :
// x' = a*x + c*y + e
// y' = b*x + d*y + f
//
// a, d : échelle horizontale et verticale
// b, c : les termes de cisaillement (shear) / rotation
// e, f : translation (où l'origine atterrit sur la page)
Vous pouvez remplir ces six valeurs à la main, mais les composer à la main est là où la rotation tourne mal, car la rotation mélange les quatre éléments a, b, c, d ensemble. Le wrapper TPdfMatrix, de l'unité FPdfMatrix, compose les opérations courantes pour vous et post-multiplie au fur et à mesure, de sorte que Translate, Scale et Rotate s'enchaînent dans l'ordre où vous les appelez. Un filigrane diagonal est une rotation suivie d'une translation pour le recentrer ; un logo de coin est une mise à l'échelle (scale) suivie d'une translation. Lorsque la matrice est prête, copiez sa valeur brute, la propriété Handle de type FS_MATRIX, dans une variable locale et transmettez-la à FPDFPageObj_SetMatrix ; l'import déclare la matrice comme un paramètre var, donc une propriété ne peut pas lui être remise directement, et son résultat est 0 en cas d'échec. Le niveau inférieur FPDFPageObj_Transform, qui prend les six valeurs directement en tant que doubles (doubles), est disponible lorsque vous préférez transmettre des nombres plutôt que de construire un wrapper
Tamponner chaque page, dans le bon ordre
Le modèle complet rassemble les pièces avec le séquencement (ordering) qu'exige la règle de durée de vie. Ouvrez les deux documents, capturez le tampon une fois, parcourez les pages de destination en définissant tour à tour PageNumber basé sur 1 et en insérant plus en positionnant une copie, en validant (committing) chaque page avec UpdatePage, puis libérez le XObject, puis enregistrez avec SaveAs et laissez le document source se fermer en dernier
procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
RawM: FS_MATRIX;
I: Integer;
begin
Dest := TPdf.Create(nil);
Stamp := TPdf.Create(nil);
try
Dest.FileName := ASource;
Dest.Active := True;
Stamp.FileName := AStamp;
Stamp.Active := True;
if not (Dest.Active and Stamp.Active) then
raise Exception.Create('Impossible d''ouvrir les documents d''entrée');
// 1. Capturer l'illustration une fois. Stamp est Active ici.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Impossible de capturer la page de tampon');
try
// 2. Placer une copie sur chaque page de Dest. PageNumber est basé sur 1.
for I := 1 to Dest.PageCount do
begin
Dest.PageNumber := I; // rendre la page I courante
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
Continue;
M := TPdfMatrix.Create;
try
M.Rotate(45); // filigrane diagonal
M.Translate(150, 100); // ajuster en position
RawM := M.Handle;
FPDFPageObj_SetMatrix(PageObj, RawM);
finally
M.Free;
end;
Dest.UpdatePage; // valider les modifications de cette page
end;
finally
XObject.Free; // 3. libérer AVANT que Stamp ne se ferme
end;
// 4. Écrire le résultat pendant que Dest est encore ouvert.
if not Dest.SaveAs(AOutput) then
raise Exception.Create('Impossible de sauvegarder ' + AOutput);
finally
Stamp.Free; // la source se ferme en dernier
Dest.Free;
end;
end;
La forme des blocs try fait le vrai travail. Le finally interne libère le XObject avant que le contrôle ne puisse jamais atteindre le finally externe qui libère Stamp, de sorte que le handle est toujours relâché pendant que sa source est encore en vie, même si une exception se déclenche au milieu de la boucle. Réussissez cette imbrication (nesting) et la règle de durée de vie s'occupe d'elle-même
Le tamponnage est un coin d'une boîte à outils plus vaste pour la création et la modification de contenu de page. Si votre tampon est lui-même une image plutôt qu'une page capturée, la conversion d'images en documents PDF avec PDFium explique comment intégrer d'abord ce bitmap dans un document. Et lorsque la chose que vous souhaitez transporter à côté du tampon visible est un fichier plutôt que de l'encre sur la page, le travail avec des pièces jointes PDF dans Delphi montre le côté des fichiers intégrés (embedded-file). Tout cela est fourni avec le Composant PDFium pour Delphi et C++Builder, aux côtés des API de rendu, de modification et de document couvertes ailleurs sur ce blog