Tehnički članak

Rijetki lijeni indeks PDF objekata u Delphiju s PDFiumPasom

Želite jedan rječnik iz 2 GB PDF-a, a alat prvo raširi cijelu tablicu unakrsnih referenci u niz dimenzioniran po prikolici /Size. PDFiumPas taj korak zamjenjuje rijetkim lijenim indeksom objekata: zadržava samo opise xref sekcija, razrješuje jedan broj objekta na zahtjev kroz ograničene prozore i kešira samo unose koje ste stvarno dodirnuli

Stari oblik ovoga koda u FPdfCompress bio je pošten ali skup. ApplyDefaultOpenAction pročitao je cijelu datoteku u jedan TBytes, a zatim alocirao gusti niz TPdfActiveXrefEntries s jednim utorom po broju objekta do /Size. U mjerilu su dva puta pošla krivo. Cijena čitanja rasla je linearno s veličinom dokumenta i kad je pozivatelj htio četiri rječnika, a gusti niz sudarao se s proračunom parsera: TPdfParserResourceBudget.Default postavlja MaxObjects na 4,000,000, pa je sasvim valjana datoteka čiji najviši broj objekta sjedi iznad toga stropa odbijena argumentom memorije umjesto argumentom ispravnosti

Rijetki lijeni indeks objekata u PDFiumPasu u Delphiju uspoređen s gustim nizom unakrsnih referenci: gusti put čita cijelu datoteku i alocira jedan utor po broju objekta do veličine prikolice, dok rijetki put zadržava samo opise sekcija
U memoriji ostaju samo opisi, unosi ostaju u datoteci, a svako čitanje ide kroz ograničeni prozor od jedne mebibajta

Zašto javni API PDFiuma ne odgovara na ovo pitanje?

Jer informacija postoji unutar PDFiuma ali nikad ne prelazi C granicu. CPDF_Parser održava tablicu unakrsnih referenci, članstvo u tokovima objekata i prednost revizija interno, a objavljena zaglavlja ne izlažu ulaznu točku koja prima broj objekta i vraća njegov sirovi pomak, generaciju, koja je revizija pobijedila ili u kojem ObjStm živi. Strana spremanja je jednako zatvorena: FPDF_SaveAsCopy i FPDF_SaveWithVersion vam predaju samo povratni poziv za slijedno pisanje. Svaka popravka na razini bajtova nad katalogom nakon izvornog spremanja zato mora biti građena u Pascal sloju, pošto PDFiumPas te strukture raščlanjuje sam umjesto da ponovno koristi DLL

Što rijetki indeks stvarno drži u memoriji?

Opise, ne unose. Za klasičnu tablicu (ISO 32000-1 §7.5.4) TPdfSparseXrefSubsection sprema prvi broj objekta, broj objekata, bajtni pomak gdje redovi unosa počinju i izmjerenu širinu unosa. Sami unosi ostaju u datoteci. Širina se mjeri iz prvog reda umjesto da se pretpostavi 20 bajtova, jer se proizvođači ne slažu oko završetaka redaka; PDFiumPas prima 18 do 64 i odbija sve izvan toga pojasa, uz svaku podsekciju čiji bi deklarirani broj odletio iza kraja toka. Za tok unakrsnih referenci (§7.5.8) sekcija drži tri širine polja /W, svaku ograničenu na 0 do 8, izravnane parove /Index i dekodirane bajtove unosa, čija se očekivana duljina računa iz /W i /Index prije nego što se jedan jedini bajt naduje

Cijeli indeks gradi Initialize iz repnog prozora najviše 1 MiB, gdje se nalazi startxref, a svako naknadno čitanje objekta koristi objektni prozor od 1 MiB. Strop sirovog toka je 64 MiB i jedan xref redak ne smije prijeći 1024 bajtova. Ako ste čitali našu napomenu o provjeri tokova objekata i unakrsnih referenci s PDFiumPasom, ista disciplina širine polja vrijedi i ovdje, samo se sada koristi da se adresira jedan unos umjesto da se revizira cijela tablica

uses
  FPdfCompress;

var
  Source: TFileStream;
  Revision: TPdfSparseRevisionInfo;
begin
  Source := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    { walks startxref, the /Prev chain and the catalog only }
    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;

Kako jedno traženje dođe do jednog objekta?

Aritmetikom, u oba rasporeda. Klasična podsekcija ima redove fiksne širine, pa je adresa unosa početak podsekcije plus pomak objekta puta izmjerena širina; PDFiumPas zatim čita taj jedan redak, raščlanjuje deseterocifreni pomak i peterocifrenu generaciju, provjerava generaciju prema stropu 65535 iz §7.5.4 i svrstava završnu ključnu riječ kao axkDirect ili axkFree. Tok unakrsnih referenci treba jedan korak više jer su /Index podsekcije spojene u dekodiranom bajtnom nizu, pa indeks akumulira brojeve prethodnih podsekcija prije množenja zbrojenom širinom /W. Tip 1 daje pomak, tip 2 daje broj toka objekata i indeks člana, a sve ostalo postaje axkUnknown umjesto nagađanja

{ classic table, ISO 32000-1 section 7.5.4 }
EntryOffset := Subsection.EntryOffset +
  Int64(ObjectNumber - Subsection.FirstObject) * Subsection.EntryWidth;

{ cross-reference stream, ISO 32000-1 section 7.5.8 }
EntryWidth := Section.Widths[0] + Section.Widths[1] + Section.Widths[2];
EntryPosition := Integer((PriorCount + ObjectNumber -
  Section.IndexValues[I]) * EntryWidth);

Ništa u nijednom putu nije proporcionalno /Size. U tome je cijela poanta prepravke: vrijednost veličine prikolice nosi se naprijed kao metapodatak i koristi pri pisanju inkrementalne revizije, ali nikad ne pokreće alokaciju. Regresijski skup ovo pribija fiksturom čije drvo stranica živi na objektu 1,000,000,000 i 1,000,000,001 pod prikolicom koja deklarira /Size 1000000002. Stara gusta izvedba tu je datoteku odbijala; rijetki indeks razrješuje obje reference i čuva deklariranu veličinu u izlaznoj prikolici

Kako PDFiumPas u Delphiju razrješuje jedan broj objekta: klasična tablica unakrsnih referenci množi izmjerenom širinom reda, dok tok unakrsnih referenci akumulira brojeve prethodnih podsekcija prije množenja zbrojenim širinama polja iz niza /W
Oba traženja su čista aritmetika, pa ni jedno nije proporcionalno broju objekata deklariranom u prikolici

Hibridne revizije, lanci /Prev i osigurači oko njih

Prednost revizija je mjesto gdje naivan lijeni indeks promaši. PDFiumPas prolazi lanac od startxref u redoslijedu od najnovije i zaustavlja traženje na prvoj sekciji koja odgovori, čime reproducira pravilo prednosti bez materijaliziranja spojene tablice. Hibridne referentne datoteke (§7.5.8.4) rukuju se unutar klasične grane: kad prikolica nosi /XRefStm, dopunska sekcija toka registrira se prije klasične sekcije koja ju je imenovala, pa se kompresirani objekti nevidljivi običnoj tablici ipak nalaze dok klasični unosi zadržavaju svoj stupanj. Starije revizije slijede se kroz /Prev

Dva osigurača vežu taj obilazak, i oba su bitna na oštećenim datotekama. Svaki posjećeni pomak bilježi se, pa /Prev koji pokazuje natrag u lanac staje umjesto da se vrti, a dubina obilaska je ograničena s MaxRecursionDepth, koji je po zadanom 1024. Zastavica šifriranja akumulira se kroz cijeli lanac umjesto da se čita samo iz najnovije prikolice, jer dokument čija zadnja prikolica izostavlja /Encrypt može biti šifriran dublje u nazad; pozivatelji koji dopisuju revizije oslanjaju se na tu zastavicu da odbiju pisanje objekata otvorenog teksta u šifriranu datoteku

Kako PDFiumPas u Delphiju obilazi hibridni lanac PDF revizija: sekcije se registriraju od najnovije od startxref, dopunska sekcija XRefStm ide ispred klasične tablice koja ju je imenovala, a obilazak /Prev ograničen je posjećenim pomacima i stropom dubine
Traženje staje na prvoj sekciji koja odgovori, čime se reproducira prednost revizija bez da se ikad materijalizira spojena tablica

Unosi tipa 2: zašto tok objekata čeka

Unos tipa 2 imenuje tok objekata, i PDFiumPas ne dira taj tok dok pozivatelj ne zatraži člana njega. Kad konačno to učini, /Type /ObjStm se potvrđuje, /N se provjerava prema proračunu objekata i /First prema stropu dekodiranih bajtova, a /N se provjerava i protiv /First jer svaki par zaglavlja treba najmanje četiri bajta. Tek tada se tok naduje, a pregled zaglavlja staje na zatraženom članu i njegovom sljedbeniku umjesto da gradi punu tablicu članova. Jedan dekodirani tok objekata zadržava se u jednom trenu, što je pravi kompromis kad se grana drveta stranica nakupi u jedan ObjStm; naš prikaz o dekodiranju tokova objekata i prediktora u Delphiju pokriva što se događa unutar toga koraka dekompresije (§7.5.7)

var
  Reader: TPdfSparseDictionaryReader;
  Generation: Integer;
  Dict: AnsiString;
begin
  { one retained index, many generation-aware reads }
  Reader := TPdfSparseDictionaryReader.Create(Source);
  try
    if Reader.Valid and
       Reader.ReadLatestDictionary(PageObjectNumber, Generation, Dict) then
      HandlePage(PageObjectNumber, Generation, Dict);
  finally
    Reader.Free;  { Source stays yours }
  end;
end;

Gdje keš prestaje davati obećanja

Indeks je snimak, i vrijedi biti prizemljen o tome. Sekcije se raščlanjuju jednom u Initialize; ako se temeljni tok poslije izmijeni, svaki keširani unos je zastario i razred to neće primijetiti. TPdfSparseDictionaryReader drži indeks za trajanje vlasništva pozivatelja nad izvorom, što je točno ono što rekurzivni obilazak drveta stranica hoće i točno ono što ne smijete raditi preko prepravke. Keš unosa je ravni niz pretraživan linearno i sprema i negativne rezultate, pa su stotine traženja jeftine a stotine tisuća nisu. ReadDictionary traži točno poklapanje generacije dok ReadLatestDictionary razrješuje aktivnu, i razlika je namjerna: razrješivanje referenci treba prvo, pregled kataloga drugo. Gdje se te granice ne mogu poštovati, okolne jedinice vraćaju se na nasljedni parser cijele datoteke umjesto da suze skup datoteka koje još rade, obrazac koji koristimo i za slijedno strujanje velikih PDF-ova na zahtjev

Regresije preko kompilatora pokrivaju isto ponašanje na sva tri alatna lanca, uključujući tvrdnju da 2 MiB izvor nikad ne vidi jedno čitanje veće od 1 MiB. Ako održavate Delphi, C++Builder ili Lazarus kod koji dotiče PDF strukturu izravno i umorili ste se od plaćanja troškova parsiranja cijele datoteke za četiri rječnika, rijetki indeks i javni šav oko njega stižu u PDFiumPas Delphi PDFium komponenti