Technical Article

Strömning av enorma PDF-filer på begäran med PDFium i Delphi

Ett skannat arkiv kan uppgå till flera gigabyte i en enda PDF-fil. Ett visningsprogram som öppnar en sådan fil vill vanligtvis visa en sida, kanske innehållsförteckningen, eller en sida som användaren hoppade till från ett bokmärke. Att läsa in hela filen i minnet för att rendera två sidor är slösaktigt på alla sätt: det tar upp adressutrymme, det fördröjer användaren bakom en lång inledande läsning, och på en 32-bitars Delphi-process kan det misslyckas helt innan en enda sida visas. PDFium byggdes med detta i åtanke. Det kan ladda ett dokument via ett återanrop som frågar efter de specifika byte-intervall det behöver, när det behöver dem, och det kräver aldrig hela filen på en gång

Komponenten exponerar den sökvägen via en strömadapter. Du överlämnar en valfri TStream, och PDFium hämtar block från den strömmen på begäran. Filen kan ligga på disk, i ett databas-blobfält eller bakom någon annan TStream-avkomma, och ingenting av det kopieras in i minnet i förväg

Hur PDFium ber om bytes

PDFiums C-API laddar ett dokument från ett objekt tillhandahållet av anroparen som beskrivs av strukturen FPDF_FILEACCESS. Strukturen har tri delar som spelar roll här: ett längdfält, ett läsåteranrop och en opak användarparameter. Startpunkten som konsumerar den är FPDF_LoadCustomDocument. När PDFium har den strukturen tolkar det trailern, lokaliserar korsreferenstabellen och läser därefter endast vad en viss operation kräver. Att öppna dokumentet rör vid filens slut och en handfull katalogobjekt. Att rendera sida 400 läser innehållsströmmarna och resurserna för den sidan och ingenting annat

Detta är skillnaden mellan en buffrad laddning och en strömmande laddning. En buffrad laddning läser filen från början till slut innan PDFium ser byte noll. A strömmande laddning inverterar förhållandet: PDFium styr läsningarna, och de bytes som aldrig berörs läses aldrig. För en fil på flera gigabyte som visas en sida i taget är detta skillnaden mellan en oanvändbar laddning och en omedelbar

Strömadaptern

Adaptern som bryggar en Delphi-TStream till FPDF_FILEACCESS är TPdfStreamAdapter. Dess konstruktor tar strömmen och en ägarskapsflagga, fångar strömlängden en gång, fyller i FPDF_FILEACCESS-posten och kopplar läsåteranropet. När PDFium senare anropar tillbaka med en offset och en storlek, adaptern söker strömmen till den offseten och kopierar exakt det intervallet till den buffert som PDFium tillhandahållit

// 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;

Ägarskapsflaggan avgör vem som frigör strömmen. Skicka False och anroparen behåller strömmen och måste hålla den vid liv under hela dokumentets livslängd. Skicka True och adaptern tar över och frigör strömmen när dokumentet stängs. Oavsett vilket måste strömmen överleva varje läsning som PDFium utför, eftersom PDFium håller FPDF_FILEACCESS-pekaren och kommer att anropa tillbaka vid vilken tidpunkt som helst medan dokumentet är öppet, inte bara under den inledande inläsningen

Varför återanropet är en statisk funktion

Läsåteranropet som PDFium sparar i m_GetBlock är en vanlig C-funktionspekare med anropskonventionen cdecl. En Delphi-metod kan inte användas direkt, eftersom en metod bär på ett dolt Self-argument som en C-anropare inte vet något om och aldrig kommer att tillhandahålla. Adaptern deklarerar därför återanropet som en class function markerad med cdecl; static, vilket kompilerar till en fristående funktion med den C-ramlayout som PDFium förväntar sig och inget implicit Self

Det löser anropskonventionen men väcker en andra fråga: hur når återanropet den specifika strömmen det är tänkt att läsa från när det inte finns något Self? Svaret är den opaka användarparametern. När adaptern bygger posten sparar den sin egen instanspekare i m_Param. PDFium lämnar tillbaka samma pekare som det första argumentet i varje återanrop. Den statiska funktionen typomvandlar den tillbaka till en TPdfStreamAdapter och skickar läsningen mot den instansens ström. Detta är standardstudsmatta (trampoline) för att överlämna objektsammanhang över en C-gräns som saknar begrepp om objekt

Taket på 4 GiB och varför det behöver ett skydd

Längdfältet m_FileLen i FPDF_FILEACCESS is ett 32-bitars osignerat värde. Dess största representerbara längd är en byte under 4 GiB. En TStream rapporterar sin storlek som en Int64, så en ström kan beskriva betydligt fler bytes än vad fältet kan rymma. I det ögonblick en ströms storlek överstiger det taket finns det inget ärligt sätt att berätta för PDFium hur stor filen är

Fel respons är att tilldela storleken och låta den slå runt. Att trunkera en 5 GiB-längd till ett 32-bitarsfält producerar ett litet, till synes rimligt tal, och PDFium kommer då att tolka filen i tro att den slutar ungefär en gigabyte in. Trailern och korsreferenstabellen ligger i den verkliga änden av filen, långt efter den trunkerade längden, so tolkningen misslyckas på ett sätt som inte har något att göra med den faktiska orsaken. Du skulle felsöka ett korsreferensfel på en fil som är helt giltig, utan någon aning om att ett heltal slog runt två lager upp

Adaptern avvisar indata istället. Konstruktorn jämför strömstorleken med High(FPDF_DWORD) och utlöser EPdfError i samma ögonblick som strömmen är för stor för att beskrivas. Ett explicit, omedelbart fel namnger det verkliga problemet vid konstruktionspunkten. En tyst trunkering döljer det bakom ett missvisande symptom som du skulle jaga mycket senare. Gränsen på 4 GiB är en verklig begränsning för denna laddningssökväg, och det ärligaste är att lyfta fram det tydligt snarare än att dölja det med aritmetik som råkar kompilera

Misslyckanden får inte korsa gränsen

En läsning kan misslyckas. Strömmen kan vara ett nätverksanslutet objekt som gör timeout, en blob-pekare som stängdes under dig eller en fil som trunkerades efter att dokumentet öppnades. PDFiums kontrakt för läsåteranropet är ett returvärde: skilt från noll för framgång, noll för misslyckande. Det är en C-ram, och den saknar maskineri för att fånga eller sprida ett Pascal-undantag

Det är därför studsmattan omsluter sökningen och läsningen i en try/except som sväljer undantaget och returnerar noll. Om ett Delphi-undantag tilläts spridas ut ur återanropet, skulle det rullas upp genom PDFiums cdecl-stackramar, vilka aldrig byggdes för att rullas upp av Pascals undantagsmaskineri. Resultatet är i bästa fall odefinierat beteende och i värsta fall en hård krasch, djupt inne i PDF-tolken utan någon användbar stack. Att returnera noll håller misslyckandet inom kontraktet. PDFium ser en misslyckad blockläsning, avbryter operationen rent, och FPDF_LoadCustomDocument rapporterar att dokumentet inte kunde laddas, vilket komponenten lyfter fram som en EPdfError på Pascal-sidan där det hör hemma

// 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;

Att öppna ett dokument på detta sätt

Komponentmetoden som driver den strömmande sökvägen är LoadCustomDocument, deklarerad som a separat metod snarare än en annan LoadDocument-överlagring för att säkerställa att överföring av en TMemoryStream aldrig av misstag landar på den buffrade sökvägen. Den bygger adaptern, anropar FPDF_LoadCustomDocument och håller adaptern vid liv under det laddade dokumentets livstid

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;

Samma anrop fungerar för en TMemoryStream, en blob-ström från ett databas-dataset eller en anpassad TStream-avkomling. Laddning på begäran gör nytta när filen är stor och endast en del av den ska läsas: en arkivvisare, en miniatyrbildsgenerator som samlar prover från några sidor, eller ett sökindex som hämtar en sida i taget. När filen är liten eller när du ändå kommer att läsa hela filen är en buffrad laddning enklare och det strömmande maskineriet ger dig ingenting. Den avgörande faktorn är förhållandet mellan de bytes du faktiskt kommer att röra vid och de bytes som filen innehåller

När sidor strömmar in på begäran är nästa fråga att hålla renderade sidor responsiva när användaren zoomar och bläddrar, vilket beskrivs i vår anteckning om renderings-cache och zoom-prestanda. När det strömmade dokumentet är ett som ett visningsprogram ska visa men inte låta användaren exportera eller ändra, passar teknikerna i genomgången av säker PDF-förhandsvisning naturligt ihop med denna laddningssökväg. Båda bygger på den strömmande laddningen som beskrivs här, vilken levereras som en del av PDFium Component för Delphi och C++Builder, tillsammans med de API:er för rendering, textutdragning och annotering som beskrivs på andra ställen i denna blogg