Skeniran arhiv lahko v enem samem PDF-ju obsega veg gigabajtov. Pregledovalnik, ki odpre takšno datoteko, običajno želi prikazati eno stran, morda kazalo vsebine ali stran, na katero je uporabnik skočil iz zaznamka. Branje celotne datoteke v pomnilnik za upodobitev dveh strani je potratno na vseh ravneh: porablja naslovni prostor, zaustavi uporabnika za dolg začetni čas branja, v 32-bitnem procesu Delphi pa lahko popolnoma spodleti, preden se prikaže ena sama stran. PDFium je bil zgrajen s tem v mislih. Dokument lahko naloži prek povratnega klica, ki zahteva specifična območja bajtov, ko jih potrebuje, in nikoli ne zahteva celotne datoteke hkrati.
Komponenta izpostavlja to pot prek adapterja toka. Predate ji poljuben TStream in PDFium na zahtevo vleče bloke iz tega toka. Datoteka se lahko nahaja na disku, v polju blob podatkovne baze ali za katerim koli drugim potomcem razreda TStream, pri čemer se nič od tega ne kopira v pomnilnik vnaprej.
Kako PDFium zahteva bajte
Vmesnik API za C knjižnice PDFium naloži dokument iz objekta, ki ga zagotovi klicatelj in ga opisuje struktura FPDF_FILEACCESS. Ta struktura ima tri dele, ki so pomembni tukaj: polje dolžine, povratni klic za branje in neprosojen uporabniški parameter. Vstopna točka, ki ga porabi, je FPDF_LoadCustomDocument. Ko PDFium pridobi to strukturo, razčleni zaključni del, locira tabelo navzkrižnih referenc in od takrat naprej bere samo tisto, kar zahteva posamezna operacija. Odpiranje dokumenta se dotakne repa datoteke in peščice objektov kataloga. Upodabljanje strani 400 prebere tokove vsebine in vire za to stran ter nič drugega.
To je razlika med predpomnjenim nalaganjem in pretočnim nalaganjem. Predpomnjeno nalaganje prebere datoteko od začetka do konca, preden PDFium sploh vidi bajt nič. Pretočno nalaganje obrne to razmerje: PDFium vodi branja, bajti, ki se jih nihče nikoli ne dotakne, pa se nikoli ne preberejo. Za večgigabajtno datoteko, ki jo pregledujemo stran za stranjo, to predstavlja razliko med neuporabnim nalaganjem in takojšnjim odpiranjem.
Adapter toka
Adapter, ki premošča Delphi TStream do FPDF_FILEACCESS, je TPdfStreamAdapter. Njegov konstruktor sprejme tok in oznako lastništva, enkrat zajame dolžino toka, izpolni zapis FPDF_FILEACCESS in poveže povratni klic za branje. Ko PDFium pozneje vrne klic z odmikom in velikostjo, adapter premakne tok na ta odmik in kopira natanko to območje v medpomnilnik, ki ga je zagotovil PDFium.
// 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 lastništva določa, kdo sprosti tok. Če predate False, klicatelj obdrži tok in ga mora ohranjati pri življenju celotno življenjsko dobo dokumenta. Če predate True, adapter prevzame nadzor in sprosti tok ob zapiranju dokumenta. V vsakem primeru mora tok preživeti vsako branje, ki ga bo izvedel PDFium, saj PDFium hrani kazalec FPDF_FILEACCESS in bo klical nazaj v kateri koli točki, ko je dokument odprt, ne le med začetnim nalaganjem.
Zakaj je povratni klic statična funkcija
Povratni klic za branje, ki ga PDFium shrani v m_GetBlock, je preprost C kazalec na funkcijo s konvencijo klicanja cdecl. Delphi metode ni mogoče uporabiti neposredno, ker metoda prenaša skriti argument Self, o katerem klicatelj v jeziku C ne ve ničesar in ga ne bo nikoli zagotovil. Adapter zato deklarira povratni klic kot class function z oznako cdecl; static, kar se prevede v samostojno funkcijo z razporeditvijo okvirja C, ki jo PDFium pričakuje, in brez implicitnega argumenta Self.
To razreši konvencijo klicanja, a sproži drugo vprašanje: kako povratni klic brez argumenta Self doseže specifični tok, iz katerega naj bi bral? Odgovor je neprosojni uporabniški parameter. Ko adapter zgradi zapis, shrani kazalec na lastno instanco v m_Param. PDFium preda ta isti kazalec nazaj kot prvi argument vsakega povratnega klica. Statična funkcija ga pretvori nazaj v TPdfStreamAdapter in sproži branje na toku te instance. To je standardna odskočna deska (trampoline) za predajo konteksta objekta preko meje C, ki nima koncepta objektov.
// 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;
Meja 4 GiB in zakaj potrebuje zaščito
Polje dolžine m_FileLen v FPDF_FILEACCESS je 32-bitna nepredznačena vrednost. Njena največja predstavljiva dolžina je za en bajt manjša od 4 GiB. Razred TStream poroča o svoji velikosti kot Int64, zato lahko tok opiše veliko več bajtov, kot jih polje lahko zadrži. V trenutku, ko velikost toka preseže to mejo, ni poštenega načina, kako bi knjižnici PDFium sporočili, kako dolga je datoteka.
Napačen odziv je dodelitev velikosti in dopuščanje, da se ta ovije. Skrajšanje dolžine 5 GiB na 32-bitno polje ustvari majhno, na videz prepričljivo številko, PDFium pa bo nato razčlenil datoteko z prepričanjem, da se konča približno pri enem gigabajtu. Zaključek in tabela navzkrižnih referenc se nahajata na dejanskem koncu datoteke, daleč čez skrajšano dolžino, zato razčlenjevanje spodleti na način, ki nima nobene zveze z dejanskim vzrokom. Odpravljali bi napako navzkrižne reference na popolnoma veljavni datoteki, brez namiga, da se je celo število ovilo dve plasti višje.
Adapter namesto tega zavrne vhod. Konstruktor primerja velikost toka z High(FPDF_DWORD) in sproži EPdfError v trenutku, ko je tok prevelik za opis. Eksplicitna, takojšnja napaka poimenuje resnično težavo na točki gradnje. Tiho skrajšanje jo skrije za zavajajočim simptomom, ki bi ga raziskovali veliko pozneje. Meja 4 GiB je resnična omejitev te poti nalaganja, pošteno pa je, da jo izpostavimo glasno, namesto da jo prekriemo z aritmetiko, ki se slučajno prevede.
Neuspehi ne smejo prečkati meje
Branje lahko spodleti. Tok je lahko omrežni objekt, ki poteče, ročica blob, ki je bila zaprta pod vami, ali datoteka, ki je bila skrajšana po odprtju dokumenta. Pogodba PDFium za povratni klic branja je povratna vrednost: neničelna za uspeh, ničelna za neuspeh. Gre za okvir C, ki nima mehanizma za lovljenje ali širjenje izjeme Pascal.
Zato odskočna deska ovije premik in branje v blok try/except, ki pogoltne izjemo in vrne ničlo. Če bi izjemi Delphi dovolili, da se razširi izven povratnega klica, bi se odvila skozi sklade cdecl knjižnice PDFium, ki niso bili nikoli zgrajeni za odvijanje z mehanizmom izjem Pascal. Rezultat je v najboljšem primeru nedoločeno obnašanje, v najslabšem pa trdo sesutje globoko v razčlenjevalniku PDF brez uporabnega sklada. Vračanje ničle ohranja neuspeh znotraj pogodbe. PDFium zazna neuspešno branje bloka, čisto prekine operacijo in FPDF_LoadCustomDocument sporoči, da dokumenta ni bilo mogoče naložiti, ko komponenta prikaže kot EPdfError na strani Pascal, kamor spada.
Odpiranje dokumenta na ta način
Metoda komponente, ki vodi pretočno pot, je LoadCustomDocument, deklarirana kot ločena metoda in ne kot še ena preobremenitev LoadDocument, tako da prenos TMemoryStream nikoli po nesreči ne pristane na predpomnjeni poti. Zgradi adapter, pokliče FPDF_LoadCustomDocument in ohranja adapter pri življenju celotno življenjsko dobo naloženega 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 klic deluje za TMemoryStream, tok blob iz nabora podatkov baze podatkov ali po meri prilagojenega potomca razreda TStream. Nalaganje na zahtevo se obnese, ko je datoteka velika in se bo prebral le del nje: pregledovalnik arhivov, generator sličic, ki vzorči le nekaj strani, ali iskalni indeks, ki bere eno stran naenkrat. Ko je datoteka majhna ali ko boste vseeno prebrali celotno vsebino, je predpomnjeno nalaganje enostavnejše, pretočni mehanizem pa vam ne prinese ničesar. Odločilni dejavnik je razmerje med bajti, ki se jih boste dejansko dotaknili, in bajti, ki jih datoteka vsebuje.
Ko se strani pretakajo na zahtevo, je naslednja skrb ohranjanje odzivnosti upodobljenih strani ob povečevanju in drsenju uporabnika, kar je obravnavano v našem zapisu o predpomnjenju upodabljanja in zmogljivosti povečave. Ko je pretočni dokument tisti, ki bi ga pregledovalnik moral prikazati, a uporabniku ne dovoli izvoza ali spreminjanja, se tehnike v vodniku za varen predogled PDF naravno ujemajo s to potjo nalaganja. Obe gradita na pretočnem nalaganju, opisanem tukaj, ki se dostavlja kot del komponente PDFium Component za Delphi in C++Builder, skupaj z vmesniki API za upodabljanje, ekstrakcijo besedila in opombe, ki so obravnavani drugje na tem blogu.