Technischer Artikel

Schwärzungen anwenden in Delphi: RO-Overlay und IC-Füllung

Ein Schwärzungs-Mark, ohne /IC-Farbe gezeichnet, kann aus dem Apply-Schritt als transparente Lücke oder hohler Umriss herauskommen, während der Kommentar des Reviewers dazu in der Datei bleibt. PDFlibPas wendet Redact-Annotationen mit ApplyRedactions an: der abgedeckte Inhalt geht zuerst, das Overlay kommt aus /RO oder aus /IC mit Schwarz als Fallback, und das Dictionary des Marks wird geleert

Markieren ist umkehrbar, Anwenden nicht

ISO 32000-1 §12.5.6.23 spaltet Schwärzung in Inhaltsidentifikation und Inhaltsentfernung. Während der Identifikation ist eine Redact-Annotation nur ein Vorschlag, den Nutzer noch sehen, verschieben und neu definieren können, was eine Review-Runde überhaupt erst möglich macht. Die Entfernung bleibt der Anwendung überlassen, mit festen Anforderungen: alles, was die Annotation identifiziert, geht, die Annotation geht, und der abgedeckte Teil jedes Bildes bekommt seine Daten zerstört, denn ein Clipping-Pfad oder eine Bildmaske über den Pixeln qualifiziert nicht. Die Schlüssel sind in Tabelle 192 von ISO 32000-1 aufgelistet (Tabelle 195 in ISO 32000-2), und die meisten beschreiben, wie die Seite nach der Entfernung aussehen soll, statt was zu entfernen ist

SchlüsselRolle in der SpezifikationWas ApplyRedactions damit macht
QuadPointsZu entfernende Region als n Vierecke; ersetzt Rect als Region, wenn vorhandenSchrubbt die Bounding Box jedes Vierecks getrennt; ein Array, dessen Länge kein Vielfaches von 8 ist, fällt auf Rect zurück
RectZu entfernende Region, wenn QuadPoints fehltBenutzt, mit umgekehrten Ecken normalisiert
ICDeviceRGB-Füllung nach der Entfernung; fehlend heißt transparent; ignoriert, wenn RO existiertBehalten, wenn vorhanden, als Schwarz ergänzt, wenn fehlend
ROForm-XObject-Overlay; geht IC, OverlayText, DA und Q vorAls normale Darstellung installiert und auf die Seite geflattet
OverlayText, RepeatLabel über der Region gezeichnet, optional gekacheltIn das erzeugte Overlay gezeichnet, gekachelt, wenn Repeat true ist
DA, QFont, Größe und Farbe des Labels; AusrichtungGröße und Farbe aus DA gelesen, Ausrichtung aus Q

Marks mit AddRedactAnnotation erzeugen

TPDFlib.AddRedactAnnotation(Left, Top, Width, Height, FillR, FillG, FillB, OverlayText, RepeatOverlay) fügt der gewählten Seite ein Mark hinzu und gibt die neue Annotationszahl der Seite zurück, die auch der 1-basierte Index des Marks ist, oder 0, wenn nichts hinzugefügt wurde. Koordinaten folgen dem aktuellen Ursprung und den Einheiten, also schaltet das Beispiel auf den Ursprung oben links um, wo Top die Oberkante ist. Der Aufruf schreibt /Rect, ein /IC aus den Füllkomponenten und ein festes /DA von /F0 10 Tf 1 1 1 rg, weißen Text auf 10 Punkt; ein nicht-leeres OverlayText ergänzt /OverlayText, /Repeat und /Q 1 für ein zentriertes Label. Redact-Annotationen sind ein PDF-1.7-Feature, also wird ein älterer Header auf 1.7 angehoben. Noch ist kein Appearance-Stream geschrieben; das ausstehende Mark sieht so aus, wie die Anwendung des Reviewers es zeichnet. Bei weiß fixiertem Label paaren Sie OverlayText mit einer dunklen Füllung, denn auf blassgelb verschwindet das Label. Marks lassen sich in jeder PDFlibPas-Edition erzeugen

uses
  System.SysUtils, PDFlibrary;

procedure MarkForReview(const SourceFile, ReviewFile: string);
var
  Pdf: TPDFlib;
  Index: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile(SourceFile, '') <> 1 then
      raise Exception.CreateFmt('Cannot open %s', [SourceFile]);
    Pdf.SetOrigin(1);            // Ursprung oben links: Top ist die Oberkante
    Pdf.SetMeasurementUnits(0);  // Punkte
    if Pdf.SelectPage(1) <> 1 then
      raise Exception.Create('Page 1 not found');
    // Zeugenname: schwarze Füllung, zentriertes weißes Label
    Index := Pdf.AddRedactAnnotation(72, 180, 220, 14, 0, 0, 0, 'REDACTED');
    if Index = 0 then
      raise Exception.Create('Mark was not added');
    // Konto-Block: den Freistellungscode über eine höhere Box kacheln
    Pdf.AddRedactAnnotation(72, 410, 300, 40, 0, 0, 0, '(b)(6)', 1);
    // Nur Marks; jedes Byte des Originalinhalts ist noch vorhanden
    Pdf.SaveToFile(ReviewFile);
  finally
    Pdf.Free;
  end;
end;

QuadPoints entscheiden die Entfernungsfläche; Antworten behalten ihren Text

Trägt ein Mark /QuadPoints, entscheiden diese Vierecke, was ApplyRedactions entfernt, und Rect begrenzt sie nur für die Anzeige. Mit Textauswahl-Werkzeugen gemachte Marks haben meist ein Viereck pro gewählter Zeile, also verschiebt das Vergrößern solcher Marks mit SetAnnotRect nur den Rahmen auf dem Bildschirm und lässt die Entfernungsfläche, wo sie war; SetAnnotQuadPoints ist der Aufruf, der die Region verschiebt, und GetAnnotQuadCount gibt 0 zurück für ein Mark, das allein auf Rect baut. Der Text, der an einem Mark hängt, folgt einer anderen Regel. Review-Werkzeuge halten die Notiz in /Contents und den Autor in /T, und eine Notiz an einem Schwärzungs-Mark beschreibt tendenziell genau das, was entfernt wird („SSN des Mandanten, mit der Rechtsabteilung klären“). ApplyRedactions leert das eigene Dictionary des Marks, aber Antworten, die über /IRT darunter eingereiht sind, sind eigene Annotationen, gelöscht nur, wenn ihr Rechteck eine Schwärzungsregion überlappt, also behält eine im Rand geparkte Antwort ihren Text. Die Auflistung unten druckt jedes Mark mit seiner Viereckzahl und den darunter eingereihten Antworten

QuadPoints gegen Rect-Anatomie im PDFlibPas-Schwärzungsfluss für Delphi: jedes Viereck begrenzt seine eigene Entfernungsfläche, während eine Rand-Antwort ihren Text behält
quadpoints begrenzen die Entfernung Zeile für Zeile, also heißt das Verschieben des Marks SetAnnotQuadPoints, und eine Rand-Antwort außerhalb der Regionen behält ihren Text
procedure ListPendingMarks(Pdf: TPDFlib);
var
  P, I, Parent: Integer;
begin
  for P := 1 to Pdf.PageCount do
  begin
    if Pdf.SelectPage(P) <> 1 then
      Continue;
    for I := 1 to Pdf.AnnotationCount do
      if Pdf.GetAnnotType(I) = 'Redact' then
        Writeln(Format('p%d #%d by %s, quads=%d: %s',
          [P, I, Pdf.GetAnnotTitle(I), Pdf.GetAnnotQuadCount(I),
           Pdf.GetAnnotContentsEx(I)]))
      else
      begin
        Parent := Pdf.GetAnnotReplyTo(I);   // 0, wenn /IRT fehlt
        if (Parent > 0) and (Pdf.GetAnnotType(Parent) = 'Redact') then
          Writeln(Format('  reply #%d to #%d: %s',
            [I, Parent, Pdf.GetAnnotContentsEx(I)]));
      end;
  end;
end;

Welches Overlay erscheint nach ApplyRedactions?

Ein /RO-Eintrag gewinnt, wann immer er vorhanden ist, wie die Vorrangregel in Tabelle 192 verlangt. ApplyRedactions installiert dieses Form XObject als die normale Darstellung der Annotation (/AP /N), wirft jeden Appearance-State weg und flattet ihn, also haben /IC, /OverlayText, /DA und /Q keine Wirkung auf dieses Mark. Die Platzierung ist der eine subtile Teil. Die Spezifikation sagt, der Ursprung des Overlays fluchtet mit der unteren linken Ecke von Rect, während das Flattening die BBox des Forms durch seine Matrix auf Rect abbildet, dieselbe BBox- und Matrix-Platzierung, die das Flattening auf Formularfeld-Widgets anwendet. Ein Overlay, dessen BBox [0 0 w h] ist, auf das Rechteck dimensioniert, mit einer identischen Matrix sieht unter beiden Lesarten gleich aus, also verlangen Sie diese Form, wenn Ihre eigenen Werkzeuge /RO-Marks erzeugen

Ohne /RO normalisiert ApplyRedactions das Dictionary und baut die Darstellung selbst. Es löscht /C und /BS und setzt /Border auf [0 0 0]: diese Einträge beschreiben das ausstehende Mark, meist einen farbigen Umriss, und es in die finale Seite einzubrennen würde eine fertige Schwärzung unfertig aussehen lassen. Ohne den Rand deckt die Füllung das ganze Rechteck. Das Label wird im erzeugten Form gezeichnet, mit Größe und Farbe aus /DA, Ausrichtung aus /Q, Kachelung aus /Repeat, und der Fontname F0 ist in den eigenen Ressourcen dieses Forms an Standard-Helvetica gebunden. Diese Bindung zählt, wenn Dateien zwischen Werkzeugen reisen: ISO 32000-1 §12.7.3.3 definiert einen DA-Fontnamen als Eintrag in den Default-Ressourcen des interaktiven Formulars (/DR), ein Dokument ohne Formularfelder hat keine, und eine andere Anwendung, die dasselbe Mark anwendet, kann einen Fallback-Font einsetzen. PDFlibPas überspringt diese Nachschlage, und mit Helvetica passt das Label zu Lateinschrift-Text wie REDACTED oder einem Freistellungscode

Overlay-Entscheidungsfluss nach ApplyRedactions in PDFlibPas für Delphi: ein vorhandenes RO gewinnt und flattet, ein fehlendes IC fällt auf Schwarz zurück
ein RO-Form-XObject wird als normale Darstellung installiert und geflattet, während Marks ohne eines eine neu gebaute Darstellung und eine schwarze Füllung bekommen, wenn IC fehlt

Ein fehlendes /IC wird eine schwarze Box

Der Default der Spezifikation für ein fehlendes /IC ist ein transparenter Innenraum, und PDFlibPas weicht davon absichtlich ab. Nach der Entfernung zeigt ein transparenter Innenraum das Papier, eine weiße Lücke, wo früher ein Name war, und diese Lücke sieht genauso aus wie ein Layout, das nie etwas dort hatte. Ein Reader kann Geschwärztes nicht von Leerem unterscheiden, und ein Reviewer kann die Schwärzung nicht bestätigen, ohne extrahierten Text zu vergleichen. Ein hohler Rahmen, was ein Apply-Schritt erzeugt, wenn er die ausstehende Darstellung eines nur-Umriss-Marks flattet, scheitert anders: er liest sich als Vorschlag, auf den niemand agiert hat. Wir haben abgewogen, dem Default wörtlich zu folgen, und die schwarze Box gewählt, denn das sichtbare Zeichen, dass etwas zurückgehalten wurde, ist Teil dessen, was ein geschwärztes Dokument seinem Leser sagt, und die Wahl betrifft nur Marks, deren Autor nie eine Füllung angab. Ein /IC, bereits im Dictionary, wird benutzt, welchen Wert es auch hat, also bekommt ein Producer, der Weiß verlangte, Weiß

Was weg ist, bevor das Overlay gezeichnet wird

ApplyRedactions arbeitet Seite für Seite und schließt jede Entfernung ab, bevor es irgendein Overlay flattet, also kann ein Overlay nie von seiner eigenen Region erwischt werden. Pro Seite sammelt es ein Rechteck pro Viereck, oder das Rect von Marks ohne Vierecke, und schrubbt jede Inhalts-Ebene gegen jedes davon:

  • Text wird Glyph für Glyph geschnitten, gemessen aus den Seiten-Fonts und dem vollen Textzustand, und der Rest einer teilweise abgedeckten Zeichenkette bleibt an seiner Originalposition
  • Vektorpfade, Inline-Bilder und Shadings, die in der Region malen, werden aus dem Content-Stream gelöscht
  • Form XObjects werden rekursiv betreten; ein Form, das andere Seiten oder Platzierungen ebenfalls zeichnen, wird über eine private Kopie geschwärzt, also behalten diese Seiten das Original
  • Bild-XObjects verlieren nur die abgedeckten Pixel, überschrieben mit Schwarz (volle K-Farbe für CMYK); JPEG-, JPEG 2000-, JBIG2- und CCITT-Bilder werden dekodiert und als verlustfreie Flate-Streams zurückgespeichert
  • In getaggtem PDF werden /Alt und /ActualText, die an entferntem Marked Content hängen, im Seiteninhalt und im Strukturbaum geleert
  • Andere Annotationen, deren Rechteck die Region trifft, werden gelöscht, und Formularfelder, deren Widgets dort sitzen, verlieren ihre Werte

Der Pixelschritt ist die Bildklausel der Spezifikation in der Praxis. Ein schwarzes Rechteck über einem gescannten Pass beruhigt das Auge und scheitert trotzdem an §12.5.6.23, denn die Originalsamples dekodieren, sobald jemand das Bild extrahiert. Dieselbe Engine sitzt hinter RedactRegion, das Inhalt aus einem Rechteck entfernt, das Sie direkt übergeben, und sich mit SanitizeDocument paart, ohne irgendeine Annotation zu lesen. Ist der Inhalt weg, wird jedes Mark geflattet und sein Dictionary bis auf /Type und /Subtype geleert. Das Flattening entfernt die Annotation aus dem /Annots-Array der Seite, aber das Dictionary ist weiterhin ein Objekt im Dokument, und ein Save, das es schreibt, würde /Contents, /RC, /T und den Rest der Review-Spur dem Empfänger aushändigen

Speichern ohne die ungeschwärzte Revision

Ein inkrementelles Save macht alles Obige zunichte. Nach §7.5.6 (dieselbe Nummer in ISO 32000-1 und ISO 32000-2) hängt ein inkrementelles Update geänderte Objekte ans Dateiende an und lässt die Originalbytes intakt, also bleibt die Originalrevision, mit ihrem Seiteninhalt, ihren Bildern und den markierten Schwärzungen, lesbar für jeden, der die Datei am früheren %%EOF abschneidet; ExtractDocumentRevision in der PDFlibPas Small Business Edition macht das in einem Aufruf. Nach ApplyRedactions schreiben Sie eine neue Datei mit SaveToFile, nie SaveChanges, das anhängt. Vor dem Speichern fegt GarbageCollectObjects die Objekttabelle und wirft Objekte weg, die nichts mehr referenzieren, etwa die Appearance-Streams, die die Marks vor dem Apply-Schritt trugen; ein vollständiges Save überspringt diesen Ausflug, außer SetGarbageCollect schaltet ihn ein, oder das GarbageCollect-Feld des TPDFlibSaveOptions-Records, den Sie SaveToFileOptions übergeben, ist gesetzt. Metadaten sind die restliche Lücke. SanitizeDocument(SANITIZE_METADATA or SANITIZE_XMP_PACKET) leert Author, Subject, Keywords, Creator und Producer und entfernt den XMP-Stream, das Title überlässt es Ihnen zur Prüfung. Meiden Sie SANITIZE_ALL hier: es schließt SANITIZE_ANNOTATIONS ein und löscht jede Annotation, die im Dokument übrig ist, Notizen, die Sie behalten wollten, eingeschlossen

Save-Zeitlinie nach ApplyRedactions in PDFlibPas für Delphi: ein inkrementelles Anhängen lässt das Original wiederherstellbar, also läuft die Freigabe Sanitize, Garbage Collect und eine vollständige Neuschreibung
nach dem Apply ist der Freigabe-Pfad Metadaten bereinigen, nicht mehr referenzierte Objekte kehren und eine neue Datei speichern, denn ein inkrementelles Save hält die ungeschwärzte Revision lesbar

ApplyRedactions ist wie RedactRegion in der Enterprise Edition verfügbar. Auf einem schlichten TPDFlib gibt es 0 zurück und setzt LastErrorCode auf PDFLIB_ERROR_ENTERPRISE_REQUIRED (900), also benutzt der Freigabe-Schritt TPDFlibEnterprise aus der Unit PDFlibEnterprise

uses
  System.SysUtils, PDFlibrary, PDFlibEnterprise;

procedure ApplyAndRelease(const ReviewFile, ReleaseFile: string);
var
  Pdf: TPDFlibEnterprise;
  Applied, Orphans: Integer;
begin
  Pdf := TPDFlibEnterprise.Create;
  try
    if Pdf.LoadFromFile(ReviewFile, '') <> 1 then
      raise Exception.CreateFmt('Cannot open %s', [ReviewFile]);
    Applied := Pdf.ApplyRedactions;   // jedes Redact-Mark auf jeder Seite
    if Applied = 0 then
      raise Exception.CreateFmt('No marks applied (error %d)',
        [Pdf.LastErrorCode]);
    // Info und XMP können noch benennen, was die Seiten nicht mehr zeigen
    Pdf.SanitizeDocument(SANITIZE_METADATA or SANITIZE_XMP_PACKET);
    Orphans := Pdf.GarbageCollectObjects;
    // Vollständige Neuschreibung in eine neue Datei; nie SaveChanges nach einer Schwärzung
    if Pdf.SaveToFile(ReleaseFile) <> 1 then
      raise Exception.Create('Save failed');
    Writeln(Format('%d marks applied, %d unreferenced objects dropped',
      [Applied, Orphans]));
  finally
    Pdf.Free;
  end;
end;

Erwartet: ApplyRedactions gibt die Anzahl der Redact-Marks zurück, die es über alle Seiten geflattet hat, 2 für die von MarkForReview geschriebene Review-Kopie, und GarbageCollectObjects gibt zurück, wie viele nicht mehr referenzierte Objekte der Ausflug fallen ließ

Welches Werkzeug auch immer ein Mark gezeichnet hat, seine /RO- und /IC-Einträge entscheiden, was der Empfänger auf der Seite sieht, während der Apply-Schritt in PDFlibPas Enterprise for Delphi entscheidet, was darunter weg ist