Tekninen artikkeli

PDF-tiedostokoon auditointi Delphissä: tavujakauma kategorioittain

Selvittääkseen, mihin PDF-tiedoston koko todella menee, losLab PDF Library tarjoaa AuditDocumentSpace-funktion, joka luokittelee jokaisen epäsuoran objektin kahteentoista kategoriaan — kuvat, fonttiohjelmat, fonttisanakirjat, sisältövirrat, lomake-XObjectit, objektivirrat, upotetut tiedostot, metadata, rakennepuu, merkinnät, sivupuu, muut — ja raportoi kunkin objektimäärän, tallennetut tavut ja prosenttiosuuden

Tilanne, jota varten tämä on olemassa, on tuttu. 40-sivuinen raportti tulee generaattoristasi ulos 80 megatavun kokoisena, asiakas kysyy miksi, ja ainoa mitä voit tarjota on arvaus. Todennäköisesti kuvat. Ehkä fontit. Joten kytket kuvien alinäytteistyksen päälle, toimitat sen, ja tiedosto päätyy 74 megatavuun, koska todellinen paino oli aivan muualla. Rinnakkaisartikkelimme fonttien osajoukkojen muodostamisesta ja kuvien alinäytteistyksestä käsittelee, miten PDF pienennetään; tämä käsittelee vaiheen, jonka pitäisi tulla ensin, eli sen mittaamisen, mitä olet aikeissa pienentää

Miksi mitata ennen pakkaamista?

Koska kolmella vakio-optimointivaiheella on villisti erilaiset tuotot millä tahansa tietyllä tiedostolla, eikä tiedosto itsessään kerro, mikä niistä pätee, ennen kuin lasket. Fonttien osajoukkojen muodostaminen asiakirjassa, jonka fontit ovat jo 2 % sen tavuista, on iltapäivä kulutettuna pyöristysvirheen siirtämiseen. Kuvien alinäytteistys tiedostossa, jonka pääosa on pakkaamattomia sisältövirtoja, tuottaa saman pettymyksen. Optimoija ei ole vaikea osa — jokaisella kirjastolla on sellainen. Sen tietäminen, mikä optimoija tähän tiedostoon kohdistetaan, on vaikea osa, ja se on kirjanpitokysymys, ei pakkauskysymys. Auditointi ottaa kiinni myös tapaukset, joissa vastaus ei ole mikään optimoija: tiedosto, joka osoittautuu 60-prosenttisesti upotetuiksi liitteiksi, ei tarvitse parempaa pakkausta, se tarvitsee keskustelun siitä, kuuluvatko nuo liitteet asiakirjaan, ja tiedosto, joka on 30-prosenttisesti rakennepuuta, maksaa saavutettavuusmerkinnöistä, mikä on yleensä tarkoituksellinen kustannus, jota ei pidä hiljaa riisua pois. Kun tavut on kohdennettu, teet tuotepäätöksen numeroiden tuella sen sijaan, että tarttuisit lähimpänä olevaan kytkimeen

Mitä kahdentoista kategorian raportti sisältää

AuditDocumentSpace palauttaa merkkijonolistan kahvan tietueen sijaan, joten raportti selviää tasaisen DLL:n ja COM-fasadien läpi muuttumattomana. Lista pitää sisällään Total,Objects,Bytes,100.0-yhteenvetorivin, jota seuraa täsmälleen kaksitoista Category,Objects,Bytes,Percent-riviä kiinteässä järjestyksessä, joka on osa sopimusta: Images, Font programs, Font dictionaries, Content streams, Form XObjects, Object streams, Embedded files, Metadata, Structure tree, Annotations, Page tree, Other. Kolmetoista riviä, aina, vaikka kategoria olisi tyhjä

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;

Yksi Delphi-yksityiskohta tuossa silmukassa puree täsmälleen kerran. GetStringListItem käyttää yhdestä alkavia alkioindeksejä, sopien yhteen GetStringListCount:n kanssa, ja alueen ulkopuolinen indeksi palauttaa tyhjän merkkijonon poikkeuksen nostamisen sijaan. Kirjoita silmukka tottumuksesta muotoon for I := 0 to Count - 1, ja saat tyhjän ensimmäisen rivin, hiljaa pudonneen viimeisen rivin, eikä yhtään poikkeusta missään kertomassa, että indeksointi on väärin. Raportti itsessään näyttää lähes oikealta, mikä on pahin vikamoodi, joka diagnostiikkatyökalulla voi olla

Miksi auditointi käyttää tallennettua pituutta puretun koon sijaan?

Koska tallennettu pituus on sekä luku, jonka haluat, että luku, joka on halpa hankkia. Jokainen epäsuora objekti kantaa TPDFIndObj.FLength-arvon, raa'an tavupituuden, jonka objekti vie tiedostossa jäsennettynä. Sen käyttäminen tarkoittaa, että 900 kilotavun DCTDecode-kuva raportoidaan 900 kilotavuna — tavuina, jotka se maksaa sinulle levyllä — eikä 40 megatavuna RGB-näytteitä, joiksi se puretaan. Se tarkoittaa myös, ettei auditoinnin koskaan tarvitse purkaa mitään: laiskasti ladatut objektit pysyvät laiskoina, suodattimet pysyvät ajamattomina, ja 500 megatavun tiedoston auditointi on läpikäynti objektien otsikoiden yli täyden purkamiskierroksen sijaan

Toinen sääntö on kaksinkertaisen laskennan puolustus. Kun objekti asuu pakatun objektivirran sisällä, mikä ilmenee nollasta poikkeavana FObjStrNum-arvona, sen tavumäärä kirjataan nollaksi. Sen tallennustila on jo maksettu kerran säilövirran toimesta, jonka ISO 32000-1 §7.5.7 määrittelee /Type /ObjStm-virraksi, joka pitää sisällään monta objektia yhdessä Flate-pakatussa hyötykuormassa. Jokaisen jäsenen veloittaminen omasta osuudestaan ja sitten säilön veloittaminen uudelleen paisuttaisi kokonaismäärän todellisen tiedostokoon yli. Tällä on suora seuraus siihen, miten tulosta luetaan, mikä käsitellään alla ja tarkemmin artikkelissamme objektivirrat ja ristiviittausvirrat

Miksi fonttiohjelma ei voi luokitella itseään?

Koska PDF:ään upotetulla TrueType-fonttitiedostolla ei ole merkkiä, joka sanoisi niin. ISO 32000-1 §9.8.1 määrittelee upotetun fonttiohjelman arvoksi /FontFile, /FontFile2 tai /FontFile3 fonttikuvaajassa, ja virtasanakirja tuon viittauksen toisessa päässä kantaa /Length1-avaimen ja suodatinavaimet, mutta ei /Type-avainta eikä /Subtype-avainta, joka tunnistaisi sen fontiksi. Yksinään tarkasteltuna se on nimetön binäärivirta. Vain kuvaaja, joka osoittaa siihen, tietää, mikä se on. Sama epäsymmetria ilmenee merkinnöissä: §12.5.2 tekee /Type /Annot:sta valinnaisen merkintäsanakirjassa, joten luotettava signaali on jäsenyys sivun /Annots-taulukossa, ei sanakirja itse

Joten luokittelu ajetaan kahdesti. Ensimmäinen kierros lukee jokaisen objektin oman /Type- ja /Subtype-arvon ja ottaa helpot voitot: /ObjStm, /Subtype /Image, /Subtype /Form, /Type /Font ja /Type /FontDescriptor, /Metadata, /EmbeddedFile ja /Filespec, /StructTreeRoot ja /StructElem, /Annot, /Page ja /Pages. Kaikki muu päätyy alustavasti Otheriin. Toinen kierros kävelee sitten viittaavaa puolta ja korvaa: jokainen sivusanakirja kohdistaa /Contents:insa sisältövirroiksi, /Annots-merkintänsä merkinnöiksi ja /Thumb:insa kuviksi, kun taas jokainen fonttisanakirja kävelee oman kuvaajaketjunsa läpi

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

Raportin lukeminen ja seuraavan siirron valitseminen

Lue osuudet ensin, objektimäärät toiseksi, ja kohtele suurta kuilua niiden välillä signaalina. Moderni PDF sijoittaa suurimman osan pienistä sanakirjoistaan objektivirtoihin, joten Page tree ja Structure tree näyttävät säännönmukaisesti kymmeniä objekteja lähes nollaa tavua vasten — niiden todellinen kustannus on taitettu Object streams -riville. Jos Object streams on itsessään suuri, tiedosto on tiheä metadatan kaltaista rakennetta eikä sisältöä, ja vipu on objektien karsiminen, ei niiden pakkaaminen. Merkintöjen ulkoasuvirrat käyttäytyvät samoin: ne kantavat /Subtype /Form:in, joten runsaasti leimattu asiakirja näyttää painonsa Form XObjects -rivillä, kun taas Annotations-rivi pysyy pienenä

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;

Kaksi muotoiluseikkaa merkitsee, jos jäsennät prosenttiosuudet niiden näyttämisen sijaan. Desimaalierotin on aina kirjaimellinen piste koneen lokaalista riippumatta, joten jäsentäminen ympäröivällä FormatSettings-asetuksella saksalaisella tai ranskalaisella työasemalla epäonnistuu tai, pahempaa, tulkitaan väärin. Ja perässä olevat nollat leikataan, joten kategoria, joka pitää sisällään täsmälleen 40 % tavuista, tulostuu muodossa 40, ei 40.0 — älä koskaan oleta kiinteää desimaalipaikkaa. Kun osuus on käsissä, reititys on mekaanista: hallitseva Images-osuus osoittaa kohti DownsampleImages:ia, hallitseva Font programs -osuus kohti SubsetEmbeddedFonts:ia, ja tilaa vievät Content streams kohti CompressContent:ia

Mitä auditointi tarkoituksella ei kerro sinulle

Kokonaissumma on summa epäsuorien objektien yli, ja PDF-tiedosto on hieman enemmän kuin objektinsa. Tiedoston otsikko, loppuosa, objektien välinen tyhjä tila ja klassinen ristiviittaustaulukko eivät ole epäsuoria objekteja, joten nuo tavut kohdennetaan ei mihinkään, ja auditoinnin kokonaismäärä jää hieman levyllä olevan koon alle. Ristiviittausvirta on erilainen — se on todellinen objekti, jolla on /Type /XRef, joten modernissa tiedostossa nuo tavut näkyvät, Other-kategoriassa. Kumpikaan käyttäytyminen ei ole vika, mutta jos täsmäytät auditointia tiedostojärjestelmän tavumäärää vasten, sieltä ero tulee

Kaksi rajaa lisää on syytä sanoa selvästi. Ensinnäkin luvut kuvaavat tiedostoa, joka ladattiin, ei sellaista, jota parhaillaan laaditaan: objekteille, jotka rakennetaan muistissa ja joilla ei vielä ole tallennettua pituutta, koko palautuu serialisoituun tulosteeseen nimellisellä varauksella virtasanakirjalle, mikä on arvio lopullisesta kirjoituksesta eikä mittaus. Auditoi tallennuksen ja uudelleenlatauksen jälkeen, jos haluat tarkat luvut. Toiseksi, lihava Other-rivi on löydös, ei vikaraportti — se tarkoittaa yleensä orpoja objekteja, joihin mikään ei enää viittaa, mikä on työ merkitse-ja-pyyhi-roskienkeruulle minkä tahansa pakkausvaiheen sijaan

Näin käytettynä auditointi muuttaa keskustelun muotoa. Sen sijaan, että arvailisit 80 megatavun raporttia, avaat sen, ajat yhden kutsun, ja luet, että kuvat ovat 8 %, fonttiohjelmat 61 %, ja asiakirja upottaa yhdeksän täyttä fonttiohjelmaa talotyylille, joka käyttää kolmea kirjasinta. Se on korjattavissa oleva vastaus, johon liittyy luku. AuditDocumentSpace, yhdessä optimointivaiheiden kanssa, joihin se ohjaa sinut, toimitetaan losLab PDF Libraryn mukana Delphille ja C++Builderille, jossa viitesivut dokumentoivat täyden kategorialistan ja sen ympärillä olevan merkkijonolista-API:n