Du vill ha en enda ordlista ur en PDF på 2 GB och verktyget expanderar först hela korsreferenstabellen till en array dimensionerad efter trailerns /Size. PDFiumPas ersätter det steget med ett glest objektindex med lata utvärderingar: det behåller bara xref-sektionsbeskrivningarna, löser ett enskilt objektnummer på begäran genom avgränsade fönster och cachar bara de poster du faktiskt rört
Den gamla formen på den här koden i FPdfCompress var ärlig men dyr. ApplyDefaultOpenAction läste in hela filen i ett enda TBytes och allokerade sedan en tät TPdfActiveXrefEntries-array med en lucka per objektnummer upp till /Size. Två saker gick fel i skala. Läskostnaden växte linjärt med dokumentstorleken även när anroparen ville ha fyra ordlistor, och den täta arrayen krockade med parserbudgeten: TPdfParserResourceBudget.Default sätter MaxObjects till 4 000 000, så en fullt giltig fil vars högsta objektnummer ligger över det taket avvisades på ett minnesargument i stället för ett korrekthetsargument
Varför svarar inte PDFiums publika API på den här frågan?
Eftersom informationen finns inuti PDFium men aldrig korsar C-gränsen. CPDF_Parser underhåller korsreferenstabellen, objektströmsmedlemskapet och revisionsföreträdet internt, men de publicerade rubrikfilerna exponerar ingen ingång som tar ett objektnummer och returnerar dess råa offset, dess generation, vilken revision som vann, eller vilken ObjStm den bor i. Spara-sidan är lika stängd: FPDF_SaveAsCopy och FPDF_SaveWithVersion ger dig bara en sekventiell skriv-callback. Varje bytenivå-lapp mot en katalog efter en native-spara måste därför byggas i Pascal-lagret, vilket är skälet till att PDFiumPas tolkar dessa strukturer själv i stället för att återanvända DLL:en
Vad behåller det glesa indexet egentligen i minnet?
Beskrivningar, inte poster. För en klassisk tabell (ISO 32000-1 §7.5.4) lagrar en TPdfSparseXrefSubsection första objektnumret, antal objekt, byteoffseten där postraderna börjar och den uppmätta postbredden. Själva posterna stannar i filen. Bredden mäts från första raden i stället för att antas vara 20 bytes, eftersom producenter är oense om radslut; PDFiumPas accepterar 18 till 64 och avvisar allt utanför det bandet, tillsammans med varje subsektion vars deklarerade antal skulle löpa bortom slutet på strömmen. För en korsreferensström (§7.5.8) håller sektionen de tre /W-fältbredderna, var och en begränsad till 0 genom 8, de tillplattade /Index-paren och de avkodade postbytena, vars förväntade längd beräknas från /W och /Index innan en enda byte expanderas
Hela indexet byggs av Initialize från ett stjärtfönster på högst 1 MiB, där startxref hittas, och varje efterföljande objektläsning använder ett objektfönster på 1 MiB. Taket för råströmmar är 64 MiB och en enskild xref-rad får inte överstiga 1024 bytes. Om du läst vår notis om att validera objekt- och korsreferensströmmar med PDFiumPas gäller samma fältbreddsdisciplin här, bara att den nu används för att adressera en post i stället för att granska en hel tabell
uses
FPdfCompress;
var
Source: TFileStream;
Revision: TPdfSparseRevisionInfo;
begin
Source := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
{ går bara igenom startxref, /Prev-kedjan och katalogen }
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;
Hur når en uppslagning ett enda objekt?
Genom aritmetik, i båda layouterna. En klassisk subsektion har rader med fast bredd, så adressen till en post är subsektionens start plus objektoffsetet gånger den uppmätta bredden; PDFiumPas läser sedan den enda raden, tolkar den tiosiffriga offseten och den femsiffriga generationen, kontrollerar generationen mot taket 65535 från §7.5.4 och klassificerar det avslutande nyckelordet som axkDirect eller axkFree. En korsreferensström behöver ett steg till eftersom /Index-subsektionerna är sammanfogade i den avkodade bytekörningen, så indexet ackumulerar antalen från föregående subsektioner innan det multiplicerar med den summerade /W-bredden. Typ 1 ger en offset, typ 2 ger ett objektströmsnummer och ett medlemsindex, och allt annat blir axkUnknown i stället för en gissning
{ klassisk tabell, ISO 32000-1 avsnitt 7.5.4 }
EntryOffset := Subsection.EntryOffset +
Int64(ObjectNumber - Subsection.FirstObject) * Subsection.EntryWidth;
{ korsreferensström, ISO 32000-1 avsnitt 7.5.8 }
EntryWidth := Section.Widths[0] + Section.Widths[1] + Section.Widths[2];
EntryPosition := Integer((PriorCount + ObjectNumber -
Section.IndexValues[I]) * EntryWidth);
Ingenting i någon av vägarna är proportionellt mot /Size. Det är hela poängen med omskrivningen: trailerstorleksvärdet förs vidare som metadata och används när den inkrementella revisionen skrivs, men det driver aldrig en allokering. Regressionssviten fäster det med ett fixtur vars sidträd bor på objekt 1 000 000 000 och 1 000 000 001 under en trailer som deklarerar /Size 1000000002. Den gamla täta implementationen vägrade den filen; det glesa indexet löser båda referenserna och bevarar den deklarerade storleken i utdatatrailern
Hybridrevisioner, /Prev-kedjor och vakterna runt dem
Revisionsföreträde är där ett naivt lätt index går fel. PDFiumPas går igenom kedjan från startxref i nyast-först-ordning och stoppar en uppslagning vid första sektion som svarar, vilket reproducerar företrädesregeln utan att materialisera en sammanslagen tabell. Hybridreferensfiler (§7.5.8.4) hanteras inuti den klassiska grenen: när trailern bär en /XRefStm registreras den kompletterande strömsektionen före den klassiska sektion som refererade den, så komprimerade objekt osynliga för den vanliga tabellen ändå hittas medan de klassiska posterna behåller sin ställning. Äldre revisioner följs sedan genom /Prev
Två vakter avgränsar den gången, och båda spelar roll på skadade filer. Varje besökt offset registreras, så en /Prev som pekar tillbaka in i kedjan terminerar i stället för att snurra, och genomtraverseringsdjupet begränsas av MaxRecursionDepth, som standard 1024. Krypteringsflaggan ackumuleras över hela kedjan i stället för att läsas från enbart den nyasta trailern, eftersom ett dokument vars senaste trailer utelämnar /Encrypt ändå kan vara krypterat längre bak; anropare som fogar in revisioner förlitar sig på den flaggan för att vägra skriva klartextsobjekt in i en krypterad fil
Typ-2-poster: varför objektströmmen väntar
En typ-2-post namnger en objektström, och PDFiumPas rör inte den strömmen förrän en anropare ber om en medlem i den. När den slutligen gör det verifieras /Type /ObjStm, /N kontrolleras mot objektbudgeten och /First mot taket för avkodade bytes, och /N sundhetskontrolleras mot /First eftersom varje huvudpar behöver minst fyra bytes. Först då expanderas strömmen, och huvudskanningen stannar vid den begärda medlemmen och dess efterföljare i stället för att bygga en fullständig medlemstabell. En avkodad objektström kvarhålls åt gången, vilket är rätt avvägning när en sidträdgren klungas in i en enda ObjStm; vår genomgång av objektströms- och prediktoravkodning i Delphi täcker vad som händer inuti det expanderingssteget (§7.5.7)
var
Reader: TPdfSparseDictionaryReader;
Generation: Integer;
Dict: AnsiString;
begin
{ ett kvarhållet index, många generationsmedvetna läsningar }
Reader := TPdfSparseDictionaryReader.Create(Source);
try
if Reader.Valid and
Reader.ReadLatestDictionary(PageObjectNumber, Generation, Dict) then
HandlePage(PageObjectNumber, Generation, Dict);
finally
Reader.Free; { källströmmen förblir din }
end;
end;
Där cachen slutar utfärda löften
Indexet är en ögonblicksbild, och det är värt att vara rättfram med det. Sektioner tolkas en gång i Initialize; om den underliggande strömmen modifieras därefter är varje cachad post inaktuell och klassen kommer inte att märka det. TPdfSparseDictionaryReader håller indexet under källans anropareägda livstid, vilket är exakt vad en rekursiv gång över ett sidträd vill ha och exakt vad du inte får göra tvärs över en omskrivning. Postcachen är en platt array som genomsöks linjärt och den lagrar även negativa resultat, så några hundra uppslagningar är billiga och några hundra tusen är det inte. ReadDictionary kräver en exakt generationsmatchning medan ReadLatestDictionary löser den aktiva, och skillnaden är avsiktlig: referensupplösning behöver det förra, kataloggranskning behöver det senare. Där dessa gränser inte kan hållas faller de omgivande enheterna tillbaka till den äldre helfilspartolkningsvägen i stället för att krympa mängden filer som fortfarande fungerar, ett mönster vi också använder för strömning av stora PDF-filer på begäran
Korskompilatorregressioner täcker samma beteende på alla tre verktygskedjorna, inklusive ett påstående att en källa på 2 MiB aldrig ser en enda läsning större än 1 MiB. Om du underhåller Delphi-, C++Builder- eller Lazarus-kod som rör PDF-struktur direkt och du tröttnat på att betala helfilsparskostnader för fyra ordlistor, skeppas det glesa indexet och den publika sömmen runt det i PDFiumPas Delphi PDFium-komponenten