フォント合成 (2) - 非 CID をスクリプトで合成

フォント合成 (2)
FontForge では、FontForge 独自のスクリプトか、Python3 スクリプトを使って、フォントを操作することができます。

「フォントの統合」による色々な問題を解決するため、スクリプトを使って、フォントを合成させることにします。

ここでは、Python3 スクリプトを使い、ベースフォント (CID フォント以外) に対して、他のフォントのグリフを合成させます。

合成先が CID フォントで、CID フォントのまま合成したい場合は、サブフォントや CID などの問題があるため、別の形で合成する必要があります。
CID フォント同士の合成は、次回で行います。

「フォントの統合」による合成では、「置き換えたいグリフが含まれたフォント+ベースフォント」という形で合成しましたが、今回は、ベースフォント+[置換/追加したいグリフが含まれたフォント...] という形で合成していきます。

なお、ベースフォントは CID 以外のフォントである必要があります。
合成するフォントは、CID フォントでもそれ以外でも OK です。
スクリプト
>> mergefont.py (右クリックで保存してください)

コマンドライン上で、引数に「出力ファイル名」「ベースフォント」「合成フォント (複数可)」を順に指定して、実行してください。

$ python3 mergefont.py output.sfd base.ttf merge.ttf ...

#!/usr/bin/python3

#--------------------------------------------
# フォントの合成 (ベースフォントは非 CID)
#
# $ python3 mergefont.py [OUTPUT_FONT] [BASE_FONT] [MERGE_FONT...]
#
# - 出力/入力ともに、.sfd も可。
#
# OUTPUT_FONT
#   出力ファイル名。
#   拡張子が .sfd なら SFD 形式。それ以外は拡張子によって、各フォント出力。
# BASE_FONT
#   ベースフォントファイル名。CID フォント以外。
# MERGE_FONT
#   合成するフォントファイル名。複数指定可。CID フォント可。
#--------------------------------------------

import fontforge
import psMat
import os
import sys

if len(sys.argv) < 4:
    print('<Usage> mergefont.py [OUTPUT_FONT] [BASE_FONT] [MERGE_FONT...]')
    sys.exit(0)

class MergeGlyph:
    GSUB_TYPES = ("Substitution", "AltSubs", "MultSubs", "Ligature")
    GSUB_TYPES_NO_LIGA = ("Substitution", "AltSubs", "MultSubs")
    GPOS_TYPES = ("Position", "Pair")

    # グリフに、有効な Unicode 値がセットされているか
    def _is_glyph_unicode(self, g):
        if g.unicode != -1 or g.glyphname == '.notdef':
            return True

        # 代替の Unicode がある場合、(unicode, -1, 0)。
        # 異体字の場合、2番目にセレクターの値が入る
        if g.altuni:
            for t in g.altuni:
                if t[1] != -1: return True

        return False

    # グリフリストに追加
    def _add_glpyh_list(self,subfont):
        for gname in self.repfont:
            # エンコーディングから切り離されているグリフは対象外
            if gname not in self.repfont: continue

            # Unicode 外の同名グリフは別名にする
            bname = gname
            if (gname in self.basefont)    and (not self._is_glyph_unicode(self.repfont[gname])):
                bname += '.rep'

            self.gname[gname] = (bname, subfont)

    # 合成するグリフリスト作成 (key=rep, val=(base,cidsubfont))
    def _create_glyph_list(self):
        self.gname = {}

        if self.repfont.is_cid:
            for i in range(self.repfont.cidsubfontcnt):
                self.repfont.cidsubfont = i
                self._add_glpyh_list(i)
        else:
            self._add_glpyh_list(-1)

    # base の全グリフの GSUB 置換参照リストを作成
    # (key=置換先グリフ名, val=参照元のグリフ名の配列)
    def _create_gsub_ref_list(self):
        ret = {}
        for gname in self.basefont:
            if gname not in self.basefont: continue
            
            for t in self.basefont[gname].getPosSub('*'):
                if t[1] in self.GSUB_TYPES_NO_LIGA:
                    for repname in t[2:]:
                        if repname not in ret:
                            ret[repname] = [gname]
                        else:
                            if gname not in ret[repname]:
                                ret[repname].append(gname)

        return ret

    # (base) GSUB 置換先のグリフを削除
    #  basename: 置換情報が設定されているグリフ名
    #  delname: 削除するグリフ名
    def _del_gsub_glyph(self, basename, delname):
        # すでに削除済みの場合
        if delname not in self.basefont: return

        # 対象に Unicode 値がある場合は削除しない
        g = self.basefont[delname]
        if self._is_glyph_unicode(g): return

        # 対象を参照しているのが basename のみの場合、削除
        if delname not in self.gsubref:
            flag = 1
        else:
            names = self.gsubref[delname]
            if len(names) == 1 and names[0] == basename:
                flag = 2
            else:
                flag = 0

        if flag:
            self.basefont.removeGlyph(delname)
            print("- del: " + delname)

        if flag == 2:
            del self.gsubref[delname]

    # グリフの作成とコピー
    def _copy_glyph(self, repname, basename, subfont):
        grep = self.repfont[repname]

        # 新規グリフ
        if (basename not in self.basefont) or repname != basename:
            code = grep.unicode
            # Unicode の定義名と異なる場合、
            # 別グリフで Unicode が重複しているため、Unicode をセットしない
            if code != -1 and fontforge.nameFromUnicode(code) != repname:
                code = -1
            
            g = self.basefont.createChar(code, basename)
            if grep.altuni:
                g.altuni = grep.altuni

        gbase = self.basefont[basename]

        # アウトラインとヒント情報コピー
        if subfont != -1:
            self.repfont.cidsubfont = subfont
        
        self.repfont.selection.select(grep)
        self.repfont.copy()
        self.basefont.selection.select(gbase)
        self.basefont.paste()

        # em が異なる場合、拡大縮小
        if self.em_scale:
            gbase.transform(psMat.scale(self.em_scale), ('round',))

        return [grep, gbase]

    # base 用 lookup 名取得
    def _get_lookup_base(self, name):
        if self.repfont.is_cid:
            addname = self.repfont.cidfontname
        else:
            addname = self.repfont.fontname

        # サブテーブル
        sname = addname + '-' + name
        if sname in self.baselookupsub:
            return sname

        # 親 lookup
        reppname = self.repfont.getLookupOfSubtable(name)
        basepname = addname + '-' + reppname

        # 親 lookup 作成
        if basepname not in self.baselookup:
            ptype,flags,val = self.repfont.getLookupInfo(reppname)
            self.basefont.addLookup(basepname, ptype, tuple(), val)
            self.baselookup[basepname] = 1

        # サブテーブル作成
        self.basefont.addLookupSubtable(basepname, sname)
        self.baselookupsub[sname] = 1

        return sname

    # base の GSUB/GPOS 情報を削除
    def _del_base_possub(self, gbase):
        for t in gbase.getPosSub('*'):
            ltype = t[1]

            # 合字以外の GSUB は置換先グリフを削除
            if (ltype in self.GSUB_TYPES) and ltype != 'Ligature':
                for name in t[2:]:
                    self._del_gsub_glyph(gbase.glyphname, name)

            gbase.removePosSub(t[0])

    # rep の GSUB 情報をコピー
    def _copy_rep_gsub(self, grep, gbase):
        for t in grep.getPosSub('*'):
            ltype = t[1]
            if ltype not in self.GSUB_TYPES: continue

            # 置換グリフ名
            # (rep 内で存在しないグリフを参照している場合は除外)
            val = []
            for name in t[2:]:
                if name in self.repfont:
                    val.append(self.gname[name][0])

            if not val: continue

            # 追加
            baselname = self._get_lookup_base(t[0])
            
            if ltype == 'Substitution':
                val = val[0]
            else:
                val = tuple(val)

            gbase.addPosSub(baselname, val)

    # rep の GPOS 情報をコピー
    def _copy_rep_gpos(self, grep, gbase):
        scale = self.em_scale

        for t in grep.getPosSub('*'):
            ltype = t[1]
            if ltype not in self.GPOS_TYPES: continue

            val = list(t[2:])

            if ltype == 'Position':
                if scale:
                    for i in range(4):
                        val[i] = round(val[i] * scale)

                baselname = self._get_lookup_base(t[0])
                gbase.addPosSub(baselname, val[0], val[1], val[2], val[3])

            else:
                # pair
                if val[0] not in self.gname: continue

                val[0] = self.gname[val[0]][0]
                if scale:
                    for i in range(1, 9):
                        val[i] = round(val[i] * scale)

                baselname = self._get_lookup_base(t[0])
                gbase.addPosSub(baselname, val[0], val[1], val[2], val[3], val[4], val[5], val[6], val[7], val[8])

    # ベースフォント開く
    def open_base(self, filename):
        print('# load base ...' + filename)
        self.basefont = fontforge.open(filename)

        if self.basefont.is_cid:
            print('[!] CID font is not supported')
            sys.exit(0)

        self.baselookup = {}
        self.baselookupsub = {}
        self.gsubref = self._create_gsub_ref_list()

    # 合成フォント開く
    def _open_merge(self, filename):
        print('# load merge ... ' + filename)

        self.repfont = fontforge.open(filename)
        self._create_glyph_list()

        # 縦書きメトリクス ON
        if (not self.basefont.hasvmetrics) and self.repfont.hasvmetrics:
            self.basefont.hasvmetrics = 1

        # EM 拡大率
        if self.basefont.em == self.repfont.em:
            self.em_scale = 0
        else:
            self.em_scale = self.basefont.em / self.repfont.em
            print('# em: {0} => {1}'.format(self.repfont.em, self.basefont.em))

    # 合成処理
    def merge(self, filename):
        self._open_merge(filename)
    
        for repname,val in self.gname.items():
            basename = val[0]
            subfont = val[1]

            if repname == basename:
                print(repname)
            else:
                print(repname + ' => ' + basename)

            grep, gbase = self._copy_glyph(repname, basename, subfont)

            self._del_base_possub(gbase)
            self._copy_rep_gsub(grep, gbase)
            self._copy_rep_gpos(grep, gbase)

        self.repfont.close()

    # 出力
    def output(self, filename):
        if self.basefont.encoding == 'Custom':
            self.basefont.encoding = 'UnicodeFull'

        root,ext = os.path.splitext(filename)

        print('# output ... ' + filename)

        if ext.lower() == ".sfd":
            self.basefont.save(filename)
        else:
            self.basefont.generate(filename, flags=('opentype','short-post'))

#

o = MergeGlyph()
o.open_base(sys.argv[2])

for fname in sys.argv[3:]:
    o.merge(fname)

o.output(sys.argv[1])
使い方
  • ベースフォントに、CID フォントは指定できません。
  • 合成するフォントは、CID フォントでも OK です。
  • ベースフォントと合成フォントで EM 値が異なる場合、ベースフォントの EM に合うように、グリフが拡大縮小されます。
  • 合成するフォントに含まれているすべてのグリフが、ベースフォントのグリフと置き換え、または追加されます。
  • ベースフォント内に、合成するグリフと同名のグリフが含まれる場合、基本的には合成するグリフに置き換えられますが、有効な Unicode 値が指定されていないグリフの場合は、末尾に ".rep" を付けた別名のグリフとして追加されます。
    (Unicode 外のグリフは、各フォントで、グリフ名とグリフ形状が一致しない場合があるため)
  • グリフを追加する際、そのグリフで指定されている Unicode 値の正式グリフ名と、元のグリフのグリフ名が異なる場合、Unicode 値を指定しない状態で追加されます。
    (合成フォント側で Unicode 値が重複しているグリフの対策)
  • 合成フォントのグリフが、ベースフォントのグリフと置き換わる場合、ベースフォントのグリフに設定されていた GSUB/GPOS 情報は削除され、合成フォントのグリフの情報がコピーされます。
    この時、ベースフォント内で他のグリフから参照されない置き換え先グリフは、自動で削除されます。
  • 合成フォントから GSUB/GPOS の情報をコピーする際、その lookup 名は、「合成フォントの名前-合成フォントの lookup 名」という名前で新規追加されます。
  • GPOS の値は、EM が異なる場合、自動調整されます。