Skenirana arhiva može imati nekoliko gigabajta u jednom PDF-u. Pregledaču je obično potrebna samo jedna stranica, sadržaj ili stranica do koje je korisnik skočio preko obeleživača. Učitavanje celog dokumenta u memoriju zbog nekoliko stranica nepotrebno troši adresni prostor, usporava pokretanje i može potpuno srušiti 32-bitni Delphi proces pre nego što se prva stranica prikaže. PDFium je predviđen upravo za ovaj slučaj: putem povratnog poziva traži samo opsege bajtova koji su mu potrebni, onda kada su mu potrebni, bez učitavanja celog fajla odjednom. Jedno ograničenje važi odmah: ovaj kanal za striming koristi dužinu od 32 bita i zato podržava pojedinačni fajl do 4 GiB. Fajlovi veći od toga zahtevaju podelu tokom skeniranja ili strategiju direktnog pristupa, što obrađujemo u posebnom odeljku
PDFium Component ovu mogućnost izlaže kroz adapter za striming. Prosleđujete mu bilo koji TStream, a PDFium po potrebi preuzima blokove iz tog toka. Fajl može biti na disku, u polju baze podataka tipa blob ili iza bilo kog drugog potomka klase TStream; nijedan njegov deo se unapred ne kopira u memoriju
Kako PDFium traži bajtove
PDFium C API tokom učitavanja koristi objekat koji prosleđuje pozivalac, opisan strukturom FPDF_FILEACCESS. Za ovaj tok važna su tri polja: dužina fajla, povratni poziv za čitanje i neproziran korisnički parametar. Strukturu koristi ulazna tačka FPDF_LoadCustomDocument. Kada je preuzme, PDFium parsira trejler, pronalazi tabelu unakrsnih referenci i zatim čita samo ono što zahteva konkretna operacija. Otvaranje dokumenta dodiruje kraj fajla i nekoliko objekata kataloga. Renderovanje stranice 400 čita tokove sadržaja i resurse te stranice, ništa više
To je razlika između baferovanog i strimovanog učitavanja. Baferovano učitavanje čita fajl od početka do kraja pre nego što PDFium vidi prvi bajt. Kod strimovanja PDFium upravlja čitanjem, pa se bajtovi do kojih se ne dođe uopšte ne čitaju. Za višegigabajtni fajl koji se pregledava stranicu po stranicu, razlika je između neupotrebljivog učitavanja i trenutnog prikaza
Adapter toka
Adapter koji povezuje Delphi TStream sa FPDF_FILEACCESS zove se TPdfStreamAdapter. Konstruktor prima tok i oznaku vlasništva, jednom preuzima njegovu dužinu, popunjava zapis FPDF_FILEACCESS i povezuje povratni poziv za čitanje. Kada PDFium kasnije prosledi pomak i veličinu, adapter pomera tok na taj pomak i kopira tačno traženi opseg u bafer koji je PDFium obezbedio
// 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;
Oznaka vlasništva određuje ko oslobađa tok. Sa False pozivalac zadržava tok i mora da ga održi živim tokom celog životnog veka dokumenta. Sa True adapter preuzima vlasništvo i oslobađa tok pri zatvaranju dokumenta. U oba slučaja tok mora nadživeti svako čitanje koje PDFium može obaviti, jer PDFium zadržava pokazivač FPDF_FILEACCESS i može pozvati povratni poziv u bilo kom trenutku dok je dokument otvoren, ne samo tokom početnog učitavanja
Zašto je povratni poziv statička funkcija
Povratni poziv za čitanje koji PDFium čuva u m_GetBlock običan je pokazivač na C funkciju sa konvencijom poziva cdecl. Delphi metoda ne može da se koristi direktno, jer nosi skriveni argument Self koji C pozivalac ne poznaje i neće proslediti. Zato adapter deklariše povratni poziv kao class function sa oznakom cdecl; static. Kompajler ga tada pretvara u samostalnu funkciju sa rasporedom C steka koji PDFium očekuje i bez implicitnog Self
Time je rešena konvencija poziva, ali ostaje pitanje kako povratni poziv bez Self dolazi do konkretnog toka. Rešenje je neprozirni korisnički parametar. Kada adapter gradi zapis, svoj pokazivač instance čuva u m_Param. PDFium taj isti pokazivač vraća kao prvi argument svakog povratnog poziva. Statička funkcija ga vraća u TPdfStreamAdapter i prosleđuje čitanje toku te instance. To je standardni trampolin za prenos konteksta objekta preko C granice koja ne poznaje objekte
// 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;
Granica od 4 GiB i zašto je potrebna provera
Granica iz uvoda proizlazi iz polja m_FileLen u strukturi FPDF_FILEACCESS, koje je 32-bitna neoznačena vrednost. Najveća predstavljiva dužina je za jedan bajt manja od 4 GiB. TStream veličinu prijavljuje kao Int64, pa može opisati mnogo više bajtova nego što to polje može da primi. Kada veličina toka pređe granicu, PDFiumu se više ne može ispravno saopštiti dužina fajla
Pogrešno rešenje je upisati vrednost i dozvoliti prelivanje. Skraćivanje dužine od 5 GiB na 32-bitno polje daje mali broj koji izgleda uverljivo, pa PDFium pokušava da parsira fajl kao da se završava približno gigabajt ranije. Trejler i tabela unakrsnih referenci nalaze se na stvarnom kraju fajla, daleko iza skraćene dužine, pa parsiranje pada iz razloga koji nema veze sa stvarnim uzrokom. Tada biste tražili grešku unakrsne reference u potpuno ispravnom fajlu, bez naznake da je došlo do prelivanja celobrojne vrednosti
Adapter umesto toga odbija ulaz. Konstruktor poredi veličinu toka sa High(FPDF_DWORD) i odmah podiže EPdfError kada je tok prevelik za opis. Neposredna greška jasno imenuje problem, dok bi tiho skraćivanje proizvelo obmanjujući simptom. Ograničenje od 4 GiB stvarno pripada ovom načinu učitavanja i treba ga jasno prijaviti. Kada arhiva zaista prelazi granicu, rešenje je izvan ovog API-ja: podelite skeniranje na fajlove po tomovima koji ostaju ispod granice ili ostavite dokument na disku i poslužite ga strategijom direktnog pristupa zasnovanom na 64-bitnim pomacima, a ne preko FPDF_FILEACCESS
Greške pri čitanju ne smeju preći granicu
Čitanje može da ne uspe: tok može biti mrežni objekat koji istekne, ručica bloba može biti zatvorena ili fajl može biti skraćen nakon otvaranja dokumenta. Ugovor PDFium-a za povratni poziv je povratna vrednost: različita od nule znači uspeh, a nula neuspeh. To je C okvir i nema mehanizam za hvatanje ili prosleđivanje Pascal izuzetka
Zato trampolin obavija pomeranje i čitanje u try/except, potiskuje izuzetak i vraća nulu. Kada bi Delphi izuzetak izašao iz povratnog poziva, prošao bi kroz PDFium-ove cdecl stek-okvire koji nisu napravljeni za Pascal mehanizam izuzetaka. Rezultat bi bio nedefinisano ponašanje ili pad unutar PDF parsera bez korisnog steka. Vraćanje nule zadržava grešku u okviru ugovora: PDFium vidi neuspešno čitanje bloka, uredno prekida operaciju, a FPDF_LoadCustomDocument prijavljuje da dokument nije moguće učitati, što komponenta izlaže kao EPdfError na Pascal strani
Otvaranje dokumenta na ovaj način
Metoda komponente koja pokreće strimovani tok je LoadCustomDocument. Deklarisana je kao posebna metoda, a ne kao još jedno preopterećenje metode LoadDocument, tako da prosleđivanje objekta TMemoryStream nikada slučajno ne završi u baferovanom toku. Ona gradi adapter, poziva FPDF_LoadCustomDocument i održava adapter živim dok je učitani dokument otvoren
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;
Isti poziv radi sa objektom TMemoryStream, tokom bloba iz skupa podataka baze ili prilagođenim potomkom klase TStream. Učitavanje na zahtev ima smisla kada je fajl velik, a čitaće se samo njegov deo: u pregledaču arhive, generatoru sličica koji uzorkuje nekoliko stranica ili indeksu pretrage koji preuzima jednu po jednu stranicu. Za mali fajl ili slučaj kada ćete ionako pročitati sve, baferovano učitavanje je jednostavnije. Odlučujući kriterijum je odnos bajtova koje ćete stvarno dodirnuti prema ukupnom broju bajtova u fajlu
Kada stranice pristižu na zahtev, važno je da renderovane stranice ostanu odzivne pri uvećavanju i pomeranju, što je obrađeno u našoj belešci o keširanju renderovanja i performansama uvećanja. Ako dokument treba prikazati, ali ne dozvoliti korisniku da ga izveze ili menja, tehnike iz vodiča za bezbedan PDF pregled prirodno se nadovezuju na ovaj tok učitavanja. Obe teme koriste strimovano učitavanje iz ovog članka, koje je deo proizvoda PDFium Component za Delphi i C++Builder, uz API-je za renderovanje, izdvajanje teksta i anotacije