Chcete jeden slovník z PDF o 2 GB a nástroj nejprve roztáhne celou tabulku křížových referencí do pole velikostního podle traileru /Size. PDFiumPas nahrazuje ten krok řídkým líným indexem objektů: podrží jen deskriptory sekcí xref, vyřeší jediné číslo objektu na požádání přes ohraničená okna a cachuje jen položky, kterých jste skutečně dotkli
Stará podoba tohoto kódu ve FPdfCompress byla poctivá, ale drahá. ApplyDefaultOpenAction načetl kompletní soubor do jednoho TBytes, pak alokoval husté pole TPdfActiveXrefEntries s jedním slotem na číslo objektu až po /Size. Ve velkém se pokazily dvě věci. Cena čtení rostla lineárně s velikostí dokumentu, i když volající chtěl čtyři slovníky, a husté pole se střetlo s rozpočtem parseru: TPdfParserResourceBudget.Default nastavuje MaxObjects na 4 000 000, takže dokonale platný soubor, jehož nejvyšší číslo objektu sedí nad tím stropem, byl odmítnut paměťovým argumentem, nikoli správnostním
Proč veřejné API PDFium neodpoví na tuto otázku?
Protože informace existuje uvnitř PDFium, ale nikdy nepřejde C hranici. CPDF_Parser udržuje uvnitř tabulku křížových referencí, členství v object streams a precedenci revizí, ale zveřejněné hlavičky vystavují žádný vstupní bod, který by vzal číslo objektu a vrátil jeho surový offset, jeho generaci, která revize vyhrála, nebo ve kterém ObjStm žije. Ukládací strana je stejně zavřená: FPDF_SaveAsCopy a FPDF_SaveWithVersion vám podají jen sekvenční zápisový callback. Jakákoli bajtová oprava katalogu po nativním uložení se proto musí stavět v Pascal vrstvě, proto PDFiumPas parsuje tyto struktury samo, místo aby znovu použilo DLL
Co řídký index skutečně drží v paměti?
Deskriptory, ne položky. Pro klasickou tabulku (ISO 32000-1 §7.5.4) ukládá TPdfSparseXrefSubsection první číslo objektu, počet objektů, bajtový offset, kde začínají řádky položek, a naměřenou šířku položky. Samotné položky zůstávají v souboru. Šířka se měří z prvního řádku, nikoli se předpokládá 20 bajtů, protože producenti se rozcházejí o konce řádků; PDFiumPas přijímá 18 až 64 a odmítá cokoli mimo ten pás, spolu s jakýmkoli podsekem, jehož deklarovaný počet by běžel za konec proudu. Pro cross-reference stream (§7.5.8) sekce drží tři šířky polí /W, každé omezené na 0 až 8, zploštělé dvojice /Index a dekódované bajty položek, jejichž očekávaná délka se vypočte z /W a /Index, dřív než se nafoukne jediný bajt
Celý index staví Initialize z ocasného okna nejvýše 1 MiB, kde se najde startxref, a každé následné čtení objektu používá okno objektu 1 MiB. Strop surového proudu je 64 MiB a jediná řádka xref nesmí přesáhnout 1024 bajtů. Načetli jste-li naši poznámku o validaci object a cross-reference streams s PDFiumPas, totéž disciplína šířek polí platí tady, jen se teď používá k adresování jedné položky místo auditu celé tabulky
uses
FPdfCompress;
var
Source: TFileStream;
Revision: TPdfSparseRevisionInfo;
begin
Source := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
{ projde jen startxref, řetězec /Prev a 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;
Jak jediný lookup dosáhne jednoho objektu?
Aritmetikou, v obou rozloženích. Klasický podsek má řádky pevné šířky, takže adresa položky je start podseku plus offset objektu krát naměřená šířka; PDFiumPas pak načte tu jednu řádku, naparsuje desetimístný offset a pětimístnou generaci, zkontroluje generaci proti stropu 65535 z §7.5.4 a klasifikuje koncové klíčové slovo jako axkDirect nebo axkFree. Cross-reference stream potřebuje jeden krok navíc, protože podseky /Index jsou zřetězeny v dekódovaném běhu bajtů, takže index akumuluje počty předcházejících podseků, než násobí sečtenou šířkou /W. Typ 1 dává offset, typ 2 dává číslo object streamu a index člena a cokoli jiného se stane axkUnknown, nikoli odhadem
{ klasická tabulka, ISO 32000-1 sekce 7.5.4 }
EntryOffset := Subsection.EntryOffset +
Int64(ObjectNumber - Subsection.FirstObject) * Subsection.EntryWidth;
{ cross-reference stream, ISO 32000-1 sekce 7.5.8 }
EntryWidth := Section.Widths[0] + Section.Widths[1] + Section.Widths[2];
EntryPosition := Integer((PriorCount + ObjectNumber -
Section.IndexValues[I]) * EntryWidth);
Nic v žádné cestě není proporcionální k /Size. To je celá pointa přepisu: hodnota velikosti traileru se nese dopředu jako metadata a použije při zápisu inkrementální revize, ale nikdy neřídí alokaci. Regresní sada to připíná fixturou, jejíž strom stránek žije na objektech 1 000 000 000 a 1 000 000 001 pod trailerem deklarujícím /Size 1000000002. Stará hustá implementace ten soubor odmítla; řídký index vyřeší obě reference a podrží deklarovanou velikost ve výstupním traileru
Hybridní revize, řetězce /Prev a stráže kolem nich
Precedence revizí je místo, kde naivní líný index selhává. PDFiumPas projde řetězec od startxref v pořadí nejnovější-dřív a zastaví vyhledávání na první sekci, která odpoví, čímž reprodukuje pravidlo precedence bez materializace sloučené tabulky. Hybridně referenční soubory (§7.5.8.4) se řeší uvnitř klasické větve: nese-li trailer /XRefStm, doplňková stream sekce se zaregistruje před klasickou sekci, která na ni odkazovala, takže komprimované objekty neviditelné pro holou tabulku se stále najdou, zatímco klasické položky si podrží své postavení. Starší revize se pak následují přes /Prev
Dvě stráže ohraničují ten průchod a obě rozhodují na poškozených souborech. Každý navštívený offset se zaznamená, takže /Prev ukazující zpět do řetězce terminuje místo otáčení a hloubka procházení se stropuje MaxRecursionDepth, které má výchozí 1024. Příznak šifrování se akumuluje napříč celým řetězcem, nikoli se načte jen z nejnovějšího traileru, protože dokument, jehož poslední trailer vynechá /Encrypt, může být stále šifrovaný dále vzad; volající připojující revize se na ten příznak spoléhají, aby odmítli psát objekty v holém textu do šifrovaného souboru
Položky typu 2: proč object stream čeká
Položka typu 2 pojmenuje object stream a PDFiumPas se toho proudu nedotkne, dokud volající nepožádá o jeho člena. Když to konečně udělá, ověří se /Type /ObjStm, /N se zkontroluje proti rozpočtu objektů a /First proti stropu dekódovaných bajtů a /N se zkoumá vůči /First, protože každá dvojice hlavičky potřebuje aspoň čtyři bajty. Teprve pak se proud nafoukne a sken hlavičky se zastaví na požadovaném členovi a jeho následníkovi, místo stavby plné tabulky členů. Jeden dekódovaný object stream se drží najednou, což je správný kompromis, když se větev stromu stránek shlukne do jediného ObjStm; náš rozbor dekódování object streamu a prediktoru v Delphi pokrývá, co se děje uvnitř toho kroku inflace (§7.5.7)
var
Reader: TPdfSparseDictionaryReader;
Generation: Integer;
Dict: AnsiString;
begin
{ jeden podržený index, mnoho čtení vědomých generace }
Reader := TPdfSparseDictionaryReader.Create(Source);
try
if Reader.Valid and
Reader.ReadLatestDictionary(PageObjectNumber, Generation, Dict) then
HandlePage(PageObjectNumber, Generation, Dict);
finally
Reader.Free; { Source zůstává váš }
end;
end;
Kde cache přestává dávat sliby
Index je snímek a stojí za to být v tom tupý. Sekce se parsují jednou v Initialize; změní-li se podkladový proud potom, každá cachovaná položka je zastaralá a třída si toho nevšimne. TPdfSparseDictionaryReader drží index po dobu života zdroje vlastněnou volajícím, což je přesně to, co chce rekurzivní průchod stromem stránek, a přesně to, co nesmíte dělat napříč přepisem. Cache položek je ploché pole prohledávané lineárně a ukládá i negativní výsledky, takže pár set vyhledání je levných a pár set tisíc ne. ReadDictionary vyžaduje přesnou shodu generace, zatímco ReadLatestDictionary vyřeší aktivní, a ten rozdíl je záměrný: řešení referencí potřebuje to první, inspekce katalogu to druhé. Kde tyto meze nelze poctít, okolní jednotky spadnou na legacy parser celého souboru, místo aby zužovaly sadu souborů, které stále fungují, vzor, který používáme též pro streamování velkých PDF na požádání
Regrese napříč kompilátory pokrývají totéž chování na všech třech toolchainech, včetně tvrzení, že zdroj o 2 MiB nikdy nevidí jediné čtení větší než 1 MiB. Udržujete-li kód Delphi, C++Builder nebo Lazarus, který dotýká struktury PDF přímo, a jste unaveni platit celosouborové parse náklady za čtyři slovníky, řídký index a veřejný šev kolem něj se dodávají v PDFiumPas Delphi PDFium component