Technischer Artikel

PDF-Schwärzung und N-up-Stitch in Delphi mit HotPDF

Eine Anfrage landet auf Ihrem Schreibtisch: Nehmen Sie einen Stapel bereits gerenderter Kontoauszüge, schwärzen Sie die Kontonummern und geben Sie zwei Seiten pro Blatt aus, um Papier zu sparen. Beide Teile dieser Aufgabe sind Eingriffe in den Content-Stream einer PDF-Datei, die Sie nicht selbst erstellt haben. Es gibt also weder eine komfortable Seite zum Zeichnen noch einen Schriftarten-Manager, auf den Sie sich stützen könnten. Sie bearbeiten den Objektgraphen eines geladenen Dokuments direkt und hängen rohe Zeichenoperatoren an eine Seite an, die von einem anderen Werkzeug gestaltet wurde. HotPDF bietet genau zwei Einstiegspunkte dafür, und der gefährlichere von beiden ist derjenige, der harmlos aussieht

HotPDF ist eine native VCL-PDF-Komponente für Delphi und C++Builder. Die Version 9 ihrer API für geladene Dokumente fügte die ersten Methoden hinzu, die nagelneue Inhalte auf einer von der Festplatte geöffneten Seite erstellen, anstatt auf einer, die Sie von Grund auf neu aufgebaut haben. Zwei davon sind hier das Thema: RedactLoadedRect, das ein undurchsichtiges Rechteck über einen Bereich zeichnet, und StitchLoadedPage, das eine Seite skaliert und auf eine andere zeichnet. Beide arbeiten, indem sie ISO 32000-1 §8.5 Content-Stream-Operatoren in den /Contents-Stream der Seite schreiben. Zu verstehen, was diese Operatoren tun und – ebenso wichtig – was sie nicht tun, macht den Unterschied zwischen einem funktionierenden Werkzeug und einer Datenpanne aus

Anhängen von Operatoren an eine geladene Seite

Wenn Sie eine Seite mit die normalen HotPDF-API erstellen, besitzt die Komponente den Content-Stream und serialisiert Ihre TextOut- und Vektoraufrufe für Sie. Eine geladene Seite ist anders: Ihre /Contents ist ein bereits vorhandenes Stream-Objekt, das möglicherweise freigegeben ist oder Teil eines Inhalts-Arrays ist, und Sie müssen sich darin einklinken, ohne das Vorhandene zu beschädigen. Die Version 9 führte drei kleine Hilfsfunktionen ein, die dies sicher machen. NewIndirectStream weist ein neues indirektes THPDFStreamObject mit einem leeren Puffer und einem Eintrag /Length 0 zu; ResolveLoadedStream folgt einer indirekten Referenz bis zum zugrunde liegenden Stream; und AppendLoadedStream schreibt rohe Bytes an das Ende des Streams und schreibt /Length neu, damit das gespeicherte Objekt fehlerfrei bleibt

Das Muster, dem beide öffentlichen Methoden folgen, ist dasselbe. Suchen Sie die /Contents der Seite, lösen Sie sie in einen Stream auf, und wenn kein brauchbarer Stream vorhanden ist, erstellen Sie einen und hängen Sie ihn an. Hängen Sie dann die Operatoren an. Da die neuen Bytes am Ende des Streams eingefügt werden, garantiert das Malermodell, dass sie über allem gerendert werden, was das ursprüngliche Layout gezeichnet hat. Diese Reihenfolge ist der gesamte Mechanismus hinter dem Schwärzungsrechteck, und es ist auch der Grund, warum dieses Rechteck nicht das ist, was die meisten Leute vermuten

RedactLoadedRect: Eine undurchsichtige Abdeckung, kein Löschen

RedactLoadedRect übernimmt einen nullbasierten Seitenindex, vier Benutzerraum-Koordinaten und drei Farbkomponenten im Bereich von 0 bis 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;

Unter der Haube gibt die Methode drei Operatoren in den Content-Stream aus: eine Füllfarbe in DeviceRGB (r g b rg), einen Rechteckpfad (x y w h re) und eine Füllung (f). Breite und Höhe werden als X2 - X1 und Y2 - Y1 berechnet, sodass Sie zwei gegenüberliegende Ecken übergeben und die Methode den Bereich berechnen lassen. Übergeben Sie 0, 0, 0 für die Farbe, um einen schwarzen Balken zu erhalten; übergeben Sie 1, 1, 1 für einen weißen Balken, der zu einer weißen Seite passt. Die Koordinaten beziehen sich auf den eigenen Benutzerraum der geladenen Seite, was bedeutet, dass der Ursprung die linke untere Ecke ist und die Einheiten in Punkten angegeben werden. Dies bedeutet auch, dass Sie die /MediaBox der Seite benötigen, um etwas präzise zu platzieren; GetLoadedPageBox mit pbMediaBox liefert Ihnen diese

Lesen Sie dies zweimal: Ein gefülltes Rechteck deckt Inhalte visuell ab, entfernt sie jedoch nicht. Der Text, das Bild oder die Vektorgrafik unter dem Rechteck ist weiterhin in der PDF-Datei vorhanden, weiterhin im Objektgraphen und kann von jedem extrahiert werden, der die Seite kopiert, ein Textextraktionswerkzeug ausführt oder einfach Ihr Rechteck aus dem Content-Stream löscht. Dies ist eine visuelle Maskierung, keine Schwärzung im rechtlichen oder sicherheitsbezogenen Sinne. Wenn Sie wirklich sensible Daten verbergen – Kontonummern, Krankenakten, Identitäten oder regulierte Informationen –, ist das Abdecken mit einem schwarzen Feld und das Versenden der Datei ein vorprogrammiertes Datenleck. Eine echte Schwärzung erfordert das Löschen der zugrunde liegenden Inhaltsobjekte, nicht das Übermalen

Der Methodenname sagt „Redact“ (Schwärzen), und das ist eine nützliche Warnung davor, wie das Ergebnis missverstanden wird, kein Versprechen über das, was gelöscht wird. Die Implementierung ist in ihrem eigenen Kommentar ehrlich darüber: Sie bezeichnet sich selbst als „visuelles Schwärzungsprimitiv“ und stellt fest, dass eine inhaltslöschende Schwärzung einen Content-Stream-Interpreter erfordert, der die vorhandenen Operatoren durchläuft und umschreibt. Der Pfad für geladene Dokumente von HotPDF tut dies hier nicht. Die Sicherheitsregel ist daher eng gefasst: Verwenden Sie RedactLoadedRect für unempfindliche, kosmetische Maskierungen – zum Ausblenden eines Entwurfswasserzeichens, zum Ausblenden eines Bereichs vor einem Screenshot oder zum Abdecken eines veralteten Logos auf einem internen Prüfbeleg. Sobald der Inhalt unter dem Feld im Falle eines Lecks eine Rolle spielen würde, ist diese Methode das falsche Werkzeug, und die richtige Antwort besteht darin, das Dokument ohne die Daten neu zu generieren oder eine echte Inhaltsentfernungs-Pipeline zu verwenden

StitchLoadedPage: Skalieren, verschieben, zeichnen

Die N-up-Ausschussseite ist das einfachere Problem, da nichts verborgen, sondern nur neu angeordnet wird. StitchLoadedPage übernimmt einen Zielseitenindex, einen Quellseitenindex, einen X/Y-Versatz und einen Skalierungsfaktor und zeichnet die Quellseite in dieser Position und Größe auf das Ziel:

// 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);

Die angehängte Operatorzeichenfolge ist eine standardmäßige Transformations- und Zeichensequenz: q zum Speichern des Grafikzustands, eine cm-Matrix, die die Skalierung auf der Diagonale und den Versatz in den Translationsfeldern enthält, /StitchSrc Do zum Aufrufen eines externen Objekts und Q zum Wiederherstellen des Zustands. Das q/Q-Paar ist wichtig: Es isoliert die Transformation, sodass die angeheftete Seite ihr Koordinatensystem nicht in nachfolgend angehängte Elemente überträgt. Die Methode beugt auch offensichtlichen Fehlern vor – Indizes außerhalb des Bereichs, Ziel identisch mit der Quelle, eine nicht-positive Skalierung (die sie auf 1.0 begrenzt) – und bricht geräuschlos ab, anstatt eine Ausnahme auszulösen. Überprüfen Sie also Ihre Eingaben, da ein stiller Fehlschlag identisch mit einem Erfolg aussieht

StitchLoadedPageSideBySide ist eine einfache Komfortfunktion über der allgemeinen Methode. Sie liest die Media-Box-Breite des Ziels, halbiert sie und ruft StitchLoadedPage mit dieser halben Breite als X-Versatz und einer festen Skalierung von 0.5 auf, wodurch die Quelle auf der rechten Hälfte platziert wird. Diese hartcodierte 0.5 setzt voraus, dass Quelle und Ziel die gleiche Breite haben. Wenn dies nicht der Fall ist, füllt die Quelle ihre Hälfte nicht sauber aus, und Sie müssen die allgemeine Methode StitchLoadedPage mit einer Skalierung verwenden, die Sie selbst aus beiden Media-Boxes berechnen

Die vereinfachte XObject-Strategie und ihr ISO-Kompromiss

Hier macht die Implementierung eine bewusste Abkürzung, die Sie kennen müssen, bevor Sie der Ausgabe in verschiedenen Viewern vertrauen. Ein korrektes N-up-Ausschießen verpackt den Inhalt der Quellseite in ein Form XObject – ein eigenständiges zeichenbares Objekt, das laut ISO 32000-1 §8.10.1 die Einträge /Type /XObject, /Subtype /Form und eine eigene /BBox-Begrenzungsbox enthalten muss. Der Stitch-Prozess der Version 9 von HotPDF baut diesen Wrapper nicht auf. Stattdessen registriert er das Quell-Seiten-Dictionary selbst direkt unter den /Resources /XObject des Ziels mit dem Namen StitchSrc und zeichnet es dann mit Do. Ein Seiten-Dictionary und ein Form XObject teilen sich genügend Gemeinsamkeiten in ihrem Inhaltsmodell – beide verweisen auf einen Content-Stream und ein Ressourcen-Dictionary –, sodass viele Reader das Ergebnis rendern

Es handelt sich jedoch nicht um ein konformes Form XObject. Es fehlen die Markierung /Subtype /Form und eine eigene /BBox, was bedeutet, dass ein strenger Reader das Recht hat, das Do zu ignorieren oder es anders zu beschneiden, als Sie erwarten. Die TechnicalNotes für diese Version sagen es ganz deutlich: Der Ansatz „rendert unter den meisten Readern“, ist aber „kein streng ISO-konformes Form XObject“, und für die vollständige Konformität muss in einem separaten Schritt ein echter Form XObject-Stream synthetisiert werden. Behandeln Sie die Stitch-Ausgabe daher wie jedes nicht-konforme Konstrukt: Überprüfen Sie sie in den spezifischen Viewern, die Ihre Kunden verwenden, nicht nur in dem auf Ihrem Rechner. Wenn Sie Archivierungs-PDFs oder PDFs für strenge Validatoren benötigen, verlassen Sie sich nicht auf diesen Pfad. Dieselbe Disziplin gilt für alles, was Sie auf dem geladenen Objektgraphen aufbauen. Aus diesem Grund verdient ein PDF-Preflight-Durchlauf in Delphi seinen Platz in der Release-Pipeline, wann immer Sie Dokumente programmgesteuert verändern

Wo diese Methoden passen und wo nicht

Beide Methoden sind Content-Stream-Werkzeuge, sodass das mentale Modell dasselbe ist, das Sie für das direkte Zeichnen verwenden. Wenn Sie mit der Komponente Seiten von Grund auf neu aufgebaut haben, werden Ihnen die Vektor- und Farbberechnungen hinter diesen Aufrufen aus dem Zeichnen auf dem HotPDF-Canvas in Delphi bekannt vorkommen; der Unterschied besteht nur darin, dass Sie hier an einen Stream anhängen, den jemand anderes verfasst hat, anstatt an einen eigenen. Beachten Sie drei Grenzen:

  • Schwärzung ist kosmetisch. RedactLoadedRect übermalt Inhalte und löscht sie niemals. Generieren Sie bei sensiblen Inhalten die Quelle neu oder verwenden Sie eine echte Inhaltsentfernung – ein schwarzes Feld ist keine Sicherheit
  • Stitching ist konstruktionsbedingt nicht konform. Die Quellseite wird als Pseudo-XObject ohne das §8.10.1 /Subtype /Form und /BBox referenziert. Überprüfen Sie daher das Rendern in Ihren Ziel-Viewern und vermeiden Sie es dort, wo eine strenge Validierung erforderlich ist
  • Koordinaten beziehen sich auf den Benutzerraum der Seite. Ursprung unten links, Einheiten in Punkten, gesteuert durch die eigene Media-Box der Seite. Lesen Sie die Box mit GetLoadedPageBox, bevor Sie etwas platzieren, da die geladene Seite möglicherweise nicht die von Ihnen angenommene Größe hat

Innerhalb dieser Grenzen deckt das Paar einen echten Arbeitsablauf ab: Seiten für den Druck neu anordnen, unkritische Bereiche maskieren und das Ergebnis mit SaveLoadedDocument zurückschreiben – alles ohne vollständiges erneutes Rendern. Die API für geladene Dokumente, die diese Stitch- und Maskierungs-Primitiven enthält, wird zusammen mit der HotPDF Component für Delphi und C++Builder ausgeliefert, neben den Formularfeld-, Anmerkungs- und FDF-Methoden aus derselben Version