PDF 1.5-objektstrømmer pakker mange små indirekte objekter inn i én Flate-komprimert beholder, og losLab PDF Library skriver dem ut ved en full lagring via PackObjectStreams-flagget. Gevinsten er reell: hundrevis av side-, font- og annotasjonsordbøker som hver koster titalls ukomprimerte byte, kollapser til en håndfull komprimerte blober. Kostnaden er at hvert pakket objekt nå trenger en kryssreferansestrøm for å beskrive det
Det er den andre halvparten som knekker de fleste skrivere. Å bygge en /ObjStm-beholder er aritmetikk; å lære opp kryssreferansemekanismen til å peke inn i den er et redesign. En skriver som produserer en fullstendig gyldig beholder og deretter beskriver medlemmene med vanlige type-1-offset, har produsert en fil Acrobat vil åpne akkurat lenge nok til å erklære den skadet. De to funksjonene er egentlig én funksjon, og denne artikkelen dekker skrivesiden av begge, slik de er definert i ISO 32000-1 §7.5.7 og §7.5.8
Hva en ObjStm-beholder faktisk inneholder
En objektstrøm er en strøm hvis dekodede byte utgjør to sammenslåtte regioner, og ISO 32000-1 §7.5.7 gir ordboken nøyaktig tre nøkler som betyr noe for konstruksjonen. /Type /ObjStm identifiserer den, /N gir antallet medlemmer, og /First gir bytelengden til hodedelen — med andre ord, offsetet der kroppen begynner. Hodet er mellomromsseparerte par av objektnummer og offset; kroppen er medlemmene serialisert etter hverandre, der hvert offset måles fra starten av kroppen fremfor fra starten av den dekodede nyttelasten. Å lese en fullt dekodet beholder gjør det åpenbart: nedenfor er /First 14 fordi de tre hodelinjene opptar fjorten byte, og objekt 7 sitter 55 byte inn i kroppen fordi objekt 4 serialiserte til 54 tegn pluss en skilletegn
// Decoded payload of: 12 0 obj << /Type /ObjStm /N 3 /First 14
// /Filter /FlateDecode /Length 118 >> stream
4 0
7 55
9 90
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
<< /Type /ExtGState /CA 1 /ca 1 >>
[ 0 0 595 842 ]
To medlemsregler er absolutte og kommer begge rett fra §7.5.7. Et strømobjekt kan aldri være et medlem, fordi en strøm bærer rå byte som ville måtte nøstes inne i en annen strøm. Og et medlem må være en komplett objektverdi, aldri en bar indirekte referanse — et komprimert objekt som bare er 5 0 R, skaper en indireksjon leseren ikke kan løse opp uten allerede å vite hvor den peker. losLab PDF Library filtrerer bort begge tilfellene under kandidatinnsamlingen, sammen med krypteringsordboken og objekt 0, og pakker deretter det som overlever i grupper på 200 per beholder. Den grensen er en avgjørelse om tilfeldig tilgang snarere enn en spesifikasjonsgrense: en leser som vil ha ett medlem, må inflatere hele beholderen, så altfor store beholdere gjør små oppslag dyre
Hvorfor må ObjStm-medlemmer bruke type-2-kryssreferanseoppføringer?
Fordi et pakket objekt ikke har noe filoffset å registrere. ISO 32000-1 §7.5.8 svarer på dette med tre oppføringstyper i en binær kryssreferansestrøm: type 0 for ledige objekter, type 1 for vanlige objekter i bruk lagret ved et byteoffset, og type 2 for komprimerte objekter, hvis to datafelter holder beholderens objektnummer og medlemsindeksen inni den. Det finnes ingen måte å uttrykke et pakket objekt i den klassiske klartekst-xref-tabellen, og det er nettopp derfor PDF 1.5 introduserte begge funksjonene sammen
Rekkefølgen som følger, felte nesten hver eneste første implementasjon, inkludert vår egen. Vanlige objekter får type-1-oppføringer. /ObjStm-beholderne selv får type-1-oppføringer, fordi en beholder er et helt normalt indirekte strømobjekt skrevet ved et ekte offset. Bare medlemmene får type-2-oppføringer. Og kryssreferansestrømmen er selv et indirekte objekt i filen, så den trenger sin egen type-1-oppføring som peker på offsetet der den nettopp ble skrevet — det samme offsetet startxref registrerer. En tidlig versjon av skriveren vår ekskluderte beholder-objektnumre fra skrivesløyfen i stedet for å ekskludere medlemmer, og resultatet var en fil med en kryssreferansestrøm og ingen objektstrømmer i det hele tatt: strukturelt sammenhengende, semantisk tom, avvist lenger nede i kjeden. /Size-verdien skjuler en tilsvarende off-by-one, siden den er det høyeste objektnummeret pluss én, og kryssreferansestrømmen er tildelt som det høyeste objektnummeret, så den må også telles med
Å dimensjonere /W-arrayen: hvorfor fire byte ikke er nok
/W-arrayen erklærer bytebredden til hvert av de tre feltene, og losLab PDF Library skriver den som /W [1 Field2 Field3] med felt 1 fast på én byte for typekoden og felt 3 fast på to byte, noe som dekker generasjonsnumre opp til 65535 og medlemsindekser likt. Felt 2 er det som ikke kan være en konstant, fordi det bærer to urelaterte størrelser: i en type-1-oppføring er det et byteoffset kun begrenset av filstørrelsen, mens det i en type-2-oppføring er et beholder-objektnummer, og i en type-0-oppføring er det neste ledige objekt i kjeden. Et fast firebyte felt 2 fungerer fint helt til filen krysser 4 GB, på hvilket punkt hvert offset forbi grensen stille kappes av og hele tabellen blir søppel. Skriveren skanner derfor den ferdigbygde tabellen for den største verdien noen felt-2-plass noensinne vil holde, inkludert offsetet til selve kryssreferansestrømmen, og utvider feltet opp til åtte byte
// Field 2 must hold the largest byte offset AND the largest
// ObjStm container number AND the largest free-chain target.
MaxField2Value := XRefStart;
for X := 0 to MaxObj do
begin
if XRefTable[X].InUse and (XRefTable[X].ObjStrNum > 0) then
Field2Value := XRefTable[X].ObjStrNum // type-2: container number
else
Field2Value := XRefTable[X].ObjPos; // type-1 offset / type-0 next-free
if Field2Value > MaxField2Value then
MaxField2Value := Field2Value;
end;
Field2 := 4;
while (Field2 < 8) and
(MaxField2Value > ((Int64(1) shl (Field2 * 8)) - 1)) do
Inc(Field2);
Field3 := 2; // generation numbers and member indices both fit
Når bredden er kjent, er nyttelastens størrelse også kjent nøyaktig, så skriveren forhåndsallokerer hele bufferen og fyller den etter indeks; å legge til oppføringer byte for byte til en AnsiString gjør tabellbyggingen kvadratisk, noe ingen legger merke til på en tisiders faktura og alle legger merke til på et dokument med to hundre tusen objekter. To ytterligere detaljer holder strenge lesere fornøyde. /Index erklærer hvilke objektnummer-intervaller tabellen dekker, og for en full omskriving er det ganske enkelt [0 N] uten hull. Og hver plass skriveren faktisk ikke skrev ut, må som standard settes til ledig fremfor i bruk: objekt 0 leder den ledige kjeden, hver ledig plass lenker til den neste, og en plass som en gang holdt et slettet objekt beholder generasjonsnummeret sitt økt med én. Følgenotatet om minnesikkerhet ved parsing av upålitelige PDF-er gjør det samme grenseargumentet fra lesesiden
Hvorfor må kryssreferansestrømmen aldri være kryptert?
Fordi en leser må parse den før den kan vite hvordan noe skal dekrypteres. Kryssreferansestrømmen er det som forteller leseren hvor /Encrypt-ordboken bor; hvis bytene dens selv var kryptert, ville leseren trenge filnøkkelen for å finne objektet som beskriver filnøkkelen. losLab PDF Library håndhever dette i ett eneste predikat: ShouldCryptStreamData returnerer False når strømordboken bærer /Type /XRef, slik at unntaket gjelder uansett hvilken vei som når serializeren
Selve /ObjStm-beholderen får den motsatte behandlingen, og asymmetrien er bevisst. En beholder krypteres i sin helhet, nøklet på sitt eget objektnummer, akkurat som enhver annen strøm. Medlemmene dens krypteres ikke individuelt — de pakkes i sin dekrypterte klartekstform, og den ene gjennomgangen over den ferdigbygde beholderen dekker dem, strenger inkludert. Å dobbeltkryptere medlemmene produserer en fil som dekrypterer til chiffertekst, og fordi det ytre laget lykkes, dukker feilen opp som en parsefeil dypt inne i objektgrafen fremfor som en autentiseringsfeil. Ett objekt holdes da helt utenfor ordningen: i et kryptert dokument beholdes katalogen som et direkte type-1-objekt og pakkes aldri, fordi å pakke den ville tvinge lasteren til å inflatere og dekryptere en objektstrøm for å nå dokumentroten, før dekrypteringskonteksten som roten bidrar til å etablere er ferdig bygget
Å slå på pakking fra Delphi
Den offentlige bryteren er PackObjectStreams, eksponert som et felt på TPDFlibSaveOptions, som den frittstående setteren SetPackObjectStreams, og som en egenskap på dokumentobjektet. Den er som standard slått på og sperres automatisk etter versjon: skriveren pakker bare når dokumentet allerede er PDF 1.5 eller nyere, og den kaller den interne minimumsversjons-vakten slik at et pakket dokument oppgraderes til 1.5 fremfor å bli feilmerket. Etter lagringen rapporterer GetLastSaveUsedObjectStreams om sperren faktisk åpnet seg, og det er assertionen du vil ha i en regresjonstest fremfor en sammenligning av bytestørrelse
var
Doc: TPDFlib;
Options: TPDFlibSaveOptions;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('report.pdf', '') <= 0 then
Exit;
Doc.SetInformation(0, '1.5'); // packing is gated on PDF 1.5+
FillChar(Options, SizeOf(Options), 0);
Options.CompressContent := True;
Options.GarbageCollect := True; // drop orphans before packing
Options.PackObjectStreams := True;
if Doc.SaveToFileOptions('report-packed.pdf', Options) = 1 then
if Doc.GetLastSaveUsedObjectStreams = 1 then
Writeln('Saved with ObjStm containers and an xref stream');
finally
Doc.Free;
end;
end;
Rekkefølgen mellom pakking og søppelinnsamling betyr noe. Nåbarhetsanalysen må kjøre først, fordi et medlem som overlever inn i en beholder drar beholderen med seg — hvis et levende objekt pakkes, er beholderens nummer nåbart per definisjon, og å feie bort beholderen strander medlemmet uten noen måte å finne det på. Å kjøre samleren først betyr også at døde objekter aldri kommer inn i en beholder i det hele tatt, og det er derfra den forsterkende størrelsesgevinsten kommer. Pakking utfyller de andre størrelsesspakene fremfor å erstatte dem; gjennomgangen av PDF-filstørrelsesoptimalisering og font-subsetting dekker spakene som virker på strømmenes nyttelast, mens objektstrømmer virker på struktur
Grenser verdt å kjenne til før du slår det på
Inkrementelle lagringer pakker aldri. En inkrementell oppdatering legger til nye objekter og en ny kryssreferanseseksjon mens tidligere revisjoner holdes fysisk intakte, så å ompakke eksisterende objekter inn i nye beholdere ville gjøre foreldreløse av de type-1-oppføringene den forrige revisjonen fortsatt refererer til; losLab PDF Library slår av pakking når append-modus er aktiv, og artikkelen om inkrementelle oppdateringer og streaming i append-modus dekker den veien i sin helhet. Dokumenter under PDF 1.5 beholder klartekst-kryssreferansetabellen ubetinget: en 1.4-konsument aner ikke hva /ObjStm betyr, og å stille og rolig oppgradere et dokument fordi skriveren foretrakk en mindre fil, ville være feil avveining å gjøre på vegne av den som kaller. Én valgfri nøkkel vi bevisst ikke skriver ut, er /Extends, som ISO 32000-1 §7.5.7 definerer slik at en beholder kan navngi en forgjenger og lesere kan behandle en kjede av beholdere som en logisk gruppe. Den er genuint valgfri, hver beholder vi skriver er selvstendig og uavhengig dekodbar, og å hoppe over den fjerner en hel klasse av sykel- og henvisende-referanse-feil fra skriveren — selv om lesere selvsagt fortsatt må respektere /Extends når de møter den i filer fra andre produsenter
Objektstrøm-pakking og kryssreferansestrøm-utskrift leveres som en del av losLab PDF Library for Delphi og C++Builder, sammen med søppelsamleren og innholdsstrøm-optimalisereren de fungerer sammen med; produktsiden inneholder den fullstendige referansen for lagringsalternativer