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
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 :
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
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