PDFlibPas poate deschide un PDF local printr-o vizualizare memory-mapped read-only cu dimensiune controlată: LoadFromMappedFile și DAOpenMappedFile păstrează exact o fereastră glisantă peste fișier, o remapează la nevoie și servesc fiecare felie de obiect prin citiri la offset absolut. Biblioteca PDF pentru Delphi nu ține întreaga sursă în memorie, așa că utilizarea spațiului de adrese rămâne constantă pe măsură ce fișierul crește. Designul este pentru un singur tip de workload: PDF-uri de gigabytes în care parser-ul a terminat încărcarea și continuă să revină pe disc, obiect cu obiect, fragment de stream cu fragment de stream
De ce rămân scumpe citirile rare după încărcarea PDF-ului?
Încărcarea unui PDF nu înseamnă că lectura s-a încheiat, iar într-un fișier de mai multe gigabytes tocmai această diferență consumă timpul. Un cross-reference table sau cross-reference stream (ISO 32000-1 §7.5.4 și §7.5.8) înregistrează doar unde începe fiecare indirect object. Bytes ajung mai târziu, când este randată o pagină, este decodat un font program sau este extras un embedded file stream (ISO 32000-1 §7.11.4). O arhivă de 2 GB cu zeci de mii de obiecte devine zeci de mii de citiri mici și neordonate, iar niciuna nu este cunoscută la momentul încărcării
Ruta pe care mergeau aceste citiri era un Seek partajat urmat de Read pe un singur stream pozițional și eșuează în două direcții deodată. Fiecare fragment plătește o citire de fișier chiar și când pagina este deja în cache-ul sistemului de operare, iar cursorul este stare mutable partajată, astfel încât un fișier local și sursa byte-range din spatele încărcării progresive PDF pe intervale cu prefetch nu puteau rula același cod de parser fără să se lupte pentru poziție. PDFlibPas repară ambele probleme transformând citirea la offset absolut din optimizare în contract
Ce garantează TPDFReadAtStream?
TPDFReadAtStream garantează o citire la un offset absolut care nici nu depinde de cursorul logic al stream-ului, nici nu îl perturbă. Este un descendent abstract de TStream cu exact o metodă virtuală, iar ambele surse independente de cursor din bibliotecă derivă din el: TReadOnlyMappedFileStream pentru fișiere locale și TByteRangeStream pentru surse remote servite pe intervale. Reader-ul de felii de obiect întreabă o singură dată dacă sursa este un TPDFReadAtStream și revine la secvența veche seek-then-read când nu este, așa că un file stream obișnuit sau un memory stream continuă să funcționeze neschimbat
type
// Stream-urile read-only ale căror citiri absolute evită un Seek plus Read partajat
TPDFReadAtStream = class(TStream)
public
function ReadAt(Offset: Int64; var Buffer;
Count: LongInt): LongInt; virtual; abstract;
end;
// Acces read-only prin fereastră la un singur fișier local
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;
Diferența contează mai mult decât sugerează semnătura. ReadAt folosește offset-ul primit și lasă Position exact unde era, ceea ce permite nivelurilor imbricate de parser să emită citiri fără dansul save-and-restore din jurul fiecărui apel. TReadOnlyMappedFileStream implementează în continuare Read, Seek și Size ca orice alt TStream, Seek limitează poziția logică în interiorul fișierului, iar Write returnează mereu 0 deoarece sursa este deschisă read-only
Deschiderea unui PDF printr-o vizualizare mapped în Delphi
Două entry point-uri explicite deschid o sursă mapped și niciunul nu schimbă comportamentul entry point-urilor pe care le folosești deja. LoadFromMappedFile încarcă și selectează un document; DAOpenMappedFile returnează un Direct Access handle peste același fișier, modul potrivit când unești și separi PDF-uri de gigabytes prin Direct Access. LoadFromFile și DAOpenFile își păstrează neschimbate semantica de sharing, erori și compatibilitate, așa că nimic nu se schimbă pentru caller-ii care nu optează pentru noua rută. Ambele entry point-uri mapped primesc un WindowSize cerut în bytes și un bitmask Options, iar pentru oricare dintre ele acceptă 0
var
Pdf: TPDFlib;
Payload: AnsiString;
Info: WideString;
begin
Pdf := TPDFlib.Create;
try
// WindowSize 0 selectează valoarea implicită de 64 MiB; mapping-ul este obligatoriu aici
if Pdf.LoadFromMappedFile('archive-2026.pdf', '', 0,
PDF_MAPPED_FILE_REQUIRE_MAPPING) <> 1 then
raise Exception.CreateFmt('mapped open refused, LastErrorCode=%d',
[Pdf.LastErrorCode]);
// Extragerea amânată parcurge acum ferestre mapped în loc să facă seek
Payload := Pdf.GetEmbeddedFileContentToString(1);
if Pdf.GetMappedFileInfo(Info) = 1 then
Writeln(Info);
finally
Pdf.Free;
end;
end;
Ce impune de fapt PDF_MAPPED_FILE_REQUIRE_MAPPING?
PDF_MAPPED_FILE_REQUIRE_MAPPING transformă un fallback silențios într-un failure imediat și diagnosticabil la deschidere. Cu Options lăsat la 0, ambele entry point-uri acceptă fallback-ul la file stream read-only: dacă platforma nu are cod de mapping sau apelul de mapping eșuează, documentul se deschide în continuare și fiecare citire trece printr-un file stream normal. Cu flag-ul activat, PDFlibPas acceptă input-ul numai când a fost stabilită prima view și raportează refuzul prin LastErrorCode 401, în loc să încarce un document care lucrează în tăcere exact ca ruta veche
Pe Windows, stream-ul mapped deschide un al doilea handle read-only cu FILE_SHARE_READ, FILE_SHARE_WRITE și FILE_SHARE_DELETE, plus FILE_FLAG_RANDOM_ACCESS, creează peste el un mapping PAGE_READONLY și mapează prima fereastră în constructor. Mapping-ul eager este esențial: un failure de tip "mapping required" apare la LoadFromMappedFile, nu la prima citire lazy a unui obiect în mijlocul unui job de rendering. Totuși, trebuie să fie clar unde se oprește garanția. Codul de mapping este compilat doar pentru target-uri Windows, iar un fișier de zero bytes nu încearcă deloc un mapping, deci PDF_MAPPED_FILE_REQUIRE_MAPPING este o cerere care poate eșua legitim, nu o promisiune portabilă. Un WindowSize negativ sau orice bit în Options în afară de singura valoare documentată este respins direct cu aceeași eroare 401
O singură fereastră, remapată la granulația alocării
Este păstrată mereu o singură view, iar asta menține utilizarea spațiului de adrese independentă de dimensiunea fișierului. Un WindowSize de 0 selectează 64 MiB; o valoare sub granulația de alocare a sistemului este ridicată la aceasta; o valoare peste 1 GiB este limitată; iar rezultatul este rotunjit în sus la un număr întreg de unități de granulație, 65536 bytes pe Windows dacă GetSystemInfo nu raportează un alt dwAllocationGranularity. Când o citire ajunge în afara view-ului curent, PDFlibPas o demapează, aliniază offset-ul cerut în jos la o limită de granulație și mapează acolo o fereastră nouă. Fereastra finală este limitată la dimensiunea fizică a fișierului, astfel încât view-ul nu trece niciodată dincolo de sfârșitul fișierului
O singură citire poate traversa oricâte ferestre: bucla copiază cât poate furniza view-ul curent, remapează și continuă, iar o cerere care trece de sfârșit returnează un count scurt în loc să eșueze. Ceea ce PDFlibPas nu face în mod deliberat este să îți dea un pointer în view, deoarece următoarea citire cross-window îl invalidează și niciun caller nu s-ar putea apăra rezonabil de asta. Bytes mapeți sunt copiați direct în bufferele de destinație deținute de parser, ceea ce elimină buffer-ul suplimentar de input al fișierului și schimbarea poziției, dar biblioteca nu pretinde zero-copy pentru stocarea finală a parser-ului. Windowing-ul la citire se combină și cu partea de scriere, deoarece schimbarea referințelor byte-level în timpul unui merge PDF rapid transmite bytes de obiect în afară în timp ce sursa mapped îi transmite înăuntru. Compromisul dimensiunii ferestrei este cel evident: o fereastră mai mică ocupă mai puțin spațiu de adrese și remapează mai des, ceea ce este de obicei alegerea corectă într-un proces pe 32 de biți
Ce protejează lock-ul și ce raportează GetMappedFileInfo
O singură critical section acoperă view-ul mapped, cursorul fișierului de fallback, poziția logică și statisticile, iar separarea dintre cele două metode de citire decurge direct de aici. ReadAt ia lock-ul și apelează reader-ul intern fără lock; Read ia același lock, apelează aceeași funcție internă la poziția logică actuală, apoi o avansează. Refolosirea funcției interne în locul lui ReadAt public evită locking-ul recursiv, iar păstrarea lock-ului pe tot parcursul buclei de copiere menține corectitudinea remapării unei singure ferestre în apeluri concurente. Un detaliu Free Pascal merită știut înainte de portare: unit-ul FPC Windows își declară propriul record numit TCriticalSection, așa că field-ul și construcția lui trebuie scrise ca SyncObjs.TCriticalSection. Delphi compilează fără probleme forma necalificată; FPC o rezolvă la un record fără Create, Enter sau 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;
memoryMappedeste false ori de câte ori este activ fallback-ul portabil la file stream și este singurul câmp care dovedește că nu a fost stabilit niciun mappingwindowSizeeste fereastra efectivă aliniată, nu valoarea cerută, iarmappedByteseste mai mic decât ea în fereastra de coadămappedOffseteste începutul aliniat la alocare al view-ului păstrat sau -1 când nu există momentan nicio view activăreadCallsnumără cererile de citire reușite în interval,bytesReadnumără bytes copiați către caller, iarremapCountinclude view-ul inițial
Regression-urile țintite acoperă citiri absolute cross-window, păstrarea cursorului logic, citiri scurte la coadă, offset-uri invalide, writes respinse, remaparea între ferestre separate, extragerea amânată a unui attachment incompressible de 220 KB și invalidarea statisticilor după DACloseFile; suitele headless Win32 și Win64 au descoperit fiecare 1467 de teste și le-au trecut pe toate fără rezultate ignorate, eșuate, cu erori sau cu leak-uri. Dacă lucrezi cu PDF-uri de gigabytes în Delphi sau C++Builder și profiler-ul indică în continuare citirile de fișier, nu parsing-ul, entry point-urile mapped-file merită o după-amiază de măsurători, iar GetMappedFileInfo îți spune dacă ai obținut cu adevărat un mapping. Referința API completă și un trial build sunt pe pagina bibliotecii PDF PDFlibPas pentru Delphi