Jedna A4 stranica renderovana na ugodnom zumu za čitanje zauzima nekoliko megabajta 32-bitne bitmap slike. Pomnožite to sa ugovorom od 400 stranica i računica prestaje da bude apstraktna: renderujte svaku stranicu unapred i tražićete od Windows-a znatno više od gigabajta bitmapa koje će korisnik gledati ekran po ekran. Aplikacija ili ostaje bez adresnog prostora na 32-bitnoj verziji ili provodi prvih nekoliko sekundi zamrznuta dok grafički procesor (GPU) i parser stranica prolaze kroz stranice do kojih korisnik još nije ni skrolovao. Čitač sa neprekidnim skrolovanjem (continuous scroll) mora da deluje kao jedna dugačka traka stranica, ali ne može zapravo da drži sve njih u memoriji odjednom
Ta tenzija je čitav problem ovde. PDFium komponenta ga rešava unutar klase TPdfView, tako da je većina posla izbor ispravnog režima prikaza (display mode) i razumevanje onoga što komponenta radi u vaše ime. Delovi koje ona ne radi za vas — određivanje veličine stranica za tok čitanja i održavanje odziva pri brzom skrolovanju — jesu mesta gde malo koda opravdava svoj udeo. Ako i dalje sklapate prateće elemente korisničkog interfejsa (traku sa alatkama, sličice, polje za pretragu), vodič kroz pregledač bogat funkcijama pokriva tu temu. Ovde je predmet sam proces skrolovanja
Izgled (layout) je režim prikaza, a ne panel sa bitmapama
Instinkt iz rada sa VCL formama jeste da posegnete za scroll box-om i u njega naslažete kontrole slika, po jednu za svaku stranicu. Oduprite se tome. Taj dizajn vas primorava da sami vodite računa o pozicioniranju stranica, matematici skrolovanja i pitanju memorije odjednom, i sve to ćete ponovo izmisliti na loš način. TPdfView već modelira dokument kao neprekidan niz stranica i izlaže taj raspored kroz svoje svojstvo 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 je čitavo podešavanje za neprekidno skrolovanje. Režim dmSingleContinuous raspoređuje stranice u jednu vertikalnu kolonu sa razmacima između njih koji se rešavaju interno, i pogled skroluje kroz tu kolonu kao kroz jednu površinu. Nema kontrola na nivou stranica koje treba povezivati i nema hendlera skrolovanja koje treba pisati za uobičajenu navigaciju. Obratite pažnju na proveru svojstva Pdf.Active nakon dodele: otvaranje dokumenta nikada ne podiže izuzetke, tako da oštećena ili lozinkom zaštićena datoteka ostavlja Active na vrednosti False bez ikakvog izuzetka koji bi se mogao uhvatiti, a pregledač koji preskoči ovu proveru prikazuje prazan panel i krivi sebe za to
Isto svojstvo nosi režime rasporeda na dve stranice (spread modes). dmTwoPageContinuous postavlja stranice jednu pored druge, po dve u redu, za čitanje u stilu knjige koje neki dokumenti zahtevaju. dmTwoPageContinuousWithCover radi isto to, ali dopušta da prva stranica stoji samostalno kao korica, tako da preostali rasporedi padaju na prirodnu granicu parnih i neparnih stranica. Sva tri režima skroluju neprekidno. Prelazak između njih je jedna obična dodela vrednosti, što dodavanje padajuće liste za izbor režima prikaza kasnije čini trivijalnim
Samo vidljive stranice se rasterizuju
Razlog zašto ovo radi na datoteci od 400 stranica jeste to što je kolona virtuelna. Klasa TPdfView zna visinu svake stranice iz stabla stranica dokumenta, tako da može izračunati ukupan opseg skrolovanja i poziciju svake stranice bez rasterizacije bilo čega. Rasterizacija, skup korak koji pretvara tok sadržaja stranice u piksele, dešava se samo za stranice koje trenutno presecaju prozor za prikaz (viewport), uz malu marginu kako bi stranica bila spremna do trenutka kada se skroluje u vidno polje. Kako skrolujete nadole, stranice koje ulaze u prozor za prikaz se renderuju, a onima koje ga napuštaju oslobađaju se bitmap slike. Potrošnja memorije ostaje proporcionalna onome što staje na ekran, a ne dužini dokumenta
Ovo vredi usvojiti jer menja način na koji razmišljate o računarskim troškovima. Otvaranje dokumenta od 400 stranica je jeftino: parsira se struktura, a ne sadržaj. Trošak se plaća po stranici i to lenjo (lazily) — u trenutku kada se stranica skroluje u blizinu. Pregledač koji deluje trenutno pri otvaranju i glatko pri skrolovanju ne radi manje posla ukupno, već ga raspoređuje duž stvarne korisničke putanje čitanja i odbacuje ono što ostane iza nje. Praktična posledica je da skoro nikada ne želite da forsirate renderovanje stranica ispred korisnika. Pustite da pogled (view) odluči šta je vidljivo
Prilagodite veličinu stranica širini, pa ostavite zum na miru
Kolona za čitanje zahteva da veličina stranica bude prilagođena širini panela, a ne fiksirana na apsolutni zum. Svojstvo FitMode radi upravo to i nastavlja sa tim kako se prozor menja
PdfView.FitMode := pfmFitWidth; // each page fills the column width; height follows
Sa postavkom pfmFitWidth komponenta ponovo izračunava zum kad god se veličina pogleda promeni, tako da kolona uvek popunjava dostupnu širinu, a visine stranica i opseg skrolovanja slede iz toga. Postoji jedna zamka u koju ljudi upadaju: direktno dodeljivanje vrednosti svojstvu Zoom vraća FitMode nazad na pfmNone. To je namerno, jer su ručni zum i automatsko uklapanje kontradiktorne namere, ali to znači da zalutali poziv PdfView.Zoom := 1.0 negde u vašem kodu prećutno isključuje uklapanje po širini i sledeća promena veličine prestaje da reflektuje izmene izgleda. Ako nudite i kontrolu zuma i dugme za uklapanje, tretirajte ih kao promenu režima rada: postavljanje jednog briše drugo, a vi odlučujete šta pobeđuje
Za kontrole apsolutnog zuma koje se čitaju prirodno, pogled izlaže zumove za uklapanje kao vrednosti koje možete primeniti ili prikazati: PageWidthZoom[PageNumber] vraća zum koji bi uklopio tu stranicu po širini, a odgovarajući PageZoom uklapa čitavu stranicu. Čitanje ovih vrednosti je način na koji popunjavate meni "Uklopi po širini" / "Uklopi stranicu" bez ručnog kodiranja magičnih procenata koji greše na horizontalnim (landscape) ili prevelikim stranicama
Održite brz odziv pri skrolovanju pomoću progresivnog renderovanja
Podrazumevana putanja renderovanja iscrtava stranicu do kraja pre nego što završi rad. Za jednu stranicu to je u redu. Tokom brzog skrolovanja kroz gust dokument to nije slučaj: svaka stranica koja bljesne pokreće punu rasterizaciju, i ako korisnik skroluje brže nego što se stranice mogu renderovati, ta renderovanja se gomilaju i panel počinje da se koči jer se radi posao za stranice koje su već van ekrana do trenutka kada se rendering završi. Rešenje je da se renderovanje učini otkazivim i da se napusti onog trenutka kada korisnik pređe dalje
Metoda RenderPageProgressive renderuje u delovima (chunks) i proverava token za otkazivanje na granici svakog dela, tako da se započeto renderovanje stranice koja je upravo skrolovana van ekrana može odbaciti umesto da se izvršava do kraja
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;
Oblik koji je važan jeste povratna vrednost. Vrednost prsDone znači da je bitmapa potpuno nacrtana i spremna za prikaz na ekranu. prsCancelled znači da je novija pozicija skrolovanja zamenila ovu stranicu, pa delimični rezultat odbacujete umesto da ga prikažete. prsFailed je stvarna greška na toj stranici. Otkazivanje se proverava prozivanjem (polling) na granicama delova pre nego preemptivno, tako da očekujte desetine milisekundi kašnjenja (latency) između pozivanja Cancel i stvarnog zaustavljanja renderovanja. To je i dalje daleko jeftinije nego da dozvolite da zastarelo renderovanje čitave stranice blokira red čekanja. Prosleđivanje vrednosti nil kao tokena renderuje direktno do kraja, što je pravi izbor za jednokratno renderovanje kao što je pregled pre štampanja gde nema ničega protiv čega bi se proces otkazao
Kada umesto toga pozovete funkcijski oblik metode RenderPage, onaj koji vraća novu TBitmap sliku, zapamtite da je pozivalac njen vlasnik i da je mora osloboditi pozivom Free. U petlji skrolovanja koja dodeljuje bitmapu po stranici, zaboravljanje ovoga je curenje (leak) koje raste sa svakom stranicom koju korisnik prođe, što je upravo pad sa neograničenom potrošnjom memorije koji je kontinualni dizajn trebalo da izbegne. Renderujte u ponovo korišćenu bitmapu kad god možete
Ono što vam preostaje
Čitač sa neprekidnim skrolovanjem je uglavnom posao same komponente. Vi birate dmSingleContinuous za izgled, podešavate pfmFitWidth kako bi se kolona refaktorisala sa prozorom, i proveravate Pdf.Active kako bi neispravna datoteka jasno prijavila grešku. Jedini deo koji vredi napisati samostalno jeste otkazivo renderovanje, jer se kvalitet čitača procenjuje na osnovu toga kako se ponaša kada neko povuče traku za skrolovanje do dna dugačkog dokumenta i panel to ili prati ili ne. Sve preko toga — selekcija teksta kroz stranice, isticanje rezultata pretrage, stablo obeleživača — jeste posao na korisničkom interfejsu koji sedi na vrhu ove površine skrolovanja, a ne unutar nje
API-ji TPdfView, DisplayMode i RenderPageProgressive prikazani ovde su deo PDFium komponente za Delphi i Lazarus