Technischer Artikel

PDF-Content-Streams sicher in Delphi umschreiben

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

PDF-Content-Stream-Operatoren wie BT, Tf und TJ fließen in die Visitor-Ereignisdaten mit Phase, Operatorname, Operandenanzahl, Byte-Bereich und Verschachtelungszustand und liefern dann eine von fünf Aktionen zurück: behalten, ersetzen, löschen, stoppen oder ablehnen
Jeder besuchte Operator liefert genau eine Entscheidung, und ein Durchlauf, der alles behält, schreibt unberührte Bereiche für bytegenau identische Ausgabe zurück

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:

Umschreib-Schutzmechanismen in Delphi: Ein Visitor deklariert Action und Replacement, die durch das Ersatz-Budget, die Nicht-Überlappungs-Prüfung des Byte-Bereichs und die Ausgabevalidierung zur validierten gespeicherten Seite geleitet werden
Jede Änderung läuft über das Aktionsfeld durch die Budget-, Überlappungs- und Neu-Parse-Prüfungen, sodass eine fehlgeschlagene Validierung abbricht, statt kaputte Operatoren zu schreiben
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

Audit-Statistik-Abgleich, bei dem die Anzahl behaltener, gelöschter und ersetzter Operatoren die Gesamtzahl der besuchten Operatoren ergibt, daneben sieben Statusabschlüsse von abgeschlossen bis Visitor-Fehler, mit dem Hinweis, dass Zustandstiefen die ursprüngliche Eingabe beschreiben, sodass Audits identisch wiederholbar sind
Zähler, die aufgehen müssen, plus ein Status, der jeden möglichen Abschluss benennt, machen ein Audit selbstprüfend auf Dokumenten, die Sie nicht selbst erstellt haben

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