Article technique

Caviardage PDF et imposition N-up en Delphi avec HotPDF

Une demande arrive sur votre bureau : prendre un lot d'états déjà rendus, masquer les numéros de compte, et sortir deux pages par feuille pour économiser du papier. Les deux moitiés de cette tâche relèvent d'une chirurgie du flux de contenu sur un PDF que vous n'avez pas créé, donc il n'y a ni canevas de page convivial sur lequel dessiner ni gestionnaire de polices sur lequel s'appuyer. Vous modifiez directement le graphe d'objets d'un document chargé, en ajoutant des opérateurs de dessin bruts à une page mise en page par un autre outil. HotPDF expose exactement deux points d'entrée pour cela, et le plus dangereux des deux est celui qui semble inoffensif

HotPDF est un composant PDF VCL natif pour Delphi et C++Builder. Son API de document chargé de la version round-nine a ajouté les premières méthodes qui create du contenu entièrement nouveau sur une page ouverte depuis le disque plutôt que sur une page construite de zéro. Deux d'entre elles nous intéressent ici : RedactLoadedRect, qui peint un rectangle opaque sur une zone, et StitchLoadedPage, qui met une page à l'échelle et la dessine sur une autre. Les deux fonctionnent en écrivant les opérateurs de flux de contenu ISO 32000-1 §8.5 dans le flux /Contents de la page. Comprendre ce que font ces opérateurs, et tout aussi important ce qu'ils ne font pas, fait toute la différence entre un outil qui marche et une fuite de données

Ajout d'opérateurs à une page chargée

Lorsque vous construisez une page avec l'API HotPDF normale, le composant possède le flux de contenu et sérialise vos TextOut appels de texte et de vecteurs pour vous. Une page chargée est différente : son /Contents est un objet de flux existant, éventuellement partagé, éventuellement partie d'un tableau de contenus, et vous devez vous y greffer sans corrompre ce qui est déjà là. La version round-nine a introduit trois petits assistants qui rendent cela sûr. NewIndirectStream alloue un nouvel THPDFStreamObject indirect avec un tampon vide et une entrée /Length 0 ; ResolveLoadedStream remonte une référence indirecte jusqu'au flux sous-jacent ; et AppendLoadedStream écrit des octets bruts à la fin du flux et réécrit /Length pour que l'objet enregistré reste bien formé

Le schéma que suivent les deux méthodes publiques est le même. Trouvez le /Contentsflux de contenu de la page, résolvez-le en flux, et s'il n'existe pas de flux exploitable, créez-en un et attachez-le. Ajoutez ensuite les opérateurs. Comme les nouveaux octets arrivent à la fin du flux, le modèle du peintre garantit qu'ils s'affichent au-dessus de tout ce que la mise en page d'origine a dessiné. Cet ordre est tout le mécanisme derrière le rectangle de caviardage, et c'est aussi pour cela que ce rectangle n'est pas ce que la plupart des gens imaginent

RedactLoadedRect : un masque opaque, pas une suppression

RedactLoadedRect prend un index de page commençant à zéro, quatre coordonnées en espace utilisateur, et trois composantes de couleur dans l'intervalle de 0 à 1 :

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('statement.pdf') > 0 then
    begin
      // Cover the account-number band on page 1 with solid black.
      // Coordinates are PDF user space: origin bottom-left, points.
      Pdf.RedactLoadedRect(0, 56, 690, 320, 706, 0, 0, 0);
      Pdf.SaveLoadedDocument('statement-covered.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Sous le capot, la méthode émet trois opérateurs dans le flux de contenu : un réglage de couleur de remplissage en DeviceRGB (r g b rg), un tracé de rectangle (x y w h re), et un remplissage (f). La largeur et la hauteur sont calculées comme X2 - X1 et Y2 - Y1, donc vous passez deux coins opposés et laissez la méthode calculer l'étendue. Passez 0, 0, 0 comme couleur et vous obtenez une barre noire ; passez 1, 1, 1 pour une barre blanche qui correspond à une page blanche. Les coordonnées sont celles de l'espace utilisateur propre à la page chargée, ce qui signifie que l'origine est le coin inférieur gauche et que les unités sont les points, et cela signifie aussi que vous avez besoin de /MediaBox pour placer quoi que ce soit avec précision ; GetLoadedPageBox avec pbMediaBox vous donne cela

Relisez ceci deux fois : un rectangle rempli masque le contenu visuellement, il ne le supprime pas. Le texte, l'image ou le dessin vectoriel sous le rectangle reste présent dans le PDF, toujours dans le graphe d'objets, toujours extractible par quiconque copie la page, lance un extracteur de texte ou supprime simplement votre rectangle du flux de contenu. C'est un masquage visuel, pas un caviardage au sens juridique ou de sécurité. Si vous cachez des données réellement sensibles, numéros de compte, dossiers médicaux, identités, quoi que ce soit de réglementé, les couvrir avec une boîte noire et envoyer le fichier revient à laisser une fuite de données prête à être découverte. Un vrai caviardage exige de supprimer les objets de contenu sous-jacents, pas de peindre par-dessus

Le nom de la méthode dit "Redact", et c'est un avertissement utile sur la façon dont le résultat sera mal interprété, pas une promesse sur ce qu'elle supprime. L'implémentation est franche à ce sujet dans son propre commentaire : elle se décrit comme la "visual redaction primitive" et note qu'un caviardage qui supprime vraiment le contenu a besoin d'un interpréteur de flux de contenu qui parcourt et réécrit les opérateurs existants. Le chemin de document chargé de HotPDF ne fait pas cela ici. La règle sûre est donc étroite : utilisez RedactLoadedRect pour un masquage cosmétique sans sensibilité, cacher un filigrane de brouillon, effacer une zone avant une capture d'écran, recouvrir un logo obsolète sur une épreuve interne. Dès que ce qu'il y a sous la boîte importerait s'il fuyait, cette méthode n'est pas le bon outil, et la bonne réponse est de régénérer le document sans ces données ou d'utiliser un vrai pipeline de suppression de contenu

Imposition StitchLoadedPage : mise à l'échelle, translation, dessin

L'imposition N-up est le problème le plus indulgent, parce que rien n'est caché, seulement réorganisé. StitchLoadedPage prend un index de page cible, un index de page source, un décalage X/Y et un facteur d'échelle, puis dessine la page source sur la cible à cette position et à cette taille :

// Overlay page 2 (index 1) onto page 1 (index 0),
// scaled to 70% and nudged up-right.
Pdf.StitchLoadedPage(0, 1, 40, 380, 0.7);

// Convenience 2-up: source page on the right half of the target.
Pdf.StitchLoadedPageSideBySide(0, 1);

La chaîne d'opérateurs qu'elle ajoute est une séquence standard de transformation et de peinture : q pour sauvegarder l'état graphique, une cm matrice qui porte l'échelle sur la diagonale et le décalage dans les termes de translation, /StitchSrc Do pour invoquer un objet externe, et Q pour restaurer l'état. Le q/Q couple compte : il isole la transformation afin que la page assemblée ne propage pas son système de coordonnées à ce qui est ajouté ensuite. La méthode protège aussi contre les erreurs évidentes, indices hors plage, cible identique à la source, échelle non positive (qu'elle borne à 1.0), et s'arrête silencieusement plutôt que de lever une exception, donc vérifiez vos entrées car un no-op silencieux ressemble exactement à un succès

StitchLoadedPageSideBySide est une mince commodité autour de la méthode générale. Elle lit la largeur de la media box cible, la divise par deux, et appelle StitchLoadedPage avec cette demi-largeur comme décalage X et un facteur d'échelle fixe de 0.5, plaçant la source sur la moitié droite. Ce 0.5 codé en dur suppose que la source et la cible partagent la même largeur ; si ce n'est pas le cas, la source ne remplira pas proprement sa moitié, et vous voudrez la méthode générale StitchLoadedPage avec une échelle que vous calculez vous-même à partir des deux media boxes

La stratégie simplifiée des XObject et son compromis ISO

Voici le raccourci délibéré pris par l'implémentation, qu'il faut connaître avant de faire confiance au résultat dans différents lecteurs. Une imposition N-up correcte enveloppe le contenu de la page source dans un Form XObject, un objet dessinable autonome qui, selon ISO 32000-1 §8.10.1, doit contenir /Type /XObject, /Subtype /Form, et sa propre /BBox boîte de rognage. Le stitch round-nine de HotPDF ne construit pas ce wrapper. À la place, il enregistre la source dictionnaire de page lui-même directement sous le /Resources /XObject de la cible avec le nom StitchSrc, puis le dessine avec Do. Un dictionnaire de page et un Form XObject partagent suffisamment de leur modèle de contenu, ils référencent tous deux un flux de contenu et un dictionnaire de ressources, pour que de nombreux lecteurs affichent le résultat

Mais ce n'est pas un Form XObject conforme. Il lui manque le /Subtype /Form marqueur et sa propre /BBox, ce qui signifie qu'un consommateur strict est en droit d'ignorer le Do ou de le découper différemment de ce à quoi vous vous attendez. Les TechnicalNotes de cette version le disent clairement : l'approche "rend sous la plupart des lecteurs" mais "n'est pas un Form XObject strictement conforme à ISO", et la conformité complète exige de synthétiser un vrai flux Form XObject dans une étape séparée. Traitez donc le résultat du stitch comme n'importe quel constructeur non conforme : vérifiez-le dans les lecteurs précis que vos clients utilisent, pas seulement dans celui de votre machine, et si vous avez besoin de PDF d'archivage ou validés strictement, ne comptez pas sur ce chemin. La même discipline s'applique à tout ce que vous construisez sur le graphe d'objets chargé, raison pour laquelle un passage de prévol PDF dans Delphi gagne sa place dans le pipeline de publication chaque fois que vous mutilez des documents par programme

Où cela s'applique, et où cela ne s'applique pas

Les deux méthodes sont des outils de flux de contenu, donc le modèle mental est le même que celui que vous utilisez pour le dessin direct. Si vous avez construit des pages à partir de zéro avec le composant, les opérateurs vectoriels et de couleur derrière ces appels vous seront familiers depuis dessin sur canvas HotPDF dans Delphi ; la différence est seulement qu'ici vous ajoutez à un flux écrit par quelqu'un d'autre plutôt qu'à un flux qui vous appartient. Gardez trois limites en tête :

  • Le caviardage est cosmétique. RedactLoadedRect recouvre le contenu et ne le supprime jamais. Pour tout ce qui est sensible, régénérez la source ou utilisez une vraie suppression de contenu, une boîte noire n'est pas une sécurité
  • Le stitch n'est pas conforme par conception. La page source est référencée comme un pseudo-XObject sans le marqueur §8.10.1 /Subtype /Form et /BBox, alors vérifiez le rendu dans vos lecteurs cibles et évitez cela lorsque une validation stricte est requise
  • Les coordonnées sont l'espace utilisateur de la page. Origine en bas à gauche, unités en points, pilotées par la media box propre à la page. Lisez la boîte avec GetLoadedPageBox avant de placer quoi que ce soit, car la page que vous avez chargée n'est pas forcément de la taille que vous supposez

Dans ces limites, le duo couvre un vrai flux de travail : réorganiser des pages pour l'impression, masquer des zones non confidentielles, et réécrire le résultat avec SaveLoadedDocument, le tout sans rerendu complet. L'API de document chargé qui inclut ces primitives de stitch et de masque est fournie avec le HotPDF Component pour Delphi et C++Builder, aux côtés des méthodes de champs de formulaire, d'annotations et de FDF de la même version