フォント合成 (3) - CID フォント同士の合成

フォント合成 (3)
ここでは、Python3 スクリプトを使って、CID フォント同士を合成します。

CID フォントの場合、CID とグリフの定義は、Adobe-Japan1-6 などのような、
[Registry]-[Ordering]-[Supplement] (ROS) で指定されます。

Supplement は、いわゆるバージョンのようなもので、数字が増えると、グリフが増えます。

2つのフォントで、RegistryOrdering が同じであれば、互換性があるということなので、例えば、Adobe-Japan1-6 のフォントに対して、Adobe-Japan1-3 のフォントを合成する場合、同じ CID のグリフをそのまま置き換えればいいということになります。

もしも、一部で合成したくないグリフがある場合は、あらかじめ、CID フォントのままの状態で、不要なグリフを削除しておいて、一旦 SFD 形式で出力し、それを合成するという方法が使えます。

CID フォント同士で合成する場合は、ベースフォントの Supplement が、合成するフォントの Supplement と同じか、それより大きいことが必須条件となりますが、ベースフォントと重複するグリフを置き換えればいいだけなので、前回の合成よりは処理が簡単になります。

EM 値も、OpenType では、基本的に 1000 で固定となっているので、2つのフォント間で EM 値を調整する必要もありません。

※ FontForge 上では、CID フォントを開いて、「定義済みのグリフのみ表示」が ON の状態で一覧を表示すると、「移動」などでグリフを検索した時に正しい位置に移動しないので、何かしらおかしい場合は、OFF にした状態で一覧表示してください。
スクリプト
>> mergefont-cid.py (右クリックで保存してください)

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

$ python3 mergefont-cid.py output.sfd base.otf merge.otf

#!/usr/bin/python3

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

import fontforge
import os
import sys

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

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

    # ベースフォントのグリフリスト作成
    def _create_base_glyph_list(self):
        self.basename = {}

        for i in range(self.basefont.cidsubfontcnt):
            self.basefont.cidsubfont = i
            for gname in self.basefont:
                self.basename[gname] = i

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

        for i in range(self.repfont.cidsubfontcnt):
            self.repfont.cidsubfont = i

            for gname in self.repfont:
                if (gname in self.basefont) and (gname in self.repfont):
                    self.gname[gname] = i

    # グリフのコピー
    def _copy_glyph(self, gname, subfont):
        grep = self.repfont[gname]
        gbase = self.basefont[gname]

        self.repfont.cidsubfont = subfont
        self.repfont.selection.select(grep)
        self.repfont.copy()

        self.basefont.cidsubfont = self.basename[gname]
        self.basefont.selection.select(gbase)
        self.basefont.paste()

        return [grep, gbase]

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

        # サブテーブル
        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 の GPOS/GSUB 情報削除
    def _del_gpos_gsub(self, gbase):
        for t in gbase.getPosSub('*'):
            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(name)

            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):
        for t in grep.getPosSub('*'):
            ltype = t[1]
            if ltype not in self.GPOS_TYPES: continue

            baselname = self._get_lookup_base(t[0])
            val = t[2:]

            if ltype == 'Position':
                gbase.addPosSub(baselname, val[0], val[1], val[2], val[3])
            else:
                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 ...' + filename)
        self.basefont = fontforge.open(filename)

        if not self.basefont.is_cid:
            print('[!] "{0}" is not CID'.format(filename))
            sys.exit(0)

        self.baselookup = {}
        self.baselookupsub = {}
        self._create_base_glyph_list()

        self._check_unicode()

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

        if not self.repfont.is_cid:
            print('[!] "{0}" is not CID'.format(filename))
            sys.exit(0)

        if self.repfont.cidregistry != self.basefont.cidregistry \
            or self.repfont.cidordering != self.basefont.cidordering \
            or self.repfont.cidsupplement > self.basefont.cidsupplement:
            print('[!] "{0}" ROS error'.format(filename))
            sys.exit(0)
        
        self._create_rep_glyph_list()

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

    # 合成処理
    def merge(self, filename):
        self._open_merge(filename)
    
        for gname,subfont in self.gname.items():
            print(gname)
            grep, gbase = self._copy_glyph(gname, subfont)

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

        self.repfont.close()

    # Unicode 重複を回避
    def _check_unicode(self):
        print('# check unicode')

        # リスト作成
        unimap = {}
        for i in range(self.basefont.cidsubfontcnt):
            self.basefont.cidsubfont = i

            for gname in self.basefont:
                g = self.basefont[gname]
                if g.unicode == -1: continue

                # 異体字の場合は除外
                if g.altuni:
                    flag = False
                    for t in g.altuni:
                        if t[1] != -1:
                            flag = True
                            break

                    if flag: continue

                if g.unicode in unimap:
                    unimap[g.unicode].append(gname)
                else:
                    unimap[g.unicode] = [gname]
                            
        # 重複削除
        for uni,names in unimap.items():
            if len(names) > 1:
                uniname = fontforge.nameFromUnicode(uni)
                for gname in names:
                    if gname != uniname:
                        self.basefont[gname].unicode = -1
                        print('x "{0}" U+{1:04X}'.format(gname, uni))

    # 出力
    def output(self, filename):
        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 フォントである必要があります。
  • 2つのフォントの ROS が一致、または、Supplement が ベースフォント >= 合成フォント である必要があります。
  • 合成フォントに含まれるすべての (定義済み) グリフが、ベースフォントのグリフと置き換わります。
    (ベースフォントと重複しないグリフがもしあった場合は、対象外となります)
  • グリフの GSUB/GPOS 情報は、ベースフォントのグリフの情報を削除した上で、合成フォントのものがコピーされます。
    (参照されていない置換先グリフの削除は行われません)
  • ベースフォント上で、Unicode が重複するグリフがある場合、重複をチェックした上で、その Unicode の正式名と異なるグリフ名の Unicode 値を削除します。