Et scannet arkiv kan køre op på flere gigabytes i en enkelt PDF. En fremviser (viewer), der åbner en sådan fil, ønsker normalt at vise én side, måske indholdsfortegnelsen, måske en side, brugeren sprang til fra et bogmærke. At læse hele filen ind i hukommelsen for at rendere to sider er spild på alle akser: det brænder adresseområde (address space) af, det forsinker (stalls) brugeren bag en lang indledende læsning, og på en 32-bit Delphi-proces kan det fejle fuldstændigt (fail outright), før en enkelt side vises. PDFium blev bygget med dette i tankerne. Den kan indlæse et dokument gennem et callback, der beder om de specifikke byte-områder (byte ranges), den har brug for, når den har brug for dem, og den kræver aldrig hele filen på én gang. Én grænse hører til fra starten: denne streamingkanal beskriver filen med en 32-bit længde, så den betjener en enkelt fil op til 4 GiB, hvilket i praksis dækker næsten ethvert scannet arkiv. En fil forbi den grænse (line) er ikke denne artikels territorium; den vil hellere splittes i volumener ved scanningstidspunktet eller åbnes gennem en direkte-adgang-strategi i stedet for, og den vagt (the guard), der håndhæver loftet, får helt ærligt sin egen sektion nedenfor
Komponenten udstiller (exposes) den sti gennem en stream-adapter. Du giver den en hvilken som helst TStream, og PDFium trækker blokke (blocks) fra den stream on-demand. Filen kan ligge på en disk, i et database blob-felt, eller bag en hvilken som helst anden TStream efterkommer (descendant), og intet af det kopieres ind i hukommelsen på forhånd
Hvordan PDFium beder om bytes
PDFiums C API indlæser et dokument fra et opkalds-leveret (caller-supplied) objekt beskrevet af FPDF_FILEACCESS strukturen. Strukturen har tre dele, der betyder noget her: et længdefelt (a length field), et læse-callback (a read callback) og en opak (opaque) brugerparameter. Indgangspunktet (The entry point), der forbruger det, er FPDF_LoadCustomDocument. Når PDFium har den struktur, parser den traileren (the trailer), lokaliserer krydsreferencetabellen (the cross-reference table), og læser fra da af kun, hvad en given operation kræver. At åbne dokumentet berører filens hale (tail) og en håndfuld katalogobjekter (catalog objects). Rendering af side 400 læser indholdsstrømmene (the content streams) og ressourcerne for den side og intet andet
Dette er forskellen mellem en bufferet indlæsning (a buffered load) og en streaming-indlæsning. En bufferet indlæsning læser filen ende-til-ende, før PDFium ser byte nul. En streaming-indlæsning vender forholdet om: PDFium driver læsningerne, og de bytes, der aldrig berøres, bliver aldrig læst. For en multi-gigabyte fil, der vises en side ad gangen, er det afstanden (the gap) mellem en ubrugelig indlæsning og en øjeblikkelig en
Stream-adapteren
Adapteren, der bygger bro (bridges) mellem en Delphi TStream og FPDF_FILEACCESS, er TPdfStreamAdapter. Dens konstruktør (constructor) tager stream'en og et ejerskabsflag (an ownership flag), indfanger (captures) stream-længden én gang, udfylder FPDF_FILEACCESS posten (record), og forbinder (wires) læse-callback'et. Når PDFium senere kalder tilbage med et offset og en størrelse, søger adapteren stream'en til det offset og kopierer præcis det område (range) over i bufferen, PDFium stillede til rådighed
// 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;
Ejerskabsflaget beslutter, hvem der frigør stream'en. Overgiv (Pass) False, og kalderen beholder stream'en og skal holde den i live for hele dokumentets levetid. Overgiv True, og adapteren tager over og frigør stream'en, når dokumentet lukkes. Uanset hvad skal stream'en overleve enhver læsning (read), PDFium vil udføre, fordi PDFium holder FPDF_FILEACCESS pointeren og vil kalde tilbage på et hvilket som helst tidspunkt, mens dokumentet er åbent, ikke kun under den indledende indlæsning (the initial load)
Hvorfor callback'et er en statisk funktion
Det læse-callback, som PDFium gemmer i m_GetBlock, er en almindelig C-funktionspointer (C function pointer) med cdecl kald-konventionen (calling convention). En Delphi-metode kan ikke bruges direkte, fordi en metode bærer et skjult Self argument, som en C-kalder ikke ved noget om, og som den aldrig vil levere. Adapteren deklarerer derfor callback'et som en class function markeret som cdecl; static, hvilket kompilerer til en fritstående (free-standing) funktion med det C-framelayout (C frame layout), PDFium forventer, og uden implicit Self
Det løser kald-konventionen (the calling convention), men rejser et andet spørgsmål: uden Self, hvordan når callback'et til den specifikke stream, det formodes at læse fra? Svaret er den opake brugerparameter. Når adapteren bygger posten (the record), gemmer den sin egen instanspointer i m_Param. PDFium giver den samme pointer tilbage som det første argument i hvert callback. Den statiske funktion kaster (casts) den tilbage til en TPdfStreamAdapter og udsender (dispatches) læsningen (the read) mod denne instans' stream. Dette er standardtrampolinen (the standard trampoline) til at overgive objektkontekst på tværs af en C-grænse, der ingen begreb har om objekter
// 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;
4 GiB-loftet, og hvorfor det behøver en vagt (a guard)
Her er, hvor grænsen angivet i indledningen kommer fra. Længdefeltet (The length field) m_FileLen i FPDF_FILEACCESS er en 32-bit usigneret (unsigned) værdi. Dens største repræsenterbare længde er én byte fra 4 GiB. En TStream rapporterer sin størrelse som en Int64, så en stream kan beskrive langt flere bytes, end feltet kan rumme. I det øjeblik en streams størrelse overstiger dette loft, er der ingen ærlig måde at fortælle PDFium, hvor lang filen er
Den forkerte reaktion (response) er at tildele (assign) størrelsen og lade den ombrydes (wrap). At afkorte (truncating) en 5 GiB længde til et 32-bit felt producerer et lille, plausibelt-udseende tal, og PDFium vil derefter parse filen i den tro, at den slutter cirka en gigabyte inde. Traileren og krydsreferencetabellen bor ved den virkelige ende af filen, langt forbi den afkortede længde, så parsningen (the parse) fejler på en måde, der intet har at gøre med den faktiske årsag. Du ville fejlsøge (debugging) en krydsreferencefejl på en fil, der er fuldstændig gyldig (perfectly valid), uden et hint om, at et heltal (integer) wrappede to lag oppe
Adapteren afviser (refuses) inputtet i stedet. Konstruktøren (The constructor) sammenligner stream-størrelsen med High(FPDF_DWORD) og kaster EPdfError det øjeblik, stream'en er for stor til at beskrive. En eksplicit, umiddelbar fejl navngiver det virkelige problem ved konstruktionstidspunktet. En stille afkortning (A silent truncation) gemmer den bag et vildledende symptom, du ville jage meget senere. 4 GiB-grænsen er en reel begrænsning (a genuine constraint) for denne indlæsningssti (loading path), og den ærlige ting er at bringe det højtlydt frem snarere end at skjule (paper over) det med aritmetik, der tilfældigvis kompilerer. Når et arkiv ægte krydser linjen, lever de midler (remedies) lovet i toppen uden for dette API: opdel (split) scanningen i pr.-volumen filer, der hver især holder sig under loftet, eller efterlad dokumentet på disken og betjen (serve) det gennem et direkte-adgang (direct-access) design bygget på 64-bit offsets frem for gennem FPDF_FILEACCESS
Fejl må ikke krydse grænsen (cross the boundary)
En læsning (A read) kan fejle. Stream'en kan være et netværks-bakket (network-backed) objekt, der timer ud, et blob-handle, der blev lukket under dig, eller en fil, der blev afkortet (truncated), efter at dokumentet åbnede. PDFiums kontrakt (contract) for læse-callback'et er en returværdi: ikke-nul for succes, nul for fejl. Det er en C-frame, og den har intet maskineri (machinery) til at fange eller udbrede (propagate) en Pascal-undtagelse (Pascal exception)
Dette er grunden til, at trampolinen (the trampoline) indpakker søgningen (the seek) og læsningen (the read) i en try/except, der opsluger (swallows) undtagelsen og returnerer nul. Hvis en Delphi-undtagelse (Delphi exception) fik lov til at udbrede (propagate) sig ud af callback'et, ville det spole tilbage (unwind) gennem PDFiums cdecl stak-frames (stack frames), som aldrig blev bygget til at blive spolet tilbage af Pascal-undtagelsesmaskineriet. Resultatet er udefineret adfærd (undefined behavior) i bedste fald og et hårdt nedbrud (a hard crash) i værste fald, dybt inde i PDF-parseren uden en brugbar stak. At returnere nul holder fejlen inden for kontrakten. PDFium ser en mislykket bloklæsning (block read), afbryder operationen rent, og FPDF_LoadCustomDocument rapporterer, at dokumentet ikke kunne indlæses, hvilket komponenten bringer frem som en EPdfError på Pascal-siden, hvor den hører hjemme
Åbning af et dokument på denne måde
Komponentmetoden, der driver streaming-stien (the streaming path), er LoadCustomDocument, deklareret som en særskilt metode frem for endnu en LoadDocument overload, således at det at overgive en TMemoryStream aldrig ved et uheld lander på den bufrede sti (the buffered path). Den bygger adapteren, kalder FPDF_LoadCustomDocument, og holder adapteren i live for levetiden af det indlæste dokument
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;
Det samme kald (call) fungerer for en TMemoryStream, en blob-stream fra et databasesæt (database dataset), eller en brugerdefineret (custom) TStream efterkommer. On-demand indlæsning fortjener sin plads (earns its keep), når filen er stor, og kun en del af den vil blive læst: en arkivfremviser (archive viewer), en miniaturebilledegenerator (thumbnail generator), der sampler nogle få sider, et søgeindeks, der trækker én side ad gangen. Når filen er lille, eller du alligevel har tænkt dig at læse den hele, er en bufferet indlæsning enklere, og streaming-maskineriet (the streaming machinery) køber dig intet. Den afgørende faktor er forholdet mellem bytes, du faktisk vil berøre, og bytes, filen indeholder
Når siderne streamer ind on-demand, er den næste bekymring at holde de rendererede sider responsive, efterhånden som brugeren zoomer og ruller (scrolls), hvilket dækkes i vores note om render caching og zoom performance. Når det streamede dokument er et, en fremviser (viewer) bør vise, men ikke lade brugeren eksportere eller ændre (alter), parrer teknikkerne i gennemgangen af sikker PDF-forhåndsvisning sig naturligt med denne indlæsningssti. Begge bygger på den streaming-indlæsning, der er beskrevet her, som leveres som en del af PDFium-komponenten til Delphi og C++Builder sammen med renderings-, tekstudtræknings- og annotations-API'erne dækket andre steder på denne blog