Tehnički članak

PDF tokovi objekata i cross-reference tokovi u Delphiju

PDF 1.5 tokovi objekata pakuju mnoštvo malih indirektnih objekata u jedan Flate-kompresovan kontejner, a losLab PDF Library ih emituje pri punom čuvanju kroz zastavicu PackObjectStreams. Dobitak je stvaran: stotine rečnika stranica, fontova i anotacija, od kojih svaki nekompresovan košta desetine bajtova, sažimaju se u šačicu kompresovanih blobova. Cena je što sada svaki upakovan objekat zahteva cross-reference tok da bi bio opisan

Ta druga polovina je mesto gde se writeri lome. Izgradnja /ObjStm kontejnera je aritmetika; naučiti cross-reference mehanizam da pokazuje u njega je redizajn. Writer koji proizvede savršeno validan kontejner, a zatim opiše njegove članove običnim type-1 ofsetima, proizveo je fajl koji će Acrobat otvoriti taman toliko dugo da proglasi da je oštećen. Ove dve mogućnosti su zapravo jedna, i ovaj članak pokriva stranu pisanja obe, kako je definisano u ISO 32000-1 §7.5.7 i §7.5.8

Šta zapravo sadrži ObjStm kontejner

Tok objekata je tok čiji su dekodirani bajtovi dva spojena regiona, a ISO 32000-1 §7.5.7 daje rečniku tačno tri ključa bitna za izgradnju. /Type /ObjStm ga identifikuje, /N daje broj članova, a /First daje dužinu u bajtovima regiona zaglavlja — ekvivalentno, ofset na kom počinje telo. Zaglavlje su parovi broja objekta i ofseta razdvojeni belinama; telo su članovi serijalizovani jedan za drugim, gde se svaki ofset meri od početka tela, a ne od početka dekodiranog sadržaja. Čitanje potpuno dekodiranog kontejnera to čini očiglednim: ispod, /First je 14 jer tri linije zaglavlja zauzimaju četrnaest bajtova, a objekat 7 se nalazi 55 bajtova unutar tela jer se objekat 4 serijalizovao u 54 karaktera plus separator

// 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 ]

Dva pravila članstva su apsolutna i oba dolaze direktno iz §7.5.7. Tok objekat nikada ne može biti član, jer tok nosi sirove bajtove koji bi morali biti ugnežđeni unutar drugog toka. A član mora biti kompletna vrednost objekta, nikada gola indirektna referenca — kompresovan objekat koji je samo 5 0 R stvara indirekciju koju čitač ne može razrešiti bez toga da već zna gde ona pokazuje. losLab PDF Library filtrira oba slučaja tokom prikupljanja kandidata, zajedno sa rečnikom enkripcije i objektom 0, a zatim pakuje šta god preživi u grupama od po 200 po kontejneru. Taj limit je odluka radi random-access pristupa, a ne granica specifikacije: čitač koji želi jednog člana mora da inflatuje ceo kontejner, pa preveliki kontejneri čine mala pretraživanja skupim

Zašto članovi ObjStm-a moraju koristiti cross-reference stavke tipa 2?

Zato što upakovan objekat nema ofset u fajlu koji bi se beležio. ISO 32000-1 §7.5.8 na ovo odgovara sa tri tipa stavki u binarnom cross-reference toku: tip 0 za slobodne objekte, tip 1 za obične objekte u upotrebi uskladištene na bajt-ofsetu, i tip 2 za kompresovane objekte, čija dva podatkovna polja nose broj objekta kontejnera i indeks člana unutar njega. Ne postoji način da se upakovan objekat izrazi u klasičnoj plaintext xref tabeli, što je upravo razlog zašto je PDF 1.5 uveo obe mogućnosti zajedno

Redosled koji sledi spotiče gotovo svaku prvu implementaciju, uključujući i našu. Obični objekti dobijaju type-1 stavke. Sami /ObjStm kontejneri dobijaju type-1 stavke, jer je kontejner sasvim običan indirektni tok objekat zapisan na stvarnom ofsetu. Samo članovi dobijaju type-2 stavke. A cross-reference tok je i sam indirektan objekat u fajlu, pa mu treba sopstvena type-1 stavka koja pokazuje na ofset gde je upravo zapisan — isti ofset koji beleži startxref. Rana verzija našeg writera isključivala je brojeve objekata kontejnera iz petlje pisanja umesto da isključi članove, a rezultat je bio fajl sa cross-reference tokom i bez ijednog toka objekata: strukturno koherentan, semantički prazan, odbačen dalje niz lanac. Vrednost /Size krije odgovarajuću off-by-one grešku, jer je to najviši broj objekta plus jedan, a cross-reference tok je alociran kao najviši broj objekta, pa i on mora biti prebrojan

Određivanje veličine niza /W: zašto četiri bajta nije dovoljno

Niz /W deklariše širinu u bajtovima svakog od tri polja, a losLab PDF Library ga piše kao /W [1 Field2 Field3], gde je polje 1 fiksirano na jedan bajt za kod tipa, a polje 3 fiksirano na dva bajta, što pokriva i brojeve generacije do 65535 i indekse članova. Polje 2 je ono koje ne može biti konstanta, jer nosi dve nepovezane veličine: u type-1 stavci to je bajt-ofset ograničen samo veličinom fajla, dok je u type-2 stavci to broj objekta kontejnera, a u type-0 stavci to je sledeći slobodan objekat u lancu. Fiksno polje 2 od četiri bajta radi bez problema dok fajl ne pređe 4 GB, u kom trenutku se svaki ofset preko te granice tiho odseca i cela tabela postaje smeće. Writer zato skenira sastavljenu tabelu tražeći najveću vrednost koju će ikada nositi bilo koji slot polja 2, uključujući ofset samog cross-reference toka, i proširuje polje do osam bajtova

// 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

Kada su širine poznate, veličina sadržaja je poznata tačno, pa writer unapred alocira ceo bafer i puni ga po indeksu; dodavanje stavki bajt po bajt u AnsiString čini izgradnju tabele kvadratnom, što niko ne primeti na fakturi od deset stranica, a svi primete na dokumentu sa dvesta hiljada objekata. Još dva detalja drže stroge čitače zadovoljnim. /Index deklariše koje opsege brojeva objekata tabela pokriva, a za puno ponovno pisanje to je jednostavno [0 N] bez praznina. I svaki slot koji writer zapravo nije emitovao mora podrazumevano biti slobodan, a ne u upotrebi: objekat 0 predvodi lanac slobodnih, svaki slobodan slot vodi ka sledećem, a slot koji je nekad sadržao obrisan objekat zadržava svoj broj generacije uvećan za jedan. Prateća beleška o bezbednosti memorije pri parsiranju nepoverljivih PDF-ova iznosi isti argument o granicama, ali sa strane čitanja

Zašto cross-reference tok nikada ne sme biti enkriptovan?

Zato što čitač mora da ga parsira pre nego što uopšte može znati kako da bilo šta dekriptuje. Cross-reference tok je taj koji govori čitaču gde živi rečnik /Encrypt; kada bi njegovi bajtovi sami bili enkriptovani, čitaču bi trebao ključ fajla da bi pronašao objekat koji opisuje ključ fajla. losLab PDF Library ovo sprovodi jednim predikatom: ShouldCryptStreamData vraća False kad god rečnik toka nosi /Type /XRef, tako da izuzetak važi bez obzira kojom putanjom se stiže do serializatora

Kontejner /ObjStm dobija suprotan tretman, i ta asimetrija je namerna. Kontejner se enkriptuje ceo, na osnovu sopstvenog broja objekta, potpuno kao svaki drugi tok. Njegovi članovi se ne enkriptuju pojedinačno — pakuju se u svom dekriptovanom plaintext obliku, i jedan prolaz preko sastavljenog kontejnera ih pokriva, uključujući stringove. Dvostruko enkriptovanje članova proizvodi fajl koji se dekriptuje u ciphertext, a pošto spoljni sloj uspeva, greška izlazi na površinu kao greška parsiranja duboko u grafu objekata, a ne kao greška autentikacije. Jedan objekat tada ostaje potpuno van te šeme: u enkriptovanom dokumentu Catalog se čuva kao direktan type-1 objekat i nikada se ne pakuje, jer bi njegovo pakovanje primoralo loader da inflatuje i dekriptuje tok objekata da bi stigao do korena dokumenta, pre nego što je kontekst dekripcije, koji upravo koren pomaže da se uspostavi, potpuno izgrađen

Uključivanje pakovanja iz Delphija

Javni prekidač je PackObjectStreams, izložen kao polje na TPDFlibSaveOptions, kao samostalni seter SetPackObjectStreams, i kao svojstvo na objektu dokumenta. Podrazumevano je uključen i automatski se ograničava po verziji: writer pakuje samo kada je dokument već PDF 1.5 ili noviji, i poziva interni čuvar minimalne verzije tako da upakovan dokument bude podignut na 1.5, a ne pogrešno obeležen. Nakon čuvanja, GetLastSaveUsedObjectStreams prijavljuje da li se ograda zaista otvorila, što je assert koji želite u regresionom testu, a ne poređenje veličine u bajtovima

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;

Redosled je bitan između pakovanja i sakupljanja smeća. Analiza dostupnosti mora prva da se izvrši, jer član koji preživi u kontejneru povlači kontejner sa sobom — ako je živ objekat upakovan, broj njegovog kontejnera je po definiciji dostupan, a čišćenje kontejnera bi ostavilo člana bez načina da se locira. Pokretanje kolektora prvo takođe znači da mrtvi objekti nikada uopšte ne ulaze u kontejner, odatle dolazi kumulativna dobit u veličini. Pakovanje dopunjuje ostale poluge za veličinu, ne zamenjuje ih; pregled optimizacije veličine PDF fajla i subsetovanja fontova pokriva poluge koje deluju na sadržaj tokova, dok tokovi objekata deluju na strukturu

Granice koje vredi znati pre nego što ga uključite

Inkrementalna čuvanja nikada ne pakuju. Inkrementalno ažuriranje dodaje nove objekte i novu cross-reference sekciju dok ranije revizije ostaju fizički netaknute, pa bi ponovno pakovanje postojećih objekata u sveže kontejnere ostavilo bez korena type-1 stavke na koje prethodna revizija i dalje referiše; losLab PDF Library isključuje pakovanje kad god je aktivan append mod, a članak o inkrementalnim ažuriranjima i streamovanju u append-modu tu putanju pokriva u potpunosti. Dokumenti ispod PDF 1.5 bezuslovno zadržavaju plaintext cross-reference tabelu: konzument verzije 1.4 nema pojma šta /ObjStm znači, a tiho unapređenje dokumenta zato što je writer preferirao manji fajl bila bi pogrešna trampa u ime pozivaoca. Jedan opcioni ključ koji namerno ne emitujemo je /Extends, koji ISO 32000-1 §7.5.7 definiše tako da kontejner može imenovati prethodnika i čitači mogu tretirati lanac kontejnera kao logičku grupu. On je zaista opcion, svaki kontejner koji pišemo je samostalan i nezavisno dekodiv, a preskakanje ga uklanja celu klasu bagova sa ciklusima i visećim referencama iz writera — mada čitači svakako i dalje moraju poštovati /Extends kad na njega naiđu u fajlovima drugih proizvođača

Pakovanje tokova objekata i izlaz cross-reference toka isporučuju se kao deo losLab PDF Library za Delphi i C++Builder, zajedno sa sakupljačem smeća i optimizatorom tokova sadržaja sa kojima se sastavljaju; proizvodna stranica nosi kompletnu referencu opcija čuvanja