Dauguma PDF puslapių rastruojami per kelias milisekundes ir jūs apie tai net nepagalvojate. Tačiau kai vartotojas atidaro A1 formato inžinerinį brėžinį, puslapį, pilną dešimčių tūkstančių vektorinių potėpių, arba plakatą, perpildytą skaidrumo grupių ir švelnių kaukių, vienintelis iškvietimas, kuris jį nupiešia, trunka dvi ar tris sekundes. Jei tas iškvietimas vykdomas vartotojo sąsajos gijoje (UI thread), langas nustoja persipiešti, pavadinimo juosta tampa pilka, o operacinė sistema pasiūlo uždaryti programą. Darbas yra teisėtas. Puslapiui tikrai reikia tiek laiko. Trūkumas yra tas, kad atvaizdavimas yra vienas nedalomas blokuojantis iškvietimas, be jokios galimybės atsikvėpti ir jokio būdo sustoti
Šis straipsnis yra būtent apie vieną iš šių dviejų problemų: ilgo vieno puslapio atvaizdavimo atšaukimą neužšaldant vartotojo sąsajos. Vartotojas spustelėjo kitą puslapį, padidino vaizdą arba uždarė dokumentą, ir tuo metu vykstantis atvaizdavimas dabar yra perniek nuėjęs darbas, kuris turėtų būti baigtas pasitaikius pirmai progai, o ne vykdomas iki pabaigos. Slinkimo ir mastelio keitimo sušvelninimas (smoothing) talpinant į podėlį tai, kas jau buvo rastruota, yra atskiras klausimas su savo dizainu, aptartas susijusiame straipsnyje, kurio nuoroda pateikta pabaigoje. Čia vienintelis klausimas yra tas, kaip priversti vieną laipsnišką atvaizdavimą greitai ir švariai atsakyti į atšaukimo užklausą
Laipsniško atvaizdavimo API, kurią PDFium jau teikia
PDFium numatė su užšaldymu susijusią problemos pusę. Kartu su vienkartiniu FPDF_RenderPageBitmap, jis pateikia laipsnišką variantą, kuris padalija puslapį į darbo dalis (chunks). Jūs iškviečiate FPDF_RenderPageBitmap_Start vieną kartą, kad nustatytumėte atvaizdavimą į tikslinę bitų schemą (bitmap), tada pakartotinai iškviečiate FPDF_RenderPage_Continue. Kiekvienas Continue rastruoja apribotą dalį ir grąžina būseną. FPDF_RENDER_TOBECONTINUED reiškia, kad yra daugiau darbo, FPDF_RENDER_DONE reiškia, kad puslapis baigtas, o FPDF_RENDER_FAILED reiškia, kad jis sustojo dėl klaidos. Kai ciklas baigiasi, iškviečiate FPDF_RenderPage_Close, kad atlaisvintumėte laipsnišką būseną, tenkančią vienam puslapiui. Kadangi valdymas grįžta jūsų kodui tarp dalių, galite apdoroti pranešimus, atnaujinti eigos indikatorių arba patikrinti, ar darbas vis dar reikalingas
Mechanizmas, kurį PDFium suteikia norint nuspręsti, kada užleisti vietą (yield), yra atgalinio iškvietimo (callback) struktūra, pavadinta IFSDK_PAUSE. Jūs perduodate ją funkcijai Start ir kiekvienam Continue. Po kiekvienos dalies PDFium iškviečia jos funkcijos rodyklę NeedToPauseNow, ir jei ji grąžina nenulinę reikšmę, dabartinis Continue sustoja anksčiau ir grąžina valdymą 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 nepakeistą. Ši nepaliesta rodyklė yra visos toliau aprašytos architektūros ašis
Pauzės panaudojimas atšaukimui
Pradinis NeedToPauseNow tikslas yra laiko skirstymas (time-slicing). Grąžinkite nenulinę reikšmę, kai jūsų kadrų biudžetas išnaudotas, grąžinkite nulį, kad tęstumėte atvaizdavimą, ir PDFium pristabdo darbą, kad galėtumėte padaryti ką nors kita prieš atnaujindami tą patį atvaizdavimą. „PDFium Component“ pakartotinai naudoja tą patį signalą kitam veiksmui. Užuot atsakęs „ar turėčiau padaryti pauzę ir leisti tau tęsti“, atgalinis iškvietimas atsako „ar šis darbas buvo atšauktas“. Šie du dalykai švariai sutampa dėl to, ką daro ciklas pamatęs vėliavėlę. Tikra pauzė tikisi vėlesnio Continue; atšaukimas – ne. Kai iškviečiantis ciklas pastebi, kad žymė (token) yra atšaukta, jis uždaro atvaizdavimo kontekstą ir daugiau niekada neiškviečia Continue, todėl ta pati nenulinė reikšmė, kurią PDFium supranta kaip „sustabdyk šią dalį“, iš tikrųjų tampa „sustabdyk visam laikui“
Atšaukimas išreiškiamas per sąsają IPdfCancellationToken, kurios savybė IsCancelled pasikeičia iš klaidingos (false) į teisingą (true), kai kuri nors kita programos dalis paprašo sustabdyti atvaizdavimą. Tiltas tarp tos Pascal sąsajos ir PDFium C atgalinio iškvietimo yra viena rodyklė. Žymės (token) sąsajos nuoroda įrašoma į IFSDK_PAUSE.user, o statinis cdecl atgalinis iškvietimas ją nuskaito ir užklausia. Tai klasikinė problema leidžiant C bibliotekai iškviesti atgalinį ryšį į Pascal kodą: atgalinis iškvietimas turi būti paprasta funkcija, naudojanti C iškvietimo konvenciją (calling convention), o ne metodas, nes PDFium saugo ir iškviečia paprastą funkcijos rodyklę, 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;
Pervedant (casting) sąsajos rodyklę per neapdorotą Pointer ir atgal atsiranda gyvavimo ciklo (lifetime) klaidos. „Delphi“ programoje IInterface yra su nuorodų skaičiavimu (reference counted), o skaičius kinta tik tada, kai kompiliatorius mato, kad priskiriamas sąsajos tipo kintamasis. Saugant žymę tik kaip paprastą rodyklę IFSDK_PAUSE.user viduje, ji būtų visiškai paslėpta nuo nuorodų skaitiklio. Jei vienintelė kita nuoroda į tą žymę išeitų iš aprėpties (scope), kol vis dar vykdomas Continue ciklas, objektas būtų atlaisvintas atgalinio iškvietimo metu, o kitas žingsnis iškviestų (dereference) kabančią rodyklę (dangling pointer)
Būtent todėl aprašas (descriptor) yra įrašas (record), turintis du dalykus, o ne vieną. Laukas Pause yra struktūra, kurią skaito PDFium. Laukas Token yra tikra sąsajos tipo nuoroda, kurią kompiliatorius skaičiuoja, ir ji egzistuoja tik tam, kad užfiksuotų žymę atmintyje tol, kol gyvuoja įrašas. Įrašas yra vietinis kintamasis atvaizdavimo procedūros steke (stack), todėl jis išlieka galiojantis visą ciklo laiką ir yra panaikinamas tik procedūrai pasibaigus. Paprasta rodyklė user lauke ir skaičiuojama nuoroda lauke Token nurodo tą patį objektą; vieną jų gali skaityti PDFium, o kita neleidžia to objekto surinkti šiukšlių surinkėjui (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, nepaisant to, kaip baigiasi ciklas
Atšaukimo žetonas turi išlikti gyvas per visą atvaizdavimo ciklą, nes atgalinis iškvietimas gali būti paleistas dar prieš grįžtant iš paskutinio PDFium kvietimo. Žetoną laikykite valdomoje būsenoje iki pat konteksto uždarymo ir neleiskite jo atlaisvinti išankstiniam išėjimui
Kiekvienas FPDF_RenderPageBitmap_Start iškvietimas paskiria laipsnišką būseną, kurią PDFium susieja su puslapiu, ir tą būseną atlaisvina tik FPDF_RenderPage_Close. Yra trys išėjimo iš varančiojo ciklo (drive loop) būdai. Puslapis baigiamas ir paskutinė būsena yra FPDF_RENDER_DONE. Žymė suveikia ir ciklas išeina anksčiau laiko pranešdamas apie atšaukimą. Kažkas nepavyksta ir būsena yra FPDF_RENDER_FAILED. Visi trys atvejai turi iškviesti Close, o atšaukimo kelyje lengviausia suklysti, nes natūrali „pamatyti atšaukimą, išeiti“ forma linkusi praleisti išvalymą pakeliui link išėjimo. Nepasiekus Close, įvyksta puslapio būsenos atminties nuotėkis (leak), ir peržiūros programa, leidžianti vartotojui atšaukti atvaizdavimą vieną po kito, kauptų šį nuotėkį su kiekvienu nutrauktu puslapiu
Saugi struktūra (robust shape) patalpina ciklą ir rezultato klasifikavimą į try bloką, o FPDF_RenderPage_Close į atitinkamą finally bloką. Tikslinė bitų schema (bitmap) sunaikinama tame pačiame bloke. Atšaukimas gali palikti ciklą per ankstyvą Exit, o finally vis tiek vykdomas, todėl yra lygiai viena vieta, atlaisvinanti laipsnišką būseną, kurios neįmanoma aplenkti
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 patikrina žymę prieš kiekvieną Continue, taip pat remiasi atgaliniu iškvietimu jo viduje. Atgalinis iškvietimas sutrumpina dabartinę dalį (chunk); ciklo patikrinimas neleidžia prasidėti kitai daliai. Kartu jie apriboja laiką, per kurį atšaukimas įsigalioja, maždaug iki vienos dalies trukmės
Trys baigtys ir ką bitų schema išsaugo po atšaukimo
Viešasis įėjimo taškas yra TPdf.RenderPageProgressive, ir jis grąžina TPdfProgressiveStatus, kuri yra viena iš prsDone, prsCancelled arba prsFailed. Šios reikšmės atspindi PDFium FPDF_RENDER_* konstantas Pascal idiomoje, tačiau atšaukimo atvejis įtraukiamas kaip pirmos klasės rezultatas, o ne kaip klaida
Dalykas, kuris klaidina žmones, yra tai, ką tikslinė bitų schema (bitmap) turi po prsCancelled. Ji nėra tuščia. PDFium laipsniškai atvaizduoja į tą pačią bitų schemą dalis po dalies, todėl kai atšaukimas sustabdo ciklą, bitų schemoje lieka tai, kas buvo nupiešta iki tos akimirkos, tai yra dalinis vaizdas: kai kurios juostos (bands) baigtos, o likusioje dalyje vis dar rodoma užpildymo spalva. Ar tas dalinis rezultatas naudingas, priklauso nuo iškviečiančiojo. Peržiūros programa, kuri ruošiasi išmesti bitų schemą, nes vartotojas perėjo kitur, gali tiesiog ją ignoruoti. Peržiūros programa, norinti parodyti nebrangią peržiūrą (preview), gali ją pasilikti. Ko jūs privalote nedaryti, tai manyti, kad prsCancelled reiškia tuščią ar neapibrėžtą bitų schemą; 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;
Nil žymė ir atgalinio iškvietimo kelias be šakų
Atšaukimas yra pasirenkamas (opt-in). Iškviečiantysis kodo blokas, norintis laipsniško atvaizdavimo tik dėl pranešimų apdorojimo (message-pumping) naudos ir neketinantis nutraukti atvaizdavimo, turėtų galėti perduoti nil kaip žymę. Naivus būdas tai palaikyti – išmėtyti „jei žymė buvo pateikta“ patikrinimus visame atgalinio iškvietimo metode ir cikle, o tai reiškia sąlyginį išsišakojimą (branch) kiekvienai daliai ir atgalinį iškvietimą, kuris turi apdoroti tiek tikrą žymę, tiek jos nebuvimą
Įgyvendinimas to išvengia, pakeisdamas (singleton) vienkartiniu objektu, kai iškviečiantysis nieko neperduoda. nil žymė pakeičiama į PdfNoCancellationToken, sąsają, kurios IsCancelled visada yra klaidinga (false). Nuo to momento atgalinis iškvietimas ir ciklas bet kokiu atveju turi žymę, kurią galima užklausti, todėl nė vienam nereikia nil patikrinimo ir specialaus kelio. Niekada neatšaukianti žymė tiesiog visada atsako klaidingai (false), atgalinis iškvietimas visada grąžina nulį, o atvaizdavimas vykdomas iki pabaigos lygiai taip pat, kaip ir neatšaukiamas. Pasirenkamas elgesys modeliuojamas kaip žymė, kuri niekada nesuveikia, o ne kaip žymės nebuvimas, kas išlaiko intensyviai vykdomą kodą (hot path) vienodą
// 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;
Atsirandanti forma yra maža ir verta pakartoti, nes tai daugkartinio naudojimo (reusable) dalis. C biblioteka, palaikanti atgalinį iškvietimą, suteikia lygiai vieną kanalą būsenos perdavimui į tą atgalinį iškvietimą – neskaidrią (opaque) vartotojo rodyklę. Už šios rodyklės padėkite skaičiuojamą Pascal sąsajos nuorodą, išlaikykite antrą tikrą nuorodą gyvą šalia struktūros, kad objekto nebūtų galima surinkti iškvietimo metu, ir nuskaitykite sąsają atgal statinės cdecl funkcijos viduje. Apgaubkite visą varantįjį ciklą try bloku ir atlaisvinkite gimtąjį (native) kontekstą finally bloke. Tas pats šablonas tinka bet kokiai laipsniškai ar atgaliniais iškvietimais pagrįstai PDFium operacijai, kai Pascal kodas turi kontroliuoti gyvavimo ciklą (lifetime), kol C kodo pusė laiko rodyklę
Atšaukimas yra tik viena reaguojančios (responsive) peržiūros programos pusė. Kita pusė – neatvaizduoti tų puslapių iš naujo, kuriuos jau nupiešėte, ir išlaikyti sklandų mastelio keitimą bei slinkimą pateikiant bitų schemas (bitmaps) iš podėlio. Tai aptariama mūsų straipsnyje apie atvaizdavimo podėlį ir mastelio keitimo našumą. Kaip atšaukiamas atvaizdavimas dera pilnoje peržiūros programoje kartu su navigacija, atranka ir paieška, žr. turtingą funkcijomis PDF peržiūros programos kūrimas naudojant „PDFium Component“. Čia aprašytas laipsniškas atvaizdavimas pateikiamas kaip „Delphi“ ir „Lazarus“ skirto PDFium Component dalis kartu su įkėlimo, atvaizdavimo ir formų API, kurios aprašytos kituose šio tinklaraščio straipsniuose