Tehnički članak

Čitanje PDF-a uz memory-mapped klizni prozor u Delphi-ju

PDFlibPas može da otvori lokalni PDF kroz ograničeni read-only memory-mapped prikaz: LoadFromMappedFile i DAOpenMappedFile drže tačno jedan klizni prozor nad fajlom, po potrebi ga ponovo mapiraju i svaki isečak objekta isporučuju čitanjem sa apsolutnim pomakom. Delphi PDF biblioteka nikada ne drži ceo izvor u memoriji, pa upotreba adresnog prostora ostaje ravna dok fajl raste. Dizajn postoji za jedno konkretno opterećenje: gigabajtne PDF-ove kod kojih je parser završio početno učitavanje, ali se i dalje vraća na disk, objekat po objekat i fragment stream-a po fragment stream-a

Zašto su retka čitanja skupa i nakon učitavanja PDF-a?

Učitavanje PDF-a ne znači da je čitanje završeno, a kod fajla od više gigabajta upravo ta razlika odnosi vreme. Cross-reference tabela ili cross-reference stream (ISO 32000-1 §7.5.4 i §7.5.8) beleži samo gde svaki indirektni objekat počinje. Bajtovi stižu kasnije, kada se stranica renderuje, dekodira font program ili izvuče ugrađeni file stream (ISO 32000-1 §7.11.4). Arhiva od 2 GB sa desetinama hiljada objekata pretvara se u desetine hiljada malih, nepovezanih čitanja, a nijedno od njih nije poznato u trenutku učitavanja

Putanja kojom su ta čitanja ranije išla bila je deljeni Seek, pa Read nad jednim pozicionim stream-om, i ona otkazuje u oba pravca. Svaki fragment plaća čitanje fajla čak i kada je stranica već u kešu operativnog sistema, a kursor je deljivo promenljivo stanje, pa lokalni fajl i byte-range izvor iza progresivnog učitavanja PDF opsega sa prefetch-om nisu mogli da koriste isti parser bez borbe oko pozicije. PDFlibPas rešava oba problema tako što čitanje sa apsolutnim pomakom pretvara iz optimizacije u ugovor

Šta garantuje TPDFReadAtStream?

TPDFReadAtStream garantuje čitanje na apsolutnom pomaku koje ni od čega ne zavisi od logičkog kursora stream-a i ne menja ga. To je apstraktni naslednik TStream sa tačno jednom virtuelnom metodom, a oba izvora u biblioteci koji ne zavise od kursora izvedena su iz njega: TReadOnlyMappedFileStream za lokalne fajlove i TByteRangeStream za udaljene izvore koji se isporučuju po opsezima. Čitač isečaka objekta jednom proverava da li je njegov izvor TPDFReadAtStream, a ako nije, vraća se na stari redosled seek-pa-read, pa običan file stream ili memory stream nastavlja da radi bez promena

type
  // Read-only stream-ovi čija apsolutna čitanja izbegavaju deljeni Seek i Read
  TPDFReadAtStream = class(TStream)
  public
    function ReadAt(Offset: Int64; var Buffer;
      Count: LongInt): LongInt; virtual; abstract;
  end;

  // Prozorasti read-only pristup jednom lokalnom fajlu
  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;

Razlika je važnija nego što potpis sugeriše. ReadAt koristi prosleđeni pomak i ostavlja Position tačno tamo gde je bio, što omogućava ugnježdenim nivoima parsera da čitaju bez save-and-restore manevra oko svakog poziva. TReadOnlyMappedFileStream i dalje implementira Read, Seek i Size kao svaki drugi TStream, Seek ograničava logičku poziciju na opseg fajla, a Write uvek vraća 0 jer se izvor otvara samo za čitanje

Otvaranje PDF-a kroz mapirani prikaz u Delphi-ju

Dve eksplicitne ulazne tačke otvaraju mapirani izvor i nijedna ne menja ponašanje ulaznih tačaka koje već koristite. LoadFromMappedFile učitava i bira dokument; DAOpenMappedFile vraća Direct Access handle nad istim fajlom, što je režim koji želite kada spajate i razdvajate gigabajtne PDF-ove kroz Direct Access. LoadFromFile i DAOpenFile zadržavaju semantiku deljenja fajla, grešaka i kompatibilnosti, pa se ništa ne pomera pozivaocima koji se ne odluče za novu putanju. Obe mapirane ulazne tačke primaju traženi WindowSize u bajtovima i bitmasku Options, a za bilo koji argument prihvataju 0

var
  Pdf: TPDFlib;
  Payload: AnsiString;
  Info: WideString;
begin
  Pdf := TPDFlib.Create;
  try
    // WindowSize 0 bira podrazumevanih 64 MiB; mapiranje je obavezno ovde
    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žena ekstrakcija sada prolazi kroz mapirane prozore umesto seek-ovanja
    Payload := Pdf.GetEmbeddedFileContentToString(1);
    if Pdf.GetMappedFileInfo(Info) = 1 then
      Writeln(Info);
  finally
    Pdf.Free;
  end;
end;

Šta PDF_MAPPED_FILE_REQUIRE_MAPPING zaista nameće?

PDF_MAPPED_FILE_REQUIRE_MAPPING pretvara tihi fallback u trenutni, dijagnostikabilni neuspeh pri otvaranju. Kada je Options 0, obe ulazne tačke prihvataju read-only fallback preko file stream-a: ako platforma nema kod za mapiranje ili poziv mapiranja ne uspe, dokument se i dalje otvara i svako čitanje ide kroz običan file stream. Kada je zastavica postavljena, PDFlibPas prihvata ulaz samo ako je prvi prikaz uspostavljen i odbijanje prijavljuje kroz LastErrorCode 401, umesto da učita dokument koji se tiho ponaša potpuno kao stara putanja

Na Windows-u mapirani stream otvara drugu read-only ručku sa FILE_SHARE_READ, FILE_SHARE_WRITE i FILE_SHARE_DELETE, uz FILE_FLAG_RANDOM_ACCESS, pravi PAGE_READONLY mapiranje nad njom i mapira prvi prozor unutar konstruktora. Rano mapiranje je cela poenta: neuspeh zbog zahteva za mapiranjem pojavljuje se na LoadFromMappedFile, a ne pri prvom lenjom čitanju objekta usred renderovanja. Ipak, budite precizni gde garancija prestaje. Kod za mapiranje kompajlira se samo za Windows ciljeve, a fajl od nula bajtova uopšte ne pokušava mapiranje, pa je PDF_MAPPED_FILE_REQUIRE_MAPPING zahtev koji legitimno može da ne uspe, a ne prenosivo obećanje. Negativni WindowSize ili bilo koji bit u Options osim jedine dokumentovane vrednosti odbija se odmah sa istom greškom 401

Jedan prozor, ponovo mapiran na granularnost alokacije

Zadržava se samo jedan prikaz i upravo to održava upotrebu adresnog prostora nezavisnom od veličine fajla. WindowSize 0 bira 64 MiB; vrednost manja od sistemske granularnosti alokacije podiže se na nju; vrednost veća od 1 GiB ograničava se; a rezultat se zaokružuje naviše na ceo broj jedinica granularnosti, 65536 bajtova na Windows-u osim ako GetSystemInfo ne prijavi drugačiji dwAllocationGranularity. Kada čitanje padne izvan trenutnog prikaza, PDFlibPas ga odmapira, poravnava traženi pomak naniže na granicu granularnosti i tamo mapira novi prozor. Poslednji prozor ograničava se na fizičku veličinu fajla, pa prikaz nikada ne prelazi kraj fajla

Jedno čitanje može preći preko proizvoljnog broja prozora: petlja kopira koliko trenutni prikaz može da isporuči, ponovo mapira i nastavlja, a zahtev koji ode iza kraja vraća kraći broj bajtova umesto greške. Ono što PDFlibPas namerno ne radi jeste da vam preda pokazivač unutar prikaza, jer ga sledeće čitanje preko granice prozora poništava i nijedan pozivalac se razumno ne bi mogao od toga odbraniti. Mapirani bajtovi kopiraju se pravo u odredišne baferе kojima upravlja parser, čime se uklanjaju dodatni ulazni bafer za fajl i menjanje pozicije, ali biblioteka ne tvrdi da je konačno skladištenje parsera zero-copy. Prozorasto čitanje uklapa se i sa upisom, jer pomeranje bajtovnih referenci tokom brzog spajanja PDF-a izbacuje bajtove objekata dok ih mapirani izvor u istom trenutku dovodi. Kompromis oko veličine prozora je očigledan: manji prozor zauzima manje adresnog prostora i češće se ponovo mapira, što je obično pravi izbor unutar 32-bitnog procesa

Šta štiti zaključavanje i šta prijavljuje GetMappedFileInfo

Jedna kritična sekcija pokriva mapirani prikaz, kursor fallback fajla, logičku poziciju i statistiku, a razdvajanje dve metode čitanja direktno proizlazi iz toga. ReadAt uzima zaključavanje i poziva internog čitača bez zaključavanja; Read uzima isto zaključavanje, poziva isti interni čitač na trenutnoj logičkoj poziciji i zatim je pomera. Ponovna upotreba interne funkcije umesto javnog ReadAt sprečava rekurzivno zaključavanje, a držanje zaključavanja kroz celu petlju kopiranja održava ispravnost ponovnog mapiranja jednog prozora pri konkurentnim pozivima. Jedan detalj Free Pascal-a vredi znati pre porta: FPC Windows jedinica definiše sopstveni zapis pod imenom TCriticalSection, pa polje i njegova konstrukcija moraju biti napisani kao SyncObjs.TCriticalSection. Delphi bez problema kompajlira nekvalifikovani oblik; FPC ga razrešava kao zapis bez Create, Enter ili 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 je false kad god je aktivan prenosivi fallback preko file stream-a i jedino je polje koje dokazuje da mapiranje nikada nije uspostavljeno
  • windowSize je efektivni poravnati prozor, a ne vrednost koju ste tražili, dok je mappedBytes manji od njega u završnom prozoru
  • mappedOffset je početak zadržanog prikaza poravnat na alokaciju ili -1 kada nijedan prikaz trenutno nije aktivan
  • readCalls broji uspešne zahteve za čitanje unutar opsega, bytesRead broji bajtove kopirane pozivaocima, a remapCount uključuje početni prikaz

Ciljane regresije pokrivaju apsolutna čitanja preko granice prozora, očuvanje logičkog kursora, kratka čitanja na kraju, nevažeće pomake, odbijene upise, ponovno mapiranje između razdvojenih prozora, odloženu ekstrakciju nekompresovanog priloga od 220 KB i nevažeću statistiku nakon DACloseFile; Win32 i Win64 headless suite-ovi svaki su otkrili 1467 testova i svi su prošli bez preskočenih, neuspešnih, neobrađenih ili procurelih rezultata. Ako u Delphi-ju ili C++Builder-u radite sa gigabajtnim PDF-ovima, a profiler stalno pokazuje na čitanja fajla umesto na parsiranje, mapirane ulazne tačke vrede jednog popodneva merenja, a GetMappedFileInfo će vam reći da li ste zaista dobili mapiranje. Puna API referenca i probna izgradnja nalaze se na stranici PDFlibPas Delphi PDF biblioteke