Dauguma PDF puslapių rasterizuojami per kelias milisekundes ir jūs niekada apie tai negalvojate. Tada vartotojas atidaro A1 inžinerinį brėžinį – puslapį, pilną dešimčių tūkstančių vektorinių potėpių (strokes), arba plakatą, perpildytą skaidrumo grupių (transparency groups) ir minkštų kaukių (soft masks), ir tas vienas iškvietimas, kuris jį nupiešia, užtrunka dvi ar tris sekundes. Jei tas iškvietimas veikia UI gijoje (UI thread), langas nustoja persipiešti, pavadinimo juosta papilkėja, o operacinė sistema pasiūlo nužudyti programą. Darbas yra teisėtas. Puslapiui tikrai reikia tiek laiko. Defektas yra tas, kad atvaizdavimas (render) yra vienas nedalomas blokuojantis iškvietimas be jokio būdo įkvėpti oro ir be jokio būdo sustoti
Šis straipsnis yra būtent apie vieną iš tų dviejų problemų: ilgo vieno puslapio atvaizdavimo atšaukimą neužšaldant UI. Vartotojas spustelėjo kitą puslapį, arba pakeitė mastelį (zoomed), arba uždarė dokumentą, ir vykstantis atvaizdavimas (render in flight) dabar yra veltui švaistomas darbas, kuris turėtų baigtis artimiausia proga, užuot veikęs iki galo. Slinkimo (scroll) ir mastelio keitimo išlyginimas talpinant tai, kas jau buvo rasterizuota (caching), yra atskiras klausimas su savo dizainu, aptariamas straipsnyje, nuoroda į kurį pateikiama pabaigoje. Čia vienintelis klausimas yra tas, kaip priversti vieną progresyvų atvaizdavimą greitai ir švariai atsakyti į atšaukimo užklausą
Progresyvaus atvaizdavimo API, kurią PDFium jau teikia
PDFium numatė problemos pusę dėl užšalimo. Kartu su vienkartiniu (one-shot) FPDF_RenderPageBitmap, ji atskleidžia progresyvų variantą, kuris padalija puslapį į darbo gabalus (chunks). Jūs iškviečiate FPDF_RenderPageBitmap_Start vieną kartą, kad paruoštumėte atvaizdavimą į paskirties bitų žemėlapį (destination bitmap), o tada pakartotinai iškviečiate FPDF_RenderPage_Continue. Kiekvienas Continue rasterizuoja ribotą griežinėlį (slice) ir grąžina būseną. FPDF_RENDER_TOBECONTINUED reiškia, kad yra daugiau ką daryti, FPDF_RENDER_DONE reiškia, kad puslapis baigtas, o FPDF_RENDER_FAILED reiškia, kad jis sustojo dėl klaidos. Kai ciklas baigiasi, jūs iškviečiate FPDF_RenderPage_Close, kad atlaisvintumėte progresyvią puslapio būseną. Kadangi valdymas grįžta jūsų kodui tarp griežinėlių, galite pumpuoti pranešimus (pump messages), atnaujinti progreso indikatorių arba patikrinti, ar darbo vis dar norima
Mechanizmas, kurį PDFium suteikia sprendžiant, kada užleisti vietą (yield), yra atgalinio iškvietimo (callback) struktūra, pavadinta IFSDK_PAUSE. Jūs perduodate ją į Start ir į kiekvieną Continue. Po kiekvieno gabalo PDFium iškviečia jo NeedToPauseNow funkcijos rodyklę, ir jei ji grąžina nenulinę reikšmę, dabartinis Continue sustoja anksti ir perduoda valdymą atgal su FPDF_RENDER_TOBECONTINUED. Struktūra taip pat turi lauką version, kuris turi būti nustatytas į 1, ir laisvos formos rodyklę user, kurios PDFium niekada neliečia ir perduoda nepaliestą. Ta nepaliesta rodyklė yra visos tolesnės konstrukcijos ašis
Pauzės (pause) pritaikymas kaip atšaukimo (cancel)
Pradinis NeedToPauseNow tikslas yra laiko dalijimas (time-slicing). Grąžinkite nenulinę reikšmę, kai jūsų kadro biudžetas išnaudotas, grąžinkite nulį, kad tęstumėte atvaizdavimą, ir PDFium padaro pauzę, kad galėtumėte padaryti kažką kita prieš atnaujindami tą patį atvaizdavimą. PDFium komponentas pakartotinai naudoja tą patį signalą kitam veiksmažodžiui. Užuot atsakęs „ar turėčiau padaryti pauzę ir leisti tau atnaujinti“, atgalinis iškvietimas atsako „ar šis darbas buvo atšauktas“. Šiedu dalykai švariai sutampa dėl to, ką ciklas daro matydamas vėliavėlę. Tikra pauzė tikisi vėlesnio Continue; atšaukimas – ne. Kai iškviestasis ciklas (calling loop) pastebi, kad žetonas yra atšauktas, jis uždaro atvaizdavimo kontekstą ir niekada daugiau nebekviečia Continue, todėl tas pats nenulinis grąžinimas, kurį PDFium skaito kaip „sustabdyk šį gabalą“, iš tikrųjų tampa „sustabdyk visam laikui“
Atšaukimas išreiškiamas per sąsają (interface) IPdfCancellationToken, kurios savybė IsCancelled persijungia iš false į true, kai kuri nors kita programos dalis paprašo atvaizdavimą sustabdyti. Tiltas tarp tos Pascal sąsajos ir PDFium C atgalinio iškvietimo yra viena rodyklė. Žetono sąsajos nuoroda įrašoma į IFSDK_PAUSE.user, o statinis cdecl atgalinis iškvietimas jį perskaito ir apklausia. Tai klasikinė problema, leidžiant C bibliotekai iškviesti atgal į Pascal: atgalinis iškvietimas turi būti paprasta funkcija su C iškvietimo konvencija (calling convention), o ne metodas, nes PDFium saugo ir iškviečia pliką funkcijos rodyklę (bare function pointer), kuri nieko nežino apie Pascal objektus ar 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;
Atgalinis iškvietimas atkuria žetoną paversdamas (casting) pThis^.user atgal į sąsajos tipą ir perskaito IsCancelled. Niekas jame nepriskiria atminties (allocates), nerakina ar neblokuoja, o tai svarbu, nes PDFium iškviečia jį atvaizdavimo gijoje po kiekvieno gabalo ir bet koks čia atliktas darbas pridedamas prie paties atvaizdavimo kainos. Apsauga nuo nil struktūros arba nil user lauko reiškia, kad tą pačią funkciją saugu įdiegti net ir atvaizdavimui, kuriam niekada nebuvo duotas tikras žetonas
Žetono išlaikymas gyvo viso ciklo metu
Sąsajos rodyklės pavertimas (casting) per grynają rodyklę (raw Pointer) ir atgal yra ten, kur gimsta gyvavimo laiko (lifetime) klaidos. Delphi sąsaja IInterface yra skaičiuojama nuorodomis (reference counted), ir skaičius keičiasi tik tada, kai kompiliatorius mato, kad priskiriamas sąsajos tipo kintamasis. Saugant žetoną vien kaip pliką rodyklę IFSDK_PAUSE.user viduje, jis būtų visiškai paslėptas nuo nuorodų skaitiklio. Jei vienintelė kita nuoroda į tą žetoną išeitų iš apimties (went out of scope) kol Continue ciklas vis dar veikė, objektas būtų atlaisvintas iš po atgalinio iškvietimo, ir kitas gabalas dereferencijuotų (dereference) kabančią rodyklę (dangling pointer)
Štai kodėl deskriptorius (descriptor) yra įrašas (record), turintis du dalykus, o ne vieną. Pause laukas yra struktūra, kurią skaito PDFium. Token laukas yra tikra sąsajos tipo nuoroda, kurią kompiliatorius skaičiuoja, ir jis egzistuoja vien tam, kad prisegtų (pin) žetoną atmintyje tol, kol gyvena įrašas. Įrašas yra vietinis kintamasis atvaizdavimo procedūros steke (stack), todėl jis išlieka galiojantis visą ciklo trukmę ir yra sunaikinamas tik tada, kai procedūra išeina. Plika rodyklė user lauke ir skaičiuojama nuoroda Token lauke vadina tą patį objektą; vienas yra tai, ką PDFium gali skaityti, kitas yra tai, kas neleidžia to objekto surinkti (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);
Atvaizdavimo konteksto uždarymas, nesvarbu kaip baigiasi ciklas
Kiekvienas FPDF_RenderPageBitmap_Start iškvietimas priskiria progresyvią būseną, kurią PDFium susieja su puslapiu, ir ta būsena atlaisvinama tik per FPDF_RenderPage_Close. Iš varomojo ciklo (drive loop) yra trys išėjimai. Puslapis baigiasi ir paskutinė būsena yra FPDF_RENDER_DONE. Žetonas suveikia (trips) ir ciklas išeina anksti pranešdamas apie atšaukimą. Kažkas nepavyksta ir būsena yra FPDF_RENDER_FAILED. Visi trys turi iškviesti Close, o atšaukimo kelią lengviausia padaryti neteisingai, nes natūrali „matai atšaukimą, ištrūk“ (see cancel, break out) forma linkusi praleisti valymą (cleanup) pakeliui į išėjimą. Palikus Close nepasiektą, nuteka (leaks) kiekvieno puslapio būsena, o peržiūros programa, leidžianti vartotojui atšaukti vieną atvaizdavimą po kito, kauptų šį nutekėjimą kiekviename nutrauktame puslapyje
Tvirta (robust) forma patalpina ciklą ir rezultato klasifikaciją try bloke, o FPDF_RenderPage_Close – atitinkamame finally. Paskirties bitų žemėlapis sunaikinamas tame pačiame bloke. Atšaukimas gali išeiti iš ciklo per ankstyvą Exit, ir finally vis tiek vykdomas, todėl yra lygiai viena vieta, kuri atlaisvina progresyvią būseną, ir jos negalima apeiti
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;
Ciklas tikrina žetoną prieš kiekvieną Continue, be to, pasikliauja atgaliniu iškvietimu jo viduje. Atgalinis iškvietimas sutrumpina dabartinį gabalą; ciklo patikrinimas sustabdo kito pradžią. Kartu jie apriboja laiką, per kurį atšaukimas įsigalioja, iki maždaug vieno gabalo trukmės
Trys baigtys, ir ką bitų žemėlapis saugo po atšaukimo
Viešas įėjimo taškas yra TPdf.RenderPageProgressive, ir jis grąžina TPdfProgressiveStatus, kuris yra vienas iš prsDone, prsCancelled arba prsFailed. Reikšmės atspindi PDFium FPDF_RENDER_* konstantas Pascal idiomoje, bet atšaukimo atvejį prideda kaip pirmaklasį (first-class) rezultatą, o ne kaip klaidą
Dalykas, kuris pagauna žmones, yra tai, ką paskirties bitų žemėlapis talpina po prsCancelled. Jis nėra tuščias. PDFium progresyviai atvaizduoja į tą patį bitų žemėlapį gabalas po gabalo, todėl kai atšaukimas sustabdo ciklą, bitų žemėlapis saugo viską, kas buvo nupiešta iki to momento, kas yra dalinis vaizdas: kai kurios juostos baigtos, likusios vis dar rodo užpildo spalvą. Ar tas dalinis rezultatas naudingas, priklauso nuo iškviestojo (caller). Peržiūros programa, kuri ruošiasi išmesti bitų žemėlapį, nes vartotojas perėjo kitur, gali tiesiog jį ignoruoti. Peržiūros programa, norinti parodyti pigią peržiūrą (low-cost preview), gali jį pasilikti. Ko jūs neturite daryti, tai manyti, kad prsCancelled reiškia tuščią ar neapibrėžtą bitų žemėlapį; tai reiškia teisingą nebaigto atvaizdavimo momentinę nuotrauką (snapshot)
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;
Nulinis žetonas (nil token) ir atgalinio iškvietimo kelias be išsišakojimų
Atšaukimas yra pasirenkamas (opt-in). Iškviestasis (caller), kuris tiesiog nori progresyvaus atvaizdavimo dėl pranešimų pumpavimo naudos, be jokio ketinimo nutraukti operaciją, turėtų galėti perduoti nil vietoje žetono. Naivus būdas tai palaikyti yra išmėtyti „jei buvo pateiktas žetonas“ patikrinimus atgaliniame iškvietime ir cikle, o tai reiškia išsišakojimą (branch) kiekviename gabale ir atgalinį iškvietimą, kuris turi apdoroti ir tikrą žetoną, ir jo nebuvimą
Realizacija to išvengia pakeisdama jį vienatiniu (singleton) objektu, kai iškviestasis perduoda nieko. nil žetonas pakeičiamas į PdfNoCancellationToken – sąsają, kurios IsCancelled visada yra false. Nuo to momento atgalinis iškvietimas ir ciklas kiekvienu atveju turi žetoną, kurį gali apklausti, todėl nė vienam nereikia nil patikrinimo ir specialaus kelio. Niekada neatšaukiamas žetonas tiesiog visada atsako false, atgalinis iškvietimas visada grąžina nulį, o atvaizdavimas veikia iki galo lygiai taip pat, kaip ir neatšaukiamas. Pasirenkamas elgesys modeliuojamas kaip žetonas, kuris niekada nesuveikia, o ne kaip žetono nebuvimas, todėl karštasis kelias (hot path) išlieka vienodas
// 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;
Išryškėjanti forma yra maža ir verta pakartoti, nes tai yra pakartotinai panaudojama dalis. C biblioteka, kuri palaiko atgalinį iškvietimą, suteikia jums lygiai vieną kanalą būsenai į tą atgalinį iškvietimą perduoti – nepermatomą vartotojo rodyklę (opaque user pointer). Už tos rodyklės įdėkite skaičiuojamą Pascal sąsajos nuorodą (counted Pascal interface reference), šalia struktūros išlaikykite gyvą antrą tikrąją nuorodą, kad objektas nebūtų surinktas (collected) iškvietimo viduryje, ir atgal nuskaitykite sąsają statinėje cdecl funkcijoje. Apgaubkite visą varomąjį ciklą try bloke, o natūralų (native) kontekstą atlaisvinkite per finally. Tas pats šablonas perkeliamas į bet kurią progresyvią arba atgaliniu iškvietimu grįstą PDFium operaciją, kur Pascal kodas turi išlaikyti gyvavimo laiko (lifetime) kontrolę, kol C laiko rodyklę
Atšaukimas yra tik pusė interaktyvios peržiūros programos. Kita pusė yra atvaizduotų puslapių pakartotinio atvaizdavimo (re-rendering) vengimas, bei sklandaus mastelio keitimo ir slinkimo užtikrinimas pateikiant talpykloje esančius (cached) bitų žemėlapius, kas aptariama mūsų straipsnyje apie atvaizdavimo talpyklą (render caching) ir mastelio keitimo našumą. Norėdami sužinoti, kaip atšaukiamas atvaizdavimas telpa į visą peržiūros programą kartu su navigacija, pasirinkimu ir paieška, žr. funkcijomis turtingos PDF peržiūros programos kūrimas su PDFium VCL komponentu. Čia aprašytas progresyvus atvaizdavimas pateikiamas kaip PDFium komponento, skirto Delphi ir Lazarus, dalis kartu su įkėlimo, atvaizdavimo ir formų API, aprašytais kitur šiame tinklaraštyje