De flesta PDF-sidor rastreras på några millisekunder och man tänker aldrig på det. Sedan öppnar en användare en A1-teknikritning, en sida packad med tiotusentals vektorstreck, eller en affisch fylld med transparensgrupper och mjuka masker, och det enda anropet som målar den tar två eller tre sekunder. Om det anropet körs i UI-tråden, slutar fönstret ritas om, namnlisten blir grå, och operativsystemet erbjuder sig att avsluta programmet. Arbetet är legitimt. Sidan behöver verkligen så lång tid. Felet är att renderingen är ett odelbart blockerande anrop utan möjlighet att andas och utan möjlighet att avbryta
Den här artikeln handlar om exakt det ena av dessa två problem: att avbryta en lång rendering av en enstaka sida utan att frysa gränssnittet. Användaren klickade på nästa sida, eller zoomade, eller stängde dokumentet, och renderingen som pågår är nu bortkastat arbete som bör avslutas vid nästa tillfälle i stället för att köras till slutet. Att göra skrollning och zoomning mjukare genom att cacha det som redan rastrerats är ett separat bekymmer med sin egen design, vilket behandlas i den kompletterande artikeln som är länkad i slutet. Här är den enda frågan hur man får en progressiv rendering att svara på en avbrytningsbegäran snabbt och rent
Den progressiva renderings-API:n som PDFium redan levereras med
PDFium förutsåg den frysande halvan av problemet. Vid sidan av en-gångs-anropet FPDF_RenderPageBitmap exponerar det en progressiv variant som delar upp en sida i bitar av arbete. Du anropar FPDF_RenderPageBitmap_Start en gång för att sätta upp renderingen mot en målbitmapp, sedan anropar du FPDF_RenderPage_Continue upprepade gånger. Varje Continue rastrerar en avgränsad skiva och returnerar en status. FPDF_RENDER_TOBECONTINUED betyder att det finns mer att göra, FPDF_RENDER_DONE betyder att sidan är färdig, och FPDF_RENDER_FAILED betyder att den stannade på ett fel. När loopen avslutas anropar du FPDF_RenderPage_Close för att frigöra det progressiva tillståndet för varje sida. Eftersom kontrollen återvänder till din kod mellan skivorna, kan du pumpa meddelanden, uppdatera en förloppsindikator eller kontrollera om arbetet fortfarande behövs
Den mekanism som PDFium tillhandahåller för att bestämma när man ska pausa är en callback-post (struct) som heter IFSDK_PAUSE. Du skickar in den till Start och till varje Continue. Efter varje bit anropar PDFium sin funktionspekare NeedToPauseNow, och om den returnerar ett värde som inte är noll stannar det aktuella Continue-anropet i förtid och lämnar tillbaka kontrollen med FPDF_RENDER_TOBECONTINUED. Posten bär också på ett version-fält, som måste sättas till 1, och en fri user-pekare som PDFium aldrig rör och skickar vidare orörd. Den orörda pekaren är hela gångjärnet i den design som följer
Att omvandla paus till avbryt
Den ursprungliga avsikten med NeedToPauseNow är tidsdelning (time-slicing). Returnera skilt från noll när din bildrutebudget är slut, returnera noll för att fortsätta rendera, och PDFium pausar så att du kan göra något annat innan du återupptar samma rendering. PDFium Component återanvänder samma signal för ett annat verb. I stället för att svara "bör jag pausa och låta dig återuppta", svarar callbacken "har detta arbete blivit avbrutet". De två mappar rent mot varandra på grund av vad loopen gör när den ser flaggan. En äkta paus förväntar sig ett senare Continue; ett avbrott gör det inte. När den anropande loopen observerar att token är avbruten, stänger den renderingskontexten och anropar aldrig Continue igen, så samma icke-noll-retur som PDFium tolkar som "stoppa denna bit" blir i praktiken "stoppa för gott"
Avbrott uttrycks genom ett gränssnitt, IPdfCancellationToken, vars IsCancelled-egenskap slår över från falskt till sant när någon annan del av programmet ber om att renderingen ska stanna. Bron mellan detta Pascal-gränssnitt och PDFium's C-callback är en enda pekare. Gränssnittsreferensen för token skrivs in i IFSDK_PAUSE.user, och en statisk cdecl-callback läser tillbaka den och ställer en fråga till den. Detta är det klassiska problemet med att låta ett C-bibliotek anropa tillbaka in i Pascal: callbacken måste vara en ren funktion med C-anropskonvention, inte en metod, eftersom PDFium lagrar och anropar en naken funktionspekare som inte vet någonting om Pascal-objekt 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;
Callbacken återskapar token genom att casta pThis^.user tillbaka till gränssnittstypen och läser IsCancelled. Ingenting i den allokerar, låser, eller blockerar, vilket spelar roll eftersom PDFium anropar den på renderingstråden efter varje bit och allt arbete som görs här läggs till kostnaden för själva renderingen. Skyddet mot en nil-post eller ett nil user-fält innebär att samma funktion är säker att installera även på en rendering som aldrig fick en riktig token
Att hålla token vid liv genom loopen
Att casta en gränssnittspekare via en rå Pointer och tillbaka är platsen där livstidsbuggar föds. Ett IInterface i Delphi är referensräknat, och räkningen rör sig bara när kompilatorn kan se en variabel av gränssnittstyp tilldelas. Att lagra token enbart som en naken pekare inuti IFSDK_PAUSE.user skulle dölja den från referensräknaren helt och hållet. Om den enda andra referensen till det token försvann ur sikt (out of scope) medan Continue-loopen fortfarande kördes, skulle objektet frigöras framför fötterna på callbacken, och nästa bit skulle avreferera en dinglande pekare (dangling pointer)
Det är anledningen till att deskriptorn är en post som håller två saker, inte en. Pause-fältet är posten som PDFium läser. Token-fältet är en riktig gränssnittstypad referens som kompilatorn räknar, och den existerar inte av något annat skäl än att låsa fast (pin) tokenet i minnet så länge posten lever. Posten är en lokal variabel på stacken för renderingsrutinen, så den förblir giltig under hela loopens varaktighet och rivs ner först när rutinen avslutas. Den nakna pekaren i user och den räknade referensen i Token pekar på samma objekt; det ena är vad PDFium kan läsa, det andra är vad som hindrar det objektet från att samlas in av skräphanteringen
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);
Att stänga renderingskontexten oavsett hur loopen slutar
Varje anrop till FPDF_RenderPageBitmap_Start allokerar progressivt tillstånd som PDFium associerar med sidan, och det tillståndet frigörs endast av FPDF_RenderPage_Close. Det finns tre vägar ut från drivloopen. Sidan blir färdig och den sista statusen är FPDF_RENDER_DONE. Token utlöses och loopen avslutas tidigt och rapporterar avbrott. Någonting misslyckas och statusen är FPDF_RENDER_FAILED. Alla tre måste anropa Close, och avbrottsvägen är den som är lättast att göra fel på, eftersom den naturliga formen av "ser ett avbrott, bryter ur" tenderar att hoppa över uppstädning på vägen mot utgången. Att lämna Close oanropat läcker det per sida progressiva tillståndet, och ett visningsprogram som låter användaren avbryta rendering efter rendering skulle ackumulera den läckan på varje avbruten sida
Den robusta formen placerar loopen och resultatklassificeringen inuti en try och FPDF_RenderPage_Close i den matchande finally. Målbitmappen förstörs i samma block. Avbrott kan lämna loopen genom ett tidigt Exit och finally körs ändå, så det finns exakt en plats som frigör det progressiva tillståndet och den kan inte kringgå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;
Loopen kontrollerar tokenet inför varje Continue såväl som att den förlitar sig på callbacken inuti det. Callbacken förkortar den aktuella biten; loop-kontrollen stoppar nästa från att starta. Tillsammans avgränsar de hur lång tid ett avbrott tar för att träda i kraft till ungefär varaktigheten av en bit
Tre utfall, och vad bitmappen håller efter ett avbrott
Den publika ingångspunkten är TPdf.RenderPageProgressive, och den returnerar en TPdfProgressiveStatus som är antingen prsDone, prsCancelled eller prsFailed. Värdena speglar PDFiums FPDF_RENDER_*-konstanter i Pascal-form men bakar in avbrottsfallet som ett förstklassigt resultat i stället för som ett fel
Den punkt som överraskar folk är vad målbitmappen innehåller efter prsCancelled. Den är inte blank. PDFium renderar progressivt in i samma bitmapp bit efter bit, så när ett avbrott stoppar loopen håller bitmappen vad som än målades fram till det ögonblicket, vilket är en partiell bild: några remsor klara, resten visar fortfarande fyllningsfärgen. Huruvida det partiella resultatet är användbart beror på anroparen. Ett visningsprogram som är på väg att kasta bort bitmappen eftersom användaren navigerade någon annanstans kan helt enkelt ignorera den. Ett visningsprogram som vill visa en billig förhandsgranskning kan behålla den. Det du inte får göra är att anta att prsCancelled antyder en tom eller odefinierad bitmapp; det antyder en sanningsenlig ögonblicksbild av en oavslutad rendering
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;
Ett nil-token och en förgreningsfri callback-väg
Avbrott är valfritt (opt-in). En anropare som bara vill ha progressiv rendering för fördelen med meddelandepumpning, utan någon avsikt att avbryta, bör kunna skicka nil för tokenet. Det naiva sättet att stödja det är att sprida "om ett token gavs"-kontroller genom callbacken och loopen, vilket innebär en förgrening för varje bit och en callback som måste hantera både ett riktigt token och dess frånvaro
Implementationen undviker det genom att byta ut mot en singleton när anroparen inte skickar någonting. Ett nil-token byts mot PdfNoCancellationToken, ett gränssnitt vars IsCancelled alltid är falskt. Från den punkten har callbacken och loopen ett token att fråga i varje fall, så inget av dem behöver en nil-kontroll och inget av dem behöver en specialväg. Det token som aldrig avbryts svarar helt enkelt alltid falskt, callbacken returnerar alltid noll, och renderingen körs till slut precis som en som inte går att avbryta skulle ha gjort. Det valfria beteendet modelleras som ett token som aldrig utlöses i stället för som frånvaron av ett token, vilket håller den varma stigen enhetlig
// 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;
Den form som framträder är liten och värd att upprepa, eftersom det är den återanvändbara delen. Ett C-bibliotek som stöder en callback ger dig exakt en kanal för att skicka tillstånd in i den callbacken, den opaka user-pekaren. Placera en räknad Pascal-gränssnittsreferens bakom den pekaren, håll en andra riktig referens levande bredvid posten så att objektet inte kan samlas in mitt i anropet, och läs ut gränssnittet igen inuti en statisk cdecl-funktion. Omslut hela drivloopen i en try och frigör den nativa kontexten i finally. Samma mall förs över till varje progressiv eller callback-driven PDFium-operation där Pascal-kod måste förbli i kontroll över livstiden medan C håller en pekare
Att avbryta är bara ena halvan av ett responsivt visningsprogram. Den andra halvan är att inte rendera om sidor du redan har ritat, och att hålla zoom och skrollning mjuk genom att servera cachade bitmappar, vilket täcks i vår artikel om renderingscache och zoomprestanda. För hur den avbrytbara renderingen passar in i ett komplett visningsprogram vid sidan av navigering, markering och sökning, se att bygga en funktionsrik PDF-läsare med VCL-komponenten för PDFium. Den progressiva renderingen som beskrivs här levereras som en del av PDFium Component för Delphi och Lazarus, vid sidan av API:erna för laddning, rendering och formulär som täcks på andra ställen på den här bloggen