Teknisk artikkel

Ikke-destruktiv PDF-highlighting i Delphi: HotPDFs gjennomgangslag

Et rektangel tegnet rundt et avsnitt under en gjennomgang, trenger ikke bli et merke inne i PDF-en. HotPDFs THPDFViewerModel eksponerer AddHighlightRegion, en metode som holder hver highlight som en post i minnet i stedet for en endring av det innlastede dokumentet, slik at en korrekturleser kan markere dusinvis av sider mens filen på disk forblir byte-for-byte det den var. Zoom til 6400 %, roter siden 90 grader, bytt fra Tilpass bredde til Tilpass side, og det samme rektangelet havner fortsatt på det samme avsnittet, fordi koordinatmatematikken går gjennom den faktiske gjengivelsesgeometrien i det øyeblikket merket ble tegnet

Gjennomgangsverktøy bygget rundt en PDF-fremviser støter stadig på dette problemet. En rettelsesskjerm, en QA-gjennomgang av genererte fakturaer, en intern godkjenningsflyt: alle trenger de å la noen rette oppmerksomheten mot et område på en side uten at hvert kladdemerke blir en permanent endring av filen, og uten å måtte ty til et fullt annoteringssubsystem bare for å vise en fargeboks mens noen fortsatt vurderer om merket hører hjemme der. HotPDF svarer på det med et dedikert highlight-lag som sitter fullstendig på Modell-siden av oppdelingen beskrevet i å bygge en egendefinert PDF-fremviser med en MVC-arkitektur i Delphi, noe som også er grunnen til at den samme highlight-listen kan drives fra en enhetstest uten et eneste vindushåndtak i sikte

Hva lagrer egentlig HotPDFs AddHighlightRegion?

AddHighlightRegion lagrer nøyaktig tre ting per merke: en nullbasert sideindeks, en THPDFRectangle i PDF-brukerromskoordinater, og en TColor, alt pakket som en THPDFViewerHighlight-record inne i THPDFViewerModel. Å kalle Viewer.HighlightRegion(PageIndex, PageRect, clYellow), eller det tilsvarende Model.AddHighlightRegion, legger til en av disse recordene i et privat array og returnerer indeksen dens, og den indeksen er det eneste håndtaket en kaller får tilbake: det finnes ikke noe separat objekt, intet referansetellet grensesnitt, ingenting å frigjøre. Alle andre muligheter i denne artikkelen, å tegne merket, kartlegge det på nytt etter en zoom-endring, slette det, er bygget på toppen av den ene lille recorden

Hvert rektangel normaliseres og klippes før det aksepteres. AddHighlightRegion bytter venstre og høyre kant hvis en korrekturleser drar fra høyre mot venstre, bytter topp og bunn for en oppoverdra, og klipper deretter resultatet mot sidens MediaBox hentet gjennom GetLoadedPageBox. Et rektangel som ender opp med null bredde, null høyde, eller helt utenfor siden, avvises rett og slett: metoden returnerer -1 og ingenting legges til listen. Den returverdien er ikke dekorativ: en batch med highlights gjenoppbygd fra en ekstern gjennomgangsfil, eller fra utdaterte koordinater etter at en side ble erstattet, kan stille miste oppføringer hvis kalleren ikke sjekker for det

Hvordan holder en highlight seg justert etter zoom eller rotasjon?

En highlight holder seg justert fordi HotPDF lagrer den i PDF-sideplass og re-projiserer den inn i skjermplass ved hver omtegning, i stedet for å lagre et skjermrektangel som ville blitt utdatert i det øyeblikket zoom-nivået endres. THPDFViewerModel.PagePointToView og dens motsatte, ViewPointToPage, utfører den projeksjonen i to trinn: først sidens egen /Rotate-oppføring, deretter Fremviserens uavhengige ViewRotation, som aldri skrives tilbake til PDF-en og bare påvirker hva Fremviseren viser. Å reversere transformasjonen ved musesleppet kjører de samme to trinnene baklengs, noe som er det som lar en highlight tegnet ved høy zoom på en side rotert 270 grader, havne på nøyaktig riktig plass etter at korrekturleseren tilbakestiller visningen til Tilpass side

DPI-en brukt til den projeksjonen betyr like mye som rotasjonen. HotPDFs Fremviser fanger den eksakte DPI-en til bitkartet som for øyeblikket er på skjermen, i FRenderedDPI rett etter hver gjengivelse, og ImageMouseUp sender den samme verdien inn i ViewPointToPage slik at en musekoordinat alltid konverteres ved bruk av oppløsningen den faktisk ble tegnet med, ikke en oppløsning gjenberegnet fra den gjeldende zoom-egenskapen. CreatePageSnapshot og dens slektninger begrenser DPI til et 12-til-2400-område, men den interaktive gjengivelsesveien har ikke noe slikt tak: den standard zoom-stigen topper ut på 6400 %, noe som beregner til godt over 2400 DPI ved standard 96 DPI-grunnlinjen, så å gjenbruke en snapshot-aktig grense for koordinatkartlegging ville forskjøvet hver highlight med flere piksler på toppen av zoom-området. To mindre standardverdier runder av interaksjonen: en dra kortere enn to piksler på hver akse behandles som et klikk og produserer ingen highlight, og highlighting kan ikke begynne før minst én side faktisk har blitt gjengitt, ettersom FRenderedDPI starter på null

Å koble interaktiv highlighting inn i en gjennomgangsskjerm

Å skru på interaktiv highlighting er en jobb med tre egenskaper på selve THPDFViewer-kontrollen: sett InteractionMode til vimHighlight i stedet for standarden vimBrowse, velg en HighlightColor, som som standard er clYellow, og håndter OnMarqueeSelect for å finne ut hva korrekturleseren nettopp tegnet. Alt annet, å fange musen, tegne det prikkete markeringsrektangelet mens korrekturleseren drar, konvertere sleppepunktet tilbake til sideplass, kalle AddHighlightRegion, skjer inne i kontrollen før den hendelsen utlø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 utløses bare for en dra-bevegelse som faktisk produserte en highlight: et klikk for lite til å telle som en dra, fjerner markeringsoverlegget umiddelbart, og en dra-bevegelse som havner helt utenfor siden, når AddHighlightRegion, men avvises der på samme måte som et programmatisk kall ville blitt, slik at hendelsen forblir stille uansett. Én implementasjonsdetalj verdt å kjenne til hvis highlighting noensinne virker til å slutte å svare ved kantene av kontrollen: musefangst tilhører selve THPDFViewer, en TScrollBox-etterkommer, ikke det interne TImage-et som viser sidebitkartet, noe som er det som lar en korrekturleser dra forbi kanten av den gjengitte siden og likevel få et rent slipp

Å legge til, fjerne, og lese highlights på nytt fra kode

Highlights trenger slett ikke å komme fra en musedra i det hele tatt. Viewer.HighlightRegion(PageIndex, PageRect, Color), som kanaliseres inn i den samme Model.AddHighlightRegion den interaktive draen kaller internt, er offentlig spesifikt slik at en gjennomgangsskjerm kan gjenoppbygge highlights fra data den allerede har: kommentarer lastet fra en database, resultater fra et tekstsøk, eller merker gjenopprettet fra en tidligere økt. Fordi koordinatene er rene PDF-brukerromstall, avhenger ingenting ved denne veien av at en side har blitt gjengitt først, i motsetning til den interaktive draen, som trenger at FRenderedDPI allerede holder en reell verdi

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;

Å fjerne én enkelt highlight er der den array-baserte lagringen skinner gjennom. RemoveHighlightRegion sletter én record og forskyver hver senere record én posisjon ned for å lukke gapet, noe som betyr at enhver indeks fanget tidligere, fra en OnMarqueeSelect-hendelse eller fra en tidligere opplisting, ikke lenger er pålitelig når noe foran den i listen blir fjernet. OnHighlightChange utløses ved hvert legg-til, fjern, og ClearHighlightRegions-kall, men den bærer ingen informasjon om hva som endret seg, så det trygge mønsteret er å behandle den som et signal om å gjenoppbygge hvilken som helst liste et gjennomgangspanel viser fra HighlightCount og TryGetHighlightRegion, i stedet for å lappe en bufret indeks på plass

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;

Når bør et merke i stedet bli en ekte Highlight-annotering?

En highlight-region bør bli en ekte annotering i det øyeblikket den trenger å overleve utenfor den ene THPDFViewer-instansen. HotPDF eksponerer også AddHighlightAnnotation for en ny side og AddLoadedHighlightAnnotation for et allerede innlastet dokument, og til tross for det nesten identiske navnet, er dette en helt annen mekanisme: begge skriver en faktisk ISO 32000-1 §12.5.6.10 tekstmarkerings-annotering, PDF /Subtype /Highlight, inn i sidens /Annots-array, med /QuadPoints som markerer det eksakte glyf-forløpet, og enhver konform PDF-fremviser gjengir den så snart filen er lagret, ikke bare HotPDFs egen. Den samme mekanismegrensen avgjør om et merke rundturer gjennom XFDF: en annotering opprettet med AddLoadedHighlightAnnotation er et normalt PDF-objekt som ExportLoadedAnnotationsToXFDF plukker opp og overleverer til Acrobat eller et annet gjennomgangsverktøy som ISO 19444-1-markering, dekket i import og eksport av PDF-annoteringer som XFDF i Delphi, mens en region lagt til gjennom AddHighlightRegion er usynlig for den eksporten fordi den aldri ble skrevet til objektgrafen i det hele tatt: den eksisterer bare så lenge THPDFViewerModel-en som opprettet den, gjør det. Hele familien av markerings- og geometriske annoteringstyper tilgjengelig på en side, og hvordan et rektangel plasserer hver av dem, er dekket i artikkelen om PDF-annoteringer i Delphi med HotPDF, og den praktiske regelen er enkel: hold et merke engangs så lenge et dokument fortsatt diskuteres, og bind det til en annotering først når en avgjørelse er endelig

Hvor highlight-laget stopper

Highlight-laget, for sin del, gjør ingen forsøk på å se ut som en gjennomskinnelig tekstmarkeringspenn: RefreshDocument tegner hver region som et to-piksel omrissrektangel i sin egen farge oppå det bufrede sidebitkartet, på samme måte som den tegner søketreff, i stedet for å blande en farget fylling over teksten under, slik at et klassisk gult vask-utseende må males i applikasjonskode eller utsettes til en forfremmet annoterings egen appearance stream. Én mulighet verdt å gjenbruke når en region først eksisterer, er CreateCurrentPageRegionSnapshot, som tar den samme THPDFRectangle-en en highlight allerede bærer, og gjengir bare det området til et bitkart, nyttig for å feste et lite forhåndsvisningsbilde til en gjennomgangskommentar uten å eksportere hele siden. Et gjennomgangsbygg trenger ikke velge mellom de to mekanismene på forhånd: la hvert nytt merke som standard bli en engangs THPDFViewerHighlight-region så lenge en kommentartråd forblir åpen, og kall AddLoadedHighlightAnnotation først når en korrekturleser avgjør den, noe som holder den innlastede PDF-en urørt gjennom frem-og-tilbake-prosessen som skaper mest gjennomtrekk. Fremviserkontrollen beskrevet her er en del av den standard HotPDF-komponenten for Delphi og C++Builder, sammen med resten av annoterings- og skjema-API-ene referert ovenfor