Technical Article

EndDoc-virhe, joka poisti hiljaisesti käytöstä fonttien osajoukkojen luomisen

Luo raportti, upota TrueType-fontti, ja tuloste avautuu oikein jokaisessa kokeilemassasi katseluohjelmassa. Glyyfit ovat oikein, teksti on valittavissa ja tiedosto on kelvollinen. Ainoa virheellinen asia on koko. Dokumentti, joka käytti muutamia kymmeniä latinalaisia merkkejä, kantaa mukanaan koko 350 KB:n fontin. Dokumentti, joka tulosti kappaleen kiinaa, kantaa 14 MB:n CJK-fonttia sen puolen megatavun siivun sijaan, jonka se tarvitsisi. Mitään poikkeusta ei nostettu, varoitusta ei kirjattu ja tiedosto läpäisi validoinnin. Tältä väärässä järjestyksessä oleva viimeistelyvaihe näyttää ulkoapäin: mikään ei epäonnistu, ja ainoa todiste on liian suuri luku.

Tämän aiheuttanut virhe oli HotPDF-komponentissa yhden julkaisulinjan ajan ja se on sittemmin korjattu. Siitä kannattaa kirjoittaa ei niinkään virheilmoituksena vaan opetuksena, koska virheen muoto on yleinen. Millä tahansa dokumenttimoottorilla on viimeistelyvaihe, joka muokkaa olioita juuri ennen niiden kirjoittamista, ja kyseisen vaiheen oikeellisuus riippuu täysin sen vaiheiden järjestyksestä suhteessa serialisointiin. Jos yksikin vaihe päätyy väärälle puolelle kirjoitusta, se ei tee mitään, hiljaisesti.

Mitä fontin osajoukkojen luomisen kuuluu tehdä

Osajoukko-fontti on se osa TrueType-tiedostosta, jota dokumentti todella käyttää. ISO 32000-1 §9.9 kuvailee, miten upotettu fonttiohjelma kulkee fontin kuvailijan viittaamassa virrassa, ja TrueType-ohjelmalle tämä virta on /FontFile2, jonka /Length1 ilmoittaa pakkaamattoman tavumäärän. Osajoukkojen muodostaminen kirjoittaa glyf- ja loca-taulukot uudelleen siten, että ne sisältävät vain dokumentin viittaamat glyyfit, numeroi glyyfitunnukset uudelleen ja lisää /BaseFont-nimen eteen kuusikirjaimisen tunnisteen, kuten ABCDEF+, merkitsemään fontti osajoukoksi, aivan kuten määrittely vaatii. Latinalainen kirjasinperhe, jonka osajoukko kutistuu kymmeneen tai viiteentoista kilotavuun, on ero kevyen PDF:n ja sellaisen välillä, joka toimittaa kokonaisen kirjasimen yhden otsikon vuoksi.

Ajankohdalla, jona tämä tapahtuu, on väliä. Osajoukkojen muodostaminen ei ole muunnos, jota sovelletaan jo levyllä oleviin tavuihin. Se muokkaa muistissa olevaa oliograafia: se kutistaa /FontFile2-virran sisällön, korjaa /Length1-arvon ja kirjoittaa /BaseFont-merkkijonon uudelleen. Kaiken tämän on oltava valmiina, kun serialisoija käy läpi graafin ja tuottaa tavut. Jos muokkaukset tapahtuvat tavujen kirjoittamisen jälkeen, ne päivittävät olioita, joita kukaan ei koskaan lue.

Oire ja miksi mikään ei varoittanut

Raportoitu käyttäytyminen oli täydet fontit tulosteessa ilman diagnostiikkaa. Käyttäjä, joka rekisteröi Unicode TrueType -fontin ja loi normaalin dokumentin, huomasi upotetun fonttiolion olevan samanpituinen kuin alkuperäinen .ttf-tiedosto, ja että /BaseFont-nimi ei sisältänyt kuusikirjaimista osajoukon etuliitettä. Tuloste ei koskaan kutistunut sellaisten ajojen välillä, jotka käyttivät kymmentä glyyfiä tai kymmentätuhatta glyyfiä.

Virheiden puuttuminen on se osa, joka tekee tästä virheluokasta kalliin. Väärään aikaan suoritettava osajoukkojen muodostusrutiini ajetaan silti. Se käy läpi kertyneen koodipisteiden käytön, rakentaa täysin oikean osajoukon ja soveltaa sitä muistissa olevaan oliograafiin. Sisäisesti työ on tehty ja kutsu palautuu puhtaasti. Ainoa virhe on se, että sen muokkaama oliograafi ei ole enää se, mitä kirjoitetaan, koska kirjoittaja on jo valmis. Kutsun tekijän näkökulmasta dokumentti luotiin ja tallennettiin ongelmitta, mikä on juuri se vaikutelma, jonka hiljainen epäonnistuminen antaa.

Juurisyy oli viimeistelyn järjestys

HotPDF-komponentissa sulkemistyö tapahtuu EndDoc-metodin sisällä. Osajoukkojen muodostusvaihe on sisäinen rutiini nimeltä BuildAndApplyUnicodeFontSubset. Se lukee dokumenttikohtaisen käytettyjen koodipisteiden joukon, jota pidetään bittikartassa, jonka tekstin tulostuspolku täyttää sitä mukaa kuin glyyfejä näytetään, kartoittaa jokaisen käytetyn koodipisteen välimuistissa olevan koodipisteestä-glyyfiin-taulukon kautta todelliseksi glyyfitunnukseksi ja kirjoittaa fonttiohjelman uudelleen kyseisen joukon ympärille. Kun Unicode TrueType -fontti rekisteröidään, tulostuspolku asettaa bitin käytettyjen koodipisteiden joukkoon jokaiselle piirtämälleen merkille, joten dokumentin sulkeutuessa moottori tietää tarkalleen, mitkä glyyfit osajoukon on säilytettävä.

Virhe oli siinä, että BuildAndApplyUnicodeFontSubset kutsuttiin sen jälkeen, kun SaveToStream tai SaveToFile oli jo serialisoinut dokumentin. Osajoukkojen muodostajan muokkaukset /FontFile2-virtaan, sen korjattu /Length1-arvo ja kuusikirjaiminen /BaseFont-etuliite laskettiin kaikki oliograafia vasten, joka oli jo muutettu tavuiksi. Korjaus oli yhden rivin uudelleenjärjestely: siirrettiin osajoukkojen muodostuskutsu ennen serialisointia, jotta kirjoittaja tulostaa osajoukkofontin alkuperäisen sijaan. Korjattu järjestys suorittaa osajoukkojen muodostuksen ensin ja serialisoi vasta sen jälkeen.

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
    Pdf.CurrentPage.TextOut(72, 760, 0, '报表标题 Report Heading');
    Pdf.EndDoc;                 // subsetting runs here, before the write
    Pdf.SaveToFile('Report.pdf');
  finally
    Pdf.Free;
  end;
end;

Kun järjestys on korjattu, mikään kutsuvassa koodissa ei muutu. Osajoukkojen muodostaminen on oletusarvoisesti käytössä, kun Unicode TrueType -fontti on rekisteröity. Rekisteröit fontin, aloitat dokumentin, piirrät ja lopetat sen, ja osajoukko rakennetaan käyttämistäsi glyyfeistä ennen kuin tavut poistuvat muistista.

Miksi yksi väärässä paikassa oleva vaihe on kokonainen luokka

Syy siihen, miksi tämä on opetuksen eikä vain alaviitteen arvoinen, on se, että EndDoc tuottaa listan lopetusvaiheista, ja jokainen niistä on herkkä sijainnilleen suhteessa kirjoittamiseen. Fonttien osajoukkojen muodostaminen on yksi. PDF/A-tuloste vaatii /CIDSet-virran, joka luettelee tarkalleen osajoukossa olevat glyyfitunnukset - tämän rajoituksen ISO 19005 asettaa, jotta validaattori voi varmistaa upotetun ohjelman vastaavan fontin kuvailijan ilmoittamaa; tämä virta tuotetaan samassa viimeistelyikkunassa ja se riippuu siitä, että osajoukko on rakennettu ensin. PDF/UA-1 vaatii ISO 14289-1 §7.18.3:n mukaisesti, että jokainen annotaation sisältävä sivu ilmoittaa /Tabs-avaimen arvolla /S, ja sisäinen rutiini nimeltä EnsurePDFUATabsOnAnnotatedPages leimaa tämän avaimen saman vaiheen aikana. Myös tulostustarkoituksen (output-intent) tarkistukset ajetaan siellä.

Sama järjestysvirhe, joka poisti osajoukkojen muodostuksen käytöstä, pudotti myös PDF/UA-sarkainjärjestysavaimen annotoiduilta sivuilta, koska kyseinen vaihe sijaitsi samalla väärällä puolella kirjoitusta. veraPDF ja PAC raportoivat puuttuvan /Tabs /S -avaimen Matterhorn-protokollan tarkistuspisteen 21-001 rikkomuksena. Joten yksittäinen väärään paikkaan sijoitettu kutsu ei ainoastaan kasvattanut tiedostokokoa; se rikkoi hiljaisesti saavutettavuuden vaatimuksen samaan aikaan, ilman mitään virheilmoitusta. Tämä on viimeistelyvaiheen vaara: sen vaiheet jakavat esiehdon, ja yksittäinen järjestysvirhe voi poistaa useita niistä kerralla käytöstä, vaikka jokainen kutsu palauttaa onnistumisen.

Miten hiljainen tulostusvirhe todellisuudessa havaitaan

Virhettä, joka ei nosta poikkeusta, ei havaita vain ohjelmaa ajamalla. Se havaitaan tarkastelemalla tulostetta ja vertaamalla sitä siihen, mitä syötteen olisi pitänyt tuottaa. Fonttien osajoukkojen muodostamiselle tarkistukset ovat konkreettisia. Vertaa tulostiedoston kokoa karkeaan odotukseen: dokumentin, joka käytti vain kourallista glyyfejä, ei pitäisi olla kokonaisen kirjasimen kokoinen. Avaa upotettu fonttiolio ja lue sen tavupituus; latinalaisen kirjasimen osajoukko /FontFile2 on vain murto-osa alkuperäisestä tiedostosta. Lue /BaseFont-nimi ja varmista, että kuusikirjaiminen etuliite on olemassa, sillä sen puuttuminen on suora merkki siitä, ettei osajoukkoa sovellettu.

var
  Pdf: THotPDF;
  Output: TMemoryStream;
begin
  Output := TMemoryStream.Create;
  try
    Pdf := THotPDF.Create(nil);
    try
      Pdf.RegisterUnicodeTTF('C:\Fonts\DejaVuSans.ttf');
      Pdf.BeginDoc;
      Pdf.CurrentPage.SetFont('DejaVu Sans', [], 11);
      Pdf.CurrentPage.TextOut(72, 760, 0, 'Subset me');
      Pdf.EndDoc;
      Pdf.SaveToStream(Output);
    finally
      Pdf.Free;
    end;
    // A few glyphs from a ~700 KB face must not yield a multi-hundred-KB stream.
    if Output.Size > 100 * 1024 then
      raise Exception.Create('Font subset did not shrink the output');
  finally
    Output.Free;
  end;
end;

PDF/A-tulosteelle tarkistus on vieläkin tarkempi, koska validaattori tekee työn puolestasi. Aseta vaatimustenmukaisuustaso ja aja tulos veraPDF-ohjelman läpi: puuttuva /CIDSet tai osajoukko, joka ei vastaa kuvailijaa, raportoidaan hylättynä ehtona sen sijaan, että se jäisi silmämääräisesti havaittavaksi. Vaatimustenmukaisuuden kytkimet, jotka ohjaavat tätä viimeistelytyötä, ovat dokumentin ominaisuuksia. PDFACompliance ottaa merkkijonon, kuten '2B' PDF/A-2 Level B:lle, ja PDFUACompliance on totuusarvo, joka ottaa käyttöön merkityn PDF:n ja sarkainjärjestyksen vaatimukset.

Pdf := THotPDF.Create(nil);
try
  Pdf.PDFACompliance := '2B';     // PDF/A-2 Level B, drives /CIDSet emission
  Pdf.PDFUACompliance := True;    // stamps /Tabs /S on annotated pages
  Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
  Pdf.BeginDoc;
  Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
  Pdf.CurrentPage.TextOut(72, 760, 0, '合规报告');
  Pdf.EndDoc;
  Pdf.SaveToFile('Report_PDFA.pdf');
finally
  Pdf.Free;
end;

Ohjelmistosuunnittelun opetus

Tästä seuraa kaksi sääntöä. Ensimmäinen on, että jokaisen olioita muuttavan viimeistelyvaiheen on ajettava ennen kuin kyseiset oliot serialisoidaan, ja dokumenttimoottorin sulkemisvaihe tulisi nähdä järjestettynä putkena, jossa serialisointi on viimeinen toiminto, ei vain yksi toiminto muiden joukossa. Toinen on se, joka maksoi tässä eniten aikaa: tulostusvaiheessa virheen puuttuminen ei ole todiste onnistumisesta. Rutiini, joka rakentaa oikean osajoukon ja soveltaa sitä väärään, jo kirjoitettuun graafiin, ei raportoi mitään virheellistä, koska sen omasta näkökulmasta mikään ei ollut väärin. Varmistuksen on kohdistuttava itse tiedostoon, ei paluukoodiin. Tarkista tulosteen koko, lue upotetun fontin tavupituus ja sen /BaseFont-etuliite, ja anna veraPDF:n tuomita PDF/A-tuloste, jossa puuttuva /CIDSet muuttaa hiljaisen puutteen nimetyksi virheeksi.

Fonttien käsittelyn tuottajapuoli, eli miten kirjasimet rekisteröidään ja upotetaan raporttitulosteeseen, käsitellään artikkelissamme fontit ja kuvat raporttitulosteessa. Validointipuoli, jossa nämä viimeistelyvaiheet tarkistetaan standardeja vasten, käsitellään PDF/A- ja PDF/UA-validoinnin läpikäynnissä. Molemmat pariutuvat tässä kuvatun osajoukkojen ja vaatimustenmukaisuuden työn kanssa, joka toimitetaan osana Delphi- ja C++Builder-ohjelmille tarkoitettua HotPDF Component -komponenttia yhdessä muiden tässä blogissa käsiteltyjen lataus-, muokkaus-, salaus- ja allekirjoitusrajapintojen kanssa.