Hold nede zoomknappen i en naiv PDF-viser og følg med på CPU-grafen. Ett trykk på en auto-repeterende zoomkontroll avfyrer et dusin eller flere zoomtrinn i sekundet, og hvis hvert trinn utløser en full-kvalitets gjenrendering av den synlige siden, hoper rendringene seg opp raskere enn de blir ferdige. Siden rasteriseres fint isolert, kanskje 180 ms for en A4-skanning, men nå kjører man et dusin 180 ms-renderinger mot arbeid brukeren allerede har passert. Viseren låser seg, en kjerne setter seg fast på 100%, og når skjermen tar igjen, har brukeren stoppet på et zoomnivå fire renderinger tilbake. Kuren er ikke en raskere rasterizer. Det er en cache som returnerer ferdige sider øyeblikkelig, og en renderløkke som er villig til å forlate arbeid i det øyeblikket det blir foreldet
PDFium Component gir deg delene til begge deler og holder seg unna policyen. Du får bitmaps som kalleren eier, en progressiv renderer som tar et kanselleringstoken, tilpasningsmoduser som beregner zoom på nytt ved størrelsesendring, og et flislegging-kall for sider som er for store til å rasterisere hele. Det komponenten bevisst ikke leverer, er selve cachen, fordi den riktige utkastelsespolicyen avhenger av viewporten din, plattformens minnetak og hvordan brukerne dine skroller. Den beslutningen er det opp til deg å ta riktig, og konsekvensene av å ta feil er nettopp fastlåsingen og lekkasjen
Hvor millisekundene og megabytene blir av
Sett tall på kostnaden før du designer noe som helst. En A4-side ved 96 DPI er omtrent 794 ganger 1123 piksler, rundt 3,5 MB som en 32-bit bitmap. Zoom til 200%, og det firedobles. Ved 400% på en høy-DPI-skjerm allokerer og fyller man en enkelt sidebitmap på 50 til 60 MB, og en viser med kontinuerlig rulling holder flere sider levende samtidig. Rasteriseringskostnaden følger utdatapiksler, så hver dobling av zoom firedobler omtrent både rendertid og minne sammen
To konsekvenser følger rett fra den regnestykken. En cache hvis nøkkel ignorerer zoomnivået, er verdiløs, fordi selve bevegelsen den skal akselerere, zooming, produserer en ny bitmap hver gang. Og en ubegrenset cache vil gjøre en 32-bit prosess tom for adresserom nettopp på dokumentene der folk zoomer hardest: tette skjøteskanninger, tekniske tegninger, storformatkart. Cachen må nøkles riktig og begrenses fast, og ingen av delene er valgfrie
Hva som hører hjemme i cache-nøkkelen
En bufret bitmap er bare trygg å gjenbruke når hvert input som formet pikslene, fortsatt stemmer. Det betyr sidetallet, den effektive zoomen (eller ekvivalent utdatapikseldimensjonene), rotasjonen, skjermens DPI og render-innstillingene som gjaldt da den ble produsert. En side rendret med reAnnotations er et annet bilde enn den samme siden uten dem, og en gråtonepassering gjennom reGrayscale er igjen forskjellig. Utelat bare én av disse fra nøkkelen, og feilene er forutsigbare: en annotasjonsoverlegg som blir hengende igjen etter at en gjennomleser sletter kommentaren, eller en side som blir uskarp i det øyeblikket en bruker drar vinduet fra et bærbar-panel til en ekstern 4K-skjerm og DPI-en endrer seg under en foreldet bitmap
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); // cachen eier nå denne bitmappen
end;
Ved et treff returnerer dette på mikrosekunder, som er hele poenget. Det vanskeligere spørsmålet er hva som skjer med bitmapene som faller ut av cachen, og det viser seg å være et spørsmål om hvem som eier dem
Hvem frigjør bitmappen
Funksjonsformen til RenderPage returnerer en TBitmap som kalleren eier. I en engangseksport er det eierskapet opplagt og lett å overholde. Inne i en cache blir det den desidert mest vanlige lekkasjen i Delphi-PDF-visere, fordi dictionaryet nå holder den eneste referansen til hver bitmap, og et vanlig TDictionary frigjør bare nøkler og verdier for deg hvis de er administrerte typer. Det er ikke en TBitmap. Kast ut en oppføring uten å kalle Free, og pikslene forblir allokert uten at noe peker på dem
Grunnen til at dette glipper gjennom, er timing. En ti-minutters røyktest zoomer aldri nok forskjellige sider til å legge merke til det; lekkasjen viser seg først etter at noen har rullet og zoomet i et langt dokument i et par timer, og på det tidspunktet holder prosessen på hundrevis av foreldreløse sidebitmaper, og maskinen begynner å veksle mot fillagringsminne. Det er derfor utkastelse hører hjemme i den første versjonen av cachen, ikke en senere. Sett tak på cachen etter estimerte byte, beregnet som bredde ganger høyde ganger fire, kast ut de minst nylig brukte sidene som ligger utenfor viewporten og prefetch-vinduet, og frigjør hver bitmap idet du fjerner den. For tegninger som virkelig er flyktige, lar overloadene som rendrer inn i en TBitmap gitt av kalleren eller rett på en HDC, deg hoppe over hele eierskapsdansen. Utskriftsforhåndsvisning er det opplagte tilfellet, siden man rendrer hvert ark én gang, og å cache det gir ingenting
Progressiv rendering og ærlig kansellering
De vanlige RenderPage-overloadene blokkerer til siden er ferdig, som er nøyaktig den oppførselen man ikke ønsker mens brukeren fortsatt beveger zoomkontrollen. Til det griper man til RenderPageProgressive. Den tar et IPdfCancellationToken og returnerer enten prsDone, prsCancelled eller prsFailed. Atferdsdetaljen som fanger folk, er at kansellering ikke er øyeblikkelig. Tokenet polles ved chunk-grenser inne i renderingen, så et token man signaliserer midt i en chunk, trer først i kraft når den chunken er ferdig. På en kompleks side løper ventetiden mellom forespørsel og stopp opp i titalls millisekunder. Design rundt det gapet i stedet for å ønske det bort: kanseller det forrige tokenet i det øyeblikket en ny zoomverdi kommer inn, men ikke anta at den gamle renderingen stopper i det øyeblikket du ber om det
procedure TViewerForm.RequestRender(TargetZoom: Single);
var
Status: TPdfProgressiveStatus;
begin
if FTokenSource <> nil then
FTokenSource.Cancel; // forlat den forrige renderingen som er i gang
FTokenSource := TPdfCancellationTokenSource.New; // FPdfAsync-modulet
Status := Pdf.RenderPageProgressive(FBackBuffer, 0, 0,
FBackBuffer.Width, FBackBuffer.Height, FTokenSource.Token,
ro0, [reAnnotations]);
case Status of
prsDone: PresentBackBuffer;
prsCancelled: ; // avløst av en nyere forespørsel: slipp stille
prsFailed: ShowRenderFailure;
end;
end;
Under interaksjon er prsCancelled det normale utfallet, ikke det eksepsjonelle. De fleste av renderingene en zoombevegelse starter, blir avløst før de blir ferdige, så behandl kansellering som rutine, og slipp resultatet stille. En renderkø som logger hver kansellering som en advarsel, vil begrave den ene feilen som faktisk betyr noe, under tusenvis av linjer med støy. For å hindre at skjermen ser død ut mens den ekte renderingen kjører, kan man parre den progressive stien med en billig stedfortreder: skaler den forrige bufrede bitmappen til den nye zoomen, og presenter den umiddelbart. Den ser myk ut i et par hundre millisekunder, men den oppleves som umiddelbar, og den kjøper full-kvalitets-renderingen tiden den trenger til enten å bli ferdig eller bli kansellert av neste bevegelse
Tilpasningsmodusen som zoom stille slår av
En visers FitMode-egenskap, satt til pfmFitPage eller pfmFitWidth, beregner zoom på nytt ved hver størrelsesendring, slik at siden fortsetter å passe etter hvert som vinduet endrer seg. Fellen er at å tildele Zoom direkte, tilbakestiller FitMode til pfmNone. Som standard er det riktig: en bruker som bevisst skrev inn 150%, ønsker ikke at neste vindusendring skal kaste det bort. Men det overrasker alle som kobler en zoom-inn-knapp som Zoom := Zoom * 1.25 og deretter ikke klarer å finne ut hvorfor tilpass-til-bredde sluttet å svare etter det første klikket. Hvis verktøylinjen din tilbyr både eksplisitt zoom og tilpasningsmoduser, må du selv huske brukerens siste tilpasningsvalg og tildele det på nytt når de trykker på tilpasningsknappen igjen. Komponenten gjenoppretter ikke en modus som en zoom-tildeling nettopp fjernet, og det er heller ikke meningen at den skal
Et minnebudsjett du kan forsvare
Et budsjett du kan skrive ned, er et budsjett du kan argumentere for i en kodegjennomgang, så start fra et konkret scenario. Si at kontinuerlig rulling holder den synlige siden pluss én forhåndshentet side over og under, ved siden av en miniatyrstripe. Ved 100% på en 96-DPI-skjerm kommer disse tre bitmapene i full størrelse til omtrent 3,5 MB hver, som er ingenting. Ved 300% på en 4K-skjerm er de samme tre bitmapene omtrent 30 MB hver, og det er før cachen har beholdt en eneste historisk side. Veksten ligger i bevegelsen, ikke i dokumentet
Et fornuftig standardvalg for en 32-bit Delphi-prosess er et bitmap-budsjett på 256 MB under LRU-utkastelse. På 64-bit kan man skalere med fysisk RAM, men behold uansett et hardt tak, fordi feilen man beskytter seg mot, ikke er at prosessen krasjer. Det er hele maskinen som tresker sidefilen sin mens viseren teknisk sett fortsetter å kjøre, og brukeren lurer på hvorfor alt annet ble tregere. Et hardt tak feiler forutsigbart; en ubegrenset cache feiler ved å ta med seg hele skrivebordet. Miniatyrbilder fortjener sin egen behandling: rendre hvert ett én gang ved sin lille målstørrelse, og hold det i en separat pool som LRU-logikken aldri rører. Å regenerere en 120-piksel miniatyr ved å nedskalere en 60 MB fullside-bitmap er den mest sløsende mulige måten å produsere et frimerke på
Noen enkeltsider overvinner ethvert budsjett. En E-format teknisk tegning eller et stort kart rendret helt ved 400% er en allokering på flere hundre megabyte, og ingen utkastelsespolicy gjør det akseptabelt. Svaret der er å slutte å rendre hele sider. RenderTile rasteriserer bare regionen ved pikseloffsettet (Left, Top) innenfor en side som nominelt skaleres til PageWidth ganger PageHeight, så du rendrer bare det synlige rektangelet pluss en margin på én flis rundt det for jevn panorering, og du folder flisoffsettene inn i cache-nøkkelen ved siden av zoom. Hold flisdimensjonene faste gjennom hele dokumentet. Faste fliser betyr at en DPI-endring ugyldiggjør hele rutenettet rent, mens variable fliser lar deg jage synlige skjøter mellom regioner rendret ved litt forskjellige skalaer
To tilstøtende funksjoner legger stille til i alt dette. Fargefilterpasseringer som gråtoner eller inversjon kjører etter rendering og produserer en ny fullstørrelses bitmap hver gang, noe som fordobler fotavtrykket per side for enhver visning som bruker dem; den kostnaden er temaet for fargefiltrering for svaksynte i Delphi-PDF-visere. Og en viser som fremhever ord under tekst-til-tale, ugyldiggjør den rendrede visningen ved hvert talte ord, så samspillet mellom fremhevings-omtegninger og talehastighet betyr mer enn det først virker, som dekket i ord-for-ord TTS-fremheving
Render-overloadene, de progressive statuskodene og selve viserkomponenten er dokumentert på produktsiden for PDFium Component