Haluat yhden sanakirjan 2 GB:n PDF:stä, ja työkalu laajentaa ensin koko ristiviittaustaulukon taulukoksi, jonka koko määräytyy trailerin /Size-arvon mukaan. PDFiumPas korvaa kyseisen vaiheen harvalla laiskalla objekti-indeksillä: se säilyttää vain xref-osioiden deskriptorit, selvittää yhden objektinumeron tarvittaessa rajattujen ikkunoiden kautta ja välimuistittaa vain ne merkinnät, joita oikeasti kosketit
Tämän koodin vanha muoto moduulissa FPdfCompress oli rehellinen mutta kallis. ApplyDefaultOpenAction luki koko tiedoston yhteen TBytes-muuttujaan ja varasi sitten tiheän TPdfActiveXrefEntries-taulukon, jossa oli yksi paikka jokaista objektinumeroa kohden arvoon /Size asti. Kaksi asiaa meni pieleen suuressa mittakaavassa. Lukukustannus kasvoi lineaarisesti asiakirjan koon mukaan, vaikka kutsuja halusi vain neljä sanakirjaa, ja tiheä taulukko törmäsi jäsennysbudjettiin: TPdfParserResourceBudget.Default asettaa MaxObjects-arvoksi 4 000 000, joten täysin kelvollinen tiedosto, jonka korkein objektinumero sijaitsi kyseisen katton yläpuolella, hylättiin muistoperusteella eikä oikeellisuusperusteella
Miksi PDFiumin julkinen API ei vastaa tähän kysymykseen?
Koska tieto on olemassa PDFiumin sisällä, mutta se ei koskaan ylitä C-rajapintaa. CPDF_Parser ylläpitää sisäisesti ristiviittaustaulukkoa, objektivirtajäsenyyttä ja revisioiden etusijajärjestystä, mutta julkaistut otsikkotiedostot eivät näytä yhtään sisääntulopistettä, joka ottaisi objektinumeron ja palauttaisi sen raakasiirtymän, sen sukupolven, sen, mikä revisio voitti, tai sen, missä objektivirrassa (ObjStm) se asuu. Tallennuspuoli on yhtä suljettu: FPDF_SaveAsCopy ja FPDF_SaveWithVersion antavat sinulle vain peräkkäisen kirjoitustakaisinkutsun. Mikä tahansa tavutason paikkaus katalogiin natiivin tallennuksen jälkeen on siksi rakennettava Pascal-kerrokseen, minkä vuoksi PDFiumPas jäsentää nämä rakenteet itse sen sijaan että käyttäisi DLL:ää uudelleen
Mitä harva indeksi oikeasti pitää muistissa?
Deskriptorit, eivät merkintöjä. Klassiselle taulukolle (ISO 32000-1 §7.5.4) TPdfSparseXrefSubsection tallentaa ensimmäisen objektinumeron, objektilukumäärän, tavusiirtymän, josta merkintärivit alkavat, ja mitatun merkintäleveyden. Merkinnät itse pysyvät tiedostossa. Leveys mitataan ensimmäisestä rivistä sen sijaan että oletettaisiin 20 tavuksi, koska tuottajat ovat eri mieltä rivinvaihdoista; PDFiumPas hyväksyy arvot 18–64 ja hylkää kaiken kyseisen välin ulkopuolelta sekä jokaisen alaosion, jonka ilmoitettu lukumäärä kuluisi virran lopun ohi. Ristiviirtavirralle (§7.5.8) osio pitää sisällään kolme /W-kenttäleveyttä, kukin rajoitettuna välille 0–8, litistetyt /Index-parit ja puretut merkintätavut, joiden odotettu pituus lasketaan arvoista /W ja /Index ennen kuin yhtään tavua puretaan
Koko indeksi rakennetaan funktiolla Initialize enintään 1 MiB:n häntäikkunasta, josta startxref löytyy, ja jokainen sitä seuraava objektiluku käyttää 1 MiB:n objektiikkunaa. Raakavirran katto on 64 MiB, ja yksittäinen xref-rivi saa olla enintään 1024 tavua. Jos olet lukenut muistiinpanomme objekti- ja ristiviirtavirtojen vahvistamisesta PDFiumPasilla, sama kenttäleveyskurin pätee täällä, vain sitä käytetään nyt yhden merkinnän osoittamiseen kokonaisen taulukon auditoinnin sijaan
uses
FPdfCompress;
var
Source: TFileStream;
Revision: TPdfSparseRevisionInfo;
begin
Source := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
{ kulkee vain startxrefin, /Prev-ketjun ja katalogin läpi }
if ReadPdfSparseRevisionInfo(Source, Revision) then
begin
Writeln('root ', Revision.RootObjectNumber, ' ',
Revision.RootGeneration);
Writeln('max obj ', Revision.MaximumObjectNumber);
Writeln('xref str ', Revision.UsesXrefStream);
Writeln('encrypted ', Revision.HasEncrypt);
Writeln(string(Revision.CatalogDictionary));
end;
finally
Source.Free;
end;
end;
Miten yksi haku yltää yhteen objektiin?
Aritmetiikalla, kummassakin asettelussa. Klassisen alaosion rivit ovat kiinteäleveitä, joten merkinnän osoite on alaosion alku plus objektin siirtymä kertaa mitattu leveys; PDFiumPas lukee sitten sen yhden rivin, jäsentää kymmennumeroisen siirtymän ja viisinumeroisen sukupolven, tarkistaa sukupolven standardin §7.5.4:n 65535-kattoa vasten ja luokittelee lopussa olevan avainsanan arvoksi axkDirect tai axkFree. Ristiviirtavirta tarvitsee yhden vaiheen lisää, koska /Index-alaosiot on liitetty yhteen puretussa tavujonossa, joten indeksi kerryttää edellisten alaosioden lukumäärät ennen kertolaskua summalla /W-leveydellä. Tyyppi 1 tuottaa siirtymän, tyyppi 2 tuottaa objektivirran numeron ja jäsenindeksin, ja mikä tahansa muu muuttuu arvoksi axkUnknown arvauksen sijaan
{ klassinen taulukko, ISO 32000-1 osio 7.5.4 }
EntryOffset := Subsection.EntryOffset +
Int64(ObjectNumber - Subsection.FirstObject) * Subsection.EntryWidth;
{ ristiviirtavirta, ISO 32000-1 osio 7.5.8 }
EntryWidth := Section.Widths[0] + Section.Widths[1] + Section.Widths[2];
EntryPosition := Integer((PriorCount + ObjectNumber -
Section.IndexValues[I]) * EntryWidth);
Mikään kummassakaan polussa ei ole verrannollinen arvoon /Size. Siinä on uudelleenkirjoituksen koko pointti: trailerin kokuarvo kuljetetaan eteenpäin metatietona ja sitä käytetään kirjoitettaessa inkrementaalinen revisio, mutta se ei koskaan ohjaa allokointia. Regressiotestisarja lukitsee tämän testifixturella, jonka sivupuu sijaitsee objekteissa 1 000 000 000 ja 1 000 000 001 trailerin alla, joka ilmoittaa arvon /Size 1000000002. Vanha tiheä toteutus hylkäsi kyseisen tiedoston; harva indeksi selvittää molemmat viittaukset ja säilyttää ilmoitetun koon tulostetrailerissa
Hybridirevisiot, /Prev-ketjut ja niiden ympärillä olevat vartijat
Revisioiden etusijajärjestys on kohta, jossa naiivi laiska indeksi menee pieleen. PDFiumPas kulkee ketjun läpi startxrefistä uusin ensin -järjestyksessä ja pysäyttää haun ensimmäiseen osioon, joka vastaa, mikä toistaa etusijasäännön ilman yhdistetyn taulukon materialisointia. Hybridiviittaukselliset tiedostot (§7.5.8.4) käsitellään klassisen haaran sisällä: kun traileri kantaa merkintää /XRefStm, täydentävä virtaosio rekisteröidään ennen klassista osiota, joka siihen viittasi, joten tavallista taulukkoa näkymättömät pakatut objektit löytyvät yhä, kun taas klassiset merkinnät säilyttävät asemansa. Vanhempia revisioita seurataan sitten merkinnän /Prev kautta
Kaksi vartijaa rajaa kyseisen kulun, ja molemmat merkitsevät vaurioituneissa tiedostoissa. Jokainen vierailtu siirtymä kirjataan muistiin, joten ketjuun takaisin osoittava /Prev päättyy pyörimisen sijaan, ja läpikäynnin syvyys rajataan arvolla MaxRecursionDepth, joka on oletuksena 1024. Salauslippu kumuloidaan koko ketjun yli sen sijaan että luettaisiin pelkästään uusimmasta trailerista, koska asiakirja, jonka uusin traileri jättää merkinnän /Encrypt pois, voi silti olla salattu ketjun aiemmassa kohdassa; revisioita liittävät kutsujat nojaavat kyseiseen lippuun kieltäytyäkseen kirjoittamasta selkotekstiobjekteja salattuun tiedostoon
Tyypin 2 merkinnät: miksi objektivirta odottaa
Tyypin 2 merkintä nimeää objektivirran, eikä PDFiumPas koske kyseiseen virtaan ennen kuin kutsuja pyytää jotain sen jäsentä. Kun se lopulta tekee niin, /Type /ObjStm vahvistetaan, /N tarkistetaan objektibudjettia ja /First purettujen tavujen kattoa vasten, ja suhde /N ja /First välillä käydään läpi terveen järjen tarkistuksessa, koska jokainen otsikkopari tarvitsee vähintään neljä tavua. Vasta sen jälkeen virta puretaan, ja otsikkoskannaus pysähtyy pyydettyyn jäseneen ja sen seuraajaan kokonaisen jäsentaulukon rakentamisen sijaan. Yksi purettu objektivirta säilytetään kerrallaan, mikä on oikea kompromissi, kun sivupuun haara klusteroituu yhdeksi ObjStm-virraksi; kirjoituksamme objektivirran ja prediktorin purku Delphissä kattaa, mitä kyseisen purkuvaiheen sisällä tapahtuu (§7.5.7)
var
Reader: TPdfSparseDictionaryReader;
Generation: Integer;
Dict: AnsiString;
begin
{ yksi säilytetty indeksi, monta sukupolvitietoista lukua }
Reader := TPdfSparseDictionaryReader.Create(Source);
try
if Reader.Valid and
Reader.ReadLatestDictionary(PageObjectNumber, Generation, Dict) then
HandlePage(PageObjectNumber, Generation, Dict);
finally
Reader.Free; { Source jää sinulle }
end;
end;
Missä välimuistin lupaukset loppuvat
Indeksi on tilannekuva, ja sen sanominen suoraan kannattaa. Osiot jäsentetään kerran funktiossa Initialize; jos taustalla olevaa virtaa muokataan sen jälkeen, jokainen välimuistissa oleva merkintä on vanhentunut, eikä luokka huomaa sitä. TPdfSparseDictionaryReader pitää indeksin kutsujan omistaman lähteen elinkaaren ajan, mikä on juuri se, mitä rekursiivinen sivupuun läpikäynti haluaa, ja juuri se, mitä ei saa tehdä uudelleenkirjoituksen yli. Merkintävälimuisti on tasaista taulukkoa, jota haetaan lineaarisesti, ja se tallentaa myös negatiiviset tulokset, joten muutama sata hakua on halpoja ja muutama sata tuhatta ei ole. ReadDictionary vaatii tarkan sukupolviotsakkeen, kun taas ReadLatestDictionary selvittää aktiivisen, ja ero on harkittu: viittausten selvittäminen tarvitsee edellistä, katalogin tarkastus jälkimmäistä. Kun näitä rajoja ei voida noudattaa, ympäröivät moduulit palaavat vanhaan kokonaisen tiedoston jäsentäjään sen sijaan että kaventaisivat vielä toimivien tiedostojen joukkoa, malli, jota käytämme myös artikkelissa suurten PDF-tiedostojen suoratoisto tarvittaessa
Kääntäjienväliset regressiotestit kattavat saman käyttäytymisen kaikilla kolmella työkaluketjulla, mukaan lukien väittämä, jonka mukaan 2 MiB:n lähde ei koskaan näe yhtään yli 1 MiB:n lukua. Jos ylläpidät Delphi-, C++Builder- tai Lazarus-koodia, joka koskettaa PDF-rakennetta suoraan, ja kyllästyt maksamaan koko tiedoston jäsentämiskustannuksia neljästä sanakirjasta, harva indeksi ja sen ympärillä oleva julkinen sauma toimitetaan PDFiumPas Delphi PDFium -komponentissa