Skenirani arhiv mo?e iznositi nekoliko gigabajta u jednom PDF-u. Preglednik koji otvara takvu datoteku obi?no ?eli prikazati jednu stranicu, mo?da tablicu sadr?aja ili stranicu na koju je korisnik sko?io iz kn?ne oznake. U?itavanje cijele datoteke u memoriju radi prikazivanja dviju stranica je rasipno na svim osima: tro?i adresni prostor, usporava korisnika iza dugog po?etnog ?itanja, a na 32-bitnom Delphi procesu mo?e potpuno zakazati prije nego ?to se pojavi ijedna stranica. PDFium je napravljen s tim na umu. Mo?e u?itati dokument putem povratnog poziva koji tra?i specifi?ne raspone bajtova koji su mu potrebni, kada su mu potrebni, i nikada ne zahtijeva cijelu datoteku odjednom
Komponenta izla?e tu putanju kroz adapter toka. Predajete joj bilo koji TStream, a PDFium povla?i blokove iz tog toka na zahtjev. Datoteka se mo?e nalaziti na disku, u blob polju baze podataka ili iza bilo kojeg drugog potomka klase TStream, a ni?ta od toga se ne kopira unaprijed u memoriju
Kako PDFium tra?i bajtove
C API PDFium-a u?itava dokument iz objekta koji isporu?uje pozivatelj, a koji je opisan strukturom FPDF_FILEACCESS. Ukupna struktura ima tri dijela koja su ovdje va?na: polje duljine, povratni poziv za ?itanje i neprozirni korisni?ki parametar. Ulazna to?ka koja ga tro?i je FPDF_LoadCustomDocument. Nakon ?to PDFium zadr?i tu strukturu, on analizira najavu, locira tablicu unakrsnih referenci i od tada ?ita samo ono ?to odre?ena operacija zahtijeva. Otvaranje dokumenta doti?e kraj datoteke i nekoliko objekata kataloga. Prikazivanje stranice 400 ?ita tokove sadr?aja i resurse za tu stranicu i ni?ta vi?e
To je razlika izme?u me?uspremljenog u?itavanja i strujnog u?itavanja. Me?uspremljeno u?itavanje ?ita datoteku od po?etka do kraja prije nego ?to PDFium vidi bajt nula. Strujno u?itavanje preokre?e odnos: PDFium upravlja ?itanjem, a bajtovi koji se nikada ne dotaknu nikada se i ne pro?itaju. Za vi?egigabajtnu datoteku koja se pregledava stranicu po stranicu, to je razlika izme?u neupotrebljivog u?itavanja i trenutnog
Adapter toka
Adapter koji premo??uje Delphi TStream na FPDF_FILEACCESS je TPdfStreamAdapter. Njegov konstruktor uzima tok i zastavicu vlasni?tva, bilje?i duljinu toka jednom, popunjava zapis FPDF_FILEACCESS i povezuje povratni poziv za ?itanje. Kada PDFium kasnije pozove natrag s pomakom i veli?inom, adapter pozicionira tok na taj pomak i kopira to?no taj raspon u spremnik koji je PDFium osigurao
// 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;
Zastavica vlasni?tva odlu?uje tko osloba?a tok. Proslijedite False i pozivatelj zadr?ava tok i mora ga odr?avati aktivnim tijekom cijelog ?ivota dokumenta. Proslijedite True i adapter preuzima kontrolu, osloba?aju?i tok kada se dokument zatvori. U svakom slu?aju, tok mora nad?ivjeti svako ?itanje koje ?e PDFium izvr?iti, jer PDFium dr?i pokaziva? FPDF_FILEACCESS i pozvat ?e natrag u bilo kojem trenutku dok je dokument otvoren, a ne samo tijekom po?etnog u?itavanja
Za?to je povratni poziv stati?ka funkcija
Povratni poziv za ?itanje koji PDFium pohranjuje u m_GetBlock je obi?an pokaziva? C funkcije s konvencijom pozivanja cdecl. Delphi metoda ne mo?e se koristiti izravno, jer metoda nosi skriveni argument Self o kojem C pozivatelj ne zna ni?ta i nikada ga ne?e dostaviti. Stoga adapter deklarira povratni poziv kao klasnu funkciju (class function) ozna?enu s cdecl; static, ?to se kompajlira u samostalnu funkciju s rasporedom okvira C koji PDFium o?ekuje i bez implicitnog Self
To rje?ava konvenciju pozivanja, ali postavlja drugo pitanje: bez Self, kako povratni poziv dolazi do specifi?nog toka iz kojeg bi trebao ?itati? Odgovor je neprozirni korisni?ki parametar. Kada adapter gradi zapis, on pohranjuje pokaziva? vlastite instance u m_Param. PDFium vra?a taj isti pokaziva? kao prvi argument svakog povratnog poziva. Stati?ka funkcija ga vra?a natrag u TPdfStreamAdapter i ?alje ?itanje prema toku te instance. Ovo je standardni trampolin za predaju konteksta objekta preko C granice koja nema pojma o objektima
// 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;
Gornja granica od 4 GiB i za?to joj treba za?tita
Polje duljine m_FileLen u FPDF_FILEACCESS je 32-bitna neozna?ena vrijednost. Njezina najve?a prikaziva duljina je za jedan bajt kra?a od 4 GiB. TStream izvje?tava o svojoj veli?ini kao Int64, tako da tok mo?e opisati mnogo vi?e bajtova nego ?to polje mo?e primiti. U trenutku kada veli?ina toka prije?e tu granicu, nema po?tenog na?ina da se PDFium-u ka?e koliko je datoteka duga?ka
Pogre?na reakcija je dodijeliti veli?inu i pustiti je da se omota. Skra?ivanje duljine od 5 GiB na 32-bitno polje daje mali broj koji izgleda uvjerljivo, a PDFium ?e tada analizirati datoteku vjeruju?i da ona zavr?ava otprilike na jednom gigabajtu. Najava i tablica unakrsnih referenci nalaze se na stvarnom kraju datoteke, daleko iza skra?ene duljine, pa analiza ne uspijeva na na?in koji nema nikakve veze sa stvarnim uzrokom. Debugirali biste pogre?ku unakrsne reference na datoteci koja je savr?eno valjana, bez naznake da se cijeli broj omotao dva sloja iznad
Umjesto toga, adapter odbija ulaz. Konstruktor uspore?uje veli?inu toka s High(FPDF_DWORD) i podi?e EPdfError onog trenutka kada je tok prevelik za opisivanje. Eksplicitna, trenutna pogre?ka imenuje stvarni problem na to?ki konstrukcije. Tiho skra?ivanje skriva ga iza zavaravaju?eg simptoma koji biste tra?ili mnogo kasnije. Ograni?enje od 4 GiB je stvarno ograni?enje ove putanje u?itavanja, a po?teno je iznijeti ga glasno, a ne zata?kavati aritmetikom koja se slu?ajno kompajlira
Neuspjesi ne smiju prije?i granicu
?itanje mo?e zakazati. Tok mo?e biti objekt podr?an mre?om koji ima vremensko ograni?enje, blob ru?ka koja je zatvorena ispod vas, ili datoteka koja je skra?ena nakon ?to je dokument otvoren. PDFium-ov ugovor za povratni poziv za ?itanje je povratna vrijednost: razli?ita od nule za uspjeh, nula za neuspjeh. To je C okvir i nema mehanizam za hvatanje ili ?irenje Pascal iznimke
Zbog toga trampolin omotava pozicioniranje i ?itanje u try/except koji guta iznimku i vra?a nulu. Ako bi se Delphi iznimci dopustilo da se pro?iri izvan povratnog poziva, ona bi se odmotavala kroz PDFium-ove cdecl okvire stoga, koji nikada nisu izgra?eni da budu odmotani Pascal mehanizmom iznimaka. Rezultat je nedefinirano pona?anje u najboljem slu?aju i te?ko ru?enje u najgorem, duboko unutar parsera PDF-a bez upotrebljivog stoga. Vra?anje nule dr?i neuspjeh unutar ugovora. PDFium vidi neuspjelo ?itanje bloka, ?isto prekida operaciju, a FPDF_LoadCustomDocument javlja da se dokument nije mogao u?itati, ?to komponenta prikazuje kao EPdfError na Pascal strani gdje i pripada
Otvaranje dokumenta na ovaj na?in
Metoda komponente koja pokre?e strujnu putanju je LoadCustomDocument, deklarirana kao zasebna metoda umjesto drugog preoptere?enja LoadDocument, tako da proslje?ivanje TMemoryStream nikada slu?ajno ne sleti na me?uspremljenu putanju. Ona gradi adapter, poziva FPDF_LoadCustomDocument i odr?ava adapter aktivnim tijekom ?ivotnog vijeka u?itanog dokumenta
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 za TMemoryStream, blob tok iz skupa podataka baze podataka ili prilago?enog potomka klase TStream. U?itavanje na zahtjev opravdava svoje postojanje kada je datoteka velika, a ?ita se samo njezin dio: preglednik arhiva, generator sli?ica koji uzima uzorke nekoliko stranica ili indeks pretra?ivanja koji povla?i jednu po jednu stranicu. Kada je datoteka mala ili ?ete ionako pro?itati cijelu, me?uspremljeno u?itavanje je jednostavnije, a strujni mehanizam vam ne donosi ni?ta. Odlu?uju?i faktor je omjer bajtova koje ?ete stvarno dotaknuti u odnosu na bajtove koje datoteka sadr?i
Jednom kada stranice po?nu strujati na zahtjev, sljede?a briga je odr?avanje responzivnosti prikazanih stranica dok korisnik zumira i skrola, ?to je pokriveno u na?oj bilje?ci o predmemoriranju prikaza i performansama zumiranja. Kada je strujani dokument onaj koji bi preglednik trebao prikazati, ali ne i dopustiti korisniku da ga izveze ili izmijeni, tehnike u vodi?u za siguran pregled PDF-a prirodno se povezuju s ovom putanjom u?itavanja. Obje se temelje na strujnom u?itavanju koje je ovdje opisano, a koje se isporu?uje kao dio softvera PDFium Component za Delphi i C++Builder, zajedno s API-jima za prikazivanje, ekstrakciju teksta i bilje?ke koji su pokriveni drugdje na ovom blogu