Naskenovaný archiv může v jediném PDF dosahovat velikosti několika gigabajtů. Prohlížeč, který takový soubor otevírá, obvykle potřebuje zobrazit jednu stránku, například obsah nebo stránku, na kterou uživatel přešel ze záložky. Načtení celého souboru do paměti za účelem vykreslení dvou stránek je neefektivní ve všech směrech: spotřebovává adresní prostor, zdržuje uživatele dlouhým počátečním čtením a na 32bitovém procesu Delphi může zcela selhat ještě před zobrazením první stránky. Knihovna PDFium byla navržena s ohledem na tyto situace. Dokáže dokument načíst prostřednictvím callbacku, který si vyžádá konkrétní rozsahy bajtů, jež právě potřebuje. Nikdy tak nevyžaduje celý soubor najednou.
Komponenta tuto možnost zpřístupňuje prostřednictvím adaptéru streamu. Předáte jí jakýkoli TStream a PDFium si z něj bloky stahuje podle potřeby. Soubor může být uložen na disku, v databázovém poli typu blob nebo za jakýmkoli jiným potomkem třídy TStream, přičemž nic z toho se předem nekopíruje do paměti.
Jak si PDFium říká o bajty
Rozhraní C API knihovny PDFium načítá dokument z objektu poskytnutého volajícím, který popisuje struktura FPDF_FILEACCESS. Struktura se skládá ze tří částí, které jsou zde důležité: pole délky, callback pro čtení a neprůhledný uživatelský parametr. Vstupním bodem, který ji zpracovává, je funkce FPDF_LoadCustomDocument. Jakmile PDFium tuto strukturu obdrží, analyzuje trailer, lokalizuje tabulku křížových odkazů a od té chvíle čte pouze to, co daná operace vyžaduje. Otevření dokumentu se dotkne pouze konce souboru a několika objektů katalogu. Vykreslení stránky 400 načte toky obsahu a prostředky pro tuto stránku a nic jiného. V tom spočívá rozdíl mezi načítáním s vyrovnávací pamětí (buffered load) a streamovaným načítáním. Načítání s vyrovnávací pamětí přečte soubor od začátku do konce ještě předtím, než PDFium spatří nultý bajt. Streamované načítání tento vztah obrací: PDFium řídí čtení a bajty, kterých se nikdy nedotkne, se nikdy nenačtou. Pro vícegigabajtový soubor prohlížený po jednotlivých stránkách to představuje rozdíl mezi nepoužitelným načítáním a okamžitým zobrazením.
Adaptér streamu
Adaptérem, který přemosťuje Delphi TStream na FPDF_FILEACCESS, je TPdfStreamAdapter. Jeho konstruktor přijímá stream a příznak vlastnictví, jednou zachytí délku streamu, vyplní záznam FPDF_FILEACCESS a propojí callback pro čtení.
// 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;
Příznak vlastnictví rozhoduje o tom, kdo stream uvolní. Předáním hodnoty False si volající stream ponechá a musí jej udržovat aktivní po celou dobu životnosti dokumentu. Předáním hodnoty True přebírá adaptér vlastnictví a uvolní stream při uzavření dokumentu. V každém případě musí stream přežít každé čtení, které PDFium provede, protože PDFium drží ukazatel na FPDF_FILEACCESS a může zavolat zpět v jakémkoli okamžiku, kdy je dokument otevřen, nikoli pouze během počátečního načítání.
Proč je callback statickou funkcí
Callback pro čtení, který PDFium ukládá do m_GetBlock, je obyčejný ukazatel na funkci v C s volací konvencí cdecl. Metodu Delphi nelze použít přímo, protože metoda s sebou nese skrytý argument Self, o kterém C volající nic neví a nikdy jej neposkytne. Adaptér proto deklaruje callback jako class function označenou jako cdecl; static, což se přeloží jako samostatná funkce s rozložením rámce C, které PDFium očekává, a bez skrytého parametru Self. To řeší volací konvenci, ale vyvolává druhou otázku: jak se callback bez parametru Self dostane ke konkrétnímu streamu, ze kterého má číst? Odpovědí je neprůhledný uživatelský parametr. Když adaptér vytváří záznam, uloží ukazatel na svou vlastní instanci do m_Param. PDFium vrací tento stejný ukazatel jako první argument každého callbacku. Statická funkce jej přetypuje zpět na TPdfStreamAdapter a provede čtení nad streamem této instance. Jedná se o standardní mechanismus (trampolínu) pro předávání kontextu objektu přes hranici C rozhraní, které nemá o objektech ani ponětí.
// 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;
Strop 4 GiB a proč vyžaduje ochranu
Pole délky m_FileLen ve struktuře FPDF_FILEACCESS je 32bitová hodnota bez znaménka. Jeho největší reprezentovatelná délka je o jeden bajt méně než 4 GiB. Třída TStream hlásí svou velikost jako Int64, takže stream může popsat mnohem více bajtů, než kolik se do tohoto pole vejde. V okamžiku, kdy velikost streamu překročí tento strop, neexistuje žádný korektní způsob, jak knihovně PDFium sdělit, jak dlouhý soubor ve skutečnosti je. Nesprávnou reakcí je přiřadit velikost a nechat ji přetéct. Oříznutí velikosti 5 GiB na 32bitové pole vytvoří malé, věrohodně vypadající číslo a PDFium pak bude analyzovat soubor s vírou, že končí zhruba po jednom gigabajtu. Trailer a tabulka křížových odkazů se nacházejí na skutečném konci souboru, daleko za oříznutou délkou, takže analýza selže způsobem, který nemá nic společného se skutečnou příčinou. Hledali byste chybu v křížových odkazech u zcela platného souboru, aniž byste tušili, o dvě vrstvy výše přeteklo celé číslo. Adaptér namísto toho vstup odmítne. Konstruktor porovná velikost streamu s hodnotou High(FPDF_DWORD) a vyvolá EPdfError ve chvíli, kdy je stream příliš velký, než aby jej bylo možné popsat. Explicitní a okamžitá chyba pojmenuje skutečný problém v místě konstrukce. Tiché oříznutí jej skryje za zavádějící symptom, který byste řešili až mnohem později. Limit 4 GiB je reálným omezením této načítací cesty, a nejsprávnějším přístupem je na něj hlasitě upozornit, místo abychom jej maskovali aritmetikou, která se náhodou zkompiluje.
Selhání nesmí překročit hranici
Čtení může selhat. Stream může být síťový objekt, u kterého vyprší časový limit, handle blobu, který se pod vámi uzavřel, nebo soubor, který byl po otevření dokumentu oříznut. Kontrakt PDFium pro callback čtení je návratová hodnota: nenulová pro úspěch, nulová pro selhání. Jedná se o C rámec, který nemá žádný mechanismus pro zachycení nebo šíření výjimky Pascalu. Proto trampolína obaluje nastavení pozice a čtení do bloku try/except, který výjimku pohltí a vrátí nulu. Pokud by se výjimce z Delphi umožnilo šířit se ven z callbacku, procházela by zásobníkové rámce cdecl knihovny PDFium, které pro to nebyly nikdy navrženy. Výsledkem je v nejlepším případě nedefinované chování a v nejhorším tvrdý pád hluboko uvnitř PDF parseru bez použitelného zásobníku. Vrácení nuly udržuje selhání v rámci kontraktu. PDFium zaznamená selhání čtení bloku, čistě přeruší operaci a FPDF_LoadCustomDocument nahlásí, že dokument nebylo možné načíst, což komponenta vyvede na povrch jako EPdfError na straně Pascalu, kam patří.
Otevření dokumentu tímto způsobem
Metoda komponenty, která řídí streamovací cestu, je LoadCustomDocument. Je deklarována jako samostatná metoda a nikoli jako další přetížení LoadDocument, aby předání TStream nikdy omylem neskončilo na načítání s vyrovnávací pamětí. Sestaví adaptér, zavolá FPDF_LoadCustomDocument a udržuje adaptér aktivní po celou dobu životnosti načteného dokumentu.
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;
Stejné volání funguje pro TMemoryStream, blob stream z databázové sady nebo vlastní potomky třídy TStream. Načítání na vyžádání se osvědčí, když je soubor velký a bude se číst pouze jeho část: prohlížeč archivu, generátor náhledů vzorkující několik stránek nebo vyhledávací index načítající jednu stránku po druhé. Pokud je soubor malý nebo se chystáte přečíst jej celý, je načítání s vyrovnávací pamětí jednodušší a streamovací mechanismus vám nic nepřinese. Rozhodujícím faktorem je poměr bajtů, kterých se skutečně dotknete, k celkovému počtu bajtů v souboru. Jakmile se stránky streamují na vyžádání, dalším krokem je udržet vykreslené stránky responzivní při přibližování a posouvání uživatelem, což je popsáno v naší poznámce o kešování vykreslování a výkonu zoomu. Pokud je streamovaný dokument určen k zobrazení v prohlížeči, ale uživatel jej nesmí exportovat nebo měnit, techniky v průvodci zabezpečeným náhledem PDF přirozeně doplňují tuto načítací cestu. Oba postupy staví na streamovaném načítání popsaném v tomto článku, které je dodáváno jako součást PDFium Component pro Delphi a C++Builder spolu s rozhraními API pro vykreslování, extrakci textu a anotace popsanými jinde na tomto blogu.