Article technique

Page Stitching and Watermark Templates in Delphi PDFs

L'assemblage de pages compose le contenu de pages existantes sur un canevas partagé, ce qui est une opération différente de la fusion de fichiers. losLab PDF Library implémente l'assemblage de pages par Form XObject et des modèles de filigranes combinables pour Delphi : StitchPageOverlay, StitchPagesSideBySide et StitchPagesVertically placent les pages capturées à n'importe quelle échelle avec une fidélité vectorielle totale, et TPDFlibCompositeWatermark superpose des filigranes de texte et d'image sur une plage de pages en un seul appel. Si ce dont vous avez réellement besoin est d'ajouter des pages entières de plusieurs documents dans un seul fichier, cela relève de la famille de fusion (merge), traitée dans l'article sur la fusion rapide de PDF au niveau des octets ; cet article traite de la combinaison du contenu des pages dans un seul espace de coordonnées

Les demandes qui mènent ici sont courantes et exigeantes. Une école souhaite imposer deux pages d'examen sur une seule feuille paysage pour réduire de moitié la consommation de papier. Un relecteur de localisation souhaite afficher la page en anglais et sa traduction côte à côte sur une seule page, en les faisant défiler ensemble. Une équipe juridique souhaite que chaque projet soit marqué de trois couches à la fois : un logo dans le coin, la mention CONFIDENTIEL en diagonale au milieu, et une petite ligne de pied de page, uniquement sur les pages 1 à 3. Ces trois situations relèvent du même problème sous-jacent, à savoir placer un contenu de page déjà mis en page ailleurs et à une autre taille sans le dégrader sous forme d'image matricielle

Pourquoi la concaténation des flux de contenu PDF échoue-t-elle ?

L'assemblage naïf, qui consiste à copier le flux /Contents de la page source dans la page cible, échoue pour trois raisons structurelles. Premièrement, les coordonnées entrent en conflit : les deux flux de contenu supposent que leur origine se trouve en bas à gauche de leur propre MediaBox, de sorte que le contenu collé se superpose au contenu existant au mauvais endroit. Deuxièmement, les noms de ressources entrent en conflit : chaque page résout les opérateurs comme /F1 Tf et /Im0 Do par rapport à son propre dictionnaire /Resources, et deux pages lient régulièrement le même nom à des polices ou des images différentes, ce qui fait que l'une d'elles s'affiche silencieusement avec la mauvaise ressource. Troisièmement, une copie de flux brut ne permet aucun redimensionnement ; le mieux que vous puissiez faire est une translation

Les Form XObjects résolvent ces trois problèmes à la fois, et c'est pourquoi losLab PDF Library y base son assemblage. La norme ISO 32000-1 §8.10 définit un Form XObject comme un flux de contenu autonome avec son propre dictionnaire /Resources et son propre espace de coordonnées, placé dans une page en invoquant son nom sous n'importe quelle matrice de transformation active. CapturePage enveloppe une page existante entière, contenu et ressources compris, dans un XObject /Subtype /Form, et les fonctions d'assemblage le positionnent ensuite avec une matrice cm : mise à l'échelle, translation, rotation, à volonté. La source restant vectorielle, le texte reste du texte et les tracés restent des tracés à n'importe quel niveau de zoom, la même propriété qui rend les graphismes vectoriels et les motifs de dégradé indépendants de la résolution. Rien n'est rastérisé, et le dictionnaire de ressources isolé empêche la page capturée d'entrer en conflit avec la page cible pour le nom /F1

PDF Library for Delphi : la copie naïve de flux de contenu PDF échoue avec collisions de coordonnées, collisions de noms de ressources /F1 et absence de mise à l'échelle, tandis que CapturePage encapsule chaque page en Form XObject autonome qu'une seule matrice cm place sur une page cible plus large, avec ressources isolées et fidélité vectorielle totale
Copier le flux brut échoue de trois manières à la fois, tandis que capturer chaque page comme un Form XObject fournit un espace de coordonnées isolé et un dictionnaire de ressources qu'une seule matrice cm positionne

Comment combiner deux pages PDF sur une seule page ?

StitchPagesSideBySide(Page1, Page2, Gap, TargetHeight) est la réponse directe : elle capture les deux pages sources, crée une nouvelle page plus large et place les deux captures à gauche et à droite avec une gouttière entre elles, chacune mise à l'échelle pour s'adapter à TargetHeight. La fonction renvoie le nouveau numéro de page (basé sur 1) ou 0 en cas d'échec, et les deux pages sources sont capturées et masquées dans le cadre de l'opération. Le cas de l'examen à deux pages par feuille est réglé en trois lignes

var
  Lib: TPDFlib;
  NewPage: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('exam.pdf', '');
    // Les pages 1 et 2 sur une nouvelle page plus large avec une gouttière de 18pt,
    // toutes deux mises à l'échelle à une hauteur de sortie de 595pt (A4 paysage)
    NewPage := Lib.StitchPagesSideBySide(1, 2, 18, 595);
    if NewPage > 0 then
      Lib.SaveToFile('exam-2up.pdf');
  finally
    Lib.Free;
  end;
end;

Pour la composition verticale, StitchPagesVertically(PageRanges, Gap, TargetWidth) empile toute une plage de pages sur une seule page haute de style affiche. PageRanges utilise la syntaxe de plage standard comme '1-3,5'. Chaque page source est mise à l'échelle pour que sa largeur corresponde à TargetWidth, de sorte que la contribution en hauteur de chaque page correspond à sa propre hauteur multipliée par son propre facteur d'échelle, et la hauteur de la page de sortie est la somme de celles-ci plus une gouttière (Gap) entre chaque paire. C'est le format idéal pour les documents de révision à défilement continu ou les longs reçus assemblés à partir de sources au format lettre

Trois modes d'assemblage de pages PDF dans Delphi : StitchPagesSideBySide place deux pages capturées sur une feuille plus large avec un écart de 18 points, StitchPagesVertically empile la plage 1-3,5 en une page haute, StitchPageOverlay peint une page source sur la couverture à une opacité de 0.45, avec l'avertissement que chaque capture cache sa source et décale d'un cran les numéros de pages supérieurs
Les trois appels d'assemblage partagent une même géométrie : les pages capturées sont placées côte à côte, empilées ou peintes sur une page existante, et chaque capture consomme sa source si bien que tout numéro de page supérieur se décale vers le bas

Comment superposer une page sur une autre ?

StitchPageOverlay(SourcePage, TargetPage, Left, Top, Width, Height, Opacity) dessine une page existante sur une autre page existante, plutôt que d'en créer une nouvelle. La page source est capturée en tant que Form XObject et dessinée sur la cible au rectangle indiqué, avec une opacité (Opacity) de 0 à 1 (1 étant opaque). La largeur et la hauteur sont des paramètres libres, le positionnement s'étire donc si votre rapport diffère de l'aspect d'origine de la page source ; passez des valeurs proportionnelles si vous souhaitez une vignette non déformée de la source. Une utilisation typique est un document dont le bloc d'approbation est géré sur sa propre page et apposé sur la couverture

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('report.pdf', '');
    // La page 5 contient le bloc d'approbation ; placez-la sur la page 1
    // dans une boîte de 260 x 180 à 45 % d'opacité. La page 5 est consommée :
    // elle est capturée et masquée, donc les pages 6..n deviennent 5..n-1
    if Lib.StitchPageOverlay(5, 1, 300, 500, 260, 180, 0.45) = 1 then
      Lib.SaveToFile('report-approved.pdf');
  finally
    Lib.Free;
  end;
end;

Qu'advient-il des numéros de page après une capture ?

CapturePage consomme la page source : son contenu est déplacé dans le XObject et la page désormais vide est masquée, de sorte que chaque page dont le numéro est supérieur à la page source descend immédiatement d'un rang. C'est la sémantique la plus importante de toute l'API d'assemblage. losLab PDF Library gère ce calcul à l'intérieur de chaque appel d'assemblage, StitchPageOverlay ajuste l'index de la cible lorsque la cible se situe après la source, et StitchPagesSideBySide capture en premier la page ayant le numéro le plus élevé pour que l'index de la seconde capture reste valide, mais votre propre code doit tenir compte de ce décalage entre les appels. Deux règles garantissent la correction du code multi-assemblage : enregistrez toutes les tailles de page ou numéros de page dont vous aurez besoin avant la première capture, car ils changeront après. Et lorsque vous capturez vous-même plusieurs pages dans une boucle, itérez dans l'ordre décroissant des pages, de sorte que masquer une page ultérieure ne perturbe jamais l'index d'une page antérieure que vous n'avez pas encore capturée

Comment appliquer un filigrane multicouche à une plage de pages ?

La couche de modèle de filigrane dans PDFlibWatermark.pas transforme le concept « définir une fois, appliquer à plusieurs pages » en objets. TPDFlibWatermarkBase contient ce que tous les filigranes partagent : une position Position en neuf points (de wpTopLeft à wpBottomRight, plus wpCustom qui utilise des coordonnées X/Y brutes), un angle Angle en degrés, une opacité Opacity de 0 à 1, et une chaîne de plage de pages PageRange où une valeur vide signifie toutes les pages. TPDFlibTextWatermark ajoute du texte, un identifiant de police (Helvetica par défaut), la taille et une couleur RGB ; TPDFlibImageWatermark ajoute un identifiant d'image provenant d'une image précédemment ajoutée ainsi qu'une largeur et une hauteur de placement. TPDFlibCompositeWatermark contient une liste de sous-filigranes et les applique de manière récursive, et TPDFlib.ApplyWatermark est le point d'entrée en un seul appel qui respecte la plage de pages propre au modèle et renvoie le nombre de pages marquées. Le marquage juridique à trois couches ressemble à ceci

var
  Lib: TPDFlib;
  Logo: TPDFlibImageWatermark;
  Diagonal, Footer: TPDFlibTextWatermark;
  Stack: TPDFlibCompositeWatermark;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('draft.pdf', '');
    Stack := TPDFlibCompositeWatermark.Create;
    try
      Logo := TPDFlibImageWatermark.Create;
      Logo.ImageID := Lib.AddImageFromFile('logo.png', 0);
      Logo.Position := wpTopRight;
      Logo.Width := 90;
      Logo.Height := 32;
      Stack.Add(Logo);

      Diagonal := TPDFlibTextWatermark.Create;
      Diagonal.Text := 'CONFIDENTIAL';
      Diagonal.Position := wpCenter;
      Diagonal.Angle := 45;
      Diagonal.FontSize := 60;
      Diagonal.Opacity := 0.15;
      Stack.Add(Diagonal);

      Footer := TPDFlibTextWatermark.Create;
      Footer.Text := 'Internal review copy';
      Footer.Position := wpBottomCenter;
      Footer.FontSize := 9;
      Footer.Opacity := 0.6;
      Stack.Add(Footer);

      Stack.PageRange := '1-3';   // les enfants héritent de cette plage
      Lib.ApplyWatermark(Stack);
      Lib.SaveToFile('draft-marked.pdf');
    finally
      Stack.Free;                 // le composite libère ses enfants
    end;
  finally
    Lib.Free;
  end;
end;

Deux détails de composition méritent d'être explicités. L'héritage de plage est unidirectionnel et non destructif : un enfant avec une plage PageRange vide hérite temporairement de la plage du composite lors de la méthode Apply, tandis qu'un enfant qui définit sa propre plage la conserve. Ainsi, vous pouvez épingler le pied de page à chaque page tandis que le filigrane diagonal ne couvre que la section du projet. De plus, la propriété suit le composite : TPDFlibCompositeWatermark.Destroy libère ses enfants, vous ne libérez donc que le composite et rien d'autre, comme dans l'exemple ci-dessus

Modèle de filigrane composite dans Delphi : TPDFlibWatermarkBase porte les neuf ancres Position, l'angle, l'opacité et la plage de pages, TPDFlibCompositeWatermark empile un logo image, une couche de texte CONFIDENTIAL pivotée et une couche de texte de pied, ApplyWatermark estampille les trois couches sur les pages 1-3, et libérer le composite libère ses enfants
Le composite porte un calque d'image, un calque de texte diagonal et un calque de texte de pied de page au-dessus des propriétés de base partagées, les applique récursivement sur son PageRange et libère ses enfants à la destruction

Limites à connaître

Deux limites réelles s'appliquent, découlant du fonctionnement du mécanisme. Premièrement, une capture enveloppe uniquement le flux de contenu et les ressources de la page. Les annotations, liens, champs de formulaire et commentaires existent en tant qu'objets distincts dans le dictionnaire de la page, et non dans le flux de contenu, ils ne sont donc pas transférés dans le XObject ; une mise en page assemblée conserve le contenu visible de la page mais pas la couche interactive de ses sources. Deuxièmement, le positionnement du texte en neuf points est une approximation : sans un moteur de mise en page complet, la bibliothèque ne peut connaître la largeur exacte d'une chaîne à l'écran, elle estime donc la boîte de texte à 40% de la largeur de la page lors de la résolution de positions comme wpCenter. C'est suffisant pour centrer un texte court, mais pour un positionnement exact au pixel près de longues chaînes, passez à wpCustom et calculez vous-même les coordonnées

La mise à l'échelle n'est pas une contrainte pratique ici, puisque la capture et l'assemblage manipulent des références d'objets plutôt que de ré-encoder le contenu de la page ; si vos sources font des centaines de mégaoctets, la même méthode décrite dans le guide sur la fusion et la division de gros fichiers avec accès direct s'applique avant l'assemblage. Les fonctions d'assemblage et les classes de modèles de filigranes présentées ici sont fournies dans la version actuelle de losLab PDF Library pour Delphi, C# et VB.NET, aux côtés des primitives de niveau inférieur CapturePage, DrawCapturedPage et DrawCapturedPageMatrix lorsque vous avez besoin d'une mise en page non couverte par les trois appels d'assemblage prédéfinis