De fleste PDF-sider rastreres på noen få millisekunder, og du tenker aldri over det. Så åpner en bruker en A1-ingeniørtegning, en side stappfull med titusenvis av vektorstrøk, eller en plakat full av gjennomsiktighetsgrupper og myke masker, og den ene samtalen som tegner det, tar to eller tre sekunder. Hvis dette kallet kjører på brukergrensesnitt-tråden, stopper vinduet å male på nytt, tittellinjen tones ut (greys out), og operativsystemet tilbyr seg å avslutte applikasjonen. Arbeidet er legitimt. Siden trenger virkelig så lang tid. Feilen er at gjengivelsen er ett udelelig blokkerende anrop uten mulighet til å trekke pusten, og ingen måte å stoppe på.
Denne artikkelen handler om nøyaktig ett av disse to problemene: å avbryte en lang enkeltside-gjengivelse uten å fryse grensesnittet. Brukeren klikket til neste side, eller zoomet, eller lukket dokumentet, og gjengivelsen som pågår er nå bortkastet arbeid som bør avsluttes ved neste mulighet i stedet for å kjøres til ferdigstillelse. Å glatte ut rulling og zooming ved å bufre det som allerede er rastrert, er en separat bekymring med sin egen design, som dekkes i søsterartikkelen lenket til på slutten. Her er det eneste spørsmålet hvordan man får en progressiv gjengivelse til å svare raskt og rent på en avbryt-forespørsel.
Den progressive API-en for gjengivelse som PDFium allerede leverer
PDFium forutså fryse-halvdelen av problemet. Ved siden av engangs-FPDF_RenderPageBitmap eksponerer de en progressiv variant som deler en side opp i arbeidsstykker. Du kaller FPDF_RenderPageBitmap_Start én gang for å sette opp gjengivelsen mot et målbilde, deretter kaller du FPDF_RenderPage_Continue gjentatte ganger. Hver Continue rastrerer en avgrenset del og returnerer en status. FPDF_RENDER_TOBECONTINUED betyr at det er mer å gjøre, FPDF_RENDER_DONE betyr at siden er ferdig, og FPDF_RENDER_FAILED betyr at den stoppet på grunn av en feil. Når løkken slutter, kaller du FPDF_RenderPage_Close for å frigi den per-side progressive tilstanden. Fordi kontrollen returneres til koden din mellom stykkene, kan du pumpe meldinger, oppdatere en fremdriftsindikator, eller sjekke om arbeidet fortsatt er ønsket.
Mekanismen PDFium gir for å bestemme når du skal gi etter (yield), er en tilbakeringingsstrukt kalt IFSDK_PAUSE. Du overleverer den til Start og til hver Continue. Etter hvert stykke kaller PDFium funksjonspekeren NeedToPauseNow, og hvis den returnerer en ikke-null-verdi, stopper den nåværende Continue tidlig og gir kontrollen tilbake med FPDF_RENDER_TOBECONTINUED. Strukten bærer også et version-felt, som må settes til 1, og en friforms user-peker som PDFium aldri rører og sender gjennom urørt. Denne urørte pekeren er selve hengselet for designet som følger.
Gjenbruk av pause som avbrytelse
Den opprinnelige hensikten med NeedToPauseNow er tidsdeling (time-slicing). Returner ikke-null når rammebudsjettet (frame budget) ditt er oppbrukt, returner null for å fortsette gjengivelsen, så pauser PDFium slik at du kan gjøre noe annet før du gjenopptar den samme gjengivelsen. PDFium Component gjenbruker dette samme signalet for et annet verb. I stedet for å svare "skal jeg ta pause og la deg fortsette", svarer tilbakeringingen "har dette arbeidet blitt avbrutt." De to kartlegges rent mot hverandre på grunn av hva løkken gjør når den ser flagget. En reell pause forventer en senere Continue; en avbrytelse gjør det ikke. Når den kallende løkken observerer at tokenet er avbrutt, lukker den gjengivelseskonteksten og kaller aldri Continue igjen, så den samme ikke-null-returen som PDFium leser som "stopp dette stykket", blir i praksis "stopp for godt."
Avbrytelse uttrykkes gjennom et grensesnitt, IPdfCancellationToken, hvis IsCancelled-egenskap vipper fra usann til sann når en annen del av programmet ber om at gjengivelsen skal stoppe. Broen mellom det Pascal-grensesnittet og PDFiums C-tilbakeringing er en enkelt peker. Tokenets grensesnittreferanse skrives inn i IFSDK_PAUSE.user, og en statisk cdecl-tilbakeringing leser den ut igjen og spør den ut. Dette er det klassiske problemet med å la et C-bibliotek kalle tilbake inn i Pascal: tilbakeringingen må være en vanlig funksjon med C-kallekonvensjon (calling convention), ikke en metode, fordi PDFium lagrer og påkaller en ren funksjonspeker som ikke vet noe om Pascal-objekter eller Self.
type
TPdfProgressivePause = record
Pause: IFSDK_PAUSE; // PDFium reads this; .user holds the token
Token: IPdfCancellationToken; // strong ref keeps the token alive
end;
function ProgressivePauseCallback(pThis: PIFSDK_PAUSE): FPDF_BOOL; cdecl;
var
Token: IPdfCancellationToken;
begin
Result := 0;
if (pThis = nil) or (pThis^.user = nil) then
Exit;
Token := IPdfCancellationToken(pThis^.user);
if Token.IsCancelled then
Result := 1; // non-zero: PDFium stops this chunk
end;
Tilbakeringingen gjenoppretter tokenet ved å kaste pThis^.user tilbake til grensesnittypen og leser IsCancelled. Ingenting i den allokerer, låser eller blokkerer, noe som betyr noe fordi PDFium kaller den på gjengivelsestråden etter hvert stykke, og eventuelt arbeid som gjøres her blir lagt til kostnaden for selve gjengivelsen. Beskyttelsen mot en nil-strukt eller et nil-user-felt betyr at den samme funksjonen er trygg å installere selv på en gjengivelse som aldri fikk et ekte token.
Holde tokenet i live gjennom løkken
Å kaste en grensesnittpeker gjennom en rå Pointer og tilbake er der levetidsfeil blir født. Et IInterface i Delphi er referansetelt (reference counted), og tellingen flytter seg bare når kompilatoren kan se en grensesnittstypet variabel bli tildelt. Å lagre tokenet utelukkende som en ren peker inne i IFSDK_PAUSE.user ville skjule det fullstendig fra referansetelleren. Hvis den eneste andre referansen til det tokenet gikk ut av omfang (out of scope) mens Continue-løkken fortsatt kjørte, ville objektet bli frigjort under tilbakeringingen, og neste stykke ville dereferert en dinglende peker (dangling pointer).
Dette er grunnen til at deskriptoren er en record som holder to ting, ikke én. Pause-feltet er strukten PDFium leser. Token-feltet er en ekte grensesnittstypet referanse som kompilatoren teller, og det eksisterer av ingen annen grunn enn å feste tokenet i minnet så lenge recorden lever. Recorden er en lokal variabel på stakken til gjengivelsesrutinen, så den forblir gyldig i hele løkkens varighet og rives ned først når rutinen avsluttes. Den rene pekeren i user og den talte referansen i Token navngir det samme objektet; den ene er det PDFium kan lese, den andre er det som hindrer det objektet i å bli samlet (collected).
var
Pause: TPdfProgressivePause;
EffectiveToken: IPdfCancellationToken;
begin
// ... choose EffectiveToken ...
// Strong ref first, then publish the same object to PDFium via .user.
Pause.Token := EffectiveToken;
Pause.Pause.version := 1;
Pause.Pause.NeedToPauseNow := ProgressivePauseCallback;
Pause.Pause.user := Pointer(EffectiveToken);
Å lukke gjengivelseskonteksten uansett hvordan løkken slutter
Hvert kall til FPDF_RenderPageBitmap_Start tildeler progressiv tilstand som PDFium knytter til siden, og den tilstanden frigis kun av FPDF_RenderPage_Close. Det er tre veier ut av den drivende løkken. Siden fullføres, og den siste statusen er FPDF_RENDER_DONE. Tokenet utløses, og løkken avsluttes tidlig og rapporterer avbrytelse. Noe svikter, og statusen er FPDF_RENDER_FAILED. Alle tre må kalle Close, og avbrytelsesstien er den enkleste å få feil, fordi den naturlige formen på "se avbryt, bryt ut" har en tendens til å hoppe over opprydding på vei mot utgangen. Å la Close være unådd lekker den per-side tilstanden, og en leser som lar brukeren avbryte gjengivelse etter gjengivelse ville akkumulert den lekkasjen på hver avbrutt side.
Den robuste formen legger løkken og resultatklassifiseringen inne i en try, og FPDF_RenderPage_Close i den matchende finally. Målbildet ødelegges i den samme blokken. Avbrytelse kan forlate løkken gjennom en tidlig Exit, og finally kjører fortsatt, så det er nøyaktig ett sted som frigjør den progressive tilstanden og det kan ikke omgås.
Status := FPDF_RenderPageBitmap_Start(PdfBmp, FPage, Left, Top,
Width, Height, Ord(Rotation), EncodeRenderOptions(Options), Pause.Pause);
try
while Status = FPDF_RENDER_TOBECONTINUED do
begin
if EffectiveToken.IsCancelled then
begin
Result := prsCancelled;
Exit;
end;
Status := FPDF_RenderPage_Continue(FPage, Pause.Pause);
end;
if EffectiveToken.IsCancelled then
Result := prsCancelled
else if Status = FPDF_RENDER_DONE then
Result := prsDone
else
Result := prsFailed;
finally
// Frees the progressive state Start allocated; mandatory on every path.
FPDF_RenderPage_Close(FPage);
FPDFBitmap_Destroy(PdfBmp);
end;
Løkken sjekker tokenet før hver Continue, samt å stole på tilbakeringingen inni det. Tilbakeringingen forkorter det nåværende stykket; løkkesjekken forhindrer den neste i å starte. Sammen begrenser de hvor lang tid en avbrytelse tar for å tre i kraft, til omtrent varigheten av ett stykke.
Tre utfall, og hva punktgrafikken inneholder etter en avbrytelse
Det offentlige inngangspunktet er TPdf.RenderPageProgressive, og det returnerer en TPdfProgressiveStatus som er én av prsDone, prsCancelled eller prsFailed. Verdiene speiler PDFiums FPDF_RENDER_*-konstanter i Pascal-idiom, men bretter avbrytelsestilfellet inn som et førsteklasses resultat i stedet for en feil.
Det som fanger folk er hva målbildet inneholder etter prsCancelled. Det er ikke tomt. PDFium gjengir progressivt inn i samme bilde, stykke etter stykke, så når en avbrytelse stopper løkken, inneholder bildet alt som ble malt frem til det øyeblikket, som er et delvis bilde: noen bånd ferdige, resten viser fortsatt fyllfargen. Om det delvise resultatet er nyttig avhenger av innringeren (the caller). En leser som er i ferd med å kaste bildet fordi brukeren navigerte et annet sted, kan ganske enkelt ignorere det. En leser som vil vise en forhåndsvisning til lav kostnad, kan beholde det. Det du ikke må gjøre, er å anta at prsCancelled innebærer et tomt eller udefinert bilde; det innebærer et sant øyeblikksbilde av en uferdig gjengivelse.
var
Bmp: TBitmap;
Token: IPdfCancellationToken;
Status: TPdfProgressiveStatus;
begin
Bmp := TBitmap.Create;
try
// Token starts un-cancelled; flip Token.IsCancelled from elsewhere
// (a UI action, a navigation event) to abort the render in flight.
Status := Pdf.RenderPageProgressive(Bmp, 0, 0, PageW, PageH, Token);
case Status of
prsDone: Image1.Picture.Assign(Bmp); // fully rendered
prsCancelled: ; // partial bitmap, usually discarded
prsFailed: ShowMessage('Render failed');
end;
finally
Bmp.Free;
end;
end;
Nil-tokenet og en grenløs tilbakeringingssti
Avbrytelse er tilvalg (opt-in). En innringer som bare ønsker progressiv gjengivelse for fordelen med meldingspumping, uten intensjon om å avbryte, skal kunne sende inn nil for tokenet. Den naive måten å støtte det på, er å strø "hvis et token ble levert"-sjekker gjennom tilbakeringingen og løkken, som betyr en forgrening (branch) på hvert stykke og en tilbakeringing som må håndtere både et ekte token og fraværet av det.
Implementasjonen unngår det ved å sette inn en singleton når innringeren ikke sender noe. Et nil-token byttes mot PdfNoCancellationToken, et grensesnitt hvis IsCancelled alltid er usant. Fra det punktet har tilbakeringingen og løkken et token å spørre ut i alle tilfeller, så ingen av dem trenger en nil-sjekk og ingen trenger en spesiell sti. Tokenet som aldri avbryter svarer rett og slett alltid usant, tilbakeringingen returnerer alltid null, og gjengivelsen kjører til ferdigstillelse akkurat som en uavbrytbar en ville gjort. Valgfri atferd er modellert som et token som aldri utløses i stedet for fraværet av et token, noe som holder den varme stien (hot path) jevn (uniform).
// nil -> never-cancel singleton, so the callback path is identical
// whether or not the caller opted into cancellation.
if AToken <> nil then
EffectiveToken := AToken
else
EffectiveToken := PdfNoCancellationToken;
Formen som dukker opp er liten og verdt å gjenta, fordi den er den gjenbrukbare delen. Et C-bibliotek som støtter en tilbakeringing gir deg nøyaktig én kanal for å sende tilstand inn i den tilbakeringingen, den ugjennomsiktige brukerpekeren (opaque user pointer). Legg en opptalt (counted) Pascal-grensesnittreferanse bak den pekeren, hold en andre ekte referanse i live ved siden av strukten, slik at objektet ikke kan samles inn (collected) midt i anropet, og les grensesnittet ut igjen inne i en statisk cdecl-funksjon. Pakk hele den drivende løkken inn i en try, og frigjør den opprinnelige (native) konteksten i finally. Den samme malen overføres til enhver progressiv eller tilbakeringingsdrevet PDFium-operasjon der Pascal-kode må ha kontroll over levetiden mens C holder en peker.
Avbrytelse er bare den ene halvdelen av en responsiv leser. Den andre halvdelen er å ikke gjengi på nytt sider du allerede har tegnet, og holde zoomen og rullingen jevn ved å servere bufrede bilder, noe som dekkes i vår artikkel om hurtigbuffer for gjengivelse og zoom-ytelse. For å se hvordan den avbrytbare gjengivelsen passer inn i en komplett leser ved siden av navigasjon, markering og søk, se å bygge en funksjonsrik PDF-leser med PDFium VCL-komponenten. Den progressive gjengivelsen beskrevet her leveres som en del av PDFium Component for Delphi og Lazarus, ved siden av innlastings-, gjengivelses- og API-ene for skjemaer som dekkes andre steder på denne bloggen.