Technical Article

Pregledovalnik PDF s continuous drsenjem v Delphiju s PDFium VCL

Ena sama stran formata A4, upodobljena pri udobni povečavi za branje, zavzame nekaj megabajtov 32-bitne bitne slike. Pomnožite to s 400-stransko pogodbo in računica preneha biti abstraktna: če upodobite vsako stran vnaprej, od sistema Windows zahtevate več kot gigabajt bitnih slik, ki si jih bo uporabnik ogledoval le po en zaslon naenkrat. Aplikaciji bodisi zmanjka naslovnega prostora v 32-bitni različici ali pa prve sekunde preživi zamrznjena, medtem ko grafična kartica in razčlenjevalnik strani meljeta strani, do katerih uporabnik sploh še ni podrsal. Bralnik z neprekinjenim (continuous) drsenjem mora dajati občutek enega dolgega traku strani, vendar ne more vseh hkrati držati v pomnilniku.

To neskladje je bistvo težave. PDFium VCL to rešuje znotraj komponente TPdfView, zato je večina dela izbira pravega načina prikaza in razumevanje tega, kaj komponenta počne v vašem imenu. Deli, ki jih ne stori namesto vas, kot sta prilagajanje velikosti strani za branje in ohranjanje odzivnosti hitrega drsenja, pa so mesta, kjer nekaj vrstic kode dokaže svojo vrednost. Če še vedno sestavljate okoliške elemente (orodno vrstico, sličice, iskalno polje), to področje pokriva vodnik za izgradnjo naprednega pregledovalnika; tukaj pa se osredotočamo na samo drsenje.

Postavitev je način prikaza, ne plošča bitnih slik

Instinkt pri delu z obrazci VCL je, da posežete po drsnem polju (scroll box) in vanj naložite kontrole slik, eno za vsako stran. Uprite se temu. Ta oblika vas prisili, da sami upravljate s pozicioniranjem strani, drsno aritmetiko in pomnilnikom naenkrat, vse to pa boste ponovno implementirali slabo. TPdfView že modelira dokument kot neprekinjen niz strani in izpostavlja postavitev prek svoje lastnosti DisplayMode.

Pdf := TPdf.Create(Self);
PdfView := TPdfView.Create(Self);
PdfView.Parent := Self;
PdfView.Align := alClient;
PdfView.Pdf := Pdf;

PdfView.DisplayMode := dmSingleContinuous;   // one page wide, scrolls vertically

Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
if not Pdf.Active then
  ShowMessage('Could not open the document');

To is celotna nastavitev za neprekinjeno drsenje. dmSingleContinuous postavi strani v en sam navpični stolpec z notranje vodenimi razmiki med njimi, pogled pa drsi po tem stolpcu kot po eni površini. Ni potrebe po povezovanju kontrol za vsako posamezno stran in ni treba pisati upravljalnika drsenja za običajno navigacijo. Upoštevajte preverjanje Pdf.Active po dodelitvi: odpiranje dokumenta nikoli ne sproži izjeme, zato poškodovana ali z geslom zaščitena datoteka pusti Active na False brez izjeme, ki bi jo lahko ulovili, pregledovalnik, ki preskoči to preverjanje, pa izriše prazno ploščo.

Ista lastnost podpira tudi dvostranske načine prikaza. dmTwoPageContinuous postavi strani vzporedno, dve v vrsto, za knjižni slog branja, ki ga nekateri dokumenti zahtevajo; dmTwoPageContinuousWithCover stori enako, vendar pusti prvi strani, da stoji samostojno kot naslovnica, tako da naslednji razporedi padejo na naravno sodo-liho mejo. Vsi trije načini drsijo neprekinjeno. Preklapljanje med njimi je vprašanje ene same dodelitve, kar olajša kasnejše dodajanje spustnega seznama za izbiro načina.

Rastersko se obdelajo le vidne strani

Razlog, zakaj se ta sistem prilagodi 400-stranski datoteki, je v tem, da je stolpec digitalen (virtualen). TPdfView pozna višino vsake strani iz drevesa strani dokumenta, zato lahko izračuna celoten obseg drsenja in položaj vsake strani brez vnaprejšnjega upodabljanja. Rasterizacija, ki je drag korak pretvorbe toka vsebine strani v slikovne pike (piksle), se zgodi le za strani, ki trenutno presekajo vidno polje (viewport), plus majhen rob, da je stran pripravljena, ko se vanjo podrsate. Ko drsite navzdol, se strani, ki vstopajo v vidno polje, upodobijo, stranem, ki ga zapustijo, pa se sprostijo bitne slike. Pomnilnik ostaja sorazmeren s tem, kar ustreza zaslonu, in ne dolžini dokumenta.

To je vredno ponotranjiti, saj spremeni vaš pogled na stroške delovanja. Odpiranje 400-stranskega dokumenta je poceni: razčleni strukturo, ne vsebine. Strošek se plača na stran in to leno (lazily), v trenutku, ko se stran približa drsenju. Pregledovalnik, ki daje občutek takojšnjega odprtja in gladkega drsenja, ne opravi manj dela na splošno, temveč delo le razporedi po dejanski bralni poti uporabnika in zavrže tisto, kar ostane zadaj. Praktična profesija je ta, da skoraj nikoli ne želite prisilno upodabljati strani pred uporabnikom. Pustite pogledu, da odloči, kaj je vidno.

Prilagodite strani širini, nato pustite povečavo pri miru

Bralni stolpec zahteva prilagoditev strani širini plošče in ne fiksne absolutne povečave. Lastnost FitMode to stori in ohranja nastavitev med spreminjanjem velikosti okna.

PdfView.FitMode := pfmFitWidth;   // each page fills the column width; height follows

S pfmFitWidth komponenta znova izračuna povečavo vsakič, ko se spremeni velikost pogleda, tako da stolpec vedno zapolni razpoložljivo širino, višine strani in s tem obseg drsenja pa sledijo temu. Obstaja ena past: neposredno dodeljevanje vrednosti Zoom ponastavi FitMode nazaj na pfmNone. To je namerno, saj sta ročna povečava in samodejno prilagajanje nasprotujoči si nameri, vendar pomeni, da naključna vrstica PdfView.Zoom := 1.0 nekje v vaši kodi tiho izklopi prilagajanje širini, naslednja sprememba velikosti pa se ne bo več prilagodila. Če ponujate nadzor nad povečavo in gumb za prilagajanje, ju obravnavajte kot preklop načina: nastavitev enega počisti doomed drugega, vi pa se odločite, kateri bo zmagal.

Za absolutne kontrole povečave, ki delujejo naravno, pogled izpostavlja prilagojene povečave kot vrednosti, ki jih lahko uporabite ali prikažete: PageWidthZoom[PageNumber] vrne povečavo, ki bi to stran prilagodila širini, ustrezen PageZoom pa prilagodi celotno stran. Branje teh lastnosti je način, kako napolnite meni "Prilagodi širini" / "Prilagodi strani" brez trdo kodiranih magičnih odstotkov, ki ne delujejo na ležečih ali prevelikih straneh.

Ohranite odzivnost hitrega drsenja s progresivnim upodabljanjem

Privzeta pot upodabljanja izriše stran do konca, preden vrne rezultat. Za eno stran je to v redu. Med hitrim drsenjem skozi obsežen dokument pa ne: vsaka stran, ki švigne mimo, sproži popolno rasterizacijo, in če uporabnik drsi hitreje, kot se strani lahko upodabljajo, se ta opravila kopičijo, plošča pa se zatika, ker se delo opravlja za strani, ki so ob koncu upodabljanja že izven zaslona. Rešitev je v tem, da naredimo upodabljanje preklicljivo in ga opustimo v trenutku, ko se uporabnik pomakne naprej.

Metoda RenderPageProgressive upodablja v kosih in preveri žeton za preklic (cancellation token) na vsaki meji kosa, tako da se lahko upodabljanje strani, ki je pravkar zdrsnila stran, opusti namesto izvede do konca.

type
  TFormMain = class(TForm)
    // ...
  private
    FRenderCancel: IPdfCancellationTokenSource;
    procedure RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
  end;

procedure TFormMain.RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
var
  Status: TPdfProgressiveStatus;
begin
  // Cancel whatever was rendering; the old token is now signaled.
  if Assigned(FRenderCancel) then
    FRenderCancel.Cancel;
  FRenderCancel := TPdfCancellationTokenSource.New;

  Pdf.PageNumber := PageNo;
  Status := Pdf.RenderPageProgressive(Bmp, 0, 0, Bmp.Width, Bmp.Height,
    FRenderCancel.Token);

  case Status of
    prsDone:      ;                    // bitmap is complete, paint it
    prsCancelled: Exit;                // superseded, discard this result
    prsFailed:    ShowMessage('Render failed for page ' + IntToStr(PageNo));
  end;
end;

Vračilna vrednost je tisto, kar šteje. prsDone pomeni, da je bitna slika popolnoma izrisana in pripravljena za prikaz; prsCancelled pomeni, da je novejši položaj drsenja nadomestil to stran, zato delni rezultat zavržete; prsFailed pa označuje dejansko napako na tej strani. Preklic se preverja na mejah kosov in ne vnaprej (preemptivno), zato pričakujte nekaj deset milisekund zakasnitve med klicem Cancel in dejansko zaustavitvijo upodabljanja. To je še vedno precej ceneje, kot da bi pustili, da zastarelo celostransko upodabljanje blokira vrsto. Posredovanje vrednosti nil kot žetona izvede upodabljanje do konca, kar je prava izbira za enkratno upodabljanje, kot je predogled tiskanja, kjer ni potrebe po preklicu.

Ko namesto tega pokličete funkcijsko obliko RenderPage, tisto, ki vrne novo TBitmap, ne pozabite, da je klicatelj njen lastnik in jo mora sprostiti s Free. V zanki drsenja, ki dodeli bitno sliko za vsako stran, je pozabljanje na to puščanje pomnilnika, ki raste z vsako stranjo, ki jo uporabnik preide, kar je natanko tista napaka z neomejenim pomnilnikom, ki naj bi jo zasnova continuous preprečila. Kjer je mogoče, upodabljajte v ponovno uporabljeno bitno sliko.

Bralnik z neprekinjenim drsenjem je večinoma delo, ki ga opravi komponenta. Izberete dmSingleContinuous za postavitev, nastavite pfmFitWidth, da se stolpec prilagaja oknu, in preverite Pdf.Active, da poškodovana datoteka jasno javi napako. Edini del, ki ga je vredno napisati samostojno, je preklicljivo upodabljanje, saj se bralnik ocenjuje po tem, kako se obnaša, ko nekdo povleče drsnik na dno dolgega dokumenta in plošča bodisi sledi bodisi ne. Vse ostalo, izbira besedila čez več strani, označevanje iskanja, drevo zaznamkov, je delo na vmesniku, ki sedi nad to površino drsenja in ne znotraj nje.

API-ji TPdfView, DisplayMode in RenderPageProgressive, prikazani tukaj, so del komponente PDFium VCL za Delphi in Lazarus.