Appliquer un filigrane ou un logo sur chaque page d'un document semble être une tâche de temps de cinq minutes jusqu'à ce que l'on vérifie la taille du fichier. L'approche évidente consiste à parcourir les pages et, sur chacune d'elles, à reconstruire les mêmes objets texte ou image. Cela fonctionne visuellement, mais c'est une source de gaspillage cumulatif. Un filigrane diagonal « DRAFT » dessiné directement sur un rapport de cent pages représente cent copies des mêmes données de chemin et de texte dans les flux de contenu, et le fichier enregistré les conserve toutes.
Un Form XObject est la structure fournie par le PDF pour éviter précisément cela. Il enveloppe un élément de contenu réutilisable (une page entière ou un petit modèle) dans un objet nommé unique qui peut être dessiné de nombreuses fois à de multiples positions. Le contenu réside une seule fois dans le fichier. Chaque page nécessitant le tampon comporte une instruction courte indiquant : « dessiner l'XObject N ici, avec cette transformation ». Un filigrane sur cent pages ajoute alors un seul objet de contenu au fichier au lieu de cent, ce qui fait toute la différence sur la croissance de la taille du fichier. Les filigranes, tampons de logos, modèles de numérotation de pages et sceaux relèvent tous du même type de problème, et le Form XObject est l'outil idéal pour chacun d'eux.
Pourquoi un objet stocké est plus efficace que cent redessins
L'économie est structurelle, non cosmétique. Une page PDF s'affiche en exécutant son flux de contenu, à savoir une séquence d'opérateurs de dessin. Lorsque vous redessinez un tampon sur chaque page, vous ajoutez la séquence complète d'opérateurs de 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 flux unique stocké une fois dans le document. La référence conservée par chaque page est minime : elle applique une matrice de transformation, appelle l'XObject et restaure l'état. Le nombre de pages ne multiplie plus le coût du graphisme.
Cela importe d'autant plus lorsque le tampon est lourd. Un sceau vectoriel comprenant des centaines de segments de chemin, ou un logo bitmap, est coûteux à stocker. Stocké une fois et référencé, la partie lourde est payée une seule fois et le surcoût par page se résume à quelques octets d'appel. Le résultat visuel sur la page reste identique à un tracé direct, ce qui est le but recherché. Le lecteur ne voit pas la différence, mais la taille du fichier, elle, la perçoit très bien.
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 ouvert, un petit PDF d'une page ne contenant que votre filigrane, ou une page particulière d'un fichier plus grand. CreateXObjectFromPage capture le contenu de cette page source dans un handle réutilisable appartenant au document de destination, celui que vous tamponnez.
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile('Report.pdf');
Stamp.LoadFromFile('Watermark.pdf'); // one page of artwork
// Capture page 0 of the stamp document into a reusable handle that
// is owned by Dest. Source must be active; the index is zero-based.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not build the stamp XObject');
// ... place it, then free it before closing Stamp (see below) ...
La signature est CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. La méthode renvoie nil en cas d'échec plutôt que de lever une exception, donc la vérification explicite ci-dessus est obligatoire. Le handle renvoyé est un TPdfXObject dont vous êtes propriétaire, et les deux contraintes de durée de vie qui lui sont associées constituent la partie de cet exercice qui surprend le plus, d'où la section dédiée ci-dessous.
Placer le tampon sur une page
Un XObject capturé ne fait rien par lui-même. Pour le faire apparaître, vous en insérez une copie sur la page active du document avec InsertFormObjectFromXObject. Cet appel renvoie l'objet de page sous-jacent, un FPDF_PAGEOBJECT, et le handle renvoyé sert à le positionner. Sans transformation, le tampon se place à l'origine dans les coordonnées de la page source, ce qui est rarement l'endroit souhaité.
Comme InsertFormObjectFromXObject insère une copie par appel et renvoie un nouvel objet de page à chaque fois, vous pouvez dessiner le même XObject plusieurs fois sur une même page avec des transformations différentes, le contenu stocké n'étant compté qu'une fois dans le fichier. Un logo d'angle et un filigrane discret sur toute la page peuvent provenir du même objet capturé.
var
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
begin
// The current page of Dest receives one copy of the XObject.
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
raise Exception.Create('Insert failed on this page');
// Position it: move 200 units right, 500 up, at 70% scale.
M := TPdfMatrix.Create;
try
M.Scale(0.7, 0.7);
M.Translate(200, 500);
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
// Dest.SaveLoadedDocument(...) when every page is done.
end;
Un détail de propriété sécurise le nettoyage. Une fois inséré, l'objet de page appartient à la page, non à l'XObject. Libérer l'XObject ultérieurement n'invalide pas les placements effectués. C'est ce qui permet au flux de création-placement-libération décrit ci-dessous de fonctionner.
La règle de durée de vie des handles qui pose piège
Deux contraintes régissent le handle XObject, et ignorer l'une d'elles produit une erreur 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 depuis le document source actif, celui-ci et sa page doivent donc être ouverts et valides lors de la construction du handle. Deuxièmement, et c'est ce qui surprend le plus, le handle doit être libéré avant la fermeture de la page source, et en pratique avant de fermer ou libérer le document source dont il provient.
La raison en est que l'XObject est une référence vers une structure que le document source possède toujours. Ce n'est pas une copie détachée et autonome que vous pouvez conserver une fois la source fermée. Fermez la source en premier et le handle pointera vers un contenu détruit, ainsi sa libération ultérieure ou tout autre usage s'effectuera sur une mémoire non valide. Le symptôme est l'erreur classique du handle suspendu (dangling handle) : une violation d'accès à l'arrêt, ou une corruption intermittente variant selon l'ordre d'allocation, avec une pile pointant vers le code de nettoyage plutôt que vers la ligne responsable. La solution réside dans l'ordonnancement, non dans le code défensif. Construisez l'XObject, insérez-le sur chaque page requise, libérez l'XObject, et seulement ensuite fermez le document source. Le destructeur de TPdfXObject libère le handle PDFium sous-jacent pour vous, ainsi libérer l'enveloppe au bon moment relève entièrement de votre responsabilité.
La matrice, et la signification de ses six chiffres
Le placement est une transformation affine 2D, celle-là même que le format PDF utilise partout pour positionner le contenu (ISO 32000-1, section 8.3.4). Elle comprend six nombres, notés a, b, c, d, e, f, et PDFium les expose sous forme d'enregistrement FS_MATRIX. Ils mappent un point de l'espace de l'objet vers l'espace de la page :
// x' = a*x + c*y + e
// y' = b*x + d*y + f
//
// a, d : horizontal and vertical scale
// b, c : the shear / rotation terms
// e, f : translation (where the origin lands on the page)
Vous pouvez renseigner ces six valeurs manuellement, mais les composer à la main est source d'erreur pour les rotations, car la rotation mélange les termes a, b, c, d. L'enveloppe TPdfMatrix compose les opérations courantes pour vous et effectue les multiplications successives, de sorte que Translate, Scale et Rotate s'enchaînent dans l'ordre de vos appels. Un filigrane diagonal est une rotation suivie d'une translation pour le recentrer ; un logo d'angle est une mise à l'échelle suivie d'une translation. Lorsque la matrice est prête, transmettez sa valeur brute à FPDFPageObj_SetMatrix(PageObj, M.Handle), où M.Handle is la structure FS_MATRIX sous-jacente. La fonction de bas niveau FPDFPageObj_Transform, qui prend les six valeurs directement sous forme de doubles, reste disponible si vous préférez passer des nombres plutôt que de construire une enveloppe.
Stamping every page, in the right order
Le modèle complet assemble ces éléments selon l'ordre requis par la règle de durée de vie. Ouvrez les deux documents, capturez le tampon une fois, parcourez les pages de destination en sélectionnant chacune à son tour pour y insérer et positionner une copie, puis libérez l'XObject, enregistrez, 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;
i: Integer;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile(ASource);
Stamp.LoadFromFile(AStamp);
// 1. Capture the artwork once. Stamp is active here.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not capture the stamp page');
try
// 2. Place a copy on every page of Dest.
for i := 0 to Dest.PageCount - 1 do
begin
Dest.CurrentPageIndex := i; // make page i current
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
Continue;
M := TPdfMatrix.Create;
try
M.Rotate(45); // diagonal watermark
M.Translate(150, 100); // nudge into position
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
end;
finally
XObject.Free; // 3. free BEFORE Stamp closes
end;
// 4. Write the result while Dest is still open.
Dest.SaveLoadedDocument(AOutput);
finally
Stamp.Free; // source closes last
Dest.Free;
end;
end;
La structure des blocs try réalise le véritable travail. Le finally interne libère l'XObject avant que l'exécution ne puisse atteindre le finally externe qui libère Stamp, ainsi le handle est toujours libéré pendant que sa source est active, même si une exception survient au milieu de la boucle. Respectez cette imbrication et la règle de durée de vie s'applique d'elle-même (utilisez le sélecteur de page active proposé par votre build, le corps de la boucle reste identique).
Le tamponnage est un aspect d'une boîte à outils plus large pour construire et éditer le contenu des pages. Si votre tampon est une image plutôt qu'une page capturée, convertir des images en documents PDF avec PDFium explique comment insérer ce bitmap dans un document préalable. Et lorsque l'élément que vous souhaitez associer au tampon visible est un fichier plutôt que de l'encre sur la page, travailler avec les pièces jointes PDF dans Delphi présente l'aspect des fichiers intégrés. Tout cela est fourni avec le composant PDFium pour Delphi et C++Builder, aux côtés des API de rendu, d'édition et de document traitées par ailleurs sur ce blog.