Annak megállapítására, hogy egy PDF fájl mérete valójában hová megy el, a losLab PDF Library közzéteszi az AuditDocumentSpace függvényt, amely minden közvetett objektumot tizenkét kategóriába sorol be — képek, betűtípusprogramok, betűtípus-szótárak, tartalomfolyamok, form XObjectek, objektumfolyamok, beágyazott fájlok, metaadatok, struktúrafa, jegyzetek, oldalfa, egyéb — és jelenti mindegyik objektumszámát, tárolt bájtjait és százalékos részesedését
A helyzet, amelyre ez a megoldás született, ismerős. Egy 40 oldalas jelentés 80 MB-tal kerül ki a generátorunkból, az ügyfél megkérdezi, miért, és mi csak találgatni tudunk. Valószínűleg a képek. Talán a betűtípusok. Ekkor bekapcsoljuk az alulmintavételezést, kiszállítjuk a fájlt, az pedig 74 MB-on landol, mert a valódi súly egészen máshol volt. A betűtípus-subsetelésről és kép-alulmintavételezésről szóló kísérőcikkünk azt tárgyalja, hogyan zsugorítsunk egy PDF-et; ez a cikk azt a lépést tárgyalja, amelynek elsőnek kellene jönnie, vagyis annak mérését, hogy mit is akarunk zsugorítani
Miért mérjünk tömörítés előtt?
Mert a három standard optimalizálási lépés rendkívül eltérő megtérüléssel jár egy adott fájlon, és a fájlban semmi nem árulja el, melyik alkalmazandó, amíg meg nem számoljuk. Egy olyan dokumentum betűtípusainak subsetelése, amelynek betűtípusai már a bájtjainak 2%-át teszik ki, egy délutánt jelent egy kerekítési hiba elmozdítására fordítva. A képek alulmintavételezése egy olyan fájlban, amelynek nagy része tömörítetlen tartalomfolyam, ugyanilyen csalódást okoz. Az optimalizáló nem a nehéz rész — minden könyvtárnak van egyike. Annak eldöntése, hogy melyik optimalizálót irányítsuk erre a fájlra, az a nehéz rész, és ez könyvelési kérdés, nem tömörítési kérdés. Az audit azokat az eseteket is elkapja, amikor a válasz egyáltalán nem az optimalizáló: egy olyan fájl, amelynek 60%-a beágyazott melléklet, nem jobb tömörítést igényel, hanem egy beszélgetést arról, hogy ezek a mellékletek egyáltalán a dokumentumban a helyükön vannak-e, egy olyan fájl pedig, amely 30%-ban struktúrafa, az akadálymentesítési jelölésért fizeti meg az árat, ami általában tudatos költség, amit nem szabad némán levágni. Miután a bájtok hozzá lettek rendelve, számokkal alátámasztott termékdöntést hozunk, ahelyett hogy a legközelebbi kapcsolóhoz nyúlnánk
Mit tartalmaz a tizenkét kategóriás jelentés
Az AuditDocumentSpace egy stringlista-kezelőt ad vissza, nem egy rekordot, így a jelentés változatlanul túléli a lapos DLL- és COM-fasade-okat is. A lista egy Total,Objects,Bytes,100.0 összegzősort tartalmaz, amelyet pontosan tizenkét Category,Objects,Bytes,Percent sor követ, rögzített sorrendben, ami a szerződés része: Images, Font programs, Font dictionaries, Content streams, Form XObjects, Object streams, Embedded files, Metadata, Structure tree, Annotations, Page tree, Other. Tizenhárom sor, mindig, még akkor is, ha egy kategória üres
var
Lib: TPDFlib;
ListID, I: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
ListID := Lib.AuditDocumentSpace; // 0 when no document is selected
if ListID = 0 then
Exit;
try
// GetStringListItem is 1-based: items run 1..GetStringListCount
for I := 1 to Lib.GetStringListCount(ListID) do
Memo1.Lines.Add(Lib.GetStringListItem(ListID, I));
finally
Lib.ReleaseStringList(ListID);
end;
finally
Lib.Free;
end;
end;
Ebben a ciklusban egy Delphi-specifikus részlet pontosan egyszer fog megharapni. A GetStringListItem egyalapú elemindexeket használ, összhangban a GetStringListCount-tal, és egy tartományon kívüli index üres sztringgel tér vissza kivétel dobása helyett. Ha megszokásból for I := 0 to Count - 1 ciklust írunk, üres első sort kapunk, csendben elveszik az utolsó sor, és sehol nincs kivétel, amely jelezné, hogy az indexelés hibás. A jelentés önmagában szinte jónak fog látszani, ami a legrosszabb hibamód, amit egy diagnosztikai eszköz produkálhat
Miért használ az audit tárolt hosszt dekódolt méret helyett?
Mert a tárolt hossz egyszerre az a szám, amelyet szeretnénk, és az a szám, amelyet olcsó előállítani. Minden közvetett objektum hordozza a TPDFIndObj.FLength értéket, a fájlban elemzés szerint elfoglalt nyers bájthosszt. Ennek használata azt jelenti, hogy egy 900 KB-os DCTDecode kép 900 KB-ként kerül jelentésre — vagyis annyi bájtba kerül a lemezen —, nem pedig a 40 MB-nyi RGB mintaként, amivé dekódolódik. Azt is jelenti, hogy az auditnak soha nem kell semmit dekódolnia: a lusta módon betöltött objektumok lusták maradnak, a szűrők futtatás nélkül maradnak, egy 500 MB-os fájl auditálása pedig egy végigfutás az objektumfejléceken, nem egy teljes kitömörítési ciklus
A második szabály egy dupla számolás elleni védelem. Amikor egy objektum egy tömörített objektumfolyamon belül él, amit egy nem nulla FObjStrNum jelez, a bájtszáma nullaként kerül rögzítésre. A tárolása már egyszer kifizetésre került a konténer folyam által, amelyet az ISO 32000-1 §7.5.7 /Type /ObjStm folyamként definiál, amely sok objektumot tartalmaz egyetlen Flate-tömörített hasznos adatban. Ha minden tagnak a saját részét, majd a konténert is még egyszer felszámolnánk, az a valós fájlméret fölé fújná a végösszeget. Ennek közvetlen következménye van arra nézve, hogyan olvassuk a kimenetet, ezt alább, és mélyebben az objektumfolyamokról és keresztreferencia-folyamokról szóló cikkünkben tárgyaljuk
Miért nem tudja egy betűtípusprogram önmagát besorolni?
Mert egy PDF-be beágyazott TrueType betűtípusfájlnak nincs erre utaló jelölője. Az ISO 32000-1 §9.8.1 a beágyazott betűtípusprogramot a betűtípus-leíróban lévő /FontFile, /FontFile2 vagy /FontFile3 értékeként definiálja, és a folyamszótár a hivatkozás másik végén hordozza a /Length1-et és a szűrőkulcsokat, de nincs olyan /Type vagy /Subtype, amely betűtípusként azonosítaná. Önmagában nézve ez egy névtelen bináris folyam. Csak a rá mutató leíró tudja, hogy micsoda. Ugyanez az aszimmetria jelentkezik a jegyzeteknél is: a §12.5.2 opcionálissá teszi a /Type /Annot-ot egy jegyzetszótárban, így a megbízható jel a tagság egy oldal /Annots tömbjében, nem maga a szótár
Ezért a besorolás kétszer fut le. Az első lépés minden objektum saját /Type és /Subtype mezőjét olvassa, és a könnyű győzelmeket viszi el: /ObjStm, /Subtype /Image, /Subtype /Form, /Type /Font és /Type /FontDescriptor, /Metadata, /EmbeddedFile és /Filespec, /StructTreeRoot és /StructElem, /Annot, /Page és /Pages. Minden más ideiglenesen az Egyéb kategóriában landol. A második lépés ezután végigmegy a hivatkozó oldalon, és felülbírál: minden oldalszótár átsorolja a /Contents mezőjét a tartalomfolyamokba, az /Annots bejegyzéseit jegyzetekbe és a /Thumb-ját képekbe, míg minden betűtípus-szótár végigjárja a saját leírólánc-hivatkozását
// Shape of the second pass: the referrer names the object
Descriptor := DictOf(FontDict.FindValueByKeyName('FontDescriptor'));
if Assigned(Descriptor) then
begin
MarkRef(FontDict.FindValueByKeyName('FontDescriptor'), catFontDicts);
MarkRef(Descriptor.FindValueByKeyName('FontFile'), catFontPrograms);
MarkRef(Descriptor.FindValueByKeyName('FontFile2'), catFontPrograms);
MarkRef(Descriptor.FindValueByKeyName('FontFile3'), catFontPrograms);
end;
// Type0 fonts keep the descriptor one level down
Descendants := FontDict.FindValueByKeyName('DescendantFonts', True);
if (Descendants is TPDFArray) and (TPDFArray(Descendants).Count > 0) then
MarkFontProgramRefs(DictOf(TPDFArray(Descendants).Item[0]));
A jelentés olvasása és a következő lépés kiválasztása
Először a részesedéseket olvassuk, másodszor az objektumszámokat, és minden nagy szakadékot köztük jelként kezeljünk. Egy modern PDF a legtöbb kis szótárát objektumfolyamokba teszi, így az Oldalfa és a Struktúrafa rendszeresen tucatnyi objektumot mutat közel nulla bájt mellett — a valódi költségük az Object streams sorba lett belegyűrve. Ha maga az Object streams nagy, a fájl inkább metaadat-jellegű struktúrával, mint tartalommal sűrű, és a kar nem a tömörítés, hanem az objektumok ritkítása. A jegyzetek megjelenítési folyamai hasonlóan viselkednek: ezek /Subtype /Form-ot hordoznak, így egy erősen lebélyegzett dokumentum a súlyát a Form XObjects alatt mutatja, miközben az Annotations sor kicsi marad
function CategoryShare(Lib: TPDFlib; ListID: Integer;
const Category: string): Double;
var
I: Integer;
Parts: TArray<string>;
Inv: TFormatSettings;
begin
Result := 0;
Inv := FormatSettings;
Inv.DecimalSeparator := '.'; // the report is locale-independent
for I := 2 to Lib.GetStringListCount(ListID) do // line 1 is Total
begin
Parts := string(Lib.GetStringListItem(ListID, I)).Split([',']);
if (Length(Parts) = 4) and SameText(Parts[0], Category) then
Exit(StrToFloatDef(Parts[3], 0, Inv));
end;
end;
Két formázási tény számít, ha a százalékokat elemezni akarjuk, nem csak megjeleníteni. A tizedesjel mindig szó szerint pont, függetlenül a gép területi beállításától, így az ambiens FormatSettings-szal történő elemzés egy német vagy francia munkaállomáson meghiúsul, vagy ami rosszabb, félreolvassa az értéket. A záró nullák pedig le vannak vágva, így egy pontosan a bájtok 40%-át kitevő kategória 40-ként jelenik meg, nem 40.0-ként — soha ne feltételezzünk fix tizedeshelyet. A részesedés ismeretében az útválasztás gépies: egy domináns Images részesedés a DownsampleImages-re mutat, egy domináns Font programs részesedés a SubsetEmbeddedFonts-ra, a terjedelmes Content streams pedig a CompressContent-re
Amit az audit szándékosan nem árul el
A végösszeg a közvetett objektumok fölötti összeg, egy PDF fájl pedig valamivel több, mint az objektumai. A fájlfejléc, a trailer, az objektumok közötti üres hely és egy klasszikus keresztreferencia-tábla nem közvetett objektumok, így ezek a bájtok semmihez sincsenek hozzárendelve, és az audit végösszege kicsit a lemezen lévő méret alatt landol. Egy keresztreferencia-folyam más: az egy valódi objektum /Type /XRef-fel, így egy modern fájlban ezek a bájtok megjelennek, az Egyéb kategóriában. Egyik viselkedés sem hiba, de ha az auditot egy fájlrendszerből származó bájtszámmal próbáljuk egyeztetni, innen ered a különbség
Érdemes még két határt egyértelműen kimondani. Először, a számok egy betöltött fájlt írnak le, nem egy éppen szerkesztés alatt állót: a memóriában felépített, még nem tárolt hosszal rendelkező objektumoknál a méret a szerializált kimenetre esik vissza a folyamszótár nominális ráhagyásával, ami a végleges írás becslése, nem mérése. Auditáljunk mentés-és-újratöltés után, ha pontos számokat akarunk. Másodszor, egy kövér Egyéb sor egy megállapítás, nem hibajelentés — általában olyan árva objektumokat jelent, amelyekre már semmi nem hivatkozik, ez pedig a mark-and-sweep szemétgyűjtés feladata, nem bármelyik tömörítési lépésé
Így használva az audit megváltoztatja a beszélgetés jellegét. Ahelyett hogy a 80 MB-os jelentésen találgatnánk, megnyitjuk, futtatunk egy hívást, és azt olvassuk, hogy a képek 8%-ot, a betűtípusprogramok 61%-ot tesznek ki, és a dokumentum kilenc teljes betűtípusprogramot ágyaz be egy olyan házi stílushoz, amely három betűkészletet használ. Ez egy javítható válasz, amelyhez szám is tartozik. Az AuditDocumentSpace, együtt az általa jelzett optimalizálási lépésekkel, a losLab PDF Library for Delphi and C++Builder részeként érhető el, ahol a referenciaoldalak dokumentálják a teljes kategórialistát és a hozzá tartozó stringlista-API-t