Tehnički članak

Revizija veličine PDF fajla u Delphi: raspodela bajtova po kategoriji

Da biste saznali gde zapravo odlazi veličina PDF fajla, losLab PDF Library izlaže AuditDocumentSpace, koji klasifikuje svaki indirektni objekat u dvanaest kategorija — slike, font programi, font rečnici, tokovi sadržaja, form XObject, tokovi objekata, ugrađeni fajlovi, metapodaci, stablo strukture, anotacije, stablo stranica, ostalo — i prijavljuje broj objekata, sačuvane bajtove i procentualni udeo svake

Situacija za koju ovo postoji je poznata. Izveštaj od 40 stranica izlazi iz vašeg generatora na 80 MB, klijent pita zašto, a vi možete ponuditi samo nagađanje. Verovatno slike. Možda fontovi. Zato uključite downsampling, isporučite ga, a fajl sleti na 74 MB jer je stvarna težina bila potpuno negde drugde. Naš prateći članak o subsetting-u fontova i downsampling-u slika pokriva kako smanjiti PDF; ovaj pokriva korak koji treba doći prvi, a to je merenje onoga što ćete smanjiti

Zašto meriti pre kompresije?

Zato što tri standardna prolaza optimizacije imaju drastično različite dobitke na bilo kom datom fajlu, i ništa u vezi sa fajlom vam ne govori koji se primenjuje dok ne izbrojite. Subsetting fontova na dokumentu čiji fontovi već čine 2% njegovih bajtova je poslepodne provedeno na pomeranje greške zaokruživanja. Downsampling slika u fajlu čija je masa nekomprimovani tokovi sadržaja proizvodi isto razočaranje. Optimizator nije teški deo — svaka biblioteka ga ima. Znati koji optimizator uperiti u ovaj fajl je teški deo, i to je pitanje računovodstva, ne pitanje kompresije. Revizija takođe hvata slučajeve gde nijedan optimizator nije odgovor: fajl koji se ispostavi da je 60% ugrađeni prilozi ne treba bolju kompresiju, treba razgovor o tome da li ti prilozi pripadaju dokumentu, a fajl koji je 30% stablo strukture plaća za tagovanje pristupačnosti, što je obično namerni trošak koji ne treba tiho ukloniti. Kad se bajtovi pripisu, donosite odluku o proizvodu sa brojevima iza sebe umesto da posegnete za bilo kojim prekidačem koji je najbliži

Šta sadrži izveštaj od dvanaest kategorija

AuditDocumentSpace vraća handle liste stringova, a ne zapis, tako da izveštaj preživljava ravne DLL i COM fasade nepromenjen. Lista sadrži zbirni red Total,Objects,Bytes,100.0, praćen tačno dvanaest redova Category,Objects,Bytes,Percent u fiksnom redosledu koji je deo ugovora: Images, Font programs, Font dictionaries, Content streams, Form XObjects, Object streams, Embedded files, Metadata, Structure tree, Annotations, Page tree, Other. Trinaest redova, uvek, čak i kad je kategorija prazna

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;

Jedan Delphi detalj u toj petlji će vas ujesti tačno jednom. GetStringListItem koristi indekse stavki od jedan, u skladu sa GetStringListCount, a indeks van opsega vraća prazan string umesto da izazove izuzetak. Napišite petlju for I := 0 to Count - 1 iz navike i dobićete prazan prvi red, tiho odbačen poslednji red, i nijedan izuzetak nigde koji bi vam rekao da je indeksiranje pogrešno. Sam izveštaj će izgledati skoro ispravno, što je najgori mogući režim otkaza koji dijagnostički alat može imati

Zašto revizija koristi sačuvanu dužinu umesto dekodovane veličine?

Zato što je sačuvana dužina i broj koji želite i broj koji je jeftino dobiti. Svaki indirektni objekat nosi TPDFIndObj.FLength, sirovu bajtovnu dužinu koju objekat zauzima u fajlu kako je parsiran. Njeno korišćenje znači da se slika od 900 KB u DCTDecode prijavljuje kao 900 KB — bajtovi koje vas košta na disku — a ne kao 40 MB RGB uzoraka u koje se dekoduje. To takođe znači da revizija nikad ne mora ništa da dekoduje: lenjo učitani objekti ostaju lenji, filteri ostaju nepokrenuti, a revizija fajla od 500 MB je prolaz kroz zaglavlja objekata, ne pun ciklus dekompresije

Drugo pravilo je odbrana od dvostrukog brojanja. Kad objekat živi unutar komprimovanog toka objekata, označeno nenultim FObjStrNum, njegov broj bajtova se beleži kao nula. Njegovo skladištenje je već plaćeno jednom od strane kontejner toka, koji ISO 32000-1 §7.5.7 definiše kao tok /Type /ObjStm koji drži mnogo objekata u jednom Flate-komprimovanom sadržaju. Naplaćivanje svakog člana za njegov deo, a zatim ponovno naplaćivanje kontejnera bi naduvalo ukupno preko stvarne veličine fajla. Ovo ima direktnu posledicu na to kako čitate izlaz, obrađenu ispod i dublje u našem članku o object streams i cross-reference streams

Zašto font program ne može sam sebe da klasifikuje?

Zato što TrueType fajl fonta ugrađen u PDF nema oznaku koja to kaže. ISO 32000-1 §9.8.1 definiše ugrađeni font program kao vrednost /FontFile, /FontFile2 ili /FontFile3 u deskriptoru fonta, a rečnik toka na drugom kraju te reference nosi /Length1 i ključeve filtera, ali nema /Type i nema /Subtype koji ga identifikuje kao font. Posmatran izolovano, to je anonimni binarni tok. Samo deskriptor koji na njega pokazuje zna šta je to. Ista asimetrija se javlja i kod anotacija: §12.5.2 čini /Type /Annot opcionim u rečniku anotacije, tako da je pouzdan signal članstvo u nizu /Annots stranice, a ne sam rečnik

Zato se klasifikacija izvodi dvaput. Prvi prolaz čita sopstveni /Type i /Subtype svakog objekta i uzima lake dobitke: /ObjStm, /Subtype /Image, /Subtype /Form, /Type /Font i /Type /FontDescriptor, /Metadata, /EmbeddedFile i /Filespec, /StructTreeRoot i /StructElem, /Annot, /Page i /Pages. Sve ostalo privremeno sleti u Other. Drugi prolaz zatim obilazi stranu koja referencira i preglasuje: svaki rečnik stranice ponovo dodeljuje svoj /Contents tokovima sadržaja, svoje /Annots unose anotacijama, i svoj /Thumb slikama, dok svaki rečnik fonta obilazi svoj sopstveni lanac 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]));

Čitanje izveštaja i biranje sledećeg poteza

Čitajte udele prvo, brojeve objekata drugo, i tretirajte svaki veliki razmak između njih kao signal. Moderan PDF stavlja većinu svojih malih rečnika unutar tokova objekata, tako da Page tree i Structure tree rutinski pokazuju desetine objekata naspram skoro nula bajtova — njihov stvarni trošak je uklopljen u red Object streams. Ako je sam Object streams velik, fajl je gust strukturom sličnom metapodacima, a ne sadržajem, i poluga je odsecanje objekata, ne njihova kompresija. Tokovi izgleda anotacija se ponašaju slično: nose /Subtype /Form, tako da jako pečatiran dokument pokazuje svoju težinu pod Form XObjects, dok red Annotations ostaje mali

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;

Dve činjenice o formatiranju su bitne ako parsirate procente umesto da ih samo prikazujete. Decimalni separator je uvek doslovna tačka bez obzira na lokal mašine, tako da parsiranje sa ambijentalnim FormatSettings na nemačkoj ili francuskoj radnoj stanici propada, ili gore, pogrešno čita. A prateće nule su odsečene, tako da kategorija koja drži tačno 40% bajtova ispisuje 40, ne 40.0 — nikad ne pretpostavljajte fiksno decimalno mesto. Sa udelom u ruci, usmeravanje je mehaničko: dominantan udeo Images ukazuje na DownsampleImages, dominantan udeo Font programs na SubsetEmbeddedFonts, a masivan Content streams na CompressContent

Šta revizija namerno ne govori

Ukupno je zbir preko indirektnih objekata, a PDF fajl je malo više od svojih objekata. Zaglavlje fajla, trailer, međuobjektni razmaci i klasična cross-reference tabela nisu indirektni objekti, tako da se ti bajtovi ne pripisuju ničemu i ukupno u reviziji sleti malo ispod veličine na disku. Cross-reference tok je drugačiji — to je pravi objekat sa /Type /XRef, tako da se u modernom fajlu ti bajtovi pojavljuju, u kategoriji Other. Nijedno ponašanje nije defekt, ali ako pomirujete reviziju sa brojem bajtova iz fajl sistema, tu je razlika odakle dolazi

Vredi jasno navesti još dve granice. Prvo, brojevi opisuju fajl koji je učitan, ne onaj koji se piše: za objekte izgrađene u memoriji koji još nemaju sačuvanu dužinu, veličina se vraća na serijalizovani izlaz sa nominalnom dozvolom za rečnik toka, što je procena eventualnog upisa, a ne merenje. Izvedite reviziju posle snimanja i ponovnog učitavanja ako želite tačne brojke. Drugo, debeo red Other je nalaz, a ne izveštaj o bagu — obično znači osirotele objekte koje ništa više ne referencira, što je posao za mark-and-sweep garbage collection, a ne za bilo koji prolaz kompresije

Korišćena ovako, revizija menja oblik razgovora. Umesto nagađanja oko izveštaja od 80 MB, otvorite ga, pokrenete jedan poziv, i pročitate da su slike 8%, font programi 61%, a dokument ugrađuje devet punih font programa za kućni stil koji koristi tri pisma. To je odgovor koji se može popraviti, sa priloženim brojem. AuditDocumentSpace, zajedno sa prolazima optimizacije ka kojima vas usmerava, isporučuje se u losLab PDF Library za Delphi i C++Builder, gde referentne stranice dokumentuju kompletan spisak kategorija i API liste stringova oko njega