PDFlibPas umí otevřít lokální PDF přes omezené paměťově mapované okno jen pro čtení: LoadFromMappedFile a DAOpenMappedFile udržují přesně jedno posuvné okno nad souborem, podle potřeby ho přemapují a každý výřez objektu obslouží čtením z absolutního offsetu. Knihovna PDF pro Delphi nikdy nedrží celý zdroj v paměti, takže využití adresního prostoru zůstává při růstu souboru konstantní. Návrh míří na jedinou úlohu: gigabajtová PDF, u nichž parser dokončil načítání, ale stále se vrací na disk objekt po objektu a fragment streamu po fragmentu streamu
Proč zůstává náhodné čtení drahé i po načtení PDF
Načtení PDF neznamená konec čtení a u několika gigabajtů je právě tato mezera zdrojem času. Křížová referenční tabulka nebo cross-reference stream (ISO 32000-1 §7.5.4 a §7.5.8) pouze zaznamenává, kde začíná každý nepřímý objekt. Bajty dorazí později, když se renderuje stránka, dekóduje program fontu nebo extrahuje embedded file stream (ISO 32000-1 §7.11.4). Archiv o velikosti 2 GB s desítkami tisíc objektů se promění v desítky tisíc malých neuspořádaných čtení a při načítání nelze předem znát žádné z nich
Cesta těchto čtení dříve vedla přes sdílené Seek následované Read nad jedním pozičním streamem a selhávala dvěma směry najednou. Každý fragment platí za čtení souboru, i když stránka už je v cache operačního systému, a kurzor je sdílený měnitelný stav, takže lokální soubor a byte-range zdroj za progresivním načítáním PDF rozsahů s prefetchingem nemohly používat stejný kód parseru bez vzájemného přepisování pozice. PDFlibPas řeší obojí tím, že čtení z absolutního offsetu povyšuje z optimalizace na smlouvu
Co zaručuje TPDFReadAtStream
TPDFReadAtStream zaručuje čtení na absolutním offsetu, které nezávisí na logickém kurzoru streamu ani ho nemění. Jde o abstraktní potomka TStream s právě jednou virtuální metodou a oba zdroje v knihovně nezávislé na kurzoru z něj dědí: TReadOnlyMappedFileStream pro lokální soubory a TByteRangeStream pro vzdálené zdroje obsluhované po rozsazích. Čtečka výřezu objektu se jednou zeptá, zda je jejím zdrojem TPDFReadAtStream, a pokud není, vrátí se ke staré sekvenci seek-then-read, takže běžný file stream nebo memory stream dál funguje beze změny
type
// Read-only streamy, jejichž absolutní čtení obchází sdílené Seek a Read
TPDFReadAtStream = class(TStream)
public
function ReadAt(Offset: Int64; var Buffer;
Count: LongInt): LongInt; virtual; abstract;
end;
// Přístup jen pro čtení v okně k jedinému lokálnímu souboru
TReadOnlyMappedFileStream = class(TPDFReadAtStream)
private
FMemoryMapped: Boolean;
public
constructor Create(const FileName: WideString; WindowSize: Int64 = 0);
function GetStats: TPDFMappedFileStats;
function ReadAt(Offset: Int64; var Buffer;
Count: LongInt): LongInt; override;
property MemoryMapped: Boolean read FMemoryMapped;
end;
Rozdíl je důležitější, než napovídá signatura. ReadAt používá předaný offset a ponechá Position přesně tam, kde byl, což umožňuje vnořeným úrovním parseru číst bez ukládání a obnovování pozice kolem každého volání. TReadOnlyMappedFileStream stále implementuje Read, Seek a Size jako každý jiný TStream, Seek omezí logickou pozici do rozsahu souboru a Write vždy vrací 0, protože zdroj se otevírá jen pro čtení
Otevření PDF přes mapované okno v Delphi
Mapovaný zdroj otevírají dva explicitní vstupní body a žádný z nich nemění chování vstupních bodů, které už používáte. LoadFromMappedFile načte a vybere dokument; DAOpenMappedFile vrací Direct Access handle nad stejným souborem, což je režim, který chcete při slučování a dělení gigabajtových PDF přes Direct Access. LoadFromFile a DAOpenFile si ponechávají své významy sdílení souboru, chyb a kompatibility, takže u volajících, kteří se nepřihlásí, se nic neposune. Oba mapované vstupní body přijímají požadovaný WindowSize v bajtech a bitovou masku Options a u obou lze zadat 0 pro libovolný z nich
var
Pdf: TPDFlib;
Payload: AnsiString;
Info: WideString;
begin
Pdf := TPDFlib.Create;
try
// WindowSize 0 zvolí výchozích 64 MiB; mapování je zde povinné
if Pdf.LoadFromMappedFile('archive-2026.pdf', '', 0,
PDF_MAPPED_FILE_REQUIRE_MAPPING) <> 1 then
raise Exception.CreateFmt('mapped open refused, LastErrorCode=%d',
[Pdf.LastErrorCode]);
// Odložená extrakce nyní prochází mapovaná okna místo seekování
Payload := Pdf.GetEmbeddedFileContentToString(1);
if Pdf.GetMappedFileInfo(Info) = 1 then
Writeln(Info);
finally
Pdf.Free;
end;
end;
Co přesně vynucuje PDF_MAPPED_FILE_REQUIRE_MAPPING
PDF_MAPPED_FILE_REQUIRE_MAPPING mění tichý fallback na okamžité a diagnostikovatelné selhání při otevírání. Když je Options 0, oba vstupní body přijmou fallback na file stream jen pro čtení: pokud platforma nemá kód pro mapování nebo volání mapování selže, dokument se přesto otevře a všechna čtení povedou přes běžný file stream. S příznakem PDFlibPas přijme vstup pouze tehdy, když bylo vytvořeno první view, a odmítnutí ohlásí přes LastErrorCode 401 místo načtení dokumentu, který by potichu fungoval přesně jako stará cesta
V systému Windows mapovaný stream otevře druhý handle jen pro čtení s FILE_SHARE_READ, FILE_SHARE_WRITE a FILE_SHARE_DELETE plus FILE_FLAG_RANDOM_ACCESS, vytvoří nad ním mapování PAGE_READONLY a první okno namapuje už v konstruktoru. Dychtivé mapování je celý smysl: selhání požadavku na mapování se objeví v LoadFromMappedFile, nikoli při prvním líném čtení objektu uprostřed renderovací úlohy. Je ale důležité vědět, kde záruka končí. Kód mapování se kompiluje pouze pro cíle Windows a soubor s nulovou délkou se o mapování vůbec nepokusí, takže PDF_MAPPED_FILE_REQUIRE_MAPPING je požadavek, který může legitimně selhat, nikoli přenosný slib. Záporný WindowSize nebo jakýkoli bit v Options kromě jediné dokumentované hodnoty se rovnou odmítne stejnou chybou 401
Jedno okno přemapované na alokační granularitu
V paměti se vždy drží jen jedno view a právě to udržuje využití adresního prostoru nezávislé na velikosti souboru. WindowSize 0 zvolí 64 MiB, hodnota pod systémovou alokační granularitou se na ni zvýší, hodnota nad 1 GiB se omezí a výsledek se zaokrouhlí nahoru na celý počet granulačních jednotek, v systému Windows 65536 bajtů, pokud GetSystemInfo neohlásí jiné dwAllocationGranularity. Když čtení dopadne mimo aktuální view, PDFlibPas ho odmapuje, zarovná požadovaný offset dolů na hranici granularity a na tomto místě namapuje nové okno. Poslední okno se omezí na fyzickou velikost souboru, takže view nikdy nepřesáhne jeho konec
Jedno čtení může překročit libovolný počet oken: smyčka zkopíruje tolik, kolik poskytne aktuální view, přemapuje a pokračuje, a požadavek za koncem vrátí zkrácený počet místo selhání. PDFlibPas záměrně nedělá to, že by vám předal pointer do view, protože další čtení přes hranici okna ho zneplatní a žádný volající by se před tím rozumně neochránil. Mapované bajty se kopírují přímo do bufferů vlastněných parserem, čímž odpadá další vstupní buffer souboru a přepínání pozice, ale knihovna netvrdí, že výsledné úložiště parseru je zero-copy. Okénkové čtení na straně čtení se skládá i se stranou zápisu, protože posun odkazů na úrovni bajtů při rychlém slučování PDF streamuje bajty objektů ven, zatímco mapovaný zdroj je streamuje dovnitř. Kompromis velikosti okna je zřejmý: menší okno zabere méně adresního prostoru a přemapovává častěji, což je obvykle správná volba uvnitř 32bitového procesu
Co chrání zámek a co hlásí GetMappedFileInfo
Jedna kritická sekce pokrývá mapované view, kurzor fallbackového souboru, logickou pozici i statistiky a rozdělení mezi dvě metody čtení z ní přímo vyplývá. ReadAt získá zámek a zavolá interní čtečku bez zámku; Read získá stejný zámek, zavolá tutéž interní funkci na aktuální logické pozici a pak ji posune. Opětovné použití interní funkce místo veřejného ReadAt zabraňuje rekurzivnímu zamykání a držení zámku přes celou kopírovací smyčku udržuje přemapování jediného okna správné i při souběžných voláních. Před portováním stojí za znalost jeden detail Free Pascalu: jeho jednotka FPC Windows deklaruje vlastní record jménem TCriticalSection, takže pole i jeho vytvoření musí být zapsány jako SyncObjs.TCriticalSection. Delphi neúplný kvalifikovaný tvar bez problémů zkompiluje, FPC ho vyřeší na record bez Create, Enter a Leave
var
Pdf: TPDFlib;
Handle, PageRef: Integer;
Info: WideString;
begin
Pdf := TPDFlib.Create;
try
Handle := Pdf.DAOpenMappedFile('archive-2026.pdf', '',
16 * 1024 * 1024, PDF_MAPPED_FILE_REQUIRE_MAPPING);
if Handle = 0 then
Exit;
try
PageRef := Pdf.DAFindPage(Handle, 1);
Writeln(Pdf.DAExtractPageText(Handle, PageRef, 0));
// {"memoryMapped":true,"fileSize":...,"remapCount":...}
if Pdf.DAGetMappedFileInfo(Handle, Info) = 1 then
Writeln(Info);
finally
Pdf.DACloseFile(Handle);
end;
finally
Pdf.Free;
end;
end;
memoryMappedje false vždy, když je aktivní přenosný fallback na file stream, a je to jediné pole dokazující, že mapování nikdy nevzniklowindowSizeje výsledné zarovnané okno, nikoli vyžádaná hodnota, amappedBytesje v koncovém okně menšímappedOffsetje na alokaci zarovnaný začátek drženého view nebo -1, když žádné view právě není aktivníreadCallspočítá úspěšné požadavky na čtení uvnitř rozsahu,bytesReadpočítá bajty zkopírované volajícím aremapCountzahrnuje počáteční view
Cílené regresní testy pokrývají absolutní čtení přes hranice oken, zachování logického kurzoru, zkrácená čtení na konci, neplatné offsety, odmítnuté zápisy, přemapování mezi oddělenými okny, odloženou extrakci 220KB nekomprimované přílohy a neplatnost statistik po DACloseFile; headless sady Win32 i Win64 objevily po 1467 testech a všechny prošly bez ignorovaných, neúspěšných, chybových nebo uniklých výsledků. Pokud v Delphi nebo C++Builderu pracujete s gigabajtovými PDF a profiler stále ukazuje na čtení souboru místo na parsování, stojí za to mapované vstupní body odpoledne měřit a GetMappedFileInfo vám řekne, zda jste skutečně získali mapování. Kompletní reference API a trial build najdete na stránce knihovny PDFlibPas Delphi PDF