Odborný článok

Audit veľkosti PDF súboru v Delphi: rozpad podľa kategórií

Aby ste zistili, kam v skutočnosti smeruje veľkosť PDF súboru, losLab PDF Library sprístupňuje AuditDocumentSpace, ktorá klasifikuje každý nepriamy objekt do dvanástich kategórií — obrázky, programy fontov, slovníky fontov, obsahové streamy, formulárové XObjects, objektové streamy, vložené súbory, metadáta, strom štruktúry, anotácie, strom strán, ostatné — a hlási počet objektov, uložené bajty a percentuálny podiel každej z nich

Situácia, pre ktorú toto existuje, je dôverne známa. 40-stranová správa vyjde z vášho generátora s veľkosťou 80 MB, zákazník sa spýta prečo, a všetko, čo mu viete ponúknuť, je odhad. Asi obrázky. Možno fonty. Tak zapnete downsampling, odošlete, a súbor pristane na 74 MB, pretože skutočná váha bola úplne inde. Náš sprievodný článok o subsettovaní fontov a downsamplingu obrázkov pokrýva, ako PDF zmenšiť; tento pokrýva krok, ktorý by mal prísť ako prvý, a to zmeranie toho, čo sa chystáte zmenšiť

Prečo merať pred kompresiou?

Pretože tri štandardné optimalizačné prechody majú na ktoromkoľvek danom súbore diametrálne odlišnú návratnosť, a nič na súbore vám nepovie, ktorý z nich sa hodí, kým to nespočítate. Subsetting fontov na dokumente, ktorého fonty už tvoria 2 % jeho bajtov, je popoludnie strávené presúvaním zaokrúhľovacej chyby. Downsampling obrázkov v súbore, ktorého objem tvoria nekomprimované obsahové streamy, prináša rovnaké sklamanie. Optimalizátor nie je tá ťažká časť — každá knižnica ho má. Vedieť, ktorý optimalizátor na daný súbor nasmerovať, je tá ťažká časť, a to je otázka účtovníctva, nie kompresie. Audit zachytí aj prípady, kde odpoveďou nie je žiadny optimalizátor: súbor, ktorý sa ukáže byť z 60 % vloženými prílohami, nepotrebuje lepšiu kompresiu, potrebuje rozhovor o tom, či tie prílohy vôbec patria do dokumentu, a súbor, ktorý je z 30 % strom štruktúry, platí za tagovanie prístupnosti, čo je zvyčajne zámerný náklad, ktorý by ste nemali ticho odstrániť. Akonáhle sú bajty priradené, robíte produktové rozhodnutie podložené číslami namiesto siahnutia po najbližšom prepínači

Čo obsahuje správa s dvanástimi kategóriami

AuditDocumentSpace vracia handle zoznamu reťazcov namiesto záznamu, takže správa prežije nezmenená cez plochú DLL a COM fasády. Zoznam obsahuje súhrnný riadok Total,Objects,Bytes,100.0, po ktorom nasleduje presne dvanásť riadkov Category,Objects,Bytes,Percent v pevnom poradí, ktoré je súčasťou kontraktu: Images, Font programs, Font dictionaries, Content streams, Form XObjects, Object streams, Embedded files, Metadata, Structure tree, Annotations, Page tree, Other. Trinásť riadkov, vždy, aj keď je kategória prázdna

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;

Jeden detail Delphi v tejto slučke vás uhryzne presne raz. GetStringListItem používa jednotkovo indexované položky, čo zodpovedá GetStringListCount, a index mimo rozsahu vracia prázdny reťazec namiesto vyvolania výnimky. Napíšte slučku ako for I := 0 to Count - 1 zo zvyku a dostanete prázdny prvý riadok, ticho zahodený posledný riadok, a žiadnu výnimku nikde, ktorá by vám povedala, že indexovanie je zlé. Samotná správa bude vyzerať skoro správne, čo je najhorší možný spôsob zlyhania diagnostického nástroja

Prečo audit používa uloženú dĺžku namiesto dekódovanej veľkosti?

Pretože uložená dĺžka je zároveň číslo, ktoré chcete, a číslo, ktoré je lacné získať. Každý nepriamy objekt nesie TPDFIndObj.FLength, surovú bajtovú dĺžku, ktorú objekt zaberá v súbore tak, ako bol parsovaný. Jej použitie znamená, že 900 KB DCTDecode obrázok sa hlási ako 900 KB — bajty, ktoré vás stoja na disku — namiesto 40 MB RGB vzoriek, na ktoré sa dekóduje. Znamená to tiež, že audit nikdy nemusí nič dekódovať: lenivo načítané objekty zostávajú lenivé, filtre zostávajú nespustené, a auditovanie 500 MB súboru je prechod cez hlavičky objektov, nie plný dekompresný cyklus

Druhé pravidlo je obrana proti dvojitému počítaniu. Keď objekt žije vo vnútri komprimovaného objektového streamu, čo indikuje nenulové FObjStrNum, jeho počet bajtov sa zaznamená ako nula. Jeho úložisko už raz zaplatil kontajnerový stream, ktorý ISO 32000-1 §7.5.7 definuje ako stream /Type /ObjStm držiaci mnoho objektov v jednom Flate-komprimovanom payloade. Účtovanie vlastného podielu každému členovi a následné opätovné účtovanie kontajnera by nafúklo celkový súčet nad skutočnú veľkosť súboru. Toto má priamy dôsledok na to, ako čítate výstup, čomu sa venujeme nižšie a hlbšie v našom článku o object streams a cross-reference streams

Prečo sa program fontu nedokáže klasifikovať sám?

Pretože súbor TrueType fontu vložený do PDF nemá žiadnu značku, ktorá by to hovorila. ISO 32000-1 §9.8.1 definuje vložený program fontu ako hodnotu /FontFile, /FontFile2 alebo /FontFile3 v deskriptore fontu, a slovník streamu na druhom konci tejto referencie nesie /Length1 a kľúče filtrov, ale žiadny /Type a žiadny /Subtype, ktorý by ho identifikoval ako font. Izolovane pozorovaný je to anonymný binárny stream. Iba deskriptor, ktorý naň ukazuje, vie, čo to je. Rovnaká asymetria sa objavuje pri anotáciách: §12.5.2 robí /Type /Annot voliteľným v slovníku anotácie, takže spoľahlivým signálom je členstvo v poli strany /Annots, nie samotný slovník

Klasifikácia teda beží dvakrát. Prvý prechod číta vlastné /Type a /Subtype každého objektu a berie ľahké výhry: /ObjStm, /Subtype /Image, /Subtype /Form, /Type /Font a /Type /FontDescriptor, /Metadata, /EmbeddedFile a /Filespec, /StructTreeRoot a /StructElem, /Annot, /Page a /Pages. Všetko ostatné dočasne pristane v Other. Druhý prechod potom prejde stranu odkazujúcich a prepíše: každý slovník strany priradí svoje /Contents obsahovým streamom, svoje položky /Annots anotáciám a svoj /Thumb obrázkom, zatiaľ čo každý slovník fontu prejde svoj vlastný reťazec deskriptora

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

Čítanie správy a výber ďalšieho kroku

Čítajte najprv podiely, počty objektov druhoradé, a akúkoľvek veľkú medzeru medzi nimi berte ako signál. Moderné PDF ukladá väčšinu svojich malých slovníkov vnútri objektových streamov, takže Page tree a Structure tree bežne vykazujú desiatky objektov proti takmer nule bajtov — ich skutočná cena bola zabalená do riadku Object streams. Ak je samotný Object streams veľký, súbor je hustý na štruktúru pripomínajúcu metadáta skôr než na obsah, a pákou je orezávanie objektov, nie ich kompresia. Podobne sa správajú appearance streamy anotácií: nesú /Subtype /Form, takže silne opečiatkovaný dokument ukáže svoju váhu pod Form XObjects, zatiaľ čo riadok Annotations zostáva malý

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;

Dva fakty o formátovaní sú dôležité, ak percentá parsujete namiesto toho, aby ste ich iba zobrazovali. Desatinný oddeľovač je vždy doslovná bodka bez ohľadu na locale stroja, takže parsovanie pomocou okolitého FormatSettings na nemeckej alebo francúzskej pracovnej stanici zlyhá alebo, čo je horšie, nesprávne prečíta. A koncové nuly sú orezané, takže kategória tvoriaca presne 40 % bajtov sa vytlačí ako 40, nie 40.0 — nikdy nepredpokladajte pevný počet desatinných miest. S podielom po ruke je smerovanie mechanické: dominantný podiel Images ukazuje na DownsampleImages, dominantný podiel Font programs na SubsetEmbeddedFonts a objemné Content streams na CompressContent

Čo vám audit zámerne nepovie

Súčet je súčtom cez nepriame objekty, a PDF súbor je o niečo viac než svoje objekty. Hlavička súboru, trailer, medzery medzi objektmi a klasická cross-reference tabuľka nie sú nepriame objekty, takže tieto bajty nie sú priradené ničomu a celkový súčet auditu pristane o niečo pod skutočnou veľkosťou na disku. Cross-reference stream je iný — je to skutočný objekt s /Type /XRef, takže v modernom súbore sa tieto bajty objavia, v kategórii Other. Ani jedno správanie nie je chyba, ale ak zosúlaďujete audit s počtom bajtov zo súborového systému, práve odtiaľ pochádza rozdiel

Stoja za jasné vyslovenie ešte dve hranice. Po prvé, čísla opisujú súbor, ktorý bol načítaný, nie taký, ktorý sa práve autorsky vytvára: pri objektoch zostavených v pamäti, ktoré ešte nemajú uloženú dĺžku, sa veľkosť vráti k serializovanému výstupu s nominálnou rezervou na slovník streamu, čo je odhad eventuálneho zápisu, nie meranie. Ak chcete presné čísla, auditujte po uložení a opätovnom načítaní. Po druhé, tučný riadok Other je zistenie, nie hlásenie chyby — zvyčajne znamená osirotené objekty, na ktoré už nič neodkazuje, čo je práca pre mark-and-sweep garbage collection, nie pre žiadny kompresný prechod

Takto použitý audit mení tvar konverzácie. Namiesto hádania nad 80 MB správou ju otvoríte, spustíte jedno volanie a prečítate si, že obrázky tvoria 8 %, programy fontov 61 %, a dokument vkladá deväť plných programov fontov pre firemný štýl, ktorý používa tri rezy. To je opraviteľná odpoveď s pripojeným číslom. AuditDocumentSpace, spolu s optimalizačnými prechodmi, na ktoré vás nasmeruje, je súčasťou losLab PDF Library pre Delphi a C++Builder, kde referenčné stránky dokumentujú úplný zoznam kategórií a API zoznamu reťazcov okolo neho