Műszaki cikk

PDFium Delphi megjelenítő: Renderelési gyorsítótár és nagyítás

Tartsd lenyomva a zoom gombot egy naiv PDF-nézegetőben, és figyeld a CPU-grafikont. Egy automatikusan ismétlődő zoom-vezérlő egyetlen megnyomása másodpercenként tucatnyi vagy több zoom-lépést tüzel el, és ha minden lépés a látható oldal teljes minőségű újrarenderelését indítja el, a renderelések gyorsabban halmozódnak fel, mint ahogy befejeződnek. Az oldal önmagában szépen raszterizálódik, talán 180 ms alatt egy A4-es szkennelt oldalnál, de most tucatnyi 180 ms-os renderelést futtatsz olyan munkára, amin a felhasználó már túllépett. A nézegető lefagy, egy mag 100%-on pörög, és mire a képernyő utoléri magát, a felhasználó egy olyan zoomszinten állt meg, ami négy renderelésnyivel korábbi. A gyógymód nem egy gyorsabb raszterizáló. Egy gyorsítótár, amely azonnal visszaadja a kész oldalakat, és egy render-ciklus, amely hajlandó elhagyni a munkát abban a pillanatban, amikor az elavulttá válik

A PDFium Component mindkettőhöz odaadja neked a részeket, és kimarad a szabályzatból. Kapsz hívó-tulajdonolta bitképeket, egy progresszív renderelőt, amely megszakítási tokent fogad, illesztési módokat, amelyek átméretezéskor újraszámolják a zoomot, és egy csempézési hívást azokhoz az oldalakhoz, amelyek túl nagyok ahhoz, hogy egészben raszterizálódjanak. Amit szándékosan nem biztosít, az maga a gyorsítótár, mert a helyes kiürítési szabályzat a viewporttól, a platformod memóriakorlátjától és attól függ, hogyan görgetnek a felhasználóid. Ez a döntés a tiéd, amit helyesen kell meghoznod, és a rossz döntés következménye pontosan a lefagyás és a szivárgás

Hova mennek az ezredmásodpercek és a megabájtok

Tégy számokat a költségre, mielőtt bármit is megterveznél. Egy A4-es oldal 96 DPI-n nagyjából 794 szor 1123 pixel, körülbelül 3,5 MB egy 32 bites bitképként. Zoomolj 200%-ra, és ez megnégyszereződik. 400%-on egy nagy DPI-jű kijelzőn egyetlen oldal-bitképet foglalsz le és töltesz fel, amely 50-60 MB, és egy folyamatos görgetésű nézegető egyszerre több oldalt tart élőben. A raszterizálás költsége a kimeneti pixeleket követi, így a zoom minden duplázása nagyjából megnégyszerezi a renderelési időt és a memóriát is együtt

Két következmény adódik egyenesen ebből az aritmetikából. Egy gyorsítótár, amelynek kulcsa figyelmen kívül hagyja a zoomszintet, értéktelen, mert épp az a gesztus, amit fel kellene gyorsítania, a zoomolás, minden alkalommal új bitképet állít elő. És egy korlátlan gyorsítótár pontosan azokon a dokumentumokon futtatja ki a 32 bites folyamatot a címtérből, ahol az emberek a legkeményebben zoomolnak: sűrű tulajdonlapi szkennek, mérnöki rajzok, nagyformátumú térképek. A gyorsítótárat helyesen kell kulcsolni, és szilárdan kell korlátozni, és egyik sem opcionális

Mi tartozik a gyorsítótár-kulcsba

Egy gyorsítótárazott bitkép csak akkor biztonságos újrafelhasználásra, amikor minden bemenet, amely a pixeleit alakította, még mindig egyezik. Ez azt jelenti: az oldalszám, a tényleges zoom (vagy azzal egyenértékűen a kimeneti pixelméretek), a forgatás, a monitor DPI-je és a render-opciók, amelyek érvényben voltak, amikor előállították. Egy reAnnotations-szal renderelt oldal más kép, mint ugyanaz az oldal jegyzetek nélkül, és egy szürkeárnyalatos átfutás a reGrayscale-en keresztül megint más. Hagyj el bármelyiket ezek közül a kulcsból, és a hibák kiszámíthatók: egy jegyzet-overlay, amely tovább él, miután egy átnéző törli a megjegyzést, vagy egy oldal, amely elhomályosodik abban a pillanatban, amikor egy felhasználó áthúzza az ablakot egy laptop kijelzőről egy külső 4K-monitorra, és a DPI megváltozik egy elavult bitkép alatt

PDFium renderelési gyorsítótárkeresés Delphi megjelenítőben: a gyorsítótárkulcs oldalt, zoomot, forgatást, monitor DPIt és render opciókat fűz össze, találat mikroszekundumokban adja a bitképet, a kiselejtezés pedig felszabadít minden eldobott bitképet
A gyorsítótárkulcs lefedi minden, a pixeleket formáló bemenetet, és a kilövés felszabadítja az eldobott bittérképeket
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);   // a gyorsítótár mostantól birtokolja ezt a bitképet
end;

Egy találatnál ez mikroszekundumok alatt visszatér, ami az egész lényeg. A nehezebb kérdés az, mi történik azokkal a bitképekkel, amelyek kiesnek a gyorsítótárból, és ez kiderül, hogy egy tulajdonjogi kérdés

Ki szabadítja fel a bitképet

A RenderPage függvényformája egy TBitmap-et ad vissza, amelyet a hívó birtokol. Egy egyszeri exportnál ez a tulajdonjog nyilvánvaló és könnyen betartható. Egy gyorsítótáron belül ez lesz a Delphi PDF-nézegetők leggyakoribb szivárgása, mert a dictionary most már az egyetlen hivatkozást tartja minden bitképre, és egy sima TDictionary csak akkor szabadítja fel neked a kulcsokat és értékeket, ha azok kezelt típusok. Egy TBitmap nem az. Ürítsd ki a bejegyzést a Free hívása nélkül, és a pixelek lefoglalva maradnak úgy, hogy semmi sem mutat rájuk

Az ok, amiért ez átcsúszik, az időzítés. Egy tízperces füst-teszt sosem zoomol elég különálló oldalt ahhoz, hogy észrevegye; a szivárgás csak azután mutatkozik meg, hogy valaki néhány órán át görgetett és zoomolt egy hosszú dokumentumot, és ekkor a folyamat már több száz árva oldal-bitképet tart, és a gép elkezd lapozni. Ezért tartozik a kiürítés a gyorsítótár első verziójába, nem egy későbbibe. Korlátozd a gyorsítótárat becsült bájtok szerint, amit szélesség szorozva magassággal szorozva néggyel számolsz ki, ürítsd ki a legrégebben használt oldalakat, amelyek a viewporton és az előtöltési ablakon kívül ülnek, és szabadíts fel minden bitképet, amint eltávolítod. Az igazán átmeneti rajzolásokhoz a túlterhelések, amelyek egy hívó által megadott TBitmap-be vagy közvetlenül egy HDC-re renderelnek, teljesen lehetővé teszik a tulajdonjogi tánc kihagyását. A nyomtatási előnézet a nyilvánvaló eset, mert minden lapot egyszer renderelsz, és a gyorsítótárazása semmit sem hoz

Progresszív renderelés és becsületes megszakítás

A sima RenderPage túlterhelések blokkolnak, amíg az oldal be nem fejeződik, ami pontosan az a viselkedés, amit nem akarsz, amíg a felhasználó még mozgatja a zoom-vezérlőt. Ehhez a RenderPageProgressive-hoz nyúlsz. Egy IPdfCancellationToken-t fogad, és a prsDone, a prsCancelled vagy a prsFailed egyikét adja vissza. A viselkedési részlet, amiben az emberek elakadnak, az, hogy a megszakítás nem azonnali. A tokent a renderelésen belüli darabhatárokon kérdezik le, így egy token, amit egy darab közepén jelzel, csak akkor lép hatályba, amikor az a darab befejeződik. Egy összetett oldalon a kérés és a leállás közötti késleltetés tíz ezredmásodpercnyi tartományba fut. Tervezz erre a résre, ahelyett hogy elkívánnád: szakítsd meg az előző tokent abban a pillanatban, amikor egy új zoomérték megérkezik, de ne feltételezd, hogy a régi renderelés abban a pillanatban leáll, amikor kéred

PDFium progresszív renderelési idővonal Delphi-ben: minden új zoom kérés lemondja az előző tokent, a lemondás chunkhatáron ér földet, a felváltott renderelések prsCancelledt adnak, a végső próbálkozás prsDonet
Minden új nagyítási kérés érvényteleníti az előző renderelési tokent, és a megszakítás darabhatáron landol
procedure TViewerForm.RequestRender(TargetZoom: Single);
var
  Status: TPdfProgressiveStatus;
begin
  if FTokenSource <> nil then
    FTokenSource.Cancel;           // elhagyja az előző, folyamatban lévő renderelést
  FTokenSource := TPdfCancellationTokenSource.New;  // FPdfAsync egység

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

  case Status of
    prsDone:      PresentBackBuffer;
    prsCancelled: ;                // egy újabb kérés felülírta: csendben eldobjuk
    prsFailed:    ShowRenderFailure;
  end;
end;

Interakció közben a prsCancelled a normál kimenetel, nem a kivételes. A legtöbb renderelést, amit egy zoom-gesztus indít, felülírják, mielőtt befejeződne, ezért kezeld a megszakítást rutinszerűként, és dobd el csendben az eredményt. Egy render-sor, amely minden megszakítást figyelmeztetésként naplóz, ezernyi sornyi zaj alá temeti azt az egy hibát, amely valóban számít. Hogy a képernyő ne tűnjön halottnak, amíg a valódi renderelés fut, párosítsd a progresszív útvonalat egy olcsó helyettesítővel: méretezd át az előzőleg gyorsítótárazott bitképet az új zoomra, és jelenítsd meg azonnal. Száz-kétszáz ezredmásodpercig lágynak néz ki, de azonnalinak olvasható, és megveszi a teljes minőségű renderelésnek azt az időt, amire szüksége van ahhoz, hogy vagy befejeződjön, vagy megszakítsa a következő gesztus

Az illesztési mód, amit a zoom csendben kikapcsol

Egy nézegető FitMode tulajdonsága, amely pfmFitPage-re vagy pfmFitWidth-re van állítva, minden átméretezésnél újraszámolja a zoomot, hogy az oldal illeszkedve maradjon, ahogy az ablak változik. A csapda az, hogy a Zoom közvetlen hozzárendelése visszaállítja a FitMode-ot pfmNone-ra. Alapértelmezettként ez helyes: egy felhasználó, aki szándékosan 150%-ot gépelt be, nem akarja, hogy a következő ablakátméretezés eldobja azt. De meglepi azt, aki egy zoom-be gombot Zoom := Zoom * 1.25-ként köt be, majd nem tudja kitalálni, miért állt le a szélességhez illesztés az első kattintás után. Ha az eszköztárad mind az explicit zoomot, mind az illesztési módokat kínálja, magadnak kell megjegyezned a felhasználó utolsó illesztési választását, és újra hozzá kell rendelned, amikor újra megnyomja az illesztés gombot. A komponens nem fog visszaállítani egy módot, amit egy zoom-hozzárendelés éppen törölt, és nem is kellene neki

Egy memóriakeret, amelyet meg tudsz védeni

Egy keret, amit le tudsz írni, egy olyan keret, amiért érvelni tudsz egy kódáttekintésen, ezért indulj ki egy konkrét forgatókönyvből. Tegyük fel, hogy a folyamatos görgetés a látható oldalt tartja, plusz egy előtöltött oldalt fent és lent, egy miniatűr-sáv mellett. 100%-on egy 96-DPI-s kijelzőn ez a három teljes méretű bitkép egyenként körülbelül 3,5 MB-ot tesz ki, ami semmi. 300%-on egy 4K-s kijelzőn ugyanez a három bitkép egyenként nagyjából 30 MB, és ez azelőtt van, hogy a gyorsítótár egyetlen korábbi oldalt is megtartott volna. A növekedés a gesztusban van, nem a dokumentumban

PDFium bitképmemóriaszámítás Delphi megjelenítőben: minden zoomduplázás négyszerezzi az oldalmemóriát, a folyamatos görgetés három oldalt tart élőben, plafonozott LRU költségvetés védi a gyorsítótárat, a RenderTile pedig kezeli a túlnagy rajzokat
Minden nagyítási duplázás megnégyszerezi a bittérkép-memóriát, ezért a gyorsítótárnak kemény plafonra és csempékre van szüksége a túlméretezett oldalakhoz

Egy 32 bites Delphi-folyamathoz egy józan alapértelmezett egy 256 MB-os bitkép-keret LRU-kiürítés alatt. 64 biten skálázhatsz a fizikai RAM-mal, de mindenképp tarts fenn egy kemény felső korlátot, mert a hiba, ami ellen védekezel, nem a folyamatod összeomlása. Az az egész gép, amely csapkod a lapozófájlával, miközben a nézegetőd technikailag tovább fut, és a felhasználó azon tűnődik, miért lassult le minden más. Egy kemény korlát kiszámíthatóan bukik el; egy korlátlan gyorsítótár úgy bukik el, hogy magával rántja az asztalt is. A miniatűrök megérdemlik a saját bánásmódjukat: rendereld mindegyiket egyszer a kicsi célméretén, és tartsd egy külön poolban, amelyet az LRU-logika soha nem érint. Egy 120 pixeles miniatűr újragenerálása egy 60 MB-os teljes oldal-bitkép lekicsinyítésével a lehető legpazarlóbb módja egy bélyeg előállításának

Néhány önálló oldal bármilyen keretet legyőz. Egy E-méretű mérnöki rajz vagy egy nagy térkép egészben renderelve 400%-on egy több száz megabájtos lefoglalás, és semmilyen kiürítési szabályzat nem teszi ezt elfogadhatóvá. A válasz ott az, hogy abbahagyod a teljes oldalak renderelését. A RenderTile csak azt a régiót raszterizálja a (Left, Top) pixeleltolásnál egy olyan oldalon belül, amely elméletileg PageWidth szer PageHeight-ra van méretezve, így csak a látható téglalapot rendereled, plusz egy egycsempényi margót körülötte a sima pásztázáshoz, és a csempe-eltolásokat a zoom mellé hajtod be a gyorsítótár-kulcsba. Tartsd rögzítve a csempeméreteket a dokumentum egészén. A rögzített csempék azt jelentik, hogy egy DPI-változás tisztán érvényteleníti az egész rácsot, míg a változó csempék azzal hagynak, hogy látható varratokat kergetsz a kissé eltérő skálákon renderelt régiók között

Két szomszédos funkció csendben hozzáad mindehhez. A szín-szűrő átfutások, mint a szürkeárnyalat vagy az invertálás, renderelés után futnak, és minden alkalommal egy második teljes méretű bitképet állítanak elő, megduplázva bármely olyan nézet oldalankénti lábnyomát, amely ezeket használja; ez a költség a gyengénlátóknak szóló színszűrés Delphi PDF-nézegetőkhöz tárgya. És egy nézegető, amely szavakat emel ki szövegfelolvasás közben, minden kimondott szónál érvényteleníti a renderelt nézetet, így a kiemelés-újrarajzolások és a beszédsebesség közötti kölcsönhatás jobban számít, mint amennyire elsőre látszik, ahogy azt a szavankénti TTS-kiemelés tárgyalja

A renderelési túlterhelések, a progresszív állapotkódok és maga a nézegető komponens a PDFium Component termékoldalán van dokumentálva