HotPDF durchläuft jeden Operator im Content-Stream einer Seite und übergibt jeden davon über VisitLoadedPageContentOperators an Ihren Code. Für jeden Operator wählen Sie eine Aktion: behalten, löschen, durch eigene Bytes ersetzen, den Durchlauf stoppen oder die gesamte Bearbeitung ablehnen. Werden alle Operatoren behalten, ist der umgeschriebene Stream bytegenau identisch mit dem Original — die Eigenschaft, die den Mechanismus sicher macht für Dokumente, die Sie nicht selbst erstellt haben
Das Bearbeiten von Content-Streams ist der Punkt, an dem PDF-Werkzeuge üblicherweise scheitern. Ein Stream ist eine Folge von Operatoren über einer Stack-Maschine, kein Dokumentbaum, und der naive Ansatz, einen regulären Ausdruck über die dekodierten Bytes laufen zu lassen, bricht in dem Moment zusammen, in dem ein String-Literal etwas enthält, das wie ein Operator aussieht, oder ein Inline-Bild beliebige Binärdaten enthält. Ein Visitor, der den Stream bereits tokenisiert hat, hat dieses Problem nicht
Was lässt sich mit Zugriff auf Operatorebene tatsächlich tun?
Drei Aufgabenfamilien. Auditing: zählen, wie oft eine Seite eine Farbe außerhalb einer bekannten Palette setzt, oder Text finden, der mit einer nicht eingebetteten Schrift gezeichnet wurde. Entfernen: ein Wasserzeichen löschen, das als markierter Inhalt gezeichnet wurde, oder einen bestimmten Bildaufruf entfernen. Und Normalisierung: eine Operatorform durch ein Äquivalent ersetzen, das ein nachgelagerter Verarbeiter besser handhabt
All diese haben eine gemeinsame Voraussetzung: Sie müssen die übrigen 99 Prozent des Streams unangetastet lassen können. Der Syntaxbaum von HotPDF speichert eine Kopie der Quelle plus kompakte Spans, und Bearbeitungen sind sortierte, nicht überlappende Overlays. Beim Schreiben des Ergebnisses werden unberührte Bereiche direkt kopiert, und Ersatz-Bytes werden nur dort ausgegeben, wo Sie etwas geändert haben, sodass ein No-Op-Durchlauf keine Abweichung einführen kann
Der Visitor-Vertrag
Implementieren Sie die Schnittstelle oder weisen Sie das Ereignis zu, und rufen Sie dann die Visit-Methode mit ApplyChanges auf false für ein Audit oder true für ein Umschreiben. Die Ereignisdaten tragen die Phase, den Operatornamen, seine Operandenanzahl, seinen Byte-Bereich, den aktuellen Verschachtelungszustand und eine geliehene View zum Lesen von Operanden:
uses
HPDFDoc, HPDFContentStream;
procedure TAuditor.HandleOperator(Sender: TObject;
var EventData: THPDFContentOperatorEventData);
var
Bytes: TBytes;
begin
if EventData.Phase <> covpOperator then
Exit;
// Textausgabe-Operatoren zählen, die innerhalb markierten Inhalts gezeichnet werden
if (EventData.OperatorName = 'Tj') or
(EventData.OperatorName = 'TJ') then
begin
Inc(FTextOperators);
if EventData.State.MarkedContentDepth > 0 then
Inc(FTextInsideMarkedContent);
end;
// Einen Operanden nur lesen, wenn er benötigt wird
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); // nur Audit
Writeln(Format('%d operators visited, max q/Q depth %d',
[Stats.VisitedOperatorCount, Stats.MaximumGraphicsStateDepth]));
finally
Pdf.Free;
end;
end;
Warum ist die View geliehen statt veränderbar?
Der Callback erhält niemals einen veränderbaren Syntaxbaum. Er erhält eine View, die auf Anfrage die Bytes der aktuellen Operation oder einen bestimmten Operanden liefern kann, und das ist alles. Jede Änderung läuft stattdessen über die Felder für Aktion und Ersatz
Diese Indirektion ist keine bloße Förmlichkeit. Eine direkte Baummutation würde es einem Visitor erlauben, das Ersatz-Budget und die Prüfung auf überlappende Bearbeitungen zu umgehen, und überlappende Bearbeitungen in einem Byte-Span-Overlay-Modell erzeugen eine Ausgabe, die still falsch ist, statt laut kaputt zu sein. Das Leiten von Änderungen über das Aktionsfeld bedeutet, dass jede Bearbeitung auf dieselbe Weise validiert wird, unabhängig davon, was der Visitor beabsichtigte
Das bedarfsgesteuerte Lesen hält außerdem den häufigen Fall günstig. Ein Audit, das nur Operatornamen zählt, materialisiert niemals Operanden-Bytes, was bei einem Content-Stream mit Hunderttausenden von Operationen eine Rolle spielt
Das State-Feld beschreibt stets die Eingabe
Jedes Ereignis trägt die Grafikzustandstiefe, die Textobjekttiefe, die Tiefe des markierten Inhalts und die Tiefe des Kompatibilitätsabschnitts so, wie sie in der ursprünglichen Eingabe unmittelbar vor der Ausführung dieses Operators standen. Das gilt auch dann, wenn Ihr Visitor frühere Operatoren gelöscht oder ersetzt hat
Den Zustand an der ursprünglichen Eingabe statt an der bearbeiteten Ausgabe zu verankern ist eine bewusste Entscheidung mit praktischem Nutzen: Dasselbe Audit zweimal über dasselbe Dokument laufen zu lassen liefert identische Ergebnisse, und ein Audit, das parallel zu Bearbeitungen läuft, verschiebt nicht seinen eigenen Bezugsrahmen. Müssen Sie die Tiefe in der Ausgabe kennen, wissen Sie, was Sie gelöscht haben, und können sie berechnen; umgekehrt, stabile Eingabepositionen aus einer sich bewegenden Ausgabe zu rekonstruieren, ist nicht möglich
Löschen und Ersetzen, mit den Regeln, die Sie schützen
Ein Ersatz besteht aus rohen Content-Stream-Bytes, und Sie sind für deren Syntax verantwortlich. Die Schutzmechanismen darum sind das Budget für die Gesamtzahl der Ersatz-Bytes, die Anforderung der Nicht-Überlappung und die optionale Ausgabevalidierung, die das Ergebnis erneut parst, bevor es akzeptiert wird:
procedure TWatermarkStripper.HandleOperator(Sender: TObject;
var EventData: THPDFContentOperatorEventData);
begin
EventData.Action := coaKeep;
if EventData.Phase <> covpOperator then
Exit;
// Die Operatoren verwerfen, die das Wasserzeichen-Formular zeichnen
if (EventData.OperatorName = 'Do') and OperandIsWatermark(EventData) then
EventData.Action := coaDelete
// Eine verirrte Farbe auf das Hausgrau normalisieren
else if EventData.OperatorName = 'rg' then
begin
EventData.Replacement := TEncoding.ANSI.GetBytes('0.2 0.2 0.2 rg');
EventData.Action := coaReplace;
end
// Die gesamte Bearbeitung ablehnen, wenn wir auf etwas Unbekanntes stoßen
else if EventData.OperatorName = 'BI' then
EventData.Action := coaReject;
end;
var
Options: THPDFContentOperatorVisitOptions;
begin
Options := THPDFContentOperatorVisitOptions.Default;
Options.ValidateOutput := True; // vor dem Akzeptieren neu parsen
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;
Der Aktionssatz enthält aus gutem Grund zwei Ausgänge. coaStop beendet den Durchlauf und behält, was bisher entschieden wurde, was richtig ist, wenn Sie gefunden haben, wonach Sie gesucht haben. coaReject bricht den gesamten Durchlauf ab und lässt die Seite unangetastet, was richtig ist, wenn Sie auf ein Konstrukt stoßen, das Ihre Umschreibregeln nicht abdecken. Inline-Bilder sind das klassische Beispiel: Ihre binäre Nutzlast ist nichts, um das herum ein farbnormalisierender Visitor editieren sollte
Die Statistik lesen, und wo der Durchlauf endet
Das Statusfeld unterscheidet zwischen Abschluss, Stopp, Ablehnung, ungültiger Eingabe, überschrittenem Limit, ungültiger Ausgabe und einem Visitor-Fehler. Das sind unterschiedliche Probleme: Ungültige Ausgabe bedeutet, dass sich Ihre Ersatz-Bytes nicht erneut parsen ließen, während ungültige Eingabe bedeutet, dass der Quell-Stream bereits fehlerhaft war und keine Bearbeitung versucht wurde
Die Zähler machen ein Audit selbstprüfend. VisitedOperatorCount muss zusammen mit KeptOperatorCount, DeletedOperatorCount und ReplacedOperatorCount aufgehen, und die maximalen Tiefenwerte sagen Ihnen, ob die Seite tief genug verschachtelt ist, um einen genaueren Blick zu lohnen. SourceBytes gegenüber OutputBytes liefert die Größenauswirkung Ihrer Bearbeitung in einer einzigen Zahl
Für die häufigen Aufgaben gibt es übergeordnete Wege, die überhaupt keinen Visitor erfordern. Das dauerhafte Entfernen von Inhalten mit der Ressourcenbereinigung, die echte Schwärzung benötigt, ist unter Schwärzung und Seitenzusammenführung bei geladenen Dokumenten behandelt. Das Ersetzen sichtbaren Texts ist unter Suchen und Ersetzen in einem bestehenden PDF behandelt. Greifen Sie zum Operator-Visitor, wenn Ihre Regel strukturell statt textuell ist, oder wenn Sie ein Audit benötigen, das kein vorhandener Bericht beantwortet
Content-Stream-Durchlauf, Extraktion und Umschreiben laufen in Delphi und C++Builder auf demselben geladenen Dokumentmodell; die vollständige Funktionsliste finden Sie auf der HotPDF-Delphi-PDF-Komponentenseite