Jedna stranica formata A4 renderirana na ugodnoj razini zumiranja za čitanje zauzima nekoliko megabajta 32-bitne bitmape. Pomnožite to s ugovorom od 400 stranica i ta računica prestaje biti apstraktna: ako renderirate svaku stranicu unaprijed, od sustava Windows tražit ćete više od gigabajta bitmapa koje će korisnik pregledavati ekran po ekran. Aplikacija će ili ostati bez adresnog prostora u 32-bitnoj verziji ili će prvih nekoliko sekundi biti zamrznuta dok grafički procesor i parser obrađuju stranice do kojih korisnik još nije ni došao. Čitač s kontinuiranim pomicanjem (continuous scroll) mora odavati dojam jedne dugačke trake stranica, ali ne može sve njih držati u memoriji odjednom.
Ta je napetost ključni problem ovdje. PDFium VCL to rješava unutar komponente TPdfView, tako da se većina posla svodi na odabir ispravnog načina prikaza i razumijevanje onoga što komponenta radi u vaše ime. Ono što ona ne radi automatski (prilagodba veličine stranica za čitanje i očuvanje responzivnosti pri brzom pomicanju) zahtijeva malo vašeg koda. Ako još uvijek sastavljate popratne elemente (alatnu traku, minijature, okvir za pretraživanje), vodič za izradu bogatog preglednika pokriva taj dio; ovdje je tema samo pomicanje.
Izgled je način prikaza, a ne ploča s bitmapama
Instinkt pri radu s VCL formama navodi nas da upotrijebimo scroll box i u njega poslažemo kontrole slika, jednu po stranici. Oduprite se tome. Takav vas dizajn tjera da se sami bavite pozicioniranjem stranica, matematikom skrolanja i memorijskim pitanjima odjednom, a sve ćete to na kraju sami loše rekreirati. Komponenta TPdfView već modelira dokument kao neprekinuti 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 cijela konfiguracija za kontinuirano pomicanje. Vrijednost dmSingleContinuous raspoređuje stranice u jedan vertikalni stupac s razmacima koji se rješavaju interno, a preglednik skrola kroz taj stupac kao kroz jedinstvenu površinu. Nema potrebe za povezivanjem kontrola po stranici niti pisanja rukovatelja skrolanjem za uobičajenu navigaciju. Obratite pozornost na provjeru svojstva Pdf.Active nakon dodjele: otvaranje dokumenta nikada ne podiže iznimku, pa oštećena ili lozinkom zaštićena datoteka ostavlja Active na False bez ikakve iznimke koju biste mogli uhvatiti, a preglednik koji preskoči ovu provjeru prikazat će prazan prostor i izgledati neispravno.
Isto svojstvo podržava i dvostranični prikaz. Vrijednost dmTwoPageContinuous postavlja stranice jednu do druge, dvije u redu, za stil čitanja knjige koji neki dokumenti zahtijevaju; dmTwoPageContinuousWithCover radi isto, ali dopušta da prva stranica stoji samostalno kao naslovnica, tako da preostali parovi stranica padaju na prirodnu granicu parnih i neparnih brojeva. Sva tri načina rada omogućuju kontinuirano skrolanje. Prebacivanje između njih obavlja se jednostavnim dodjeljivanjem vrijednosti, što olakšava kasniju implementaciju padajućeg izbornika za način prikaza.
Samo se vidljive stranice rasteriziraju
Razlog zašto ovo funkcionira i na datoteci od 400 stranica jest taj što je stupac virtualan. Komponenta TPdfView zna visinu svake stranice iz stabla stranica dokumenta, pa može izračunati ukupni raspon pomicanja i položaj svake stranice bez rasterizacije. Rasterizacija, skupi korak koji pretvara tok sadržaja stranice u piksele, događa se samo za stranice koje trenutno sijeku vidljivo područje (viewport), uz malu marginu kako bi stranica bila spremna kada se dokotrlja u vidno polje. Kako se pomičete prema dolje, stranice koje ulaze u vidno polje se iscrtavaju, a stranicama koje izlaze oslobađa se memorija bitmapa. Korištenje memorije ostaje proporcionalno onome što stane na zaslon, a ne ukupnoj duljini dokumenta.
Ovo je važno usvojiti jer mijenja način na koji razmišljate o performansama. Otvaranje dokumenta od 400 stranica je brzo: analizira se struktura, a ne sadržaj. Trošak obrade nastaje po stranici i plaća se lijeno (lazily), tek u trenutku kada se stranica približi vidnom polju. Preglednik koji djeluje trenutačno pri otvaranju i glatko pri pomicanju ne radi manje posla ukupno, već raspoređuje rad duž stvarne putanje čitanja korisnika i odbacuje ono što ostane iza njega. Praktična je posljedica da gotovo nikada ne želite prisilno renderirati stranice unaprijed. Prepustite pregledniku da sam odluči što je vidljivo.
Prilagodite stranice širini, a zumiranje ostavite po strani
Stupac za čitanje zahtijeva da stranice budu prilagođene širini ploče, a ne fiksirane na apsolutno zumiranje. Svojstvo FitMode rješava to i nastavlja održavati prilagodbu pri promjeni veličine prozora.
PdfView.FitMode := pfmFitWidth; // each page fills the column width; height follows
Uz opciju pfmFitWidth komponenta ponovno izračunava zumiranje kad god se promijeni veličina preglednika, tako da stupac uvijek ispunjava dostupnu širinu, a visine stranica (pa time i raspon pomicanja) prate tu promjenu. Ovdje postoji jedna zamka: izravna dodjela vrijednosti svojstvu Zoom vraća FitMode natrag na pfmNone. To je namjerno, jer su ručno zumiranje i automatska prilagodba proturječne namjere, ali znači da nasumični PdfView.Zoom := 1.0 negdje u vašem kodu tiho isključuje prilagodbu širini i sljedeća promjena veličine prestaje reflowati. Ako nudite i kontrolu zumiranja i gumb za prilagodbu, tretirajte ih kao promjenu načina rada: postavljanje jednog poništava drugo, a vi odlučujete što ima prednost.
Za prirodne kontrole apsolutnog zumiranja, preglednik nudi vrijednosti zumiranja za prilagodbu koje možete primijeniti ili prikazati: PageWidthZoom[PageNumber] vraća razinu zumiranja koja bi prilagodila tu stranicu širini, dok odgovarajuće svojstvo PageZoom prilagođava cijelu stranicu. Čitanje tih vrijednosti omogućuje vam popunjavanje izbornika 'Prilagodi širini' / 'Prilagodi stranici' bez hardkodiranja fiksnih postotaka koji neće raditi na vodoravno okrenutim (landscape) ili prevelikim stranicama.
Održite responzivnost pri brzom pomicanju pomoću progresivnog renderiranja
Zadani tijek renderiranja iscrtava cijelu stranicu prije nego što završi i vrati kontrolu. Za jednu stranicu to je u redu. No pri brzom skrolanju kroz opsežan dokument to stvara probleme: svaka stranica koja proleti pokreće potpunu rasterizaciju, pa ako korisnik skrola brže nego što se stranice mogu iscrtati, renderi se gomilaju i sučelje trza jer se obrađuju stranice koje su u trenutku završetka posla već davno izvan zaslona. Rješenje je omogućiti otkazivanje renderiranja i prekidanje istog čim se korisnik pomakne dalje.
Metoda RenderPageProgressive renderira u dijelovima (chunks) i provjerava token za otkazivanje na svakoj granici dijela, pa se aktivno iscrtavanje stranice koja je upravo skrolana izvan vidnog polja može prekinuti umjesto da se izvodi 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;
Najvažniji dio je povratna vrijednost. Vrijednost prsDone znači da je bitmapa potpuno iscrtana i spremna za prikaz na zaslonu; prsCancelled znači da je novi položaj skrolanja nadvladao ovu stranicu, pa djelomični rezultat odbacujete umjesto da ga prikažete; prsFailed ukazuje na stvarnu pogrešku na toj stranici. Otkazivanje se provjerava periodički na granicama dijelova, a ne trenutno, pa očekujte kašnjenje od nekoliko desetaka milisekundi između poziva metode Cancel i stvarnog zaustavljanja renderiranja. To je i dalje neusporedivo brže i jeftinije nego da dopustite da zastarjelo renderiranje cijele stranice blokira red čekanja. Prosljeđivanje vrijednosti nil kao tokena renderira stranicu do kraja bez otkazivanja, što je pravi izbor za jednokratna renderiranja poput pregleda prije ispisa gdje nema potrebe za otkazivanjem.
Kada umjesto toga pozovete funkcijski oblik metode RenderPage, onaj koji vraća novu instancu TBitmap, zapamtite da je pozivatelj vlasnik tog objekta i mora ga osloboditi pozivom Free. Ako u petlji pomicanja dodjeljujete novu bitmapu po stranici, zaboravljanje ovog koraka uzrokovat će curenje memorije koje raste sa svakom stranicom koju korisnik prođe, što je upravo suprotno od cilja očuvanja memorije koji smo željeli postići. Renderirajte u ponovno korištenu (reused) bitmapu kad god je to moguće.
Što vam na kraju preostaje
Čitač s kontinuiranim pomicanjem uglavnom se oslanja na ono što komponenta nudi. Odabirete dmSingleContinuous za raspored, postavljate pfmFitWidth kako bi se stupac prilagođavao prozoru i provjeravate Pdf.Active kako bi neispravna datoteka odmah prijavila pogrešku. Jedini dio koji se isplati samostalno napisati jest renderiranje s mogućnošću otkazivanja, jer se čitač procjenjuje po tome kako se ponaša kada netko povuče klizač na dno dugog dokumenta, a sučelje to uspije pratiti ili zaostane. Sve nakon toga (označavanje teksta kroz stranice, isticanje pretrage, stablo oznaka) predstavlja rad na sučelju koji se nadograđuje na ovu površinu, a ne unutar nje.
API-jevi TPdfView, DisplayMode i RenderPageProgressive prikazani ovdje dio su komponente PDFium VCL za Delphi i Lazarus.