Tekninen artikkeli

Fonttien osajoukon sulkeuma: kadonneet muodostetut glyfit Delphi PDF:issä

Muodostetut glyfit renderöityvät .notdef-laatikoina, kun fonttien osajoukotin säilyttää vain glyfit, jotka ovat tavoitettavissa tuotetuista koodipisteistä. HotPDF, natiivi VCL PDF -komponentti Delphille ja C++Builderille, kantoi täsmälleen tätä vikaa versioon 2.435.0 asti: OpenType GSUB -tuloste tallennettiin sisäiseen käyttöbittikarttaan, jonka osajoukotin ilmoitti kunnioittavansa mutta ei sitten koskaan todellisuudessa lukenut

Tämä on eri vika kuin se, joka kuvataan artikkelissa EndDoc-viasta, joka hiljaa poisti fonttien osajoukotuksen käytöstä. Tuo vika koski milloin osajoukotus ajettiin suhteessa sarjallistukseen, ja se poisti osajoukotuksen käytöstä kokonaan. Tämä koskee sitä, mitä osajoukko sisältää, kun osajoukotus ajetaan täysin aikataulussa. Putki laukeaa oikealla hetkellä, kuusikirjaiminen osajoukon etuliite ilmestyy kohtaan /BaseFont täsmälleen kuten ISO 32000-1 §9.6.4 vaatii, tiedosto pienenee, jokainen latinalainen sivu vedostuu puhtaana, ja arabiankielinen sivu tulee ulos rivinä tyhjiä suorakulmioita. Järjestysbugit ovat äänekkäitä heti kun katsoo. Sulkeumabugit pysyvät hiljaisina ikuisesti, koska osajoukko on rakenteellisesti pätevä ja väärässä vain omasta jäsenyysluettelostaan

Miksi muodostetut glyfit renderöityvät .notdef-arvoina?

Koska koodipisteiden joukko, jonka dokumentti tuottaa, ei ole sama kuin glyfien joukko, jonka dokumentti piirtää, ja osajoukotin, joka sekoittaa nämä kaksi keskenään, pudottaa jokaisen muodostuksen tuottaman glyfin. Tekstin muodostus muuttaa loogisen merkkisekvenssin sijoitetuksi glyfisekvenssiksi, ja sen koko tarkoitus on tuottaa glyfejä, joihin mikään yksittäinen syötemerkki ei kartoitu: arabian keskiasentoinen heh, fi-ligatuuri, devanagari-konjunktio, rclt-piirteen valitsema kontekstuaalinen vaihtoehto. Jokainen näistä on glyfitunnus, jonka GSUB-haku valmisti, ei sellainen, jonka cmap-taulukko antaa sinulle mille tahansa merkkijonosi merkille. Pelkän cmap:n ohjaama osajoukotin siis kulkee väärää indeksiä. Se säilyttää uskollisesti jokaisen glyfin, jota teksti olisi voinut käyttää ennen muodostusta, ja hylkää juuri ne glyfit, joita teksti käyttää muodostuksen jälkeen. Renderöijä pyytää sitten upotetulta fontilta GID 1847:ää, osajoukko on nollannut tuon merkinnän loca:ssa, ja takaisin tulee glyfi-indeksi 0. Glyfi-indeksi 0 on .notdef OpenTypen määritelmän mukaan, minkä vuoksi vian tunnusmerkki on tyhjä laatikko eikä väärä kirjain tai kaatuminen. Mikään PDF:ssä ei ole väärin muodostettu; fontti ei yksinkertaisesti sisällä glyfiä, jota sisältövirta pyysi

Koodipisteet eivät ole glyfejä: osajoukon kolme lähdettä

Oikean osajoukon sulkeuman täytyy yhdistää kolme itsenäistä lähdettä, kullakin oma kertymänsä. Ensimmäinen on koodipisteestä johdettu joukko: HotPDF kerää FUnicodeUsedCps-muuttujaan BMP-merkkejä sitä mukaa kuin ne tuotetaan, ja FUnicodeSmpUsed-muuttujaan lisätasoja täydentäviä merkkejä, jotka saavutetaan sijaispariparien kautta, ja kartoittaa sitten kunkin FUnicodeCpToGid:n kautta glyfitunnukseksi. Toinen on muodostuksesta johdettu joukko, GSUB-korvauksen tuottamat glyfitunnukset, tallennettuna MarkUnicodeGlyphUsed- ja EnableShapingFeatureForSubset-funktioiden kautta muuttujaan FUnicodeExtraUsedGlyphs. Kolmas on komposiittisulkeuma: glyfi, jonka numberOfContours on -1 glyf:ssä, kootaan komponenttiglyfitunnuksista, ja komposiitin säilyttäminen sen komponentteja pudottaen tuottaa tyhjän ääriviivan .notdef:n sijaan, mikä on väitetysti pahempi, koska se lukeutuu välistysbugiksi

HotPDF on aina käsitellyt ensimmäisen ja kolmannen. BuildAndApplyUnicodeFontSubset, osajoukotuksen aloituspiste, jota EndDoc kutsuu ennen sarjallistusta, kylvää käytettyjen glyfien taulukon GID 0:lla, kulkee läpi BMP-koodipisteet, kulkee läpi SMP-käyttöluettelon, ja antaa taulukon osajoukon rakentajalle, joka ratkaisee komposiittikomponentit sisäisesti. Toinen lähde kirjoitettiin mutta sitä ei koskaan käytetty, ja koska kolme lähdettä epäonnistuvat eri sisällöllä, aukko voi piiloutua vuosia koodikannassa, jonka regressiokorpus on enimmäkseen latinalaista

Taulukko, joka kirjoitettiin muttei koskaan luettu

Sopimus dokumentoitiin kolmessa paikassa ja kunnioitettiin ei yhdessäkään niistä. FUnicodeExtraUsedGlyphs:n julistus totesi, että EndDoc-osajoukotin yhdistää sen koodipisteestä johdetun käytön kanssa; ApplyArabicGSUBRefinement:n otsikkokommentti lupasi, että jokainen tuotettu korvaava GID välitetään MarkUnicodeGlyphUsed:n kautta, jotta osajoukotin vetää glyfin mukaan upotettuun fonttiin; sama lupaus esiintyy sanasta sanaan kohdassa ApplyArabicGSUBContextualRefinement rclt-polulle. Molemmat kutsujat pitivät oman puoliskonsa. Grep-haku jokaisesta viittauksesta kenttään ratkaisi toisen puoliskon noin yhdeksässäkymmenessä sekunnissa: yksi julistus, yksi SetLength-varaus RegisterUnicodeTTF:n sisällä, ja kirjoituksia kahdessa merkintärutiinissa. Ei yhtäkään lukua. Se on diagnoosi, joka kannattaa sisäistää, koska se yleistyy hyvin fonttien ulkopuolelle. Kun kenttää kirjoittaa useampi kutsupaikka eikä lue yksikään, ominaisuutta, jota se edustaa, ei ole olemassa, olipa se miten perusteellisesti kommentoitu tahansa. Osajoukottimen vaihe 1 on tarpeeksi pieni luettavaksi yhdellä näytöllä, ja aukko on ilmeinen heti kun tietää etsiä sitä

// Step 1: derive the used-glyph set (as it stood before 2.435.0)
SetLength(UsedGlyphs, FUnicodeNumGlyphs);
for I := 0 to FUnicodeNumGlyphs - 1 do
  UsedGlyphs[I] := False;
UsedGlyphs[0] := True;                       // .notdef is always present

for Cp := 0 to $FFFF do                      // source 1a: BMP code points
  if (Cp < Length(FUnicodeUsedCps)) and FUnicodeUsedCps[Cp]
     and (Cp < Length(FUnicodeCpToGid)) then
  begin
    GID := FUnicodeCpToGid[Cp];
    if (GID > 0) and (GID < FUnicodeNumGlyphs) then
      UsedGlyphs[GID] := True;
  end;

for I := 0 to High(FUnicodeSmpUsed) do       // source 1b: SMP code points
begin
  GID := FUnicodeSmpUsed[I].GID;
  if (GID > 0) and (GID < FUnicodeNumGlyphs) then
    UsedGlyphs[GID] := True;
end;

// source 2 was missing here: nothing ever consulted FUnicodeExtraUsedGlyphs

Yhden silmukan korjaus, ja glyfien merkitseminen itse

Korjaus on yhdiste, ja sen turvallisuusperustelu tulee toiminnon suunnasta: se vain asettaa bittejä, ei koskaan tyhjennä niitä, joten mikään glyfi, joka ennen selvisi osajoukkoon, ei voi alkaa pudota siitä

// v2.435.0: pull GSUB-derived extra glyphs into the subset.
// MarkUnicodeGlyphUsed / EnableShapingFeatureForSubset record GIDs that
// shaping produced but that no emitted code point maps to directly.
for I := 0 to FUnicodeNumGlyphs - 1 do
  if (I < Length(FUnicodeExtraUsedGlyphs)) and FUnicodeExtraUsedGlyphs[I] then
    UsedGlyphs[I] := True;

Kolme ominaisuutta tekevät tästä matalariskisen muutoksen fonttimoottorin uudelleenkirjoituksen sijaan. Se on monotoninen, kuten yllä. Se on no-op fonteilla, jotka eivät koskaan muodostaneet mitään, koska FUnicodeExtraUsedGlyphs pysyy kokonaan False-arvoisena ja tavutuloste puhtaasti latinalaiselle dokumentille pysyy muuttumattomana. Ja se saapuu ennen vaihetta 2, joten molemmat osajoukon rakentajat perivät sen: harva rakentaja, joka säilyttää alkuperäisen GID-numeroinnin, ja tiivis rakentaja _BuildCompactSubsetTTF, jonka HotPDF valitsee PDF/A:n alla numeroimaan säilytetyt glyfit uudelleen tiiviiseen alueeseen, kutistamaan maxp.numGlyphs:n, ja tuottamaan vanha-uuteen-kartoituksen /CIDToGIDMap-virtana, jonka ISO 32000-1 §9.7.4.2 vaatii. Molemmat kutsuvat _TTFWalkCompositeClosure:a sisäisesti, joten muodostettu glyfi, joka sattuu olemaan komposiitti, vetää nyt myös komponenttinsa mukaan. Komposiittisulkeuma ei koskaan ollut rikki; siihen ei vain koskaan päästy näiden glyfitunnusten kohdalla, koska glyfitunnukset eivät olleet joukossa, jota se kulkee läpi. Jos ohjaat GSUB-moottoria suoraan sen sijaan, että luottaisit sisäänrakennettuihin hienosäätöajoihin, sulkeuma tulee sinun vastuullesi, ja jokainen tuottamasi korvaava glyfitunnus täytyy merkitä ennen kuin EndDoc jäädyttää käytettyjen glyfien joukon

var
  Pdf: THotPDF;
  GIDs: array[0..1] of Word;
  LigGID: Word;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'shaped.pdf';
    Pdf.BeginDoc;
    Pdf.RegisterUnicodeTTF('C:\Fonts\NotoNaskhArabic-Regular.ttf');
    Pdf.ShapingFeatures := [sfArabicGSUB, sfStandardLigatures,
                            sfContextualAlternates];

    GIDs[0] := Pdf.GetUnicodeGlyphForCodepoint($0644);   // lam
    GIDs[1] := Pdf.GetUnicodeGlyphForCodepoint($0627);   // alef
    if Pdf.ApplyLigatureSubstitution(GIDs, 0, 'liga', LigGID) then
      Pdf.MarkUnicodeGlyphUsed(LigGID);   // omit this and you get .notdef

    Pdf.EnableShapingFeatureForSubset('rclt');
    Pdf.CurrentPage.RtLTextOut(50, 700, 0, WideString(ArabicText));
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

EnableShapingFeatureForSubset on eräkäsittelyn vastine yksittäisen GID:n kutsulle, ja se on tarkoituksella varovainen. Se kulkee läpi GSUB-hakuluetteloa etsien hakuja, jotka on kytketty yhteen neljätavuiseen piirretagiin nykyisen valitun kirjoitusjärjestelmä- ja kielipolun alla, ja merkitsee korvaavat glyfitunnukset, joita nuo haut voivat tuottaa. Se on puolustava no-op, kun fontissa ei ole GSUB-taulukkoa lainkaan tai kun piirre puuttuu siltä polulta, joten sen kutsuminen ehdoitta on turvallista. Se on myös tarkoituksella yli-approksimointi: se voi säilyttää glyfejä, joita mikään dokumentti ei koskaan piirrä. Osajoukotukselle liiallinen sisällyttäminen maksaa tavuja ja liian vähäinen sisällyttäminen maksaa oikeellisuutta, mikä tekee tuosta kompromissista helpon. Näiden hakujen rakenne, ja peittotaulukot, jotka päättävät mitkä glyfit osallistuvat, käsitellään artikkelissa GSUB-tyylivaihtoehtojen läpikäynnistä puhtaassa Delphissä

Miten todistat, että glyfi on todella osajoukossa?

Lukemalla tuotettu fontti, ei tuijottamalla sivua katseluohjelmassa, joka saattaa korvata järjestelmäfontin selkäsi takana. Tarkistus, joka nappaa koko tämän vikaluokan, on mekaaninen: pura /FontFile2-virta tulostetusta PDF:stä, jäsennä loca, ja vahvista, että odottamallasi glyfitunnuksella on tyhjentymätön merkintä, eli sen alku- ja loppusiirtymät eroavat toisistaan. Tyhjä merkintä tarkoittaa, että osajoukotin päätti, että glyfi on käyttämätön. Kaksi tapaa tekevät tämän jälkeen paljon vaikeammaksi lähettää sama vika uudelleen. Pidä muodostettua kirjoitusjärjestelmää käyttävä sivu automaattisessa savukorpuksessa eikä vain manuaalisessa vedostusjoukossa, koska arabia, devanagari ja khmer koettelevat sulkeumapolkuja, joihin mikään määrä latinalaista kattavuutta ei koske. Ja aina kun kertymä on olemassa, varmista, että jokin kuluttaa sen, koska vain-kirjoitettava kenttä on ominaisuus, joka kääntyy, testaa vihreää väärällä korpuksella, eikä tee mitään

Mihin korjaus pysähtyy

Osajoukon sulkeuma on välttämätön, jotta muodostettu glyfi renderöityisi, eikä se ole riittävä. Glyfin täytyy myös olla osoitettavissa sisältövirrasta, mikä on erillinen ongelma omalla rajallaan. HotPDF:n sisäänrakennetut arabian hienosäätöajot vahvistavat korvauksen vain, kun jokainen korvaava glyfitunnus on tavoitettavissa Unicode-esitysmuodon koodipisteen kautta käänteisen cmap-skannauksen kautta suunnilleen 690 koodipisteen yli väliltä U+FB50–U+FDFF ja U+FE70–U+FEFF. Kun korvaus laskeutuu glyfitunnukseen tuon alueen ulkopuolella, syöteikkuna läpäisee muuttumattomana sen sijaan, että se tuottaisi jotain, jota lukija ei pysty osoittamaan; fonttikohtaiset vaihtoehdot mielivaltaisissa glyfitunnuksissa tarvitsevat synteettisen yksityiskäyttökoodipisteen, joka on varattu väliltä U+E000–U+F8FF, kuljettaakseen ne läpi tuotantopolun. Rehellinen yhteenveto siis on, että 2.435.0-korjaus poisti kovan esteen sen sijaan, että se olisi täydentänyt tarinan. Ennen sitä glyfi saattoi muodostua oikein, tuottua oikein, ja silti kadota osajoukon vaiheessa, mikä tarkoitti, ettei muodostusmoottoriin voinut luottaa päästä päähän riippumatta siitä, kuinka hyviä sen haut olivat. Se, mikä jää jäljelle, on osoitettavuus, ja tuo rajoite ainakin epäonnistuu näkyvästi tuotantohetkellä eikä hiljaa kaiken sen jälkeen ajettavassa build-vaiheessa, jota olit seuraamassa. Saman putken tuotantopuolelle katso opas arabian ja RTL-tekstin muodostukseen Delphi PDF:issä

Tässä kuvattu fonttien osajoukotus, GSUB-moottori ja monimutkaisen kirjoitusjärjestelmän muodostus toimitetaan vakiona HotPDF-komponentissa Delphille ja C++Builderille; tuotesivu sisältää täydellisen API-viitteen edellä nimetyille Unicode-fontti- ja muodostuskutsuille