Article technique

Réécrire les flux de contenu PDF en toute sécurité en Delphi

HotPDF parcourt chaque opérateur du flux de contenu d'une page et remet chacun d'eux à votre code via VisitLoadedPageContentOperators. Pour chaque opérateur, vous choisissez une action : le conserver, le supprimer, le remplacer par vos propres octets, arrêter le parcours, ou rejeter toute l'édition. Quand chaque opérateur est conservé, le flux réécrit est identique octet pour octet à l'original, ce qui est la propriété qui rend le mécanisme sûr à exécuter sur des documents que vous n'avez pas créés

L'édition de flux de contenu est l'endroit où les outils PDF se trompent habituellement. Un flux est une séquence d'opérateurs sur une machine à pile, pas un arbre de document, et l'approche naïve consistant à exécuter une expression régulière sur les octets décodés se casse dès qu'un littéral de chaîne contient quelque chose qui ressemble à un opérateur, ou qu'une image en ligne contient du binaire arbitraire. Un visiteur qui a déjà tokenisé le flux n'a pas ce problème

Les opérateurs de flux de contenu PDF tels que BT, Tf et TJ alimentant les données d'événement du visiteur avec phase, nom d'opérateur, nombre d'opérandes, étendue en octets et état d'imbrication, puis renvoyant l'une de cinq actions : conserver, remplacer, supprimer, arrêter ou rejeter
Chaque opérateur visité produit exactement une décision, et une passe qui conserve tout réécrit les plages intactes pour une sortie identique octet pour octet

Que pouvez-vous réellement faire avec un accès au niveau des opérateurs ?

Trois familles de tâches. Audit : compter combien de fois une page définit une couleur en dehors d'un ensemble connu, ou trouver du texte dessiné avec une police non intégrée. Suppression : supprimer un filigrane dessiné comme contenu marqué, ou retirer une invocation d'image spécifique. Et normalisation : remplacer une forme d'opérateur par une équivalente qu'un consommateur en aval gère mieux

Chacune de ces tâches partage une exigence : vous devez pouvoir laisser intacts les 99 autres pourcents du flux. L'arbre syntaxique de HotPDF stocke une copie de la source plus des étendues compactes, et les éditions sont des superpositions triées et non chevauchantes. Écrire le résultat copie directement les plages intactes et n'émet des octets de remplacement que là où vous avez changé quelque chose, si bien qu'un aller-retour sans opération ne peut introduire aucune dérive

Le contrat du visiteur

Implémentez l'interface ou affectez l'événement, puis appelez la méthode de visite avec ApplyChanges à false pour un audit ou à true pour une réécriture. Les données de l'événement portent la phase, le nom de l'opérateur, son nombre d'opérandes, son étendue en octets, l'état d'imbrication courant et une vue empruntée pour lire les opérandes :

uses
  HPDFDoc, HPDFContentStream;

procedure TAuditor.HandleOperator(Sender: TObject;
  var EventData: THPDFContentOperatorEventData);
var
  Bytes: TBytes;
begin
  if EventData.Phase <> covpOperator then
    Exit;

  // Compte les opérateurs d'affichage de texte dessinés dans du contenu marqué
  if (EventData.OperatorName = 'Tj') or
     (EventData.OperatorName = 'TJ') then
  begin
    Inc(FTextOperators);
    if EventData.State.MarkedContentDepth > 0 then
      Inc(FTextInsideMarkedContent);
  end;

  // Lire un opérande seulement quand vous en avez besoin
  if EventData.OperatorName = 'Do' then
    if EventData.View.GetOperandBytes(0, Bytes) then
      NoteXObjectUse(TEncoding.ANSI.GetString(Bytes));

  EventData.Action := coaKeep;
end;

procedure TAuditor.Run(const FileName: string);
var
  Pdf: THotPDF;
  Stats: THPDFContentOperatorVisitStatistics;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile(FileName) <= 0 then
      Exit;
    Pdf.OnContentOperator := HandleOperator;
    for I := 0 to Pdf.LoadedPageCount - 1 do
      Pdf.VisitLoadedPageContentOperators(I, False, Stats);   // audit uniquement
    Writeln(Format('%d operators visited, max q/Q depth %d',
      [Stats.VisitedOperatorCount, Stats.MaximumGraphicsStateDepth]));
  finally
    Pdf.Free;
  end;
end;

Pourquoi la vue est-elle empruntée plutôt que mutable ?

Le callback ne reçoit jamais un arbre syntaxique modifiable. Il reçoit une vue qui peut récupérer les octets de l'opération courante ou un opérande spécifique à la demande, et c'est tout. Chaque changement passe par les champs action et remplacement à la place

Cette indirection n'est pas une formalité. La mutation directe de l'arbre permettrait à un visiteur de contourner le budget de remplacement et la vérification des éditions chevauchantes, et des éditions chevauchantes dans un modèle de superposition par étendue d'octets produisent une sortie silencieusement fausse plutôt que bruyamment cassée. Faire passer les changements par le champ action signifie que chaque édition est validée de la même façon, quelles que soient les intentions du visiteur

La lecture à la demande garde également le cas courant bon marché. Un audit qui ne fait que compter les noms d'opérateurs ne matérialise jamais les octets d'opérande du tout, ce qui compte sur un flux de contenu comportant des centaines de milliers d'opérations

Le champ état décrit toujours l'entrée

Chaque événement porte la profondeur d'état graphique, la profondeur d'objet texte, la profondeur de contenu marqué et la profondeur de section de compatibilité telles qu'elles se trouvaient dans l'entrée d'origine immédiatement avant l'exécution de cet opérateur. C'est vrai même si votre visiteur a supprimé ou remplacé des opérateurs antérieurs

Ancrer l'état à l'entrée d'origine plutôt qu'à la sortie éditée est un choix délibéré doté d'un bénéfice pratique : exécuter le même audit deux fois sur le même document produit des résultats identiques, et un audit qui s'exécute en parallèle d'éditions ne déplace pas son propre cadre de référence. Si vous devez connaître la profondeur dans la sortie, vous savez ce que vous avez supprimé, donc vous pouvez la calculer ; l'inverse, reconstruire des positions d'entrée stables à partir d'une sortie mouvante, n'est pas possible

Supprimer et remplacer, avec les règles qui vous protègent

Un remplacement est constitué d'octets bruts de flux de contenu, et vous êtes responsable de leur syntaxe. Les garde-fous autour d'eux sont le budget sur le total d'octets de remplacement, l'exigence de non-chevauchement, et la validation de sortie optionnelle qui réanalyse le résultat avant de l'accepter :

Garde-fous de réécriture en Delphi : un visiteur déclarant une intention Action et Replacement acheminée à travers le budget de remplacement, la vérification de non-chevauchement d'étendue d'octets et la validation de sortie jusqu'à la page enregistrée validée
Chaque changement passe par le champ action vers les garde-fous de budget, de chevauchement et de réanalyse, si bien qu'une validation échouée annule au lieu d'écrire des opérateurs cassés
procedure TWatermarkStripper.HandleOperator(Sender: TObject;
  var EventData: THPDFContentOperatorEventData);
begin
  EventData.Action := coaKeep;
  if EventData.Phase <> covpOperator then
    Exit;

  // Retire les opérateurs qui peignent le formulaire de filigrane
  if (EventData.OperatorName = 'Do') and OperandIsWatermark(EventData) then
    EventData.Action := coaDelete
  // Normalise une couleur parasite vers le gris maison
  else if EventData.OperatorName = 'rg' then
  begin
    EventData.Replacement := TEncoding.ANSI.GetBytes('0.2 0.2 0.2 rg');
    EventData.Action := coaReplace;
  end
  // Rejette toute l'édition si l'on rencontre quelque chose de non compris
  else if EventData.OperatorName = 'BI' then
    EventData.Action := coaReject;
end;

var
  Options: THPDFContentOperatorVisitOptions;
begin
  Options := THPDFContentOperatorVisitOptions.Default;
  Options.ValidateOutput := True;        // réanalyse avant acceptation
  Options.MaxReplacementBytes := 64 * 1024;

  Pdf.OnContentOperator := HandleOperator;
  if Pdf.VisitLoadedPageContentOperators(PageIndex, True, Options, nil, Stats) then
    if Stats.Modified then
      Pdf.SaveLoadedDocument('cleaned.pdf');
end;

L'ensemble d'actions inclut deux sorties pour une raison. coaStop termine le parcours et conserve ce qui a été décidé jusque-là, ce qui convient quand vous avez trouvé ce que vous cherchiez. coaReject abandonne toute la visite et laisse la page intacte, ce qui convient quand vous rencontrez une construction que vos règles de réécriture ne couvrent pas. Les images en ligne en sont l'exemple classique : leur charge utile binaire n'est pas quelque chose qu'un visiteur normalisant les couleurs devrait éditer

Lire les statistiques, et où le parcours s'arrête

Le champ statut distingue l'achèvement de l'arrêt, du rejet, de l'entrée invalide, d'une limite dépassée, d'une sortie invalide et d'une erreur de visiteur. Ce sont des problèmes différents : une sortie invalide signifie que vos octets de remplacement ne se sont pas réanalysés, tandis qu'une entrée invalide signifie que le flux source était déjà malformé et qu'aucune édition n'a été tentée

Les compteurs rendent un audit auto-vérifiable. VisitedOperatorCount comparé à KeptOperatorCount, DeletedOperatorCount et ReplacedOperatorCount doit s'équilibrer, et les chiffres de profondeur maximale indiquent si la page s'imbrique assez profondément pour valoir un examen plus poussé. SourceBytes comparé à OutputBytes donne l'effet de taille de votre édition en un seul chiffre

Réconciliation des statistiques d'audit où les comptes d'opérateurs conservés, supprimés et remplacés s'additionnent au compte visité, à côté de sept fins de statut allant de terminé à erreur de visiteur, avec une note indiquant que les profondeurs d'état décrivent l'entrée d'origine afin que les audits se répètent à l'identique
Des compteurs qui doivent s'équilibrer, plus un statut nommant chaque fin possible, rendent un audit auto-vérifiable sur des documents que vous n'avez pas créés

Pour les tâches courantes, il existe des voies de plus haut niveau qui ne nécessitent pas d'écrire un visiteur du tout. Supprimer du contenu de façon permanente, avec le nettoyage de ressources dont a besoin une vraie rédaction, est traité dans la rédaction et l'assemblage de pages sur des documents chargés. Remplacer du texte visible est traité dans la recherche et le remplacement de texte dans un PDF existant. Recourez au visiteur d'opérateurs quand votre règle est structurelle plutôt que textuelle, ou quand vous avez besoin d'un audit auquel aucun rapport existant ne répond

La visite, l'extraction et la réécriture de flux de contenu s'exécutent sur le même modèle de document chargé en Delphi et C++Builder ; la liste complète des fonctionnalités se trouve sur la page du composant PDF HotPDF Delphi