Technical Article

Pregledovalnik PDFium v Delphiju: Pomnilnik izrisa (Render Cache) in taktike za gladko približevanje (Zoom)

Pridržite gumb za približevanje v preprostem bralniku PDF in opazujte grafikon procesorja. En sam pritisk gumba za samodejno ponavljanje približevanja sproži ducat or več korakov povečave v sekundi. Če vsak korak začne celovit izris vidne strani v polni kakovosti, se izrisi kopičijo hitreje, kot se dokončajo. Stran se posamično sicer hitro rasterizira, morda v 180 ms za skeniran A4 list, vendar sedaj izvajate ducat 180 ms izrisov za stanje, ki ga je uporabnik že premaknil naprej. Pregledovalnik zamrzne, eno procesorsko jedro se obremeni na 100 % in ko se zaslon končno posodobi, se uporabnik ustavi na stopnji povečave, ki je bila aktualna štiri izrise nazaj. Rešitev ni hitrejši rasterizator, temveč pomnilnik (cache), ki takoj vrne že dokončane strani, in zanka izrisa, ki je pripravljena opustiti delo v trenutku, ko postane zastarelo.

PDFium Component vam ponuja gradnike za oboje, odločanje o pravilih delovanja pa prepušča vam. Prejmete bitne slike v lasti klicatelja, progresivni izrisovalnik, ki sprejme žeton za preklic, načine prilagajanja, ki ponovno izračunajo zoom ob spremembi velikosti, in klic za deljenje na ploščice (tiling) za strani, ki so prevelike za celotno rasterizacijo. Knjižnica pa namerno ne ponuja samega pomnilnika (cache), saj je prava politika odstranjevanja iz pomnilnika odvisna od vašega vidnega polja (viewport), pomnilniške omejitve platforme in načina, kako vaši uporabniki drsijo skozi dokument. Ta odločitev je vaša, posledice napačne odločitve pa sta ravno zamrznitev sistema in uhajanje pomnilnika.

Kam izginjajo milisekunde in megabajti

Pred načrtovanjem si ponazorite stroške s številkami. Stran A4 pri 96 DPI meri približno 794 krat 1123 slikovnih pik, kar predstavlja približno 3,5 MB kot 32-bitna bitna slika. Povečajte na 200 % in ta vrednost se štirikrat poveča. Pri 400 % povečavi na zaslonu z visoko gostoto pik (high-DPI) alocirate in polnite bitno sliko ene same strani z velikostjo 50 do 60 MB, pregledovalnik z neprekinjenim drsenjem pa ohranja več strani hkrati aktivnih. Strošek rasterizacije sledi izhodnim slikovnim pikam, zato vsaka podvojitev povečave približno štirikrat poveča tako čas izrisa kot porabo pomnilnika.

Iz tega izhajata dve neposredni posledici. Pomnilnik, katerega ključ ne upošteva stopnje povečave, je neuporaben, saj prav dejanje približevanja (zooming), ki ga želite pospešiti, vsakič ustvari novo bitno sliko. Poleg tega bo neomejen pomnilnik porabil ves naslovni prostor 32-bitnega procesa ravno pri dokumentih, kjer uporabniki najbolj približujejo: pri podrobnih skeniranih listinah, inženirskih načrtih ali zemljevidih velikih formatov. Pomnilnik mora imeti pravilno določene ključe in strogo omejeno velikost, pri čemer nobena od teh zahtev ni izbirna.

Kaj spada v ključ pomnilnika

Shranjeno bitno sliko je varno ponovno uporabiti le, če se še vedno ujemajo vsi vhodni podatki, ki so oblikovali njene slikovne pike. To pomeni številko strani, dejansko povečavo (oziroma izhodne dimenzije v slikovnih pikah), zasuk (rotation), DPI monitorja in možnosti izrisa, ki so bile v veljavi ob nastanku slike. Stran, izrisana z možnostjo reAnnotations, se razlikuje od iste strani brez njih, prav tako pa se razlikuje sivinski prehod z možnostjo reGrayscale. Če katero koli od teh postavk izpustite iz ključa, so napake predvidljive: prekrivna plast z anotacijami lahko ostane na zaslonu tudi po tem, ko je ocenjevalec izbrisal komentar, ali pa stran postane zamegljena v trenutku, ko uporabnik povleče okno z zaslona prenosnika na zunanji 4K monitor in se DPI spremeni pod zastarelo bitno sliko.

function TPageCache.Acquire(Pdf: TPdf; PageNo: Integer; ZoomPct: Single;
  Rotation: TRotation; Opts: TRenderOptions): TBitmap;
var
  Key: string;
begin
  Key := Format('%d|%.0f|%d|%d|%d',
    [PageNo, ZoomPct, Ord(Rotation), Screen.PixelsPerInch, OptionsMask(Opts)]);
  if FBitmaps.TryGetValue(Key, Result) then
    Exit;

  Pdf.PageNumber := PageNo;
  Result := Pdf.RenderPage(0, 0, OutputWidth(PageNo, ZoomPct),
    OutputHeight(PageNo, ZoomPct), Rotation, Opts);
  FBitmaps.Add(Key, Result);   // the cache now owns this bitmap
end;

Ob zadetku se funkcija vrne in v nekaj mikrosekundah, kar je bistvo. Težje vprašanje pa je, kaj se zgodi z bitnimi slikami, ki so izločene iz pomnilnika, kar se izkaže za vprašanje o njihovem lastništvu.

Kdo sprosti bitno sliko

Funkcijska oblika metode RenderPage vrne instanco TBitmap, katere lastnik je klicatelj. Pri enkratnem izvozu je to lastništvo očitno in ga je enostavno spoštovati. Znotraj pomnilnika pa to postane najpogostejši razlog za uhajanje pomnilnika v pregledovalnikih PDF v Delphiju, saj slovar (dictionary) sedaj drži edino referenco do vsake bitne slike, običajni TDictionary pa ključe in vrednosti sprosti le, če gre za upravljane (managed) tipe. TBitmap to ni. Če odstranite vnos iz pomnilnika brez klica metode Free, slikovne pike ostanejo alocirane, ne da bi nanje karkoli kazalo.

Razlog, da se ta napaka izmuzne preverjanju, je čas izvajanja. Desetminutni hitri preizkus nikoli ne približa toliko različnih strani, da bi to opazili. Uhajanje pomnilnika se pokaže šele, ko nekdo nekaj ur drsi skozi dolg dokument in ga približuje. Takrat proces zadržuje stotine osirotelih bitnih slik strani, računalnik pa začne uporabljati navidezni pomnilnik (paging). Zato mora biti izločanje del prve različice pomnilnika in ne poznejših izboljšav. Omejite pomnilnik na podlagi ocene bajtov (izračunano kot širina krat višina krat štiri), izločite najmanj uporabljene strani (LRU), ki so zunaj vidnega polja in območja predpomnjenja, ter sprostite vsako bitno sliko ob njenem odstranjevanju. Pri izrisih, ki so resnično prehodne narave, vam preobložene metode, ki izrišejo neposredno v vnaprej pripravljen TBitmap ali na HDC, omogočajo, da se v celoti izognete upravljanju lastništva. Predogled tiskanja je očiten primer, saj vsak list izrišete le enkrat in shranjevanje v pomnilnik ne prinaša koristi.

Progresivni izris in zanesljiv preklic

Navadne preobložene metode RenderPage blokirajo izvajanje, dokler se izris strani ne konča, kar pa je natanko tisto, česar ne želite, medtem ko uporabnik še vedno premika drsnik za povečavo. V ta namen uporabite RenderPageProgressive. Ta sprejme vmesnik IPdfCancellationToken in vrne eno od stanj: prsDone, prsCancelled ali prsFailed. Podrobnost, ki pogosto preseneti razvijalce, je, da preklic ni trenuten. Stanje žetona se preverja na mejah blokov znotraj izrisa, zato žeton, ki ga sprožite sredi bloka, začne veljati šele, ko se ta del izrisa konča. Na kompleksni strani lahko zakasnitev med zahtevo in zaustavitvijo traja več deset milisekund. Oblikujte kodo okoli te časovne vrzeli: prekličite prejšnji žeton takoj, ko pride nova vrednost povečave, vendar ne predvidevajte, da se bo prejšnji izris ustavil v istem trenutku.

procedure TViewerForm.RequestRender(TargetZoom: Single);
var
  Status: TPdfProgressiveStatus;
begin
  if FTokenSource <> nil then
    FTokenSource.Cancel;           // abandon the previous in-flight render
  FTokenSource := TPdfCancellationTokenSource.New;  // FPdfAsync unit

  Status := Pdf.RenderPageProgressive(FBackBuffer, 0, 0,
    FBackBuffer.Width, FBackBuffer.Height, FTokenSource.Token,
    ro0, [reAnnotations]);

  case Status of
    prsDone:      PresentBackBuffer;
    prsCancelled: ;                // superseded by a newer request: drop silently
    prsFailed:    ShowRenderFailure;
  end;
end;

Med interakcijo je rezultat prsCancelled običajen pojav in ne izjema. Večina izrisov, ki jih sproži približevanje, bo nadomeščenih, preden se zaključijo, zato obravnavajte preklic ako rutinski dogodek in rezultat tiho zavrzite. Čakalna vrsta izrisov, ki vsak preklic zabeleži kot opozorilo, bo tisto eno napako, ki je dejansko pomembna, pokopala pod tisoči vrstic šuma. Da zaslon ne bi deloval neodzivno, medtem ko poteka dejanski izris, povežite progresivno pot s preprosto zamenjavo: raztegnite prejšnjo shranjeno bitno sliko na novo povečavo in jo takoj prikažite. Slika bo za sto ali dvesto milisekund videti mehka, vendar bo delovala takojšnje ter pridobila čas, ki ga kakovosten izris potrebuje, da se dokonča ali pa ga prekliče naslednji gib.

Način prilagajanja, ki ga zoom tiho izklopi

Lastnost pregledovalnika FitMode, nastavljena na pfmFitPage ali pfmFitWidth, ob vsaki spremembi velikosti okna preračuna povečavo, da se stran še naprej prilagaja oknu. Težava pa je v tem, da neposredna dodelitev vrednosti lastnosti Zoom ponastavi FitMode nazaj na pfmNone. Kot privzeto delovanje je to pravilno: uporabnik, ki je namerno vpisal 150 %, ne želi, da naslednja sprememba velikosti okna to ponastavi. Vendar pa to preseneti vsakogar, ki gumb za približevanje poveže kot Zoom := Zoom * 1.25 in nato ne more ugotoviti, zakaj se prilagajanje širini po prvem kliku ne odziva več. Če vaša orodna vrstica ponuja tako eksplicitno povečavo kot načine prilagajanja, si morate zadnjo uporabnikovo izbiro prilagajanja zapomniti sami in jo ponovno dodeliti, ko znova pritisnejo gumb za prilagajanje. Komponenta ne bo obnovila načina, ki ga je dodelitev povečave pravkar počistila, in to se od nje tudi ne pričakuje.

Pomnilniški proračun, ki ga lahko zagovarjate

Proračun, ki ga lahko zapišete, je proračun, ki ga lahko zagovarjate pri pregledu kode. Začnite s konkretnim scenarijem. Recimo, da neprekinjeno drsenje ohranja vidno stran ter po eno vnaprej naloženo stran zgoraj in spodaj, poleg tega pa še trak sličic. Pri 100 % povečavi na zaslonu s 96 DPI te tri bitne slike v polni velikosti nanesejo približno 3,5 MB vsaka, kar je zanemarljivo. Pri 300 % povečavi na 4K zaslonu pa so iste tri bitne slike velike približno 30 MB vsaka, še preden je pomnilnik shranil kakšno zgodovinsko stran. Rast je v dejanju uporabnika in ne v samem dokumentu.

Primerna privzeta vrednost za 32-bitni proces v Delphiju je proračun 256 MB za bitne slike ob uporabi LRU izločanja. V 64-bitnem okolju lahko to prilagodite količini fizičnega pomnilnika RAM, vendar kljub temu ohranite trdo omejitev. Težava, pred katero se varujete, namreč ni le sesutje vašega procesa, temveč to, da celoten sistem začne pretirano uporabljati izmenjevalno datoteko (thrashing), medtem ko vaš pregledovalnik tehnično še naprej deluje, uporabnik pa se sprašuje, zakaj se je vse ostalo upočasnilo. Trda omejitev odpove predvidljivo, neomejen pomnilnik pa odpove tako, da ohromi celotno namizje. Sličice (thumbnails) si zaslužijo svojo obravnavo: vsako izrišite le enkrat v njeni majhni ciljni velikosti in jo hranite v ločenem naboru, ki se ga LRU logika nikoli ne dotakne. Ponovno ustvarjanje 120-pikselne sličice z zmanjševanjem 60 MB bitne slike celotne strani je najbolj potraten možen način za izdelavo miniature.

Nekatere posamezne strani presežejo vsak proračun. Inženirski načrt velikosti E ali velik zemljevid, izrisan v celoti pri 400 %, pomeni alokacijo več sto megabajtov in nobena politika izločanja ne more narediti tega sprejemljivega. Rešitev v tem primeru je, da prenehate z izrisovanjem celotnih strani. Metoda RenderTile rasterizira le območje na odmiku slikovnih pik (Left, Top) znotraj strani, ki je navidezno prilagojena na velikost PageWidth krat PageHeight. Tako izrišete le vidni pravokotnik in rob ene ploščice okoli njega za gladko pomikanje, odmike ploščic pa poleg povečave vključite v ključ pomnilnika. Dimenzije ploščic naj bodo fiksne za celoten dokument. Fiksne ploščice pomenijo, da sprememba DPI čisto neveljavno označi celotno mrežo, medtem ko spremenljive ploščice povzročijo vidne šive med območji, izrisanimi pri rahlo različnih merilih.

K vsemu temu potihem prispevata še dve sorodni funkciji. Prehodi barvnih filtrov, kot sta pretvorba v sivo lestvico ali inverzija, se izvedejo po izrisu in vsakič ustvarijo drugo bitno sliko polne velikosti, s čimer podvojijo porabo pomnilnika na stran za vsak pogled, ki jih uporablja; ta strošek je tema članka o barvnih filtrih za slabovidne v pregledovalnikih PDF v Delphiju. Pregledovalnik, ki med pretvorbo besedila v govor označuje besede, pa razveljavi izrisani pogled ob vsaki izgovorjeni besedi, zato je interakcija med označevanjem besed in hitrostjo govora pomembnejša, kot se zdi na prvi pogled, kar obravnava članek o označevanju besed pri pretvorbi besedila v govor.

Preobložitve izrisa, progresivne statusne kode in sama komponenta pregledovalnika so dokumentirani na predstavitveni strani izdelka PDFium Component.