Tekninen artikkeli

HotPDF CompressDocument: tiiviit fonttiosajoukot Delphissä

HotPDF:n THotPDF.CompressDocument on yksi kytkin, joka saa BeginDocin tuottamaan pienimmän häviöttömän PDF:n, jonka komponentti osaa kirjoittaa: FlateDecode korkeimmalla tasolla, ristiviittausstreami objektistreameilla, fonttisubsetting ja tiiviit fonttiosajoukot, jotka numeroivat säästetyt glyyfit uudelleen eksplisiittisen /CIDToGIDMapin taakse. EndDoc laittaa sitten omat asetuksesi takaisin. Kolmisivuinen Arial- ja SimSun-testiasiakirja putosi arvosta 10.2 MB arvoon 20 KB identtisellä renderöinnillä

Mitä CompressDocument oikeasti kytkee päälle?

CompressDocument ohittaa kuusi kirjoittajan asetusta sekä objektistreamikaton yhden asiakirjan ajaksi ja palauttaa ne kaikki sen jälkeen. BeginDocissa, ennen kuin PDF-versio vakiintuu, HotPDF kirjaa arvosi ja asettaa Compressionin arvoon cmFlateDecode, CompressionLevelin arvoon clMaximum, kytkee päälle EnableFontSubsettingin ja CompactFontSubsettingin sekä ottaa käyttöön UseXRefStreamin ja UseObjectStreamsin (ISO 32000-1 §7.5.7 ja §7.5.8). Objektistreamit tarvitsevat PDF 1.5:n, joten vanhempi Version nostetaan arvoon 1.5, jos se ei ole lukittu. PDF/A-1 kieltää molemmat rakenteet, joten PDF/A-1-asiakirja pitää klassisen ristiviittaustaulukkonsa ja saa vain Flate- ja fonttityön. Kuvat jätetään täsmälleen sellaisiksi kuin upotit ne

Kaavio HotPDF CompressDocumentin elinkaaresta Delphissä: BeginDoc kirjaa kirjoittajan omat arvot, ohittaa kuusi asetusta mukaan lukien Compression ja UseObjectStreams yhden asiakirjan ajaksi, ja EndDoc palauttaa jokaisen lainatun arvon sen uloimmassa finallyssä, kun taas CompressDocument-ominaisuus itsessään pysyy True-na
Kuutta kirjoittajan asetusta ja objektistreamikattoa lainataan täsmälleen yhden asiakirjan ajaksi ja ne palautetaan, kun EndDoc ajaa, joten epäonnistunut raportti ei koskaan jätä komponenttia jumiin maksimipakkaukseen
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := 'invoice-2026-1042.pdf';
    Pdf.CompressDocument := True;    // BeginDoc ottaa käyttöön, EndDoc kumoaa
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-1042');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Palautus tapahtuu EndDocin uloimmassa finallyssä, joten poikkeus raportin puolivälissä ei jätä pitkäikäistä komponenttia jumiin maksimipakkaukseen seuraavaa työtä varten. CompressDocument-ominaisuus itsessään pysyy Truena; vain kuusi sen lainaamaa asetusta menevät takaisin. Versioon suhtaudutaan huolellisemmin. HotPDF kumoaa oman nostonsa arvoon 1.5 vain, jos asiakirja yhä päättyy arvoon 1.5, joten kun toinen ominaisuus työnsi tiedoston arvoon 1.6 ajon aikana (esimerkiksi upotettu OpenType-fontti), korkeampi versio jää, täsmälleen niin kuin se jäisi ilman pakkaustakin

Miksi fonttiosajoukot ovat yhä suuria ilman tiivistystä?

Klassinen TrueType-osajoukko pudottaa ääriviivat, joita et koskaan piirrä, mutta pitää jokaisen glyyfi-ID:n paikallaan, ja juuri tuo numerointi pitää sen raskaana. Sisältöstreami näyttää CIDit, jotka vastaavat alkuperäisiä GIDejä, joten osajoukon on pidettävä loca-offset ja hmtx-merkintä jokaisesta slotista korkeimpaan säästämäänsä glyyfiin asti, tyhjä tai ei. Latinalaiselle fontille se ylikustannus on melua. CJK-fontille kuten SimSun, jonka ideografit istuvat syvällä hyvin suuressa glyyfitaulukossa, kaksi kiinalaista merkkiä raahaa mukanaan koko fontin kokoisia taulukoita. Kirjoituksen fonttiosajoukon sulkemissäännöt muodostetuille glyyfeille päättävät, mitkä glyyfit selviävät; tiivistys on siitä, paljonko selviytyjät maksavat

CompactFontSubsetting numeroo säästetyt glyyfit uudelleen tiheäksi alueeksi nollasta alkaen ja kirjoittaa /CIDToGIDMap-streamin CIDFontille, jonka ISO 32000-1 §9.7.4.2 määrittelee kaksitavuisten GIDien taulukoksi, jonka CID indeksoi. Tuo taulukko on koko temppu. Sisältöstreamit, /W-leveystaulukko ja ToUnicode-CMap pitävät kaikki alkuperäiset CIDit, joten mikään jo kirjoitettu ei joudu muuttumaan; vain haku CID:stä glyyfiin siirtyy mappiin. Testissä, joka innoitti ominaisuuden, SimSun kahdella merkillä meni arvosta 24.8 KB fonttidataa arvoon 3.1 KB

Vertailu harvan HotPDF:n fonttiosajoukon, joka säilyttää loca- ja hmtx-merkinnät jokaiselle alkuperäiselle glyyfi-ID:lle korkeimpaan säästettyyn GIDiin asti, ja CompactFontSubsetting-tulosteen välillä, joka numeroo säästetyt glyyfit tiheästi nollasta ja mappaa CIDit CIDToGIDMap-streamin kautta, sillä välin kun sisältöstreamit, /W ja ToUnicode pysyvät muuttumattomina
Uudellenumerointi siirtää kustannuksen pois fonttiohjelmasta yhteen pieneen mapistreamiin — kaksi SimSun-merkkiä putosi arvosta 24.8 KB arvoon 3.1 KB koskematta yhteenkään tavuun jo kirjoitetusta sisällöstä

Tiivistyksellä on jäykät rajat, ja se heikkenee hiljaa sen sijaan, että epäonnistuisi. HotPDF rakentaa tiiviit osajoukot vain Type 0 TrueType-fonteille, sekä niille, jotka asetetaan funktiolla SetFont subsetting päällä, että funktiolla RegisterUnicodeTTF rekisteröidylle fontille. Yksinkertainen TrueType-fontti löytää glyyfinsä fonttiohjelman sisäisen cmapin kautta, jonka uudellenumerointi rikkoisi, joten se pitää harvan osajoukon. OpenType-CFF-fonteilla ei ole tiivistyspolkuakaan. Epäonnistuva tiivis rakennus putoaa takaisin harvaan osajoukkoon poikkeuksen sijaan. Ominaisuus on oletukselta pois päältä, joten olemassa oleva tuloste pysyy tavuiltaan samana, kun taas PDF/A:n alla rekisteröity Unicode-fontti saa aina tiiviin osajoukon

Pdf.EnableFontSubsetting := True;
Pdf.CompactFontSubsetting := True;   // toimii myös ilman CompressDocumentia
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('SimSun', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, WideString('Total: '#$4E2D#$6587));
Pdf.EndDoc;

Miten pakattu kirjoittaja puristaa tiedostorakenteen?

Kun fontit ja streamit ovat pieniä, sanakirjoista ja ristiviittausdatasta tulee suurin jäljelle jäävä kustannus, joten CompressDocumentin takana oleva objektistreamikirjoittaja trimmaa myös niitä. Kirjoitus objektistreamit ja inkrementaaliset päivitykset kattaa konttimuodon itsensä; pakkauspolku lisää päälle neljä hienosäätöä:

  • Tiivis syntaksi ISO 32000-1 §7.2.2:n mukaan: välilyönti kirjoitetaan vain kahden tokenin väliin, jotka muuten juoksisivat yhteen tavallisina merkkeinä, joten /Type /Page muuttuu muodoksi /Type/Page
  • Ristiviittausstreamin kentät ottavat minkä tahansa leveyden, jonka §7.5.8.2 sallii, joten alle 16 MB:n tiedosto tallettaa jokaisen offsetin 3 tavuun 4:n sijaan
  • Jopa 250 objektia menee jokaiseen objektistreamiin tavallisen 100 sijaan, ellei aseta omaa kattoaan funktiolla ConfigureAdaptiveObjectStreamPacking
  • Kun tiedosto ei ole salattu, myös Catalog ja Info-sanakirja pakataan objektistreameihin; salattu tuloste pitää ne ylimmällä tasolla

Tiivis syntaksi tuli ansan mukanaan, joka on syytä tuntea, jos laajennat kirjoittajaa. Allekirjoitus täyttää allekirjoituksen tiedoston kirjoittamisen jälkeen etsimällä tavuista litteraalit paikanpitäjät /ByteRange ( ja /Contents <, ja tiivis kirjoitusasu kääntäisi ne muotoihin /ByteRange( ja /Contents<, joita haku ei koskaan löydä. Allekirjoitussanakirjat (Type Sig tai DocTimeStamp, FT Sig) ja salausosanakirja pitävät siksi välilyönnillisen asettelun. Asiaan liittyvä vika vaikutti versioihin ennen v2.766.41: jokainen objektistreamitallennus, CompressDocument mukaan lukien, alkoi kahdella %PDF--otsikkorivillä, joten päivitä, jos tiukka validaattori liputtaa tulosteesi

Voitko pakata PDF:n, joka on jo ladattu?

Kyllä, optio-overloadin CompressLoadedDocument(Options, Info) kautta, joka ajaa samat häviöttömät vaiheet olemassa olevalle tiedostolle. Arvolla THPDFLoadedDocumentCompressionOptions.Default se poistaa käyttämättömät sivuresurssit, yhdistää identtiset fontit ja formit, tekee upotetuista fonteista osajoukot tiivit osajoukot päällä, pakkaa uudelleen suodattamattomat, Flate-, LZW-, ASCII- ja RunLength-streamit Flatella, kun tulos on pienempi, ja saa seuraavan tallennuksen käyttämään objektistreameja. HighRatioFlate on oletukselta pois, ja objektistreamit ohitetaan PDF/A-1:llä ja inkrementaalisissa tallennuksissa. Parametriton CompressLoadedDocument-overload on vanhempi, kapeampi kutsu, joka vain Flate-pakkaa pakaamattomat streamit

HotPDF CompressLoadedDocumentin kulku Delphissä: kutsu poistaa käyttämättömät sivuresurssit, yhdistää identtiset fontit ja formit, tekee upotetuista fonteista tiiviit osajoukot, pakkaa streamit uudelleen Flatella vain, kun tulos on pienempi, ja kytkee objektistreamit päälle seuraavaa tallennusta varten, kun taas allekirjoituskentät laukaisevat RefusedBySignaturePolicyn ja jättävät tiedoston koskemattomaksi
Jokainen vaihe kirjoittaa uudelleen tavuja, joita allekirjoitus kattaa, joten koko asiakirja torjutaan, ellet nimenomaisesti salli mitätöintiä — Info.BytesSaved laskee silloin yhteen vain resurssi-, fontti- ja streamityön
var
  Doc: THotPDF;
  Options: THPDFLoadedDocumentCompressionOptions;
  Info: THPDFLoadedDocumentCompressionInfo;
begin
  Doc := THotPDF.Create(nil);
  try
    Doc.AutoLaunch := False;
    Doc.LoadFromFile('quarterly-report.pdf');
    Options := THPDFLoadedDocumentCompressionOptions.Default;
    Doc.CompressLoadedDocument(Options, Info);
    if Info.RefusedBySignaturePolicy then
      Writeln(Format('Left untouched: %d signature fields', [Info.SignatureCount]))
    else
    begin
      Writeln(Format('Compact fonts: %d, stream bytes saved: %d',
        [Info.Fonts.CompactSubsetFontCount, Info.BytesSaved]));
      Doc.SaveLoadedDocument('quarterly-report-compact.pdf');
    end;
  finally
    Doc.Free;
  end;
end;

Kaksi rajaa merkitsee ladatulla polulla. Jokainen vaihe kirjoittaa uudelleen tavuja, joita allekirjoitus kattaa, joten allekirjoituskenttiä sisältävä asiakirja torjutaan kokonaisuutena: kutsu palauttaa arvon 0, asettaa lipun RefusedBySignaturePolicy eikä muuta mitään, ellet aseta AllowSignatureInvalidationia, jonka jälkeen Info.SignaturesInvalidated kertoo, mistä luopuit. Tiivistys on täällä myös varovaisempi kuin luontipolulla. HotPDF tiivistää vain fonttiohjelmat, joita käyttävät yksinomaan CIDFontType2-fontit Identity-/CIDToGIDMapilla, jossa CID vastaa GIDiä, ja ohittaa ohjelmat, joilla on jo olemassa oleva mapistream, /CIDSet tai väriglyyfitaulukot kuten COLR, sbix, CBDT tai SVG, koska tiivis uudelleenrakennus pudottaisi värikerrokset. Huomaa myös, että Info.BytesSaved laskee yhteen vain resurssi-, fontti- ja streamivaiheet; objektistreamin hyöty ilmenee, kun tiedosto kirjoitetaan

Mitä tuloksia kannattaa odottaa käytännössä?

Hyödyt seuraavat sitä, kuinka suuri osa tiedostosta on pakaamatonta rakennetta ja ylikokoista fonttidataa, eivät sivumäärää. Kolmisivuinen Arial- ja SimSun-näyte kutistui arvosta 10.2 MB arvoon 20 KB, kun se generoitiin arvolla CompressDocument, ja arvosta 10.2 MB arvoon 19.8 KB, kun pakaamaton alkuperä ladattiin ja ajettiin läpi funktiolla CompressLoadedDocument, identtisellä renderöinnillä molemmin puolin. Jo tiivis PDF tuskin liikahtaa: regressiojoukossa tällaiset tiedostot säästivät alueella -0.07 % – +0.06 % alkuperäisestä koostaan. Valokuvapainotteiset tiedostot hyötyvät vähän, koska kumpikaan polku ei kosketa kuvadataa

Jos generoit samat CJK-raportit joka yö, yhdistä tiiviit osajoukot pysyvään fonttiosajoukovälimuistiin levyltä, jotta subsetting-työtä ei toisteta jokaista ajoa kohden, ja diffaa pakatut tulosteet objektisisällön eikä tavujen perusteella, sillä yksi muuttunut kenttä pakkaa koko objektistreamin uudelleen. Täysi ominaisuus- ja tietuereferenssi on HotPDF Delphi PDF component -tuotesivulla