Hatena::ブログ(Diary)

Webと文字

よければ、はてブしてください。( ´∀`) George.Nagaoka@gmail.com

JavaScript IME:海外からブラウザで日本語を変換 新URL 旧URL:
多言語入力ブックマークレット:ブラウザでロシア語、中国語、アラビア語・・・
【軍曹が】携帯電話開発の現状【語る】をAA化した
AAのデータベース
趣味のページ

2010-11-06

Webフォント変換サービスを作ろう その1( ´_ゝ`)

Webフォント*1を日本語で*2使うとこんな感じ。


f:id:project_the_tower2:20101106215709p:image


日本語フォントは概して巨大。そのまま読み込むと重いです。必要な字だけのフォントがあるといいですね。

もっと言えば指定した字だけが入ったフォントを返してくれるサービスがあるといいですね!

そんな時、頼りになるのがFontForge*3ですが…、


f:id:project_the_tower2:20101106221345p:image


FontForgeは色々と多機能過ぎてちょっと重いです。これでサービスを作ってもすぐ行き詰まりそうです。


f:id:project_the_tower2:20101106222429p:image


そんなわけで、サービスを作ってみることにしました。

全体の流れ

 毎回、フォントファイルから字句を検索しては非効率です。一度ファイルをDBに入れて、要求があればそこからフォントファイルを作成するようにします。

フォントファイル(TTF/OTF/その他)

 ―1.展開→

  データベース(MySQL/sqlite)

   ―2.生成→

    フォントファイル(SVG/TTF/OTF)

     ―3.変換→

      Webフォント形式(WOFF/EOT)

1.1 展開(要件定義)

 ここではフォントファイルを展開してデータベースに入れるまでを考えます。OpenTypeをベースにするとデータベースには以下のことが必要そうです。

  1. UCS4コード
  2. 異体字セレクタ
  3. 特殊なグリフ
  4. 合成グリフ
  5. 縦書きメトリクス、横書きメトリクス
  6. 様々なグリフデータ
  7. OpenTypeレイアウト
1.UCS4コード

 OpenTypeが指定できるマッピング可能な文字コードUnicode(UCS2/UCS4)、ShiftJIS、Big5等が規定されています*4が、現在のフォントのほぼすべてがUnicode(UCS2)のみのマッピングテーブル(Format4)しか持ちません*5。ただし、Mac用に1バイトマッピングテーブル(Format0,Foramt10)を提供することが仕様書で推奨されています。BMP領域を超える文字を含むフォントの場合、UCS4用のマッピングテーブル(Format12)を追加として持ちます。

 データベースは4バイト(long)のUCS4コードをもつことが理想的です。

2.異体字セレクタ

 異体字セレクタ文字コードの後ろに付与して字体を変えるコード値です*6。OpenTypeでは1.5(現在は1.6)から対応したようです。cmapのマッピングテーブル(Format 14)がそれにあたります。

 データベースはUCS4コードと別に異体字セレクタをもつのが良さそうです。

3.特殊なグリフ

 OpenTypeの仕様書ではフォント内に存在しない文字の代替グリフ(missing glyph)をグリフテーブルの最初(index =0)に置くように推奨しています。また、null,space,CRをindex1,2,3に置くようにも推奨しています。null,space,CRはそれぞれUCS4コードを持ちますが、missing glyphはコードを持ちません。

 データベースはmissing glyphを定義できることが必要です。

4.合成グリフ*7

 アウトライングリフ(glyfテーブル)で定義されている合成グリフ(Composite Glyph)は欧米フォントのアクセント記号付きグリフの表現によく使われる形式です。合成グリフは「A」+「˜」= 「Ã」のように複数の単独グリフ(Simple Glyph)から定義されます。

5.縦書きメトリクス、横書きメトリクス

 OpenTypeに必須なメトリクステーブルは横書き用(hhea+hmtx)だけですが、日本語フォントの多くに縦書き用のメトリクステーブル(vhea+vmtx)が存在します。

6.様々なグリフデータ

 OpenTypeにはアウトライン形式(glyf+loca)意外にも、ビットマップ形式(EBDT)、PostScript形式(CFF)があります。複数のグリフをUCS4コード値に対応させる仕組みが必要です。

7.OpenTypeレイアウト*8

 OpenTypeレイアウトテーブルはグリフを置換(Substitution)、ポジショニング(Positioning)するための情報を提供します。日本語フォントでは縦書き時にグリフを置き換えするのによく使われます。

1.2 展開(実装1:準備)

 フォントファイルの展開はFreeType2を使ってもFontForgeを使ってもいいですが、ここではTTX/FontTools/ttLibというPythonライブラリを使用することにしました。これはFonts & Encodings*9で使用されたり、AFDKO*10にも入っている実績あるライブラリです。


さっそく使用してみます。準備です。

  1. Python2.Xの実行環境を用意します。
  2. Numpyをダウンロード*11して、インストールします。
  3. fonttools-2.3.tar.gzをダウンロード*12して、展開します。

これで、フォントファイルをXML形式に変換してみます。

from fontTools import ttLib

tt = ttLib.TTFont("mplus-1m-regular.ttf");
tt.saveXML("mplus-1m-regular.xml");

上のスクリプトを実行すると10MBくらいのXMLファイルが出来ます。

<?xml version="1.0" encoding="ISO-8859-1"?>
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="2.3">
  <GlyphOrder>
    <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
    <GlyphID id="0" name=".notdef"/>
    <GlyphID id="1" name="glyph00001"/>

実はttLibにはxmlからフォントを再生成する機能も有ります。さらに読み込んだフォントから文字を削ってフォントを生成することもできます。

これを使えばサービスを作れそうですが…


f:id:project_the_tower2:20101106221345p:image


残念ながら、これをやるとかなり遅い(10秒以上)です。ここまで遅いのは、文字を削ると最大バウンディングボックスが変わってしまうため、再計算のためにすべてのグリフを一度バイナリから展開する必要があるからです。その為にグリフ数に応じて変換時間は長くなります。

 ここから高速化のためにはデータベースにはグリフバイナリと別にバウンディングボックス情報を入れておき、展開せずに最大バウンディングボックスを求められるようにする必要があります(追加要件)。

1.3 展開(実装2:DB構造)

 DBにはpythonで標準で使えるsqlite3を選びました。sqlite3はあまり変更せずにMySQLに移行できます。1.1の要件定義の一部と1.2の追加要件を踏まえるとDBは以下のように成ります。ただし、OpenTypeレイアウトアウトライン形式以外のデータには対応していません。

#createTable.py
import sqlite3

db = sqlite3.connect('font.db');

sql="""
CREATE TABLE cmap(
    FontID,
    UCS4,
    IVS,
    GlyphID,
    GlyphType
)""";
db.execute(sql);

sql="""
CREATE TABLE simple_glyf(
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    xMin INTEGER,
    xMax INTEGER,
    yMin INTEGER,
    yMax INTEGER,
    point INTEGER,
    contour INTEGER,
    data BLOB,
    instLength INTEGER,
    length INTEGER,
    advanceWidth INTEGER,
    leftSideBearing INTEGER,
    advanceHeight INTEGER,
    topSideBearing INTEGER
)""";
db.execute(sql);

sql="""
CREATE TABLE composite_glyf(
        GlyphID,
        refGlyfID,
        flags,
        argument1,
        argument2,
        data
)""";
db.execute(sql);

sql="""
CREATE TABLE info(
    FontID,
    key,
    value
)""";
db.execute(sql);

db.close();

1.3 展開(実装3:DB挿入)

データベースの雛形ができたら、DBにデータを追加していきます。この際に、いくつかttLib/tables/_g_l_y_f.pyに修正を加える必要が有ります。

_g_l_y_f.pyの修正点

  1. Glyph.expandの「del self.data」をコメントアウト
  2. GlyphComponent.decompileを以下のように変更
		flags, glyphID = struct.unpack(">HH", data[:4])
		self.flags = int(flags)
		tmp = data;#追加
		glyphID = int(glyphID)
		self.glyphName = glyfTable.getGlyphName(int(glyphID))
		#print ">>", reprflag(self.flags)
		data = data[4:]
		offset =4;#追加
		
		if self.flags & ARG_1_AND_2_ARE_WORDS:
			if self.flags & ARGS_ARE_XY_VALUES:
				self.x, self.y = struct.unpack(">hh", data[:4])
			else:
				x, y = struct.unpack(">HH", data[:4])
				self.firstPt, self.secondPt = int(x), int(y)
			data = data[4:]
			offset +=4;#追加
		else:
			if self.flags & ARGS_ARE_XY_VALUES:
				self.x, self.y = struct.unpack(">bb", data[:2])
			else:
				x, y = struct.unpack(">BB", data[:2])
				self.firstPt, self.secondPt = int(x), int(y)
			data = data[2:]
			offset +=2;#追加
		
		if self.flags & WE_HAVE_A_SCALE:
			scale, = struct.unpack(">h", data[:2])
			self.transform = numpy.array(
					[[scale, 0], [0, scale]]) / float(0x4000)  # fixed 2.14
			data = data[2:]
			offset +=2;#追加
		elif self.flags & WE_HAVE_AN_X_AND_Y_SCALE:
			xscale, yscale = struct.unpack(">hh", data[:4])
			self.transform = numpy.array(
					[[xscale, 0], [0, yscale]]) / float(0x4000)  # fixed 2.14
			data = data[4:]
			offset +=4;#追加
		elif self.flags & WE_HAVE_A_TWO_BY_TWO:
			(xscale, scale01, 
					scale10, yscale) = struct.unpack(">hhhh", data[:8])
			self.transform = numpy.array(
					[[xscale, scale01], [scale10, yscale]]) / float(0x4000)  # fixed 2.14
			data = data[8:]
			offset +=8;#追加
		more = self.flags & MORE_COMPONENTS
		haveInstructions = self.flags & WE_HAVE_INSTRUCTIONS
		#コメントアウト
		#self.flags = self.flags & (ROUND_XY_TO_GRID | USE_MY_METRICS | 
		#		SCALED_COMPONENT_OFFSET | UNSCALED_COMPONENT_OFFSET |
		#		NON_OVERLAPPING)
		self.data = tmp[:offset];#追加
		return more, haveInstructions, data

以下はDBにデータを追加するコードです。

# -*- coding: utf8 -*-
#db.py
import sqlite3
from fontTools import ttLib

########

fontid = 1;

########
#Saint-Andrews_Queen.ttf
tt = ttLib.TTFont("font/Saint-Andrews_Queen.ttf");
db = sqlite3.connect('font.db');

#sql
sql_insert_simple_glyf ="""
INSERT INTO simple_glyf(
        id,
        xMin,xMax,yMin,yMax,
        point,
        contour,
        data,
        instLength,length,
        advanceWidth,leftSideBearing,
        advanceHeight,topSideBearing
) VALUES(
        NULL,
        ?,?,?,?,
        ?,
        ?,
        ?,
        ?,?,
        ?,?,
        ?,?
)
""";
sql_insert_cmap="""
INSERT INTO cmap(
        FontID,
        UCS4,
        IVS,
        GlyphID,
        GlyphType
) VALUES (
        ?,?,?,?,?
)
""";
sql_insert_composite_glyf="""
INSERT INTO composite_glyf(
        GlyphID,
        refGlyfID,
        flags,
        argument1,
        argument2,
        data
) VALUES (
        ?,?,?,?,?,?
)
""";
sql_get_GlyphID="""
SELECT id
FROM simple_glyf
WHERE id = last_insert_rowid()
""";

simpleGlyph =[];
compGlyph =[];
compGlyphRef=[];

#cmap
subtable = tt['cmap'].getcmap(3,1);
for name,g in tt['glyf'].glyphs.items():
        g.expand(tt['glyf']);
        
        #unicode
        if tt.getGlyphID(name) == 1:#.null GlyphID =1
                code = 0;
        elif tt.getGlyphID(name) == 2:#space GlyphID =2
                code = 32;
        elif tt.getGlyphID(name) == 3:#CR GlyphID =3
                code = 13;
        elif tt.getGlyphID(name) ==0:#.notdef GlyphID =0
                code =-1;
        else:
                try:
                        code = subtable.cmap.keys()[subtable.cmap.values().index(name)];
                except:
                        code =-2;

        #verical metrics
        try:
                advanceHeight = tt['vmtx'].metrics[name][0];
                topSideBearing = tt['vmtx'].metrics[name][1];
        except:
                advanceHeight = 0;
                topSideBearing =0;

        #horizontal metrics
        try:
                advanceWidth = tt['hmtx'].metrics[name][0];
                leftSideBearing = tt['hmtx'].metrics[name][1];
        except:
                print "err : hmtx not include";
                raise;

        #glyfinfo
        if 0<= g.numberOfContours:
                glyfType =1;
                if hasattr(g,"data"):
                        xMin = g.xMin;
                        xMax = g.xMax;
                        yMin = g.yMin;
                        yMax = g.yMax;
                        point = g.getMaxpValues()[0];
                        contour = g.getMaxpValues()[1];
                        data = buffer(g.data);
                        length = len(g.data);
                        instLength = len(g.program.bytecode);
                else:
                        xMin = 0;
                        xMax = 0;
                        yMin = 0;
                        yMax = 0;
                        point = 0;
                        contour = 0;
                        data = 0;
                        length = 0;
                        instLength = 0;
        elif 0> g.numberOfContours:
                #Composite Glyph
                glyfType =2;
                xMin = g.xMin;
                xMax = g.xMax;
                yMin = g.yMin;
                yMax = g.yMax;
                point = 0;
                contour = 0;
                data = 0;
                length = 0;
                instLength = 0;
                        
                for component in g.components:
                        print str(component.flags)
                        compGlyphRef.append(component.glyphName);
                        compGlyph.append({
                                'code':code,
                                'arg1':component.x,
                                'arg2':component.y,
                                'flags':component.flags,
                                'GlyphName':component.glyphName,
                                'data':component.data
                                });

        simpleGlyph.append({
                'code':code,
                'xMin':xMin,
                'xMax':xMax,
                'yMin':yMin,
                'yMax':yMax,
                'point':point,
                'contour':contour,
                'data':data,
                'length':length,
                'instLength':instLength,
                'refName':name,
                'advanceHeight':advanceHeight,
                'topSideBearing':topSideBearing,
                'advanceWidth':advanceWidth,
                'leftSideBearing':leftSideBearing,
                'glyfType':glyfType});

#composite glyph ref list
compGlyphRef =  sorted(set(compGlyphRef), key=compGlyphRef.index);
code2GID={};
compGlyphRef2code={};

for s in simpleGlyph:
        db.execute(sql_insert_simple_glyf,
                   (s['xMin'],s['xMax'],s['yMin'],s['yMax'],
                    s['point'],s['contour'],s['data'],s['length'],
                    s['instLength'],s['advanceWidth'],s['leftSideBearing'],
                    s['advanceHeight'],s['topSideBearing']));
        
        for x, in db.execute(sql_get_GlyphID):
                code2GID[s['code']] = x;

        if not s['code'] == -2:
                db.execute(sql_insert_cmap,
                           (fontid,s['code'],0,x,s['glyfType']));
        
        if s['refName'] in compGlyphRef:
                compGlyphRef2code[s['refName']] = x;

for s in compGlyph:
        #print s,s['GlyphName'];
        db.execute(sql_insert_composite_glyf,
                   (code2GID[s['code']],
                    compGlyphRef2code[s['GlyphName']],
                    s['flags'],
                    s['arg1'],
                    s['arg2'],
                    buffer(s['data'])));

sql="""
INSERT INTO info(
        FontID,
        key,
        value
) VALUES (
        ?,?,?
)
""";
db.executemany(sql,[(fontid,30,tt['head'].fontRevision),
                    (fontid,31,tt['head'].flags),
                    (fontid,32,tt['head'].unitsPerEm),
                    (fontid,33,tt['head'].created),
                    (fontid,34,tt['head'].modified),
                    (fontid,35,tt['head'].macStyle),
                    (fontid,36,tt['head'].lowestRecPPEM),
                    (fontid,37,tt['head'].fontDirectionHint),
                    (fontid,40,tt['hhea'].ascent),
                    (fontid,41,tt['hhea'].descent),
                    (fontid,42,tt['hhea'].lineGap),
                    (fontid,43,tt['hhea'].caretSlopeRise),
                    (fontid,44,tt['hhea'].caretSlopeRun),
                    (fontid,45,tt['hhea'].caretOffset)]);
db.commit();
db.close();

1.4 展開(実装4:インデックス作成)

DBインデックスを付けます。

import sqlite3
db = sqlite3.connect('font.db');
sql="""
CREATE INDEX idx_UCS4  ON cmap(UCS4);
"""
db.execute(sql);
db.close();

次回

 次回は作成したDBからのフォント生成をやります。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証