らんだむな記憶

blogというものを体験してみようか!的なー

Name-keyed な OpenType/CFF のグリフ名を変更したい

ufo2ft/postProcessor.py#L168-L191 に大いなるヒントがある。特に

            cff.charset = [rename_map.get(n, n) for n in cff.charset]

がキーのようだ。これを確認しよう。このために参照する仕様書は 5176.CFF.pdf である。「13 Charsets」を見れば良い。Top DICT INDEX のオフセット位置のバイナリからこの構造を構築することになる。一連の SIDs が得られるが、それに対応する具体的な文字列は「10 String INDEX」から取得することになる。

Charsets の読み取り

この Charsets の読み取りは fontTools においては fontTools/cffLib/__init__.py#L1455-L1513 で実行されている。仕様書の通りに String INDEX をパースしているだけなので詳細は割愛する。

Charsets と String INDEX の保存

これが保存される時にどうなっているか?ということであるが、TopDictCompiler クラスの動作を見ることになる。fontTools/cffLib/__init__.py#L2327-L2335 を頼りに、CharsetCompiler を見ると、fontTools/cffLib/__init__.py#L1521-L1522 でバイナリデータへと pack している。

String INDEX 再構築のトリック

実はこの時に String INDEX が再構築されるという面白い動きをしている。どういうことかと言うと、compile を実行する際に事前に fontTools/cffLib/__init__.py#L158-L159 のように空の IndexedStrings を作っておいてここに新しく文字列データを詰めていくのである。具体的には packCharset0 時に getSIDfromName で新しい SID を決めながら Charsets のバイナリを作っていく。つまり IndexedStrings.getSID の中で fontTools/cffLib/__init__.py#L2734-L2737

           SID = len(self.strings) + cffStandardStringCount
            self.strings.append(s)
            self.stringMapping[s] = SID

のように、逐次新しい SID を発行しているのである。

まとめ

ということで、cff.charset を更新しておくと、compile 時に String INDEX も作り直されて保存されることが分かった。これで cff.charset がグリフ名を変更するキーになることが確認できた。

実際

topDict.charset = new_charset
ttFont.save(new_path)

という snippet でグリフ名を変えることができた。一方で、冒頭の ufo2ft/postProcessor.py では ttFont.setGlyphOrder をしていて少し気になる。

グリフオーダーも一緒に変更しているのは・・・?

これは推測だが、fontTools/cffLib/__init__.py#L185-L188

       for topDict in self.topDictIndex:
            if not hasattr(topDict, "charset") or topDict.charset is None:
                charset = otFont.getGlyphOrder()
                topDict.charset = charset

に見られるように、ttFont.getGlyphOrdertopDict.charset を一致させたいように見える。実際 ttFont.getGlyphOrder の実装を見ると fontTools/ttLib/ttFont.py#L431-L453 のように

       if 'CFF ' in self:
            cff = self['CFF ']
            self.glyphOrder = cff.getGlyphOrder()

となっている。丁寧に追いかけると ttFont["CFF "].cff. topDictIndex.getGlyphOrder() を呼んでいることが分かる。それは fontTools/cffLib/__init__.py#L2561-L2563 より、

   def getGlyphOrder(self):
        """Returns a list of glyph names in the CFF font."""
        return self.charset

ということで、一周回って topDict.charset へと戻ってきた。

本当のまとめ

以上から、手堅いところとしては

        otf.setGlyphOrder([rename_map.get(n, n) for n in otf.getGlyphOrder()])
        cff.charset = [rename_map.get(n, n) for n in cff.charset]

の “同期” をさせた上で TTFont.save してあげるのが良さそうだ。まぁ、でも、この snippet はたぶんグリフ名を書き換える時に実行してすぐに保存したほうが良い。変えたままで TTFont に触り続けるとした場合、GSUBGPOS でのグリフ名に不整合を起こしていそうだからだ。