Jedna A4 stranica iscrtana na ugodnom zumu za čitanje zauzima nekoliko megabajta 32-bitne bitmape. Pomnožite to s ugovorom od 400 stranica i ta aritmetika prestaje biti apstraktna: ako iscrtate svaku stranicu unaprijed, tražite od Windowsa više od gigabajta bitmapa koje će korisnik gledati samo jedan po jedan zaslon. Aplikacija ili ostaje bez adresnog prostora u 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 došao. Čitač s neprekidnim pomicanjem (continuous scrolling) mora se osjećati kao jedna duga vrpca stranica, ali ne može zapravo sve njih držati u memoriji odjednom
Ta napetost je ovdje cijeli problem. PDFium komponenta ga rješava unutar TPdfView, pa je većina posla odabir ispravnog načina prikaza (display mode) i razumijevanje onoga što komponenta radi u vaše ime. Dijelovi koje ne radi za vas, poput određivanja veličine stranica za tijek čitanja i održavanja responzivnosti brzog pomicanja, mjesto su gdje malo koda zarađuje svoju plaću. Ako još uvijek sastavljate okolne elemente (alatnu traku, minijature, okvir za pretraživanje), vodič kroz bogat preglednik pokriva to područje; ovdje je tema samo pomicanje
Izgled (layout) je način prikaza, a ne ploča bitmapa
Instinkt kod rada s VCL obrascima je posegnuti za scroll boxom i slagati kontrole slika unutar njega, jednu po stranici. Oduprite se tome. Taj dizajn vas tjera da sami upravljate pozicioniranjem stranica, matematikom pomicanja i pitanjem memorije odjednom, i sve ćete to ponovno osmisliti na loš način. TPdfView već modelira dokument kao neprekinuti niz stranica i izlaže izgled 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 postavka neprekidnog pomicanja. dmSingleContinuous raspoređuje stranice u jedan okomiti stupac s razmacima među njima koji se rješavaju interno, a prikaz se pomiče kroz taj stupac kao kroz jednu površinu. Nema potrebe za povezivanjem kontrola po stranici niti za pisanjem rukovatelja pomicanjem za običnu navigaciju. Primijetite provjeru Pdf.Active nakon dodjele: otvaranje dokumenta nikada ne izaziva iznimku, pa oštećena datoteka ili datoteka zaštićena lozinkom ostavlja Active na False bez iznimke koju bi trebalo uhvatiti, a preglednik koji preskoči ovu provjeru prikazuje praznu ploču i krivi samog sebe
Isto svojstvo nosi i načine dvostrukog prikaza (spread modes). dmTwoPageContinuous postavlja stranice jednu pored druge, dvije u retku, za čitanje u stilu knjige koje neki dokumenti zahtijevaju; dmTwoPageContinuousWithCover čini isto, ali dopušta da prva stranica stoji samostalno kao naslovnica kako bi preostali parovi pali na prirodnu parno-neparnu granicu. Sva tri načina pomiču se neprekidno. Prebacivanje između njih je jednostavna dodjela, što čini dodavanje padajućeg izbornika (combo box) za odabir načina prikaza trivijalnim
Samo se vidljive stranice rasteriziraju
Razlog zašto se ovo može skalirati na datoteku od 400 stranica je taj što je stupac virtualan. TPdfView zna visinu svake stranice iz stabla stranica dokumenta, pa može izračunati ukupni opseg pomicanja i položaj svake stranice bez rasterizacije ičega. Rasterizacija, skupi korak koji pretvara struju sadržaja stranice u piksele, događa se samo za stranice koje trenutno sijeku vidno polje (viewport), plus mala margina kako bi stranica bila spremna do trenutka kada se urola u vidno polje. Kako se pomičete prema dolje, stranice koje ulaze u vidno polje se iscrtavaju, a onima koje ga napuštaju oslobađaju se njihove bitmape. Memorija ostaje proporcionalna onome što stane na zaslon, a ne duljini dokumenta
Ovo vrijedi usvojiti jer mijenja način na koji razmišljate o trošku performansi. Otvaranje dokumenta od 400 stranica je jeftino: ono raščlanjuje strukturu, a ne sadržaj. Trošak se plaća po stranici i to lijeno (lazily), u trenutku kada se stranica približi pomicanjem. Preglednik koji se čini trenutačnim pri otvaranju i glatkim pri pomicanju ne radi manje posla ukupno, već raspoređuje posao duž stvarne korisnikove staze čitanja i odbacuje ono što ostaje iza. Praktična posljedica je da gotovo nikada ne želite prisilno iscrtavati stranice ispred korisnika. Pustite prikaz (view) da odluči što je vidljivo
Prilagodite stranice širini, a zatim ostavite zumiranje na miru
Stupac za čitanje želi da stranice budu prilagođene širini ploče, a ne prikovane za apsolutni zum. Svojstvo FitMode to radi i nastavlja raditi kako se prozor mijenja
PdfView.FitMode := pfmFitWidth; // each page fills the column width; height follows
S pfmFitWidth komponenta ponovno izračunava zum kad god se prikaz promijeni, tako da stupac uvijek ispunjava dostupnu širinu, a visine stranica, pa stoga i opseg pomicanja, slijede iz toga. Postoji jedna zamka u koju ljudi upadaju: izravno dodjeljivanje vrijednosti svojstvu Zoom vraća FitMode na pfmNone. To je namjerno, jer su ručni zum i automatsko prilagođavanje kontradiktorne namjere, ali to znači da zalutali kôd PdfView.Zoom := 1.0 negdje u vašem programu tiho isključuje prilagođavanje širini i sljedeća promjena veličine prestaje reflowati izgled. Ako nudite i kontrolu zuma i gumb za prilagođavanje, tretirajte ih kao prebacivanje načina rada: postavljanje jednog briše drugi, a vi odlučujete koji pobjeđuje
Za apsolutne kontrole zuma koje se čitaju prirodno, prikaz izlaže zumove prilagođavanja kao vrijednosti koje možete primijeniti ili prikazati: PageWidthZoom[PageNumber] vraća zum koji bi prilagodio tu stranicu širini, a odgovarajući PageZoom prilagođava cijelu stranicu. Čitanje tih vrijednosti je način na koji popunjavate izbornik 'Prilagodi širini' / 'Prilagodi stranici' bez tvrdog kodiranja čarobnih postotaka koji ispadaju pogrešni na položenim (landscape) ili prevelikim stranicama
Održavajte brzo pomicanje responzivnim pomoću progresivnog iscrtavanja
Zadani put iscrtavanja crta stranicu do kraja prije nego što se vrati. Za jednu stranicu to je u redu. Tijekom brzog listanja kroz gust dokument nije: svaka stranica koja bljesne pokreće punu rasterizaciju, a ako korisnik lista brže nego što se stranice mogu iscrtati, ta se iscrtavanja gomilaju i ploča trza jer se radi posao za stranice koje su već izvan zaslona do trenutka kada završi. Rješenje je učiniti iscrtavanje otkazivim i napustiti ga onog trenutka kada korisnik krene dalje
Metoda RenderPageProgressive iscrtava u dijelovima (chunks) i provjerava token za otkazivanje na svakoj granici dijela, tako da se iscrtavanje u tijeku za stranicu koja je upravo odlistana može odbaciti 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;
Oblik koji je važan je povratna vrijednost. prsDone znači da je bitmapa u potpunosti oslikana i spremna za prikaz na zaslonu; prsCancelled znači da je noviji položaj pomicanja nadomjestio ovu stranicu, pa djelomični rezultat bacate umjesto da ga prikažete; prsFailed je stvarna pogreška na toj stranici. Otkazivanje se provjerava periodički na granicama dijelova, a ne preemptivno, stoga očekujte desetke milisekundi latencije između poziva Cancel i stvarnog zaustavljanja iscrtavanja. To je i dalje daleko jeftinije nego dopustiti da zastarjelo iscrtavanje cijele stranice blokira red čekanja. Prosljeđivanje nil-a kao tokena iscrtava ravno do kraja, što je ispravan izbor za jednokratno iscrtavanje poput pretpremijere ispisa gdje nema potrebe za otkazivanjem
Kada umjesto toga pozovete funkcijski oblik RenderPage, onaj koji vraća novu TBitmap, zapamtite da je pozivatelj vlasnik te bitmape i mora je osloboditi (Free). U petlji pomicanja koja dodjeljuje bitmapu po stranici, zaboravljanje ovoga je curenje (leak) koje raste sa svakom stranicom koju korisnik prođe, što je upravo onaj kvar neograničene memorije koji je dizajn neprekidnog pomicanja trebao izbjeći. Iscrtavajte u ponovno korištenu bitmapu gdje god možete
Što vam na kraju ostaje
Čitač s neprekidnim pomicanjem uglavnom isporučuje sama komponenta. Odabirete dmSingleContinuous za izgled, postavljate pfmFitWidth tako da se stupac reflowa s prozorom i provjeravate Pdf.Active kako bi neispravna datoteka glasno zakazala. Jedini dio koji vrijedi sami napisati je otkazivo iscrtavanje, ove se o čitaču sudi po tome kako se ponaša kada netko povuče traku za pomicanje na dno dugog dokumenta i ploča to prati ili ne prati. Sve osim toga, selekcija teksta na više stranica, isticanje pretrage, stablo oznaka (bookmarks), je rad na sučelju koji se nalazi na vrhu ove površine za pomicanje, a ne unutar nje
API-ji TPdfView, DisplayMode i RenderPageProgressive prikazani ovdje dio su PDFium komponente za Delphi i Lazarus