Tehnični članak

Pregledovalnik PDF s continuous drsenjem v Delphiju s PDFium Component

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 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 Component 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 dobesedno ž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 je 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 in krivdo prevzame nase

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 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, se zgodi le za strani, ki trenutno presekajo vidno polje, 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, 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 posledica je, 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;   // vsaka stran zapolni širino stolpca; višina sledi

Pri načinu pfmFitWidth komponenta ponovno izračuna povečavo ob vsaki spremembi velikosti pogleda, tako da stolpec vedno zapolni razpoložljivo širino, višine strani (in s tem obseg drsenja) pa sledijo temu. Tukaj 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 to pomeni, da nekje v vaši kodi pozabljen klic PdfView.Zoom := 1.0 tiho izklopi prilagajanje širini in naslednja sprememba velikosti ne bo več reflowala. Če ponujate tako nadzor povečave kot gumb za prilagoditev, to obravnavajte kot preklop načina: nastavitev enega počisti drugega in vi se odločite, kateri prevlada

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

Ohranjajte odzivnost hitrega drsenja s postopnim upodabljanjem

Privzeta pot upodabljanja izriše stran do konca, preden se vrne. 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 te zahteve kopičijo in plošča stuka, saj se dela za strani, ki so v trenutku dokončanja že zunaj zaslona. Rešitev je, da upodabljanje naredimo preklicljivo in ga opustimo v trenutku, ko se uporabnik premakne naprej

Metoda RenderPageProgressive upodablja v kosih in preverja žeton za preklic na vsaki meji kosa, tako da se lahko trenutno izvajanje upodabljanja strani, ki je pravkar odzdrsnila stran, opusti, namesto da bi se izvedlo 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
  // Prekliči vse, kar se je upodabljalo; stari žeton je zdaj signaliziran.
  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:      ;                    // bitna slika je dokončana, nariši jo
    prsCancelled: Exit;                // nadomeščeno, zavrzi ta rezultat
    prsFailed:    ShowMessage('Upodabljanje ni uspelo za stran ' + IntToStr(PageNo));
  end;
end;

Ključni del je povratna vrednost. prsDone pomeni, da je bitna slika v celoti naslikana in jo je vredno prenesti na zaslon; prsCancelled pomeni, da je nov položaj drsenja nadomestil to stran, zato delni rezultat raje zavržete, kot pa prikažete; prsFailed pa pomeni dejansko napako na tej strani. Preklic se preverja na mejah kosov in ne vnaprej, zato pričakujte nekaj deset milisekund zakasnitve med klicem Cancel in dejansko ustavitvijo upodabljanja. Posredovanje vrednosti nil kot žetona upodablja neposredno do konca, kar je prava izbira za enkratna upodabljanja, kot je predogled tiskanja, kjer ni ničesar za preklicati

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 drsni zanki, ki dodeli bitno sliko za vsako stran, je pozabljanje tega uhajanje pomnilnika (leak), ki raste z vsako stranjo, ki jo uporabnik preide, kar je natanko tista težava z neomejenim pomnilnikom, ki bi se ji z neprekinjenim drsenjem morali izogniti. Kjer je mogoče, upodabljajte v ponovno uporabljeno bitno sliko

Kaj vam ostane

Pregledovalnik z neprekinjenim drsenjem je večinoma naloga, ki jo opravi komponenta sama. Izberete dmSingleContinuous za postavitev, nastavite pfmFitWidth, da se stolpec prilagaja oknu, in preverite Pdf.Active, da poškodovana datoteka jasno spodleti. Edini del, ki ga je vredno napisati samostojno, je upodabljanje z možnostjo preklica, saj se bralnik ocenjuje po tem, kako se obnaša, ko nekdo povleče drsni trak na dno dolgega dokumenta in plošča temu sledi ali pa ne. Vse ostalo — izbira besedila med stranmi, označevanje iskanja, drevo zaznamkov — je delo na vmesniku, ki sedi na vrhu te drsne površine in ne zunaj nje

API-ji TPdfView, DisplayMode in RenderPageProgressive, prikazani tukaj, so del paketa PDFium Component za Delphi in Lazarus