文字セットの違いで生じる大量のフォントを自動で整理する

モリサワパスポートには大量のフォントが収録されているのだけど、その中には単なる収録文字セットの違い (Std, Pro, Pr5, Pr6など) によるvariantsも数多くある。例えば以下のような感じだ:
- A-OTF リュウミン Pro (Adobe Japan 1-4)
- A-OTF リュウミン Pr5 (Adobe Japan 1-5)
- A-OTF リュウミン Pr6 (Adobe Japan 1-6)
- A-OTF リュウミン Pr6N (Adobe Japan 1-6, JIS新字形)
これらのvariantsが統制なく使われると、結局、データをやりとりする全員がすべてのvariantsをインストールしなければならなくなる。これを避けるために、なるべく最小限のvariantsのみが使われるようにしておきたい。
具体的な方針としては…
- スーパーセット(例: Pr6N)のファミリが存在するなら、サブセット(例: ProN)のファミリは捨てる。つまり常にスーパーセットを使うようにする。(大きめの文字セットを使うと計算資源も食うようだけど、問題ないだろう)。
- 字形の違い(例: ProとProN)があるものについては、新字形のみを保持する。
- ついでにフォルダ分けもしておく: フォントファミリ名でフォルダ分けする。つまり、同じファミリ名のサブファミリ違いを、同一のフォルダにまとめる。
…という感じにしようと思う。さすがにこれを手動でやるわけにはいかないので、作業を自動化した。
Pythonスクリプトはこんな感じ:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import unicodedata | |
import re | |
import os.path | |
import glob | |
import shutil | |
from fontTools import ttLib | |
def get_family_and_subfamily(path): | |
font = ttLib.TTFont(path, fontNumber=0) | |
family = None | |
subfamily = None | |
for record in font['name'].names: | |
name_str = record.toUnicode() | |
if (record.langID == 0 and not family) or record.langID in [11, 1041]: | |
if record.nameID == 16: | |
family = name_str | |
elif record.nameID == 17: | |
subfamily = name_str | |
if not family: | |
for record in font['name'].names: | |
name_str = record.toUnicode() | |
if (record.langID == 0 and not family) or record.langID in [11, 1041]: | |
if record.nameID == 1: | |
family = name_str | |
elif record.nameID == 2: | |
subfamily = name_str | |
if family: | |
famiy = family.strip() | |
family = unicodedata.normalize("NFKC", family) | |
# m = re.match("(.*?)-?(W?\d+|B|DB|EL|H|M|B|U|S|EU|L|R|D|ORU|Bold|Light|Medium|Regu|Thin|Heavy|Ultra|EB|UB|HS|HL|E)$", family) | |
# if m: | |
# family = m.group(1).strip() | |
return (family, subfamily) | |
def split_aj_coverage(family): | |
m = re.match("(.*?)\s*((Std|Pro|Pr5|Pr6|Min|Upr)N?)$", family) | |
if m: | |
pure_family = m.group(1) | |
aj_coverage = m.group(2) | |
return (pure_family, aj_coverage) | |
else: | |
return (family, None) | |
def iter_fonts(): | |
for path in glob.glob("/Library/Fonts/*.otf"): | |
(family, subfamily) = get_family_and_subfamily(path) | |
if family is None: | |
print("fatal: " + path) | |
continue | |
(pure_family, aj_coverage) = split_aj_coverage(family) | |
yield (family, pure_family, aj_coverage, subfamily, path) | |
def filter_variants(variants): | |
r = {} | |
for aj in ["Pr6", "Pr5", "Pro", "Std"]: | |
aj_n = aj + "N" | |
if aj_n in variants: | |
r[aj_n] = variants[aj_n] | |
break | |
if aj in variants: | |
r[aj] = variants[aj] | |
break | |
if "Upr" in variants: | |
r["Upr"] = variants["Upr"] | |
if "Min" in variants: | |
r["Min"] = variants["Min"] | |
if (not r) and None in variants: | |
r[None] = variants[None] | |
return r | |
if __name__ == "__main__": | |
fam_aj_path_map = {} | |
for (family, pure_family, aj, subfamily, path) in iter_fonts(): | |
key = (pure_family, subfamily) | |
fam_aj_path_map.setdefault(key, {}) | |
fam_aj_path_map[key][aj] = (family, path) | |
for ((pure_family, _), aj_path_map) in fam_aj_path_map.items(): | |
variants = filter_variants(aj_path_map) | |
for (family, path) in variants.values(): | |
dirname = os.path.join('fonts', family) | |
if not os.path.exists(dirname): | |
os.makedirs(dirname) | |
shutil.copy( | |
path, | |
os.path.join(dirname, os.path.basename(path)) | |
) |