Tehnički članak

Retki lenji indeks PDF objekata u Delphi-ju sa PDFiumPas-om

Želite jedan rečnik iz 2 GB PDF-a i alat prvo razvija celu tablicu unakrsnih referenci u niz dimenzionisan trailerovim /Size. PDFiumPas zamenjuje taj korak retkim lenjim indeksom objekata: zadržava samo deskriptore xref sekcija, razrešava jedan broj objekta na zahtev kroz ograničene prozore, i kešira samo unose koje ste zaista dotakli

Stari oblik ovog koda u FPdfCompress bio je pošten ali skup. ApplyDefaultOpenAction je čitao ceo fajl u jedan TBytes, pa dodeljivao gust TPdfActiveXrefEntries niz sa jednim slotom po broju objekta do /Size. Dve stvari su pošle naopako na razmeri. Cena čitanja je rasla linearno sa veličinom dokumenta i kad je pozivalac hteo samo četiri rečnika, a gusti niz se sudarao sa budžetom parsera: TPdfParserResourceBudget.Default postavlja MaxObjects na 4.000.000, pa je savršeno validan fajl čiji najviši broj objekta sedi iznad tog plafona bio odbijen na memorijskom argumentu a ne na ispravnosti

Retki lenji indeks objekata u PDFiumPas-u u Delphi-ju upoređen sa gustim nizom unakrsnih referenci: gusti put čita ceo fajl i dodeljuje jedan slot po broju objekta do veličine trailer-a, dok retki put zadržava samo deskriptore sekcija
Samo deskriptori ostaju u memoriji, unosi ostaju u fajlu, i svako čitanje ide kroz ograničen prozor od jednog mebibajta

Zašto javni API PDFium-a ne odgovara na ovo pitanje?

Zato što informacija postoji unutar PDFium-a ali nikad ne prelazi C granicu. CPDF_Parser održava tablicu unakrsnih referenci, članstvo u objektnim tokovima i prednost revizije interno, a objavljeni headeri ne izlažu ulaznu tačku koja prima broj objekta i vraća njegov sirovi ofset, njegovu generaciju, koja je revizija pobedila ili u kom ObjStm živi. Strana čuvanja je jednako zatvorena: FPDF_SaveAsCopy i FPDF_SaveWithVersion vam daju samo sekvencijalni write callback. Svaka zakrpa kataloga na nivou bajtova posle nativnog čuvanja zato mora biti izgrađena u Pascal sloju, po čemu PDFiumPas sam raščlanjuje ove strukture umesto da ponovo koristi DLL

Šta retki indeks zaista drži u memoriji?

Deskriptore, ne unose. Za klasičnu tablicu (ISO 32000-1 §7.5.4) TPdfSparseXrefSubsection čuva prvi broj objekta, broj objekata, bajt ofset gde redovi unosa počinju i izmerenu širinu unosa. Unosi sami ostaju u fajlu. Širina se meri iz prvog reda umesto da se pretpostavi 20 bajtova, jer se proizvođači ne slažu oko završetaka linija; PDFiumPas prihvata 18 do 64 i odbija bilo šta van tog opsega, zajedno sa svakom podsekcijom čija bi deklarisana količina pretrčala kraj toka. Za tok unakrsnih referenci (§7.5.8) sekcija drži tri širine polja /W, svaku ograničenu na 0 do 8, spljoštene parove /Index, i dekodirane bajtove unosa, čija se očekivana dužina računa iz /W i /Index pre nego što se jedan jedini bajt razvije

Celi indeks gradi Initialize iz repnog prozora od najviše 1 MiB, gde se nalazi startxref, i svako naredno čitanje objekta koristi objektni prozor od 1 MiB. Plafon sirovog toka je 64 MiB i jedna xref linija ne sme preći 1024 bajta. Ako ste čitali našu belešku o validaciji objektnih i tokova unakrsnih referenci sa PDFiumPas-om, ista disciplina širine polja važi ovde, samo se sada koristi da adresuje jedan unos umesto da audituje celu tablicu

uses
  FPdfCompress;

var
  Source: TFileStream;
  Revision: TPdfSparseRevisionInfo;
begin
  Source := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    { prelazi samo startxref, lanac /Prev i katalog }
    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 ofset objekta puta izmerena širina; PDFiumPas zatim čita taj jedan red, raščlanjuje desetocifreni ofset i petocifrenu generaciju, proverava generaciju protiv plafona 65535 iz §7.5.4, i klasifikuje završnu ključnu reč kao axkDirect ili axkFree. Tok unakrsnih referenci treba još jedan korak jer su /Index podsekcije nadovezane u dekodiranom bajt nizu, pa indeks akumulira količine prethodnih podsekcija pre množenja zbirom širina /W. Tip 1 daje ofset, tip 2 daje broj objektnog toka i indeks člana, a sve ostalo postaje axkUnknown umesto nagađanja

{ klasična tablica, ISO 32000-1 odeljak 7.5.4 }
EntryOffset := Subsection.EntryOffset +
  Int64(ObjectNumber - Subsection.FirstObject) * Subsection.EntryWidth;

{ tok unakrsnih referenci, ISO 32000-1 odeljak 7.5.8 }
EntryWidth := Section.Widths[0] + Section.Widths[1] + Section.Widths[2];
EntryPosition := Integer((PriorCount + ObjectNumber -
  Section.IndexValues[I]) * EntryWidth);

Ništa u bilo kojem putu nije proporcionalno /Size. U tome je poenta prepisivanja: vrednost veličine trailer-a se prenosi dalje kao metapodatak i koristi se pri upisu inkrementalne revizije, ali nikad ne pokreće dodelu memorije. Regresiona suita ovo pribija fiksturom čije drvo stranica živi na objektu 1.000.000.000 i 1.000.000.001 pod trailerom koji deklariše /Size 1000000002. Stara gusta implementacija je odbila taj fajl; retki indeks razrešava obe reference i čuva deklarisanu veličinu u izlaznom traileru

Kako PDFiumPas razrešava jedan broj objekta u Delphi-ju: klasična tablica unakrsnih referenci množi izmerenu širinu reda, dok tok unakrsnih referenci akumulira količine prethodnih podsekcija pre množenja zbirom širina polja iz niza /W
Oba traženja su čista aritmetika, pa nijedno nije proporcionalno broju objekata deklarisanom u traileru

Hibridne revizije, lanci /Prev i stražari oko njih

Prednost revizije je mesto gde naivan lenji indeks greši. PDFiumPas prelazi lanac od startxref redosledom od najnovije i zaustavlja traženje na prvoj sekciji koja odgovori, što reprodukuje pravilo prednosti bez materijalizacije spojene tablice. Hibridno-referencirani fajlovi (§7.5.8.4) se rukuju unutar klasične grane: kad trailer nosi /XRefStm, dopunska sekcija toka se registruje pre klasične sekcije koja ju je imenovala, pa se komprimovani objekti nevidljivi običnoj tablici ipak nalaze dok klasični unosi zadržavaju svoj stav. Starije revizije se zatim prate kroz /Prev

Dva stražara ograničavaju taj obilazak, i oba su važna na oštećenim fajlovima. Svaki posjećeni ofset se beleži, pa /Prev koji ukazuje nazad u lanac terminira umesto da vrti u krug, a dubina obilaska je ograničena sa MaxRecursionDepth, koja po podrazumevanom iznosi 1024. Flag šifrovanja se akumulira kroz ceo lanac umesto da se čita samo iz najnovijeg trailer-a, jer dokument čiji poslednji trailer izostavlja /Encrypt može i dalje biti šifrovan dalje unazad; pozivaoci koji dodaju revizije oslanjaju se na taj flag da odbiju upis otvorenog teksta u šifrovani fajl

Kako PDFiumPas prelazi hibridni lanac PDF revizija u Delphi-ju: sekcije se registruju od najnovije iz startxref, dopunska XRefStm sekcija ide ispred klasične tablice koja ju je imenovala, a obilazak /Prev je ograničen posjećenim ofsetima i plafonom dubine
Traženje se zaustavlja na prvoj sekciji koja odgovori, što reprodukuje prednost revizije bez da ikad materijalizuje spojenu tablicu

Tip-2 unosi: zašto objektni tok čeka

Tip-2 unos imenuje objektni tok, i PDFiumPas ne dodiruje taj tok dok pozivalac ne zatraži člana njega. Kad konačno dodirne, /Type /ObjStm se verifikuje, /N se proverava protiv budžeta objekata i /First protiv plafona dekodiranih bajtova, i /N se proverava protiv /First pošto svaki par headera treba najmanje četiri bajta. Tek tada se tok razvija, i skeniranje headera se zaustavlja na zatraženom članu i njegovom nasledniku umesto da gradi punu tablicu članova. Jedan dekodirani objektni tok se zadržava u datom trenutku, što je pravi kompromis kad se grana drveta stranica grupiše u jedan ObjStm; naš tekst o dekodiranju objektnih tokova i prediktora u Delphi-ju pokriva šta se dešava unutar tog koraka razvijanja (§7.5.7)

var
  Reader: TPdfSparseDictionaryReader;
  Generation: Integer;
  Dict: AnsiString;
begin
  { jedan zadržani indeks, mnogo čitanja svesnih generacije }
  Reader := TPdfSparseDictionaryReader.Create(Source);
  try
    if Reader.Valid and
       Reader.ReadLatestDictionary(PageObjectNumber, Generation, Dict) then
      HandlePage(PageObjectNumber, Generation, Dict);
  finally
    Reader.Free;  { Source ostaje vaš }
  end;
end;

Gde keš prestaje davati obećanja

Indeks je snimak, i vredi biti direktan oko toga. Sekcije se raščlanjuju jednom u Initialize; ako se tok ispod kasnije izmeni, svaki keširani unos je zastareo i klasa to neće primetiti. TPdfSparseDictionaryReader drži indeks tokom životnog veka izvora u vlasništvu pozivaoca, što je tačno ono što rekurzivan obilazak drveta stranica želi i tačno ono što ne smete raditi preko prepisivanja. Keš unosa je ravan niz pretraživan linearno i čuva i negativne rezultate, pa je nekoliko stotina traženja jeftino a nekoliko stotina hiljada nije. ReadDictionary zahteva tačno poklapanje generacije dok ReadLatestDictionary razrešava aktivnu, i razlika je namerna: razrešavanje referenci treba prvo, inspekcija kataloga drugo. Gde se ove granice ne mogu poštovati, okružne jedinice se vraćaju na nasleđeni parser celog fajla umesto da suze skup fajlova koji još rade, obrazac koji koristimo i za strujanje velikih PDF-ova na zahtev

Unakrsno-kompajlerske regresije 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 direktno dodiruje PDF strukturu i umorili ste se od plaćanja troškova parsiranja celog fajla za četiri rečnika, retki indeks i javni šav oko njega isporučuju se u PDFiumPas Delphi PDFium komponenti