Tehnični članak

Nedestruktivno označevanje PDF v Delphiju: pregledna plast HotPDF

Pravokotnik, narisan okoli odstavka med pregledom, ni nujno oznaka v PDF-ju. THPDFViewerModel v HotPDF izpostavlja AddHighlightRegion, metodo, ki vsako označitev hrani kot zapis v pomnilniku in ne kot spremembo naloženega dokumenta, zato lahko pregledovalec označi na desetine strani, medtem ko datoteka na disku ostane bajt za bajtom enaka. Povečajte na 6400 %, zavrtite stran za 90 stopinj, preklopite z možnosti Fit Width na Fit Page in isti pravokotnik bo še vedno na istem odstavku, ker se izračun koordinat v trenutku risanja oznake opira na dejansko geometrijo izrisa

Orodja za pregled, zgrajena okoli pregledovalnika PDF, se nenehno srečujejo s to težavo. Zaslon za rdeče označevanje, preverjanje ustvarjenih računov, notranji potek potrjevanja: vsa morajo omogočiti, da nekdo opozori na območje strani, ne da bi vsaka osnutna oznaka postala trajna sprememba datoteke in ne da bi bilo treba uporabiti celoten sistem pripisov samo za prikaz obarvanega polja, medtem ko se še odloča, ali oznaka sploh sodi v dokument. HotPDF to rešuje z namensko plastjo za označevanje, ki v celoti sedi na strani Model razdelitve, opisane v članku izdelava prilagojenega pregledovalnika PDF z arhitekturo MVC v Delphiju, zato lahko isti seznam oznak poganja tudi enotni test brez ročaja okna

Kaj dejansko shranjuje HotPDF AddHighlightRegion?

AddHighlightRegion za vsako oznako shrani natanko tri stvari: indeks strani z osnovo nič, THPDFRectangle v koordinatah uporabniškega prostora PDF in TColor, vse skupaj zapakirano kot zapis THPDFViewerHighlight znotraj THPDFViewerModel. Klic Viewer.HighlightRegion(PageIndex, PageRect, clYellow) ali enakovredni Model.AddHighlightRegion doda enega od teh zapisov v zasebno polje in vrne njegov indeks, ta indeks pa je edini ročaj, ki ga klicatelj prejme: ločenega objekta ni, ni vmesnika s štetjem sklicev in ničesar ni treba sprostiti. Vse druge zmožnosti v tem članku, risanje oznake, preslikava po spremembi povečave in brisanje, temeljijo na tem majhnem zapisu

Vsak pravokotnik se pred sprejemom normalizira in obreže. AddHighlightRegion zamenja levi in desni rob, če pregledovalec vleče z desne proti levi, zamenja zgornji in spodnji rob pri vlečenju navzgor, nato pa rezultat obreže glede na MediaBox strani, pridobljen prek GetLoadedPageBox. Pravokotnik, ki ima na koncu širino nič, višino nič ali v celoti leži zunaj strani, je takoj zavrnjen: metoda vrne -1 in na seznam ni dodano nič. Ta vrnjena vrednost ni zgolj okras: paket oznak, obnovljen iz zunanje pregledne datoteke ali iz zastarelih koordinat po zamenjavi strani, lahko tiho izgubi vnose, če je klicatelj ne preveri

Kako oznaka ostane poravnana po povečavi ali vrtenju?

Oznaka ostane poravnana, ker jo HotPDF shrani v prostor strani PDF in jo pri vsakem ponovnem izrisu znova projicira v prostor zaslona, namesto da bi shranil zaslonski pravokotnik, ki bi zastarel takoj, ko se spremeni raven povečave. THPDFViewerModel.PagePointToView in njegova obratna metoda ViewPointToPage to projekcijo izvedeta v dveh stopnjah: najprej z lastnim vnosom strani /Rotate, nato z neodvisnim vnosom pregledovalnika ViewRotation, ki se nikoli ne zapiše nazaj v PDF in vpliva samo na prikaz pregledovalnika. Razveljavitev transformacije ob sprostitvi miške izvede isti stopnji v obratnem vrstnem redu, zato lahko oznaka, narisana pri veliki povečavi na strani, zavrteni za 270 stopinj, pristane točno na pravem mestu, ko pregledovalec pogled ponastavi na Fit Page

Za to projekcijo je DPI enako pomemben kot vrtenje. Pregledovalnik HotPDF zajame natančen DPI bitne slike, ki je trenutno prikazana na zaslonu, v FRenderedDPI takoj po vsakem izrisu, ImageMouseUp pa isto vrednost posreduje v ViewPointToPage, zato se koordinata miške vedno pretvori z ločljivostjo, pri kateri je bila dejansko izrisana, ne z ločljivostjo, ponovno izračunano iz trenutne lastnosti povečave. CreatePageSnapshot in sorodne metode omejijo DPI na razpon od 12 do 2400, vendar interaktivna pot izrisa nima take zgornje meje: standardna lestvica povečave se konča pri 6400 %, kar pri privzeti osnovi 96 DPI pomeni precej več kot 2400 DPI, zato bi ponovna uporaba omejitve v slogu posnetka pri preslikavi koordinat premaknila vsako oznako za več slikovnih pik na vrhu razpona povečave. Interakcijo dopolnjujeta še dve manjši privzeti vrednosti: vlečenje, krajše od dveh slikovnih pik na kateri koli osi, se obravnava kot klik in ne ustvari oznake, označevanje pa se ne more začeti, dokler ni izrisana vsaj ena stran, saj se FRenderedDPI začne pri nič

Vključitev interaktivnega označevanja v pregledni zaslon

Vklop interaktivnega označevanja je opravilo s tremi lastnostmi neposredno na kontrolniku THPDFViewer: nastavite InteractionMode na vimHighlight namesto privzete vrednosti vimBrowse, izberite HighlightColor, ki je privzeto clYellow, in obravnavajte OnMarqueeSelect, da ugotovite, kaj je pregledovalec pravkar narisal. Vse drugo, zajem miške, risanje pikčastega izbirnega pravokotnika med vlečenjem, pretvorba končne točke nazaj v prostor strani in klic AddHighlightRegion, se izvede znotraj kontrolnika, preden se sproži ta dogodek

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 sproži samo pri vlečenju, ki je dejansko ustvarilo oznako: premajhen klik, da bi štel kot vlečenje, takoj počisti prekrivno plast izbire, vlečenje, ki se v celoti konča zunaj strani, pa doseže AddHighlightRegion, vendar je tam zavrnjeno enako kot programski klic, zato dogodek v obeh primerih ostane tih. Če se zdi, da se označevanje na robovih kontrolnika preneha odzivati, je pomembna ena izvedbena podrobnost: zajem miške pripada samemu THPDFViewer, potomcu TScrollBox, ne notranjemu TImage, ki prikazuje bitno sliko strani, zato lahko pregledovalec povleče čez rob izrisane strani in še vedno dobi čist dogodek sprostitve

Dodajanje, odstranjevanje in ponovno branje oznak iz kode

Oznake sploh ni treba ustvariti z vlečenjem miške. Viewer.HighlightRegion(PageIndex, PageRect, Color), ki vodi v isti Model.AddHighlightRegion, ki ga interaktivno vlečenje kliče interno, je javno izpostavljen prav zato, da lahko pregledni zaslon obnovi oznake iz podatkov, ki jih že ima: komentarjev, naloženih iz podatkovne zbirke, rezultatov besedilnega iskanja ali oznak, obnovljenih iz prejšnje seje. Ker so koordinate navadne številke uporabniškega prostora PDF, ta pot ni odvisna od tega, ali je bila stran najprej izrisana, za razliko od interaktivnega vlečenja, ki potrebuje, da FRenderedDPI že vsebuje dejansko 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;

Pri odstranjevanju posamezne oznake postane shranjevanje v polju očitno. RemoveHighlightRegion izbriše en zapis in vsak poznejši zapis premakne za eno mesto navzdol, da zapre vrzel, kar pomeni, da noben indeks, zajet prej, iz dogodka OnMarqueeSelect ali prejšnjega naštevanja, ni več zanesljiv, ko je odstranjen element pred njim na seznamu. OnHighlightChange se sproži ob vsakem dodajanju, odstranjevanju in klicu ClearHighlightRegions, vendar ne vsebuje podatka o tem, kaj se je spremenilo, zato je varen vzorec, da ga obravnavate kot signal za ponovno izgradnjo seznama, ki ga prikazuje pregledna plošča, iz HighlightCount in TryGetHighlightRegion, namesto da bi predpomnjeni indeks sproti popravljali

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;

Kdaj naj oznaka postane pravi pripis Highlight?

Območje označevanja mora postati pravi pripis v trenutku, ko mora preživeti zunaj tega enega primerka THPDFViewer. HotPDF izpostavlja tudi AddHighlightAnnotation za novo stran in AddLoadedHighlightAnnotation za že naložen dokument, vendar je to kljub skoraj enakemu imenu povsem drugačen mehanizem: oba v polje /Annots strani zapišeta dejanski pripis za označevanje besedila ISO 32000-1 §12.5.6.10, PDF /Subtype /Highlight, pri čemer /QuadPoints označujejo natančen niz glifov, skladen pregledovalnik PDF pa ga izriše, ko je datoteka shranjena, ne le HotPDF. Ista meja mehanizmov odloča, ali bo oznaka prešla skozi XFDF: pripis, ustvarjen z AddLoadedHighlightAnnotation, je običajen objekt PDF, ki ga ExportLoadedAnnotationsToXFDF zajame in preda Acrobatu ali drugemu preglednemu orodju kot oznako ISO 19444-1, kar je obravnavano v članku uvoz in izvoz pripisov PDF kot XFDF v Delphiju, medtem ko je območje, dodano prek AddHighlightRegion, pri tem izvozu nevidno, ker sploh ni bilo zapisano v graf objektov: obstaja samo toliko časa, kolikor obstaja THPDFViewerModel, ki ga je ustvaril. Celotna družina pripisov za označevanje in geometrijskih pripisov, ki so na voljo na strani, ter način, kako pravokotnik umesti vsakega od njih, je opisana v članku pripisi PDF v Delphiju s komponento HotPDF, praktično pravilo pa je preprosto: oznaka naj bo začasna, dokler se o dokumentu še razpravlja, v pripis pa jo zapišite, ko je odločitev dokončna

Kje se plast za označevanje konča

Plast za označevanje se ne poskuša obnašati kot prosojni označevalnik: RefreshDocument vsako območje nariše kot dvotočkovni obrisni pravokotnik v njegovi barvi čez predpomnjeno bitno sliko strani, enako kot riše zadetke iskanja, namesto da bi čez besedilo spodaj zlila obarvano polnilo, zato je treba klasičen videz rumenega premaza izrisati v kodi aplikacije ali ga prepustiti lastnemu toku videza promoviranega pripisa. Ko območje obstaja, je vredno ponovno uporabiti možnost CreateCurrentPageRegionSnapshot, ki sprejme isti THPDFRectangle, kot ga že vsebuje oznaka, in v bitno sliko izriše samo to območje, kar je uporabno za pripenjanje majhne predogledne slike k pregledu komentarja brez izvoza celotne strani. Pregledna izdelava se ni treba vnaprej odločiti med mehanizmoma: vsako novo oznako lahko privzeto obravnavate kot začasno območje THPDFViewerHighlight, dokler je nit komentarjev odprta, in AddLoadedHighlightAnnotation pokličete šele, ko jo pregledovalec razreši, s čimer ostane naloženi PDF nedotaknjen med izmenjavo, ki povzroči največ sprememb. Tukaj opisan kontrolnik pregledovalnika je del standardne komponente HotPDF za Delphi in C++Builder, skupaj z drugimi API-ji za pripise in obrazce, navedenimi zgoraj