Vreți un singur dicționar dintr-un PDF de 2 GB și unealta mai întâi expandează întregul tabel de cross-reference într-un array dimensionat după trailer-ul /Size. PDFiumPas înlocuiește pasul acela cu un index sparse și lazy de obiecte: păstrează doar descriptorii de secțiuni xref, rezolvă un singur număr de obiect la cerere prin ferestre mărginite, și cache-uiește doar intrările pe care le-ați atins de fapt
Vechea formă a codului ăstuia în FPdfCompress era onestă dar scumpă. ApplyDefaultOpenAction citea fișierul complet într-un singur TBytes, apoi aloca un array dens TPdfActiveXrefEntries cu un slot per număr de obiect până la /Size. Două lucruri mergeau greșit la scară. Costul de citire creștea liniar cu mărimea documentului chiar când apelantul voia patru dicționare, iar array-ul dens se ciocnea cu bugetul parser-ului: TPdfParserResourceBudget.Default setează MaxObjects la 4.000.000, deci un fișier perfect valid al cărui cel mai mare număr de obiect stă deasupra acelui plafon era respins pe un argument de memorie, nu de corectitudine
De ce API-ul public PDFium nu răspunde la întrebarea asta?
Pentru că informația există în interiorul PDFium dar nu traversează niciodată frontiera C. CPDF_Parser întreține tabelul de cross-reference, apartenența la object stream și precedența reviziilor în interior, iar header-urile publicate nu expun niciun entry point care ia un număr de obiect și întoarce offset-ul său brut, generația sa, ce revizie a câștigat, sau în ce ObjStm locuiește. Partea de salvare e la fel de închisă: FPDF_SaveAsCopy și FPDF_SaveWithVersion vă dau doar un callback de scriere secvențial. Orice petic la nivel de byte pe un catalog după o salvare nativă trebuie deci construit în stratul Pascal, de aceea PDFiumPas parsează el însuși aceste structuri în loc să reutilizeze DLL-ul
Ce păstrează de fapt indexul sparse în memorie?
Descriptori, nu intrări. Pentru un tabel clasic (ISO 32000-1 §7.5.4) un TPdfSparseXrefSubsection stochează primul număr de obiect, numărul de obiecte, offset-ul de byte unde încep rândurile de intrări și lățimea măsurată a intrării. Intrările înseși rămân în fișier. Lățimea e măsurată din primul rând în loc să fie presupusă de 20 bytes, pentru că producătorii nu sunt de acord despre line endings; PDFiumPas acceptă 18 până la 64 și respinge orice în afara benzii aceleia, împreună cu orice subsecțiune a cărei numărătoare declarată ar trece de sfârșitul stream-ului. Pentru un cross-reference stream (§7.5.8) secțiunea ține cele trei lățimi de câmp /W, fiecare constrânsă la 0 până la 8, perechile /Index aplatizate, și bytes de intrare decodați, a căror lungime așteptată e calculată din /W și /Index înainte ca un singur byte să fie umflat
Întregul index e construit de Initialize dintr-o fereastră de coadă de cel mult 1 MiB, acolo unde e găsit startxref, iar fiecare citire ulterioară de obiect folosește o fereastră de obiect de 1 MiB. Plafonul de stream brut e 64 MiB, iar o singură linie xref nu poate depăși 1024 bytes. Dacă ați citit nota noastră despre validarea object și cross-reference streams cu PDFiumPas, aceeași disciplină de lățime de câmp se aplică aici, doar că acum e folosită să adreseze o singură intrare în loc să auditeze un tabel întreg
uses
FPdfCompress;
var
Source: TFileStream;
Revision: TPdfSparseRevisionInfo;
begin
Source := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
{ parcurge doar startxref, lanțul /Prev și catalogul }
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;
Cum ajunge o căutare la un singur obiect?
Prin aritmetică, în ambele layout-uri. O subsecțiune clasică are rânduri de lățime fixă, deci adresa unei intrări e începutul subsecțiunii plus offset-ul obiectului ori lățimea măsurată; PDFiumPas citește apoi acea singură linie, parsează offset-ul de zece cifre și generația de cinci cifre, verifică generația împotriva plafonului 65535 din §7.5.4, și clasifică cuvântul cheie de la coadă ca axkDirect sau axkFree. Un cross-reference stream are nevoie de un pas în plus pentru că subsecțiunile /Index sunt concatenate în run-ul de bytes decodat, deci indexul acumulează numărătoările subsecțiunilor precedente înainte să înmulțească cu lățimea /W sumată. Tipul 1 dă un offset, tipul 2 dă un număr de object stream și un index de membru, iar orice altceva devine axkUnknown în loc de o ghicire
{ tabel clasic, ISO 32000-1 secțiunea 7.5.4 }
EntryOffset := Subsection.EntryOffset +
Int64(ObjectNumber - Subsection.FirstObject) * Subsection.EntryWidth;
{ cross-reference stream, ISO 32000-1 secțiunea 7.5.8 }
EntryWidth := Section.Widths[0] + Section.Widths[1] + Section.Widths[2];
EntryPosition := Integer((PriorCount + ObjectNumber -
Section.IndexValues[I]) * EntryWidth);
Nimic din oricare drum nu e proporțional cu /Size. Asta e tot sensul rescrierii: valoarea de mărime din trailer e purtată mai departe ca metadata și folosită la scrierea reviziei incrementale, dar nu conduce niciodată o alocare. Suita de regresie fixează asta cu un fixture al cărui page tree locuiește la obiectul 1.000.000.000 și 1.000.000.001 sub un trailer care declară /Size 1000000002. Vechea implementare densă refuza acel fișier; indexul sparse rezolvă ambele referințe și păstrează mărimea declarată în trailer-ul de ieșire
Revizii hibride, lanțuri /Prev și gărzile din jurul lor
Precedența reviziilor e locul unde un index lazy naiv greșește. PDFiumPas parcurge lanțul din startxref în ordine cel-mai-nou-întâi și oprește o căutare la prima secțiune care răspunde, ceea ce reproduce regula de precedență fără a materializa un tabel fuzionat. Fișierele hybrid-reference (§7.5.8.4) sunt tratate în interiorul ramurii clasice: când trailer-ul poartă un /XRefStm, secțiunea de stream suplimentară e înregistrată înaintea secțiunii clasice care a referențiat-o, deci obiectele comprimate invizibile pentru tabelul plat sunt încă găsite în timp ce intrările clasice își păstrează statutul. Reviziile mai vechi sunt apoi urmărite prin /Prev
Două gărzi mărginesc parcurgerea aceea, iar ambele contează pe fișierele avariate. Fiecare offset vizitat e înregistrat, deci un /Prev care arată înapoi în lanț se termină în loc să se învârtească, iar adâncimea de traversare e plafonată de MaxRecursionDepth, implicit 1024. Flag-ul de criptare e acumulat pe întreg lanțul în loc să fie citit doar din cel mai nou trailer, pentru că un document al cărui ultim trailer omite /Encrypt poate fi încă criptat mai în spate; apelanții care adaugă revizii se bazează pe flag-ul acela ca să refuze scrierea de obiecte plaintext într-un fișier criptat
Intrările de tip 2: de ce așteaptă object stream-ul
O intrare de tip 2 numește un object stream, iar PDFiumPas nu atinge stream-ul acela până când un apelant cere un membru al lui. Când în sfârșit o face, /Type /ObjStm e verificat, /N e verificat împotriva bugetului de obiecte și /First împotriva plafonului de bytes decodați, iar /N e verificat sanitar împotriva lui /First întrucât fiecare pereche de header are nevoie de cel puțin patru bytes. Abia atunci stream-ul e umflat, iar scanarea header-ului se oprește la membrul cerut și succesorul său în loc să construiască un tabel complet de membri. Un singur object stream decodat e reținut odată, care e schimbul corect când o ramură de page tree se aglomerează într-un singur ObjStm; scrierea noastră despre decodarea object stream și predictor în Delphi acoperă ce se întâmplă în interiorul pasului aceluia de inflate (§7.5.7)
var
Reader: TPdfSparseDictionaryReader;
Generation: Integer;
Dict: AnsiString;
begin
{ un singur index reținut, multe citiri conștiente de generație }
Reader := TPdfSparseDictionaryReader.Create(Source);
try
if Reader.Valid and
Reader.ReadLatestDictionary(PageObjectNumber, Generation, Dict) then
HandlePage(PageObjectNumber, Generation, Dict);
finally
Reader.Free; { Source rămâne al tău }
end;
end;
Unde cache-ul încetează să mai facă promisiuni
Indexul e un snapshot, și merită spus fără ocoliri. Secțiunile sunt parsate o dată în Initialize; dacă stream-ul de dedesubt e modificat după, fiecare intrare din cache e perimată și clasa nu va observa. TPdfSparseDictionaryReader ține indexul pe durata de viață deținută de apelant a sursei, ceea ce e exact ce vrea o parcurgere recursivă peste un page tree și exact ce nu trebuie să faceți în timpul unei rescrieri. Cache-ul de intrări e un array plat căutat liniar și stochează și rezultate negative, deci câteva sute de căutări sunt ieftine, iar câteva sute de mii nu sunt. ReadDictionary cere o potrivire exactă de generație pe când ReadLatestDictionary rezolvă cea activă, iar diferența e deliberată: rezoluția de referințe are nevoie de prima, inspecția de catalog are nevoie de a doua. Acolo unde limitele acestea nu pot fi respectate, unitățile din jur cad înapoi pe parser-ul legacy de fișier întreg în loc să îngusteze mulțimea de fișiere care încă funcționează, un tipar pe care îl folosim și pentru streaming la cerere al PDF-urilor mari
Regresiile cross-compiler acoperă același comportament pe toate trei toolchain-urile, inclusiv o aserțiune că o sursă de 2 MiB nu vede niciodată o singură citire mai mare de 1 MiB. Dacă întrețineți cod Delphi, C++Builder sau Lazarus care atinge direct structura PDF și sunteți obosit să plătiți costuri de parsare de fișier întreg pentru patru dicționare, indexul sparse și cusătura publică din jurul lui sosesc în componenta PDFiumPas Delphi PDFium