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
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
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
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