Egy 2 GB-os PDF-ből egyetlen szótár kell, és az eszköz előbb az egész kereszthivatkozási táblát kibontja a trailer /Size-a szerint méretezett tömbbe. A PDFiumPas ezt a lépést ritka lusta objektumindexre cseréli: csak az xref szakaszleírókat tartja meg, egyetlen objektumszámot igény szerint old fel korlátos ablakokon keresztül, és csak azokat a bejegyzéseket gyorsítótárazza, amelyeket ténylegesen megérintett
A kód régi alakja a FPdfCompress-ben becsületes, de drága volt. Az ApplyDefaultOpenAction a teljes fájlt egyetlen TBytes-ba olvasta, majd sűrű TPdfActiveXrefEntries tömböt osztott ki, objektumszámonként egy hellyel a /Size-ig. Két dolog romlott el nagy léptékben. Az olvasási költség lineárisan nőtt a dokumentum méretével, akkor is, amikor a hívó négy szótárt akart, és a sűrű tömb ütközött az elemző költségvetésével: a TPdfParserResourceBudget.Default a MaxObjects-t 4 000 000-ra állítja, így egy tökéletesen érvényes fájl, amelynek legmagasabb objektumszáma e fölött ül, memória-érvvel, nem helyességi érvvel utasítódott el
Miért nem válaszol erre a kérdésre a PDFium nyilvános API-ja?
Mert az információ a PDFium belsejében létezik, de sosem lépi át a C határt. A CPDF_Parser belsőleg karbantartja a kereszthivatkozási táblát, az objektumstream-tagságot és a revíziós elsőbbséget, mégis a közzétett fejlécek nem tárnak fel olyan belépési pontot, amely átvesz egy objektumszámot, és visszaadja annak nyers eltolását, generációját, hogy melyik revízió nyert, vagy melyik ObjStm-ben él. A mentési oldal ugyanilyen zárt: az FPDF_SaveAsCopy és az FPDF_SaveWithVersion csak szekvenciális írási visszahívást ad a kezébe. Egy natív mentés utáni, bájtszintű katalógusjavítás ezért a Pascal rétegben épül, ezért elemzi maga a PDFiumPas ezeket a struktúrákat a DLL újrafelhasználása helyett
Mit tart valójában a memóriában a ritka index?
Leírókat, nem bejegyzéseket. Klasszikus táblánál (ISO 32000-1 §7.5.4) egy TPdfSparseXrefSubsection tárolja az első objektumszámot, az objektumok számát, azt a bájtoffszetet, ahol a bejegyzéssorok kezdődnek, és a mért bejegyzésszélességet. Maguk a bejegyzések a fájlban maradnak. A szélesség az első sorból mérődik, nem feltételezett 20 bájt, mert a gyártók eltérnek a sorvégekben; a PDFiumPas 18-tól 64-ig fogad el, és mindent elutasít ebből a sávból kifelé, akárcsak minden olyan alszakaszt, amelynek deklarált száma túlfutna a stream végén. Kereszthivatkozási streamnél (§7.5.8) a szakasz tartja a három /W mezőszélességet, mindegyiket 0 és 8 közé szorítva, a lapított /Index párokat és a dekódolt bejegyzésbájtokat, amelyek várt hosszát /W-ből és /Index-ből számolják, mielőtt egyetlen bájt felfújódna
Az egész indexet az Initialize építi fel legfeljebb 1 MiB-os farokablakból, amely a startxref megtalálásának helye, és minden ezt követő objektumolvasás 1 MiB-os objektumablakot használ. A nyers stream plafonja 64 MiB, és egyetlen xref sor nem haladhatja meg az 1024 bájtot. Ha elolvasta a objektum- és kereszthivatkozási streamek validálása PDFiumPasszal című jegyzetünket, ugyanaz a mezőszélességi fegyelem érvényes itt is, csak most egyetlen bejegyzés címzésére használják, nem egy egész tábla auditálására
uses
FPdfCompress;
var
Source: TFileStream;
Revision: TPdfSparseRevisionInfo;
begin
Source := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
{ csak a startxref-et, a /Prev láncot és a katalógust járja }
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;
Hogyan ér el egy keresés egy objektumot?
Számítással, mindkét elrendezésben. Egy klasszikus alszakasz rögzített szélességű sorokból áll, így egy bejegyzés címe az alszakasz kezdete plusz az objektumoffszet szorozva a mért szélességgel; a PDFiumPas ezután beolvassa azt az egy sort, elemzi a tízjegyű ofszetet és az ötjegyű generációt, a generációt a §7.5.4 szerinti 65535-es plafonhoz ellenőrzi, és a záró kulcsszót axkDirect-ként vagy axkFree-ként osztályozza. Egy kereszthivatkozási stream egy plusz lépést igényel, mert a /Index alszakaszok a dekódolt bájtfutamban összefűződnek, így az index a megelőző alszakaszok számait halmozza, mielőtt az összegzett /W szélességgel szorozna. Az 1-es típus ofszetet ad, a 2-es típus objektumstreamsorszámot és tagsági indexet ad, minden más axkUnknown-dé válik találgatás helyett
{ klasszikus tábla, ISO 32000-1 7.5.4 szakasz }
EntryOffset := Subsection.EntryOffset +
Int64(ObjectNumber - Subsection.FirstObject) * Subsection.EntryWidth;
{ kereszthivatkozási stream, ISO 32000-1 7.5.8 szakasz }
EntryWidth := Section.Widths[0] + Section.Widths[1] + Section.Widths[2];
EntryPosition := Integer((PriorCount + ObjectNumber -
Section.IndexValues[I]) * EntryWidth);
Semmi az utak közül nem arányos a /Size-szal. Ez az újraírás teljes értelme: a trailer méretértéke metaadatként kerül tovább, és az inkrementális revízió írásakor használatos, de sosem vezet kiosztást. A regressziós készlet egy olyan javítójeggyel rögzíti ezt, amelynek lapfája az 1 000 000 000 és 1 000 000 001 objektumszámokon ül, egy /Size 1000000002-t deklaráló trailer alatt. A régi sűrű megvalósítás elutasította azt a fájlt; a ritka index mindkét hivatkozást feloldja, és megőrzi a deklarált méretet a kimeneti trailerben
Hibrid revíziók, /Prev láncok és a védelmek körülöttük
A revíziós elsőbbség az, ahol egy naiv lusta index elromlik. A PDFiumPas a láncot a startxref-től legújabb-előbb sorrendben járja, és a keresést az első válaszoló szakasznál állítja meg, ami az elsőbbségi szabályt reprodukálja összefésült tábla felépítése nélkül. A hibrid-hivatkozású fájlokat (§7.5.8.4) a klasszikus ág belül kezeli: amikor a trailer /XRefStm-et hordoz, a kiegészítő stream szakasz az őt megnevező klasszikus szakasz előtt regisztrálódik, így a sima tábla számára láthatatlan tömörített objektumok is megtalálhatók, miközben a klasszikus bejegyzések megőrzik érvényességüket. A régebbi revíziók ezután /Prev-en keresztül követődnek
Két védelem korlátozza azt a járást, és mindkettő számít sérült fájloknál. Minden meglátogatott ofszet rögzítésre kerül, így a láncba visszamutató /Prev leáll pörgés helyett, és a bejárási mélységet a MaxRecursionDepth koronázza, amelynek alapértéke 1024. A titkosítási jelző az egész láncra halmozódik, nem csak a legújabb trailerből olvasódik, mert egy olyan dokumentum, amelynek legújabb trailere kihagyja az /Encrypt-et, még mindig titkosított lehet hátrébb; a revíziókat hozzáfűző hívók arra a jelzőre támaszkodnak, hogy ne írjanak nyílt szöveges objektumokat titkosított fájlba
2-es típusú bejegyzések: miért vár az objektumstream
Egy 2-es típusú bejegyzés objektumstreamet nevez meg, és a PDFiumPas nem érinti azt a streamet, amíg egy hívó nem kér belőle tagot. Amikor végre megteszi, ellenőrzésre kerül a /Type /ObjStm, a /N-t az objektumkerethez, a /First-t a dekódolt bájtok plafonjához mérik, és a /N-t ésszerűségi vizsgálat alá vetik a /First-höz képest, mivel minden fejlécpárnak legalább négy bájtra van szüksége. Csak ezután fújódik fel a stream, és a fejlécátvizsgálás a kért tagnál és annak utódjánál áll meg, nem pedig teljes tagtáblát épít. Egyszerre egy dekódolt objektumstream marad megőrizve, ami a helyes kompromisszum, amikor egy lapfaág egyetlen ObjStm-be tömörül; az objektumstream- és prediktordekódolás Delphiben írásunk azt tárgyalja, mi történik az említett felfújási lépésen belül (§7.5.7)
var
Reader: TPdfSparseDictionaryReader;
Generation: Integer;
Dict: AnsiString;
begin
{ egy megőrzött index, sok generációtudatos olvasás }
Reader := TPdfSparseDictionaryReader.Create(Source);
try
if Reader.Valid and
Reader.ReadLatestDictionary(PageObjectNumber, Generation, Dict) then
HandlePage(PageObjectNumber, Generation, Dict);
finally
Reader.Free; { a Source az Öné marad }
end;
end;
Ahol a gyorsítótár felhagy az ígéretekkel
Az index egy pillanatfelvétel, és érdemes egyenesen kimondani. A szakaszok egyszer, az Initialize-ben elemződnek; ha az alapul szolgáló stream ezután módosul, minden gyorsítótárazott bejegyzés elavult, és az osztály észre sem veszi. A TPdfSparseDictionaryReader az indexet a forrás hívó tulajdonában lévő élettartamára tartja meg, ami pontosan az, amit egy lapfa feletti rekurzív járás akar, és pontosan az, amit egy átírás ideje alatt nem szabad megtenni. A bejegyzés-gyorsítótár lapos tömb, lineárisan kereshető, és negatív eredményeket is tárol, így néhány száz keresés olcsó, néhány százezer nem. A ReadDictionary pontos generációs egyezést követel, míg a ReadLatestDictionary az aktívat oldja fel, és a különbség szándékos: a hivatkozásfeloldásnak az első kell, a katalógusvizsgálatnak az utóbbi. Ahol ezek a határok nem tartathatók meg, a környező egységek a hagyományos egész-fájl elemzőre esnek vissza, nem szűkítve azon fájlok halmazát, amelyek még működnek; ezt a mintát alkalmazzuk a nagy PDF-ek igény szerinti streamelése esetén is
Az átfordítói regressziók mindhárom eszközláncra lefedik ugyanazt a viselkedést, ideértve egy állítást, hogy egy 2 MiB-os forrás sosem lát 1 MiB-nál nagyobb egyetlen olvasást sem. Ha Ön Delphi, C++Builder vagy Lazarus kódot tart karban, amely közvetlenül nyúl a PDF-struktúrához, és belefáradt abba, hogy négy szótárért egész-fájl elemzési költséget fizessen, a ritka index és körülötte a nyilvános varrat a PDFiumPas Delphi PDFium komponensben szállít