Pentru a afla unde se duce de fapt dimensiunea unui fișier PDF, losLab PDF Library expune AuditDocumentSpace, care clasifică fiecare obiect indirect în douăsprezece categorii — imagini, programe de font, dicționare de font, fluxuri de conținut, XObjects de formular, fluxuri de obiecte, fișiere încorporate, metadate, arbore de structură, adnotări, arbore de pagini, altele — și raportează numărul de obiecte, octeții stocați și cota procentuală a fiecăreia
Situația pentru care există aceasta este familiară. Un raport de 40 de pagini iese din generatorul dumneavoastră la 80 MB, clientul întreabă de ce, iar tot ce puteți oferi este o presupunere. Probabil imaginile. Poate fonturile. Așa că activați downsampling-ul, îl livrați, iar fișierul ajunge la 74 MB pentru că greutatea reală era cu totul altundeva. Articolul nostru asociat, despre subsetarea fonturilor și downsampling-ul imaginilor, acoperă cum să micșorați un PDF; acesta acoperă pasul care ar trebui să vină primul, adică măsurarea a ceea ce urmează să micșorați
De ce să măsurați înainte de a comprima?
Pentru că cele trei treceri standard de optimizare au randamente extrem de diferite pe orice fișier dat, iar nimic din fișier nu vă spune care se aplică până când nu numărați. Subsetarea fonturilor pe un document ale cărui fonturi sunt deja 2% din octeții lui înseamnă o după-amiază petrecută mutând o eroare de rotunjire. Downsampling-ul imaginilor într-un fișier a cărui masă principală este fluxuri de conținut necomprimate produce aceeași dezamăgire. Optimizatorul nu este partea grea — fiecare bibliotecă are unul. Să știți spre care optimizator să îndreptați acest fișier este partea grea, iar aceasta este o întrebare de contabilitate, nu o întrebare de compresie. Un audit prinde și cazurile în care niciun optimizator nu este răspunsul: un fișier care se dovedește a fi 60% atașamente încorporate nu are nevoie de compresie mai bună, ci de o discuție despre dacă acele atașamente își au locul în document, iar un fișier care este 30% arbore de structură plătește pentru marcarea de accesibilitate, care este de obicei un cost deliberat pe care nu ar trebui să îl eliminați în tăcere. Odată ce octeții sunt atribuiți, luați o decizie de produs cu cifre în spate, nu apucați orice comutator este cel mai la îndemână
Ce conține raportul cu douăsprezece categorii
AuditDocumentSpace returnează un handle de listă de șiruri, nu o înregistrare, astfel încât raportul supraviețuiește neschimbat fațadelor DLL plate și COM. Lista conține o linie de rezumat Total,Objects,Bytes,100.0 urmată de exact douăsprezece linii Category,Objects,Bytes,Percent într-o ordine fixă care face parte din contract: Images, Font programs, Font dictionaries, Content streams, Form XObjects, Object streams, Embedded files, Metadata, Structure tree, Annotations, Page tree, Other. Treisprezece linii, întotdeauna, chiar și când o categorie este goală
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;
Un detaliu Delphi din acea buclă vă va mușca exact o dată. GetStringListItem folosește indici de element bazați pe 1, în concordanță cu GetStringListCount, iar un indice în afara intervalului returnează un șir gol în loc să ridice o excepție. Scrieți bucla for I := 0 to Count - 1 din obișnuință și obțineți o primă linie goală, o ultimă linie eliminată în tăcere și nicio excepție nicăieri care să vă spună că indexarea este greșită. Raportul în sine va arăta aproape corect, ceea ce este cel mai rău mod de eșec pe care îl poate avea un instrument de diagnostic
De ce auditul folosește lungimea stocată în loc de dimensiunea decodată?
Pentru că lungimea stocată este atât cifra pe care o doriți, cât și cifra care este ieftin de obținut. Fiecare obiect indirect poartă TPDFIndObj.FLength, lungimea brută în octeți pe care obiectul o ocupă în fișier așa cum a fost analizat. Folosirea ei înseamnă că o imagine DCTDecode de 900 KB este raportată ca 900 KB — octeții pe care vă costă pe disc — nu cei 40 MB de eșantioane RGB la care se decodează. Mai înseamnă și că auditul nu trebuie niciodată să decodeze nimic: obiectele încărcate leneș rămân leneșe, filtrele rămân nerulate, iar auditarea unui fișier de 500 MB este o trecere peste anteturile obiectelor, nu un ciclu complet de decompresie
A doua regulă este o apărare împotriva numărării duble. Când un obiect trăiește într-un flux de obiecte comprimat, indicat printr-un FObjStrNum diferit de zero, numărul lui de octeți este înregistrat ca zero. Stocarea lui a fost deja plătită o dată de fluxul container, pe care ISO 32000-1 §7.5.7 îl definește ca un flux /Type /ObjStm care conține mai multe obiecte într-un singur payload comprimat cu Flate. Taxarea fiecărui membru cu propria cotă și apoi taxarea din nou a containerului ar umfla totalul peste dimensiunea reală a fișierului. Aceasta are o consecință directă asupra modului în care citiți ieșirea, tratată mai jos și mai în profunzime în articolul nostru despre fluxurile de obiecte și fluxurile cross-reference
De ce un program de font nu se poate clasifica singur?
Pentru că un fișier de font TrueType încorporat într-un PDF nu are niciun marcaj care să spună asta. ISO 32000-1 §9.8.1 definește programul de font încorporat ca valoarea lui /FontFile, /FontFile2 sau /FontFile3 într-un descriptor de font, iar dicționarul de flux de la celălalt capăt al acelei referințe poartă cheile /Length1 și de filtru, dar niciun /Type și niciun /Subtype care să îl identifice drept font. Privit izolat, este un flux binar anonim. Doar descriptorul care indică spre el știe ce este. Aceeași asimetrie apare pentru adnotări: §12.5.2 face /Type /Annot opțional într-un dicționar de adnotare, astfel încât semnalul de încredere este apartenența la un array /Annots al paginii, nu dicționarul în sine
Așadar clasificarea rulează de două ori. Prima trecere citește propriile /Type și /Subtype ale fiecărui obiect și ia victoriile ușoare: /ObjStm, /Subtype /Image, /Subtype /Form, /Type /Font și /Type /FontDescriptor, /Metadata, /EmbeddedFile și /Filespec, /StructTreeRoot și /StructElem, /Annot, /Page și /Pages. Tot restul aterizează provizoriu la Other. A doua trecere parcurge apoi partea referentă și suprascrie: fiecare dicționar de pagină reatribuie /Contents la fluxuri de conținut, intrările sale /Annots la adnotări și /Thumb la imagini, în timp ce fiecare dicționar de font parcurge propriul lanț de descriptori
// 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]));
Citirea raportului și alegerea următorului pas
Citiți cotele mai întâi, numărul de obiecte al doilea și tratați orice diferență mare între ele ca pe un semnal. Un PDF modern pune majoritatea dicționarelor sale mici în fluxuri de obiecte, așa că Page tree și Structure tree afișează frecvent zeci de obiecte contra a aproape zero octeți — costul lor real a fost plicuit în linia Object streams. Dacă Object streams este ea însăși mare, fișierul este dens în structură asemănătoare metadatelor, nu în conținut, iar pârghia este eliminarea de obiecte, nu comprimarea lor. Fluxurile de aspect ale adnotărilor se comportă similar: poartă /Subtype /Form, astfel încât un document intens ștampilat își arată greutatea sub Form XObjects, în timp ce linia Annotations rămâne mică
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;
Două aspecte de formatare contează dacă analizați procentele în loc să le afișați. Separatorul zecimal este întotdeauna un punct literal, indiferent de setările regionale ale mașinii, așa că analiza cu FormatSettings ambientale pe o stație de lucru germană sau franceză va eșua sau, mai rău, va citi greșit. Iar zerourile finale sunt tăiate, așa că o categorie care deține exact 40% din octeți se afișează ca 40, nu 40.0 — nu presupuneți niciodată un număr fix de zecimale. Cu cota în mână, direcționarea este mecanică: o cotă dominantă la Images indică spre DownsampleImages, o cotă dominantă la Font programs spre SubsetEmbeddedFonts, iar Content streams voluminoase spre CompressContent
Ce nu vă spune deliberat auditul
Totalul este o sumă peste obiectele indirecte, iar un fișier PDF este puțin mai mult decât obiectele sale. Antetul fișierului, trailerul, spațiile albe dintre obiecte și un tabel cross-reference clasic nu sunt obiecte indirecte, așa că acei octeți nu sunt atribuiți nimic, iar totalul auditului ajunge puțin sub dimensiunea de pe disc. Un flux cross-reference este diferit — este un obiect real cu /Type /XRef, așa că într-un fișier modern acei octeți apar, în categoria Other. Niciunul dintre comportamente nu este un defect, dar dacă reconciliați auditul cu un număr de octeți din sistemul de fișiere, de acolo vine diferența
Merită enunțate clar încă două limite. Întâi, cifrele descriu un fișier care a fost încărcat, nu unul aflat în curs de creare: pentru obiectele construite în memorie care nu au încă o lungime stocată, dimensiunea revine la ieșirea serializată cu o alocație nominală pentru dicționarul de flux, ceea ce este o estimare a scrierii finale, nu o măsurătoare. Auditați după o salvare și reîncărcare dacă doriți cifre exacte. În al doilea rând, o linie Other grasă este o constatare, nu un raport de bug — de obicei înseamnă obiecte orfane pe care nimic nu le mai referențiază, ceea ce este o sarcină pentru colectarea de gunoi mark-and-sweep, nu pentru vreo trecere de compresie
Folosit astfel, auditul schimbă natura conversației. În loc să ghiciți despre raportul de 80 MB, îl deschideți, rulați un apel și citiți că imaginile sunt 8%, programele de font sunt 61%, iar documentul încorporează nouă programe de font complete pentru un stil casei care folosește trei fonturi. Acesta este un răspuns rezolvabil cu o cifră atașată. AuditDocumentSpace, împreună cu trecerile de optimizare spre care vă îndreaptă, vine cu losLab PDF Library pentru Delphi și C++Builder, unde paginile de referință documentează lista completă de categorii și API-ul de listă de șiruri din jurul ei