A PDF 1.5 objektumfolyamok sok kis indirekt objektumot csomagolnak egyetlen Flate-tömörített tárolóba, és a losLab PDF Library ezeket teljes mentéskor bocsátja ki a PackObjectStreams jelzőjén keresztül. A nyereség valós: több száz oldal-, betűtípus- és annotációszótár, amelyek mindegyike tucatnyi tömörítetlen bájtba kerül, néhány tömörített blobbá zsugorodik. Az ára az, hogy minden csomagolt objektumhoz most már kereszthivatkozási folyam kell, hogy leírja azt
Ez a második fele az, ahol az írók elbuknak. Egy /ObjStm tároló felépítése aritmetika; a kereszthivatkozási gépezet megtanítása arra, hogy belé mutasson, újratervezés. Egy író, amely tökéletesen érvényes tárolót állít elő, majd a tagjait hétköznapi type-1 eltolásokkal írja le, olyan fájlt hoz létre, amelyet az Acrobat épp csak addig nyit meg, hogy sérültnek nyilvánítsa. A két funkció egy funkció, és ez a cikk mindkettő írási oldalát tárgyalja, ahogy az ISO 32000-1 §7.5.7 és §7.5.8 meghatározza
Mit tartalmaz valójában egy ObjStm tároló
Egy objektumfolyam olyan stream, amelynek dekódolt bájtjai két összefűzött régió, és az ISO 32000-1 §7.5.7 pontosan három olyan kulcsot ad a szótárnak, amely a felépítés szempontjából számít. A /Type /ObjStm azonosítja, a /N megadja a tagok számát, a /First pedig a fejléc régió bájthosszát — ezzel egyenértékűen azt az eltolást, ahol a törzs kezdődik. A fejléc szóközzel elválasztott objektumszám-eltolás párokból áll; a törzs a tagok egymás után szerializálva, ahol minden eltolás a törzs elejétől számít, nem a dekódolt payload elejétől. Egy teljesen dekódolt tároló olvasása egyértelművé teszi ezt: alább a /First 14, mert a három fejlécsor tizennégy bájtot foglal el, és a 7-es objektum 55 bájttal beljebb ül a törzsben, mert a 4-es objektum 54 karakterré szerializálódott plusz egy elválasztó
// 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 ]
Két tagsági szabály abszolút, és mindkettő egyenesen a §7.5.7-ből származik. Egy stream-objektum sosem lehet tag, mert egy stream nyers bájtokat hordoz, amelyeket egy másik streambe kellene ágyazni. És egy tagnak teljes objektumértéknek kell lennie, sosem puszta indirekt hivatkozásnak — egy tömörített objektum, amely csupán 5 0 R, olyan indirekciót hoz létre, amelyet az olvasó nem tud feloldani anélkül, hogy már tudná, hova mutat. A losLab PDF Library mindkét esetet kiszűri a jelölt gyűjtés során, az titkosítási szótárral és a 0-s objektummal együtt, majd 200-as csoportokba csomagolja, ami túlélte, tárolónként. Ez a felső korlát véletlen-hozzáférési döntés, nem specifikációs limit: egy olvasó, amely egyetlen tagot akar, kénytelen az egész tárolót kicsomagolni, így a túlméretezett tárolók drágává teszik a kis kereséseket
Miért kell az ObjStm tagoknak type-2 kereszthivatkozási bejegyzéseket használniuk?
Mert egy csomagolt objektumnak nincs fájleltolása, amit rögzíthetne. Az ISO 32000-1 §7.5.8 erre három bejegyzéstípussal válaszol egy bináris kereszthivatkozási folyamban: type 0 a szabad objektumokra, type 1 a bájteltolásnál tárolt hétköznapi használatban lévő objektumokra, és type 2 a tömörített objektumokra, amelyek két adatmezője a tároló objektumszámát és a tagon belüli indexet tárolja. Nincs mód egy csomagolt objektum kifejezésére a klasszikus, egyszerű szöveges xref táblában, ami pontosan az oka annak, hogy a PDF 1.5 mindkét funkciót együtt vezette be
Az ezt követő sorrend majdnem minden első implementációt megbotlik benne, a miénket is beleértve. A hétköznapi objektumok type-1 bejegyzéseket kapnak. Maguk az /ObjStm tárolók type-1 bejegyzéseket kapnak, mert egy tároló tökéletesen normál indirekt stream-objektum, valódi eltolásnál írva. Csak a tagok kapnak type-2 bejegyzéseket. És a kereszthivatkozási folyam maga is indirekt objektum a fájlban, így saját type-1 bejegyzésre van szüksége, amely arra az eltolásra mutat, ahol éppen az imént íródott — ugyanarra az eltolásra, amit a startxref rögzít. Az íránk egy korai verziója kizárta a tároló objektumszámait az írási ciklusból a tagok kizárása helyett, és az eredmény egy olyan fájl volt, amelynek kereszthivatkozási folyama volt, de egyáltalán nem volt objektumfolyama: strukturálisan koherens, szemantikailag üres, downstream elutasítva. A /Size érték megfelelő off-by-one hibát rejt, mivel ez a legmagasabb objektumszám plusz egy, és a kereszthivatkozási folyam a legmagasabb objektumszámként van kiosztva, így azt is számolni kell
A /W tömb méretezése: miért nem elég négy bájt
A /W tömb deklarálja a három mező mindegyikének bájtszélességét, és a losLab PDF Library /W [1 Field2 Field3] formában írja, ahol az 1. mező rögzítetten egy bájt a típuskódhoz, a 3. mező pedig rögzítetten két bájt, ami lefedi a 65535-ig terjedő generációs számokat és a tagindexeket egyaránt. A 2. mező az, amely nem lehet állandó, mert két, egymással nem összefüggő mennyiséget hordoz: egy type-1 bejegyzésben bájteltolás, amelyet csak a fájlméret korlátoz, míg egy type-2 bejegyzésben tároló objektumszám, egy type-0 bejegyzésben pedig a lánc következő szabad objektuma. Egy rögzített négybájtos 2. mező addig működik jól, amíg a fájl át nem lépi a 4 GB-ot, ekkor minden, a határon túli eltolás csendben csonkul, és az egész tábla szemétté válik. Az író ezért végigpásztázza az összeállított táblát a legnagyobb érték után kutatva, amit bármely 2. mező-hely valaha tartalmazni fog, beleértve magának a kereszthivatkozási folyamnak az eltolását is, és a mezőt akár nyolc bájtra is kiszélesíti
// 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
Amint a szélességek ismertek, a payload mérete pontosan ismert, így az író előre lefoglalja a teljes puffert, és indexenként tölti fel; a bejegyzések bájtonkénti hozzáfűzése egy AnsiString-hez kvadratikussá teszi a tábla felépítését, amit senki nem vesz észre egy tízoldalas számlán, de mindenki észrevesz egy kétszázezer objektumos dokumentumon. Két további részlet tartja elégedetten a szigorú olvasókat. Az /Index deklarálja, hogy mely objektumszám-tartományokat fedi le a tábla, és egy teljes újraírásnál ez egyszerűen [0 N], rés nélkül. És minden hely, amelyet az író ténylegesen nem bocsátott ki, alapértelmezetten szabadnak kell lennie, nem használtnak: a 0-s objektum a szabad lánc élén áll, minden szabad hely a következőre láncol, és egy egykor törölt objektumot tartó hely megtartja generációját, eggyel megnövelve. A nem megbízható PDF-ek elemzésekor alkalmazott memóriabiztonságról szóló kísérőjegyzet ugyanezt a határérvelést teszi meg az olvasás oldaláról
Miért nem szabad soha titkosítani a kereszthivatkozási folyamot?
Mert az olvasónak elemeznie kell, mielőtt tudná, hogyan dekódoljon bármit. A kereszthivatkozási folyam az, ami megmondja az olvasónak, hol él az /Encrypt szótár; ha a bájtjai maguk is titkosítva lennének, az olvasónak kellene a fájlkulcs ahhoz, hogy megtalálja azt az objektumot, amely a fájlkulcsot leírja. A losLab PDF Library ezt egyetlen predikátumban kényszeríti ki: a ShouldCryptStreamData False-t ad vissza, valahányszor a stream-szótár /Type /XRef-et hordoz, így a mentesség fennáll, függetlenül attól, melyik útvonal éri el a szerializálót
Az /ObjStm tároló az ellenkező kezelést kapja, és az aszimmetria szándékos. Egy tároló teljes egészében titkosítva van, saját objektumszámával kulcsolva, pontosan úgy, mint bármely más stream. A tagjai nem titkosítottak egyénileg — dekódolt sima szöveges formájukban vannak csomagolva, és az összeállított tárolón végzett egyetlen áthaladás fedi le őket, beleértve a szövegeket is. A tagok kétszeres titkosítása olyan fájlt hoz létre, amely titkosított szöveggé dekódolódik, és mivel a külső réteg sikeres, a hiba mélyen az objektumgráfban jelentkezik elemzési hibaként, nem hitelesítési hibaként. Egy objektum ekkor teljesen kívül marad a sémán: egy titkosított dokumentumban a Catalog direkt type-1 objektumként marad, és sosem csomagolják, mert a csomagolása kikényszerítené, hogy a betöltő kicsomagoljon és dekódoljon egy objektumfolyamot, hogy elérje a dokumentumgyökeret, mielőtt a dekódolási kontextus, amelyet a gyökér segít felállítani, teljesen felépülne
A csomagolás bekapcsolása Delphiből
A nyilvános kapcsoló a PackObjectStreams, amely mezőként érhető el a TPDFlibSaveOptions-on, önálló beállítóként a SetPackObjectStreams, és tulajdonságként a dokumentumobjektumon. Alapértelmezésben engedélyezett, és verzió szerint automatikusan kapuzott: az író csak akkor csomagol, ha a dokumentum már PDF 1.5 vagy újabb, és meghívja a belső minimumverzió-őrt, hogy egy csomagolt dokumentum 1.5-re legyen emelve, ne pedig félrecímkézve. A mentés után a GetLastSaveUsedObjectStreams jelenti, hogy a kapu ténylegesen kinyílt-e, ami az az assert, amit egy regressziós tesztben szeretnél, nem egy bájtméret-összehasonlítás
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;
A csomagolás és a szemétgyűjtés közötti sorrend számít. Az elérhetőségi elemzésnek előbb kell futnia, mert egy tag, amely bekerül egy tárolóba, magával húzza a tárolót is — ha egy élő objektum csomagolva van, a tárolójának objektumszáma definíció szerint elérhető, és a tároló elsöprése hajótörötten hagyja a tagot, mód nélkül a megtalálására. A gyűjtő előbbre futtatása azt is jelenti, hogy a halott objektumok sosem kerülnek be egyáltalán egy tárolóba, ahonnan a halmozódó méretnyereség jön. A csomagolás kiegészíti a többi méretcsökkentő eszközt, nem helyettesíti azokat; a PDF fájlméret-optimalizálásról és betűtípus-részhalmazolásról szóló bemutató azokat az eszközöket tárgyalja, amelyek a stream tartalmakra hatnak, ahol az objektumfolyamok a struktúrára hatnak
Határok, amelyeket érdemes ismerni, mielőtt bekapcsolnád
Az inkrementális mentések sosem csomagolnak. Egy inkrementális frissítés új objektumokat és új kereszthivatkozási szakaszt fűz hozzá, miközben a korábbi revíziókat fizikailag érintetlenül hagyja, így a meglévő objektumok friss tárolókba történő újracsomagolása árvává tenné azokat a type-1 bejegyzéseket, amelyekre az előző revízió továbbra is hivatkozik; a losLab PDF Library letiltja a csomagolást, valahányszor az append mód aktív, és a inkrementális frissítésekről és append-módú streamingről szóló cikk teljes körűen tárgyalja ezt az útvonalat. A PDF 1.5 alatti dokumentumok feltétel nélkül megtartják a sima szöveges kereszthivatkozási táblát: egy 1.4-es fogyasztó fogalma sincs, mit jelent az /ObjStm, és egy dokumentum csendes felfejlesztése, mert az író kisebb fájlt preferált, rossz csere lenne a hívó nevében meghozni. Egy opcionális kulcsot, amelyet szándékosan nem bocsátunk ki, az /Extends, amelyet az ISO 32000-1 §7.5.7 azért definiál, hogy egy tároló megnevezhessen egy elődöt, és az olvasók tárolók láncát logikai csoportként kezelhessék. Ez valóban opcionális, minden általunk írt tároló önmagában áll és önállóan dekódolható, és a kihagyása eltávolítja az író hibáinak egy egész osztályát, ciklusokat és lógó hivatkozásokat — bár az olvasóknak természetesen továbbra is tiszteletben kell tartaniuk az /Extends-t, amikor más előállítóktól származó fájlokban találkoznak vele
Az objektumfolyam-csomagolás és a kereszthivatkozási folyam kimenet a losLab PDF Library részeként érkezik Delphihez és C++Builderhez, a szemétgyűjtő és a tartalomfolyam-optimalizáló mellett, amelyekkel összeáll; a termékoldal tartalmazza a teljes mentési beállítások referenciát