Technischer Artikel

Textmarkierungs-Annotationen mit PDFium QuadPoints in Delphi

Die PDFium Component erzeugt Textmarkierungs-Annotationen, also Hervorhebung, Unterstreichung, Durchstreichung und Wellenlinie, über TPdf.CreateAnnotation: Sie setzen HasAttachmentPoints := True auf dem TPdfAnnotation-Record und füllen dessen AttachmentPoints-Viereck, und die Komponente schreibt den in ISO 32000-1 §12.5.6.10 definierten QuadPoints-Eintrag. Das ist die gesamte API-Oberfläche. Der Grund, warum dieser Artikel existiert, ist das, was darunter geschieht, denn die rohe PDFium-Aufrufkette hat einen Fehlermodus, der das am wenigsten hilfreiche Symptom im ganzen Werkzeugkasten erzeugt: FPDFAnnot_SetAttachmentPoints gibt auf einer frisch erzeugten Annotation false zurück, jedes Mal, ohne Fehlercode und ohne Hinweis. Dies ist das erstellungsseitige Gegenstück zu unserem Artikel über das Lesen und Prüfen vorhandener Annotationen, der die andere Richtung durch dieselben Strukturen geht

Die Debugging-Szene ist immer dieselbe. Sie erzeugen eine Hervorhebungs-Annotation, rufen den Setter für die Attachment-Points mit 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 Seitenraum gegen Geräteraum. Nichts davon hilft, denn die Koordinaten waren nie das Problem. Das Problem ist die Indexsemantik der C-API, und sobald Sie sie sehen, ist die Abhilfe zwei Zeilen lang

Was QuadPoints in ISO 32000-1 bedeuten

QuadPoints ist ein Array aus 8×n Zahlen, das n Vierecke beschreibt, und ISO 32000-1 §12.5.6.10 verlangt es auf jeder Textmarkierungs-Annotation: Jedes Viereck markiert ein Wort oder eine Gruppe zusammenhängender Wörter, auf die sich die Hervorhebung, Unterstreichung oder Durchstreichung bezieht. Der Rect-Eintrag der Annotation existiert weiterhin, aber für Markierungs-Subtypen begrenzt er nur die Region; die Quads sind das, was der Renderer tatsächlich zeichnet. Ein Viereck statt eines Rechtecks, weil Text gedreht oder geschert 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 Ort, an dem sich die Spezifikation und die installierte Basis trennen. Der Spezifikationstext beschreibt die Punkte als das Viereck gegen den Uhrzeigersinn umfahrend, aber Adobes eigener Renderer hat sie schon immer stattdessen in einem Z-Muster interpretiert: zuerst die Oberkante von links nach rechts, dann die Unterkante von links nach rechts. Weil jeder Autor gegen Acrobat getestet hat, folgt praktisch jeder Renderer, PDFium eingeschlossen, dem Z-Muster, und Dateien, die dem wörtlichen Wortlaut der Spezifikation folgen, rendern in manchen Betrachtern als zusammengefallene oder verdrehte Hervorhebungen. PDFiums FS_QUADPOINTSF-Struktur kodiert genau diese Konvention: (x1,y1) ist die Ecke oben links, (x2,y2) oben rechts, (x3,y3) unten links, (x4,y4) unten rechts, in Seitenkoordinaten, in denen Y nach oben wächst. Halten Sie sich an diese Reihenfolge und fertig; Renderer sind bei vielem nachsichtig, aber ein durcheinandergebrachtes Quad gehört nicht dazu

QuadPoints-Geometriediagramm in PDF-Seitenkoordinaten mit der Z-Reihenfolge der Eckennummerierung TL, TR, BL, BR, die PDFium-Textmarkierungs-Annotationen verwenden
PDFium erwartet QuadPoints in Z-Reihenfolge, oben links nach oben rechts, dann unten links nach unten rechts, in Seitenkoordinaten, in denen Y nach oben wächst

Warum gibt FPDFAnnot_SetAttachmentPoints false zurück?

FPDFAnnot_SetAttachmentPoints schlägt auf einer neuen Annotation fehl, weil sein Vertrag lautet, das Viereck an einem gegebenen Index zu ersetzen, und eine frisch erzeugte Annotation null Vierecke zum Ersetzen hat. Die Signatur nimmt ein Annotations-Handle, einen quad_index und die Punkte; Index 0 bedeutet nicht „der erste Slot, bei Bedarf angelegt“, sondern „das bereits vorhandene Quad Nummer 0“, und wenn FPDFAnnot_CountAttachmentPoints 0 meldet, gibt es kein solches Quad und der Aufruf liefert false. Die Funktion, die einen Slot anlegt, ist FPDFAnnot_AppendAttachmentPoints. Jede über FPDFPage_CreateAnnot erzeugte Annotation beginnt mit einer Anzahl von null, daher muss der Erstellungspfad zuerst Append aufrufen, und nur spätere Aktualisierungen dürfen Set aufrufen

Das hat die PDFium Component selbst erwischt. Bis v1.79.0 hatte die interne Routine, die sich CreateAnnotation und SetAnnotation teilen, FPDFAnnot_SetAttachmentPoints(Annotation, 0, ...) fest verdrahtet, was für das Aktualisieren einer vorhandenen Markierungs-Annotation korrekt war und für eine neue garantiert fehlschlug, sichtbar als EPdfException mit der Meldung 'Cannot set attachment points'. Die Korrektur, ausgeliefert in v1.79.1, verzweigt anhand der Anzahl

// Im Annotations-Writer der Komponente (v1.79.1+):
// eine neue Annotation hat noch keine Quad-Slots, daher erzeugt Append
// den ersten; Set ersetzt nur einen bereits vorhandenen 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 Ihnen erlaubt, da alle FPDFAnnot_*-Einstiegspunkte in PDFium.pas verfügbar sind. Wann immer Sie ein FPDF_ANNOTATION-Handle halten und Quads schreiben wollen, fragen Sie zuerst FPDFAnnot_CountAttachmentPoints und verzweigen Sie entsprechend. Wenn Sie nach „FPDFAnnot_SetAttachmentPoints returns false“ suchen, ist diese Verzweigung aus Zählen und dann Anhängen mit ziemlicher Sicherheit Ihre Antwort

Eine Hervorhebung mit TPdf.CreateAnnotation erzeugen

Da die Komponente die Weichenstellung zwischen Append und Set für Sie übernimmt, reduziert sich das Erzeugen einer Hervorhebung auf das Füllen eines Records. Das Beispiel unten erzeugt eine A4-Seite und legt eine halbtransparente gelbe Hervorhebung über eine Region von 200×20 Punkt; beachten Sie, dass das Quad der oben beschriebenen Z-Reihenfolge folgt und dass Rectangle so gesetzt ist, dass es das Quad umschließt, was Betrachter, die per Hit-Test gegen Rect prüfen, vernünftig reagieren lässt

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 := 'Highlighted region';
    Pdf.CreateAnnotation(A);

    Pdf.SaveAs('highlighted.pdf');
  finally
    Pdf.Free;
  end;
end;

Der Wechsel des Subtyps kostet eine Zeile. anUnderline, anStrikeout und anSquiggly nehmen die identische Record-Form, Quads und alles, weil ISO 32000-1 alle vier als dieselbe Annotationsfamilie behandelt, die sich nur darin unterscheidet, wie die Quad-Region dekoriert wird. Subtypen, die keine Textmarkierung sind, wie anSquare, anCircle und anText, positionieren sich allein aus Rectangle; lassen Sie HasAttachmentPoints für diese auf False, und die Quad-Maschinerie läuft nie

Warum kompiliert AttachmentPoints[0] in Delphi, schlägt aber in FPC fehl?

TQuadrilateralPoint ist als array [1..4] of TPdfPoint deklariert, ein 1-basiertes Array, und das stolpert jeden, dessen Finger standardmäßig nullbasiert indizieren. Schreiben Sie A.AttachmentPoints[0], und Delphis dcc32 kompiliert es ohne Beanstandung, weil die Bereichsprüfung standardmäßig aus ist; zur Laufzeit liest oder schreibt der Ausdruck stillschweigend den Speicher direkt vor dem Array, der in einem TPdfAnnotation-Record ein benachbartes Feld ist. Ihre Hervorhebung bekommt eine Müll-Ecke, oder ein Nachbarfeld wird beschädigt, und nichts löst eine Exception aus. Free Pascal hat genau diesen Fehler in unseren eigenen Demo-Quellen während der Lazarus-Portierung gefangen: fpc führt bei konstanten Indizes eine Bereichsprüfung zur Kompilierzeit durch und wies AttachmentPoints[0..3] rundheraus zurück, wodurch der Off-by-one-Fehler und der Bibliotheksfehler zwischen Set und Append gemeinsam ans Licht kamen

Zwei Gewohnheiten folgen daraus. Indizieren Sie das Quad von 1 bis 4, passend zur Eckenreihenfolge im Code oben, und bauen Sie Ihren Annotationscode mindestens einmal mit eingeschalteter Bereichsprüfung, entweder {$R+} in Delphi oder mit einem beliebigen fpc-Build, bevor Sie ihm vertrauen. Ein durchlaufender Standard-dcc32-Build ist kein Beleg dafür, dass die Indizes stimmen; er ist nur ein Beleg dafür, dass nichts an dem Speicher abgestürzt ist, der zufällig dort lag

Quad-Koordinaten aus echtem Text gewinnen

Fest kodierte Rechtecke sind für eine Demo in Ordnung, aber produktive Hervorhebungen folgen tatsächlichen Glyphen, und die Koordinaten sollten aus PDFiums Textseitengeometrie kommen statt aus Raterei. Die in unserem Leitfaden zur Textextraktion mit der PDFium Component behandelten Routinen liefern Ihnen Begrenzungsrahmen pro Zeichen in demselben Seitenkoordinatenraum, den die Quads verwenden, sodass sich ein Suchtreffer direkt in Eckpunkte übersetzt: links vom ersten Zeichen, rechts vom letzten, oben und unten aus der Ausdehnung der Zeile. Wenn Sie den Text selbst erzeugen und wissen müssen, wo Zeilen liegen werden, bevor sie existieren, behandelt der Artikel über Textmessung und Zeilenumbruch das vorausschauende Berechnen dieser Ausdehnungen

Eine ehrliche Grenze: Der TPdfAnnotation-Record trägt ein einzelnes TQuadrilateralPoint, sodass ein CreateAnnotation-Aufruf ein Viereck schreibt. Eine Auswahl über drei Zeilen braucht drei Quads, eines pro Zeile, gemäß §12.5.6.10, und Sie haben zwei Wege dorthin. Der einfache Weg ist eine Annotation pro Zeile, was überall korrekt rendert und die API auf Komponentenebene beibehält. Der kompakte Weg, eine Annotation mit drei Quads, bedeutet, die Annotation über die Komponente zu erzeugen und dann für das zweite und dritte Quad selbst das exportierte FPDFAnnot_AppendAttachmentPoints aufzurufen, was genau deshalb funktioniert, weil Append Slots anlegt, statt sie zu ersetzen. Versuchen Sie nicht, Multi-Quad über wiederholte SetAttachmentPoints-Aufrufe zu erreichen; jeder Index jenseits der aktuellen Anzahl gibt schlicht false zurück, aus demselben Grund, aus dem Index 0 es auf der frischen Annotation tat

Mehrzeilige Hervorhebung, aufgebaut aus drei Vierecken, die nacheinander an eine einzelne in Delphi erzeugte PDFium-Annotation angehängt werden
Eine Auswahl über drei Zeilen wird zu einer Annotation mit drei Quads, aufgebaut mit AppendAttachmentPoints-Aufrufen, die die Slot-Anzahl wachsen lassen

Prüfen Sie nach dem Schreiben in einem echten Betrachter, statt den Rückgabecodes zu vertrauen: Öffnen Sie die Datei in Acrobat oder einem beliebigen PDFium-basierten Betrachter und bestätigen Sie, dass die Markierung auf dem Text landet, mit der beabsichtigten Deckkraft erscheint und einen Roundtrip aus Speichern und erneutem Laden übersteht. Die Annotationstypen, die Quad-Behandlung und der zählbewusste Writer, die hier gezeigt werden, sind alle Teil der Standard-PDFium Component für Delphi, C++Builder und Lazarus; die Produktseite enthält die vollständige Annotations-API-Referenz neben dem Rest der Bibliothek