Skenirana arhiva može narasti na nekoliko gigabajta unutar jednog PDF-a. Preglednik koji otvara takvu datoteku obično želi prikazati jednu stranicu, možda sadržaj, možda stranicu na koju je korisnik skočio iz zabilješke. Učitavanje cijele datoteke u memoriju radi iscrtavanja dviju stranica rasipno je u svakom pogledu: troši adresni prostor, zadržava 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 izgrađen s tim na umu. Može učitati dokument kroz povratni poziv koji traži određene raspone bajtova koje treba, kad ih treba, i nikad ne zahtijeva cijelu datoteku odjednom. Jedna granica se ističe unaprijed: ovaj protočni kanal opisuje datoteku 32-bitnom duljinom, pa poslužuje jednu datoteku do 4 GiB, što u praksi pokriva gotovo svaku skeniranu arhivu. Datoteka preko te granice nije područje ovog članka; nju treba podijeliti u sveske u vrijeme skeniranja ili je umjesto toga otvoriti kroz strategiju izravnog pristupa, a čuvar koji provodi taj strop iskreno dobiva vlastiti odjeljak niže
Komponenta izlaže taj put kroz adapter toka. Predate mu bilo koji TStream, a PDFium povlači blokove iz tog toka na zahtjev. Datoteka može sjediti na disku, u blob polju baze podataka, ili iza bilo kojeg drugog potomka TStream, i ništa se od toga unaprijed ne kopira u memoriju
Kako PDFium traži bajtove
PDFium-ov C API učitava dokument iz objekta koji predaje pozivatelj, opisanog strukturom FPDF_FILEACCESS. Struktura ima tri dijela koja su ovdje bitna: polje duljine, povratni poziv za čitanje, i neproziran korisnički parametar. Ulazna točka koja je koristi je FPDF_LoadCustomDocument. Kad PDFium drži tu strukturu, raščlanjuje trailer, pronalazi tablicu unakrsnih referenci, i od tada nadalje čita samo ono što dana operacija zahtijeva. Otvaranje dokumenta dotiče kraj datoteke i pregršt objekata kataloga. Iscrtavanje stranice 400 čita tokove sadržaja i resurse za tu stranicu i ništa drugo
To je razlika između učitavanja s međuspremnikom i protočnog učitavanja. Učitavanje s međuspremnikom čita datoteku od početka do kraja prije nego što PDFium vidi bajt nula. Protočno učitavanje obrće taj odnos: PDFium pokreće čitanja, a bajtovi koji se nikad ne dotaknu nikad se ne čitaju. Za datoteku od nekoliko gigabajta koju se pregledava stranicu po stranicu, to je razlika između neupotrebljivog učitavanja i trenutnog
Adapter toka
Adapter koji premošćuje Delphi TStream prema FPDF_FILEACCESS je TPdfStreamAdapter. Njegov konstruktor uzima tok i zastavicu vlasništva, jednom zabilježi duljinu toka, popuni zapis FPDF_FILEACCESS, i poveže povratni poziv za čitanje. Kad ga PDFium kasnije pozove s pomakom i veličinom, adapter pozicionira tok na taj pomak i kopira upravo taj raspon u međuspremnik koji je PDFium osigurao
// Doslovno iz komponente: most stream-to-FPDF_FILEACCESS
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 je 32-bitni unsigned long. Odbij tok
// koji bi se tiho skratio preko 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. Predate li False, pozivatelj zadržava tok i mora ga održati živim tijekom cijelog trajanja dokumenta. Predate li True, adapter preuzima vlasništvo i oslobađa tok kad se dokument zatvori. U oba slučaja tok mora nadživjeti svako čitanje koje će PDFium obaviti, jer PDFium drži pokazivač FPDF_FILEACCESS i pozivat će natrag u bilo kojem trenutku dok je dokument otvoren, 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 obični je C pokazivač na funkciju s pozivnom konvencijom cdecl. Delphi metoda ne može se koristiti izravno, jer metoda nosi skriveni argument Self o kojem C pozivatelj ništa ne zna i koji nikad neće predati. Adapter zato deklarira povratni poziv kao class function označenu s cdecl; static, koja se kompilira u samostalnu funkciju s C rasporedom okvira kakav PDFium očekuje i bez implicitnog Self
Time se rješava pozivna konvencija, ali se otvara drugo pitanje: bez Self, kako povratni poziv dopre do određenog toka iz kojeg treba čitati? Odgovor je neproziran korisnički parametar. Kad adapter gradi zapis, pohranjuje vlastiti pokazivač na instancu u m_Param. PDFium taj isti pokazivač vraća kao prvi argument svakog povratnog poziva. Statička funkcija ga pretvara natrag u TPdfStreamAdapter i usmjerava čitanje prema toku te instance. To je standardni trampolin za predaju konteksta objekta preko C granice koja nema pojma o objektima
// Doslovno iz komponente: cdecl trampoline natrag do 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); // dohvati instancu iz 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; // prijavi neuspjeh povratnom vrijednošću, nikad iznimkom
end;
end;
Strop od 4 GiB i zašto mu treba čuvar
Odavde dolazi granica spomenuta na početku. Polje duljine m_FileLen u FPDF_FILEACCESS je 32-bitna vrijednost bez predznaka. Njegova najveća prikaziva duljina je jedan bajt manje od 4 GiB. TStream prijavljuje svoju veličinu kao Int64, pa tok može opisati mnogo više bajtova nego što polje može sadržati. Čim veličina toka premaši taj strop, ne postoji pošten način da se PDFium-u kaže koliko je datoteka duga
Pogrešan odgovor je dodijeliti veličinu i pustiti da se prelije. Skraćivanje duljine od 5 GiB na 32-bitno polje proizvodi malen, uvjerljivo izgledajući broj, pa će PDFium tada raščlaniti datoteku vjerujući da ona završava otprilike gigabajt unutra. Trailer i tablica unakrsnih referenci nalaze se na stvarnom kraju datoteke, daleko iza skraćene duljine, pa raščlamba zakaže na način koji nema nikakve veze sa stvarnim uzrokom. Otklanjali biste pogrešku unakrsne reference na datoteci koja je posve valjana, bez ikakvog nagovještaja da se cijeli broj prelio dva sloja iznad
Adapter umjesto toga odbija ulaz. Konstruktor uspoređuje veličinu toka s High(FPDF_DWORD) i diže EPdfError u trenutku kad je tok prevelik da bi se mogao opisati. Izričita, trenutna greška imenuje stvarni problem u točki konstrukcije. Tiho skraćivanje ga skriva iza zavaravajućeg simptoma koji biste jurili mnogo kasnije. Ograničenje od 4 GiB stvarno je ograničenje ovog puta učitavanja, a pošteno je to glasno istaknuti umjesto prekriti aritmetikom koja slučajno prolazi kompilaciju. Kad arhiva stvarno prijeđe tu granicu, rješenja obećana na početku žive izvan ovog API-ja: podijelite skeniranje u datoteke po svesku od kojih svaka ostaje ispod stropa, ili ostavite dokument na disku i poslužujte ga dizajnom izravnog pristupa izgrađenim na 64-bitnim pomacima umjesto kroz FPDF_FILEACCESS
Neuspjesi ne smiju prijeći granicu
Čitanje može zakazati. Tok može biti objekt podržan mrežom kojemu istekne vrijeme, blob rukovatelj koji je zatvoren ispod vas, ili datoteka koja je skraćena nakon što je dokument otvoren. PDFium-ov ugovor za povratni poziv čitanja je povratna vrijednost: različito od nule za uspjeh, nula za neuspjeh. To je C okvir, i nema mehanizam za hvatanje ili propagiranje Pascal iznimke
Zato trampolin omata pozicioniranje i čitanje u try/except koji proguta iznimku i vrati nulu. Kad bi se Delphi iznimci dopustilo da propagira izvan povratnog poziva, odmotavala bi se kroz PDFium-ove cdecl okvire stoga, koji nikad nisu izgrađeni da ih odmotava Pascal mehanizam iznimki. Rezultat je u najboljem slučaju nedefinirano ponašanje, a u najgorem tvrdi pad, duboko unutar PDF raščlanjivača bez upotrebljivog stoga. Vraćanje nule zadržava neuspjeh unutar ugovora. PDFium vidi neuspjelo čitanje bloka, uredno prekida operaciju, a FPDF_LoadCustomDocument prijavljuje da se dokument nije mogao učitati, što komponenta na Pascal strani, gdje i pripada, izlaže kao EPdfError
Otvaranje dokumenta na ovaj način
Metoda komponente koja pokreće protočni put je LoadCustomDocument, deklarirana kao zasebna metoda, a ne kao još jedno preopterećenje metode LoadDocument, tako da predaja TMemoryStream nikad slučajno ne završi na putu s međuspremnikom. Ona gradi adapter, poziva FPDF_LoadCustomDocument, i održava adapter živim tijekom trajanja učitanog dokumenta
var
Pdf: TPdf;
FileStream: TFileStream;
begin
Pdf := TPdf.Create(nil);
FileStream := TFileStream.Create('Archive_4GB.pdf', fmOpenRead or fmShareDenyWrite);
try
// Predaj vlasništvo nad tokom objektu Pdf: on oslobađa FileStream kad se dokument zatvori.
Pdf.LoadCustomDocument(FileStream, True);
// PDFium je dosad pročitao samo trailer i katalog.
// Iscrtavanje stranice povlači samo bajtove te stranice kroz povratni poziv.
// ... ovdje iscrtajte ili pregledajte stranice ...
finally
Pdf.Free; // zatvara dokument, čime oslobađa adapter i tok
end;
end;
Isti poziv radi za TMemoryStream, blob tok iz skupa podataka baze, ili prilagođeni potomak TStream. Učitavanje na zahtjev opravdava se kad je datoteka velika, a čitat će se samo njezin dio: preglednik arhiva, generator sličica koji uzorkuje nekoliko stranica, indeks pretraživanja koji povlači po jednu stranicu odjednom. Kad je datoteka mala ili ćete je svejedno pročitati u cijelosti, učitavanje s međuspremnikom je jednostavnije, a protočni mehanizam vam ništa ne donosi. Odlučujući čimbenik je omjer bajtova koje ćete stvarno dotaknuti naspram bajtova koje datoteka sadrži
Kad stranice stižu protočno na zahtjev, sljedeća briga je održati iscrtane stranice odzivnima dok korisnik zumira i pomiče prikaz, što je obrađeno u našoj bilješci o predmemoriranju iscrtavanja i performansama zumiranja. Kad je protočni dokument onaj koji bi preglednik trebao prikazati, ali ne dopustiti korisniku da ga izveze ili izmijeni, tehnike iz prolaska kroz sigurni pregled PDF-a prirodno se uparuju s ovim putem učitavanja. Oboje se nadograđuje na ovdje opisano protočno učitavanje, koje se isporučuje kao dio PDFium komponente za Delphi i C++Builder, uz API-je za iscrtavanje, izdvajanje teksta i napomene obrađene drugdje na ovom blogu