Ein während der Review um einen Absatz gezogenes Rechteck muss nicht zu einer Markierung im PDF selbst werden. HotPDFs THPDFViewerModel stellt AddHighlightRegion bereit, eine Methode, die jedes Highlight als reinen Speicher-Datensatz führt statt als Änderung am geladenen Dokument, sodass ein Reviewer Dutzende Seiten markieren kann, während die Datei auf der Festplatte byteidentisch bleibt, was sie war. Auf 6400% zoomen, die Seite um 90 Grad drehen, von Breite anpassen auf Seite anpassen wechseln – dasselbe Rechteck landet weiterhin auf demselben Absatz, weil die Koordinatenmathematik über die tatsächliche Render-Geometrie zum Zeitpunkt des Zeichnens der Markierung läuft
Review-Werkzeuge, die um einen PDF-Viewer herum gebaut werden, stoßen ständig auf dieses Problem. Ein Redlining-Bildschirm, ein QA-Durchgang über generierte Rechnungen, ein interner Freigabe-Workflow: Sie alle müssen jemandem erlauben, auf einen Seitenbereich aufmerksam zu machen, ohne dass jede Entwurfsmarkierung zu einer dauerhaften Änderung der Datei wird, und ohne für eine ganze Annotations-Subsystem-Maschinerie greifen zu müssen, nur um eine farbige Box zu zeigen, während noch entschieden wird, ob die Markierung bleibt. HotPDF beantwortet das mit einer eigenen Highlight-Ebene, die vollständig auf der Model-Seite der Trennung sitzt, die in dem Aufbau eines eigenen PDF-Viewers mit einer MVC-Architektur in Delphi beschrieben wird – und genau deshalb lässt sich dieselbe Highlight-Liste auch aus einem Unit-Test heraus steuern, ohne dass ein Fenster-Handle in Sicht ist
Was speichert HotPDFs AddHighlightRegion tatsächlich?
AddHighlightRegion speichert pro Markierung genau drei Dinge: einen nullbasierten Seitenindex, ein THPDFRectangle in PDF-User-Space-Koordinaten und eine TColor, alles verpackt als THPDFViewerHighlight-Record innerhalb von THPDFViewerModel. Der Aufruf von Viewer.HighlightRegion(PageIndex, PageRect, clYellow), oder das äquivalente Model.AddHighlightRegion, hängt einen dieser Records an ein privates Array an und gibt dessen Index zurück, und dieser Index ist das einzige Handle, das ein Aufrufer zurückbekommt: Es gibt kein separates Objekt, keine referenzgezählte Schnittstelle, nichts freizugeben. Jede andere Fähigkeit in diesem Artikel – die Markierung zeichnen, sie nach einer Zoom-Änderung neu abbilden, sie löschen – baut auf genau diesem einen kleinen Record auf
Jedes Rechteck wird normalisiert und beschnitten, bevor es akzeptiert wird. AddHighlightRegion tauscht die linke und rechte Kante, wenn ein Reviewer von rechts nach links zieht, tauscht oben und unten bei einem Zug nach oben, und beschneidet das Ergebnis anschließend gegen die MediaBox der Seite, die über GetLoadedPageBox abgerufen wird. Ein Rechteck, das am Ende null Breite, null Höhe hat oder vollständig außerhalb der Seite liegt, wird rundweg abgelehnt: Die Methode gibt -1 zurück und nichts wird der Liste hinzugefügt. Dieser Rückgabewert ist nicht dekorativ: Eine Charge von Highlights, die aus einer externen Review-Datei rekonstruiert wird, oder aus veralteten Koordinaten nach dem Austausch einer Seite, kann still und leise Einträge verlieren, wenn der Aufrufer nicht darauf prüft
Wie bleibt ein Highlight nach Zoom oder Rotation ausgerichtet?
Ein Highlight bleibt ausgerichtet, weil HotPDF es im PDF-Seitenraum speichert und bei jedem Neuzeichnen erneut in den Bildschirmraum projiziert, statt ein Bildschirmrechteck zu speichern, das in dem Moment veraltet wäre, in dem sich die Zoomstufe ändert. THPDFViewerModel.PagePointToView und seine Umkehrung, ViewPointToPage, führen diese Projektion in zwei Stufen durch: zuerst den eigenen /Rotate-Eintrag der Seite, dann die unabhängige ViewRotation des Viewers, die nie in das PDF zurückgeschrieben wird und nur beeinflusst, was der Viewer anzeigt. Das Rückgängigmachen der Transformation beim Loslassen der Maus durchläuft dieselben zwei Stufen in umgekehrter Reihenfolge, und genau das lässt ein bei hohem Zoom auf einer um 270 Grad gedrehten Seite gezeichnetes Highlight exakt am richtigen Platz landen, nachdem der Reviewer die Ansicht wieder auf Seite anpassen zurücksetzt
Die für diese Projektion verwendete DPI ist ebenso wichtig wie die Rotation. HotPDFs Viewer erfasst die exakte DPI der aktuell auf dem Bildschirm angezeigten Bitmap in FRenderedDPI unmittelbar nach jedem Rendering, und ImageMouseUp übergibt genau diesen Wert an ViewPointToPage, sodass eine Mauskoordinate immer mit der Auflösung umgerechnet wird, mit der tatsächlich gezeichnet wurde, nicht mit einer aus der aktuellen Zoom-Eigenschaft neu berechneten Auflösung. CreatePageSnapshot und verwandte Methoden begrenzen die DPI auf einen Bereich von 12 bis 2400, aber der interaktive Render-Pfad trägt keine solche Obergrenze: Die Standard-Zoomleiter endet bei 6400%, was bei der Standard-Basis von 96 DPI deutlich über 2400 DPI ergibt, sodass die Wiederverwendung einer Snapshot-artigen Grenze für die Koordinatenabbildung jedes Highlight am oberen Ende des Zoombereichs um mehrere Pixel verschieben würde. Zwei kleinere Standardverhalten runden die Interaktion ab: Ein Zug, der auf beiden Achsen kürzer als zwei Pixel ist, wird als Klick behandelt und erzeugt kein Highlight, und das Highlighting kann erst beginnen, sobald mindestens eine Seite tatsächlich gerendert wurde, da FRenderedDPI bei null startet
Interaktives Highlighting in einen Review-Bildschirm einbinden
Interaktives Highlighting einzuschalten ist eine Drei-Eigenschaften-Aufgabe direkt am THPDFViewer-Steuerelement: InteractionMode auf vimHighlight statt auf das Standard-vimBrowse setzen, eine HighlightColor wählen, die standardmäßig clYellow ist, und OnMarqueeSelect behandeln, um zu erfahren, was der Reviewer gerade gezeichnet hat. Alles andere – die Maus erfassen, das gepunktete Auswahlrechteck während des Ziehens des Reviewers zeichnen, den Loslasspunkt zurück in den Seitenraum umrechnen, AddHighlightRegion aufrufen – geschieht innerhalb des Steuerelements, bevor dieses Ereignis feuert
type
TReviewForm = class(TForm)
Viewer: THPDFViewer;
ReviewLog: TMemo;
procedure FormCreate(Sender: TObject);
private
procedure ViewerMarqueeSelect(Sender: TObject; Shift: TShiftState;
PageIndex: Integer; const PageRect: THPDFRectangle;
HighlightIndex: Integer);
end;
// PdfDoc is a THotPDF already loaded elsewhere on the form
procedure TReviewForm.FormCreate(Sender: TObject);
begin
Viewer.PDFDocument := PdfDoc;
Viewer.InteractionMode := vimHighlight;
Viewer.HighlightColor := clLime;
Viewer.OnMarqueeSelect := ViewerMarqueeSelect;
end;
procedure TReviewForm.ViewerMarqueeSelect(Sender: TObject; Shift: TShiftState;
PageIndex: Integer; const PageRect: THPDFRectangle; HighlightIndex: Integer);
begin
ReviewLog.Lines.Add(Format('page %d, mark #%d at (%.1f, %.1f)-(%.1f, %.1f)',
[PageIndex + 1, HighlightIndex, PageRect.Left, PageRect.Bottom,
PageRect.Right, PageRect.Top]));
end;
OnMarqueeSelect feuert nur bei einem Zug, der tatsächlich ein Highlight erzeugt hat: Ein Klick, der zu klein ist, um als Zug zu zählen, löscht das Auswahl-Overlay sofort, und ein Zug, der vollständig außerhalb der Seite landet, erreicht AddHighlightRegion, wird dort aber genauso abgelehnt wie es ein programmatischer Aufruf wäre, sodass das Ereignis in beiden Fällen stumm bleibt. Ein Implementierungsdetail, das sich zu kennen lohnt, falls das Highlighting an den Rändern des Steuerelements einmal nicht mehr zu reagieren scheint: Die Maus-Erfassung gehört dem THPDFViewer selbst, einem Abkömmling von TScrollBox, nicht dem internen TImage, das die Seiten-Bitmap anzeigt – und genau das erlaubt es einem Reviewer, über den Rand der gerenderten Seite hinauszuziehen und trotzdem ein sauberes Loslassen zu bekommen
Highlights per Code hinzufügen, entfernen und wieder auslesen
Highlights müssen überhaupt nicht aus einem Mausziehvorgang stammen. Viewer.HighlightRegion(PageIndex, PageRect, Color), das intern in dasselbe Model.AddHighlightRegion mündet, das auch der interaktive Zug aufruft, ist gezielt öffentlich, damit ein Review-Bildschirm Highlights aus bereits vorhandenen Daten rekonstruieren kann: aus einer Datenbank geladene Kommentare, Ergebnisse einer Textsuche oder aus einer früheren Sitzung wiederhergestellte Markierungen. Weil die Koordinaten einfache PDF-User-Space-Zahlen sind, hängt dieser Pfad im Gegensatz zum interaktiven Zug, der bereits einen echten Wert in FRenderedDPI benötigt, nicht davon ab, dass zuvor eine Seite gerendert wurde
var
I: Integer;
Item: TPriorComment; // your own record: PageIndex + PageRect
NewIndex: Integer;
begin
for I := 0 to PriorComments.Count - 1 do
begin
Item := TPriorComment(PriorComments[I]);
NewIndex := Viewer.HighlightRegion(Item.PageIndex, Item.PageRect, clAqua);
if NewIndex < 0 then
LogWarning('comment %d fell outside the page and was dropped', [I]);
end;
end;
Beim Entfernen eines einzelnen Highlights zeigt sich die array-basierte Speicherung. RemoveHighlightRegion löscht einen Record und verschiebt jeden späteren Record um eine Position nach unten, um die Lücke zu schließen, was bedeutet, dass jeder zuvor erfasste Index – aus einem OnMarqueeSelect-Ereignis oder aus einer früheren Aufzählung – nicht mehr vertrauenswürdig ist, sobald etwas vor ihm in der Liste entfernt wird. OnHighlightChange feuert bei jedem Hinzufügen, Entfernen und bei jedem ClearHighlightRegions-Aufruf, trägt aber keine Information darüber, was sich geändert hat, sodass das sichere Muster darin besteht, es als Signal zu behandeln, jede Liste, die ein Review-Panel anzeigt, aus HighlightCount und TryGetHighlightRegion neu aufzubauen, statt einen zwischengespeicherten Index an Ort und Stelle zu reparieren
procedure TReviewForm.ViewerHighlightChange(Sender: TObject);
var
I: Integer;
Mark: THPDFViewerHighlight;
begin
MarkList.Items.Clear;
for I := 0 to Viewer.Model.HighlightCount - 1 do
if Viewer.Model.TryGetHighlightRegion(I, Mark) then
MarkList.Items.AddObject(Format('page %d', [Mark.PageIndex + 1]),
TObject(I));
end;
Wann sollte eine Markierung stattdessen zu einer echten Highlight-Annotation werden?
Ein Highlight-Bereich sollte in dem Moment zu einer echten Annotation werden, in dem er diese eine THPDFViewer-Instanz überleben muss. HotPDF stellt außerdem AddHighlightAnnotation für eine neue Seite und AddLoadedHighlightAnnotation für ein bereits geladenes Dokument bereit, und trotz des fast identischen Namens ist dies ein völlig anderer Mechanismus: Beide schreiben eine echte Text-Markup-Annotation nach ISO 32000-1 §12.5.6.10, PDF /Subtype /Highlight, in das /Annots-Array der Seite, mit /QuadPoints, die den exakten Glyphenlauf markieren, und jeder konforme PDF-Viewer stellt sie dar, sobald die Datei gespeichert wurde, nicht nur HotPDFs eigener. Dieselbe Mechanismus-Grenze entscheidet auch, ob eine Markierung über XFDF hin- und zurückläuft: eine mit AddLoadedHighlightAnnotation erzeugte Annotation ist ein normales PDF-Objekt, das ExportLoadedAnnotationsToXFDF erfasst und als ISO-19444-1-Markup an Acrobat oder ein anderes Review-Tool übergibt, behandelt in dem Import und Export von PDF-Annotationen als XFDF in Delphi, während ein über AddHighlightRegion hinzugefügter Bereich für diesen Export unsichtbar ist, weil er nie in den Objektgraphen geschrieben wurde: Er existiert nur so lange, wie das THPDFViewerModel, das ihn erzeugt hat, existiert. Die vollständige Familie der auf einer Seite verfügbaren Markup- und geometrischen Annotationstypen, und wie ein Rechteck jeden davon platziert, wird in dem Artikel zu PDF-Annotationen in Delphi mit HotPDF behandelt, und die praktische Regel ist einfach: Eine Markierung bleibt wegwerfbar, solange ein Dokument noch diskutiert wird, und wird erst zu einer Annotation committet, sobald eine Entscheidung endgültig ist
Wo die Highlight-Ebene aufhört
Die Highlight-Ebene versucht ihrerseits nicht, wie ein durchscheinender Textmarker auszusehen: RefreshDocument zeichnet jeden Bereich als zwei Pixel breites Umrissrechteck in seiner eigenen Farbe über die zwischengespeicherte Seiten-Bitmap, genau wie es Suchtreffer zeichnet, statt eine farbige Füllung über den darunterliegenden Text zu blenden, sodass ein klassischer gelber Textmarker-Look in Anwendungscode gemalt oder auf den eigenen Appearance-Stream einer hochgestuften Annotation verschoben werden muss. Eine Fähigkeit, die sich lohnt, sobald ein Bereich existiert, ist CreateCurrentPageRegionSnapshot, die dasselbe THPDFRectangle, das ein Highlight bereits trägt, entgegennimmt und genau diesen Bereich in eine Bitmap rendert – nützlich, um einem Review-Kommentar ein kleines Vorschaubild anzuhängen, ohne die gesamte Seite zu exportieren. Ein Review-Aufbau muss sich nicht von vornherein zwischen den beiden Mechanismen entscheiden: Jede neue Markierung standardmäßig als wegwerfbaren THPDFViewerHighlight-Bereich behandeln, solange ein Kommentar-Thread offen bleibt, und erst AddLoadedHighlightAnnotation aufrufen, sobald ein Reviewer ihn abschließt – das hält das geladene PDF während des Hin und Her, das die meiste Fluktuation erzeugt, unangetastet. Das hier beschriebene Viewer-Steuerelement ist Teil der Standard-HotPDF-Komponente für Delphi und C++Builder, zusammen mit dem Rest der oben referenzierten Annotations- und Formular-APIs