top
おさだのホームページ

Pythonでのフォント作成を楽にするモジュールを自作する
そうだ、フォントを作ろう。

1. 目的

Pythonでのフォント作成には ufo2ft と defcon の二つのモジュールを用いる。 これらは頂点を与えるとそれで囲まれた範囲をフォントの文字部分として出力するものである。


例えば正方形を描きたければ、

 
正方形の画像
defcon.Font().contour.appendPoint(defcon.Point((0, 0), "line"))
defcon.Font().contour.appendPoint(defcon.Point((100, 0), "line"))
defcon.Font().contour.appendPoint(defcon.Point((100, 100), "line"))
defcon.Font().contour.appendPoint(defcon.Point((0, 100), "line"))

このように4つの頂点を指定する必要がある。

正直面倒である。

今回はこの面倒な頂点の指定をある程度自動で行ってくれるクラス myFont() を作成する。


2. ソースコード

長いので折り畳み欄に記載

copy
myFont.py
展開
折り畳む
#! /usr/local/bin/env python3
#! encode : -*- utf-8 -*-

import defcon
import ufo2ft
import os
from math import *
import time
from scipy import interpolate
import numpy as np


class myFont:
    
    def __init__(self,
                 familyName:str=f"myFont_{time.time()}",
                 unitsPerEm:int=1000,
                 descender:int=-120,
                 ascender:int=880,
                 xHeight=500,
                 capHeight=800,
                 directory:str=None):
        
        self.familyName = familyName
        self.unitsPerEm = unitsPerEm
        self.descender = descender
        self.ascender = ascender
        self.xHeight = xHeight
        self.capHeight = capHeight
        
        self.font = defcon.Font()
        self.font.info.familyName = self.familyName
        self.font.info.unitsPerEm = self.unitsPerEm
        self.font.info.descender = self.descender
        self.font.info.ascender = self.ascender
        self.font.info.xHeight = self.xHeight
        self.font.info.capHeight = self.capHeight
        
        self.directory = directory
        if(directory != None):
            self.chdir(self.directory)
        
        self.chars = dict()
        
        self.GOLD = 1.618033988749895 # 黄金比
        self.PALT = 2.414213562373095 # 白銀比
        self.PI = 3.141592653589793 # 円周率
    
    
    ## ディレクトリの変更
    def chdir(self, directory):
        os.chdir(directory)


    ## フォントの保存
    def save(self, familyName:str=None):
        otf = ufo2ft.compileOTF(self.font)
        file = f"{self.familyName if familyName == None else familyName}.otf"
        otf.save(file)
        print(f"{file} に保存")
    
    
    ## 文字を作成する
    def makeChar(self, char:str, width:float=1., caption:str=None):
        make_char = self.Char(char, width, caption, self.font)
        return make_char

    
    ## 作成したフォントをHTMLで確認
    def makeHtml(self, familyName:str=None, fontSize:int=4):
        familyName = self.familyName if familyName == None else familyName
        with open(f"{familyName}.html", "w", encoding="utf8") as f:
            f.write("""\
<style>
    @font-face {
        font-family: "%s";
        src: url("%s.otf");
    }
    p {
        font-family: "%s";
        font-size: %svw;
    }
</style>
<body>
    <p style="color:#000">
        abcdefghijklmnopqrstuvwxyz<br>
        <br>
        ABCDEFGHIJKLMNOPQRSTUVWXYZ<br>
        <br>
        12345678901234567890<br>
        <br>
        あいうえお かきくけこ さしすせそ たちつてと なにぬねの はひふへほ まみむめも やゆよ らりるれろ わをん<br>
        <br>
        アイウエオ カキクケコ サシスセソ タチツテト ナニヌネノ ハヒフヘホ マミムメモ ヤユヨ ラリルレロ ワヲン<br>
    </p>
</body>
"""%(familyName, familyName, familyName, fontSize))
        
        print(f"{familyName}.html を作成")
    
    
    ## 作成する文字のクラス
    class Char:
        
        def __init__(self, char:str, width:float, caption:str, myfont):
            self.setting = [f"hoge = myFont.makeChar(char=\"{char}\", width={width}, caption={caption})"]
            self.glyph = myfont.newGlyph(char)
            self.glyph.unicode = ord(char)
            if(width == 1) and (caption != "up") and (caption != "big"):
                if(caption == "down") or (caption == "small") or (char in list("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890+-*/!?\"#$%&"()=ˆ˜¥|[]{}@:;`_,.<>")):
                    width = 0.6
            self.glyph.width = int(myfont.info.unitsPerEm * width)
            self.flush(is_repr=False)
        
        
        ## ポリゴンの頂点の設定
        def addPoint(self, x:int, y:int, is_repr:bool=True):
            if(is_repr):
                self.setting.append(f"hoge.addPoint(x={x}, y={y})")
            self.contour.appendPoint(defcon.Point((x, y), "line"))
        
        
        ## 点で囲まれたポリゴンの作成
        def polygon(self, coords_x, coords_y, is_repr:bool=True):
            if(is_repr):
                self.setting.append(f"hoge.polygon(coords_x={coords_x}, coords_y={coords_y})")
            for i in range(len(coords_x)):
                self.addPoint(coords_x[i], coords_y[i], is_repr=False)
            
            self.push(isFlush=True, is_repr=False)
        
        
        ## 長方形の作成
        def rectangle(self, origin_x:int, origin_y:int, x:int, y:int, deg:float=0., is_repr:bool=True):
            if(is_repr):
                self.setting.append(f"hoge.rectangle(origin_x={origin_x}, origin_y={origin_y}, x={x}, y={y}, deg={deg})")
            coords = [[0, 0],
                      [x, 0],
                      [x, y],
                      [0, y]]
            
            for i in range(4):
                coord_x, coord_y = self.rotate(coords[i][0], coords[i][1], deg)
                self.addPoint(origin_x+coord_x, origin_y+coord_y, is_repr=False)
                
            self.push(isFlush=True, is_repr=False)
        
        
        ## 太字(平行四辺形)の作成
        def bold(self, coord_a=[0, 0], coord_b=[0, 0], size:int=100, deg:float=0., padding:str="center", is_repr:bool=True):
            if(is_repr):
                self.setting.append(f"hoge.bold(coord_a={coord_a}, coord_b={coord_b}, size={size}, deg={deg}, padding=\"{padding}\")")
            if(padding == "left"):
                pad = [size, 0]
            elif(padding == "right"):
                pad = [0, size]
            else:
                pad = [size/2, size/2]
             
            coords = [[coord_a[0]-pad[0], coord_a[1]],
                      [coord_a[0]+pad[1], coord_a[1]],
                      [coord_b[0]+pad[1], coord_b[1]],
                      [coord_b[0]-pad[0], coord_b[1]]]
            
            for i in range(4):
                coord_x, coord_y = self.rotate(coords[i][0], coords[i][1], deg)
                self.addPoint(coord_x, coord_y, is_repr=False)
                
            self.push(isFlush=True, is_repr=False)
        
        
        ## 楕円の作成(正円も含む)
        def ellipce(self, origin_x:int, origin_y:int, rad:int, square:int=30, pow_x:float=1., pow_y:float=1., deg_range=[0., 360.], deg:float=0., is_repr:bool=True):
            if(is_repr):
                self.setting.append(f"hoge.ellipce(origin_x={origin_x}, origin_y={origin_y}, rad={rad}, square={square}, pow_x={pow_x}, pow_y={pow_y}, deg_range={deg_range}, deg={deg})")
            deg_diff = (deg_range[1] - deg_range[0]) / square
            self.addPoint(origin_x, origin_y, is_repr=False)
            for i in range(square+1):
                deg_now = deg_range[0] + deg_diff*i
                coord_x = rad*cos(radians(deg_now)) * pow_x
                coord_y = rad*sin(radians(deg_now)) * pow_y
                coord_x, coord_y = self.rotate(coord_x, coord_y, -deg)
                self.addPoint(origin_x+coord_x, origin_y+coord_y, is_repr=False)
            self.addPoint(origin_x, origin_y, is_repr=False)
            
            self.push(isFlush=True, is_repr=False)
        
        
        ## 複数の座標を通る直線の作成
        def line(self, coords_x, coords_y, size:int=100, circle:bool=True, padding="center", is_repr:bool=True):
            if(is_repr):
                self.setting.append(f"hoge.line(coords_x={coords_x}, coords_y={coords_y}, size={size}, circle={circle}, padding=\"{padding}\")")
            pad = size/2
            if(padding == "left"):
                for i in range(len(coords_x)):
                    coords_x[i] -= pad
                    coords_y[i] -= pad
            elif(padding == "right"):
                for i in range(len(coords_x)):
                    coords_x[i] += pad
                    coords_y[i] += pad
                
            for i in range(1, len(coords_x)):
                self.addPoint(coords_x[i-1]-pad, coords_y[i-1]+pad, is_repr=False)
                self.addPoint(coords_x[i]-pad, coords_y[i]+pad, is_repr=False)
                self.addPoint(coords_x[i]-pad, coords_y[i]-pad, is_repr=False)
                self.addPoint(coords_x[i-1]-pad, coords_y[i-1]-pad, is_repr=False)
                self.push(isFlush=True, is_repr=False)
                self.addPoint(coords_x[i-1]-pad, coords_y[i-1]+pad, is_repr=False)
                self.addPoint(coords_x[i]-pad, coords_y[i]+pad, is_repr=False)
                self.addPoint(coords_x[i]+pad, coords_y[i]+pad, is_repr=False)
                self.addPoint(coords_x[i-1]+pad, coords_y[i-1]+pad, is_repr=False)
                self.push(isFlush=True, is_repr=False)
                self.addPoint(coords_x[i-1]+pad, coords_y[i-1]+pad, is_repr=False)
                self.addPoint(coords_x[i]+pad, coords_y[i]+pad, is_repr=False)
                self.addPoint(coords_x[i]+pad, coords_y[i]-pad, is_repr=False)
                self.addPoint(coords_x[i-1]+pad, coords_y[i-1]-pad, is_repr=False)
                self.push(isFlush=True, is_repr=False)
                self.addPoint(coords_x[i-1]-pad, coords_y[i-1]-pad, is_repr=False)
                self.addPoint(coords_x[i]-pad, coords_y[i]-pad, is_repr=False)
                self.addPoint(coords_x[i]+pad, coords_y[i]-pad, is_repr=False)
                self.addPoint(coords_x[i-1]+pad, coords_y[i-1]-pad, is_repr=False)
                self.push(isFlush=True, is_repr=False)
            
            if(circle):
                for i in range(len(coords_x)):
                    origin_pad = pad - size/2
                    self.ellipce(coords_x[i]+origin_pad, coords_y[i]+origin_pad, (size/2)*(2**(1/2)), is_repr=False)
        
        
        ## 0〜3次のスプライン補間による太線の作成
        def spline(self, coords_x, coords_y, size:int=100, dim:int=None, square:int=30, fudeji:bool=False, circle:bool=True, padding="center", is_repr:bool=True):
            if(is_repr):
                self.setting.append(f"hoge.spline(coords_x={coords_x}, coords_y={coords_y}, size={size}, dim={dim}, square={square}, fudeji={fudeji}, circle={circle}, padding=\"{padding}\")")
            if(dim == None):
                dim = len(coords_x)-1
            if(dim > 3):
                dim = 3
            elif(dim < 0):
                dim = 0
            if(dim >= len(coords_x)):
                dim = len(coords_x)-1
            
            pad = size/2
            
            kind = {0:"zero", 1:"slinear", 2:"quadratic", 3:"cubic"}[dim]
            func = interpolate.interp1d(coords_x, coords_y, kind=kind)
            xs = np.linspace(coords_x[0], coords_x[len(coords_x)-1], square)
            ys = [func(x) for x in xs]
            if(fudeji):
                dist = []
                for i in range(1, square):
                    dist.append(abs(ys[i-1] - ys[i]))
                dist_diff = max(dist) - min(dist)
                old_y = ys[0]
                for i in range(square):
                    y_diff = abs(old_y - ys[i]) / dist_diff
                    old_y = ys[i]
                    pad_x = pad * y_diff
                    pad_y = pad * y_diff
                    if(padding == "left"):
                        pad_buf = -(pad_x+pad_y)/2
                    elif(padding == "right"):
                        pad_buf = (pad_x+pad_y)/2
                    else:
                        pad_buf = 0
                    if(circle):
                        self.ellipce(xs[i]+pad_buf, ys[i]+pad_buf, (pad_x+pad_y)/2, is_repr=False)
                    else:
                        self.rectangle(xs[i]-pad_x+pad_buf, ys[i]-pad_y+pad_buf, x=pad_x, y=pad_y, is_repr=False)
            else:
                for i in range(square):
                    pad_x = pad
                    pad_y = pad
                    if(circle):
                        self.ellipce(xs[i], ys[i], (pad_x+pad_y)/2, is_repr=False)
                    else:
                        self.rectangle(xs[i]-pad_x, ys[i]-pad_y, x=pad_x, y=pad_y, is_repr=False)
        
        
        ## スプライン補間関数の筆字機能を呼び出す
        def fudeji(self, coords_x, coords_y, size:int=100, dim:int=None, square:int=30, padding="center", is_repr:bool=True):
            if(is_repr):
                self.setting.append(f"hoge.fudeji(coords_x={coords_x}, coords_y={coords_y}, size={size}, dim={dim}, square={square}, padding=\"{padding}\")")
            self.spline(coords_x, coords_y, size=size, dim=dim, square=square, fudeji=True, circle=True, padding=padding, is_repr=False)
        
        
        ## 座標軸の回転
        def rotate(self, x:int, y:int, deg:float):
            return cos(radians(-deg))*x-sin(radians(-deg))*y, sin(radians(-deg))*x+cos(radians(-deg))*y
            
            
        ## 保存した座標の初期化
        def flush(self, is_repr:bool=True):
            self.contour = defcon.Contour()
            if(is_repr):
                self.setting.append(f"hoge.flush()")
        
        
        ## 座標をフォントへ出力
        def push(self, isFlush:bool=True, is_repr:bool=True):
            self.glyph.appendContour(self.contour)
            if(isFlush):
                self.flush(is_repr=is_repr)
            if(is_repr):
                self.setting.append(f"hoge.push(isFlush=False)")
        
        
        ## 現在の状態を文字列として出力
        def __repr__(self):
            return "\n".join(self.setting)


3. 仕様

大まかな仕様は下記である。

 
myfont = myFont("フォント名")
myfont.chdir("作業ディレクトリ") # なくても良い
font_a = myfont.makeChar("a") # a に当てるフォントを作成

※赤太字は必須の引数

○ 複数の点で囲まれた図形
font_a.polygon(
    [x座標],
    [y座標])


○ 長方形
font_a.rectangle(
    開始点のx座標,
    開始点のy座標,
    横の長さ,
    縦の長さ,
    deg=回転する角度)


○ 縦方向は太く、横方向は細い平行四辺形(筆記体の太字)
font_a.bold(
    [開始点x, y],
    [終点x, y],
    size=太さ,
    padding=厚みをつける方向,
    deg=回転する角度)


○ 楕円(正円も含む)
font_a.ellipce(
    中心のx座標,
    中心のy座標,
    半径,
    square=角の数(大きければ正確な円に、小さければ角ばってくる),
    pow_x=横の半径の倍率,
    pow_y=縦の半径の倍率,
    deg_range=[描画を開始する角度, 描画を終了する角度],
    deg=回転する角度)


○ 複数の点を通る直線
font_a.line(
    [x座標],
    [y座標],
    size=太さ,
    circle=角を丸くするかどうか,
    padding=厚みをつける方向)


○ 複数の点を0〜3次スプライン補間して得られる曲線
font_a.spline(
    [x座標],
    [y座標],
    size=太さ,
    dim=スプライン補間の次元数,
    square=曲線の座標の数,
    circle=角を丸くするかどうか,
    fudeji=筆字のような形にするかどうか,
    padding=厚みをつける方向)


○ 筆字のような曲線(spline(fudeji=True)と同義)
font_a.fudeji(
    [x座標],
    [y座標],
    size=太さ,
    dim=スプライン補間の次元数,
    square=曲線の座標の数,
    padding=厚みをつける方向)


myfont.save() # フォントの保存
myfont.makeHtml() # 作成したフォントを適用したHTMLを作成



4. 実際にフォントを作ってみる

copy
make_font.py
from myFont import myFont


myDir = "~/python/font/myFont"

myfont = myFont("myfont")
myfont.chdir(myDir)


# A
font_A = myfont.makeChar("A")
font_A.line([0, 0, 600],
               [0, 700, 0], size=50, circle=False)
font_A.line([0, 350],
               [250, 250], size=50, circle=False)


# B
font_B = myfont.makeChar("B")
font_B.bold([100, 0],
                [200, 750])
font_B.bold([200, 750],
                [500, 750/2])
font_B.bold([500, 750/2 + 20],
                [150, 750/2])
font_B.bold([150, 750/2],
                [500, 0])
font_B.bold([500, 20],
                [100, 0])


# C
font_C = myfont.makeChar("C")
font_C.fudeji([50, 100, 300, 550],
                  [350, 650, 800, 600],
                  size=200, dim=3, square=50)
font_C.fudeji([55, 200, 550],
                  [450, 100, 200],
                  size=200, dim=3, square=30)


myfont.save()
myfont.makeHtml()


出力されたフォント:
A :    A    line()を使用
B :    B    bold()を使用
C :    C    fudeji()を使用



※上のフォントが正しく反映されてない場合は下のスクショ
をご覧ください。