OpenTypeテーブル解析で詰まった所

パッケージ開発担当のよみです。

前回は機械学習のオープンソース「TensorFlow」を触りましたその流れで
次はカメラをと思ったのですが、いいカメラが手元にないため断念しました。。。
なので今触っている「TrueType」「OpenType」の解析時に自分が詰まった個所を自分メモ的に書いておこうと思います。

はじめに

以下の「glyfテーブル」には「locaテーブル」が必要になります、
さらに「locaテーブル」には必須テーブルの「maxp」と「head」テーブルが必要になります。
順番に読み込んでいればいい訳ではないので注意して下さい。(今回はglyfテーブルについて書くので特に説明しません)

glyfテーブル

とりあえず先頭10バイトにヘッダーがあります。
(MicrosoftのOpenTypeテーブル仕様のglyf説明ではなぜか省かれてます…
このページはころころ変わってたりするので公開時にはどうなっているかわかりませんが)

名前 バイト数 説明
numberOfContours 2バイト符号あり 0以上ならシンプルグリフ、負数ならコンポジットグリフ
xMin 2バイト符号あり グリフ内の最小X座標
yMin 2バイト符号あり グリフ内の最小Y座標
xMax 2バイト符号あり グリフ内の最大X座標
yMax 2バイト符号あり グリフ内の最大Y座標

次は「numberOfContours」の値を見てシンプルグリフなのかコンポジットグリフなのかで読み込む項目が変わります。

シンプルグリフ

この項目はflagsから下がそれなりに複雑で注意しなければいけないみたいです。

名前 バイト数 説明
endPtsOfContours[numberOfContours] 2バイト符号なし 各輪郭の最終点
instructionLength 2バイト符号なし 命令の合計バイト数
instructions[instructionLength] 1バイト符号なし グリフの命令バイトコードの配列(instructionLengthが0の場合この項目は読み込まない)
flags[numberOfContours+1] 1バイト符号なし アウトライングリフの各ポイントの詳細情報
xCoordinates[(flagsの内容により変化)] 1バイト符号なしor2バイト符号あり 輪郭点x座標。最初は0とし以降は前の値からの数値となる
yCoordinates[(flagsの内容により変化)] 1バイト符号なしor2バイト符号あり 輪郭点y座標。最初は0とし以降は前の値からの数値となる

まずflagsですが、ビット単位で意味があり以下のようになっています。

0ビット目 1:このポイントは曲線上にある。0:このポイントは曲線外にある
1ビット目 1:このX座標は1バイト符号なし。0:このX座標は2バイト符号あり
2ビット目 1:このY座標は1バイト符号なし。0:このY座標は2バイト符号あり
3ビット目 1:繰り返しで次も同じ値が入る。0:繰り返しでなく普通に読み込む
4ビット目 1:1ビット目が1の場合この座標は整数。1ビット目が0の場合X座標は前の値と同じ。0:2ビット目が0の場合この座標は負数
5ビット目 1:2ビット目が1の場合この座標は整数。2ビット目が0の場合X座標は前の値と同じ。0:2ビット目が0の場合この座標は負数
6ビット目/7ビット目 予約済み、現状意味なし0に設定

上記を意識しないといけないのは「flags」を読み込む時と「xCoordinates」「yCoordinates」を読み込む時です、
まず「flags」を読み込む際は3ビット目を確認して繰り返しなら、次読み込まれる1バイトは繰り返し回数が格納されています
ですので読み込む際はnumberOfContours+1ループ内で繰り返し回数分前回と同じ値を格納してスキップしないといけません。
<Pythonだとこんな感じ>

flags = []
for i in range(endPtsOfContours[len(endPtsOfContours)-1] + 1):
    flag = (1バイト読み込む)
    flags.append(flag)
    if flag & 0b0001000:
        sidecount = (1バイト読み込む)
        i += sidecount
        for j in range(sidecount):
            flags.append(flag)

次に「xCoordinates」と「yCoordinates」ですがまず1ビット目or2ビット目を確認し
1バイトで読み込むのか、2バイトで読み込むのか調べます、
次に1バイトの場合4ビット目or5ビット目を確認し、整数なのか負数なのか決めます。
2バイトの場合は4ビット目or5ビット目を確認し繰り返しなら0を格納しスキップします。
<Pythonだとこんな感じ>

xCoordinates = []
for flag in flags:
    if flag & 0b00000010:
        data = (1バイト読み込む)
        if flag & 0b00010000:
            (dataを整数する)
        else:
            (dataを負数にする)
    else:
        if flag & 0b00010000:
            # 前回から変化がない場合0を設定する
            data = 0
        else:
            data = (2バイト読み込む)
    xCoordinates.append(data)

なお「xCoordinates」と「yCoordinates」はxCoordinatesをすべて読み込んでからyCoordinatesの読み込みを行うので
2回flags[]でループさせます。

コンポジットグリフ

「A」と「-」をくっ付けたような文字「Ā」等に使用されるグリフ。(環境依存だから表示されないかも)

名前 バイト数 説明
flags 2バイト符号なし 詳細情報(各ビットに意味があります)
glyphIndex 2バイト符号なし コンポーネントのグリフインデックス
argument1 1バイト/2バイト・符号あり/符号なし コンポーネントまたはポイント番号のxオフセット
argument2 1バイト/2バイト・符号あり/符号なし コンポーネントまたはポイント番号のyオフセット
変換オプション... 2バイト符号あり(下位14ビットは小数点以下を意味する) flagsにより追加されるオプション数が変わる

flagsの各ビットの意味

0ビット目 1:「argument1・2」は2バイトである。0:「argument1・2」は1バイトである
1ビット目 1:「argument1・2」は符号付である。0:「argument1・2」は符号無である
2ビット目 よく分からなかった。。。(コチラ参照)
3ビット目 1:変換オプションscaleを読み込む。0:変換オプションはない
4ビット目 予約済み、現状意味なし0に設定
5ビット目 1:この次にもまだグリフが存在する。0:もう読み込むグリフはない
6ビット目 1:変換オプションxscaleとyscale。0:変換オプションはない
7ビット目 1:変換オプションxscaleとscale01とscale10とyscale。0:変換オプションはない
8ビット目 1:最後の構成要素の後に、合成文字の指示がある。0:指示がない
9ビット目 よく分からなかった。。。(コチラ参照)
10ビット目 重ねるか重ねないからしいがOpenTypeでは使用しないらしい(Appleの仕様)
11ビット目 よく分からなかった。。。(コチラ参照)
12ビット目 よく分からなかった。。。(コチラ参照)
13ビット目 予約済み、現状意味なし0に設定
14ビット目 予約済み、現状意味なし0に設定
15ビット目 予約済み、現状意味なし0に設定

flagsによって長さがガンガン変わるので注意が必要です
・flagの1バイト目を確認し2バイトで読み込むのか、1バイトで読み込むのかの確認
・その値は符号有りなのか無しなのかをflagの2バイト目で確認
・変換オプションがあるのか、あればどの変換オプションがあるのかをflagの3ビット目、6ビット目、7ビット目で確認
・5ビット目でまだ読み込む輪郭が存在するのかを確認
・すべての読み込みが完了した後、8ビット目で最後の変換オプションがあるのか確認
が一連の流れです。。。多分ソースで見た方がわかりやすいです

<Pythonだとこんな感じ>

while True:
    flags = (2バイト読み込む)
    glyphIndex = (2バイト読み込む)
    if flags & 0x0001:
        if flags & 0x0002:
            argument1 = (2バイト符号付読み込む)
            argument2 = (2バイト符号付読み込む)
        else:
            argument1 = (2バイト読み込む)
            argument2 = (2バイト読み込む)
    else:
        if flags & 0x0002:
            argument1 = (2バイト符号付読み込む)
            argument2 = (2バイト符号付読み込む)
        else:
            argument1 = (1バイト読み込む)
            argument2 = (1バイト読み込む)
    if flags & 0x0008:
        # 変換オプションはscale
        scale = (2バイト読み込む)
    elif flags & 0x0040:
        # 変換オプションはxscaleとyscale
        xscale= (2バイト読み込む)
        yscale= (2バイト読み込む)
    elif flags & 0x0080:
        # 変換オプションはxscaleとscale01とscale10とyscale
        xscale= (2バイト読み込む)
        scale01= (2バイト読み込む)
        scale10= (2バイト読み込む)
        yscale= (2バイト読み込む)
    if flags & 0x0020 == 0:
        # 繰り返しフラグが無ければループを抜ける
        break
if flags & 0x0100:
    numInstr = (2バイト読み込む)
    instr = []
    for i in range(numInstr):
        instr.append((1バイト読み込む))

参考

MicrosoftのOpenTypeテーブル仕様説明
https://docs.microsoft.com/en-us/typography/opentype/spec/glyf
AppleのOpenTypeテーブル仕様説明
https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6glyf.html
OpenTypeの解析ツールを作っていた方の解説(現在解析ツールはダウンロードできません)
http://vanillasky-room.cocolog-nifty.com/blog/2008/03/opentype8glyf-9.html
TrueTypeのテーブル解説(glyfテーブル)
http://d.hatena.ne.jp/project_the_tower2/20100630/1277838854

最後に

正直まだわからない箇所がありすぎて雲をつかむような状態です。。。
本当に私はこれをどうにかできるのでしょうか、別にフォントに興味がある訳ではないので
あまり楽しくはないですが、どうにかできたらうれしいですね