PDF 1.5 objektų srautai supakuoja daug mažų netiesioginių objektų į vieną Flate suspaustą konteinerį, o losLab PDF Library juos išrašo pilno išsaugojimo metu per PackObjectStreams vėliavėlę. Nauda yra reali: šimtai puslapio, šrifto ir anotacijos žodynų, kurių kiekvienas nesuspaustas kainuoja dešimtis baitų, susilieja į saujelę suspaustų blobų. Kaina ta, kad kiekvienam supakuotam objektui dabar reikia kryžminės nuorodos srauto, kuris jį apibūdintų
Būtent ta antra pusė yra vieta, kur rašytojai lūžta. /ObjStm konteinerio sukūrimas yra aritmetika; kryžminės nuorodos mechanizmo išmokymas rodyti į jį yra perprojektavimas. Rašytojas, sukuriantis visiškai galiojantį konteinerį, o tada aprašantis jo narius įprastais 1 tipo poslinkiais, sukuria failą, kurį Acrobat atidarys tik tam, kad paskelbtų jį pažeistu. Šios dvi funkcijos yra viena funkcija, ir šis straipsnis apima abiejų rašymo pusę, kaip apibrėžta ISO 32000-1 §7.5.7 ir §7.5.8
Ką iš tikrųjų turi ObjStm konteineris
Objektų srautas yra srautas, kurio dekoduoti baitai yra dvi sujungtos sritys, o ISO 32000-1 §7.5.7 pateikia žodynui lygiai tris raktus, svarbius konstravimui. /Type /ObjStm jį identifikuoja, /N nurodo narių skaičių, o /First nurodo antraštės srities baitų ilgį — lygiaverčiai, poslinkį, kuriame prasideda pagrindinė dalis. Antraštę sudaro tarpais atskirtos objekto numerio ir poslinkio poros; pagrindinę dalį sudaro nariai, serializuoti vienas po kito, kiekvieno poslinkiui matuojant nuo pagrindinės dalies pradžios, o ne nuo dekoduoto turinio pradžios. Perskaičius pilnai dekoduotą konteinerį tai tampa akivaizdu: žemiau /First yra 14, nes trys antraštės eilutės užima keturiolika baitų, o objektas 7 yra 55 baitais gilyn į pagrindinę dalį, nes objektas 4 serializuotas į 54 simbolius plius skirtukas
// 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 ]
Dvi narystės taisyklės yra absoliučios, ir abi tiesiai kyla iš §7.5.7. Srauto objektas niekada negali būti nariu, nes srautas neša žalius baitus, kuriuos reikėtų įdėti į kitą srautą. O narys turi būti pilna objekto reikšmė, niekada tik netiesioginė nuoroda — suspaustas objektas, kuris tėra 5 0 R, sukuria netiesiogumą, kurio skaitytuvas negali išspręsti, dar nežinodamas, kur jis rodo. losLab PDF Library abu atvejus atmeta kandidatų rinkimo metu, kartu su šifravimo žodynu ir objektu 0, tada supakuoja tai, kas išliko, po 200 vienam konteineryje. Ta riba yra atsitiktinės prieigos sprendimas, ne specifikacijos apribojimas: skaitytuvui, norinčiam vieno nario, tenka išpūsti visą konteinerį, todėl per dideli konteineriai padaro mažas paieškas brangias
Kodėl ObjStm nariai turi naudoti 2 tipo kryžminės nuorodos įrašus?
Todėl, kad supakuotas objektas neturi failo poslinkio, kurį galėtų įrašyti. ISO 32000-1 §7.5.8 į tai atsako trimis įrašų tipais dvejetainiame kryžminės nuorodos sraute: 0 tipas laisviems objektams, 1 tipas įprastiems naudojamiems objektams, saugomiems baitų poslinkyje, ir 2 tipas suspaustiems objektams, kurių du duomenų laukai laiko konteinerio objekto numerį ir nario indeksą jame. Nėra būdo išreikšti supakuoto objekto klasikinėje paprasto teksto xref lentelėje, ir būtent todėl PDF 1.5 abi funkcijas pristatė kartu
Tvarka, kuri seka toliau, paklumpa beveik kiekvieną pirmąją realizaciją, įskaitant mūsų. Įprasti objektai gauna 1 tipo įrašus. Patys /ObjStm konteineriai gauna 1 tipo įrašus, nes konteineris yra visiškai normalus netiesioginis srauto objektas, parašytas realiame poslinkyje. Tik nariai gauna 2 tipo įrašus. O kryžminės nuorodos srautas pats yra netiesioginis objektas faile, todėl jam reikia savo paties 1 tipo įrašo, rodančio į poslinkį, kuriame jis ką tik buvo parašytas — tą patį poslinkį, kurį įrašo startxref. Ankstyva mūsų rašytuvo versija rašymo cikle neįtraukė konteinerio objekto numerių vietoje narių neįtraukimo, ir rezultatas buvo failas su kryžminės nuorodos srautu ir jokių objektų srautų: struktūriškai nuoseklus, semantiškai tuščias, atmestas žemiau eigos. /Size reikšmė slepia atitinkantį klaidą per vienetą, nes tai yra didžiausias objekto numeris plius vienas, o kryžminės nuorodos srautas paskirstomas kaip didžiausias objekto numeris, todėl jis irgi turi būti skaičiuojamas
W masyvo dydžio nustatymas: kodėl keturių baitų neužtenka
/W masyvas nurodo kiekvieno iš trijų laukų baitų plotį, o losLab PDF Library jį rašo kaip /W [1 Field2 Field3], kur laukas 1 fiksuotas vienam baitui tipo kodui, o laukas 3 fiksuotas dviem baitams, kas apima kartos numerius iki 65535 ir nario indeksus vienodai. Laukas 2 yra tas, kuris negali būti konstanta, nes jis neša du nesusijusius dydžius: 1 tipo įraše tai baitų poslinkis, ribojamas tik failo dydžio, o 2 tipo įraše tai konteinerio objekto numeris, o 0 tipo įraše tai kitas laisvas objektas grandinėje. Fiksuotas keturių baitų 2 laukas veikia gerai iki failui peržengiant 4 GB, kuriame kiekvienas poslinkis už ribos tyliai nukerpamas, o visa lentelė tampa šiukšlėmis. Todėl rašytuvas nuskaito surinktą lentelę, ieškodamas didžiausios reikšmės, kurią kada nors turės bet kuris 2 lauko langelis, įskaitant paties kryžminės nuorodos srauto poslinkį, ir praplečia lauką iki aštuonių baitų
// 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
Kai pločiai žinomi, žinomas ir tikslus duomenų dydis, todėl rašytuvas iš anksto paskirsto visą buferį ir jį užpildo pagal indeksą; įrašų pridėjimas baitas po baito prie AnsiString paverčia lentelės konstravimą kvadratiniu, ko niekas nepastebi dešimties puslapių sąskaitoje ir ką pastebi kiekvienas dokumente su dviem šimtais tūkstančių objektų. Dvi tolesnės smulkmenos palaiko griežtus skaitytuvus patenkintus. /Index nurodo, kuriuos objektų numerių intervalus apima lentelė, o pilnam perrašymui tai tiesiog [0 N] be tarpų. Ir kiekvienas langelis, kurio rašytuvas iš tikrųjų neišrašė, pagal numatymą turi būti laisvas, o ne naudojamas: objektas 0 vadovauja laisvųjų grandinei, kiekvienas laisvas langelis susieja su kitu, o langelis, kuris kadaise laikė ištrintą objektą, išlaiko savo kartos numerį, padidintą vienetu. Papildoma pastaba apie atminties saugumą nagrinėjant nepatikimus PDF failus pateikia tą patį ribų argumentą iš skaitymo pusės
Kodėl kryžminės nuorodos srautas niekada neturi būti šifruotas?
Todėl, kad skaitytuvas turi jį išnagrinėti prieš žinodamas, kaip ką nors iššifruoti. Kryžminės nuorodos srautas yra tai, kas pasako skaitytuvui, kur gyvena /Encrypt žodynas; jei jo baitai patys būtų šifruoti, skaitytuvui reikėtų failo rakto, kad rastų objektą, aprašantį failo raktą. losLab PDF Library tai užtikrina vienu predikatu: ShouldCryptStreamData grąžina False visada, kai srauto žodynas neša /Type /XRef, todėl išimtis galioja nepriklausomai nuo to, kuris kelias pasiekia serializatorių
/ObjStm konteineris gauna priešingą traktavimą, ir asimetrija yra tyčinė. Konteineris šifruojamas visas, raktuojamas pagal savo objekto numerį, lygiai kaip bet koks kitas srautas. Jo nariai nešifruojami individualiai — jie supakuoti savo iššifruotoje paprasto teksto formoje, o vienas perėjimas per surinktą konteinerį apima juos visus, įskaitant eilutes. Dvigubas narių šifravimas sukuria failą, kuris iššifruojamas į šifruotą tekstą, ir kadangi išorinis sluoksnis pavyksta, klaida iškyla kaip nagrinėjimo klaida giliai objektų grafe, o ne kaip autentifikavimo klaida. Vienas objektas tada visiškai lieka už schemos ribų: šifruotame dokumente Catalog laikomas kaip tiesioginis 1 tipo objektas ir niekada nepakuojamas, nes pakavimas priverstų įkroviklį išpūsti ir iššifruoti objektų srautą, kad pasiektų dokumento šaknį, dar prieš iššifravimo kontekstui, kurį šaknis padeda nustatyti, būnant pilnai sukurtam
Pakavimo įjungimas iš Delphi
Viešas jungiklis yra PackObjectStreams, atskleistas kaip laukas TPDFlibSaveOptions, kaip atskiras nustatytojas SetPackObjectStreams ir kaip dokumento objekto savybė. Pagal numatymą jis įjungtas ir automatiškai užrakintas pagal versiją: rašytuvas pakuoja tik tada, kai dokumentas jau yra PDF 1.5 ar naujesnis, ir kviečia vidinę minimalios versijos apsaugą, kad supakuotas dokumentas būtų pakeltas iki 1.5, o ne neteisingai pažymėtas. Po išsaugojimo GetLastSaveUsedObjectStreams praneša, ar vartai iš tikrųjų atsivėrė, ir tai yra assert, kurį norite regresinio testo metu, o ne baitų dydžio palyginimas
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;
Tvarka svarbi tarp pakavimo ir šiukšlių surinkimo. Pasiekiamumo analizė turi vykti pirmiausia, nes narys, kuris išlieka konteineryje, tempia konteinerį kartu su savimi — jei gyvas objektas supakuotas, jo konteinerio numeris yra pasiekiamas pagal apibrėžimą, o konteinerio nušlavimas paliktų narį be jokio būdo jį rasti. Pirmiausia paleidus surinkėją taip pat reiškia, kad negyvi objektai niekada nepatenka į konteinerį, iš kur ir ateina kaupiamasis dydžio laimėjimas. Pakavimas papildo kitus dydžio svertus, o ne juos pakeičia; apžvalga PDF failo dydžio optimizavimas ir šrifto poaibio kūrimas apima svertus, veikiančius srauto duomenis, o objektų srautai veikia struktūrą
Ribos, kurias verta žinoti prieš įjungiant
Laipsniški išsaugojimai niekada nepakuoja. Laipsniškas atnaujinimas prideda naujus objektus ir naują kryžminės nuorodos sekciją, paliekant ankstesnes versijas fiziškai nepaliestas, todėl esamų objektų pertvarkymas į naujus konteinerius paverstų našlaičiais 1 tipo įrašus, į kuriuos vis dar nurodo ankstesnė versija; losLab PDF Library išjungia pakavimą, kai tik veikia append režimas, o straipsnis apie laipsniškus atnaujinimus ir append režimo srautavimą tą kelią apima pilnai. Dokumentai žemiau PDF 1.5 besąlygiškai išlaiko paprasto teksto kryžminės nuorodos lentelę: 1.4 vartotojas neturi supratimo, ką reiškia /ObjStm, o tylus dokumento paaukštinimas, nes rašytuvas pageidavo mažesnio failo, būtų neteisingas mainas iškvietėjo naudai. Vienas neprivalomas raktas, kurio sąmoningai neišrašome, yra /Extends, kurį ISO 32000-1 §7.5.7 apibrėžia, kad konteineris galėtų įvardyti pirmtaką, o skaitytuvai galėtų traktuoti konteinerių grandinę kaip loginę grupę. Jis iš tiesų neprivalomas, kiekvienas mūsų rašomas konteineris yra savarankiškas ir nepriklausomai dekoduojamas, o praleidimas pašalina visą ciklų ir kabančių nuorodų klaidų klasę iš rašytuvo — nors skaitytuvai, žinoma, vis tiek turi paisyti /Extends, kai jį sutinka failuose iš kitų gamintojų
Objektų srautų pakavimas ir kryžminės nuorodos srauto išvestis pristatomi kaip losLab PDF Library dalis, skirta Delphi ir C++Builder, kartu su šiukšlių surinkėju ir turinio srauto optimizatoriumi, su kuriais jie derinasi; produkto puslapyje pateikiama pilna išsaugojimo parinkčių nuoroda