Tehnički članak

Preglednik PDF-a s neprekidnim pomicanjem (continuous scrolling) u Delphiju pomoću PDFium komponente

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