Tekninen artikkeli

Muistikuvaan mapatun PDF:n liukuikkuna Delphissä

PDFlibPas voi avata paikallisen PDF:n rajattuna read-only-muistikuvaan mapattuna näkymänä: LoadFromMappedFile ja DAOpenMappedFile pitävät tiedostosta täsmälleen yhden liukuvan ikkunan, mapittavat sen tarvittaessa uudelleen ja tarjoavat jokaisen objektiviipaleen absoluuttiseen offsetiin perustuvilla luvuilla. Delphi PDF -kirjasto ei koskaan pidä koko lähdettä muistissa, joten osoiteavaruuden käyttö pysyy tasaisena tiedoston kasvaessa. Rakenne on tehty yhtä työkuormaa varten: gigatavujen PDF:t, joiden jäsentäminen on ladattu loppuun mutta jotka käyvät yhä levyllä objekti objektilta ja streamin palanen palaselta

Miksi hajautetut luvut ovat kalliita vielä PDF:n lataamisen jälkeen?

PDF:n lataaminen ei tarkoita, että sen lukeminen olisi valmis, ja monen gigatavun tiedostossa juuri tämä kuilu kuluttaa ajan. Cross-reference-taulukko tai cross-reference-stream (ISO 32000-1 §7.5.4 ja §7.5.8) kirjaa vain kunkin epäsuoran objektin aloituskohdan. Tavuaineisto saadaan myöhemmin, kun sivu renderöidään, fonttiohjelma dekoodataan tai upotettu tiedostostream (ISO 32000-1 §7.11.4) puretaan. Kahden gigatavun arkisto, jossa on kymmeniä tuhansia objekteja, muuttuu kymmeniksi tuhansiksi pieniksi järjestämättömiksi luvuiksi, eikä yksikään niistä ole tiedossa lataushetkellä

Nämä luvut tehtiin aiemmin jaetulla Seek-kutsulla, jota seurasi Read yhdessä positionaalisessa streamissa, ja ratkaisu pettää kahdella tavalla yhtä aikaa. Jokainen palanen maksaa tiedostonluvun hinnan, vaikka sivu olisi jo käyttöjärjestelmän cachessa, ja kursori on jaettua muuntuvaa tilaa, joten paikallinen tiedosto ja ennakoivalla haulla toteutetun progressiivisen PDF range -latauksen takana oleva byte-range-lähde eivät voineet ajaa samaa jäsentäjäkoodia taistelematta positiosta. PDFlibPas korjaa molemmat nostamalla absoluuttiseen offsetiin perustuvan lukemisen optimoinnista sopimukseksi

Mitä TPDFReadAtStream takaa?

TPDFReadAtStream takaa absoluuttisesta offsetista tehtävän luvun, joka ei riipu loogisesta stream-kursorista eikä häiritse sitä. Se on abstrakti TStream-johdannainen, jossa on täsmälleen yksi virtuaalinen metodi, ja kirjaston molemmat kursorista riippumattomat lähteet periytyvät siitä: paikallisille tiedostoille tarkoitettu TReadOnlyMappedFileStream ja range-palveluista luettava TByteRangeStream. Objektiviipaleiden lukija tarkistaa kerran, onko lähde TPDFReadAtStream, ja palaa vanhaan seek-then-read-järjestykseen, jos ei ole, joten tavallinen filestream tai memorystream toimii edelleen muuttamatta mitään

type
  // Read-only-streamit, joiden absoluuttiset luvut välttävät jaetun Seek- ja Read-parin
  TPDFReadAtStream = class(TStream)
  public
    function ReadAt(Offset: Int64; var Buffer;
      Count: LongInt): LongInt; virtual; abstract;
  end;

  // Ikkunoitu read-only-pääsy yhteen paikalliseen tiedostoon
  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;

Ero on merkittävämpi kuin allekirjoitus antaa ymmärtää. ReadAt käyttää sille annettua offsetia ja jättää Position-arvon täsmälleen ennalleen, mikä antaa sisäkkäisille jäsentäjätasoille mahdollisuuden lukea ilman jokaisen kutsun ympärille rakennettavaa tallenna-ja-palauta-tanssia. TReadOnlyMappedFileStream toteuttaa silti Read-, Seek- ja Size-jäsenet kuten mikä tahansa muu TStream, Seek rajoittaa loogisen position tiedoston sisälle ja Write palauttaa aina 0, koska lähde avataan read-only-tilassa

PDF:n avaaminen mapatun näkymän kautta Delphissä

Kaksi eksplisiittistä entry pointia avaa mapatun lähteen, eikä kumpikaan muuta jo käyttämiesi entry pointien toimintaa. LoadFromMappedFile lataa ja valitsee dokumentin; DAOpenMappedFile palauttaa Direct Access -kahvan saman tiedoston päälle, mikä on haluamasi tila, kun yhdistät ja jaat gigatavujen PDF:iä Direct Accessin kautta. LoadFromFile ja DAOpenFile säilyttävät tiedoston jakamisen, virheiden ja yhteensopivuuden semantiikan muuttumattomina, joten opt-inistä kieltäytyvät kutsujat eivät huomaa mitään. Molemmat mapatut entry pointit saavat pyydetyn WindowSize-koon tavuina ja Options-bittimaskin, ja molemmat hyväksyvät kummallekin arvon 0

var
  Pdf: TPDFlib;
  Payload: AnsiString;
  Info: WideString;
begin
  Pdf := TPDFlib.Create;
  try
    // WindowSize 0 valitsee 64 MiB:n oletuksen; mapitus on tässä pakollinen
    if Pdf.LoadFromMappedFile('archive-2026.pdf', '', 0,
      PDF_MAPPED_FILE_REQUIRE_MAPPING) <> 1 then
      raise Exception.CreateFmt('mapped open refused, LastErrorCode=%d',
        [Pdf.LastErrorCode]);

    // Viivästetty purku kulkee nyt mapattujen ikkunoiden kautta Seekin sijaan
    Payload := Pdf.GetEmbeddedFileContentToString(1);
    if Pdf.GetMappedFileInfo(Info) = 1 then
      Writeln(Info);
  finally
    Pdf.Free;
  end;
end;

Mitä PDF_MAPPED_FILE_REQUIRE_MAPPING oikeastaan pakottaa?

PDF_MAPPED_FILE_REQUIRE_MAPPING muuttaa hiljaisen vararatkaisun välittömäksi ja diagnosoitavaksi avausvirheeksi. Kun Options on 0, molemmat entry pointit hyväksyvät read-only-filestream-vararatkaisun: jos alustalla ei ole mapituskoodia tai mapituskutsu epäonnistuu, dokumentti avautuu silti ja jokainen luku kulkee tavallisen filestreamin kautta. Kun lippu on asetettu, PDFlibPas hyväksyy syötteen vain, jos ensimmäinen näkymä on saatu muodostettua, ja ilmoittaa hylkäyksestä LastErrorCode-arvolla 401 sen sijaan, että lataisi dokumentin joka toimii hiljaisesti aivan kuten vanha polku

Windowsissa mapattu stream avaa toisen read-only-kahvan lipuilla FILE_SHARE_READ, FILE_SHARE_WRITE ja FILE_SHARE_DELETE sekä lipulla FILE_FLAG_RANDOM_ACCESS, luo sen päälle PAGE_READONLY-mapituksen ja mapittaa ensimmäisen ikkunan konstruktorissa. Eager-mapitus on koko ominaisuuden ydin: "mapping required" -virhe tulee näkyviin kohdassa LoadFromMappedFile, ei ensimmäisessä laiskassa objektiluvussa kesken renderöintityön. On silti syytä tietää, mihin takuu päättyy. Mapituskoodi käännetään vain Windows-kohteille, eikä nollatavuinen tiedosto yritä mapitusta lainkaan, joten PDF_MAPPED_FILE_REQUIRE_MAPPING on pyyntö joka voi oikeutetusti epäonnistua eikä siirrettävä lupaus. Negatiivinen WindowSize tai jokainen muu Options-bitti kuin dokumentoitu arvo hylätään suoraan samalla virheellä 401

Yksi ikkuna, joka mapitetaan uudelleen allokointigranulariteettiin

Vain yksi näkymä säilytetään kerrallaan, ja juuri tämä pitää osoiteavaruuden käytön riippumattomana tiedoston koosta. WindowSize 0 valitsee 64 MiB:n koon; järjestelmän allokointigranulariteettia pienempi arvo nostetaan siihen; yli 1 GiB:n arvo rajataan; ja tulos pyöristetään ylöspäin kokonaisiksi granulariteettiyksiköiksi, jotka ovat Windowsissa 65536 tavua ellei GetSystemInfo ilmoita muuta dwAllocationGranularity-arvoa. Kun luku osuu nykyisen näkymän ulkopuolelle, PDFlibPas poistaa mapituksen, kohdistaa pyydetyn offsetin alaspäin granulariteettirajalle ja mapittaa siihen uuden ikkunan. Viimeinen ikkuna rajoitetaan fyysisen tiedoston kokoon, joten näkymä ei koskaan ulotu tiedoston lopun yli

Yksi luku voi ylittää kuinka monta ikkunaa tahansa: silmukka kopioi sen minkä nykyinen näkymä pystyy tarjoamaan, mapittaa uudelleen ja jatkaa, ja tiedoston lopun yli ulottuva pyyntö palauttaa lyhyen lukumäärän epäonnistumisen sijaan. PDFlibPas ei tarkoituksella anna osoitinta näkymän sisälle, koska seuraava ikkunan ylittävä luku mitätöi sen eikä kutsuja voisi järkevästi suojautua tältä. Mapatut tavut kopioidaan suoraan jäsentäjän omistamiin kohdepuskureihin, mikä poistaa ylimääräisen tiedostosyötepuskuriin ja position vaihtamiseen liittyvän työn, mutta kirjasto ei väitä lopullista jäsentäjätallennusta zero-copyksi. Lukupuolen ikkunointi sopii myös kirjoituspuoleen, sillä tavutasoinen viiteoffsetien siirto nopean PDF-yhdistämisen aikana virtauttaa objektit ulos samalla kun mapattu lähde virtauttaa ne sisään. Ikkunan koon vaihtokauppa on ilmeinen: pienempi ikkuna käyttää vähemmän osoiteavaruutta ja mapitetaan useammin, mikä on yleensä oikea valinta 32-bittisessä prosessissa

Mitä lukko suojaa ja mitä GetMappedFileInfo ilmoittaa

Yksi kriittinen osio suojaa mapattua näkymää, varalla käytettävää tiedostokursoria, loogista positiota ja tilastoja, ja jako kahden lukumetodin välillä seuraa suoraan tästä. ReadAt ottaa lukon ja kutsuu lukotonta sisäistä lukijaa; Read ottaa saman lukon, kutsuu samaa sisäistä lukijaa nykyisestä loogisesta positiosta ja siirtää sitten sitä eteenpäin. Sisäisen funktion käyttäminen julkisen ReadAt-metodin sijaan estää rekursiivisen lukituksen, ja lukon pitäminen koko kopiointisilmukan ajan pitää yhden ikkunan uudelleenmapituksen oikeana rinnakkaisissa kutsuissa. Yksi Free Pascal -yksityiskohta kannattaa tietää ennen porttausta: FPC:n Windows-unit määrittelee oman TCriticalSection-nimisen recordin, joten kenttä ja sen konstruktio täytyy kirjoittaa muodossa SyncObjs.TCriticalSection. Delphi kääntää kvalifioimattoman muodon mukisematta, mutta FPC ratkaisee sen recordiksi, jossa ei ole Create-, Enter- eikä Leave-metodia

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 on false aina, kun käytössä on siirrettävä filestream-vararatkaisu, ja se on ainoa kenttä joka osoittaa ettei mapitusta koskaan muodostettu
  • windowSize on efektiivinen kohdistettu ikkuna eikä pyytämäsi arvo, ja mappedBytes on loppuikkunassa sitä pienempi
  • mappedOffset on säilytetyn näkymän allokointiin kohdistettu alku tai -1, kun aktiivista näkymää ei ole
  • readCalls laskee onnistuneet tiedoston sisällä pysyvät lukupyynnöt, bytesRead kutsujille kopioidut tavut ja remapCount sisältää alkuperäisen näkymän

Kohdennetut regressiot kattavat ikkunoiden ylittävät absoluuttiset luvut, loogisen kursorin säilymisen, lyhyet luvut lopussa, virheelliset offsetit, hylätyt kirjoitukset, erillisten ikkunoiden välisen uudelleenmapituksen, 220 kilotavun pakkaamattoman liitteen viivästetyn purun sekä tilastojen muuttumisen virheellisiksi DACloseFile-kutsun jälkeen; Win32- ja Win64-headless-testit löysivät kumpikin 1467 testiä ja läpäisivät ne kaikki ilman ohitettuja, epäonnistuneita, virheellisiä tai vuotaneita tuloksia. Jos käsittelet gigatavujen PDF:iä Delphillä tai C++Builderilla ja profilerisi osoittaa yhä tiedostonlukuihin jäsentämisen sijaan, mapattujen tiedostojen entry pointit kannattaa mitata yhden iltapäivän ajan, ja GetMappedFileInfo kertoo saitko todella mapituksen. Täydellinen API-viite ja kokeiluversio löytyvät PDFlibPas Delphi PDF -kirjaston sivulta