O arhivă scanată poate ajunge la câțiva gigaocteți într-un singur PDF. Un vizualizator care deschide un astfel de fișier dorește de obicei să afișeze o pagină, poate cuprinsul sau o pagină la care utilizatorul a trecut de la un semn de carte (bookmark). Citirea întregului fișier în memorie pentru a reda două pagini este o risipă pe fiecare axă: consumă spațiu de adrese, blochează utilizatorul în spatele unei citiri inițiale lungi, iar pe un proces Delphi pe 32 de biți poate eșua direct înainte de a apărea o singură pagină. PDFium a fost construit având în vedere acest lucru. Acesta poate încărca un document printr-un apel invers care solicită intervalele specifice de octeți de care are nevoie, atunci când are nevoie de ele, și nu solicită niciodată întregul fișier deodată.
Componenta expune acea cale printr-un adaptor de flux. Îi transmiteți orice TStream, iar PDFium preia blocuri din acel flux la cerere. Fișierul se poate afla pe disc, într-un câmp de tip blob de bază de date sau în spatele oricărui alt descendent TStream, și nimic din el nu este copiat în memorie în prealabil.
Cum solicită PDFium octeți
API-ul C al PDFium încarcă un document dintr-un obiect furnizat de apelant descris de structura FPDF_FILEACCESS. Structura are trei părți care contează aici: un câmp de lungime, un apel invers de citire și un parametru opac al utilizatorului. Punctul de intrare care îl consumă este FPDF_LoadCustomDocument. Odată ce PDFium deține acea structură, analizează trailerul, localizează tabelul de referințe încrucișate și, de atunci înainte, citește doar ceea ce necesită o anumită operațiune. Deschiderea documentului atinge sfârșitul fișierului și câteva obiecte de catalog. Redarea paginii 400 citește fluxurile de conținut și resursele pentru acea pagină și nimic altceva.
Aceasta este diferența dintre o încărcare cu buffer și o încărcare în flux (streaming). O încărcare cu buffer citește fișierul cap la cap înainte ca PDFium să vadă octetul zero. O încărcare în flux inversează relația: PDFium conduce citirile, iar octeții care nu sunt atinși nu sunt citiți niciodată. Pentru un fișier de câțiva gigaocteți vizualizat pagină cu pagină, aceasta reprezintă diferența dintre o încărcare inutilizabilă și una instantanee.
Adaptorul de flux
Adaptorul care conectează un TStream Delphi la FPDF_FILEACCESS este TPdfStreamAdapter. Constructorul său preia fluxul și un indicator de proprietate (ownership), captează lungimea fluxului o singură dată, completează înregistrarea FPDF_FILEACCESS și activează apelul invers de citire. Când PDFium apelează ulterior cu un offset și o dimensiune, adaptorul caută în flux la acel offset și copiază exact acel interval în bufferul pe care PDFium l-a furnizat.
// Verbatim from the component: the stream-to-FPDF_FILEACCESS bridge
constructor TPdfStreamAdapter.Create(AStream: TStream; AOwnsStream: Boolean);
begin
inherited Create;
if AStream = nil then
raise EPdfError.Create('TPdfStreamAdapter: AStream is nil');
FStream := AStream;
FOwnsStream := AOwnsStream;
// FPDF_FILEACCESS.m_FileLen is a 32-bit unsigned long. Refuse a stream
// that would silently truncate past 4 GiB.
if AStream.Size > High(FPDF_DWORD) then
raise EPdfError.Create('TPdfStreamAdapter: stream exceeds the 4 GiB limit');
FillChar(FFileAccess, SizeOf(FFileAccess), 0);
FFileAccess.m_FileLen := FPDF_DWORD(AStream.Size);
FFileAccess.m_GetBlock := GetBlockCallback;
FFileAccess.m_Param := Self;
end;
Indicatorul de proprietate decide cine eliberează fluxul. Transmiteți False, iar apelantul păstrează fluxul și trebuie să îl mențină activ pe toată durata de viață a documentului. Transmiteți True, iar adaptorul preia controlul, eliberând fluxul când documentul se închide. Oricum, fluxul trebuie să supraviețuiască fiecărei citiri pe care PDFium o va efectua, deoarece PDFium deține indicatorul FPDF_FILEACCESS și va apela înapoi în orice moment în care documentul este deschis, nu doar în timpul încărcării inițiale.
De ce funcția callback este o funcție statică
Apelul invers de citire pe care PDFium îl stochează în m_GetBlock este un indicator simplu de funcție C cu convenția de apelare cdecl. O metodă Delphi nu poate fi folosită direct, deoarece o metodă poartă un argument ascuns Self despre care un apelant C nu știe nimic și pe care nu îl va furniza niciodată. Adaptorul declară de aceea apelul invers ca o class function marcată cdecl; static, care se compilează la o funcție independentă cu aspectul de cadru C pe care îl așteaptă PDFium și fără un Self implicit.
Aceasta rezolvă convenția de apelare, dar ridică o a doua întrebare: fără Self, how does the callback reach the specific stream it is supposed to read from? Răspunsul este parametrul opac al utilizatorului. Când adaptorul construiește înregistrarea, își stochează propriul indicator de instanță în m_Param. PDFium transmite înapoi același indicator ca prim argument al fiecărui apel invers. Funcția statică îl convertește înapoi la un TPdfStreamAdapter și trimite citirea către fluxul acelei instanțe. Acesta este trambulina standard pentru transmiterea contextului obiectului peste o graniță C care nu are noțiunea de obiecte.
// Verbatim from the component: the cdecl trampoline back to the instance
class function TPdfStreamAdapter.GetBlockCallback(
param : Pointer;
position: FPDF_DWORD;
pBuf : PByte;
size : FPDF_DWORD): Integer; cdecl;
var
Adapter: TPdfStreamAdapter;
begin
Result := 0;
if (param = nil) or (pBuf = nil) or (size = 0) then
Exit;
Adapter := TPdfStreamAdapter(param); // recover the instance from m_Param
if Adapter.FStream = nil then
Exit;
try
Adapter.FStream.Position := Int64(position);
Adapter.FStream.ReadBuffer(pBuf^, Int64(size));
Result := 1;
except
Result := 0; // report failure by return value, never by raising
end;
end;
Plafonul de 4 GiB și de ce are nevoie de o protecție
Câmpul de lungime m_FileLen din FPDF_FILEACCESS este o valoare fără semn pe 32 de biți. Cea mai mare lungime reprezentabilă a sa este cu un octet mai mică de 4 GiB. Un TStream își raportează dimensiunea ca Int64, astfel încât un flux poate descrie mult mai mulți octeți decât poate conține câmpul. În momentul în care dimensiunea unui flux depășește acel plafon, nu există o modalitate corectă de a spune PDFium cât de lung este fișierul.
Răspunsul greșit este să atribuiți dimensiunea și să o lăsați să se depășească. Trunchierea unei lungimi de 5 GiB la un câmp pe 32 de biți produce un număr mic, cu aspect plauzibil, iar PDFium va analiza apoi fișierul crezând că acesta se termină la aproximativ un gigaoctet. Secțiunea trailer și tabelul de referințe încrucișate se află la sfârșitul real al fișierului, mult dincolo de lungimea trunchiată, astfel încât analiza eșuează într-un mod care nu are nimic de-a face cu cauza reală. Ați depana o eroare de referință încrucișată pe un fișier care este perfect valid, fără niciun indiciu că un întreg s-a depășit cu două straturi mai sus.
În schimb, adaptorul respinge datele de intrare. Constructorul cumpără dimensiunea fluxului cu High(FPDF_DWORD) și generează EPdfError în clipa în care fluxul este prea mare pentru a fi descris. O eroare explicită, imediată, numește problema reală în punctul de construire. O trunchiere silențioasă o ascunde în spatele unui simptom înșelător pe care l-ați urmări mult mai târziu. Limita de 4 GiB este o constrângere reală a acestei căi de încărcare, iar lucrul corect este să o expuneți clar, în loc să o mascați cu calcule aritmetice care se întâmplă să se compileze.
Erorile nu trebuie să depășească granița dintre module
O citire poate eșua. Fluxul ar putea fi un obiect bazat pe rețea care expiră, un descriptor de tip blob care a fost închis sub dvs. sau un fișier care a fost trunchiat după deschiderea documentului. Contractul PDFium pentru apelul invers de citire este o valoare de returnare: diferită de zero pentru succes, zero pentru eșec. Este un cadru C și nu are niciun mecanism pentru a prinde sau propaga o excepție Pascal.
Acesta este motivul pentru care trambulina îmbracă poziționarea și citirea într-un bloc try/except care înghite excepția și returnează zero. Dacă unei excepții Delphi i s-ar permite să se propage în afara apelului invers, s-ar desfășura prin cadrele de stivă cdecl ale PDFium, care nu au fost niciodată construite pentru a fi derulate de mecanismul de excepții Pascal. Rezultatul este un comportament nedefinit în cel mai bun caz și un blocaj sever în cel mai rău caz, profund în interiorul analizorului PDF, fără o stivă utilizabilă. Returnarea valorii zero menține eșecul în cadrul contractului. PDFium vede o citire de bloc eșuată, abandonează operațiunea curat, iar FPDF_LoadCustomDocument raportează că documentul nu a putut fi încărcat, ceea ce componenta expune ca un EPdfError pe partea Pascal, unde îi este locul.
Deschiderea unui document în acest mod
Metoda componentei care conduce calea de redare în flux este LoadCustomDocument, declarată ca o metodă distinctă în loc de o altă supraîncărcare (overload) LoadDocument, astfel încât transmiterea unui TMemoryStream nu ajunge niciodată accidental pe calea cu buffer. Aceasta construiește adaptorul, apelează FPDF_LoadCustomDocument și menține adaptorul activ pe durata de viață a documentului încărcat.
var
Pdf: TPdf;
FileStream: TFileStream;
begin
Pdf := TPdf.Create(nil);
FileStream := TFileStream.Create('Archive_4GB.pdf', fmOpenRead or fmShareDenyWrite);
try
// Hand stream ownership to Pdf: it frees FileStream when the document closes.
Pdf.LoadCustomDocument(FileStream, True);
// PDFium has read only the trailer and catalog so far.
// Rendering a page pulls just that page's bytes through the callback.
// ... render or inspect pages here ...
finally
Pdf.Free; // closes the document, which frees the adapter and the stream
end;
end;
Același apel funcționează pentru un TMemoryStream, un flux blob dintr-un set de date de bază de date sau un descendent personalizat TStream. Încărcarea la cerere își dovedește utilitatea atunci când fișierul este mare și doar o parte din el va fi citită: un vizualizator de arhivă, un generator de miniaturi care eșantionează câteva pagini, un index de căutare care preia câte o pagină pe rând. Când fișierul este mic sau oricum veți citi totul, o încărcare cu buffer este mai simplă, iar mecanismul de streaming nu vă aduce niciun avantaj. Factorul decisiv este raportul dintre octeții pe care îi veți atinge efectiv și octeții pe care îi conține fișierul.
Odată ce paginile sunt transmise în flux la cerere, următoarea preocupare este menținerea paginilor redate receptive pe măsură ce utilizatorul face zoom și defilează, aspect acoperit în nota noastră despre stocarea în cache a redării și performanța zoom-ului. Când documentul transmis în flux este unul pe care un vizualizator ar trebui să-l afișeze, dar să nu permită utilizatorului să-l exporte sau să-l modifice, tehnicile din ghidul despre previzualizarea securizată PDF se potrivesc natural cu această cale de încărcare. Ambele se bazează pe încărcarea în flux descrisă aici, care este livrată ca parte a PDFium Component pentru Delphi și C++Builder, alături de API-urile de redare, extragere de text și adnotare acoperite în alte părți ale acestui blog.