Hold zoomknappen nede i en naiv PDF-viewer, og hold øje med CPU-grafen. Ét tryk på en auto-gentagende zoomkontrol affyrer et dusin eller flere zoomtrin i sekundet, og hvis hvert trin igangsætter en fuldkvalitets gen-rendering af den synlige side, hober rendringerne sig op hurtigere, end de bliver færdige. Siden rasteriseres fint isoleret, måske 180 ms for en A4-scanning, men man kører nu et dusin 180 ms-renderinger mod arbejde, brugeren allerede er kommet forbi. Viewer'en låser, en kerne sidder fast på 100%, og når skærmen indhenter det, er brugeren stoppet ved et zoomniveau fire renderinger tilbage. Kuren er ikke en hurtigere rasterizer. Det er en cache, der returnerer færdige sider øjeblikkeligt, og en render-løkke, der er villig til at opgive arbejde i det øjeblik, det bliver forældet
PDFium Component giver dig delene til begge dele og blander sig ikke i politikken. Du får bitmaps, som kalderen ejer, en progressiv renderer, der tager et annulleringstoken, tilpasningstilstande, der genberegner zoom ved størrelsesændring, og et tile-kald til sider, der er for store til at rasterisere hele. Det, komponenten bevidst ikke leverer, er selve cachen, fordi den rette udsmidningspolitik afhænger af din viewport, din platforms hukommelsesloft og den måde, dine brugere scroller på. Den beslutning er din at få rigtig, og konsekvenserne af at få den galt er præcis fastlåsningen og lækken
Hvor millisekunderne og megabyttene forsvinder hen
Sæt tal på omkostningen, før du designer noget som helst. En A4-side ved 96 DPI er cirka 794 gange 1123 pixels, omkring 3,5 MB som en 32-bit bitmap. Zoom til 200%, og det firdobles. Ved 400% på en skærm med høj DPI allokerer og udfylder man en enkelt sidebitmap på 50 til 60 MB, og en viewer med kontinuerlig scroll holder flere sider levende ad gangen. Rasteriseringsomkostningen følger outputpixels, så hver fordobling af zoom firdobler nogenlunde både rendertid og hukommelse sammen
To konsekvenser følger direkte af det regnestykke. En cache, hvis nøgle ignorerer zoomniveauet, er værdiløs, fordi selve den gestus, den skal accelerere, zoomning, producerer en ny bitmap hver gang. Og en ubegrænset cache vil løbe tør for adresseplads i en 32-bit proces netop på de dokumenter, hvor folk zoomer hårdest: tætte skøde-scanninger, tekniske tegninger, storformatkort. Cachen skal nøgles korrekt og begrænses fast, og ingen af delene er valgfri
Hvad hører hjemme i cache-nøglen
En cachet bitmap er kun sikker at genbruge, når hvert input, der formede dens pixels, stadig stemmer. Det betyder sidetallet, den effektive zoom (eller ækvivalent outputpixel-dimensionerne), rotationen, skærmens DPI og de render-indstillinger, der var gældende, da den blev produceret. En side rendret med reAnnotations er et andet billede end den samme side uden dem, og et gråtonepass gennem reGrayscale er igen forskelligt. Udelad blot ét af disse fra nøglen, og fejlene er forudsigelige: en annotationsoverlejring, der hænger ved, efter en reviewer sletter kommentaren, eller en side, der bliver sløret i det øjeblik, en bruger trækker vinduet fra et laptop-panel til en ekstern 4K-skærm, og DPI'en ændrer sig under en forældet 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 ejer nu denne bitmap
end;
Ved et hit returnerer dette på mikrosekunder, hvilket er hele pointen. Det sværere spørgsmål er, hvad der sker med de bitmaps, der falder ud af cachen, og det viser sig at være et spørgsmål om, hvem der ejer dem
Hvem frigiver bitmappen
Funktionsformen af RenderPage returnerer en TBitmap, som kalderen ejer. I en engangseksport er det ejerskab åbenlyst og let at overholde. Inde i en cache bliver det den absolut mest almindelige lækage i Delphi-PDF-viewere, fordi dictionary'et nu holder den eneste reference til hver bitmap, og et almindeligt TDictionary frigiver kun nøgler og værdier for dig, hvis de er managed typer. Det er en TBitmap ikke. Udsmid en indgang uden at kalde Free, og pixels forbliver allokeret uden noget, der peger på dem
Grunden til, at dette glider igennem, er timing. En ti-minutters røgtest zoomer aldrig nok forskellige sider til at bemærke det; lækagen viser sig først, efter nogen har scrollet og zoomet i et langt dokument i et par timer, på hvilket tidspunkt processen holder på hundredvis af forældreløse sidebitmaps, og maskinen begynder at page. Det er grunden til, at udsmidning hører hjemme i cachens første version, ikke en senere. Begræns cachen efter estimerede bytes, beregnet som bredde gange højde gange fire, udsmid de mindst-nyligt-brugte sider, der ligger uden for viewport og prefetch-vinduet, og frigiv hver bitmap, når du fjerner den. For tegninger, der reelt er flygtige, lader overloadene, der rendrer ind i en TBitmap leveret af kalderen eller direkte på en HDC, dig springe ejerskabsdansen helt over. Udskriftsforhåndsvisning er det oplagte tilfælde, eftersom man renderer hvert ark én gang, og at cache det giver intet
Progressiv rendering og ærlig annullering
De almindelige RenderPage-overloads blokerer, indtil siden er færdig, hvilket er præcis den adfærd, man ikke ønsker, mens brugeren stadig bevæger zoomkontrollen. Til det griber man til RenderPageProgressive. Den tager et IPdfCancellationToken og returnerer enten prsDone, prsCancelled eller prsFailed. Den adfærdsdetalje, der fanger folk, er, at annullering ikke er øjeblikkelig. Tokenet polles ved chunk-grænser inde i renderingen, så et token, man signalerer midt i en chunk, træder først i kraft, når den chunk er færdig. På en kompleks side løber ventetiden mellem forespørgsel og stop op i titalls millisekunder. Design omkring det hul i stedet for at ønske det væk: annullér det forrige token i det øjeblik, en ny zoomværdi ankommer, men gå ikke ud fra, at den gamle rendering stopper, i det øjeblik man beder om det
procedure TViewerForm.RequestRender(TargetZoom: Single);
var
Status: TPdfProgressiveStatus;
begin
if FTokenSource <> nil then
FTokenSource.Cancel; // opgiv den forrige rendering, der 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: ; // afløst af en nyere anmodning: drop stille
prsFailed: ShowRenderFailure;
end;
end;
Under interaktion er prsCancelled det normale udfald, ikke det exceptionelle. De fleste af de renderinger, en zoomgestus starter, vil blive afløst, før de bliver færdige, så behandl annullering som rutine, og drop resultatet stille. En renderkø, der logger hver annullering som en advarsel, vil begrave den ene fejl, der rent faktisk betyder noget, under tusindvis af linjer støj. For at forhindre, at skærmen ser død ud, mens den rigtige rendering kører, kan man parre den progressive sti med en billig stedfortræder: skalér den forrige cachede bitmap til den nye zoom, og præsentér den øjeblikkeligt. Det ser blødt ud i et par hundrede millisekunder, men det opleves som øjeblikkeligt, og det køber fuldkvalitetsrenderingen den tid, den har brug for til enten at blive færdig eller blive annulleret af den næste gestus
Tilpasningstilstanden, som zoom stille slår fra
En viewers FitMode-egenskab, sat til pfmFitPage eller pfmFitWidth, genberegner zoom ved hver størrelsesændring, så siden bliver ved med at passe, efterhånden som vinduet ændrer sig. Faldgruben er, at en direkte tildeling af Zoom nulstiller FitMode tilbage til pfmNone. Som standard er det korrekt: en bruger, der bevidst har indtastet 150%, ønsker ikke, at den næste vinduesstørrelsesændring smider det væk. Men det overrasker enhver, der forbinder en zoom-ind-knap som Zoom := Zoom * 1.25 og derefter ikke kan forstå, hvorfor tilpas-til-bredde stoppede med at reagere efter det første klik. Hvis din værktøjslinje tilbyder både eksplicit zoom og tilpasningstilstande, skal man selv huske brugerens sidste tilpasningsvalg og gentildele det, når vedkommende trykker på tilpasningsknappen igen. Komponenten genopretter ikke en tilstand, som en zoom-tildeling lige har ryddet, og det er heller ikke meningen, den skal
Et hukommelsesbudget, man kan forsvare
Et budget, man kan skrive ned, er et budget, man kan argumentere for i en kodegennemgang, så start fra et konkret scenarie. Sig, at kontinuerlig scroll holder den synlige side plus én forudhentet side over og under, ved siden af en miniaturestrimmel. Ved 100% på en 96-DPI-skærm kommer disse tre bitmaps i fuld størrelse til omkring 3,5 MB hver, hvilket er ingenting. Ved 300% på en 4K-skærm er de samme tre bitmaps omkring 30 MB hver, og det er før cachen har beholdt en eneste historisk side. Væksten ligger i gestussen, ikke i dokumentet
Et fornuftigt standardvalg for en 32-bit Delphi-proces er et bitmap-budget på 256 MB under LRU-udsmidning. På 64-bit kan man skalere med fysisk RAM, men behold under alle omstændigheder et hårdt loft, fordi den fejl, man beskytter sig imod, ikke er ens proces, der crasher. Det er hele maskinen, der tærsker sin swap-fil, mens ens viewer teknisk set bliver ved med at køre, og brugeren undrer sig over, hvorfor alt andet blev langsommere. Et hårdt loft fejler forudsigeligt; en ubegrænset cache fejler ved at tage skrivebordet med sig. Miniaturebilleder fortjener deres egen behandling: render hvert ét én gang ved dets lille målstørrelse, og hold det i en separat pulje, som LRU-logikken aldrig rører. At gengenerere en 120-pixel miniature ved at nedskalere en 60 MB fuldsidebitmap er den mest spildende mulige måde at producere et frimærke på
Nogle enkeltsider overvinder ethvert budget. En E-format teknisk tegning eller et stort kort rendret helt ved 400% er en allokering på flere hundrede megabyte, og ingen udsmidningspolitik gør det acceptabelt. Svaret dér er at stoppe med at rendre hele sider. RenderTile rasteriserer kun regionen ved pixel-offsettet (Left, Top) inden for en side, der nominelt skaleres til PageWidth gange PageHeight, så man kun rendrer det synlige rektangel plus en margin på én tile omkring det for jævn panorering, og man folder tile-offsettene ind i cache-nøglen ved siden af zoom. Hold tile-dimensionerne faste på tværs af dokumentet. Faste tiles betyder, at en DPI-ændring ugyldiggør hele gitteret rent, hvor variable tiles efterlader en jagt på synlige sømme mellem regioner rendret ved lidt forskellige skalaer
To tilstødende funktioner lægger stille og roligt til alt dette. Farvefilterpasseringer såsom gråtoner eller inversion kører efter rendering og producerer hver gang en anden bitmap i fuld størrelse, hvilket fordobler fodaftrykket pr. side for enhver visning, der bruger dem; den omkostning er emnet for farvefiltrering for svagtseende til Delphi-PDF-viewere. Og en viewer, der fremhæver ord under tekst-til-tale, ugyldiggør den rendrede visning ved hvert talt ord, så samspillet mellem fremhævnings-gentegninger og talehastighed betyder mere, end det først virker, som dækket i ord-for-ord TTS-fremhævning
Render-overloadene, de progressive statuskoder og selve viewer-komponenten er dokumenteret på produktsiden for PDFium Component