PDFlibPas може да отвори локален PDF през ограничен read-only memory-mapped view: LoadFromMappedFile и DAOpenMappedFile поддържат точно един плъзгащ се прозорец върху файла, remap-ват го при нужда и обслужват всеки object slice чрез четене по абсолютен offset. Delphi PDF library никога не държи целия source в паметта, така че използваното address space остава равно, докато файлът расте. Дизайнът съществува за една конкретна workload: гигабайтови PDF-и, при които parser-ът е приключил loading-а, но продължава да се връща към диска, object по object и stream fragment по stream fragment
Защо sparse read-овете остават скъпи след зареждането на PDF-а?
Зареждането на PDF не означава, че четенето е приключило, а при многогигабайтов файл точно тази разлика поглъща времето. Cross-reference table или cross-reference stream (ISO 32000-1 §7.5.4 и §7.5.8) записва само откъде започва всеки indirect object. Байтовете идват по-късно, когато се render-ва страница, decode-ва се font program или се извлича embedded file stream (ISO 32000-1 §7.11.4). Архив от 2 GB с десетки хиляди object-и се превръща в десетки хиляди малки unordered read-ове и никой от тях не е известен при load
Пътят на тези read-ове преди минаваше през общ Seek, последван от Read върху един positional stream, и се проваляше в две посоки едновременно. Всеки fragment плаща за file read, дори когато страницата вече е в operating-system cache-а, а cursor-ът е споделено mutable state, така че локален файл и byte-range source-ът зад progressive PDF range loading с prefetch не можеха да използват един и същ parser code без борба за position-а. PDFlibPas решава и двете, като превръща четенето по absolute offset от optimization в contract
Какво гарантира TPDFReadAtStream?
TPDFReadAtStream гарантира read по абсолютен offset, който нито зависи от, нито променя логическия cursor на stream-а. Това е abstract TStream descendant с точно един virtual method и от него произлизат двата cursor-independent source-а в library-то: TReadOnlyMappedFileStream за локални файлове и TByteRangeStream за remote source-ове, обслужвани чрез range. Object-slice reader-ът проверява веднъж дали source-ът е TPDFReadAtStream и се връща към старата последователност seek-then-read, когато не е, така че обикновен file stream или memory stream продължава да работи без промяна
type
// Read-only stream-ове, чиито absolute read-ове избягват shared Seek плюс Read
TPDFReadAtStream = class(TStream)
public
function ReadAt(Offset: Int64; var Buffer;
Count: LongInt): LongInt; virtual; abstract;
end;
// Windowed read-only достъп до един локален файл
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;
Разликата е по-важна, отколкото подсказва signature-ът. ReadAt използва подадения offset и оставя Position точно там, където е бил, което позволява на вложените parser нива да издават read-ове без save-and-restore танц около всяко извикване. TReadOnlyMappedFileStream все пак имплементира Read, Seek и Size като всеки друг TStream, Seek ограничава логическата позиция в рамките на файла, а Write винаги връща 0, защото source-ът е отворен read-only
Отваряне на PDF през mapped view в Delphi
Две explicit entry point-а отварят mapped source и нито един от тях не променя поведението на вече използваните entry point-и. LoadFromMappedFile зарежда и избира document; DAOpenMappedFile връща Direct Access handle към същия файл, което е режимът за сливане и разделяне на гигабайтови PDF-и през Direct Access. LoadFromFile и DAOpenFile запазват своите file-sharing, error и compatibility semantics, така че за caller-и, които не opt-in-ват, нищо не се променя. И двата mapped entry point-а приемат заявен WindowSize в bytes и Options bitmask, като и за двата 0 е допустима стойност
var
Pdf: TPDFlib;
Payload: AnsiString;
Info: WideString;
begin
Pdf := TPDFlib.Create;
try
// WindowSize 0 избира default от 64 MiB; mapping-ът е задължителен тук
if Pdf.LoadFromMappedFile('archive-2026.pdf', '', 0,
PDF_MAPPED_FILE_REQUIRE_MAPPING) <> 1 then
raise Exception.CreateFmt('mapped open refused, LastErrorCode=%d',
[Pdf.LastErrorCode]);
// Deferred extraction вече обхожда mapped windows вместо да seek-ва
Payload := Pdf.GetEmbeddedFileContentToString(1);
if Pdf.GetMappedFileInfo(Info) = 1 then
Writeln(Info);
finally
Pdf.Free;
end;
end;
Какво всъщност налага PDF_MAPPED_FILE_REQUIRE_MAPPING?
PDF_MAPPED_FILE_REQUIRE_MAPPING превръща тихия fallback в незабавен и диагностицируем failure още при open. Когато Options е 0, и двата entry point-а приемат read-only file-stream fallback: ако platform-ата няма mapping code или mapping call-ът се провали, document-ът пак се отваря и всеки read минава през обикновен file stream. С flag-а PDFlibPas приема input-а само когато първият view е установен и докладва отказа чрез LastErrorCode 401, вместо да зареди document, който тихо се държи точно като стария path
В Windows mapped stream-ът отваря втори read-only handle с FILE_SHARE_READ, FILE_SHARE_WRITE и FILE_SHARE_DELETE плюс FILE_FLAG_RANDOM_ACCESS, създава PAGE_READONLY mapping върху него и map-ва първия window вътре в constructor-а. Eager mapping-ът е цялата идея: failure за „mapping required“ се появява при LoadFromMappedFile, а не при първия lazy object read по средата на rendering job. Важно е обаче да се каже къде спира гаранцията. Mapping code-ът се компилира само за Windows target-и, а файл с нулев размер изобщо не опитва mapping, така че PDF_MAPPED_FILE_REQUIRE_MAPPING е заявка, която легитимно може да се провали, а не portable promise. Отрицателен WindowSize или който и да е bit в Options, различен от единствената документирана стойност, се отхвърля директно със същата грешка 401
Един прозорец, remap-нат към allocation granularity
Задържа се само един view и точно това прави използването на address space независимо от размера на файла. WindowSize 0 избира 64 MiB; стойност под system allocation granularity се повишава до нея; стойност над 1 GiB се ограничава; резултатът се закръгля нагоре до цяло число granularity units, 65536 bytes в Windows, освен ако GetSystemInfo не върне различен dwAllocationGranularity. Когато read попадне извън текущия view, PDFlibPas unmap-ва стария, подравнява заявения offset надолу към granularity boundary и map-ва нов window там. Последният window се ограничава до физическия размер на файла, така че view-ът никога не излиза след края му
Един read може да пресече произволен брой windows: loop-ът копира каквото текущият view може да даде, remap-ва и продължава, а заявка, която излиза след края, връща short count вместо failure. PDFlibPas нарочно не ви дава pointer към view-а, защото следващият cross-window read го инвалидира и никой caller не би могъл разумно да се защити от това. Mapped bytes се копират директно в parser-owned destination buffers, което премахва допълнителния file input buffer и превключването на position-а, но library-то не твърди zero-copy за крайното parser storage. Windowing-ът при read се съчетава и със страната за write, тъй като byte-level reference shifting при бързо PDF merge извежда object bytes, докато mapped source-ът ги подава навътре. Trade-off-ът за window size е очевиден: по-малък window държи по-малко address space и remap-ва по-често, което обикновено е правилният избор в 32-bit process
Какво защитава lock-ът и какво докладва GetMappedFileInfo
Един critical section покрива mapped view-а, fallback file cursor-а, логическата позиция и statistics-ите, а разделянето между двата read метода следва директно от това. ReadAt взема lock-а и извиква lock-free internal reader; Read взема същия lock, извиква същия internal reader при текущата логическа позиция и после я придвижва. Повторното използване на internal function вместо public ReadAt избягва recursive locking, а задържането на lock-а през целия copy loop запазва коректността на single-window remap при concurrent call-ове. Един Free Pascal detail си струва да се знае преди port: FPC Windows unit-ът декларира собствен record с име TCriticalSection, затова field-ът и конструирането му трябва да бъдат написани като SyncObjs.TCriticalSection. Delphi компилира неквалифицираната форма без проблем, а FPC я разрешава до record без Create, Enter или 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;
memoryMappedе false, когато е активен portable file-stream fallback-ът, и е единственото поле, което доказва, че mapping никога не е установенwindowSizeе ефективният подравнен window, а не заявената стойност, докатоmappedBytesе по-малък в tail window-аmappedOffsetе allocation-aligned началото на задържания view или -1, когато в момента няма активен viewreadCallsброи успешните in-range read заявки,bytesReadброи байтовете, копирани към caller-и, аremapCountвключва първия view
Targeted regression-ите покриват absolute read-ове през граница на window, запазване на логическия cursor, short read-ове в края, невалидни offset-и, отхвърлени write-ове, remapping между разделени windows, deferred extraction на 220 KB incompressible attachment и невалидни statistics след DACloseFile; headless suite-овете за Win32 и Win64 откриха по 1467 теста и преминаха всички без ignored, failed, errored или leaked резултати. Ако работите с гигабайтови PDF-и в Delphi или C++Builder и profiler-ът ви продължава да сочи file read-ове вместо parsing, mapped-file entry point-ите заслужават един следобед измерване, а GetMappedFileInfo ще ви каже дали действително сте получили mapping. Пълният API reference и trial build са на страницата на PDFlibPas Delphi PDF library