Eine falsche Ziffer in einer Rechnungsnummer, und das einzige verfügbare Bearbeitungsprimitive schreibt den gesamten Textlauf neu. PDF Library for Delphi schließt diese Lücke: GetTextBlockCharContentLocation führt jede extrahierte UTF-16-Position zurück zu der Content-Stream-Anweisung, dem Operanden und dem kodierten Bytebereich, der sie erzeugt hat, und ReplaceTextBlockCharSourceBytes überschreibt nur diesen Bereich. Bei normaler Textextraktion geht alles verloren, was man dafür bräuchte. Es bleiben Unicode, Breiten und Geometrie, die Herkunft verschwindet, und das Zeichen an Position 7 von Block 3 ist nur noch ein Zeichen. Welcher Stream, welche Anweisung, welcher Operand, welches Byte in diesem Operanden: weg. Jede darauf aufbauende Punktbearbeitung muss raten, meist indem sie im dekodierten Inhalt nach einem Teilstring sucht und hofft, dass er genau einmal vorkommt. Auf einer echten Seite ist das nicht der Fall
Warum zerstört das Neuschreiben eines ganzen Textlaufs die Seite?
Weil ein Lauf nicht nur aus Text besteht. Zu den Textanzeigeoperatoren aus ISO 32000-1 §9.4.3 gehört TJ, dessen Operand ein Array ist, das Strings mit numerischen Anpassungen verschachtelt, und genau diese Zahlen bilden den Satz. Eine mit [(AB) -120 (CD)] TJ gesetzte Zeile trägt zwischen den beiden Strings ein Kerning von 120 Tausendstel eines Em. Gibt man einen frischen Tj mit dem zusammengefügten Text aus, ist das Kerning weg, die Zeile verschiebt sich um ein paar Pixel und auf einem Formular rutscht der Wert aus seiner Box. Dasselbe gilt für den Font: Die Bytes des Operanden sind Codes in der von Tf ausgewählten Kodierung und nicht Unicode; bei einem Composite-Font können sie zweibyte CIDs ohne Bezug zum vom Extractor gelieferten Zeichen sein. Einen Lauf neu zu erzeugen verlangt, dass Kodierung, /ToUnicode-Map und Glyph-Abdeckung des Fonts korrekt getroffen werden. Punktbearbeitung umgeht all das, indem sie nie den Bytebereich verlässt
Was liefert GetTextBlockCharContentLocation zurück?
Die Methode löst ein Zeichen in einen Record mit neun Feldern auf, und jedes Feld ist eine Adresse statt eines Werts. ContentLayer ist der 1-basierte Index im /Contents-Array der Seite oder 0, wenn das Zeichen aus verschachteltem Inhalt stammt. StreamObjectNumber und StreamGeneration identifizieren den umschließenden Stream. InstructionIndex ist die nullbasierte Position im dekodierten Inhaltsprogramm, OperandIndex der Textstring-Operand und ArrayElementIndex das Element innerhalb eines TJ-Arrays oder -1 bei einem direkten String-Operand. Danach benennen SourceByteOffset und SourceByteLength den Bytebereich innerhalb dieses dekodierten Strings
Var
Lib: TPDFlib;
ListID, Block, CharPos: Integer;
ContentLayer, StreamObjectNumber, StreamGeneration: Integer;
InstructionIndex, OperandIndex, ArrayElementIndex: Integer;
SourceByteOffset, SourceByteLength, Flags: Integer;
Begin
Lib:= TPDFlib.Create;
Try
Lib.LoadFromFile('invoice.pdf', '');
Lib.SelectPage(1);
ListID:= Lib.ExtractPageTextBlocks(3);
Try
// Block und CharPos stammen aus Ihrem eigenen Scan von GetTextBlockText
If Lib.GetTextBlockCharContentLocation(ListID, Block, CharPos,
ContentLayer, StreamObjectNumber, StreamGeneration,
InstructionIndex, OperandIndex, ArrayElementIndex,
SourceByteOffset, SourceByteLength, Flags)= 1 Then
Begin
// ContentLayer = 0 bedeutet, dass das Glyph in einem verschachtelten Form XObject liegt
// ArrayElementIndex = -1 bedeutet einen einfachen Tj-Operand, kein TJ-Array
End;
Finally
Lib.ReleaseTextBlocks(ListID);
End;
Finally
Lib.Free;
End;
End;
Die Abfrage kostet zur Anfragezeit nichts. Während der Renderer jede Inhaltsebene dekodiert, registriert er die logischen Bereiche, die er durchläuft. Eine Positionsabfrage ist deshalb eine Binärsuche über einer sortierten Intervallliste und kein linearer Scan aller Inhaltsbereiche für jedes Zeichen. Beim Abfragen wird nichts neu geparst; die Map wurde während des ohnehin bezahlten Extraktionsdurchlaufs aufgebaut. Wenn Sie bereits Treffer mit PDF-Textsuche mit Trefferkoordinaten aufzählen, kostet eine Content-Location je Treffer fast nichts
Bytes bearbeiten, nicht Unicode
ReplaceTextBlockCharSourceBytes nimmt ein AnsiString mit rohen Ersatzbytes in der aktiven PDF-Fontkodierung an. Das ist das gesamte Design, und es ist beabsichtigt. Nichts wird transkodiert, nichts neu kodiert, nichts über die Schrift geraten. Die Bibliothek schneidet Ihre Bytes über den benannten Bereich des Zielstrings und gibt die umschließende Inhaltsebene neu aus. Angrenzende Strings im selben TJ-Array und die numerischen Kernings dazwischen bleiben byteidentisch. Im Beispiel oben liefert die Suche nach dem B in [(AB) -120 (CD)] TJ ArrayElementIndex 0, SourceByteOffset 1 und SourceByteLength 1. Ersetzt man es durch Z, enthält der ausgegebene Inhalt (AZ), gefolgt von -120 und (CD), beide unverändert. Die Regressionssuite prüft genau das, denn die Aussage "Kerning erhalten" hört leise auf zu stimmen, wenn man sie nicht testet
Function EditableHere(Flags: Integer): Boolean;
Begin
Result:= ((Flags and PDF_TEXT_CHAR_CONTENT_LOCATION_VALID)<> 0)and
((Flags and (PDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED or
PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT or
PDF_TEXT_CHAR_CONTENT_LOCATION_NESTED or
PDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER or
PDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED))= 0);
End;
// ...
If EditableHere(Flags) Then
Begin
If Lib.ReplaceTextBlockCharSourceBytes(ListID, Block, CharPos, 'Z')= 1 Then
Begin
// Jede Location in der alten Liste ist jetzt veraltet. Neu extrahieren.
Lib.ReleaseTextBlocks(ListID);
ListID:= Lib.ExtractPageTextBlocks(3);
End
Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_STALE Then
// Die Ebene hat sich seit der Extraktion unter uns geändert
Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_READ_ONLY Then
// Ein Flag, dessen Prüfung wir verpasst haben, oder ein später hinzugefügtes Flag
End;
Zwei operative Details sollte man verinnerlichen. Der Aufruf wechselt vorübergehend auf die Seite, aus der die Textliste extrahiert wurde, und stellt die zuvor ausgewählte Seite bei Erfolg und Fehler gleichermaßen wieder her; der Cursor wird also nicht still verschoben. Bei Erfolg werden außerdem die Snapshots der Seitenelemente gelöscht, wodurch alle Handles ungültig werden, die Sie aus einem früheren Enumerationsdurchlauf noch halten
Welche Zeichen lassen sich nicht bearbeiten?
Es gibt sechs Kategorien, und die Bibliothek benennt jede im Flags-Bitmaskenfeld, statt vage zu scheitern. Das ist wichtiger als der Erfolgsweg, denn in realen Dokumenten sind die nicht abbildbaren Fälle häufig, und jeder hat einen anderen Grund
PDF_TEXT_CHAR_CONTENT_LOCATION_LIGATURE: Mehrere extrahierte UTF-16-Positionen entstehen aus einem Quell-Glyph. Ein/ToUnicode-Eintrag, der einen Code auffiabbildet, liefert zwei Zeichen mit demselben Bytebereich. Behandeln Sie sie als ein Quell-Glyph und bearbeiten Sie den Bereich einmalPDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED: Das Zeichen wurde beim Layout synthetisiert. Abgeleitete Wortzwischenräume sind der übliche Fall; sie besitzen überhaupt keine Quellbytes, daher kommtSourceByteOffsetals -1 undSourceByteLengthals 0 zurückPDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT: Der gelesene Text stammt aus einer/ActualText-Ersetzung. Es gibt keine eindeutige Rückabbildung des ersetzten Strings auf Quellbytes, daher ist die Location nur diagnostischPDF_TEXT_CHAR_CONTENT_LOCATION_NESTED: Das Glyph liegt in einem Form XObject. Die Bytes sind adressierbar, aber das Form kann von mehreren Seiten gezeichnet werden. Eine Bearbeitung über die High-Level-API wäre daher eine Änderung, die Sie nicht angefordert habenPDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED: Der Operand war ein Hexstring mit einem UTF-16BE-Byte-Order-Mark, den der bestehende Extraktionspfad vor dem Font-Mapping dekodiert. Offsets im dekodierten Ergebnis adressieren dann nicht mehr die Originalbytes, daher wird das Valid-Flag gelöschtPDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER: String-Operand und Textanzeigeoperator liegen in zwei verschiedenen Streams
Der letzte Fall verdient einen eigenen Satz, denn Ingenieure nehmen regelmäßig an, dass er nicht vorkommen kann. ISO 32000-1 §7.8.2 sagt, dass die Streams im /Contents-Array einer Seite verkettet werden und die Trennung nur an einer lexikalischen Grenze liegen muss. BT /F1 16 Tf 220 340 Td (CrossLayer) in einem Stream und Tj ET im nächsten ist deshalb eine völlig legale Seite. Das Mapping behält die Diagnoseposition bei, markiert sie aber als schreibgeschützt, weil der Anweisungsindex des Operators zu einer anderen Ebene gehört als die Bytes des Operanden und eine gemeinsame Adressierung die Datei beschädigen würde
Woher weiß die Bibliothek, dass die Map noch gültig ist?
Durch Fingerabdrücke, die unmittelbar vor dem Schreiben geprüft werden. Jede Extraktionsliste speichert die Quellseite sowie für jede Inhaltsebene deren Länge und zwei unabhängige rollende Hashes: einen FNV-1a-Hash und einen DJB2-ähnlichen XOR-Hash. Vor ReplaceTextBlockCharSourceBytes liest die Bibliothek die Zielebene erneut ein und vergleicht alle drei Werte. Jede Byteänderung irgendwo in dieser Ebene liefert PDFLIB_ERROR_TEXT_LOCATION_STALE, und der Schreibvorgang findet nicht statt. Das ist bewusst konservativ: Die Prüfung gilt pro Ebene und nicht pro Anweisung, daher macht auch eine unabhängige Änderung an anderer Stelle desselben Content-Streams Ihre Location ungültig. Das ist der richtige Kompromiss: Ein Offset in einem Stream, der sich auch nur um ein Byte verschoben hat, ist kein fast richtiger Treffer, sondern eine stille Beschädigung. Dieselbe Disziplin gilt für die übrige Bearbeitungsfläche, einschließlich des Content-Stream-State-Trackers für CTM und Clipping. Nach jeder erfolgreichen Ersetzung die Liste verwerfen und erneut extrahieren
Schreibgeschütztes Mapping über Direct Access
DAGetTextBlockCharContentLocation liefert für eine über den Direct-Access-Weg geöffnete Seite denselben Record mit demselben Vokabular an Flags. Das ist konstruktionsbedingt nur diagnostisch: ReplaceTextBlockCharSourceBytes arbeitet am ausgewählten bearbeitbaren Dokument, Direct Access ist ein Leseweg. Die Location-Daten bleiben nach dem Schließen des Dateihandles in der Textblockliste erhalten, sodass sie für Offline-Audits verwendet werden können
FileHandle:= Lib.DAOpenFileReadOnly('audit.pdf', '');
Try
PageRef:= Lib.DAFindPage(FileHandle, 1);
DirectList:= Lib.DAExtractPageTextBlocks(FileHandle, PageRef, 3);
Try
Lib.DAGetTextBlockCharContentLocation(DirectList, Block, 1,
ContentLayer, StreamObjectNumber, StreamGeneration,
InstructionIndex, OperandIndex, ArrayElementIndex,
SourceByteOffset, SourceByteLength, Flags);
// Locations bleiben nach DACloseFile lesbar
Finally
Lib.DAReleaseTextBlocks(DirectList);
End;
Finally
Lib.DACloseFile(FileHandle);
End;
Nutzen Sie das, um Fragen zu beantworten, nicht um Dinge zu verändern. Welche Seiten enthalten Text, den Sie nie an Ort und Stelle bearbeiten könnten? Wie viel dieses Korpus kommt mit /ActualText-Überschreibungen? Welche Hersteller-Ausgabe teilt Operatoren über mehrere Content-Ebenen? Sobald jedes Zeichen eine Adresse hat, sind das billige Abfragen, und sie lohnen sich vor dem Aufbau einer Korrekturpipeline
Wo Punktbearbeitung endet
Punktbearbeitung ist ein Skalpell und keine Text-Engine. Sie verändert Bytes an Ort und Stelle, daher wird Ersatztext, der breiter oder schmaler als das Original ist, nicht neu gesetzt, nicht umgebrochen und aktualisiert auch die angrenzenden Kernings nicht. Eine Ziffer in einem monospaced Feld durch eine andere zu ersetzen, passt gut. Einen Absatz neu zu tippen, nicht. Und es ist ausdrücklich kein Sicherheitswerkzeug: Das Überschreiben von Glyphbytes lässt die Originalbytes in der Versionshistorie der Datei wiederherstellbar. Alles mit Vertraulichkeitsanforderung gehört daher in echte Redaction, die Inhalt entfernt statt ihn zu überdecken. Im Gegenzug erhalten Sie Ehrlichkeit. Jedes Zeichen hat entweder eine Byteadresse, an der Sie handeln können, oder ein benanntes Flag, das erklärt, warum nicht, und die Fingerabdruckprüfung macht aus einer veralteten Map einen harten Fehler statt einer beschädigten Seite. Character-to-Content-Byte-Mapping und das Ersetzen von Quellbytes an Ort und Stelle gehören zur Oberfläche für Textextraktion und Inhaltsbearbeitung von PDF Library for Delphi, der nativen Object-Pascal-PDF-Bibliothek für Delphi, C++Builder und Lazarus