Håll ner zoomknappen i en naiv PDF-visare och titta på CPU-diagrammet. Ett enda tryck på en autoupprepande zoomkontroll avfyrar ett dussin eller fler zoomsteg per sekund, och om varje steg startar en fullkvalitetsomrendering av den synliga sidan, hopar sig renderingarna snabbare än de blir klara. Sidan rasteriseras fint i isolering, kanske 180 ms för en A4-skanning, men du kör nu ett dussin 180 ms-renderingar mot arbete användaren redan lämnat bakom sig. Visaren låser sig, en kärna spikar på 100 %, och när skärmen hinner ikapp har användaren stannat vid en zoomnivå fyra renderingar tidigare. Boten är inte en snabbare rasteriserare. Det är en cache som returnerar färdiga sidor omedelbart och en renderingsloop villig att överge arbete i samma ögonblick det blir inaktuellt
PDFium Component ger dig delarna för båda och håller sig utanför policyn. Du får anroparägda bitmappar, en progressiv renderare som tar en avbrytningstoken, anpassningslägen som räknar om zoom vid storleksändring, och ett tile-anrop för sidor för stora för att rastreriseras i sin helhet. Vad den avsiktligt inte tillhandahåller är själva cachen, eftersom rätt utkastningspolicy beror på din visningsyta, din plattforms minnestak, och hur dina användare rullar. Det beslutet är ditt att få rätt, och konsekvenserna av att få det fel är precis låsningen och läckan
Var millisekunderna och megabyten tar vägen
Sätt siffror på kostnaden innan du designar något. En A4-sida vid 96 DPI är ungefär 794 gånger 1123 pixlar, cirka 3,5 MB som en 32-bitars bitmapp. Zooma till 200 % och det fyrdubblas. Vid 400 % på en high-DPI-skärm allokerar och fyller du en enda sidbitmapp på 50 till 60 MB, och en visare med kontinuerlig rullning håller flera sidor levande samtidigt. Rasteriseringskostnad följer utdatapixlar, så varje fördubbling av zoom fyrdubblar ungefär både renderingstid och minne tillsammans
Två konsekvenser faller rakt ut ur den aritmetiken. En cache vars nyckel ignorerar zoomnivå är värdelös, eftersom just den gest den behöver accelerera, zoomning, producerar en ny bitmapp varje gång. Och en obegränsad cache kommer att köra en 32-bitars process ur adressutrymme precis på de dokument där folk zoomar hårdast: täta lagfartsskanningar, ingenjörsritningar, storformatskartor. Cachen måste nyckelsättas korrekt och tak-begränsas stadigt, och ingetdera är valfritt
Vad som hör hemma i cachenyckeln
En cachad bitmapp är säker att återanvända bara när varje indata som format dess pixlar fortfarande stämmer. Det betyder sidnumret, den effektiva zoomen (eller likvärdigt utdatapixeldimensionerna), rotationen, skärmens DPI, och renderingsalternativen som gällde när den producerades. En sida renderad med reAnnotations är en annan bild än samma sida utan dem, och en gråskalepassering via reGrayscale är annorlunda igen. Släpp någon av dessa från nyckeln och buggarna är förutsägbara: en annoteringsöverlagring som dröjer kvar efter att en granskare raderat kommentaren, eller en sida som blir suddig i samma ögonblick en användare drar fönstret från en laptop-panel till en extern 4K-skärm och DPI:n ändras under en föråldrad bitmapp
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 äger nu den här bitmappen
end;
Vid en träff returnerar det här på mikrosekunder, vilket är hela poängen. Den svårare frågan är vad som händer med bitmapparna som faller ur cachen, och det visar sig vara en fråga om vem som äger dem
Vem frigör bitmappen
Funktionsformen av RenderPage returnerar en TBitmap som anroparen äger. I en engångsexport är det ägarskapet uppenbart och lätt att respektera. Inuti en cache blir det den enskilt vanligaste läckan i Delphi PDF-visare, eftersom ordboken nu håller den enda referensen till varje bitmapp, och en vanlig TDictionary frigör nycklar och värden åt dig bara om de är hanterade typer. En TBitmap är det inte. Kasta ut en post utan att anropa Free och pixlarna förblir allokerade utan att något pekar på dem
Skälet till att det här slinker igenom är timing. Ett tio-minuters rökstest zoomar aldrig tillräckligt många distinkta sidor för att märka det; läckan visar sig bara efter att någon har rullat och zoomat ett långt dokument i ett par timmar, vid vilken punkt processen håller hundratals föräldralösa sidbitmappar och maskinen börjar sidväxla. Det är därför utkastning hör hemma i cachens första version, inte en senare. Begränsa cachen efter uppskattade byte, beräknat som bredd gånger höjd gånger fyra, kasta ut de minst nyligen använda sidorna som ligger utanför visningsytan och förhämtningsfönstret, och frigör varje bitmapp när du tar bort den. För ritningar som genuint är övergående låter overloaden som renderar in i en anroparförsedd TBitmap eller rakt på en HDC dig hoppa över ägarskapsdansen helt. Utskriftsförhandsgranskning är det uppenbara fallet, eftersom du renderar varje ark en gång och att cacha det köper ingenting
Progressiv rendering och ärlig avbrytning
De enkla RenderPage-overloaderna blockerar tills sidan är klar, vilket är precis det beteende du inte vill ha medan användaren fortfarande rör zoomkontrollen. För det tar du till RenderPageProgressive. Den tar en IPdfCancellationToken och returnerar en av prsDone, prsCancelled, eller prsFailed. Beteendedetaljen som fångar folk är att avbrytning inte är omedelbar. Token pollas vid chunkgränser inuti renderingen, så en token du signalerar mitt i en chunk träder i kraft först när den chunken är klar. På en komplex sida löper latensen mellan att fråga och stanna till tiotals millisekunder. Designa kring den klyftan i stället för att önska bort den: avbryt den föregående token i samma ögonblick ett nytt zoomvärde anländer, men anta inte att den gamla renderingen stannar i samma ögonblick du ber den om det
procedure TViewerForm.RequestRender(TargetZoom: Single);
var
Status: TPdfProgressiveStatus;
begin
if FTokenSource <> nil then
FTokenSource.Cancel; // överge den föregående pågående renderingen
FTokenSource := TPdfCancellationTokenSource.New; // FPdfAsync-enhet
Status := Pdf.RenderPageProgressive(FBackBuffer, 0, 0,
FBackBuffer.Width, FBackBuffer.Height, FTokenSource.Token,
ro0, [reAnnotations]);
case Status of
prsDone: PresentBackBuffer;
prsCancelled: ; // ersatt av en nyare begäran: släpp tyst
prsFailed: ShowRenderFailure;
end;
end;
Under interaktion är prsCancelled det normala utfallet, inte det undantagsvisa. De flesta renderingar en zoomgest startar kommer att ersättas innan de blir klara, så behandla avbrytning som rutin och släpp resultatet tyst. En renderingskö som loggar varje avbrytning som en varning kommer att begrava det enda fel som faktiskt spelar roll under tusentals rader brus. För att hindra skärmen från att se död ut medan den riktiga renderingen körs, para den progressiva vägen med en billig ersättare: skala den föregående cachade bitmappen till den nya zoomen och visa den omedelbart. Den ser suddig ut i hundra millisekunder eller två, men den läses som omedelbar, och den köper fullkvalitetsrenderingen den tid den behöver för att antingen bli klar eller avbrytas av nästa gest
Anpassningsläget zoom tyst stänger av
En visares egenskap FitMode, satt till pfmFitPage eller pfmFitWidth, räknar om zoom vid varje storleksändring så att sidan fortsätter passa medan fönstret ändras. Haken är att tilldela Zoom direkt återställer FitMode tillbaka till pfmNone. Som standard är det korrekt: en användare som medvetet skrev in 150 % vill inte att nästa fönsterstorleksändring kastar bort det. Men det överraskar den som kopplar en zooma-in-knapp som Zoom := Zoom * 1.25 och sedan inte kan räkna ut varför anpassa-till-bredd slutade svara efter det första klicket. Om din verktygsrad erbjuder både explicit zoom och anpassningslägen, måste du själv komma ihåg användarens senaste anpassningsval och tilldela om det när de trycker på anpassningsknappen igen. Komponenten kommer inte att återställa ett läge en zoomtilldelning just rensade, och den ska inte göra det
En minnesbudget du kan försvara
En budget du kan skriva ner är en budget du kan argumentera för i en kodgranskning, så börja från ett konkret scenario. Säg att kontinuerlig rullning håller den synliga sidan plus en förhämtad sida ovanför och under, jämte en miniatyrremsa. Vid 100 % på en 96-DPI-skärm kommer de tre fullstorleksbitmapparna till ungefär 3,5 MB var, vilket är ingenting. Vid 300 % på en 4K-skärm är samma tre bitmappar ungefär 30 MB var, och det är innan cachen har behållit en enda historisk sida. Tillväxten ligger i gesten, inte i dokumentet
Ett sunt standardvärde för en 32-bitars Delphi-process är en 256 MB bitmappsbudget under LRU-utkastning. På 64-bitar kan du skala med fysiskt RAM, men behåll ett hårt tak ändå, eftersom felet du skyddar dig mot inte är att din process kraschar. Det är hela maskinen som tröskar sin växlingsfil medan din visare tekniskt sett fortsätter köra och användaren undrar varför allt annat saktade ner. Ett hårt tak misslyckas förutsägbart; en obegränsad cache misslyckas genom att ta skrivbordet med sig. Miniatyrer förtjänar sin egen behandling: rendera varje en gång vid sin lilla målstorlek och håll den i en separat pool som LRU-logiken aldrig rör. Att regenerera en 120-pixel miniatyr genom att nedskala en 60 MB helsidesbitmapp är det mest slösaktiga möjliga sättet att producera ett frimärke
Vissa enskilda sidor besegrar vilken budget som helst. En E-storleks ingenjörsritning eller en stor karta renderad i sin helhet vid 400 % är en allokering på flera hundra megabyte, och ingen utkastningspolicy gör det acceptabelt. Svaret där är att sluta rendera hela sidor. RenderTile rastreriserar bara regionen vid pixelförskjutningen (Left, Top) inom en sida nominellt skalad till PageWidth gånger PageHeight, så du renderar bara den synliga rektangeln plus en tile-marginal runt den för mjuk panorering, och du viker in tile-förskjutningarna i cachenyckeln jämte zoom. Håll tile-dimensionerna fasta över hela dokumentet. Fasta tiles betyder att en DPI-ändring ogiltigförklarar hela rutnätet rent, medan variabla tiles lämnar dig jagande synliga sömmar mellan regioner renderade vid lite olika skalor
Två angränsande funktioner lägger tyst till allt det här. Färgfilterpassager som gråskala eller invertering körs efter rendering och producerar en andra fullstorleksbitmapp varje gång, vilket fördubblar fotavtrycket per sida för varje vy som använder dem; den kostnaden är ämnet för färgfiltrering för synsvaga i Delphi PDF-visare. Och en visare som markerar ord under text-till-tal ogiltigförklarar den renderade vyn vid varje talat ord, så interaktionen mellan markeringsomritningar och taltakt spelar större roll än det först verkar, som täcks i ord-för-ord TTS-markering
Renderingsoverloaderna, de progressiva statuskoderna, och själva visarkomponenten dokumenteras på produktsidan för PDFium Component