Du vil ha én ordbok ut av en 2 GB PDF, og verktøyet utvider først hele kryssreferansetabellen til et array dimensjonert av trailerens /Size. PDFiumPas erstatter det trinnet med en sparse lazy objektindeks: den beholder bare xref-seksjonsbeskrivelsene, slår opp ett enkelt objektnummer på forespørsel gjennom avgrensede vinduer og legger bare oppføringene du faktisk berører i hurtigbuffer
Den gamle formen på denne koden i FPdfCompress var ærlig, men dyr. ApplyDefaultOpenAction leste hele filen inn i én TBytes, og tildelte deretter et tett TPdfActiveXrefEntries-array med én plass per objektnummer opp til /Size. To ting gikk galt i stor skala. Lesekostnaden vokste lineært med dokumentstørrelsen selv når kalleren bare ville ha fire ordbøker, og det tette arrayet kolliderte med parserbudsjettet: TPdfParserResourceBudget.Default setter MaxObjects til 4 000 000, så en helt gyldig fil hvis høyeste objektnummer ligger over taket ble avvist av et minneargument i stedet for et korrekthetsargument
Hvorfor svarer ikke PDFiums offentlige API på dette spørsmålet?
Fordi informasjonen finnes inne i PDFium, men aldri krysser C-grensen. CPDF_Parser vedlikeholder kryssreferansetabellen, objektstrømmedlemskapet og revisjonsforrangen internt, men de publiserte headerne eksponerer ikke noe inngangspunkt som tar et objektnummer og returnerer dets rå offset, generasjon, hvilken revisjon som vant, eller hvilken ObjStm det bor i. Lagringssiden er like lukket: FPDF_SaveAsCopy og FPDF_SaveWithVersion gir deg bare en sekvensiell skrive-callback. Enhver patch på byte-nivå mot en katalog etter en nativ lagring må derfor bygges i Pascal-laget, og det er derfor PDFiumPas parser disse strukturene selv i stedet for å gjenbruke DLL-en
Hva beholder den sparsomme indeksen egentlig i minnet?
Beskrivelser, ikke oppføringer. For en klassisk tabell (ISO 32000-1 §7.5.4) lagrer en TPdfSparseXrefSubsection det første objektnummeret, objektantallet, byte-offseten der oppføringsradene begynner og den målte oppføringsbredden. Oppføringene selv blir i filen. Bredden måles fra den første raden i stedet for å antas å være 20 byte, fordi produsentene er uenige om linjeendinger; PDFiumPas godtar 18 til 64 og avviser alt utenfor det båndet, sammen med enhver delseksjon hvis deklarerte antall ville løpe forbi slutten av strømmen. For en kryssreferansestrøm (§7.5.8) inneholder seksjonen de tre /W-feltbreddene, hver begrenset til 0 til 8, parene fra /Index som én flat liste, og de dekodede oppføringsbytene, hvis forventede lengde beregnes fra /W og /Index før én eneste byte dekomprimeres
Hele indeksen bygges av Initialize fra et halevindu på maksimalt 1 MiB, som er der startxref finnes, og hver påfølgende objektlesing bruker et objektvindu på 1 MiB. Taket for rå strøm er 64 MiB, og en enkelt xref-linje kan ikke overstige 1024 byte. Hvis du har lest notatet vårt om validering av objekt- og kryssreferansestrømmer med PDFiumPas, gjelder den samme feltbreddedisiplinen her, bare at den nå brukes til å adressere én oppføring i stedet for å gjennomgå en hel tabell
uses
FPdfCompress;
var
Source: TFileStream;
Revision: TPdfSparseRevisionInfo;
begin
Source := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
{ går bare gjennom startxref, /Prev-kjeden og 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;
Hvordan når ett oppslag fram til ett objekt?
Ved aritmetikk, i begge layoutene. En klassisk delseksjon har rader med fast bredde, så adressen til en oppføring er delseksjonens start pluss objektens offset ganger den målte bredden; PDFiumPas leser deretter den ene linjen, parser offseten med ti siffer og generasjonen med fem siffer, sjekker generasjonen mot taket på 65535 fra §7.5.4 og klassifiserer det avsluttende nøkkelordet som axkDirect eller axkFree. En kryssreferansestrøm trenger ett trinn til, fordi /Index-delseksjonene ligger sammenkjedet i den dekodede byterekken, så indeksen akkumulerer antallene til foregående delseksjoner før den multipliserer med den summerte /W-bredden. Type 1 gir en offset, type 2 gir et objektstrømnummer og en medlemsindeks, og alt annet blir axkUnknown i stedet for en gjetning
{ klassisk tabell, ISO 32000-1 seksjon 7.5.4 }
EntryOffset := Subsection.EntryOffset +
Int64(ObjectNumber - Subsection.FirstObject) * Subsection.EntryWidth;
{ kryssreferansestrøm, ISO 32000-1 seksjon 7.5.8 }
EntryWidth := Section.Widths[0] + Section.Widths[1] + Section.Widths[2];
EntryPosition := Integer((PriorCount + ObjectNumber -
Section.IndexValues[I]) * EntryWidth);
Ingenting i noen av stiene er proporsjonalt med /Size. Det er hele poenget med omskrivingen: trailerens størrelsesverdi føres videre som metadata og brukes ved skriving av den inkrementelle revisjonen, men den driver aldri en allokering. Regresjonspakken fester dette med en testfil hvis sidetre bor på objekt 1 000 000 000 og 1 000 000 001 under en trailer som deklarerer /Size 1000000002. Den gamle tette implementeringen nektet den filen; den sparsomme indeksen slår opp begge referansene og bevarer den deklarerte størrelsen i utdatatraileren
Hybridrevisjoner, /Prev-kjeder og vaktene rundt dem
Revisjonsforrang er der en naiv lazy indeks går galt. PDFiumPas går kjeden fra startxref i nyesteførst-rekkefølge og stopper et oppslag ved den første seksjonen som svarer, noe som gjenskaper forrangsregelen uten å materialisere en sammenslått tabell. Hybridreferansefiler (§7.5.8.4) håndteres inne i den klassiske grenen: når traileren bærer en /XRefStm, registreres den supplerende strømseksjonen før den klassiske seksjonen som refererte til den, så komprimerte objekter som er usynlige for den vanlige tabellen blir fortsatt funnet, mens de klassiske oppføringene beholder sin gyldighet. Eldre revisjoner følges deretter gjennom /Prev
To vakter begrenser den vandringen, og begge betyr noe på skadde filer. Hver besøkt offset registreres, så en /Prev som peker tilbake inn i kjeden terminerer i stedet for å snurre rundt, og traverseringsdybden takles av MaxRecursionDepth, som har standardverdien 1024. Krypteringsflagget akkumuleres over hele kjeden i stedet for å leses fra den nyeste traileren alene, fordi et dokument hvis nyeste trailer utelater /Encrypt fortsatt kan være kryptert lenger bak; kallere som legger til revisjoner støtter seg på det flagget for å nekte å skrive klartekstobjekter inn i en kryptert fil
Type-2-oppføringer: hvorfor objektstrømmen venter
En type-2-oppføring navngir en objektstrøm, og PDFiumPas rører ikke den strømmen før en kaller ber om et medlem av den. Når det endelig skjer, verifiseres /Type /ObjStm, /N sjekkes mot objektbudsjettet og /First mot taket for dekodede byte, og /N fornuftssjekkes mot /First siden hvert par i hodet trenger minst fire byte. Først da dekomprimeres strømmen, og hodeskanningen stopper ved det etterspurte medlemmet og dets etterfølger i stedet for å bygge en fullstendig medlemstabell. Én dekodet objektstrøm beholdes om gangen, noe som er den riktige avveiningen når en gren av sidetreet samles i én enkelt ObjStm; oppsummeringen vår om dekoding av objektstrømmer og predictorer i Delphi dekker hva som skjer inne i dekomprimeringstrinnet (§7.5.7)
var
Reader: TPdfSparseDictionaryReader;
Generation: Integer;
Dict: AnsiString;
begin
{ én beholdt indeks, mange generasjonsbevisste lesinger }
Reader := TPdfSparseDictionaryReader.Create(Source);
try
if Reader.Valid and
Reader.ReadLatestDictionary(PageObjectNumber, Generation, Dict) then
HandlePage(PageObjectNumber, Generation, Dict);
finally
Reader.Free; { Source forblir din }
end;
end;
Der hurtigbufferen slutter å love noe
Indeksen er et øyeblikksbilde, og det er verdt å være brutalt ærlig om det. Seksjonene parses én gang i Initialize; hvis den underliggende strømmen endres etterpå, er hver bufret oppføring foreldet, og klassen legger ikke merke til det. TPdfSparseDictionaryReader holder indeksen så lenge kalleren eier kilden, som er akkurat det en rekursiv vandring over et sidetre vil ha, og akkurat det du ikke må gjøre på tvers av en omskriving. Oppføringshurtigbufferen er et flatt array som søkes lineært, og den lagrer også negative resultater, så noen hundre oppslag er billige og noen hundre tusen er ikke det. ReadDictionary krever eksakt generasjonssamsvar, mens ReadLatestDictionary slår opp den aktive, og forskjellen er bevisst: referanseoppløsning trenger den første, kataloginspeksjon trenger den siste. Der disse grensene ikke kan overholdes, faller de omkringliggende enhetene tilbake til den gamle helefilsparseren i stedet for å innsnevre mengden filer som fortsatt fungerer, et mønster vi også bruker for strømming av store PDF-er på forespørsel
Krysskompilator-regresjoner dekker samme oppførsel på alle tre verktøykjedene, inkludert en påstand om at en kilde på 2 MiB aldri ser en enkelt lesing større enn 1 MiB. Hvis du vedlikeholder Delphi-, C++Builder- eller Lazarus-kode som rører PDF-strukturen direkte, og du er lei av å betale helefilsparseskostnader for fire ordbøker, leveres den sparsomme indeksen og det offentlige grensesnittet rundt den i PDFiumPas Delphi PDFium-komponenten