PDF 1.5 -objektivirrat pakkaavat monta pientä epäsuoraa objektia yhteen Flate-pakattuun säiliöön, ja losLab PDF Library tuottaa niitä täydellä tallennuksella PackObjectStreams-lipun kautta. Hyöty on todellinen: sadat sivu-, fontti- ja huomautussanakirjat, joista jokainen maksaa kymmeniä pakkaamattomia tavuja, romahtavat kourallisiksi pakattuja blobeja. Hinta on se, että jokainen pakattu objekti tarvitsee nyt ristiviittausvirran kuvaamaan sitä
Tuo jälkimmäinen puolisko on se, missä kirjoittajat rikkoutuvat. /ObjStm-säiliön rakentaminen on aritmetiikkaa; ristiviittauskoneiston opettaminen osoittamaan siihen on uudelleensuunnittelu. Kirjoittaja, joka tuottaa täysin pätevän säiliön ja sitten kuvaa sen jäsenet tavallisilla tyypin 1 siirtymillä, on tuottanut tiedoston, jonka Acrobat avaa juuri sen ajan, että ehtii julistaa sen vahingoittuneeksi. Nämä kaksi ominaisuutta ovat yksi ominaisuus, ja tämä artikkeli kattaa molempien kirjoituspuolen, kuten ISO 32000-1 §7.5.7 ja §7.5.8 määrittelevät
Mitä ObjStm-säiliö oikeasti sisältää
Objektivirta on virta, jonka dekoodatut tavut ovat kaksi peräkkäistä aluetta, ja ISO 32000-1 §7.5.7 antaa sanakirjalle täsmälleen kolme rakentamisen kannalta oleellista avainta. /Type /ObjStm tunnistaa sen, /N antaa jäsenten lukumäärän, ja /First antaa otsikkoalueen tavupituuden — vastaavasti siirtymän, josta runko alkaa. Otsikko on välilyönnein erotettuja objektinumero-siirtymä-pareja; runko on jäsenet peräkkäin sarjallistettuina, kunkin siirtymän mitattuna rungon alusta eikä dekoodatun hyötykuorman alusta. Täysin dekoodatun säiliön lukeminen tekee tästä ilmeisen: alla /First on 14, koska kolme otsikkoriviä vievät neljätoista tavua, ja objekti 7 istuu 55 tavun päässä rungossa, koska objekti 4 sarjallistui 54 merkiksi plus erotin
// 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 ]
Kaksi jäsenyyssääntöä ovat ehdottomia ja tulevat molemmat suoraan kohdasta §7.5.7. Virtaobjekti ei voi koskaan olla jäsen, koska virta kantaa raakoja tavuja, jotka pitäisi upottaa toisen virran sisään. Ja jäsenen täytyy olla täydellinen objektiarvo, ei koskaan paljas epäsuora viittaus — pakattu objekti, joka on pelkkä 5 0 R, luo epäsuoruuden, jota lukija ei pysty ratkaisemaan tietämättä jo etukäteen, mihin se osoittaa. losLab PDF Library suodattaa molemmat tapaukset pois ehdokkaiden keräyksen aikana, samoin kuin salaussanakirjan ja objektin 0, ja pakkaa sitten kaiken säilyneen ryhmiin 200 kappaletta säiliötä kohti. Tuo yläraja on satunnaispääsyyn liittyvä päätös eikä spesifikaation rajoitus: lukija, joka haluaa yhden jäsenen, joutuu purkamaan koko säiliön, joten liian suuret säiliöt tekevät pienistä hauista kalliita
Miksi ObjStm-jäsenten täytyy käyttää tyypin 2 ristiviittausmerkintöjä?
Koska pakatulla objektilla ei ole tallennettavaa tiedostosiirtymää. ISO 32000-1 §7.5.8 vastaa tähän kolmella merkintätyypillä binäärisessä ristiviittausvirrassa: tyyppi 0 vapaille objekteille, tyyppi 1 tavallisille käytössä oleville objekteille, jotka on tallennettu tavusiirtymään, ja tyyppi 2 pakatuille objekteille, joiden kaksi datakenttää sisältävät säiliön objektinumeron ja jäsenindeksin sen sisällä. Pakattua objektia ei ole mitään tapaa ilmaista klassisessa selkotekstisessä xref-taulukossa, ja juuri siksi PDF 1.5 esitteli molemmat ominaisuudet yhdessä
Tästä seuraava järjestys kompastuttaa lähes jokaisen ensimmäisen toteutuksen, meidän mukaan lukien. Tavalliset objektit saavat tyypin 1 merkinnät. /ObjStm-säiliöt itse saavat tyypin 1 merkinnät, koska säiliö on täysin tavallinen epäsuora virtaobjekti, joka on kirjoitettu todelliseen siirtymään. Vain jäsenet saavat tyypin 2 merkinnät. Ja ristiviittausvirta on itse epäsuora objekti tiedostossa, joten se tarvitsee oman tyypin 1 merkintänsä, joka osoittaa siirtymään, johon se juuri kirjoitettiin — samaan siirtymään, jonka startxref tallentaa. Erään kirjoittajamme aikaisen version poissuljenta koski säiliön objektinumeroita kirjoitussilmukasta jäsenten poissulkemisen sijaan, ja tuloksena oli tiedosto, jossa oli ristiviittausvirta eikä yhtään objektivirtaa: rakenteellisesti johdonmukainen, semanttisesti tyhjä, hylätty jatkokäsittelyssä. /Size-arvo piilottaa vastaavan yhden virheen, koska se on suurin objektinumero plus yksi, ja ristiviittausvirralle on varattu suurin objektinumero, joten sekin täytyy laskea mukaan
W-taulukon mitoitus: miksi neljä tavua ei riitä
/W-taulukko ilmoittaa kunkin kolmen kentän tavuleveyden, ja losLab PDF Library kirjoittaa sen muodossa /W [1 Field2 Field3], jossa kenttä 1 on kiinteästi yksi tavu tyyppikoodille ja kenttä 3 kiinteästi kaksi tavua, mikä kattaa sekä sukupolvinumerot 65535 asti että jäsenindeksit. Kenttä 2 on se, joka ei voi olla vakio, koska se kantaa kahta toisiinsa liittymätöntä suuretta: tyypin 1 merkinnässä se on tavusiirtymä, jota rajoittaa vain tiedoston koko, kun taas tyypin 2 merkinnässä se on säiliön objektinumero ja tyypin 0 merkinnässä seuraava vapaa objekti ketjussa. Kiinteä nelitavuinen kenttä 2 toimii hyvin, kunnes tiedosto ylittää 4 GB:n, jolloin jokainen rajan yli menevä siirtymä katkeaa hiljaa ja koko taulukosta tulee roskaa. Kirjoittaja siis tarkastaa koostetun taulukon suurimman arvon, jonka mikä tahansa kenttä 2 -paikka koskaan pitää, mukaan lukien itse ristiviittausvirran siirtymä, ja levittää kenttää aina kahdeksaan tavuun asti
// 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
Kun leveydet tiedetään, hyötykuorman koko tiedetään täsmälleen, joten kirjoittaja varaa koko puskurin etukäteen ja täyttää sen indeksin mukaan; merkintöjen liittäminen tavu tavulta AnsiString-muuttujaan tekee taulukon rakentamisesta neliöllistä, mitä kukaan ei huomaa kymmensivuisessa laskussa ja kaikki huomaavat dokumentissa, jossa on kaksisataatuhatta objektia. Kaksi muuta yksityiskohtaa pitävät tiukat lukijat tyytyväisinä. /Index ilmoittaa, mitkä objektinumeroalueet taulukko kattaa, ja täydelle uudelleenkirjoitukselle se on yksinkertaisesti [0 N] ilman aukkoja. Ja jokaisen paikan, jota kirjoittaja ei todellisuudessa tuottanut, täytyy oletusarvoisesti olla vapaa eikä käytössä: objekti 0 johtaa vapaata ketjua, jokainen vapaa paikka linkittyy seuraavaan, ja paikka, joka kerran piti poistettua objektia, säilyttää sukupolvinumeronsa yhdellä kasvatettuna. Rinnakkainen huomautus artikkelissa muistiturvallisuudesta epäluotettuja PDF-tiedostoja jäsennettäessä tekee saman rajaperustelun lukupuolelta
Miksi ristiviittausvirta ei koskaan saa olla salattu?
Koska lukijan täytyy jäsentää se ennen kuin se voi tietää, miten mitään puretaan salauksesta. Ristiviittausvirta on se, joka kertoo lukijalle, missä /Encrypt-sanakirja sijaitsee; jos sen tavut olisivat itse salattuja, lukija tarvitsisi tiedostoavaimen löytääkseen objektin, joka kuvaa tiedostoavaimen. losLab PDF Library toteuttaa tämän yhdellä predikaatilla: ShouldCryptStreamData palauttaa arvon False aina, kun virran sanakirja kantaa avainta /Type /XRef, joten poikkeus pätee riippumatta siitä, mitä polkua kautta sarjallistaja saavutetaan
/ObjStm-säiliö saa päinvastaisen kohtelun, ja epäsymmetria on tarkoituksellinen. Säiliö salataan kokonaisuutena, avaimena sen oma objektinumero, aivan kuten mikä tahansa muu virta. Sen jäseniä ei salata yksitellen — ne pakataan puretussa selkotekstimuodossaan, ja yksi ajo koostetun säiliön yli kattaa ne, merkkijonot mukaan lukien. Jäsenten kaksinkertainen salaus tuottaa tiedoston, joka purkautuu salatekstiksi, ja koska ulompi kerros onnistuu, virhe ilmenee jäsennysvirheenä syvällä objektigraafissa eikä todennusvirheenä. Yksi objekti pysyy silloin kokonaan järjestelmän ulkopuolella: salatussa dokumentissa Catalog säilytetään suorana tyypin 1 objektina eikä sitä koskaan pakata, koska sen pakkaaminen pakottaisi lataajan purkamaan ja avaamaan salauksesta objektivirran vain päästäkseen dokumenttijuureen, ennen kuin salauskonteksti, jota juuri auttaa muodostamaan, on täysin rakennettu
Pakkauksen käyttöönotto Delphistä
Julkinen kytkin on PackObjectStreams, joka on esillä kenttänä TPDFlibSaveOptions-tietueessa, itsenäisenä asetusfunktiona SetPackObjectStreams, ja ominaisuutena dokumenttiobjektissa. Se on oletuksena käytössä ja porrastettu automaattisesti version mukaan: kirjoittaja pakkaa vain, kun dokumentti on jo PDF 1.5 tai uudempi, ja se kutsuu sisäistä vähimmäisversiovartijaa, jotta pakattu dokumentti nostetaan versioon 1.5 eikä merkitä väärin. Tallennuksen jälkeen GetLastSaveUsedObjectStreams raportoi, avautuiko portti todella, mikä on juuri se väite, jonka haluat regressiotestiin tavukokovertailun sijaan
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;
Järjestyksellä pakkauksen ja roskienkeruun välillä on väliä. Tavoitettavuusanalyysin täytyy ajaa ensin, koska jäsen, joka selviää säiliöön asti, vetää säiliön mukanaan — jos elävä objekti pakataan, sen säiliönumero on tavoitettavissa määritelmän mukaan, ja säiliön lakaiseminen pois jättäisi jäsenen ilman keinoa löytää sitä. Kerääjän ajaminen ensin tarkoittaa myös sitä, että kuolleet objektit eivät koskaan pääse säiliöön ollenkaan, mistä kumuloituva kokohyöty tulee. Pakkaus täydentää muita kokovipuja korvaamatta niitä; kävelyläpikäynti artikkelissa PDF-tiedoston koon optimoinnista ja fonttien osajoukotuksesta kattaa vivut, jotka vaikuttavat virtojen hyötykuormiin, kun taas objektivirrat vaikuttavat rakenteeseen
Rajat, jotka kannattaa tietää ennen käyttöönottoa
Asteittaiset tallennukset eivät koskaan pakkaa. Asteittainen päivitys liittää uusia objekteja ja uuden ristiviittausosion jättäen aiemmat versiot fyysisesti koskemattomiksi, joten olemassa olevien objektien uudelleenpakkaaminen tuoreisiin säiliöihin tekisi orvoiksi ne tyypin 1 merkinnät, joihin edellinen versio yhä viittaa; losLab PDF Library poistaa pakkauksen käytöstä aina, kun liitostila on aktiivinen, ja artikkeli asteittaisista päivityksistä ja liitostilan suoratoistosta kattaa tuon polun kokonaan. PDF 1.5:tä vanhemmat dokumentit säilyttävät selkotekstisen ristiviittaustaulukon ehdoitta: 1.4-kuluttajalla ei ole aavistustakaan, mitä /ObjStm tarkoittaa, ja dokumentin hiljainen ylentäminen, koska kirjoittaja piti pienemmästä tiedostosta, olisi väärä kompromissi tehtäväksi kutsujan puolesta. Yksi valinnainen avain, jota tarkoituksella emme tuota, on /Extends, jonka ISO 32000-1 §7.5.7 määrittelee niin, että säiliö voi nimetä edeltäjänsä ja lukijat voivat käsitellä säiliöketjua loogisena ryhmänä. Se on aidosti valinnainen, jokainen kirjoittamamme säiliö on itsenäinen ja riippumattomasti dekoodattavissa, ja sen ohittaminen poistaa kirjoittajasta kokonaisen luokan sykli- ja roikkuvan viittauksen bugeja — vaikka lukijoiden täytyy tietysti yhä kunnioittaa /Extends-avainta kohdatessaan sen muiden tuottajien tiedostoissa
Objektivirtojen pakkaus ja ristiviittausvirran tuotanto toimitetaan osana losLab PDF Library -kirjastoa Delphille ja C++Builderille, yhdessä roskienkerääjän ja sisältövirran optimoijan kanssa, joiden kanssa se toimii yhteen; tuotesivu sisältää täydellisen tallennusasetusten viitteen