Et rektangel tegnet omkring et afsnit under gennemgang behøver ikke blive et mærke inde i PDF'en. HotPDFs THPDFViewerModel eksponerer AddHighlightRegion, en metode der holder hver highlight som en post i hukommelsen frem for en ændring af det indlæste dokument, så en reviewer kan markere op mod snesevis af sider, mens filen på disk forbliver byte-for-byte den samme, den var. Zoom til 6400%, roter siden 90 grader, skift fra Fit Width til Fit Page, og det samme rektangel lander stadig på det samme afsnit, fordi koordinat-matematikken kører gennem den faktiske gengivelsesgeometri på det tidspunkt, mærket blev tegnet
Review-værktøjer bygget omkring en PDF-fremviser støder konstant på dette problem. En redline-skærm, en QA-gennemgang af genererede fakturaer, en intern godkendelses-workflow: alle har brug for at lade nogen henlede opmærksomheden på en region af en side uden at hvert udkastmærke bliver til en permanent ændring af filen, og uden at gribe til et fuldt annotationssubsystem bare for at vise en farvet boks, mens nogen stadig beslutter, om mærket hører til. HotPDF svarer på det med et dedikeret highlight-lag, der ligger udelukkende på Model-siden af opdelingen beskrevet i at bygge en brugerdefineret PDF-fremviser med en MVC-arkitektur i Delphi, hvilket også er grunden til, at den samme highlight-liste kan drives fra en enhedstest uden noget vindueshandle i sigte
Hvad gemmer HotPDFs AddHighlightRegion egentlig?
AddHighlightRegion gemmer nøjagtigt tre ting pr. mærke: et nul-indekseret sideindeks, et THPDFRectangle i PDF user-space-koordinater og en TColor, alt sammen pakket som en THPDFViewerHighlight-record inde i THPDFViewerModel. At kalde Viewer.HighlightRegion(PageIndex, PageRect, clYellow), eller det tilsvarende Model.AddHighlightRegion, tilføjer en af disse records til et privat array og returnerer dens indeks, og det indeks er det eneste handle en kalder får tilbage: der er ikke noget separat objekt, ingen referenceoptalt grænseflade, intet at frigive. Alle andre egenskaber i denne artikel — at tegne mærket, remappe det efter en zoomændring, slette det — er bygget oven på den ene lille record
Hvert rektangel normaliseres og klippes, før det accepteres. AddHighlightRegion bytter venstre og højre kant, hvis en reviewer trækker fra højre mod venstre, bytter top og bund for et opadgående træk, og klipper derefter resultatet mod sidens MediaBox hentet gennem GetLoadedPageBox. Et rektangel, der ender med nul bredde, nul højde eller helt uden for siden, afvises direkte: metoden returnerer -1, og intet tilføjes til listen. Den returværdi er ikke dekorativ: en batch af highlights genopbygget fra en ekstern review-fil, eller fra forældede koordinater efter en side blev udskiftet, kan i stilhed miste poster, hvis kalderen ikke tjekker for det
Hvordan forbliver en highlight justeret efter zoom eller rotation?
En highlight forbliver justeret, fordi HotPDF gemmer den i PDF-sidekoordinater og reprojicerer den til skærmkoordinater ved hver gentegning, i stedet for at gemme et skærmrektangel, der ville blive forældet, i det øjeblik zoomniveauet ændres. THPDFViewerModel.PagePointToView og dens inverse, ViewPointToPage, udfører den projektion i to trin: først sidens egen /Rotate-post, derefter Viewerens uafhængige ViewRotation, som aldrig skrives tilbage til PDF'en og kun påvirker, hvad Viewer'en viser. At fortryde transformationen ved museslip kører de samme to trin i omvendt rækkefølge, hvilket er det, der lader en highlight tegnet ved høj zoom på en side roteret 270 grader lande netop det rigtige sted, efter reviewer'en nulstiller visningen tilbage til Fit Page
DPI'en brugt til den projektion betyder lige så meget som rotationen. HotPDFs Viewer indfanger den nøjagtige DPI for den bitmap, der aktuelt er på skærmen, i FRenderedDPI lige efter hver gengivelse, og ImageMouseUp sender den samme værdi ind i ViewPointToPage, så en musekoordinat altid konverteres ved brug af den opløsning, den faktisk blev tegnet med, ikke en opløsning genberegnet fra den aktuelle zoom-egenskab. CreatePageSnapshot og dens slægtninge begrænser DPI til et interval fra 12 til 2400, men den interaktive gengivelsesvej har intet sådant loft: standard-zoomstigen topper ved 6400%, hvilket beregner til langt over 2400 DPI ved standard-basislinjen på 96 DPI, så genbrug af en snapshot-agtig grænse til koordinat-mapning ville forskyde hver highlight med adskillige pixels øverst i zoomintervallet. To mindre standardindstillinger runder interaktionen af: et træk kortere end to pixels på begge akser behandles som et klik og producerer ingen highlight, og highlighting kan ikke begynde, før mindst én side rent faktisk er blevet gengivet, da FRenderedDPI starter ved nul
At koble interaktiv highlighting til en review-skærm
At slå interaktiv highlighting til er en opgave med tre egenskaber på selve THPDFViewer-kontrollen: sæt InteractionMode til vimHighlight i stedet for standarden vimBrowse, vælg en HighlightColor, som som standard er clYellow, og håndter OnMarqueeSelect for at finde ud af, hvad reviewer'en lige har tegnet. Alt andet — at indfange musen, tegne det stiplede markeringsrektangel mens reviewer'en trækker, konvertere slippunktet tilbage til sidekoordinater, kalde AddHighlightRegion — sker inde i kontrollen, før den hændelse udløses
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 udløses kun for et træk, der rent faktisk producerede en highlight: et klik for lille til at tælle som et træk rydder markerings-overlayet med det samme, og et træk, der lander helt uden for siden, når frem til AddHighlightRegion, men afvises der på samme måde, som et programmatisk kald ville blive, så hændelsen forbliver tavs enten som eller. Én implementeringsdetalje er værd at kende, hvis highlighting nogensinde ser ud til at stoppe med at reagere ved kontrollens kanter: musefangst tilhører selve THPDFViewer, en TScrollBox-efterkommer, ikke det interne TImage, der viser side-bitmappen, hvilket er det, der lader en reviewer trække forbi kanten af den gengivne side og stadig få et rent slip
At tilføje, fjerne og genindlæse highlights fra kode
Highlights behøver slet ikke komme fra et musetræk. Viewer.HighlightRegion(PageIndex, PageRect, Color), som render ind i den samme Model.AddHighlightRegion det interaktive træk kalder internt, er offentlig netop, så en review-skærm kan genopbygge highlights fra data, den allerede har: kommentarer indlæst fra en database, resultater fra en tekstsøgning, eller mærker gendannet fra en tidligere session. Fordi koordinaterne er almindelige PDF-user-space-tal, afhænger intet ved denne vej af, at en side allerede er blevet gengivet, i modsætning til det interaktive træk, som kræver, at FRenderedDPI allerede holder en reel værdi
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;
At fjerne en enkelt highlight er, hvor den array-baserede lagring skinner igennem. RemoveHighlightRegion sletter én record og flytter hver senere record ned én position for at lukke hullet, hvilket betyder, at ethvert indeks fanget tidligere, fra en OnMarqueeSelect-hændelse eller fra en tidligere optælling, ikke længere er pålideligt, når noget forud for det på listen bliver fjernet. OnHighlightChange udløses ved hver tilføjelse, fjernelse og ClearHighlightRegions-kald, men den bærer ingen information om, hvad der ændrede sig, så det sikre mønster er at behandle den som et signal til at genopbygge, uanset hvilken liste et review-panel viser, fra HighlightCount og TryGetHighlightRegion, i stedet for at rette et cachet indeks på plads
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;
Hvornår bør et mærke i stedet blive en rigtig Highlight-annotation?
En highlight-region bør blive en rigtig annotation, i det øjeblik den skal overleve uden for den ene THPDFViewer-instans. HotPDF eksponerer også AddHighlightAnnotation til en ny side og AddLoadedHighlightAnnotation til et allerede indlæst dokument, og på trods af det næsten identiske navn er dette en helt anden mekanisme: begge skriver en rigtig ISO 32000-1 §12.5.6.10 tekst-markup-annotation, PDF /Subtype /Highlight, ind i sidens /Annots-array, med /QuadPoints der markerer det præcise glyf-forløb, og enhver konform PDF-fremviser gengiver den, når filen er gemt, ikke kun HotPDFs egen. Den samme mekanisme-grænse afgør, om et mærke går tur-retur gennem XFDF: en annotation oprettet med AddLoadedHighlightAnnotation er et normalt PDF-objekt, som ExportLoadedAnnotationsToXFDF samler op og overdrager til Acrobat eller et andet review-værktøj som ISO 19444-1-markup, dækket i import og eksport af PDF-annotationer som XFDF i Delphi, mens en region tilføjet gennem AddHighlightRegion er usynlig for den eksport, fordi den aldrig blev skrevet til objektgrafen overhovedet: den eksisterer kun, så længe den THPDFViewerModel, der oprettede den, gør. Hele familien af markup- og geometriske annotationstyper tilgængelige på en side, og hvordan et rektangel placerer hver enkelt, er dækket i artiklen om PDF-annotationer i Delphi med HotPDF, og den praktiske regel er enkel: hold et mærke engangs, mens et dokument stadig diskuteres, og gør det til en annotation, når en beslutning er endelig
Hvor highlight-laget stopper
Highlight-laget forsøger for sin del ikke at ligne en gennemsigtig overstregningstusch: RefreshDocument tegner hver region som et to-pixel omridsrektangel i sin egen farve oven på den cachede side-bitmap, på samme måde som den tegner søgetræffere, i stedet for at blande en farvet udfyldning over teksten under, så et klassisk gult-vask-udseende skal males i applikationskode eller overlades til en forfremmet annotations egen appearance stream. Én egenskab værd at genbruge, når en region findes, er CreateCurrentPageRegionSnapshot, som tager det samme THPDFRectangle en highlight allerede bærer og gengiver netop det område til en bitmap, nyttigt til at vedhæfte et lille forhåndsvisningsbillede til en review-kommentar uden at eksportere hele siden. En review-build behøver ikke vælge mellem de to mekanismer på forhånd: sæt hvert nyt mærke som standard til en engangs THPDFViewerHighlight-region, så længe en kommentartråd forbliver åben, og kald først AddLoadedHighlightAnnotation, når en reviewer afgør den, hvilket holder den indlæste PDF urørt gennem den frem-og-tilbage-proces, der producerer mest omskiftning. Fremviser-kontrollen beskrevet her er en del af standard-HotPDF-komponenten til Delphi og C++Builder, sammen med resten af de annotations- og formular-API'er, der refereres til ovenfor