Tehnički članak

Nedestruktivno isticanje PDF-a u Delphiju: HotPDF sloj za pregled

Pravougaonik nacrtan oko pasusa tokom pregleda ne mora postati oznaka u PDF-u. HotPDF-ov THPDFViewerModel izlaže AddHighlightRegion, metod koji svako isticanje čuva kao zapis u memoriji, a ne kao izmenu učitanog dokumenta, pa pregledalac može da označi desetine stranica dok datoteka na disku ostaje bajt po bajt ista. Uvećajte prikaz na 6400%, rotirajte stranicu za 90 stepeni, pređite sa opcije Fit Width na Fit Page i isti pravougaonik će i dalje pasti na isti pasus, jer se proračun koordinata u trenutku crtanja oznake zasniva na stvarnoj geometriji prikaza

Alatke za pregled zasnovane na PDF pregledaču stalno nailaze na ovaj problem. Ekran za redigovanje, QA provera generisanih faktura ili interni tok odobravanja — svi moraju da omoguće skretanje pažnje na deo stranice, a da svaka probna oznaka ne postane trajna izmena datoteke i da se ne uvodi čitav podsistem anotacija samo da bi se prikazao obojeni okvir dok se još odlučuje da li oznaka treba da ostane. HotPDF to rešava namenskim slojem za isticanje koji se u potpunosti nalazi na strani Model podele opisane u članku o izradi prilagođenog PDF pregledača sa MVC arhitekturom u Delphiju, zbog čega istom listom isticanja može da upravlja i jedinični test bez ikakvog prozorskog handle-a

Šta AddHighlightRegion zapravo čuva u HotPDF-u?

AddHighlightRegion čuva tačno tri stvari po oznaci: indeks stranice koji počinje od nule, THPDFRectangle u koordinatama PDF korisničkog prostora i TColor, sve upakovano kao zapis THPDFViewerHighlight unutar THPDFViewerModel. Poziv Viewer.HighlightRegion(PageIndex, PageRect, clYellow), ili ekvivalentni Model.AddHighlightRegion, dodaje jedan takav zapis u privatni niz i vraća njegov indeks, što je jedini handle koji pozivalac dobija: nema posebnog objekta, interfejsa sa brojanjem referenci niti nečega što treba osloboditi. Svaka druga mogućnost u ovom članku, crtanje oznake, njeno ponovno mapiranje posle promene uvećanja i brisanje, izgrađena je na tom jednom malom zapisu

Svaki pravougaonik se normalizuje i odseca pre prihvatanja. AddHighlightRegion zamenjuje levu i desnu ivicu ako pregledalac prevlači zdesna nalevo, zamenjuje gornju i donju ivicu pri prevlačenju nagore, a zatim odseca rezultat prema MediaBox-u stranice preuzetom preko GetLoadedPageBox. Pravougaonik čija širina ili visina na kraju iznosi nulu, ili koji se u celini nalazi izvan stranice, odmah se odbacuje: metod vraća -1 i ništa se ne dodaje na listu. Ta povratna vrednost nije ukrasna: grupa isticanja ponovo izgrađena iz spoljne datoteke za pregled ili iz zastarelih koordinata nakon zamene stranice može nečujno izgubiti unose ako je pozivalac ne proveri

Kako isticanje ostaje poravnato posle zumiranja ili rotacije?

Isticanje ostaje poravnato zato što ga HotPDF čuva u prostoru PDF stranice i pri svakom ponovnom iscrtavanju projektuje u prostor ekrana, umesto da čuva pravougaonik ekrana koji bi zastareo čim se promeni nivo uvećanja. THPDFViewerModel.PagePointToView i njegova inverzna funkcija ViewPointToPage obavljaju tu projekciju u dve faze: prvo primenjuju sopstveni unos stranice /Rotate, a zatim nezavisni unos pregledača ViewRotation, koji se nikada ne upisuje nazad u PDF i utiče samo na ono što pregledač prikazuje. Poništavanje transformacije pri otpuštanju miša prolazi kroz iste dve faze obrnutim redom, pa isticanje nacrtano pri velikom uvećanju na stranici rotiranoj za 270 stepeni završava tačno na pravom mestu kada pregledalac vrati prikaz na Fit Page

DPI koji se koristi za tu projekciju jednako je važan kao i rotacija. HotPDF-ov Viewer beleži tačan DPI trenutno prikazane bitmape u FRenderedDPI odmah posle svakog iscrtavanja, a ImageMouseUp prosleđuje istu vrednost funkciji ViewPointToPage, tako da se koordinata miša uvek pretvara pomoću rezolucije pri kojoj je zaista nacrtana, a ne pomoću rezolucije ponovo izračunate iz trenutnog svojstva uvećanja. CreatePageSnapshot i srodne funkcije ograničavaju DPI na raspon od 12 do 2400, ali interaktivna putanja iscrtavanja nema takvu gornju granicu: standardna lestvica uvećanja dostiže 6400%, što pri podrazumevanoj osnovi od 96 DPI daje znatno više od 2400 DPI, pa bi ponovno korišćenje ograničenja za snimke pomerilo svako isticanje za nekoliko piksela pri najvećem uvećanju. Interakciju dopunjuju još dve male podrazumevane vrednosti: prevlačenje kraće od dva piksela po bilo kojoj osi tretira se kao klik i ne pravi isticanje, a isticanje ne može da počne dok se bar jedna stranica stvarno ne iscrtа, jer FRenderedDPI počinje od nule

Povezivanje interaktivnog isticanja sa ekranom za pregled

Uključivanje interaktivnog isticanja svodi se na tri svojstva same kontrole THPDFViewer: postavite InteractionMode na vimHighlight umesto podrazumevanog vimBrowse, izaberite HighlightColor, čija je podrazumevana vrednost clYellow, i obradite OnMarqueeSelect da biste saznali šta je pregledalac upravo nacrtao. Sve ostalo, hvatanje miša, crtanje isprekidanog pravougaonika izbora tokom prevlačenja, pretvaranje tačke otpuštanja nazad u prostor stranice i poziv AddHighlightRegion, obavlja se unutar kontrole pre nego što se taj događaj pokrene

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 se pokreće samo kada prevlačenje zaista napravi isticanje: premali klik koji se ne računa kao prevlačenje odmah uklanja sloj izbora, a prevlačenje koje se u celosti završi izvan stranice stiže do AddHighlightRegion, gde se odbacuje na isti način kao programski poziv, pa događaj u oba slučaja ostaje neaktivan. Jedan detalj implementacije vredi zapamtiti ako isticanje ikada prestane da reaguje na ivicama kontrole: hvatanje miša pripada samom THPDFViewer-u, izvedenom iz TScrollBox-a, a ne unutrašnjem TImage-u koji prikazuje bitmapu stranice, pa pregledalac može da prevuče pokazivač preko ivice iscrtane stranice i da i dalje dobije uredno otpuštanje

Dodavanje, uklanjanje i ponovno učitavanje isticanja iz koda

Isticanja uopšte ne moraju da nastanu prevlačenjem miša. Viewer.HighlightRegion(PageIndex, PageRect, Color), koji vodi do istog Model.AddHighlightRegion koji interaktivno prevlačenje poziva interno, javno je dostupan upravo da bi ekran za pregled mogao da obnovi isticanja iz podataka koje već ima: komentara učitanih iz baze, rezultata tekstualne pretrage ili oznaka vraćenih iz prethodne sesije. Pošto su koordinate obični brojevi u PDF korisničkom prostoru, ova putanja ne zavisi od toga da li je stranica prethodno iscrtana, za razliku od interaktivnog prevlačenja koje zahteva da FRenderedDPI već sadrži stvarnu vrednost

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;

Uklanjanje jednog isticanja pokazuje da je skladištenje zasnovano na nizu vidljivo spolja. RemoveHighlightRegion briše jedan zapis i pomera svaki kasniji zapis za jedno mesto naniže da bi zatvorio prazninu, što znači da indeks sačuvan ranije, bilo iz događaja OnMarqueeSelect ili iz prethodnog nabrajanja, više nije pouzdan kada se ukloni nešto što se nalazilo ispred njega na listi. OnHighlightChange se pokreće pri svakom dodavanju, uklanjanju i pozivu ClearHighlightRegions, ali ne nosi informaciju o tome šta se promenilo, pa je bezbedan obrazac tretirati ga kao signal za ponovnu izgradnju liste koju prikazuje panel za pregled pomoću HighlightCount i TryGetHighlightRegion, umesto menjanja keširanog indeksa na mestu

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;

Kada oznaka treba da postane prava Highlight anotacija?

Region isticanja treba da postane prava anotacija čim mora da preživi izvan te jedne instance THPDFViewer-a. HotPDF takođe izlaže AddHighlightAnnotation za novu stranicu i AddLoadedHighlightAnnotation za već učitani dokument, a uprkos gotovo identičnom nazivu, to je potpuno drugačiji mehanizam: oba upisuju stvarnu tekstualnu anotaciju za označavanje prema ISO 32000-1 §12.5.6.10, PDF /Subtype /Highlight, u niz /Annots stranice, sa /QuadPoints koji označavaju tačan niz glifova, a svaki usklađeni PDF pregledač je prikazuje kada se datoteka sačuva, ne samo HotPDF. Ista granica mehanizma određuje da li se oznaka prenosi kroz XFDF: anotacija napravljena pomoću AddLoadedHighlightAnnotation jeste uobičajeni PDF objekat koji ExportLoadedAnnotationsToXFDF preuzima i prosleđuje programu Acrobat ili drugoj alatki za pregled kao oznaku prema ISO 19444-1, što je obrađeno u članku o uvozu i izvozu PDF anotacija kao XFDF u Delphiju, dok je region dodat preko AddHighlightRegion nevidljiv tom izvozu jer nikada nije upisan u graf objekata: postoji samo dok postoji THPDFViewerModel koji ga je napravio. Cela porodica tipova oznaka i geometrijskih anotacija dostupnih na stranici, kao i način na koji pravougaonik postavlja svaku od njih, obrađena je u članku o PDF anotacijama u Delphiju sa HotPDF-om, a praktično pravilo je jednostavno: držite oznaku privremenom dok se o dokumentu još raspravlja, a zatim je upišite kao anotaciju kada odluka bude konačna

Gde se sloj za isticanje završava

Sloj za isticanje ne pokušava da izgleda kao providni marker: RefreshDocument iscrtava svaki region kao pravougaonik sa konturom debljine dva piksela u njegovoj sopstvenoj boji preko keširane bitmape stranice, na isti način na koji iscrtava rezultate pretrage, umesto da stapa obojenu ispunu sa tekstom ispod nje, pa klasičan izgled žutog premaza mora da se iscrta u kodu aplikacije ili odloži do pojavljivanja anotacije sa sopstvenim tokom izgleda. Jedna mogućnost koju vredi ponovo upotrebiti čim region postoji jeste CreateCurrentPageRegionSnapshot, koji uzima isti THPDFRectangle koji isticanje već nosi i iscrtava samo tu oblast u bitmapu, što je korisno za dodavanje male slike pregleda uz komentar bez izvoza cele stranice. Verzija za pregled ne mora unapred da bira između ta dva mehanizma: podrazumevano svaku novu oznaku čuvajte kao privremeni region THPDFViewerHighlight dok je nit komentara otvorena, a pozovite AddLoadedHighlightAnnotation tek kada je pregledalac reši, čime učitani PDF ostaje netaknut tokom rasprave koja donosi najviše promena. Ovde opisana kontrola pregledača deo je standardnog HotPDF Component za Delphi i C++Builder, zajedno sa ostatkom API-ja za anotacije i obrasce pomenutog iznad