Et scannet arkiv kan fylde flere gigabyte i en enkelt PDF. En fremviser, der åbner en sådan fil, vil normalt vise én side, måske indholdsfortegnelsen eller en side, brugeren hoppede til fra et bogmærke. At indlæse hele filen i hukommelsen for at rendere to sider er spild på alle fronter: det brænder adresseområde, det standser brugeren bag en lang indledende læsning, og på en 32-bit Delphi-proces kan det fejle fuldstændigt, før en enkelt side overhovedet vises. PDFium blev bygget med dette i tankerne. Det kan indlæse et dokument via et callback, der anmoder om de specifikke byte-områder, det har brug for, når det har brug for dem, og det kræver aldrig hele filen på én gang.
Komponenten eksponerer denne sti via en strømadapter. Du giver den en hvilken som helst TStream, og PDFium trækker blokke fra denne strøm efter behov. Filen kan ligge på disken, i et database-blobfelt eller bag en hvilken som helst anden TStream-efterkommer, og intet af det kopieres ind i hukommelsen på forhånd.
Hvordan PDFium anmoder om bytes
PDFiums C-API indlæser et dokument fra et kalder-leveret objekt beskrevet af FPDF_FILEACCESS-strukturen. Strukturen har tre dele, der er vigtige her: et længdefelt, et læse-callback og en uigennemsigtig brugerparameter. Indgangspunktet, der forbruger den, er FPDF_LoadCustomDocument. Når PDFium har fat i denne struktur, fortolker den traileren, finder krydsreferencetabellen og læser derfra kun det, som en given handling kræver. Åbning af dokumentet berører filens hale og en håndfuld katalogobjekter. Rendering af side 400 læser indholdsstrømmene og ressourcerne for denne side og intet andet.
Dette er forskellen mellem en bufferet indlæsning og en streaming-indlæsning. En bufferet indlæsning læser filen ende til ende, før PDFium ser byte nul. A streaming-indlæsning vender forholdet om: PDFium driver læsningerne, og de bytes, der aldrig berøres, læses aldrig. For en fil på flere gigabyte, der ses én side ad gangen, er det forskellen på en ubrugelig indlæsning og en øjeblikkelig en.
Strømadapteren
Adapteren, der forbinder en Delphi TStream med FPDF_FILEACCESS, er TPdfStreamAdapter. Dens constructor tager strømmen og et ejerskabsflag, registrerer strømlængden én gang, udfylder FPDF_FILEACCESS-recorden og forbinder læse-callbacket. Når PDFium senere kalder tilbage med en forskydning og en størrelse, den adapter søger strømmen til denne forskydning og kopierer præcis dette område ind i den buffer, som PDFium leverede.
// 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 bestemmer, hvem der frigør strømmen. Send False, og kalderen beholder strømmen og skal holde den i live i hele dokumentets levetid. Send True, og adapteren overtager og frigør strømmen, når dokumentet lukkes. Uanset hvad skal strømmen overleve hver læsning, som PDFium vil udføre, fordi PDFium holder FPDF_FILEACCESS-pegeren og vil kalde tilbage på ethvert tidspunkt, mens dokumentet er åbent, ikke kun under den indledende indlæsning.
Hvorfor callbacket er en statisk funktion
Læse-callbacket, som PDFium gemmer i m_GetBlock, er en almindelig C-funktionspeger med kaldekonventionen cdecl. En Delphi-metode kan ikke bruges direkte, fordi en metode bærer et skjult Self-argument, som en C-kaldende part intet ved om og aldrig vil levere. Adapteren erklærer derfor callbacket som en class function markeret som cdecl; static, hvilket compilerer to en fritstående funktion med det C-rammelayout, som PDFium forventer, og uden implicit Self.
Det løser kaldekonventionen, men rejser et andet spørgsmål: Hvordan når callbacket den specifikke strøm, det skal læse fra, når der ikke er noget Self? Svaret er den uigennemsigtige brugerparameter. Når adapteren bygger recorden, gemmer den sin egen instanspeger i m_Param. PDFium giver den samme peger tilbage som det første argument i hvert callback. Den statiske funktion caster den tilbage til en TPdfStreamAdapter og sender læsningen mod denne instans' strøm. Dette er den standardiserede trampolin til at overføre objektkontekst over en C-grænse, der ikke har begreb 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;
Loftet på 4 GiB, og hvorfor det har brug for en beskyttelse
Længdefeltet m_FileLen i FPDF_FILEACCESS is en 32-bit værdi uden fortegn. Dens største repræsentative længde is ene byte under 4 GiB. En TStream rapporterer sin størrelse som en Int64, så en strøm kan beskrive langt flere bytes, end feltet kan indeholde. I det øjeblik en strøms størrelse overstiger dette loft, er der ingen ærlig måde at fortælle PDFium, hvor lang filen er.
Den forkerte reaktion er at tildele størrelsen og lade den wrappe. At afkorte en længde på 5 GiB til et 32-bit felt producerer et lille, plausibelt udseende tal, og PDFium vil derefter fortolke filen i den tro, at den slutter cirka en gigabyte inde. Traileren og krydsreferencetabellen findes i den reelle ende af filen, langt forbi den afkortede længde, så fortolkningen fejler på en måde, der intet har med den reelle årsag at gøre. Du ville fejlfinde en krydsreferencefejl på en fil, der er fuldstændig gyldig, uden antydning af, at et heltal wrappede to lag længere oppe.
Adapteren afviser i stedet inputtet. Constructoren sammenligner strømstørrelsen med High(FPDF_DWORD) og rejser EPdfError i det øjeblik, strømmen er for stor til at beskrive. En eksplicit, øjeblikkelig fejl nævner det reelle problem på oprettelsestidspunktet. En lydløs afkortning skjuler det bag et misvisende symptom, du ville jagte langt senere. Grænsen på 4 GiB er en reel begrænsning for denne indlæsningssti, og det ærlige er at bringe det frem i lyset i stedet for at dække over det med aritmetik, der tilfældigvis compilerer.
Fejl må ikke krydse grænsen
En læsning kan fejle. Strømmen kan være et netværksbaseret objekt, der timer ud, et blob-håndtag, der blev lukket under dig, eller en fil, der blev afkortet, efter at dokumentet åbnede. PDFiums kontrakt for læse-callbacket er en returværdi: ikke-nul for succes, nul for fejl. Det er en C-ramme, og den har intet maskineri til at fange eller udbrede en Pascal-undtagelse.
Dette er grunden til, at trampolinen pakker søgningen og læsningen ind i en try/except, der opsluger undtagelsen og returnerer nul. Hvis en Delphi-undtagelse fik lov til at forplante sig ud af callbacket, ville den afvikle gennem PDFiums cdecl-stakrammer, som aldrig er bygget til at blive afviklet af Pascals undtagelsesmaskineri. Resultatet er i bedste fald udefineret adfærd og i værste fald et hårdt crash dybt inde i PDF-fortolkeren uden en anvendelig stak. At returnere nul holder fejlen inden for kontrakten. PDFium ser en mislykket bloklæsning, afbryder handlingen rent, og FPDF_LoadCustomDocument rapporterer, at dokumentet ikke kunne indlæses, hvilket komponenten præsenterer som en EPdfError på Pascals side, hvor den hører hjemme.
Åbning af et dokument på denne måde
Komponentmetoden, der driver streamingstien, er LoadCustomDocument, erklæret som en særskilt metode frem for en anden LoadDocument-overload, så overførsel af en TMemoryStream aldrig ved et uheld lander på den bufferede sti. Den bygger adapteren, kalder FPDF_LoadCustomDocument og holder adapteren i live i hele det indlæste dokuments levetid.
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 fungerer for en TMemoryStream, en blob-strøm fra et databasedatasæt eller en brugerdefineret TStream-efterkommer. Indlæsning efter behov tjener sig ind, når filen er stor, og kun en del af den vil blive læst: en arkivfremviser, en miniaturegenerator, der udtager prøver af et par sider, eller et søgeindeks, der trækker én side ad gangen. Når filen er lille, eller du alligevel vil læse det hele, er en bufferet indlæsning enklere, og streaming-maskineriet giver dig intet. Den afgørende faktor er forholdet mellem de bytes, du rent faktisk vil røre ved, og de bytes, filen indeholder.
Når siderne streames ind efter behov, er den næste bekymring at holde rendered sider responsive, når brugeren zoomer og scroller, hvilket er dækket i vores note om render-caching og zoom-ydeevne. Når det streamede dokument er et, som en fremviser skal vise, men ikke lade brugeren eksportere eller ændre, teknikkerne i gennemgangen af sikker PDF-forhåndsvisning parres naturligt med denne indlæsningssti. Begge bygger på den her beskrevne streamingindlæsning, som leveres som en del af PDFium Component til Delphi og C++Builder sammen med API'erne til rendering, tekstekstraktion og annoteringer, der er dækket andre steder på denne blog.