Die PDFium-Komponente erstellt Textmarkierungs-Anmerkungen (d. h. Hervorhebungen, Unterstreichungen, Durchstreichungen und wellenförmige Linien) über TPdf.CreateAnnotation. Sie müssen HasAttachmentPoints := True im Datensatz TPdfAnnotation festlegen und dessen QuadPoints-Viereck AttachmentPoints füllen. Die Komponente schreibt anschließend den in ISO 32000-1 §12.5.6.10 definierten QuadPoints-Eintrag. Das ist die gesamte Schnittstelle der API. Der Grund für diesen Artikel liegt in dem begründet, was unter der Haube geschieht: Die direkte PDFium-Aufrufkette besitzt ein Fehlerszenario, das eines der unpraktischsten Symptome überhaupt erzeugt: FPDFAnnot_SetAttachmentPoints gibt bei einer neu erstellten Anmerkung jedes Mal false zurück — ohne Fehlercode und ohne jeden Hinweis. Dies ist das Erstellungspendant zu unserem Artikel über das Lesen und Überprüfen bestehender Anmerkungen, der den umgekehrten Weg durch dieselben Strukturen beschreibt
Das Fehlerszenario beim Debuggen ist immer identisch: Sie erstellen eine Hervorhebungs-Anmerkung (Highlight), rufen den Setter für die Attachment-Points mit dem Index 0 auf, die Funktion gibt false zurück, und Sie beginnen, an Ihren Koordinaten zu zweifeln. Sie vertauschen die Punkte, spiegeln die Y-Achse, tauschen den Seitenbereich gegen den Gerätebereich. Nichts davon hilft, da die Koordinaten nie das Problem waren. Das Problem liegt in der Indexsemantik der C-API, und wenn man diese erst einmal verstanden hat, ist die Korrektur eine Sache von zwei Zeilen
Was QuadPoints in ISO 32000-1 bedeuten
QuadPoints ist ein Array aus 8×n Zahlen, die n Vierecke (Quadrilaterals) beschreiben. ISO 32000-1 §12.5.6.10 schreibt dieses auf jeder Textmarkierungs-Anmerkung vor: Jedes Viereck markiert ein Wort oder eine Gruppe zusammenhängender Wörter, auf die sich die Hervorhebung, Unterstreichung oder Durchstreichung bezieht. Der Eintrag Rect der Anmerkung existiert zwar weiterhin, begrenzt bei Markup-Subtypen aber nur den Bereich. Die eigentlichen Vierecke (Quads) sind das, was der Renderer darstellt. Ein Viereck wird anstelle eines Rechtecks verwendet, da Text rotiert oder geneigt sein kann. Daher werden die vier Ecken als vier unabhängige Punkte gespeichert: x1 y1 x2 y2 x3 y3 x4 y4
Die Reihenfolge dieser vier Punkte ist der Bereich, in dem die Spezifikation und die tatsächliche Praxis auseinandergehen. Der Spezifikationstext beschreibt, dass die Punkte das Viereck gegen den Uhrzeigersinn umreisen. Der Acrobat-Renderer von Adobe hat sie jedoch schon immer in einem Z-Muster interpretiert: zuerst die obere Kante von links nach rechts, dann die untere Kante von links nach rechts. Da jeder Entwickler gegen Acrobat getestet hat, folgen praktisch alle Renderer, einschließlich PDFium, diesem Z-Muster. Dateien, die der wörtlichen Formulierung der Spezifikation folgen, werden in einigen Viewern als verzerrte oder in sich zusammengefallene Highlights gerendert. PDFiums Struktur FS_QUADPOINTSF codiert genau diese Konvention: (x1,y1) ist die obere linke Ecke, (x2,y2) oben rechts, (x3,y3) unten links und (x4,y4) unten rechts — gemessen in Seitenkoordinaten, bei denen Y nach oben wächst. Folgen Sie dieser Reihenfolge, und die Sache ist erledigt. Renderer verzeihen vieles, aber ein fehlerhaftes Quad gehört nicht dazu
Warum gibt FPDFAnnot_SetAttachmentPoints false zurück?
FPDFAnnot_SetAttachmentPoints schlägt bei einer neuen Anmerkung fehl, da sein Zweck darin besteht, das Viereck an einem bestimmten Index zu ersetzen, und eine frisch erstellte Anmerkung besitzt noch keine Vierecke, die ersetzt werden könnten. Die Signatur erwartet ein Anmerkungs-Handle, einen quad_index und die Punkte. Der Index 0 bedeutet nicht „der erste Slot, der bei Bedarf erstellt wird“, sondern „das bereits existierende Quad Nummer 0“. Wenn FPDFAnnot_CountAttachmentPoints den Wert 0 meldet, gibt es kein solches Quad und der Aufruf gibt false zurück. Die Funktion, die einen Slot erstellt, ist FPDFAnnot_AppendAttachmentPoints. Jede über FPDFPage_CreateAnnot erstellte Anmerkung startet mit einem Zählerstand von Null. Der Erstellungspfad muss daher zuerst Append aufrufen, und nur nachfolgende Aktualisierungen dürfen Set aufrufen
Dieser Fehler betraf auch die PDFium-Komponente selbst. Bis v1.79.0 war in der internen Routine, die von CreateAnnotation und SetAnnotation genutzt wird, FPDFAnnot_SetAttachmentPoints(Annotation, 0, ...) fest verdrahtet. Dies war zwar für das Aktualisieren einer bestehenden Markup-Anmerkung korrekt, schlug jedoch bei einer neuen Anmerkung garantiert fehl, was zu einer EPdfException mit der Meldung 'Cannot set attachment points' führte. Die in v1.79.1 ausgelieferte Fehlerbehebung prüft nun den Zählerstand
// Innerhalb des Anmerkungs-Writers der Komponente (v1.79.1+):
// Eine neue Anmerkung besitzt noch keine Quad-Slots, daher erstellt Append den
// ersten. Set ersetzt nur einen bereits existierenden Slot.
if FPDFAnnot_CountAttachmentPoints(Annotation) = 0 then
Check(FPDFAnnot_AppendAttachmentPoints(Annotation, QuadPoints) <> 0,
'Cannot set attachment points')
else
Check(FPDFAnnot_SetAttachmentPoints(Annotation, 0, QuadPoints) <> 0,
'Cannot set attachment points');
Dasselbe Muster gilt, wenn Sie die exportierten C-Funktionen direkt aufrufen, was die Komponente erlaubt, da alle FPDFAnnot_*-Einstiegspunkte in PDFium.pas bereitgestellt werden. Wann immer Sie ein FPDF_ANNOTATION-Handle halten und Quads schreiben möchten, fragen Sie zuerst FPDFAnnot_CountAttachmentPoints ab und leiten Sie den Aufruf entsprechend um. Suchen Sie nach „FPDFAnnot_SetAttachmentPoints returns false“, ist dieser Zähler-Append-Verzweigungspfad fast sicher die Lösung
Erstellen eines Highlights mit TPdf.CreateAnnotation
Da die Komponente die Weiterleitung zwischen Append und Set für Sie übernimmt, reduziert sich das Erstellen einer Hervorhebung (Highlight) auf das Ausfüllen eines Datensatzes. Das folgende Beispiel erstellt eine DIN-A4-Seite und platziert eine halbtransparente gelbe Hervorhebung über einen Bereich von 200×20 Punkten. Beachten Sie, dass das Quad der oben beschriebenen Z-Reihenfolge folgt und Rectangle so gesetzt ist, dass es das Quad einschließt. Dies sorgt dafür, dass Viewer, die Hit-Tests gegen das Rect ausführen, korrekt reagieren
var
Pdf: TPdf;
A: TPdfAnnotation;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument;
Pdf.AddPage(0, 595, 842);
FillChar(A, SizeOf(A), 0);
A.Subtype := anHighlight;
A.HasColor := True;
A.Color := clYellow;
A.ColorAlpha := $80; // 50% Deckkraft
A.HasAttachmentPoints := True;
A.AttachmentPoints[1].X := 50; A.AttachmentPoints[1].Y := 700; // oben links
A.AttachmentPoints[2].X := 250; A.AttachmentPoints[2].Y := 700; // oben rechts
A.AttachmentPoints[3].X := 50; A.AttachmentPoints[3].Y := 680; // unten links
A.AttachmentPoints[4].X := 250; A.AttachmentPoints[4].Y := 680; // unten rechts
A.Rectangle.Left := 50; A.Rectangle.Top := 700;
A.Rectangle.Right := 250; A.Rectangle.Bottom := 680;
A.ContentsText := 'Hervorgehobener Bereich';
Pdf.CreateAnnotation(A);
Pdf.SaveAs('highlighted.pdf');
finally
Pdf.Free;
end;
end;
Das Wechseln der Subtypen erfordert nur eine Zeile. anUnderline, anStrikeout und anSquiggly verwenden dieselbe Struktur, Quads inklusive, da ISO 32000-1 alle vier als dieselbe Anmerkungsfamilie behandelt, die sich nur durch die visuelle Darstellung des Quads unterscheidet. Subtypen, die keine Textmarkierungen sind, wie anSquare, anCircle und anText, positionieren sich ausschließlich über Rectangle. Lassen Sie HasAttachmentPoints für diese auf False gesetzt, und die Quad-Logik wird nicht ausgeführt
Warum kompiliert AttachmentPoints[0] in Delphi, scheitert aber in FPC?
TQuadrilateralPoint ist als array [1..4] of TPdfPoint deklariert — ein 1-basiertes Array — und das verwirrt jeden, dessen Finger standardmäßig auf die 0-basierte Indizierung voreingestellt sind. Schreiben Sie A.AttachmentPoints[0], kompiliert Delphis dcc32 dies ohne Beanstandung, da die Bereichsprüfung (Range Checking) standardmäßig deaktiviert ist. Zur Laufzeit liest oder schreibt der Ausdruck stillschweigend den Speicher direkt vor dem Array, was im Datensatz TPdfAnnotation einem benachbarten Feld entspricht. Ihre Hervorhebung erhält eine fehlerhafte Ecke oder ein benachbartes Feld wird beschädigt, ohne dass eine Fehlermeldung ausgegeben wird. Free Pascal hat genau diesen Fehler in unseren Demo-Quellen während des Lazarus-Ports abgefangen: fpc führt eine Bereichsprüfung zur Compilezeit bei konstanten Indizes durch und lehnte AttachmentPoints[0..3] rundweg ab. So wurden der falsche Index und der Set-versus-Append-Bibliotheksfehler zusammen aufgedeckt
Daraus ergeben sich zwei Verhaltensregeln: Indizieren Sie das Quad mit 1 bis 4, passend zur Eckenreihenfolge im obigen Code, und erstellen Sie Ihren Anmerkungscode mindestens einmal mit aktivierter Bereichsprüfung — entweder {$R+} in Delphi oder in einem fpc-Build — bevor Sie ihm vertrauen. Ein erfolgreicher dcc32-Standardbuild beweist nicht, dass die Indizes korrekt sind. Er zeigt nur, dass nichts auf dem Speicher abgestürzt ist, der zufällig dort lag
Ermitteln von Quad-Koordinaten aus echtem Text
Fest programmierte Rechtecke eignen sich gut für Demos, aber produktive Hervorhebungen folgen echten Glyphen. Die Koordinaten sollten daher aus der Textseitengeometrie von PDFium stammen und nicht geschätzt werden. Die im Leitfaden zur Textextraktion mit der PDFium-Komponente beschriebenen Routinen liefern Ihnen zeichenweise Begrenzungsboxen im gleichen Seitenkoordinatenraum, den die Quads nutzen. So lässt sich ein Suchtreffer direkt in Eckenpunkte umwandeln: links vom ersten Zeichen, rechts vom letzten sowie oben und unten aus den Abmessungen der Zeile. Wenn Sie den Text selbst generieren und wissen müssen, wo Zeilen umbrechen werden, behandelt der Artikel über Textmessung und Zeilenumbruch die Berechnung dieser Abmessungen im Vorfeld
Eine ehrliche Systemgrenze
Der Datensatz TPdfAnnotation enthält ein einzelnes TQuadrilateralPoint. Ein Aufruf von CreateAnnotation schreibt somit ein einziges Viereck. Eine Markierung, die sich über drei Zeilen erstreckt, benötigt gemäß §12.5.6.10 drei Quads — eines pro Zeile — und Sie haben zwei Möglichkeiten, dies zu lösen. Der einfache Weg ist eine Anmerkung pro Zeile. Dies wird überall korrekt gerendert und behält die API auf Komponentenebene bei. Der kompakte Weg, bei dem eine Anmerkung drei Quads trägt, bedeutet, dass Sie die Anmerkung über die Komponente erstellen und anschließend das exportierte FPDFAnnot_AppendAttachmentPoints selbst für das zweite und dritte Quad aufrufen. Dies funktioniert, da Append Slots erstellt, anstatt sie zu ersetzen. Versuchen Sie nicht, mehrere Quads über wiederholte Aufrufe von SetAttachmentPoints zu erreichen — jeder Index, der den aktuellen Zählerstand überschreitet, gibt false zurück, aus demselben Grund wie der Index 0 bei der neuen Anmerkung
Überprüfen Sie die Datei nach dem Schreiben in einem echten Viewer, anstatt sich auf Rückgabecodes zu verlassen: Öffnen Sie die Datei in Acrobat oder einem PDFium-basierten Viewer und bestätigen Sie, dass die Markierung auf dem Text liegt, die gewünschte Deckkraft besitzt und einen Round-Trip aus Speichern und Neuladen übersteht. Die Anmerkungstypen, die Quad-Verarbeitung und der zähler-bewusste Writer, die hier gezeigt werden, sind alle Teil der standardmäßigen PDFium Component für Delphi, C++Builder und Lazarus. Die Produktseite enthält die vollständige Referenz der Anmerkungs-API neben den restlichen Teilen der Bibliothek