A PDFlibPas korlátozott, csak olvasható memóriatérképes nézeten keresztül nyit meg helyi PDF-et: a LoadFromMappedFile és a DAOpenMappedFile pontosan egy csúszó ablakot tart fenn a fájlon, szükség szerint újratérképezi, és minden objektumszeletet abszolút offset alapú olvasással szolgál ki. A Delphi PDF library soha nem tartja memóriában a teljes forrást, ezért a címtérhasználat a fájl növekedésével is állandó marad. A megoldás egyetlen terhelésre készült: gigabájtos PDF-ekre, amelyeknél a parser befejezte a betöltést, de objektumról objektumra és streamrészletről streamrészletre még visszajár a lemezhez
Miért marad drága a ritka olvasás a PDF betöltése után is?
A PDF betöltése nem jelenti az olvasás végét, és több gigabájtos fájlnál éppen ez a rés viszi el az időt. A kereszt-hivatkozási tábla vagy stream (ISO 32000-1 §7.5.4 és §7.5.8) csak azt rögzíti, hogy hol kezdődik az egyes indirekt objektumok sora. A bájtok később érkeznek, amikor egy oldalt renderelünk, egy fontprogramot dekódolunk, vagy egy beágyazott fájl streamjét (ISO 32000-1 §7.11.4) nyerjük ki. Egy 2 GB-os archívum több tízezer objektuma több tízezer kis, rendezetlen olvasássá válik, amelyek közül egyiket sem ismerjük a betöltéskor
Ezek az olvasások korábban egy közös Seek, majd ugyanazon pozicionális streamen végzett Read útvonalán mentek, és ez egyszerre két irányban hibás. Minden részlet fizet egy fájlolvasást akkor is, ha az oldal már az operációs rendszer cache-ében van, ráadásul a kurzor megosztott, módosítható állapot, ezért egy helyi fájl és a prefetch-csel végzett progresszív PDF range-betöltés mögötti byte-range forrás nem futhatott ugyanazzal a parserkóddal anélkül, hogy össze ne akadtak volna a pozíción. A PDFlibPas mindkettőt azzal javítja, hogy az abszolút offset alapú olvasást optimalizálásból szerződéssé emeli
Mit garantál a TPDFReadAtStream?
A TPDFReadAtStream olyan abszolút offseten végzett olvasást garantál, amely nem függ a logikai streamkurzortól, és nem is módosítja azt. Ez egy absztrakt TStream-leszármazott, pontosan egy virtuális metódussal, és a library mindkét kurzorfüggetlen forrása ebből származik: a TReadOnlyMappedFileStream helyi fájlokhoz, a TByteRangeStream távolról kiszolgált range-ekhez. Az objektumszelet-olvasó egyszer ellenőrzi, hogy a forrás TPDFReadAtStream-e, és ha nem, visszaesik a régi seek-then-read sorozatra, így a hagyományos fájl- vagy memorystream változatlanul működik
type
// A csak olvasható streamek abszolút olvasásai elkerülik a közös Seek és Read párost
TPDFReadAtStream = class(TStream)
public
function ReadAt(Offset: Int64; var Buffer;
Count: LongInt): LongInt; virtual; abstract;
end;
// Ablakos, csak olvasható hozzáférés egy helyi fájlhoz
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;
A különbség fontosabb, mint amit a szignatúra sugall. A ReadAt a neki átadott offsetet használja, és a Position értékét pontosan változatlanul hagyja, így a beágyazott parser-szintek minden hívás köré épített mentés-visszaállítás nélkül olvashatnak. A TReadOnlyMappedFileStream ugyanúgy megvalósítja a Read, Seek és Size műveleteket, mint bármely más TStream, a Seek a fájlon belülre korlátozza a logikai pozíciót, a Write pedig mindig 0-val tér vissza, mert a forrás csak olvasható
PDF megnyitása leképezett nézeten keresztül Delphiben
Két explicit belépési pont nyit meg leképezett forrást, és egyik sem változtatja meg a már használt belépési pontok működését. A LoadFromMappedFile betölti és kiválasztja a dokumentumot, a DAOpenMappedFile pedig Direct Access handle-t ad vissza ugyanahhoz a fájlhoz; ezt érdemes használni, amikor gigabájtos PDF-eket egyesítesz és darabolsz Direct Access segítségével. A LoadFromFile és DAOpenFile fájlmegosztási, hiba- és kompatibilitási szemantikája változatlan, ezért az opt-in nélkül működő hívóknál semmi sem mozdul el. Mindkét leképezett belépési pont bájtban kért WindowSize értéket és Options bitmaszkot kap, és mindkettőnél elfogadható a 0 mindkét paraméterhez
var
Pdf: TPDFlib;
Payload: AnsiString;
Info: WideString;
begin
Pdf := TPDFlib.Create;
try
// A WindowSize 0 a 64 MiB alapértéket választja; itt a leképezés kötelező
if Pdf.LoadFromMappedFile('archive-2026.pdf', '', 0,
PDF_MAPPED_FILE_REQUIRE_MAPPING) <> 1 then
raise Exception.CreateFmt('mapped open refused, LastErrorCode=%d',
[Pdf.LastErrorCode]);
// A késleltetett kinyerés most leképezett ablakokon jár végig seek helyett
Payload := Pdf.GetEmbeddedFileContentToString(1);
if Pdf.GetMappedFileInfo(Info) = 1 then
Writeln(Info);
finally
Pdf.Free;
end;
end;
Mit kényszerít ki valójában a PDF_MAPPED_FILE_REQUIRE_MAPPING?
A PDF_MAPPED_FILE_REQUIRE_MAPPING a csendes fallbacket azonnali, diagnosztizálható megnyitási hibává alakítja. Ha az Options értéke 0, mindkét belépési pont elfogadja a csak olvasható fájlstreames fallbacket: ha a platformon nincs leképezési kód, vagy a leképezési hívás meghiúsul, a dokumentum akkor is megnyílik, és minden olvasás hagyományos fájlstreamen megy tovább. A flaggel a PDFlibPas csak akkor fogadja el a bemenetet, ha az első nézet létrejött, és a dokumentum csendes visszaesés nélküli elutasítását a LastErrorCode 401 jelzi, nem pedig egy olyan betöltést kapsz, amely észrevétlenül pontosan a régi úton működik
Windowson a leképezett stream egy második, csak olvasható handle-t nyit FILE_SHARE_READ, FILE_SHARE_WRITE és FILE_SHARE_DELETE megosztással, valamint FILE_FLAG_RANDOM_ACCESS jelzővel, erre PAGE_READONLY leképezést hoz létre, és a konstruktorban leképezi az első ablakot. A korai leképezés a lényeg: a „mapping required” hiba már a LoadFromMappedFile hívásnál megjelenik, nem pedig az első lusta objektumolvasásnál egy renderelési feladat felénél. Azt azonban tisztázni kell, meddig tart a garancia. A leképezési kód csak Windows célokon fordul le, a nulla bájtos fájl pedig egyáltalán nem próbál leképezést, ezért a PDF_MAPPED_FILE_REQUIRE_MAPPING olyan kérés, amely jogszerűen meghiúsulhat, nem pedig hordozható ígéret. A negatív WindowSize, illetve az Options dokumentált egyetlen értékén kívüli bármely bit azonnal, ugyanazzal a 401-es hibával elutasításra kerül
Egy ablak, az allokációs granularitáshoz igazítva újraleképezve
Mindig csak egy nézet marad meg, ezért a címtérhasználat független a fájl méretétől. A 0 értékű WindowSize 64 MiB-t választ, a rendszer allokációs granularitásánál kisebb érték erre a minimumra emelkedik, az 1 GiB-nál nagyobb érték pedig korlátozódik, majd az eredményt egész granularitási egységekre kerekíti a kód: Windowson ez 65536 bájt, hacsak a GetSystemInfo más dwAllocationGranularity értéket nem jelent. Amikor egy olvasás kilép az aktuális nézetből, a PDFlibPas megszünteti azt, a kért offsetet lefelé egy granularitási határra igazítja, és ott új ablakot képez. Az utolsó ablakot a fizikai fájlmérethez igazítja, ezért a nézet soha nem nyúlik túl a fájl végén
Egyetlen olvasás tetszőleges számú ablakon átnyúlhat: a ciklus bemásolja, amit az aktuális nézet ki tud szolgálni, újraleképez, majd folytatja, a fájl végén túlfutó kérés pedig hiba helyett rövid darabszámmal tér vissza. A PDFlibPas szándékosan nem ad a kezedbe nézeten belüli pointert, mert a következő ablakváltó olvasás érvényteleníti, és ezt egyetlen ésszerű hívó sem tudná biztonságosan kezelni. A leképezett bájtokat közvetlenül a parser saját célpuffereibe másolja, így elmarad a külön fájl-bemeneti puffer és a pozícióváltogatás, de a library nem ígér zero-copy végső parser-tárolást. Az ablakos olvasás az írási oldallal is összeilleszthető, mert a gyors PDF-egyesítés közbeni byte-szintű referenciaeltolás kivezeti az objektumbájtokat, miközben a leképezett forrás bevezeti őket. Az ablakméret kompromisszuma kézenfekvő: a kisebb ablak kevesebb címtartományt tart meg, de gyakrabban képez újra, ami általában helyes választás 32 bites folyamatban
Mit véd a lock, és mit jelent a GetMappedFileInfo?
Egyetlen kritikus szakasz védi a leképezett nézetet, a fallback fájlkurzorát, a logikai pozíciót és a statisztikát, és ebből közvetlenül következik a két olvasási metódus felosztása. A ReadAt felveszi a lockot, majd a lockmentes belső olvasót hívja; a Read ugyanazt a lockot veszi fel, a belső olvasót az aktuális logikai pozíción hívja, majd előrelépteti azt. A belső függvény újrahasznosítása a publikus ReadAt helyett kerüli a rekurzív lockolást, a teljes másolási ciklus alatt tartott lock pedig a párhuzamos hívások mellett is helyessé teszi az egyablakos újraleképezést. Egy Free Pascal-részletet érdemes ismerni portolás előtt: az FPC Windows unitja saját, TCriticalSection nevű rekordot deklarál, ezért a mezőt és a példányosítást SyncObjs.TCriticalSection alakban kell írni. A Delphi gond nélkül lefordítja a minősítés nélküli alakot, az FPC viszont egy olyan rekordra oldja fel, amelynek nincs Create, Enter vagy Leave metódusa
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;
memoryMappedhamis, amikor a hordozható fájlstreames fallback aktív, és ez az egyetlen mező, amely bizonyítja, hogy nem jött létre leképezéswindowSizea tényleges, igazított ablak, nem a kért érték, amappedBytespedig a záró ablakban kisebb ennélmappedOffseta megtartott nézet allokációhoz igazított kezdete, vagy -1, ha jelenleg nincs aktív nézetreadCallsa sikeres, fájlon belüli olvasási kéréseket számolja, abytesReada hívóknak másolt bájtokat, aremapCountpedig a kezdeti nézetet is tartalmazza
A célzott regressziók lefedik az ablakokon átnyúló abszolút olvasásokat, a logikai kurzor megőrzését, a fájlvégi rövid olvasásokat, az érvénytelen offseteket, az elutasított írásokat, az egymástól távoli ablakok közötti újraleképezést, egy 220 KB-os, nehezen tömöríthető melléklet késleltetett kinyerését, valamint a statisztika érvénytelenedését a DACloseFile után; a Win32 és Win64 headless tesztsorozat egyenként 1467 tesztet fedezett fel, és mind átment, kihagyott, hibás, kivételes vagy szivárgó eredmény nélkül. Ha gigabájtos PDF-ekkel dolgozol Delphiben vagy C++Builderben, és a profiler mindig a fájlolvasásokra mutat, nem a parsingra, a leképezett-fájlos belépési pontokra érdemes egy délutánt mérésre szánni, a GetMappedFileInfo pedig megmutatja, hogy valóban leképezést kaptál-e. A teljes API-referencia és a próbaverzió a PDFlibPas Delphi PDF library oldalán található