Tehnički članak

Tokovi objekata i cross-reference tokovi u PDF-u u Delphiju

PDF 1.5 tokovi objekata pakiraju mnoge male indirektne objekte u jedan Flate-komprimirani kontejner, a losLab PDF Library ih zapisuje pri punom spremanju putem zastavice PackObjectStreams. Dobitak je stvaran: stotine rječnika stranica, fontova i napomena, od kojih svaki nekomprimiran košta desetke bajtova, sažimaju se u šačicu komprimiranih blobova. Cijena je što svaki upakirani objekt sada treba cross-reference tok koji ga opisuje

Ta druga polovica je mjesto gdje se pisci lome. Izgradnja kontejnera /ObjStm je aritmetika; podučavanje mehanizma cross-reference-a da pokazuje u njega je redizajn. Pisac koji proizvede savršeno valjan kontejner, a zatim opiše njegove članove običnim tip-1 pomacima, proizveo je datoteku koju će Acrobat otvoriti dovoljno dugo da je proglasi oštećenom. Ta dva svojstva su jedno svojstvo, a ovaj članak pokriva stranu zapisivanja obaju, kako su definirana u ISO 32000-1 §7.5.7 i §7.5.8

Što kontejner ObjStm zapravo sadrži

Tok objekta je tok čiji dekodirani bajtovi čine dvije spojene regije, a ISO 32000-1 §7.5.7 rječniku daje točno tri ključa koji su bitni za izgradnju. /Type /ObjStm ga identificira, /N daje broj članova, a /First daje duljinu zaglavlja u bajtovima — odnosno, pomak na kojem počinje tijelo. Zaglavlje je razmakom odvojen niz parova broja objekta i pomaka; tijelo su članovi serijalizirani jedan za drugim, pri čemu se svaki pomak mjeri od početka tijela, a ne od početka dekodiranog sadržaja. Čitanje potpuno dekodiranog kontejnera to čini očitim: ispod, /First je 14 jer tri retka zaglavlja zauzimaju četrnaest bajtova, a objekt 7 sjedi 55 bajtova unutar tijela jer je objekt 4 serijaliziran u 54 znaka plus razdjelnik

// 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 izravno iz §7.5.7. Objekt toka nikad ne može biti član, jer tok nosi sirove bajtove koji bi morali biti ugniježđeni unutar drugog toka. A član mora biti potpuna vrijednost objekta, nikad gola indirektna referenca — komprimirani objekt koji je tek 5 0 R stvara indirekciju koju čitač ne može razriješiti bez da već zna kamo pokazuje. losLab PDF Library filtrira oba slučaja tijekom prikupljanja kandidata, zajedno s rječnikom enkripcije i objektom 0, a zatim pakira sve što preživi u skupinama od 200 po kontejneru. Ta granica je odluka o slučajnom pristupu, a ne ograničenje specifikacije: čitač koji želi jednog člana mora napuhati cijeli kontejner, pa preveliki kontejneri čine male pretrage skupima

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

Zato što upakirani objekt nema pomak datoteke za bilježenje. ISO 32000-1 §7.5.8 na ovo odgovara s tri tipa unosa u binarnom cross-reference toku: tip 0 za slobodne objekte, tip 1 za obične objekte u upotrebi pohranjene na pomaku bajtova i tip 2 za komprimirane objekte, čija dva polja podataka nose broj objekta kontejnera i indeks člana unutar njega. Ne postoji način izraziti upakirani objekt u klasičnoj tekstualnoj xref tablici, što je upravo razlog zašto je PDF 1.5 uveo obje značajke zajedno

Redoslijed koji slijedi spotakne gotovo svaku prvu implementaciju, uključujući našu. Obični objekti dobivaju unose tipa 1. Sami kontejneri /ObjStm dobivaju unose tipa 1, jer je kontejner sasvim normalan indirektni objekt toka zapisan na stvarnom pomaku. Samo članovi dobivaju unose tipa 2. A cross-reference tok je sam indirektni objekt u datoteci, pa treba vlastiti unos tipa 1 koji pokazuje na pomak na kojem je upravo zapisan — isti pomak koji bilježi startxref. Rana verzija našeg pisca isključivala je brojeve objekata kontejnera iz petlje zapisivanja umjesto da isključi članove, a rezultat je bila datoteka s cross-reference tokom i bez ijednog toka objekata: strukturno koherentna, semantički prazna, odbačena nizvodno. Vrijednost /Size skriva odgovarajuću grešku za jedan, budući da je najviši broj objekta plus jedan, a cross-reference tok je dodijeljen kao najviši broj objekta, pa se i on mora brojati

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

Polje /W deklarira širinu u bajtovima svakog od tri polja, a losLab PDF Library ga zapisuje kao /W [1 Field2 Field3] s poljem 1 fiksiranim na jedan bajt za kôd tipa i poljem 3 fiksiranim na dva bajta, što pokriva brojeve generacije do 65535 i indekse članova podjednako. Polje 2 je ono koje ne može biti konstanta, jer nosi dvije nepovezane veličine: u unosu tipa 1 to je pomak u bajtovima ograničen samo veličinom datoteke, dok je u unosu tipa 2 broj objekta kontejnera, a u unosu tipa 0 sljedeći slobodni objekt u lancu. Fiksno polje 2 od četiri bajta radi dobro sve dok datoteka ne prijeđe 4 GB, u kojem trenutku svaki pomak preko granice tiho se odsijeca i cijela tablica postaje smeće. Pisac zato pretražuje sastavljenu tablicu za najvećom vrijednošću koju će ikad sadržavati bilo koji slot polja 2, uključujući pomak samog cross-reference toka, i širi 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

Kad su širine poznate, veličina sadržaja je poznata točno, pa pisac unaprijed dodjeljuje cijeli spremnik i puni ga po indeksu; dodavanje unosa bajt po bajt u AnsiString čini izgradnju tablice kvadratnom, što nitko ne primijeti na desetostraničnoj fakturi, a svatko primijeti na dokumentu s dvjesto tisuća objekata. Još dvije pojedinosti drže stroge čitače zadovoljnima. /Index deklarira koje raspone brojeva objekata tablica pokriva, a za puno prepisivanje to je jednostavno [0 N] bez praznina. I svaki slot koji pisac stvarno nije zapisao mora prema zadanom biti slobodan, a ne u upotrebi: objekt 0 predvodi lanac slobodnih, svaki slobodni slot povezuje se na sljedeći, a slot koji je nekad sadržavao obrisani objekt zadržava svoju generaciju uvećanu za jedan. Popratna bilješka o sigurnosti memorije pri parsiranju nepouzdanih PDF-ova iznosi isti argument o granicama sa strane čitanja

Zašto cross-reference tok nikad ne smije biti šifriran?

Zato što ga čitač mora parsirati prije nego što zna kako bilo što dešifrirati. Cross-reference tok je ono što čitaču govori gdje se nalazi rječnik /Encrypt; kad bi njegovi bajtovi bili sami šifrirani, čitaču bi trebao ključ datoteke da pronađe objekt koji opisuje ključ datoteke. losLab PDF Library ovo provodi jednim predikatom: ShouldCryptStreamData vraća False kad god rječnik toka nosi /Type /XRef, pa iznimka vrijedi bez obzira koji put dosegne serijalizator

Kontejner /ObjStm dobiva suprotan tretman, a asimetrija je namjerna. Kontejner se šifrira u cijelosti, ključan po vlastitom broju objekta, potpuno kao svaki drugi tok. Njegovi članovi se ne šifriraju pojedinačno — upakirani su u svom dešifriranom, čistom obliku, a jedan prolazak preko sastavljenog kontejnera pokriva ih, uključujući nizove. Dvostruko šifriranje članova proizvodi datoteku koja se dešifrira u šifrat, a zato što vanjski sloj uspijeva, kvar se pojavljuje kao pogreška parsiranja duboko u grafu objekata, a ne kao pogreška autentifikacije. Jedan objekt tada ostaje potpuno izvan sheme: u šifriranom dokumentu Katalog se čuva kao direktan objekt tipa 1 i nikad se ne pakira, jer bi njegovo pakiranje prisililo učitavač da napuše i dešifrira tok objekta kako bi dosegnuo korijen dokumenta, prije nego što je kontekst dešifriranja koji korijen pomaže uspostaviti u potpunosti izgrađen

Uključivanje pakiranja iz Delphija

Javni prekidač je PackObjectStreams, izložen kao polje na TPDFlibSaveOptions, kao samostalni postavljač SetPackObjectStreams i kao svojstvo na objektu dokumenta. Prema zadanom je uključen i automatski ograničen verzijom: pisac pakira samo kad je dokument već PDF 1.5 ili noviji, i poziva interni čuvar minimalne verzije tako da se upakirani dokument podigne na 1.5, a ne pogrešno označi. Nakon spremanja, GetLastSaveUsedObjectStreams prijavljuje je li ograda zapravo otvorena, što je tvrdnja koju želite u regresijskom testu, a ne usporedba veličine bajtova

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;

Redoslijed je bitan između pakiranja i sakupljanja smeća. Analiza dostupnosti mora prvo biti pokrenuta, jer član koji preživi u kontejner povlači kontejner sa sobom — ako je živi objekt upakiran, broj njegova kontejnera je po definiciji dostupan, a čišćenje kontejnera ostavilo bi člana bez načina da se locira. Pokretanje sakupljača prvo također znači da mrtvi objekti nikad ne ulaze u kontejner, što je odakle dolazi kumulativni dobitak u veličini. Pakiranje nadopunjuje ostale poluge veličine umjesto da ih zamjenjuje; pregled optimizacije veličine PDF datoteke i podskupljanja fontova pokriva poluge koje djeluju na sadržaj tokova, dok tokovi objekata djeluju na strukturu

Granice koje vrijedi znati prije uključivanja

Inkrementalna spremanja nikad ne pakiraju. Inkrementalno ažuriranje dodaje nove objekte i novu cross-reference sekciju ostavljajući ranije revizije fizički netaknutima, pa bi ponovno pakiranje postojećih objekata u svježe kontejnere ostavilo bez vlasnika unose tipa 1 na koje se prethodna revizija i dalje poziva; losLab PDF Library onemogućuje pakiranje kad god je aktivan način dodavanja, a članak o inkrementalnim ažuriranjima i streamanju u načinu dodavanja pokriva taj put u cijelosti. Dokumenti ispod PDF-a 1.5 bezuvjetno zadržavaju tekstualnu cross-reference tablicu: čitač verzije 1.4 nema pojma što /ObjStm znači, a tiho promoviranje dokumenta jer je pisac preferirao manju datoteku bio bi pogrešan kompromis na račun pozivatelja. Jedan opcionalni ključ koji namjerno ne zapisujemo je /Extends, koji ISO 32000-1 §7.5.7 definira kako bi kontejner mogao imenovati prethodnika, a čitači mogu tretirati lanac kontejnera kao logičku skupinu. Doista je opcionalan, svaki kontejner koji zapisujemo je samostalan i neovisno dekodiv, a preskakanje uklanja klasu bugova cikličkih i visećih referenci iz pisca — iako čitači, naravno, i dalje moraju poštovati /Extends kad ga sretnu u datotekama drugih proizvođača

Pakiranje tokova objekata i izlaz cross-reference tokova isporučuju se kao dio losLab PDF Library za Delphi i C++Builder, uz sakupljač smeća i optimizator tokova sadržaja s kojima se nadopunjuju; produktna stranica sadrži potpunu referencu opcija spremanja