Väčšina stránok PDF sa rastruje za niekoľko milisekúnd a nikdy o tom nepremýšľate. Potom používateľ otvorí technický výkres formátu A1, stránku plnú desiatok tisíc vektorových ťahov alebo plagát preplnený skupinami transparentnosti a jemnými maskami, a jediné volanie, ktoré ho vykreslí, trvá dve alebo tri sekundy. Ak toto volanie beží na vlákne UI, okno prestane prekresľovať, titulný panel zošedivie a operačný systém ponúkne ukončenie aplikácie. Práca je legitímna. Stránka naozaj potrebuje toľko času. Chybou je, že vykresľovanie je jedno nedeliteľné blokujúce volanie bez možnosti vzdychnutia a bez možnosti zastavenia
Tento článok sa venuje presne jednému z týchto dvoch problémov: zrušeniu dlhého vykresľovania jednej stránky bez zmrazenia UI. Používateľ klikol na ďalšiu stránku, priblížil obraz alebo zatvoril dokument a prebiehajúce vykresľovanie je teraz zbytočná práca, ktorá by mala skončiť pri najbližšej príležitosti namiesto toho, aby dobehla do konca. Plynulé posúvanie a priblíženie ukladaním do vyrovnávacej pamäte toho, čo už bolo rastrované, je samostatná téma s vlastným návrhom, ktorá je popísaná v sprievodnom článku prepojenom na konci. Tu je jedinou otázkou, ako prinútiť jedno progresívne vykresľovanie reagovať na žiadosť o zrušenie rýchlo a čisto
Progresívne rozhranie API vykresľovania, ktoré PDFium už obsahuje
PDFium predvídal polovicu problému so zmrazovaním. Popri jednorazovom FPDF_RenderPageBitmap poskytuje progresívny variant, ktorý rozdelí stránku na bloky práce. Raz zavoláte FPDF_RenderPageBitmap_Start na nastavenie vykresľovania oproti cieľovej bitovej mape a potom opakovane voláte FPDF_RenderPage_Continue. Každé volanie Continue rastruje ohraničený úsek a vracia stav. FPDF_RENDER_TOBECONTINUED znamená, že je ešte čo robiť, FPDF_RENDER_DONE znamená, že stránka je hotová, a FPDF_RENDER_FAILED znamená, že sa zastavila kvôli chybe. Keď sa slučka skončí, zavoláte FPDF_RenderPage_Close na uvoľnenie progresívneho stavu na úrovni stránky. Keďže riadenie sa vracia do vášho kódu medzi úsekmi, môžete spracovávať správy, aktualizovať indikátor priebehu alebo skontrolovať, či je práca stále žiaduca
Mechanizmus, ktorý PDFium poskytuje na rozhodnutie o podstúpení kontroly, je štruktúra spätného volania s názvom IFSDK_PAUSE. Odovzdáte ju do Start a každého volania Continue. Po každom bloku PDFium zavolá jeho ukazovateľ na funkciu NeedToPauseNow, a ak vráti nenulová hodnotu, aktuálne volanie Continue sa predčasne zastaví a vráti riadenie s hodnotou FPDF_RENDER_TOBECONTINUED. Štruktúra obsahuje aj pole version, ktoré musí byť nastavené na 1, a voľný ukazovateľ user, ktorého sa PDFium nikdy nedotýka a prechádza ním nezmeneným. Tento nedotknutý ukazovateľ je celým kĺbom nasledujúceho návrhu
Prepoužitie pauzy ako zrušenia
Pôvodným zámerom funkcie NeedToPauseNow je časové rozdeľovanie. Vrátite nenulová hodnotu, keď je váš rámcový rozpočet vyčerpaný, vrátite nulu, aby ste pokračovali vo vykresľovaní, a PDFium sa pozastaví, aby ste mohli urobiť niečo iné pred obnovením toho istého vykresľovania. PDFium Component opätovne používa ten istý signál pre iné sloveso. Namiesto odpovede „mám sa pozastaviť a nechať vás pokračovať" spätné volanie odpovedá „bola táto práca zrušená". Obe sa na seba mapujú čisto kvôli tomu, čo slučka robí, keď vidí príznak. Skutočná pauza očakáva neskoršie volanie Continue; zrušenie nie. Keď volajúca slučka zistí, že token je zrušený, uzavrie kontext vykresľovania a nikdy viac nevolá Continue, takže tá istá nenulová návratová hodnota, ktorú PDFium číta ako „zastaviť tento blok", sa fakticky stáva „zastaviť navždy"
Zrušenie je vyjadrené prostredníctvom rozhrania IPdfCancellationToken, ktorého vlastnosť IsCancelled sa prepne z false na true, keď iná časť programu požiada o zastavenie vykresľovania. Mostom medzi týmto pascalovým rozhraním a spätným volaním C jazyka PDFium je jeden ukazovateľ. Odkaz na rozhranie tokenu je zapísaný do IFSDK_PAUSE.user a statické spätné volanie cdecl ho číta späť a dotazuje sa naň. Toto je klasický problém toho, ako nechať knižnicu C volať späť do Pascalu: spätné volanie musí byť prostou funkciou s konvenciou volania C, nie metódou, pretože PDFium ukladá a vyvoláva holý ukazovateľ na funkciu, ktorý nič nevie o pascalových objektoch ani o 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;
Spätné volanie obnoví token pretypovaním pThis^.user späť na typ rozhrania a prečíta IsCancelled. Nič v ňom neprideľuje, neblokuje ani nezdržiava, čo je dôležité, pretože PDFium ho volá na vlákne vykresľovania po každom bloku a akákoľvek práca vykonaná tu sa pripočítava k cene samotného vykresľovania. Ochrana pred štruktúrou nil alebo poľom user s hodnotou nil znamená, že tá istá funkcia je bezpečná na inštaláciu aj pri vykresľovaní, ktorému nebol nikdy daný skutočný token
Udržiavanie tokenu nažive počas celej slučky
Pretypovanie ukazovateľa rozhrania cez holý Pointer a späť je miestom, kde sa rodia chyby životnosti. IInterface v Delphi je počítaný odkazmi a počet sa pohybuje iba vtedy, keď kompilátor vidí priradenie premennej s typom rozhrania. Ukladanie tokenu výhradne ako holého ukazovateľa vo vnútri IFSDK_PAUSE.user by ho úplne skrylo pred počítadlom odkazov. Ak by jediný iný odkaz na tento token vyšiel z rozsahu, kým slučka Continue stále bežala, objekt by bol uvoľnený pod spätným volaním a nasledujúci blok by odkazoval na visiacu smerník
Preto je deskriptor záznam obsahujúci dve veci, nie jednu. Pole Pause je štruktúra, ktorú PDFium číta. Pole Token je skutočný odkaz s typom rozhrania, ktorý kompilátor počíta, a existuje len preto, aby ukotvil token v pamäti po dobu životnosti záznamu. Záznam je lokálna premenná na zásobníku rutiny vykresľovania, takže zostáva platný po celú dobu trvania slučky a je zrušený iba vtedy, keď rutina skončí. Holý ukazovateľ v poli user a počítaný odkaz v poli Token pomenúvajú rovnaký objekt; jeden je to, čo PDFium môže čítať, druhý je to, čo zabraňuje zhromažďovaniu tohto objektu
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);
Uzatváranie kontextu vykresľovania bez ohľadu na to, ako sa slučka skončí
Každé volanie FPDF_RenderPageBitmap_Start prideľuje progresívny stav, ktorý PDFium priraďuje stránke, a tento stav je uvoľnený iba volaním FPDF_RenderPage_Close. Zo slučky riadenia existujú tri cesty von. Stránka sa dokončí a posledný stav je FPDF_RENDER_DONE. Token sa spustí a slučka predčasne skončí s hlásením zrušenia. Niečo zlyhá a stav je FPDF_RENDER_FAILED. Všetky tri musia volať Close, a cesta zrušenia je najľahšia na chybu, pretože prirodzený tvar „uvidieť zrušenie, vyskočiť" má tendenciu preskočiť čistenie na ceste k výstupu. Ponechanie Close nedosiahnutého uvoľňuje stav na úrovni stránky a prehliadač, ktorý umožňuje používateľovi zrušiť vykresľovanie za vykresľovaním, by akumuloval tento únik pri každej prerušenej stránke
Robustný tvar vloží slučku a klasifikáciu výsledku do bloku try a FPDF_RenderPage_Close do zodpovedajúceho bloku finally. Cieľová bitová mapa je zničená v rovnakom bloku. Zrušenie môže opustiť slučku cez predčasný Exit a blok finally sa stále spustí, takže existuje práve jedno miesto, ktoré uvoľní progresívny stav a nemožno ho obí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;
Slučka kontroluje token pred každým volaním Continue a zároveň sa spolieha na spätné volanie vo vnútri neho. Spätné volanie skracuje aktuálny blok; kontrola slučky zabraňuje spusteniu nasledujúceho. Spoločne ohraničujú dobu, za ktorú zrušenie nadobudne účinnosť, na približne trvanie jedného bloku
Tri výsledky a čo bitová mapa obsahuje po zrušení
Verejným vstupným bodom je TPdf.RenderPageProgressive, ktorý vracia TPdfProgressiveStatus, čo je jedna z hodnôt prsDone, prsCancelled alebo prsFailed. Hodnoty zrkadlia konštanty PDFium FPDF_RENDER_* v pascalovej idiome, ale zahŕňajú prípad zrušenia ako výsledok prvej triedy, nie ako chybu
Bod, ktorý ľudí zaskočí, je to, čo cieľová bitová mapa obsahuje po výsledku prsCancelled. Nie je prázdna. PDFium vykreslí progresívne do tej istej bitovej mapy blok po bloku, takže keď zrušenie zastaví slučku, bitová mapa obsahuje všetko, čo bolo namaľované až do tohto okamihu — čo je čiastočný obrázok: niektoré pásy sú hotové, zvyšok stále zobrazuje farbu výplne. To, či je tento čiastočný výsledok užitočný, závisí od volajúceho. Prehliadač, ktorý chystá zahodiť bitovú mapu, pretože používateľ prešiel na iné miesto, ju môže jednoducho ignorovať. Prehliadač, ktorý chce zobraziť lacný náhľad, ju môže ponechať. Čo nesmie urobiť, je predpokladať, že prsCancelled naznačuje prázdnu alebo nedefinovanú bitovú mapu; naznačuje pravdivý snímok nedokončeného vykresľovania
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;
Token nil a cesta spätného volania bez vetvenia
Zrušenie je voliteľné. Volajúci, ktorý chce len progresívne vykresľovanie kvôli výhode spracovania správ bez úmyslu prerušiť, by mal byť schopný odovzdať nil pre token. Naivným spôsobom, ako to podporiť, je rozptýliť kontroly „ak bol token poskytnutý" v spätnom volaní a slučke, čo znamená vetvu pri každom bloku a spätné volanie, ktoré musí zvládnuť skutočný token aj jeho absenciu
Implementácia sa tomu vyhne nahradením singletonu, keď volajúci nič neodovzdá. Token nil je nahradený za PdfNoCancellationToken, rozhranie, ktorého IsCancelled je vždy false. Od tohto bodu má spätné volanie aj slučka token na dopytovanie v každom prípade, takže ani jedno nepotrebuje kontrolu na nil a ani jedno nepotrebuje špeciálnu cestu. Token „nikdy nezrušiť" jednoducho vždy odpovedá false, spätné volanie vždy vracia nulu a vykresľovanie prebehne do konca presne tak, ako by to urobilo nevypínateľné. Voliteľné správanie je modelované ako token, ktorý sa nikdy nespustí, nie ako absencia tokenu, čo udržuje horúcu cestu jednotnou
// 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;
Výsledný tvar je malý a stojí za zopakovanie, pretože je to opakovateľne použiteľná časť. Knižnica C, ktorá podporuje spätné volanie, vám poskytuje presne jeden kanál na odovzdanie stavu do tohto spätného volania — nepriehľadný ukazovateľ user. Vložte počítaný odkaz na pascalové rozhranie za tento ukazovateľ, udržte druhý skutočný odkaz nažive vedľa štruktúry, aby objekt nemohol byť zhromažďovaný počas volania, a prečítajte rozhranie späť vo vnútri statickej funkcie cdecl. Obaľte celú slučku riadenia do bloku try a uvoľnite natívny kontext v bloku finally. Rovnaká šablóna sa prenáša na akúkoľvek progresívnu alebo spätným volaním riadenú operáciu PDFium, kde pascalový kód musí zostať v kontrole životnosti, kým C drží ukazovateľ
Zrušenie je len jednou polovicou responzívneho prehliadača. Druhou polovicou je nevykreslenie znova stránok, ktoré ste už nakreslili, a plynulé udržiavanie priblíženia a posúvania pomocou obsluhovania uložených bitových máp, čo je popísané v našom článku o vyrovnávacej pamäti vykresľovania a výkone priblíženia. Ako zrušiteľné vykresľovanie zapadá do kompletného prehliadača spolu s navigáciou, výberom a vyhľadávaním, pozrite si vytvorenie funkciami bohatého prehliadača PDF s PDFium VCL komponentom. Progresívne vykresľovanie popísané tu je dodávané ako súčasť PDFium Component pre Delphi a Lazarus spolu s rozhraním API načítavania, vykresľovania a formulárov, ktoré sú popísané inde na tomto blogu