Cele mai multe pagini PDF se rasterizează în câteva milisecunde și nu vă gândiți niciodată la asta. Apoi un utilizator deschide un desen tehnic A1, o pagină împachetată cu zeci de mii de trasee vectoriale, sau un poster aglomerat cu grupuri de transparență și măști soft, iar apelul singular care îl pictează durează două sau trei secunde. Dacă acel apel rulează pe firul UI, fereastra încetează să se repicteze, bara de titlu se îngroașă, iar sistemul de operare oferă să termine aplicația. Munca este legitimă. Pagina chiar are nevoie de atât timp. Defectul este că randarea este un apel de blocare individual, fără nicio modalitate de a lua aer și fără nicio modalitate de a opri
Acest articol tratează exact una dintre acele două probleme: anularea unei randări lungi de o singură pagină fără înghețarea UI-ului. Utilizatorul a apăsat pe pagina următoare, sau a mărit/micșorat, sau a închis documentul, iar randarea în desfășurare este acum muncă irosită care ar trebui să se termine la prima oportunitate în loc să ruleze până la finalizare. Netezirea derulării și a zoom-ului prin memorarea în cache a ceea ce a fost deja rasterizat este o preocupare separată cu propriul proiect, acoperită în articolul companion legat la final. Aici singura întrebare este cum să faci o randare progresivă să răspundă rapid și curat la o cerere de anulare
API-ul de randare progresivă pe care PDFium îl livrează deja
PDFium a anticipat jumătatea de înghețare a problemei. Alături de FPDF_RenderPageBitmap cu o singură lovitură, expune o variantă progresivă care împarte o pagină în bucăți de muncă. Apelați FPDF_RenderPageBitmap_Start o dată pentru a configura randarea față de un bitmap destinație, apoi apelați FPDF_RenderPage_Continue în mod repetat. Fiecare Continue rasterizează o felie delimitată și returnează un status. FPDF_RENDER_TOBECONTINUED înseamnă că mai este de făcut, FPDF_RENDER_DONE înseamnă că pagina este terminată, și FPDF_RENDER_FAILED înseamnă că s-a oprit pe o eroare. Când bucla se termină, apelați FPDF_RenderPage_Close pentru a elibera starea progresivă per pagină. Deoarece controlul revine la codul dvs. între felii, puteți pompa mesaje, actualiza un indicator de progres sau verifica dacă munca este încă dorită
Mecanismul pe care PDFium îl oferă pentru a decide când să cedeze este o structură callback numită IFSDK_PAUSE. O înmânați la Start și la fiecare Continue. După fiecare bucată, PDFium apelează pointer-ul funcției sale NeedToPauseNow, iar dacă acesta returnează o valoare nenulă, Continue-ul curent se oprește devreme și predă controlul înapoi cu FPDF_RENDER_TOBECONTINUED. Structura mai conține un câmp version, care trebuie setat la 1, și un pointer liber user pe care PDFium nu îl atinge niciodată și îl trece neatins. Acel pointer neatins este toată balamaua proiectului care urmează
Reutilizarea pauzei ca anulare
Intenția originală a lui NeedToPauseNow este felierea în timp. Returnați nenul când bugetul cadrului dvs. este epuizat, returnați zero pentru a continua randarea, iar PDFium face pauză astfel încât puteți face altceva înainte de a relua aceeași randare. PDFium Component reutilizează acel semnal pentru un verb diferit. În loc să răspundă „ar trebui să fac pauză și să vă las să reluați", callback-ul răspunde „a fost anulată această muncă". Cele două se mapează una pe cealaltă în mod curat datorită a ceea ce face bucla când vede indicatorul. O pauză autentică se așteaptă la un Continue ulterior; o anulare nu. Odată ce bucla apelantă observă că token-ul este anulat, închide contextul de randare și nu mai apelează niciodată Continue, deci același return nenul pe care PDFium îl citește ca „oprește această bucată" devine, în esență, „oprește pentru totdeauna"
Anularea este exprimată printr-o interfață, IPdfCancellationToken, a cărei proprietate IsCancelled trece de la false la true când o altă parte a programului solicită oprirea randării. Puntea între acea interfață Pascal și callback-ul C al PDFium este un singur pointer. Referința de interfață a token-ului este scrisă în IFSDK_PAUSE.user, iar un callback static cdecl o citește înapoi și o interoghează. Aceasta este problema clasică de a lăsa o bibliotecă C să apeleze înapoi în Pascal: callback-ul trebuie să fie o funcție simplă cu convenție de apel C, nu o metodă, deoarece PDFium stochează și invocă un pointer de funcție simplu care nu știe nimic despre obiecte Pascal sau 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;
Callback-ul recuperează token-ul prin cast-area lui pThis^.user înapoi la tipul de interfață și citește IsCancelled. Nimic în el nu alocă, blochează sau se oprește, ceea ce contează deoarece PDFium îl apelează pe firul de randare după fiecare bucată, iar orice muncă efectuată aici se adaugă la costul randării în sine. Garda împotriva unui struct nil sau a unui câmp user nil înseamnă că aceeași funcție este sigur de instalat chiar și pe o randare care nu a primit niciodată un token real
Menținerea token-ului în viață pe parcursul buclei
Cast-area unui pointer de interfață printr-un Pointer brut și înapoi este locul unde se nasc erorile de durată de viață. Un IInterface în Delphi este numărat prin referință, iar contorul se mișcă numai când compilatorul poate vedea că o variabilă tipizată ca interfață este atribuită. Stocarea token-ului exclusiv ca pointer simplu în interiorul IFSDK_PAUSE.user l-ar ascunde complet de la contorul de referință. Dacă singura altă referință la acel token a ieșit din domeniu în timp ce bucla Continue era încă în rulare, obiectul ar fi eliberat sub callback, iar bucata următoare ar dereferenția un pointer suspendat
De aceea descriptorul este o înregistrare care conține două lucruri, nu unul. Câmpul Pause este structura pe care PDFium o citește. Câmpul Token este o referință reală tipizată ca interfață pe care compilatorul o numără, și există pentru niciun alt motiv decât să fixeze token-ul în memorie atâta timp cât înregistrarea trăiește. Înregistrarea este o variabilă locală pe stiva rutinei de randare, deci rămâne validă pe toată durata buclei și este distrusă numai când rutina iese. Pointer-ul simplu din user și referința numărată din Token denumesc același obiect; unul este ceea ce PDFium poate citi, celălalt este ceea ce menține acel obiect să nu fie colectat
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);
Închiderea contextului de randare indiferent cum se termină bucla
Fiecare apel la FPDF_RenderPageBitmap_Start alocă starea progresivă pe care PDFium o asociază cu pagina, iar acea stare este eliberată numai de FPDF_RenderPage_Close. Există trei căi de ieșire din bucla de drive. Pagina se termină și ultimul status este FPDF_RENDER_DONE. Token-ul se declanșează și bucla iese devreme raportând anularea. Ceva eșuează și statusul este FPDF_RENDER_FAILED. Toate trei trebuie să apeleze Close, iar calea de anulare este cea mai ușor de greșit, deoarece forma naturală a „vezii anularea, ieșiți" tinde să sară curățenia pe drumul spre ieșire. Lăsând Close neatins, se scurge starea per pagină, iar un vizualizator care permite utilizatorului să anuleze randare după randare ar acumula acea scurgere pe fiecare pagină abandonată
Forma robustă pune bucla și clasificarea rezultatelor în interiorul unui try și FPDF_RenderPage_Close în blocul finally potrivit. Bitmap-ul destinație este distrus în același bloc. Anularea poate ieși din buclă printr-un Exit devreme și finally tot rulează, deci există exact un singur loc care eliberează starea progresivă și nu poate fi ocolit
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;
Bucla verifică token-ul înainte de fiecare Continue, pe lângă că se bazează pe callback-ul din interiorul lui. Callback-ul scurtează bucata curentă; verificarea buclei oprește pe cea următoare de la a porni. Împreună, ele limitează cât durează un cancel să intre în vigoare la aproximativ durata unei singure bucăți
Trei rezultate și ce conține bitmap-ul după o anulare
Punctul de intrare public este TPdf.RenderPageProgressive, și returnează un TPdfProgressiveStatus care este unul dintre prsDone, prsCancelled sau prsFailed. Valorile oglindesc constantele FPDF_RENDER_* ale PDFium în idiomul Pascal, dar includ cazul de anulare ca rezultat de prim rang, nu ca eroare
Punctul care surprinde oamenii este ce conține bitmap-ul destinație după prsCancelled. Nu este gol. PDFium randează progresiv în același bitmap bucată după bucată, deci când o anulare oprește bucla, bitmap-ul conține tot ceea ce a fost pictat până în acel moment, care este o imagine parțială: unele benzi terminate, restul încă afișând culoarea de umplere. Dacă acel rezultat parțial este util depinde de apelant. Un vizualizator care urmează să arunce bitmap-ul deoarece utilizatorul a navigat în altă parte poate pur și simplu să-l ignore. Un vizualizator care vrea să afișeze o previzualizare ieftină poate să-l păstreze. Ce nu trebuie să faceți este să presupuneți că prsCancelled implică un bitmap gol sau nedefinit; implică un instantaneu fidel al unei randări neterminate
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-ul nil și o cale de callback fără ramificații
Anularea este opt-in. Un apelant care vrea doar randare progresivă pentru beneficiul pompării mesajelor, fără intenția de a abandona, ar trebui să poată trece nil pentru token. Modul naiv de a susține aceasta este să împrăștii verificări „dacă a fost furnizat un token" prin callback și buclă, ceea ce înseamnă o ramificație pe fiecare bucată și un callback care trebuie să gestioneze atât un token real cât și absența lui
Implementarea evită aceasta prin substituirea unui singleton când apelantul nu trece nimic. Un token nil este schimbat cu PdfNoCancellationToken, o interfață al cărei IsCancelled este întotdeauna false. De la acel punct, callback-ul și bucla au un token de interogat în fiecare caz, deci nici unul nu are nevoie de o verificare nil și nici unul nu are nevoie de o cale specială. Token-ul care nu anulează niciodată pur și simplu răspunde mereu false, callback-ul returnează întotdeauna zero, iar randarea rulează până la finalizare exact ca una non-anulabilă. Comportamentul opțional este modelat ca un token care nu se declanșează niciodată, nu ca absența unui token, ceea ce menține calea rapidă 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;
Forma care emerge este mică și merită restabilită, deoarece este partea reutilizabilă. O bibliotecă C care suportă un callback vă oferă exact un canal pentru a trece starea în acel callback, pointer-ul opac de utilizator. Puneți o referință de interfață Pascal numărată în spatele acelui pointer, mențineți o a doua referință reală vie lângă struct astfel încât obiectul să nu poată fi colectat în mijlocul apelului, și citiți interfața înapoi în interiorul unei funcții statice cdecl. Înfășurați întreaga buclă de drive într-un try și eliberați contextul nativ în finally. Același șablon se aplică oricărei operații PDFium progresive sau bazate pe callback unde codul Pascal trebuie să rămână în controlul duratei de viață în timp ce C deține un pointer
Anularea este numai jumătate dintr-un vizualizator reactiv. Cealaltă jumătate este să nu re-randați pagini pe care le-ați desenat deja, și să mențineți zoom-ul și derularea fluide prin servirea bitmap-urilor memorare în cache, ceea ce este acoperit în articolul nostru despre memorarea în cache a randărilor și performanța la zoom. Pentru cum se integrează randarea anulabilă într-un vizualizator complet alături de navigare, selecție și căutare, vedeți construirea unui vizualizator PDF bogat în funcții cu componenta PDFium VCL. Randarea progresivă descrisă aici este livrată ca parte a componentei PDFium Component pentru Delphi și Lazarus alături de API-urile de încărcare, randare și formulare acoperite în altă parte pe acest blog