Tehnični članak

Pomnilniško preslikano branje PDF-ja v Delphiju: drsno okno

PDFlibPas lahko lokalni PDF odpre prek omejenega pomnilniško preslikanega pogleda samo za branje: LoadFromMappedFile in DAOpenMappedFile nad datoteko ohranjata natanko eno drsno okno, ga po potrebi znova preslikata in vsak izsek objekta strežeta z branjem na absolutnem odmiku. Knjižnica PDF za Delphi nikoli ne zadrži celotnega vira v pomnilniku, zato uporaba naslovnega prostora ostane nespremenjena, ko datoteka raste. Zasnova je namenjena eni delovni obremenitvi: gigabajtnim PDF-jem, pri katerih je razčlenjevalnik končal začetno nalaganje, vendar se še vedno vrača na disk po objektih in delcih tokov

Zakaj so redka branja draga tudi po nalaganju PDF-ja?

Nalaganje PDF-ja še ne pomeni, da je branje končano, pri večgigabajtni datoteki pa se čas porablja prav v tej razliki. Navzkrižna tabela ali navzkrižni tok (ISO 32000-1 §7.5.4 in §7.5.8) zabeleži samo, kje se začne vsak posredni objekt. Bajti prispejo pozneje, ko se izriše stran, dekodira program pisave ali izloči vdelani datotečni tok (ISO 32000-1 §7.11.4). Arhiv velikosti 2 GB z več deset tisoč objekti postane več deset tisoč majhnih neurejenih branj, pri nalaganju pa nobeno od njih še ni znano

Ta branja so prej potekala z deljenim Seek, ki mu je sledil Read nad enim pozicijskim tokom, in ta pot odpove v obeh smereh hkrati. Vsak izsek plača ceno branja datoteke, tudi ko je stran že v predpomnilniku operacijskega sistema, kazalec pa je deljeno spremenljivo stanje, zato lokalna datoteka in vir bajtnih razponov za progresivno nalaganje PDF-rangov s prednalaganjem nista mogla izvajati iste kode razčlenjevalnika brez boja za položaj. PDFlibPas oboje popravi tako, da branje na absolutnem odmiku iz optimizacije spremeni v pogodbo

Kaj zagotavlja TPDFReadAtStream?

TPDFReadAtStream zagotavlja branje na absolutnem odmiku, ki ni odvisno od logičnega kazalca toka in ga tudi ne spremeni. Gre za abstraktnega potomca TStream z natanko eno virtualno metodo, iz njega pa izhajata oba vira v knjižnici, ki nista odvisna od kazalca: TReadOnlyMappedFileStream za lokalne datoteke in TByteRangeStream za oddaljene vire, strežene po razponih. Bralnik izsekov objektov enkrat preveri, ali je njegov vir TPDFReadAtStream, in se vrne na staro zaporedje seek-then-read, kadar ni, zato običajen datotečni ali pomnilniški tok še naprej deluje nespremenjeno

type
  // Tokovi samo za branje, katerih absolutna branja se izognejo deljenemu Seek in Read
  TPDFReadAtStream = class(TStream)
  public
    function ReadAt(Offset: Int64; var Buffer;
      Count: LongInt): LongInt; virtual; abstract;
  end;

  // Dostop samo za branje do ene lokalne datoteke skozi okno
  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 pomembnejša, kot nakazuje podpis. ReadAt uporabi podani odmik in pusti Position natančno tam, kjer je bil, zato lahko ugnezdene ravni razčlenjevalnika izvajajo branja brez shranjevanja in obnavljanja okoli vsakega klica. TReadOnlyMappedFileStream še vedno implementira Read, Seek in Size kot vsak drug TStream, Seek omeji logični položaj na datoteko, Write pa vedno vrne 0, ker je vir odprt samo za branje

Odpiranje PDF-ja skozi preslikan pogled v Delphiju

Preslikani vir odpreta dve izrecni vstopni točki in nobena ne spremeni vedenja vstopnih točk, ki jih že uporabljate. LoadFromMappedFile naloži in izbere dokument, DAOpenMappedFile pa vrne ročaj Direct Access nad isto datoteko, kar je način, ki ga želite pri združevanju in razdeljevanju gigabajtnih PDF-jev prek Direct Access. LoadFromFile in DAOpenFile ohranita semantiko deljenja datotek, napak in združljivosti, zato se za klicatelje, ki se ne odločijo za novo možnost, nič ne spremeni. Obe preslikani vstopni točki sprejmeta zahtevani WindowSize v bajtih in bitno masko Options, pri obeh pa je mogoče za katero koli vrednost podati 0

var
  Pdf: TPDFlib;
  Payload: AnsiString;
  Info: WideString;
begin
  Pdf := TPDFlib.Create;
  try
    // WindowSize 0 izbere privzeto vrednost 64 MiB; preslikava je tukaj obvezna
    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ženo izločanje zdaj hodi po preslikanih oknih namesto po seekih
    Payload := Pdf.GetEmbeddedFileContentToString(1);
    if Pdf.GetMappedFileInfo(Info) = 1 then
      Writeln(Info);
  finally
    Pdf.Free;
  end;
end;

Kaj dejansko uveljavlja PDF_MAPPED_FILE_REQUIRE_MAPPING?

PDF_MAPPED_FILE_REQUIRE_MAPPING tiho nadomestno pot spremeni v takojšnjo in diagnosticirano napako ob odpiranju. Če je Options enak 0, obe vstopni točki sprejmeta nadomestni datotečni tok samo za branje: če platforma nima kode za preslikavo ali če klic preslikave odpove, se dokument vseeno odpre in vsako branje poteka skozi običajen datotečni tok. Ko je zastavica nastavljena, PDFlibPas vhod sprejme samo, če je bil vzpostavljen prvi pogled, zavrnitev pa sporoči prek LastErrorCode 401, namesto da bi naložil dokument, ki se tiho vede natanko kot stara pot

V sistemu Windows preslikani tok odpre drugi ročaj samo za branje z FILE_SHARE_READ, FILE_SHARE_WRITE in FILE_SHARE_DELETE ter FILE_FLAG_RANDOM_ACCESS, nad njim ustvari preslikavo PAGE_READONLY in prvi odsek preslika že v konstruktorju. Takojšnja preslikava je bistvo: napaka »preslikava je obvezna« se pokaže pri LoadFromMappedFile, ne šele pri prvem lenem branju objekta sredi opravila izrisovanja. Vendar je treba jasno povedati, kje se zagotovilo konča. Koda za preslikavo je prevedena samo za cilje Windows, datoteka z nič bajti pa preslikave sploh ne poskusi, zato je PDF_MAPPED_FILE_REQUIRE_MAPPING zahteva, ki lahko upravičeno odpove, ne pa prenosljiva obljuba. Negativen WindowSize ali katerikoli bit v Options, ki ni dokumentirana vrednost, je neposredno zavrnjen z isto napako 401

Eno okno, znova preslikano na mejo dodeljevanja

Vedno se ohrani samo en pogled, zato uporaba naslovnega prostora ni odvisna od velikosti datoteke. WindowSize 0 izbere 64 MiB, vrednost pod sistemsko granularnostjo dodeljevanja se dvigne do nje, vrednost nad 1 GiB se omeji, rezultat pa se zaokroži navzgor na celo število enot granularnosti, kar v sistemu Windows pomeni 65536 bajtov, razen če GetSystemInfo sporoči drugačen dwAllocationGranularity. Ko branje pade zunaj trenutnega pogleda, ga PDFlibPas odpreslika, zahtevani odmik poravna navzdol na mejo granularnosti in tam preslika novo okno. Končno okno je omejeno na fizično velikost datoteke, zato pogled nikoli ne sega čez njen konec

Eno branje lahko prečka poljubno število oken: zanka kopira toliko, kolikor lahko zagotovi trenutni pogled, nato znova preslika in nadaljuje, zahteva, ki sega čez konec, pa vrne krajše število namesto napake. PDFlibPas namerno ne naredi tega, da bi vam izročil kazalec v pogled, saj ga naslednje branje čez mejo okna razveljavi in se pred tem noben klicatelj ne bi mogel razumno zavarovati. Preslikani bajti se kopirajo naravnost v ciljne medpomnilnike, ki jih ima v lasti razčlenjevalnik, s čimer se odstranita dodatni vhodni medpomnilnik datoteke in preklapljanje položaja, vendar knjižnica ne trdi, da je končna shramba razčlenjevalnika brez kopiranja. Okensko branje se združi tudi s stranjo za pisanje, saj premikanje bajtnih referenc pri hitrem združevanju PDF-jev pretaka bajte objektov iz preslikanega vira. Kompromis glede velikosti okna je očiten: manjše okno zadrži manj naslovnega prostora in se pogosteje znova preslika, kar je znotraj 32-bitnega procesa običajno prava izbira

Kaj ščiti ključavnica in kaj sporoča GetMappedFileInfo

En kritični odsek pokriva preslikani pogled, kazalec nadomestnega datotečnega toka, logični položaj in statistiko, iz tega pa neposredno izhaja ločitev med metodama branja. ReadAt prevzame ključavnico in pokliče notranji bralnik brez ključavnice, Read prevzame isto ključavnico, pokliče isti notranji bralnik na trenutnem logičnem položaju in ga nato pomakne naprej. Ponovna uporaba notranje funkcije namesto javnega ReadAt prepreči rekurzivno zaklepanje, zadržanje ključavnice čez celotno zanko kopiranja pa zagotovi pravilnost ponovne preslikave enega okna pri sočasnih klicih. Pred prenosom je vredno poznati eno podrobnost Free Pascala: enota FPC Windows deklarira lasten zapis z imenom TCriticalSection, zato je treba polje in njegovo konstrukcijo zapisati kot SyncObjs.TCriticalSection. Delphi nepopolno obliko brez kvalifikatorja prevede brez težav, FPC pa jo razreši v zapis brez Create, Enter ali 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, kadar je aktivna prenosljiva nadomestna pot z datotečnim tokom, in je edino polje, ki dokazuje, da preslikava ni bila nikoli vzpostavljena
  • windowSize je učinkovita poravnana velikost okna, ne vrednost, ki ste jo zahtevali, mappedBytes pa je v končnem oknu manjši od nje
  • mappedOffset je na dodeljevanje poravnan začetek zadržanega pogleda ali -1, ko trenutno ni aktivnega pogleda
  • readCalls šteje uspešne zahteve za branje znotraj razpona, bytesRead šteje bajte, kopirane klicateljem, remapCount pa vključuje začetni pogled

Ciljne regresije pokrivajo absolutna branja čez mejo okna, ohranjanje logičnega kazalca, kratka branja na koncu, neveljavne odmike, zavrnjena pisanja, ponovno preslikovanje med ločenimi okni, odloženo izločanje 220-KB nestisljive priloge in neveljavno statistiko po DACloseFile; brezkrajne zbirke za Win32 in Win64 so vsaka odkrile 1467 testov in vse opravile brez prezrtih, neuspešnih, napačnih ali puščajočih rezultatov. Če delate z gigabajtnimi PDF-ji v Delphiju ali C++Builderju in profilirnik še naprej kaže na branja datotek namesto na razčlenjevanje, si vstopne točke za preslikane datoteke zaslužijo popoldne meritev, GetMappedFileInfo pa vam bo povedal, ali ste preslikavo res dobili. Celotna referenca API-ja in poskusna gradnja sta na strani PDFlibPas Delphi PDF library