Technischer Artikel

PDF-Anmerkungsprüfung in Delphi mit PDFium Component

Ein Prüfer öffnet denselben Vertrag in Ihrem Delphi-Viewer und in Adobe Acrobat. Acrobats Kommentarbereich listet vierzehn Einträge; Ihr Prüfpanel zeigt elf. Ihre Schleife ist nicht falsch. Die drei fehlenden Einträge sind zwei Antworten — vollständige Anmerkungsobjekte, die über In-reply-to-Referenzen mit ihren übergeordneten Objekten verbunden sind — und ein Popup-Fenster, das zu einer Haftnotiz gehört, die Sie bereits einmal gezählt haben. PDF-Anmerkungen sind keine flache Liste farbiger Rechtecke: ISO 32000-1 §12.5 definiert ein Netz aus Dictionaries mit Subtypes, Flags, Appearance-Streams und Eltern-Kind-Beziehungen, und ein Prüfpanel, das diese Beziehungen ignoriert, wird mit jedem anderen Viewer des Kunden uneins bleiben. Diese Anleitung baut einen Workflow zur Anmerkungsprüfung mit PDFium Component auf, der PDFium-basierten VCL/LCL-Komponente für Delphi, C++Builder und Lazarus, und konzentriert sich auf die Stellen, an denen real geprüfte Dokumente Widerstand leisten.

Warum Ihre Zählung nie zum Kommentarbereich von Acrobat passt

Acrobat zeigt eine kuratierte Sicht: Markup-Anmerkungen werden zu Antwort-Threads gruppiert, Popups in ihre übergeordneten Einträge eingefaltet. Das rohe Anmerkungsarray jeder Seite enthält zugleich mehr und weniger, als diese Ansicht vermuten lässt.

  • Popup-Anmerkungen sind separate Objekte, die an eine übergeordnete Notiz gebunden sind — wer sie mitzählt, zählt jede Haftnotiz doppelt.
  • Antworten sind vollständige Text-Anmerkungen mit einer Referenz auf ein übergeordnetes Objekt; wer nur sichtbare Markierungen filtert, verliert still den Diskussionsthread.
  • Die Flags Hidden und NoView entfernen eine Anmerkung aus der Anzeige, nicht aus dem Array, daher gehören Flag-Prüfungen in den Indizierungslauf.
  • Link-Anmerkungen liegen im selben Array, und kein Prüfer betrachtet einen Hyperlink als Kommentar.

Legen Sie die Zählregel fest, bevor Sie Code schreiben, und nehmen Sie sie in die Spezifikation auf, denn „warum zeigt Ihr Panel eine andere Zahl als Acrobat“ ist das erste Supportticket, das eine Review-Funktion erzeugt.

Alles einmal indizieren, Seiten danach nie neu parsen

Filter nach Autor, Typ oder Seite dürfen kein erneutes Parsen der Seitenobjekte auslösen — bei einem 300-Seiten-Dokument mit viel Markup würde jeder Dropdown-Wechsel sonst Sekunden dauern. Die Komponente stellt AnnotationCount und die indizierte Eigenschaft Annotation[] für die aktuell geladene Seite bereit, und der zurückgegebene Datensatz TPdfAnnotation enthält alles, was eine Listenansicht braucht: Subtype, Flags, Color, Rectangle, ContentsText und AuthorText. Bauen Sie den Index beim Öffnen in einem Durchlauf auf:

procedure TReviewPanel.BuildIndex;
var
  PageNo, i: Integer;
  A: TPdfAnnotation;
begin
  FItems.Clear;
  for PageNo := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := PageNo;
    for i := 0 to Pdf.AnnotationCount - 1 do
    begin
      A := Pdf.Annotation[i];
      // Keep reviewer-relevant subtypes only; record the page and
      // index pair because all later edits are addressed by it
      if A.Subtype in [anText, anHighlight, anInk] then
        FItems.Add(TReviewItem.Create(PageNo, i,
          A.AuthorText, A.ContentsText, A.Rectangle, A.Color));
    end;
  end;
end;

Das Paar, das hervorgehoben werden sollte, ist (PageNo, i). Jeder spätere Änderungsaufruf — Umfärben, Löschen — wird über Seitennummer plus Anmerkungsindex adressiert, und Indizes verschieben sich, sobald eine Anmerkung entfernt wird. Planen Sie nach jeder Löschung ein Neuaufbauen der Einträge der betroffenen Seite ein, statt Indizes lokal zu flicken; der Neuaufbau kostet Millisekunden, ein veralteter Index löscht dagegen den Kommentar des falschen Prüfers.

Antwort-Threads verdienen schon im Indexentwurf einen Platz, selbst wenn die erste Version Antworten nur zählt, statt sie anzuzeigen. Gruppieren Sie Einträge beim Aufbau nach ihrer Parent-Referenz, damit das Panel einen Thread später wie Acrobat einklappen kann — die Gruppierung träge während des Scrollens zu rekonstruieren öffnet Seiten erneut, deren Parsing bereits bezahlt wurde. Dasselbe Ein-Durchlauf-Denken gilt für Geometrie: Rectangle in jedem Datensatz ist im Seitenraum ausgedrückt, also wandeln Sie sie über genau einen gemeinsamen Helfer in Ansichtskordinaten um. Review-Panels sammeln Koordinatenfehler, wenn Auswahl, Hit-Testing und Zeichnen jeweils eigene Zoom- und Rotationsarithmetik tragen; ein gemeinsamer Konvertierungspfad hält Hervorhebung, Listeneintrag und Klickziel auf derselben Tinte.

Markup umfärben und das Appearance-Stream-Veto

Ein Highlight von Gelb auf Amber zu ändern klingt nach einer Zeile, und manchmal ist es das. Die Komplikation steht in ISO 32000-1 §12.5.5: Wenn eine Anmerkung einen /AP-Appearance-Stream besitzt, rendern konforme Viewer diesen vorberechneten Stream, und der Farbeintrag im Anmerkungs-Dictionary wird zur Dekoration. Weil Acrobat für praktisch alles, was es erzeugt, Appearance-Streams schreibt, befinden sich die meisten von Kunden eingehenden Anmerkungen genau in diesem Zustand. Das Umfärben ist ein Read-modify-write über die Eigenschaft Annotation[] — und die Komponente meldet ein Engine-Veto ehrlich, indem sie EPdfError auslöst:

A := Pdf.Annotation[Item.Index];
A.HasColor := True;
A.Color := $0000B0FF;       // amber
A.ColorAlpha := 160;
try
  Pdf.Annotation[Item.Index] := A;
except
  on EPdfError do
  begin
    // The annotation owns a pre-rendered /AP stream; the dictionary
    // color alone cannot change what viewers paint
    Item.AppearanceLocked := True;
    StatusBar.SimpleText := 'Color is fixed by the annotation appearance';
  end;
end;

Behandeln Sie diese Ausnahme jedes Mal. Ohne Schutz zeigt Ihr Panel in der eigenen Liste die neue Farbe, während die Seite weiter die alte malt, und die Abweichung taucht Wochen später als Bericht „Ihr Viewer ignoriert meine Änderungen“ auf. Wenn das Appearance gesperrt ist, bleiben die ehrlichen Optionen: die eigene Auswahlüberlagerung statt der Anmerkung umfärben oder den Eintrag in der Oberfläche als appearance-locked markieren.

Anmerkungen löschen, ohne Geisterbilder stehen zu lassen

DeleteAnnotation löst das Objekt von der aktuellen Seite, baut aber das zwischengespeicherte Seitenbild nicht neu auf — malen Sie sofort danach, ist das gelöschte Highlight noch sichtbar. Rendern Sie den Seitenraster im selben Vorgang neu:

Pdf.PageNumber := Item.PageNo;
Pdf.DeleteAnnotation(Item.Index);   // raises EPdfError on failure
Bmp := Pdf.RenderPage(0, 0, ViewWidth, ViewHeight, ro0, [reAnnotations]);
try
  PaintPageBitmap(Bmp);
finally
  Bmp.Free;  // RenderPage hands bitmap ownership to the caller
end;
RebuildPageEntries(Item.PageNo);  // indices after Item.Index shifted

Achten Sie auf die Option reAnnotations im Renderaufruf: Ohne sie enthält der Raster keine der verbleibenden Anmerkungen, was für den Benutzer wie eine Massenlöschung aussieht. Und achten Sie auf Bmp.Free — die Funktionsüberladung von RenderPage übergibt den Bitmap-Besitz an den Aufrufer, daher leckt jede Löschung einen vollständigen Seitenraster, wenn Sie nicht freigeben.

Prüfmarken aus der eigenen Oberfläche hinzufügen

Anmerkungen werden über CreateAnnotation erzeugt. Der Aufruf erwartet einen gefüllten Datensatz TPdfAnnotation — Subtype, Rechteck, Farbe, Inhalt, Autor — und fügt ihn der aktuellen Seite hinzu. Haftnotizen (anText) sind der einfache Fall: Position, Inhalt, Autor, fertig. Ink-Anmerkungen sind die Falle: Das Rechteck des Datensatzes begrenzt nur die Zeichnung, die eigentlichen Striche sind Punktarrays und müssen separat über den Ink-Stroke-Aufruf der Engine angebunden werden (FPDFAnnot_AddInkStroke mit FS_POINTF-Daten), Strich für Strich aus Maus- oder Stifteingaben erfasst. Eine Ink-Anmerkung nur aus einem Rechteck zu erzeugen, ergibt eine leere Kritzelei, die als nichts rendert.

Entscheiden Sie die Autorschaftsregel zugleich: Jede Markierung, die Ihre Oberfläche erstellt, sollte ein konsistentes AuthorText tragen, denn nachgelagertes Filtern nach Prüfer ist nur so zuverlässig wie die Namen, die Sie heute schreiben.

Review-Daten aus dem Viewer herausbekommen

Review-Daten verdienen ihren Platz erst, wenn sie den Viewer verlassen: als Zusammenfassung, die ein Projektleiter ohne Öffnen der Datei liest, oder als CSV für ein Tracking-Sheet. Exportieren Sie aus dem Index, nicht aus einem erneuten Parse, und halten Sie Referenzen stabil — Seitennummer plus Anmerkungsrechteck überstehen Rundläufe besser als ein Arrayindex, den die nächste Löschung ungültig macht.

Eine belastbare Exportzeile enthält Seite, Subtype, Autor, Erstellungsinformationen soweit vorhanden, Inhaltstext und Ihre eigene Statusspalte. Für Dokumente von außerhalb des Teams lohnt es sich, denselben Indizierungslauf schon bei der Eingangstriage auszuführen; der Artikel zur PDF-Eingangs-Workbench zeigt dieses Muster, und Formularfeld-Navigation behandelt das Nachbarproblem von Dokumenten, die Daten statt Kommentare sammeln.

FAQ

Warum zeigt ein von mir umgefärbtes Highlight weiter die alte Farbe?

Die Anmerkung besitzt fast sicher einen /AP-Appearance-Stream, den konforme Viewer vor der Dictionary-Farbe malen (ISO 32000-1 §12.5.5). Das Zurückschreiben des Datensatzes über Annotation[] löst in diesem Fall EPdfError aus — behandeln Sie die Ausnahme als Wahrheit, nicht die Farbe, die Sie setzen wollten.

Warum zeigt die Seite noch eine Anmerkung, die ich entfernt habe?

DeleteAnnotation aktualisiert das Dokumentmodell, nicht den zwischengespeicherten Raster. Rendern Sie die Seite nach erfolgreicher Entfernung mit RenderPage neu, und bauen Sie die Indexeinträge dieser Seite neu auf, weil Anmerkungsindizes nach der entfernten Position nach unten rutschen.

Erscheinen geflachte Anmerkungen im Anmerkungsarray?

Nein. Flattening wandelt Anmerkungs-Appearances in gewöhnlichen Seiteninhalt um, sodass sie gar keine Anmerkungsobjekte mehr sind. Wenn eine Kundendatei sichtbares Markup zeigt, aber AnnotationCount null ist, ist vorgelagertes Flattening die übliche Erklärung — programmatisch bleibt nichts mehr zu prüfen.

Die in diesem Artikel genutzte Anmerkungs-API — Enumeration, Erstellung, Umfärbung, Entfernung und die Renderoptionen, die die Anzeige ehrlich halten — ist Teil von PDFium Component für Delphi, C++Builder und Lazarus/FPC.