Teknisk artikkel

Revisjon av PDF-filstørrelse i Delphi: byte-fordeling etter kategori

For å finne ut hvor en PDF-filstørrelse egentlig går, eksponerer losLab PDF Library AuditDocumentSpace, som klassifiserer hvert indirekte objekt i tolv kategorier — bilder, fontprogrammer, fontordbøker, innholdsstrømmer, form-XObjects, objektstrømmer, innebygde filer, metadata, strukturtre, annotasjoner, sidetre, annet — og rapporterer objektantall, lagrede bytes og prosentandel for hver

Situasjonen dette finnes for er velkjent. En 40-siders rapport kommer ut av generatoren din på 80 MB, kunden spør hvorfor, og alt du kan tilby er en gjetning. Sannsynligvis bildene. Kanskje fontene. Så slår du på nedsampling, sender den, og filen lander på 74 MB fordi den reelle vekten lå et helt annet sted. Vår følgeartikkel om font-subsetting og bildenedsampling dekker hvordan du krymper en PDF; denne dekker steget som burde komme først, nemlig å måle det du er i ferd med å krympe

Hvorfor måle før du komprimerer?

Fordi de tre standard optimaliseringspassene har vidt forskjellig gevinst på en gitt fil, og ingenting ved filen forteller deg hvilken som gjelder før du teller. Å subsette fonter i et dokument der fontene allerede utgjør 2% av bytene er en ettermiddag brukt på å flytte en avrundingsfeil. Nedsampling av bilder i en fil hvis bulk er ukomprimerte innholdsstrømmer gir samme skuffelse. Optimalisereren er ikke den vanskelige delen — hvert bibliotek har en. Å vite hvilken optimaliserer som skal rettes mot denne filen er den vanskelige delen, og det er et regnskapsspørsmål, ikke et komprimeringsspørsmål. En revisjon fanger også opp tilfellene der ingen optimaliserer er svaret: en fil som viser seg å være 60% innebygde vedlegg trenger ikke bedre komprimering, den trenger en samtale om hvorvidt de vedleggene hører hjemme i dokumentet, og en fil som er 30% strukturtre betaler for tilgjengelighetsmerking, som vanligvis er en bevisst kostnad du ikke bør fjerne stilltiende. Når bytene er tilordnet, tar du en produktbeslutning med tall bak i stedet for å gripe etter hvilken bryter som er nærmest

Hva den tolv-kategoris rapporten inneholder

AuditDocumentSpace returnerer et strenglistehåndtak i stedet for en record, slik at rapporten overlever den flate DLL- og COM-fasaden uendret. Listen inneholder en Total,Objects,Bytes,100.0-oppsummeringslinje etterfulgt av nøyaktig tolv Category,Objects,Bytes,Percent-linjer i en fast rekkefølge som er del av kontrakten: Images, Font programs, Font dictionaries, Content streams, Form XObjects, Object streams, Embedded files, Metadata, Structure tree, Annotations, Page tree, Other. Tretten linjer, alltid, selv når en kategori er tom

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;

Én Delphi-detalj i den løkken vil bite deg nøyaktig én gang. GetStringListItem bruker 1-baserte elementindekser, i tråd med GetStringListCount, og en indeks utenfor rekkevidden returnerer en tom streng i stedet for å kaste et unntak. Skriv løkken for I := 0 to Count - 1 av vane, og du får en blank første linje, en stille droppet siste linje, og ikke noe unntak noe sted som forteller deg at indekseringen er feil. Rapporten i seg selv vil se nesten riktig ut, som er den verste feilmodusen et diagnoseverktøy kan ha

Hvorfor bruker revisjonen lagret lengde i stedet for dekodet størrelse?

Fordi lagret lengde både er tallet du vil ha og tallet som er billig å hente. Hvert indirekte objekt bærer TPDFIndObj.FLength, den rå bytelengden objektet opptar i filen slik det er parset. Å bruke den betyr at et 900 KB DCTDecode-bilde rapporteres som 900 KB — bytene det koster deg på disk — i stedet for de 40 MB med RGB-samples det dekodes til. Det betyr også at revisjonen aldri trenger å dekode noe: lat-innlastede objekter forblir late, filtre forblir ukjørte, og å revidere en 500 MB-fil er et pass over objektheadere i stedet for en full dekomprimeringssyklus

Den andre regelen er et forsvar mot dobbelttelling. Når et objekt lever inne i en komprimert objektstrøm, indikert av et FObjStrNum som ikke er null, registreres byteantallet som null. Lagringen er allerede betalt for én gang av containerstrømmen, som ISO 32000-1 §7.5.7 definerer som en /Type /ObjStm-strøm som holder mange objekter i én Flate-komprimert nyttelast. Å belaste hvert medlem sin egen andel og deretter belaste containeren igjen ville blåst opp totalen forbi den reelle filstørrelsen. Dette har en direkte konsekvens for hvordan du leser utdataen, dekket nedenfor og mer inngående i vår artikkel om objektstrømmer og kryssreferansestrømmer

Hvorfor kan et fontprogram ikke klassifisere seg selv?

Fordi en TrueType-fontfil innebygd i en PDF ikke har noen markør som sier det. ISO 32000-1 §9.8.1 definerer det innebygde fontprogrammet som verdien av /FontFile, /FontFile2 eller /FontFile3 i en fontdeskriptor, og strømordboken i den andre enden av den referansen bærer /Length1 og filternøkler, men ingen /Type og ingen /Subtype som identifiserer den som en font. Sett isolert er det en anonym binærstrøm. Bare deskriptoren som peker på den vet hva den er. Den samme asymmetrien viser seg for annotasjoner: §12.5.2 gjør /Type /Annot valgfri i en annotasjonsordbok, så det pålitelige signalet er medlemskap i en side-/Annots-array, ikke ordboken selv

Så klassifiseringen kjøres to ganger. Det første passet leser hvert objekts egen /Type og /Subtype og tar de enkle gevinstene: /ObjStm, /Subtype /Image, /Subtype /Form, /Type /Font og /Type /FontDescriptor, /Metadata, /EmbeddedFile og /Filespec, /StructTreeRoot og /StructElem, /Annot, /Page og /Pages. Alt annet havner foreløpig i Other. Det andre passet går deretter gjennom den refererende siden og overstyrer: hver sideordbok tilordner sin /Contents på nytt til innholdsstrømmer, sine /Annots-oppføringer til annotasjoner, og sin /Thumb til bilder, mens hver fontordbok går gjennom sin egen deskriptorkjede

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

Lese rapporten og velge neste trekk

Les andelene først, objektantallene deretter, og behandle et stort gap mellom dem som et signal. En moderne PDF putter de fleste av sine små ordbøker inne i objektstrømmer, så Page tree og Structure tree viser rutinemessig dusinvis av objekter mot nesten null bytes — deres reelle kostnad er foldet inn i Object streams-linjen. Hvis Object streams selv er stor, er filen tett med metadata-lignende struktur snarere enn innhold, og løftestangen er å beskjære objekter, ikke å komprimere dem. Utseendestrømmer for annotasjoner oppfører seg likende: de bærer /Subtype /Form, så et tungt stemplet dokument viser vekten sin under Form XObjects mens Annotations-linjen forblir liten

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;

To formateringsfakta betyr noe hvis du parser prosentandelene i stedet for å bare vise dem. Desimalskilletegnet er alltid et bokstavelig punktum uansett maskinens lokalitet, så parsing med det omgivende FormatSettings på en tysk eller fransk arbeidsstasjon vil feile eller, verre, mistolke. Og etterfølgende nuller er kuttet, så en kategori som holder nøyaktig 40% av bytene skriver ut som 40, ikke 40.0 — anta aldri en fast desimalplassering. Med andelen i hånden er rutingen mekanisk: en dominerende Images-andel peker mot DownsampleImages, en dominerende Font programs-andel mot SubsetEmbeddedFonts, og fyldige Content streams mot CompressContent

Hva revisjonen bevisst ikke forteller deg

Totalen er en sum over indirekte objekter, og en PDF-fil er litt mer enn sine objekter. Filheaderen, traileren, mellomobjekt-mellomrom og en klassisk kryssreferansetabell er ikke indirekte objekter, så de bytene tilordnes ingenting og revisjonstotalen lander litt under størrelsen på disk. En kryssreferansestrøm er annerledes — den er et reelt objekt med /Type /XRef, så i en moderne fil dukker de bytene faktisk opp, i Other-kategorien. Ingen av oppførselene er en defekt, men hvis du avstemmer revisjonen mot et byteantall fra filsystemet, er det derfra gapet kommer

To grenser til er verdt å nevne tydelig. For det første beskriver tallene en fil som ble lastet inn, ikke en som er under forfatterskap: for objekter bygget i minnet som ennå ikke har en lagret lengde, faller størrelsen tilbake til den serialiserte utdataen med en nominell tillatelse for strømordboken, som er et estimat av den eventuelle skrivingen snarere enn en måling. Revider etter en lagre-og-last-på-nytt hvis du vil ha eksakte tall. For det andre er en fet Other-linje et funn, ikke en feilrapport — det betyr vanligvis foreldreløse objekter som ingenting lenger refererer til, som er en jobb for mark-and-sweep søppelinnsamling snarere enn for noe komprimeringspass

Brukt på denne måten endrer revisjonen formen på samtalen. I stedet for å gjette på 80 MB-rapporten åpner du den, kjører ett kall, og leser at bilder er 8%, fontprogrammer er 61%, og dokumentet bygger inn ni fullstendige fontprogrammer for en husstil som bruker tre snitt. Det er et fikserbart svar med et tall festet til seg. AuditDocumentSpace, sammen med optimaliseringspassene den peker deg mot, leveres i losLab PDF Library for Delphi og C++Builder, hvor referansesidene dokumenterer hele kategorilisten og strenglist-API-et rundt den