にせねこメモ

はてなダイアリーがUTF-8じゃないので移ってきました。

このブログについて

文字・フォント・プログラム・技術・趣味などについて、Twitterでは書きづらい長い内容などをまとめるためのブログです。基本的には自分用のメモとして書いている部分が多いです。

リンク等

Pixiv http://pixiv.me/nixeneko
Tumblr http://nixeneko.tumblr.com/ 絵。Pixivアカウント持ってなくても見れます
Pleroma @nixeneko@nixeneko.info Mastodonとかやってる人はフォローしてください
Twitter @nixeneko  
MediaMarker http://mediamarker.net/u/nixeneko/ 主に文字・言語関係の蔵書
GitHub https://github.com/nixeneko プログラム用、あまり使ってない
ナナシスID ZhRYMnA
デレステID 421820148

同人誌(無料公開)

http://nixeneko.hatenablog.com/entry/c88_russian_alphabethttp://nixeneko.hatenablog.com/entry/c90_greek_latin_cyrillichttp://nixeneko.hatenablog.com/entry/20170811_dentyu

ノートPCのCPU速度が0.38GHz固定状態になる問題

Windows 10の載ったノートPCを使っていたら、ある日突然動作が遅くなってしまった。リカバリすれば大丈夫かと思い、OSを「このPCを初期状態に戻す」により初期化してみたが治らなかった。

システム:

タスクマネージャを開いてみたところ、CPUの速度が0.38 GHzに張り付いて、使用率は14%のまま推移している。これはおかしい。

f:id:nixeneko:20190524220926p:plain
タスクマネージャー。CPUの速度が0.38 GHzになっている

また、Windows 10をセーフモードで起動してみたところ、CPUの速度が2.71 GHzとなり、正常に動いているようだった。


ネットを検索したところ、次のようなサイトがみつかった。
Solved: CPU frequency stuck at 0.38GHz on battery - HP Support Community - 6979093

これによると、解決法は次のようにするとよいらしい。

  1. 「デバイス マネージャー」を開く
  2. ファームウェア」→「システム ファームウェア」を右クリックし、「ドライバーの更新」を選択
  3. 「ドライバー ソフトウェアの最新版を自動検索」を選択

あとはウィザードに従い、再起動すると、正常に戻った。

f:id:nixeneko:20190524223848p:plain
治った時のデバイスマネージャー。速度が2.71 GHzとなっている

(2019-06-19追記)
一度は直ったものの、しばらく使っているとまた同じ現象に悩まされるようになった。システムファームウェアのドライバを元に戻したりするとまた復活したりするのだが、しばらくすると元に戻ってしまうようだ。

設定→システム→バッテリー→「バッテリー残量が次の数値を下回ったときにバッテリー節約機能を自動的にオンにする」をオフにして再起動したところ、2.71GHzの速度で動くようになった(気がする)。

(2019-06-28追記)
「バッテリー残量が次の数値を下回ったときにバッテリー節約機能を自動的にオンにする」をオフにしてても0.38 GHzになったので関係ない気がする…。

ハードリセットを行ったところ、速度が速くもどった。しばらく様子見する。
Windows OS 搭載のノートパソコンでのハードリセットを行う方法 | サポート 公式 | ASUS 日本

Twitterで旧字が化ける? CJK互換漢字という罠

概要

Twitterに投稿するとCJK互換漢字が対応する統合漢字に化けるので、投稿時に化けないStandard Variant形式と互換漢字とを相互変換するページを作成した: 互換漢字-異体字セレクタ コンバータ

はじめに

Twitterで「社」(U+FA4C)、「羽」(U+FA1E)などの一部の旧漢字を投稿しようとすると、「社」(U+793E)、「羽」(U+7FBD)に化ける。

Twitterに次の文字列を投稿すると

神(U+FA19)と神(U+795E)
福(U+FA1B)と福(U+798F)
羽(U+FA1E)と羽(U+7FBD)
既(U+FA42)と既(U+65E2)
梅(U+FA44)と梅(U+6885)
社(U+FA4C)と社(U+793E)
練(U+FA57)と練(U+7DF4)
者(U+FA5B)と者(U+8005)

こうなる:


左側の字が右側の字と同じになっている。


これは一体どういうことなのだろうか? 実は、Unicodeの問題である。

CJK互換漢字

これらの化ける文字は、UnicodeにおいてCJK互換漢字という領域に収録されている。互換漢字に対し、基本となる漢字はCJK統合漢字と呼ばれる。CJKはChina, Japan, Koreaの略で、各国で微妙に字体が違っても、同一とされた*1漢字は国に関わらず一つとし、原則として重複収録はしない方針となっている。
しかし、CJK互換漢字は名前の通り、互換性を保つために収録された漢字である。それまでの文字コード等との互換性を保つ関係*2で「同じ文字を重複して収録した」扱いであり、CJK統合漢字の中に対応する漢字をもつ。

Unicode正規化

ここで、Unicodeは検索等の利便性のために、「Unicode正規化(normalization)」という処理を提供している。これは、記号付きのアルファベットなど、二種類以上のコード列で表すことができる文字列を適切に比較したりするために重要な処理である。しかし、CJK互換漢字に対してUnicode正規化を行うと、対応するCJK統合漢字に「化けて」しまう*3

Twitter投稿時のUnicode正規化

Twitterは、2015年から投稿時にUnicode正規化を行うようになった*4。そのため、CJK互換漢字をTwitterに投稿すると対応するCJK統合漢字に置換されてしまい、CJK互換漢字を投稿することはできなくなった。

Standardized Variant

とはいっても、Unicode正規化によってCJK互換漢字が化けてしまうと困る。そういった場合のために、Unicode異体字セレクタという仕組みを使ってCJK互換漢字相当の漢字を表現できる枠組みが作られた(2013年9月制定のUnicode 6.3)。これによって表現されたものをStandardized Variantとよぶ。これは、Unicode Character DatabaseのStandardizedVariants.txtの後半に定義されている。
この仕組みを使うと、Unicode正規化を行っても化けないため、Twitter等にCJK互換漢字の字形の漢字を投稿することができる(ただし、正しい形で表示されるかは環境による)。

互換漢字↔Standardized Variant変換器

CJK互換漢字はともかく、Standardized Variant形式を入力することは不便なので、CJK互換漢字とStandardized Variant形式を相互に変換するページを作成した。

TwitterにCJK互換漢字に含まれる字形を投稿したい場合等に利用していただきたい。

Standardized Variantの実際の表示


Standardized Variantで表現されたものは、環境によるが互換漢字の字形で表示される。手元のWindows 10環境では、Firefoxでは互換漢字の字形で表示されたが、Chromeではそうはならなかった。
f:id:nixeneko:20190316185449p:plain
Firefoxで見たStandardized VariantによるTwitter投稿結果
(20190318追記)これを正しく互換漢字の字形で表示するためにはフォントが対応している必要がある*5Chrome等では対応フォント以外で表示すると互換漢字の形で表示できない。(6.0 Marshmallow以降の)Androidのデフォルトフォントである源ノ角ゴシックは対応しているので、Androidでは正しく互換漢字の字形で表示される。一方、Firefoxは独自に互換漢字の字形で表示するような実装をしている可能性があり、フォントによらず表示できるようだ。Standardized Variant (というより異体字セレクタ)を使った投稿をする場合は、このようにフォントやソフトウェアの対応が微妙であり、必ずしも狙った形で表示されないことに留意する必要がある。(追記終)

ちなみに、ちゃんと変換できているかなどは、

などのページで確認できる。

*1:細かい字体の違いを無視し同一とみなすことを包摂(unification)という。

*2:「ラウンドトリップ変換(round-trip conversion)の確保」といい、既存の文字コード(Shift_JISなど)からUnicodeに変換し、その後元の文字コードに再度変換したときに、内容(文字コード列)が同一になるように保証されている。

*3:Unicode正規化における等価性は正準等価と互換等価という2種類がある(Unicodeの等価性 - Wikipedia)のだが、CJK互換漢字については正準等価となっている。正準等価は見た目も機能としても同じものに対して適用されるべきものなので、互換漢字という見た目が異なるものに対して適用されているので問題となっている。Unicode正規化には種類があり、互換等価については対象としないものもあるが、正準等価は常に対象になる。

*4:厳密な時期はこちらで推定されている: TwitterにUnicode正規化が導入された時刻の推定 by zeeksphere - Togetter

*5:https://twitter.com/monokano/status/1106927404579061761

iPodのLast.fm scrobblingが死んだ

10年位前からLast.fmを利用している。Last.fmとは、再生した音楽を記録することができるサービスである。私のユーザーページはこちら。

さて、このLast.fmで再生した曲を投稿(scrobbleという)するには、専用のアプリを使う。
ところが、Windowsでは、iTunesが12.7.xになってから(たぶん)、古いLast.fm Desktop Appは使えなくなり、新しいLast.fm Desctop Scrobblerが用意された。これにより、古いAppでサポートされていたiPodのscrobblingは使えなくなった。


以前は、iPodをPCに接続するとダイアログが開き、新規に再生されたトラックをscrobbleすることができたが、これがなくなってしまった。

iPod Touch向けworkaround

現在利用しているのがiPod Touch (第 5 世代)で、iOSのバージョンは9.3.5である。なので、iOS向けのLast.fm Scrobbler iOS Appを入れて、曲を再生した後にアプリのScanを押すことによって再生した曲をscrobbleすることができる。
ただし、このAppによるscrobbleは、再生回数の変化を見ているらしく、PC上のiTunesで再生したものについても拾ってscrobbleしてしまう。iTunesでの再生時にもscrobbleしているので、二重にscrobbleされることになる。
そのため現在は、iTunesに接続する前に一度scanしてscrobbleし、iTunesに繋いで同期した後にもう一度scanをし、変更をdiscardすることで、二重でscrobbleしてしまうことを防いでいる。

面倒臭い…。

地方住みでもアニメがみたい―東京でなくてもアニメは放送してるというのは本当か

前にTwitterで、「別に東京でなくてもBSとかで放送あるからアニメ見れるよ」って感じの話を見たのだけど、しょぼいカレンダーを見ながら2018年7月開始の今期新アニメの中で、どれだけが長野県で視聴できるかを調べてみた。今期開始でない、長期に渡って放送している番組については扱わないので注意。
また、テレビ放送だけで、配信についてはここでは扱わない。

×→放送なし
○→放送あり(東京の地上波より放送が遅い)
◎→東京の地上波より放送が早いか同時

タイトル 地上波 BS無料 AT-X 備考
ISLAND ×
はねバド! × BS11AT-X・MX同時
バキ × × × 日テレ
千銃士 × ×
Back Street Girls -ゴクドルズ- × BS11・MX同時
中間管理録トネガワ × × × 日テレ
すのはら荘の管理人さん × AT-X最速
異世界魔王と召喚少女の奴隷魔術 × AT-X最速
夢王国と眠れる100人の王子様 × AT-X最速
BANANA FISH × × フジテレビ系
七星のスバル × × TBS
ちおちゃんの通学路 ×
殺戮の天使 × AT-X最速
音楽少女 × AT-X最速, BS11・MX同時
はるかなレシーブ × AT-X最速
ゾイドワイルド × × TBS系
はたらく細胞 × BS11・MX同時
百錬の覇王と聖約の戦乙女 × BS11・MX同時
スペースバグ × × × MX
あそびあそばせ × AT-X最速
プラネット・ウィズ ×
銀魂. (銀ノ魂篇 後半戦) × × × テレビ東京
悪偶 -天才人形- × × BS11・MX同時
邪神ちゃんドロップキック × BSフジ最速
Phantom in the Twilight × × BSフジ・MX同時
京都寺町三条のホームズ × テレビ東京
深夜!天才バカボン × × テレビ東京
オーバーロード × AT-X最速
アンゴルモア元寇合戦記 × ×
Free!-Dive to the Future- ×
天狼 Sirius the Jaeger × AT-X最速, BS11・MX同時
少女☆歌劇 レヴュースタァライト × × TBS系
ハイスコアガール × × BS11・MX同時
ロード オブ ヴァーミリオン 紅蓮の王 ×
ハッピーシュガーライフ × TBS系
ぐらんぶる × × TBS系
ゆらぎ荘の幽奈さん ×
つくもがみ貸します × × NHK総合
進撃の巨人 Season3 × × NHK総合
ヤマノススメ サードシーズン × 15分枠
おしえて魔法のペンデュラム~リルリルフェアリル × × × キッズステーション最速, アニマックス, MX. 15分枠
じょしおちっ!~2階から女の子が…降ってきた!?~ × × 5分枠
One Room セカンドシーズン × × 5分枠
アイドルマスター シンデレラガールズ劇場(第3期) × 5分枠
陰陽師・平安物語 × × × MX. 5分枠
働くお兄さん!の2! × MX. 5分枠
闇芝居(第6期) × × テレビ東京系. 5分枠
兄に付ける薬はない!2 -快把我哥帯走2- × × × MX. 5分枠
BanG Dream! ガルパ☆ピコ × × 5分枠

作品数: 49

最速または地上波同時 放送あり
地上波のみ 2作品(4.1%) 4作品(8.2%)
BS無料放送あり 14作品(28.6%) 38作品(77.6%)
AT-Xあり 21作品(42.9%) 42作品(85.7%)

東京の地上波と同時あるいは最速となると少なくなってしまうが、放送だけであればBSの無料放送で8割近くカバーできている。
BSで放送がないものについてはAT-Xで補完できるものもあるが、日テレ放送の番組(BSでの放送がある場合もあるが3ヶ月ラグがあくことも)やMXでしか放送されない作品などで補完できないものがある。その場合、見たければネット配信に頼るしかなさそう。
東京の地上波が最速である場合が多いので、実況がしたければ結局東京が強い。

それでも現代ではdアニメストアNetflix、プライムビデオといった配信サービスがあり、そこで配信している場合も多いため、実況とかしないなら配信サービス契約した方が快適な気がする。

Windows 10上のDarknetでYolo v3をトレーニングしOpenCVから使ってみる

DarknetはCで書かれたディープラーニングフレームワークである。物体検出のYOLOというネットワークの著者実装がDarknet上で行われている。
もともとはLinux等で動かすもののようだが、ありがたいことにWindowsコンパイルできるようにしたフォークが存在している:
github.com
これを利用してWindowsで動かしてみる。

環境

事前準備

Visual Studio 2017 Community

まずVisual Studio 2017 Communityをインストール。
プロジェクトがVisual Studio 2015のものなので、v140ツールセットも入れる。インストーラを起動し、「変更」を押し、右側のペインの「C++によるデスクトップ開発」の下のオプションから「デスクトップ用VC++ 2015.3 v14.00 (v140) ツールセット」にチェックを入れ、「変更」を押してインストール。

CUDA/cuDNNのインストール

次にCUDAとcuDNNをインストール。今回入れたバージョンは次の通り。リポジトリにはそれぞれ9.1, 7.0を使うように書かれていたが、最新版でも問題なさそうである。

  • CUDA 9.2
  • cuDNN 7.1

CUDAのインストールはインストーラに従えばよい。cuDNNはCUDAと同じディレクトリに突っ込んでおくと楽。

PathにC:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v9.2\binを追加しておく。

OpenCVのインストール

OpenCVをダウンロードしてきて展開する。最新はv3.4.2だが、リポジトリの注意書きにあるようにv3.4.0(またはそれ以前)を使う。
OpenCVのReleasesから3.4.0のWin packをダウンロードしてきて適当な場所(以下、C:\opencv340とする)に展開する。

OpenCVのdllが存在するディレクトリ(C:\opencv340\opencv\build\x64\vc14\bin)にPathを通しておく。

コンパイル

ここからソースコード一式をダウンロードしてくる。ReleasesからYolo_v3のタグがついたものをダウンロードしてきたが、git cloneしても問題ないはず。

ダウンロードしてきたものを展開し、build/darknet/darknet.slnをVisual Studio 2017 (Community)で開く。
開いた際にツールセットを変換するか聞かれたのだが変換しないようにした。(これはどうするのが最適なのか不明)

ソリューション構成・プラットフォームの変更

構成: Release, プラットフォーム: x64に変更。

カスタム ビルド ツールの指定

プロジェクト名(darknet)を右クリックして「ビルドの依存関係」→「ビルドのカスタマイズ」を開き、「CUDA 9.1」のチェックを外し、代りに「CUDA 9.2」をオンにする。

プロジェクトのプロパティの編集

プロジェクト名を右クリックして「プロパティ」を開き、

これらを編集して、実際にインストールしたOpenCVディレクトリに合わせる(C:\opencv_3.0\…C:\opencv340\…)。
また、
「リンカー」→「全般」→「追加のライブラリディレクトリ」の中の

  • $(CUDA_PATH)lib\$(PlatformName)$(CUDA_PATH)\lib\$(PlatformName)

が変な気がしたので\を足しておいた。

コンパイル

「ビルド」→「ソリューションのビルド」などでコンパイルする。

すると、build\darknet\x64以下にdarknet.exeが出来上がる。

試す

ちゃんと動くか確認する。
YOLOのページ(https://pjreddie.com/darknet/yolo/)からYOLOv3のweightsファイルをダウンロードしてきてbuild\darknet\x64以下に入れる。
コマンドプロンプトbuild\darknet\x64を開き、

darknet.exe detector test data/coco.data yolov3.cfg yolov3.weights -i 0 -thresh 0.25 dog.jpg

を実行する。これはdarknet_yolo_v3.cmdの中身でもある。認識がうまくいくと次のような認識結果が表示される。
f:id:nixeneko:20180803152630p:plain

トレーニング準備

トレーニングについては

に詳しく書いてある。
また、データセットの見本として、

のページの「The data set I composed for this article can be found here (19.4Mb).」と書かれているところからダウンロードできるデータを参考にできる。(これをそのまま使う場合、改行コードがWindows用\r\nになっているため変換が必要になるかもしれないらしい)

今回はイラストと顔の領域を指定したデータを用意し、イラストの顔検出をやってみる。

学習データを用意

アノテーションLabelImg等を使うと楽。形式が異なっている場合は適宜変換する。

学習データは、画像ファイル(.jpg)群*1と(拡張子以外)同名のテキストファイル(.txt)を用意する。画像ファイルとテキストファイルは同一のディレクトリに入れる。テキストファイルはアノテーションを定義する。
アノテーションの形式は、

<クラスNo.> <領域の中心のx座標> <領域の中心のy座標> <領域の幅> <領域の高さ>

である。クラスNo.は.namesファイルで定義したものに対応する番号である(0オリジン)。今回はクラスが1つだけの場合を考えるのでクラスNoは必ず0になる。
座標や幅・高さは左上を(0.0, 0.0), 右下を(1.0, 1.0)とする割合で示す。2,3番目の座標は領域の中心の座標であることに注意。一行に一つの領域をかき、領域が複数になる場合は同様の内容を複数行書くことになる。

一例をあげると

0 0.2 0.2 0.1 0.1

は0番目のクラス、領域の中心は画像の左から2/10、上から2/10の位置で、幅が画像の1/10、高さが画像の1/10となる。

画像ファイルのリスト

学習用(train.txt)とテスト用(test.txt)に、画像ファイルの一覧を書いたファイルを用意する。darknet.exeからの相対パスを一行一ファイルとして羅列したものとなる。画像ファイル群を適当な割合で分割して学習用とテスト用にする。今回は1000個弱の画像ファイルを学習:テスト=9:1に分けた。

画像ファイルは最終的にbuild\darknet\x64\data\obj以下に入れるとすると、train.txttest.txtは次のような内容になる。

data/obj/00001.jpg
data/obj/00002.jpg
data/obj/00003.jpg

これらをbuild\darknet\x64\data\obj\train.txt, build\darknet\x64\data\obj\test.txtに用意した。

クラス名の設定

.namesファイルがクラス名の設定である。一行に一つのクラスを書く。今回は一つのクラスだけにするので、obj.namesファイルを

face

とだけ書いて保存する。

.dataファイルの設定

classes= 1
train  = data/obj/train.txt
valid  = data/obj/test.txt
#difficult = data/difficult_2007_test.txt
names = data/obj.names
backup = backup/

クラス数を1に変更、trainvalidに先ほど用意したtrain.txt, test.txtを指定。
クラスの名前を書いたファイルをnamesに指定。backupはトレーニング結果の重みの出力先となる。

cfgファイルの設定

yolov3.cfgをコピーしてyolov3-obj.cfgとし、編集する。

max_batchesは何回繰り返すかを指定する。今回は10000とした(トレーニングに丸1日程度かかった)。

GPUのメモリが6GBと少ないのでsubdivisionを増やす。メモリが十分ある場合は小さい値(8や16)で問題ないが、メモリ関係のエラーで止まる場合は32, 64と値を増やすといいらしい。

  • subdivision=16subdivision=32

クラスの数に合わせて、3ヵ所について次の変更を施す。filtersはfilters=(クラス数+5)×3で計算し、クラスが1つの場合は18となる。

  • classes=80classes=1
  • filters=255filters=18

トレーニング

ネットワークの重みの初期値としてdarknet53.conv.74をダウンロードしてきて使う。

これをbuild\darknet\x64に入れる。

build\darknet\x64に移動して、次のコマンドを実行する。

darknet.exe detector train data/obj.data yolov3-obj.cfg darknet53.conv.74

lossが下がっていくのを眺めながら、トレーニングが終了するまで待つ(あるいは適当なところで終了する)。

トレーニング途中でdarknetが終了してしまう場合

darknet.exeと同じディレクトリにbad.listというファイルが生成され、問題となった画像ファイルが追記されるようだ。このファイルを用意したtrain.txttest.txtから取り除いてトレーニングし直すといいっぽい。これを学習が止まらなくなるまで繰り返す。

結果の確認

トレーニング結果は

darknet.exe detector test data/obj.data yolov3-obj.cfg backup/yolov3-obj_final.weghts -i 0

で確認できる。動画だと

darknet.exe detector test data/obj.data yolov3-obj.cfg backup/yolov3-obj_final.weghts moviefile.mp4 -i 0 -out_filename result.avi

とか。

OpenCV/Pythonで動かす

最新のOpenCVにはDNNモジュールがあり、darknetのネットワークも利用できる。
ただし、YOLOv3(内部で利用しているshortcutレイヤ)を使うためにはOpenCV 3.4.2より前のバージョンでは対応していないので、最新版をインストールする必要がある。Python版はpip install opencv-pythonなどで入れられる。


OpenCV公式のsamples/dnn/object_detection.pyにDNNモジュールによる物体検出のサンプルがあるので、これ改変して動くものをつくった。

コード

動作環境

gist.github.com

ダウンロード

weightsファイル等も含めて動かせるセットは次からダウンロードできる。Python 3向け。OpenCV 3.4.2より前のバージョンでは動かないので注意。

備考

入力に与えたい画像と、ネットワークの受け付ける入力の形が異なるので、cv2.dnn.blobFromImage関数を利用してデータの橋渡しをしてやる必要がある、ということのようだ。.cfgファイルがネットワークを定義しているので、入力画像の大きさについてもここに定義されている(今回は(416, 416))。
また、スケーリングを行い入力が0.0~1.0の範囲になるようにしているようだ。
OpenCVがBGRなので、(BGRでなく)RGB順を使うよう指定している。

動作画面

f:id:nixeneko:20180807170439p:plain
まあ動く。

*1:.jpg以外の形式も使えるかは確認してない。

OpenType/CFFのフォントを読んでみる

フォントと仲良くなりたい。
どうすれば仲良くなれるのか。フォントの構造を知れば多少は仲良くなったと言えるのではないか。という訳で、現在コンピュータで一般的に使われているOpenTypeフォントを、構造を調べつつ読んでいくことにする。


OpenTypeフォントはアウトライン形式にTrueType形式かPostScript (CFF)形式のどちらかを選ぶことができる。TrueTypeアウトラインのフォントを読むのは以前やった。

今回は、PostScriptアウトラインのフォントを読むのをやってみる。
実際に読んでいくフォントとして、Noto Sans CJK JP RegularのVersion 1.004を用意した。(NotoSansCJKjp-Regular.otf)

「ねこ」という文字列を描画してみたい。


全部のテーブルを読むのは大変なので、アウトラインの描画に必要な部分だけ押さえていくことにする。
バイナリエディタ(今回はStirlingを利用した)を使ってNotoSansCJKjp-Regular.otfを開いて、見ていく。
f:id:nixeneko:20180518204021p:plain

OpenTypeの仕様書は次にあるので、これを参照しつつ読んでいく。

また、TrueTypeアウトラインのOpenTypeフォントと共通な部分については前回の記事も理解の役に立つかもしれない。

OpenType font file

まず、最初の4バイトが4F 54 54 4F'OTTO'であることからCFFアウトラインであることがわかる。
Offset Tableは次のようになっている。

Offset Table

Type Name Value 備考
uint32 sfntVersion 0x4F54544F ('OTTO') CFF
uint16 numTables 0x0010 (16) Number of tables.
uint16 searchRange 0x0100 (256) (Maximum power of 2 <= numTables) x 16.
uint16 entrySelector 0x0004 (4) Log2(maximum power of 2 <= numTables).
uint16 rangeShift 0x0000 (0) NumTables x 16-searchRange.

numTablesに示された通り、テーブルが16個ある。
テーブルは次のような形式である。

Table Records

Type Name Description
uint32 tag テーブルを特定するための4文字の識別子
uint32 checkSum テーブルのチェックサム
Offset32 offset テーブルのフォントファイルの先頭からのオフセット
uint32 length テーブルの長さ

に従って読む。どんなテーブルがあるかを見ていくと、次のようになっている。

tag checkSum offset length
'BASE' 0xEDFAF516 0x00FAA7EC 0x000000F0
'CFF ' 0x099D40CA 0x00045230 0x00EBA647
'DSIG' 0x00000001 0x00F3F594 0x00000008
'GPOS' 0x6016F4D4 0x00F3F59C 0x0000A97A
'GSUB' 0x4B9CB038 0x00F49F18 0x0002054A
'OS/2' 0x9F6317EE 0x00000170 0x00000060
'VORG' 0xBB25F31E 0x00F6A464 0x000003A0
'cmap' 0x28008683 0x00000FB8 0x00044255
'head' 0x09E560BE 0x0000010C 0x00000036
'hhea' 0x0C1209A5 0x00000144 0x00000024
'hmtx' 0x934BC902 0x00F6A804 0x0003FFC4
'maxp' 0xFFFF5000 0x00000168 0x00000006
'name' 0x4CE51A83 0x000001D0 0x00000DE5
'post' 0xFF860032 0x00045210 0x00000020
'vhea' 0x097F15AE 0x00FAA7C8 0x00000024
'vmtx' 0xD03AB332 0x00EFF878 0x0003FD1C

ここで、

  • TrueType・CFF共通の必須テーブルがcmap, head, hhea, hmtx, maxp, name, OS/2, post
  • CFF独自のものがCFF, VORG (VORGはoptional)
  • 高度組版機能向けがBASE, GDEF, GPOS, GSUB
  • 他のoptionalテーブルがvhea, vmtx

である。

'head' - Font Header Table

Offset Tableから、フォントファイル先頭から数えて0x10Cバイト目から0x142バイト目までが'head'である。

uint16 majorVersion 0x0001 1固定
uint16 minorVersion 0x0000 0固定
Fixed fontRevision 0x00010106 およそ1.004
uint32 checkSumAdjustment 0x9B4A5969
uint32 magicNumber 0x5F0F3CF5 0x5F0F3CF5固定
uint16 flags 0x0003 Bit 0: Baseline for font at y=0;
Bit 1: Left sidebearing point at x=0
uint16 unitsPerEm 0x03E8 1000
LONGDATETIME created 0x00000000D1A40DF0 2015-06-15T05:06:56+00:00
LONGDATETIME modified 0x00000000D1A40DF0 同上
int16 xMin 0xFC16 -1002; バウンディングボックスのx最小値
int16 yMin 0xFBE8 -1048; 同y最小値
int16 xMax 0x0B70 2928; 同x最大値
int16 yMax 0x0710 1808; 同y最大値
uint16 macStyle 0x0000
uint16 lowestRecPPEM 0x0003 最小可読サイズ3px
int16 fontDirectionHint 0x0002 2固定
int16 indexToLocFormat 0x0000 short offsets
int16 glyphDataFormat 0x0000 0固定

'maxp' - Maximum Profile

0x00000168から6バイト。

Type Name Value 備考
Fixed version 0x00005000 0x00005000 for version 0.5
uint16 numGlyphs 0xFFFF フォントに含まれるグリフの数

グリフ数がOpenTypeの仕様が許す最大(65535個)になっている。

'cmap' - Character To Glyph Index Mapping Table

文字コードからGlyph ID (GID)を引くための情報が記録されている。offset 0x00000FB8から読む。

cmap Header

Type Name Value 備考
uint16 version 0x0000 0
uint16 numTables 0x0006 encoding tableの数: 6個

6つのencoding tableが存在しているのがわかる。Encoding tableの形式は

Type Name 備考
uint16 platformID Platform ID
uint16 encodingID Platform固有encoding ID
Offset32 offset このテーブル先頭からのオフセット

なので、読み出すと、次のようになっている。

platformID encodingID offset
0x0000 (Unicode) 0x0003 (Unicode 2.0 and onwards semantics, Unicode BMP only) 0x00006AC1
0x0000 (Unicode) 0x0004 (Unicode 2.0 and onwards semantics, Unicode full repertoire) 0x00014CF5
0x0000 (Unicode) 0x0005 (Unicode Variation Sequences) 0x00000034
0x0001 (Macintosh) 0x0001 (Japanese) 0x00006AB5
0x0003 (Windows) 0x0001 (Unicode BMP (UCS-2)) 0x00006AC1
0x0003 (Windows) 0x000A (Unicode UCS-4) 0x00014CF5

「ねこ」(U+306D, U+3053)はBMP内で収まるし、Unicode BMP (UCS-2)が妥当だろう。Windowsのつもりでいくと、platformID=0x0003 (Windows), encodingID=0x0001 (Unicode BMP (UCS-2))のサブテーブルはオフセットが0x00006AC1なので*1、0x00000FB8+0x00006AC1=0x7A79番地から読んでいく。

00 04で始まるので'cmap' Subtable Format 4であることがわかる。サブテーブルの長さが0xE234=57908と長い。

cmap Subtable Format 4
Type Name Value 備考
uint16 format 0x0004 Format number 4
uint16 length 0xE234 57908; このsubtableの長さ
uint16 language 0x0000 Mac用以外では0
uint16 segCountX2 0x0CB6 3254; 2 × segCount. よってsegCount=1627
uint16 searchRange 0x0800 2 × (2**floor(log2(segCount)))
uint16 entrySelector 0x000A log2(searchRange/2)
uint16 rangeShift 0x04B6 2 × segCount - searchRange
uint16*1627 endCount[segCount] (0x7A87番地~) End characterCode for each segment, last=0xFFFF.
uint16 reservedPad 0x0000 0固定 (0x873D番地)
uint16*1627 startCount[segCount] (0x873F番地~) Start character code for each segment.
int16*1627 idDelta[segCount] (0x93F5番地~) Delta for all character codes in segment.
uint16*1627 idRangeOffset[segCount] (0xA0AB番地~) Offsets into glyphIdArray or 0
uint16 glyphIdArray[ ] Glyph index array (arbitrary length)

「ねこ」(U+306D, U+3053)なので、ひらがなの辺りに固まって定義されているだろうと考えられる。
endCountを順に見ていって、初めて0x306D, 0x3053を超えるところを調べると、0x3096 (0x7BBB番地)がそれであり、そのindexは(0x7BBB-0x7A87)/2 = 0x9A (154)である。対応するstartCountは0x873F+0x9A*2=0x8873番地の0x3041である。idDeltaは0xD56C (0x9529番地)であり、idRangeOffsetは0x0000 (0xA1DF番地)である。

index endCount startCount idDelta idRangeOffset
154 0x3096 0x3041 0xD56C (-10900) 0

ここから、U+306D, U+3053はstartCount~endCountの間に挟まれているのでマッピングが定義されていることがわかる。idRangeOffsetが0なので、idDeltaを加算してGlyph IDを計算すると、

文字 文字コード Glyph ID
0x306D 0x5D9
0x3053 0x5BF

となる。

'hmtx' - Horizontal Metrics

グリフ幅と左サイドベアリングが記録されている。

Offsetが0x00F6A804なのでそこから読む。Glyph IDに対応する場所(ね: 0x00F6A804+0x5D9*4=0xF6BF68, こ: 0x00F6A804+0x5BF*4=0xF6BF00)を読むと、次のようになっている。

文字 Glyph ID advanceWidth lsb
0x5D9 0x03E8 (1000) 0x003B
0x5BF 0x03E8 (1000) 0x00B0

advanceWidth、つまりグリフ幅は両方とも1000であることが分かる。

'CFF ' - Compact Font Format table

さて、PostScriptアウトラインが格納されているCFFフォントの本体である。0x00045230番地から0x00EBA647バイト長のテーブルである。
OpenType仕様書にはCFFに関する仕様がほとんどなく、Adobeの技術文書を参照せよということになっている*2

The Compact Font Format SpecificationはCFFにどんな構造でデータが格納されているかを定義してあり、Type 2 Charstring Formatはアウトラインデータの記述方法を定義している。また、Type 2 CharstringはAdobe Type 1 Font Formatの知識を前提としている部分があり、そちらの仕様書も参照した方がよいかもしれない。
まずはThe Compact Font Format Specificationを参照しながら、CFFテーブルを解析していく。

また、日本語では次のサイトも参考になる。

データ型(type)

CFFのデータ型としては次のものがある。

Name Range バイト数 説明
Card8 0-255 1 符号無し整数
Card16 0-65536 2 符号無し整数
OffSize 1-4 1 符号無し整数。オフセットのバイト数を指定する
Offset 可変 1-4 オフセット(サイズはOffSizeで指定されたバイト数)
SID 0-64999 2 文字列ID

SIDは文字列を指定する数値である。

CFFのデータ構造

Entry コメント
Header 先頭固定
Name INDEX Headerの次固定
Top DICT INDEX Name INDEXの次固定
String INDEX Top DICT INDEXの次固定
Global Subr INDEX String INDEXの次固定
Encodings OpenTypeフォントのCFFテーブルとしては存在なし
Charsets
FDSelect CIDフォントのみ
CharStrings INDEX フォント毎
Font DICT INDEX フォント毎、CIDフォントのみ
Private DICT フォント毎
Local Subr INDEX フォント毎、またはCIDフォントのPrivate DICT毎

データ構造としてINDEXとDICTという構造があり、INDEXはリストや配列のようなデータを、DICTはキーとそれに対する値の組を保持する。
Header~Name INDEX~Top DICT INDEX~String INDEX~Global Subr INDEXは必ず存在し、この順番で隙間なく配置されるため、CFFデータの頭から順番に読んでいかないといけない。
他のデータは順番が不定であり、Top DICT INDEX等に含まれるオフセットを使って参照することになる。Encodings, Charsetsは存在しなくてもよい。

Header

最初にHeaderがくる。(0x00045230番地)

Type Name Value Description
Card8 major 0x01 Format major version (starting at 1)
Card8 minor 0x00 Format minor version (starting at 0)
Card8 hdrSize 0x04 ヘッダーのサイズ (バイト)
OffSize offSize 0x03 Absolute offset (0) size

offSizeによって、CFFデータ先頭からのオフセット(“offset (0)”)が3バイトであると指定されている。
CFFの仕様書ではCFFテーブル先頭からのオフセットを“offset (0)”、各データ構造の先頭からのオフセットを“offset (self)”と書いて区別している。HeaderのoffSizeはすべての“offset (0)”のサイズを指定する。

Name INDEX

続いてName INDEXがくる。

INDEX

INDEXは次のような形式である。

Type Name 説明
Card16 count INDEXに含まれるオブジェクトの個数
OffSize offSize オフセット列の各要素のバイト数
Offset offset[count+1] オフセット列
Card8 data[] オブジェクトデータ

INDEXは配列やリストのようなデータを保管する。
countがオブジェクトの個数を表す。オブジェクトが一個もない空のINDEXの場合はcountが0となり、他のフィールドは存在せず、全体として2バイトとなる。
offSizeは続いて指定するオフセット列に含まれる一つ一つのオフセットのサイズ(バイト数)を指定する。
offsetには(データの個数+1)個のオフセットが並ぶ。dataにはオブジェクトが隙間なく並べてあり、オフセットでアクセスする。オフセットはオブジェクトデータ先頭の直前のバイトからのオフセットであり、オブジェクトデータ先頭は1となる。n番目(1 ≦ n ≦ count)のオブジェクトは、オフセット列のn番目とn+1番目のオフセットをつかってアクセスできる。n+1番目のオフセットはn+1番目のオブジェクトの先頭を表すので、n+1番目のオフセットの指す位置の直前までがn番目のオブジェクトである。

これにしたがってName INDEXを解析すると

Type Name Value 備考
Card16 count 0x0001 データ個数1
OffSize offSize 0x01 1バイト
Offset offset[count+1] 0x01 0x16 オフセットが1から22まで
Card8 data[] NotoSansCJKjp-Regular

となる。
Name INDEXはCFFデータに含まれるフォントの名前(PostScript language name)を指定する。CFFデータ自体はフォントを複数含むことができ、フォントの数だけフォント名が含まれる。しかし、OpenTypeのテーブルとしてのCFFデータではName INDEXのデータは必ず一つだけで、つまり一つのフォントしか含むことはできない。ここでは“NotoSansCJKjp-Regular”である。この名前はOpenTypeの'name'テーブルのID 6で指定されるものと一致している必要がある。

Top DICT INDEX

続いて0x04524E番地からTop DICTを入れたINDEXがくる。

Type Name Value 備考
Card16 count 0x0001 データ個数1
OffSize offSize 0x01 1バイト
Offset offset[count+1] 0x01 0x56 オフセットが1から86まで
Card8 data[] (Top DICT Data)

CFFデータに含まれるフォントの数が1つなので、フォントに対応するTop DICTも1つである。

Top DICT Data

読みだされたTop DICTのデータは次のバイナリである。

F8 1B F8 1C 8B 0C 1E F8 1D 01 F8 1E 02 F8 1F 03 
F8 18 04 FB 2A 0C 03 FE 7E FE AC 1D 00 00 0B 70 
1D 00 00 07 10 05 8C 96 1D 00 8D 83 C8 0E 1E 1A 
00 4F 0C 1F 1D 00 00 FF FF 0C 22 1D 00 00 3C B6 
0F 1D 00 00 3C BB 0C 25 1D 00 DC 32 6B 0C 24 1D 
00 00 3E 13 11
DICTデータ

DICTデータはキーと値のペアをバイト長を小さくなるような形で記録したものである。
一つ以上の値が並び、その後にキーがくることで、キーと値のペアを表現する。例えば、次のような感じになる。

[値A] [キーA] [値B1] [値B2] [値B3] [値B4] [キーB]

ここで、0~21の範囲のバイトがキーを示し(12のみ例外で、後続のバイトと合わせて2バイトでキーとなる)、それ以外は値を示す。値の表現は、データのバイト長を削減するために複雑になっており、詳しくは仕様書を参照。これがDICTデータの終端まで続く。

仕様書を読むと分かるが、人が読めるようにはなっていないので、プログラムを書いて何とかする。これを人が読める形にするための簡易的なPythonスクリプトを作成した。出力がキーは数値が< >で囲まれるようにして値と区別している。

このスクリプトで先ほどのDICTを読み出すと次のようになった。キーによって指定される項目について名前や説明を付記した。いくつかの項目はOpenTypeのテーブルと同じ値が格納されている。

Value(s) Key Name Operand 説明
391 392 0 12 30 ROS SID SID number CID用. Registry Ordering Supplement
393 1 Notice SID
394 2 FullName SID
395 3 FamilyName SID
388 4 Weight SID
-150 12 3 UndelinePosition number
-1002 -1048 2928 1808 5 FontBBox array
1 11 9274312 14 XUID array
1.004 12 31 CIDFontVersion number CID用
65535 12 34 CIDCount number CID用
15542 15 charset number charset offset (0)
15547 12 37 FDSelect number CID用. FDSelect offset(0)
14430827 12 36 FDArray number CID用. Font DICT (FD) INDEX offset(0)
15891 17 CharStrings number CharStrings offset (0)

最初のROSは、次に読んでいくString INDEXの文字列を参照することにより、Adobe-Identity-0であるとわかる。これは独自定義のCIDである。

String INDEX

続いて0x452A8番地からString INDEXがくる。フォント名以外の文字列が格納されているらしい。アクセスにはstring identifier (SID)を使う。SIDの一部は事前定義されており、Appendix AによるとSID 390までがstandard stringとして定義済みとなっている。そのため、SID 391がString INDEXの0番目の項目となる。

Type Name Value 備考
Card16 count 0x0018 データ個数24
OffSize offSize 0x02 2バイト

offsetとdataは次のようになる。

offset data
0x0001 Adobe
0x0006 Identity
0x000E Copyright 2014, 2015 Adobe Systems Incorporated (http://www.adobe.com/). Noto is a trademark of Google Inc.
0x0079 Noto Sans CJK JP Regular
0x0091 Noto Sans CJK JP
0x00A1 NotoSansCJKjp-Regular-Alphabetic
0x00C1 NotoSansCJKjp-Regular-AlphabeticDigits
0x00E7 NotoSansCJKjp-Regular-Bopomofo
0x0105 NotoSansCJKjp-Regular-Dingbats
0x0123 NotoSansCJKjp-Regular-DingbatsDigits
0x0147 NotoSansCJKjp-Regular-Generic
0x0164 NotoSansCJKjp-Regular-HDingbats
0x0183 NotoSansCJKjp-Regular-HHangul
0x01A0 NotoSansCJKjp-Regular-HKana
0x01BB NotoSansCJKjp-Regular-HWidth
0x01D7 NotoSansCJKjp-Regular-HWidthCJK
0x01F6 NotoSansCJKjp-Regular-HWidthDigits
0x0218 NotoSansCJKjp-Regular-Hangul
0x0234 NotoSansCJKjp-Regular-Ideographs
0x0254 NotoSansCJKjp-Regular-Kana
0x026E NotoSansCJKjp-Regular-Proportional
0x0290 NotoSansCJKjp-Regular-ProportionalCJK
0x02B5 NotoSansCJKjp-Regular-ProportionalDigits
0x02DD NotoSansCJKjp-Regular-VKana
0x02F8

Global Subr INDEX

次は0x455D4番地からGlobal Subr INDEXがくる。これは、文字のアウトラインの定義から呼び出すことのできるサブルーチンの集合で、globalとつくように、CFFデータに含まれるどのフォントからでも呼び出すことができる。サブルーチンが一つも存在しない場合は空のINDEXとなる。

最初を見ていくと、

Type Name Value 備考
Card16 count 0x06 4D データ個数1613
OffSize offSize 0x02 2バイト

となり、個数も多いので必要になったら参照することにする。

Encodings

Top DICTでencodingが存在する場合はオフセットが指定されるが、これはCIDフォントなのでencodingはない。また、OpenType/CFFでも省略される。

Charsets

グリフはSID (name-keyed)またはCID (CID-keyed)によって同定される。一方で、GID (glyph index)とSID/CIDは一致しているとは限らず*3マッピングを定義する必要がある。これを指定するのがcharsetである。GID 0は必ず“.notdef”なので、GID 0のグリフの指定は必須でなく、この場合charsetの配列のインデックスはGID 1から始まる。

Top DICTで指定されたようにオフセットが15542である。つまり0x00045230+15542=0x48EE6番地から始まる。
02…と始まっているのでこれはFormat 2のcharsetであることがわかり、次のように定義されている。

charset format 2
Type Name Description
Card8 format 2
struct Range2[] Range2 array
Range2 format
Type Name Description
SID first 範囲の最初のグリフ
Card16 nLeft 最初のグリフを除いた、範囲に含まれるグリフの個数

format 0はSIDがランダムな場合、format 1はそこそこまとまっている場合、format 2は東アジアのCIDフォントのようなlarge well-ordered charsetの場合に向くらしい。format 1と2はnLeftのバイト数が1か2かだけが異なる。

読み出して行くと、

first nLeft
0x0001 0xFFFD

これだけ。(GID 0の.notdef以外の)65534個、つまりすべてのグリフについてCIDはGIDに一致するということらしい。

CharStrings INDEX

Top DICTより、0x00045230+15891=0x49043番地から始まる。実際の文字のアウトラインの定義がType 2 Charstringの形式で記録されている。

Type Name Value 備考
Card16 count 0xFFFF データ個数65535
OffSize offSize 0x02 3バイト

Charstringのデータは仕様書をみるとGIDでアクセスするらしい。
必要なのはGID 0x5D9 (ね), 0x5BF (こ)であるので、これらを読みだす。

0x49043+3+0x5D9*3=0x4A1D1番地からの数値を読み出すと0x0167C6, 0x016885となっている。対応するデータを0x49043+3+0x10000*3+0x0167C6-1=0x8F80B~0x8F8CA番地の間で読みだすと

85 D2 F7 22 CC F7 90 DE 71 D1 12 F7 A1 D3 52 CE 
52 D2 F5 D0 F7 93 D7 13 D9 80 F9 15 CC 15 53 5E 
A1 BE B3 B8 A8 BE BA B8 7E 74 B6 1F 4A 75 64 62 
46 1B 13 D3 80 FB A3 F8 D4 15 3A 0A 13 E3 80 80 
89 67 88 5D 1E 58 82 51 85 6B 89 73 8A 78 8A 75 
8C 93 39 18 13 E5 80 31 0A 13 D9 80 24 0A B1 8C 
B7 8E B8 1E E8 E2 F7 14 EA E7 1B DD C8 3F FB 10 
59 89 5C 85 61 1F 9E 60 5B 96 56 1B 26 44 51 3C 
27 DB 62 EA EF C6 BB DF AC 1F A8 73 A9 6E A8 6A 
B5 CC 18 69 AF 67 AC 63 A7 08 94 BB 8F C2 C9 1A 
F7 38 44 F7 0C FB 1A 1E 13 D5 80 FB 00 FB 0F 33 
40 37 1F 8C 9C 8D 9B 8C 9B 08 13 D3 80 B8 0A

である。

0x49043+3+0x5BF*3=0x4A183番地からの数値を読み出すと0x015A39, 0x015A81となっている。対応するデータを
0x49043+3+0x10000*3+0x015A39-1=0x8EA7E~0x8EAC6番地の間で読みだすと

77 DD F8 B6 DB 01 F7 44 DD 03 F7 81 F8 FF 15 84 
DA DF 87 EE 1B E6 F7 01 92 90 CE 1F DD 07 84 44 
26 5C 0A 62 FB CA 15 82 62 CB 0A 49 F7 5D F7 21 
F7 12 9A 9E D2 1E 8A E1 05 74 60 0A BF D5 B0 93 
AF 97 B3 1F 39 93 05 0E

となっている。
これらはType 2 Charstringであり、その仕様書にあたる必要がある。後で読んでいく。

FDSelect

CIDフォントでは、描画に必要なPrivate DICTを指定するFont DICTの配列FDArrayが存在し、どのFont DICTを用いるかはFDSelectによって指定されている。
0x00045230+15547=0x48EEB番地からFDSelectが始まる。FDSelectの構造はcharsetと似ている。ここで、03から始まっているのでFormat 3だとわかる。

FDSelect Format 3
Type Name Value 説明
Card8 format 0x03 format 3
Card16 nRanges 0x0071 範囲の個数: 113個
struct Range3[nRanges] (略) Range3 array
Card16 sentinel 0xFFFF Sentinel GID

Range3は次のように定義されている。

Range3 Format
Type Name 説明
Card16 first 範囲の最初のグリフのインデックス
Card8 fd 範囲に含まれる全てのグリフに対応するFD index

あるGIDについて最初から順番にみていって、初めて求めるGIDより大きなfirstをもつRange3があったら、その直前のRange3構造体のもつfdが求めるFD indexとなる。
欲しいのはGID 0x5D9 (ね), 0x5BF (こ)に対応するFD indexであるから、0x48F90番地からの

first fd
0x05AD 0x0E
0x0603 0x03

から、GID 0x5D9 (ね), 0x5BF (こ)のFD indexはともに0x0E (14)であるとわかる。

FDArray

0x00045230+14430827=0xE0849B番地にFDArrayがある。これはFont DICTの入ったINDEXである。
ここから、FD indexが0x0E (14)のものを取り出してみる。

Type Name Value 備考
Card16 count 0x0013 データ個数19
OffSize offSize 0x01 1バイト

(0オリジンで)14, 15番目のoffsetをみると0x9B, 0xA6となっているので、その間(0xE084B2+0x9B-1=0xE0854C~0xE08557)を読むと、

F8 2E 0C 26 A1 1D 00 EB 78 6C 12

となっており、これを先ほどのDICTを読み出すpythonスクリプトで読みだすと

Value(s) Key Name Operand 説明
410 12 38 FontName SID NotoSansCJKjp-Regular-Kana
22 15431788 18 Private number number Private DICT size, offset (0)

となった。Appendix AによりSID 390までがstandard stringとして定義済みのようなので、SID 391がString INDEXの0番目の項目となる。FontNameのSID 410は、ここではString INDEXの410-391=19番目(最初の項目を0番目として数える)の項目であり、これはNotoSansCJKjp-Regular-Kanaである。仮名なので、正しそう。

そしてPrivate DICTのサイズと(CFFテーブル最初からの)オフセットが記載されている。
0x00045230+15431788=0xEFCA9C番地から22バイトであるとわかる。

FB 8E 8B 1D 00 00 05 46 8B 06 D3 0A D5 0B 8C 0C 
11 FA 7C 14 A1 13

同様にPythonスクリプトでDICTを解析すると、

Value(s) Key Name Operand 説明
-250 0 1350 0 6 BlueValues delta
72 10 StdHW number
74 11 StdVW number
1 12 17 LanguageGroup number
1000 20 defaultWidthX number
22 19 Subrs number Offset (self) to local subrs

defaultWidthXは重要で、この値と同じグリフ幅を持つ場合はcharstringでは省略できるらしい。

また、ローカルサブルーチンを格納したINDEXへのオフセットが記録されている。これはPrivate DICTの先頭からのオフセットである。

Local Subrs INDEX

0xEFCA9C+22=0xEFCAB2番地から。charstringあるいはサブルーチンから呼び出されるローカルサブルーチンが記録されている。先頭のみ解析しておく。

Type Name Value 備考
Card16 count 0x00B4 データ個数180
OffSize offSize 0x02 2バイト

あとは必要になったら読みだすこととする。

Charstringの解読

Charstringの数値はCFFのDICTとよく似ていて、人が読むには向かないので、Type 2 Charstringの仕様書に従って、一旦逆アセンブルすることにする。
解析するための簡易的なPythonスクリプトを作成した。次である。

「ね」

前に読みだした「ね」のcharstringを解析すると、次のようになっている。

-6 71 142 65 252 83 -26 70 hstemhm 269 72 -57 67 -57 71 106 69 255 76 
hintmask 0xD980 641 65 rmoveto -56 -45 22 51 40 45 29 51 47 45 -13 -23 43 hvcurveto 
-65 -22 -39 -41 -69 hhcurveto hintmask 0xD380 -271 576 rmoveto -81 callsubr 
hintmask 0xE380 -11 -2 -36 -3 -46 vhcurveto -51 -9 -58 -6 -32 -2 -24 -1 -19 -1 -22 
1 8 -82 rcurveline hintmask 0xE580 -90 callsubr hintmask 0xD980 -103 callsubr 38 1 
44 3 45 vhcurveto 93 87 128 95 92 hhcurveto 82 61 -76 -124 -50 -2 -47 -6 -42 
hvcurveto 19 -43 -48 11 -53 hhcurveto -101 -71 -58 -79 -100 80 -41 95 100 59 48 
84 33 hvcurveto 29 -24 30 -29 29 -33 42 65 rcurveline -34 36 -36 33 -40 28 
rrcurveto 9 48 4 55 62 vvcurveto 164 -71 120 -134 vhcurveto hintmask 0xD580 
-108 -123 -88 -75 -84 hvcurveto 1 17 2 16 1 16 rrcurveto hintmask 0xD380 45 
callsubr

少しずつ読んでいく。今回、ヒントは無視する。


グリフ幅はdefaultWidthX (1000)と同じであるので、最初にWidthが来ず、ヒントステムの指定がくる。

-6 71 142 65 252 83 -26 70 hstemhm 
269 72 -57 67 -57 71 106 69 255 76 
hintmask 0xD980

hstemhmはヒントの水平ステムを指定する。hstemhmに先行する数字列が指定された水平ステムである。この命令の後、hintmask命令がくるまでの値の列は垂直ステムvstemの指定であるらしい。
一つのステムは2つの値のペアで指定するため、水平ステム4個、垂直ステム5個である。hintmask命令は有効にするヒントステムを指定する命令で、マスクが後続し、ステムの数×1ビットのマスク用のフラグがつくため、ステムの数によって命令長が変わる*4。このグリフはステム9個なのでマスクが2バイトである。
以下、#以降はコメントとする。

641 65 rmoveto #(641, 65)

rmovetoによって現在位置を(0, 0)から(641, 65)に移動し、パスの描画を始める。このように、(hintmask, cntrmask命令を除いて)逆ポーランド記法的に引数が前に来るようになっている。数値がくると引数スタックに積まれて行く。命令によっては(特にパス定義命令は)引数が可変であり、引数の個数によって動作が変わることがある。
特にパス定義の命令などは実行されると引数スタックを空にする。スタックを空にする命令は仕様書の定義において

|- dx1 {dya dxb}* hlineto (6) |-

のように“|-”(スタックの底を表す記号)で終わっており、命令が引数スタックを空にすることを示す。また、先頭の“|-”はスタックの底から順番に引数にとっていくことを示している。

CFFフォントにおいては、アウトラインを直線と三次ベジエ曲線によって定義する。
一つの(三次)ベジエ曲線は4つの制御点で定義されるが、charstringにおいては基本的には座標の差分を表す6つの数値dxa, dya, dxb, dyb, dxc, dycで表現される。Type 2 Charstringには特別な条件でバイト数を節約できるようにいくつものオペレータが用意されている。
各オペレータがどのようにパスを定義するかは次掲のページの最後に図次されているものがわかりやすい。

これに従ってパスの定義をみていく。

-56 -45 22 51 40 45 29 51 47 45 -13 -23 43 hvcurveto 

hvcurveto命令は、最初の端点の接ベクトルが水平で最後の端点の接ベクトルが垂直であるベジエ曲線(と、最初の端点の接ベクトルが垂直で最後の端点の接ベクトルが水平であるベジエ曲線とが交互に並ぶ場合)を定義する。次のパスが得られる。

  • 曲線: (641, 65)-(585, 65)-(540, 87)-(540, 138)
  • 曲線: (540, 138)-(540, 178)-(585, 207)-(636, 207)
  • 曲線: (636, 207)-(683, 207)-(728, 194)-(771, 171)
-65 -22 -39 -41 -69 hhcurveto 

hhcurveto命令は端点の接ベクトルが水平のベジエ曲線(の並び)を定義する。次のパスを得る。

  • 曲線: (771, 171)-(749, 106)-(710, 65)-(641, 65)

最初の地点に戻ってきた。

hintmask 0xD380 #無視
-271 576 rmoveto #(370, 641)
-81 callsubr 

rmovetoで次のパスへ移動。続いてcallsubrでローカルサブルーチンの呼び出しを行っている。

callsubrの引数である-81はbiased indexであり、CFFの仕様書にバイアスの詳細があり、Local Subr INDEXに含まれるサブルーチンの個数によって場合分けが行われる。ここではLocal Subr INDEXの個数(count)が180 (<1240)なのでバイアス107を加える。よって、-81は26番目のローカルサブルーチンを表す。
26番目のローカルサブルーチンは、0xEFCAB5+26*2=0xEFCAE9番地から0x0513, 0x0526と書かれていることから、0xEFCAB5+(180+1)*2+0x0513-1=0xEFD131番地から0xEFCAB5+(180+1)*2+0x0526-1=0xEFD144番地までが26番目のサブルーチンである。次に挙げるが、charstringと同じ形式である。

86 89 92 D0 93 C3 90 A3 19 2C 8E 05 90 72 8A 71 74 1A 0B

先掲のPythonスクリプトを使ってType 2 Charstringを訳すと、

-5 -2 7 69 8 56 5 24 rlinecurve -95 3 rlineto 5 -25 -1 -26 -23 vvcurveto return

となる。これを読んでいく。

-5 -2 7 69 8 56 5 24 rlinecurve 

rlinecurveは一つ以上の直線のあとに曲線がくる場合に使われる。

  • 直線: (370, 641)-(365, 639)
  • 曲線: (365, 639)-(372, 708)-(380, 764)-(385, 788)
-95 3 rlineto 

rlinetoは直線を表現する。

  • 直線: (385, 788)-(290, 791)
5 -25 -1 -26 -23 vvcurveto return

vvcurvetoは、端点の接ベクトルが垂直なベジエ曲線(が並ぶ)場合を表現するのに適している。

  • 曲線: (290, 791)-(295, 766)-(294, 740)-(294, 717)

return命令によってサブルーチンを終了している。


charstringに戻って、

hintmask 0xE380 #無視
-11 -2 -36 -3 -46 vhcurveto 
  • 曲線: (294, 717)-(294, 706)-(292, 670)-(289, 624)
-51 -9 -58 -6 -32 -2 -24 -1 -19 -1 -22 1 8 -82 rcurveline 

rcurvelineは一つ以上の曲線の後に直線が続く場合を表現する。

  • 曲線: (289, 624)-(238, 615)-(180, 609)-(148, 607)
  • 曲線: (148, 607)-(124, 606)-(105, 605)-(83, 606)
  • 直線: (83, 606)-(91, 524)
hintmask 0xE580 #無視
-90 callsubr 

callsubrで、-90+107=17番目のサブルーチンを呼び出す。
0xEFCAB5+17*2=0xEFCAD7番地から0x040B, 0x0423と書かれているので、0xEFCAB5+(180+1)*2+0x040B-1=0xEFD029番地から0xEFCAB5+(180+1)*2+0x0423-1=0xEFD041までを読み出すと

C9 93 E1 97 B8 90 89 6C 89 6B 89 6B 59 3D FB 06 FB 2E 54 46 BD 46 18 0B

となっている。これを解読すると

62 8 86 12 45 5 -2 -31 -2 -32 -2 -32 -50 -78 -114 -154 -55 -69 50 -69 rcurveline 
return

となり、次のパスが得られる

  • 曲線: (91, 524)-(153, 532)-(239, 544)-(284, 549)
  • 曲線: (284, 549)-(282, 518)-(280, 486)-(278, 454)
  • 曲線: (278, 454)-(228, 376)-(114, 222)-(59, 153)
  • 直線: (59, 153)-(109, 84)


続いてcharstringに戻り、

hintmask 0xD980 #無視
-103 callsubr 

再度callsubrによるサブルーチン呼び出し。-103+107=4番目のサブルーチンを呼び出す。
0xEFCAB5+4*2=0xEFCABD番地から0x021D, 0x0247と書かれているので、0xEFCAB5+(180+1)*2+0x021D-1=0xEFCE3B番地から0xEFCAB5+(180+1)*2+0x0247-1=0xEFCE65番地までを読み出すと、

BB CD CC EA BB D4 08 7C 8A 7E 81 1A 89 FB 02 8B 
5A 8A 2C 08 7B 89 6E 8A 7D 1E E2 06 89 9D 89 A4 
8A 9C 08 87 E4 8B C7 E5 1A 0B

となっている。これを解読すると、

48 66 65 95 48 73 rrcurveto -15 -1 -13 -10 vvcurveto -2 -110 0 -49 -1 -95 rrcurveto 
-16 -2 -29 -1 -14 vhcurveto 87 hlineto -2 18 -2 25 -1 17 rrcurveto -4 89 0 60 90 
vvcurveto return

である。

順に読んでいくと、

48 66 65 95 48 73 rrcurveto 
  • 曲線: (109, 84)-(157, 150)-(222, 245)-(270, 318)
-15 -1 -13 -10 vvcurveto 
  • 曲線: (270, 318)-(270, 303)-(269, 290)-(269, 280)
-2 -110 0 -49 -1 -95 rrcurveto 
  • 曲線: (269, 280)-(267, 170)-(267, 121)-(266, 26)
-16 -2 -29 -1 -14 vhcurveto 
  • 曲線: (266, 26)-(266, 10)-(264, -19)-(263, -33)
87 hlineto 
  • 直線: (263, -33)-(350, -33)
-2 18 -2 25 -1 17 rrcurveto 
  • 曲線: (350, -33)-(348, -15)-(346, 10)-(345, 27)
-4 89 0 60 90 vvcurveto return
  • 曲線: (345, 27)-(341, 116)-(341, 176)-(341, 266)

サブルーチンは終了。


charstringに戻って、

38 1 44 3 45 vhcurveto 
  • 曲線: (341, 266)-(341, 304)-(342, 348)-(345, 393)
93 87 128 95 92 hhcurveto 
  • 曲線: (345, 393)-(432, 486)-(560, 581)-(652, 581)
82 61 -76 -124 -50 -2 -47 -6 -42 hvcurveto 
  • 曲線: (652, 581)-(734, 581)-(795, 505)-(795, 381)
  • 曲線: (795, 381)-(795, 331)-(793, 284)-(787, 242)
19 -43 -48 11 -53 hhcurveto 
  • 曲線: (787, 242)-(744, 261)-(696, 272)-(643, 272)
-101 -71 -58 -79 -100 80 -41 95 100 59 48 84 33 hvcurveto 
  • 曲線: (643, 272)-(542, 272)-(471, 214)-(471, 135)
  • 曲線: (471, 135)-(471, 35)-(551, -6)-(646, -6)
  • 曲線: (646, -6)-(746, -6)-(805, 42)-(838, 126)
29 -24 30 -29 29 -33 42 65 rcurveline 
  • 曲線: (838, 126)-(867, 102)-(897, 73)-(926, 40)
  • 直線: (926, 40)-(968, 105)
-34 36 -36 33 -40 28 rrcurveto 
  • 曲線: (968, 105)-(934, 141)-(898, 174)-(858, 202)
9 48 4 55 62 vvcurveto 
  • 曲線: (858, 202)-(867, 250)-(871, 305)-(871, 367)
164 -71 120 -134 vhcurveto 
  • 曲線: (871, 367)-(871, 531)-(800, 651)-(666, 651)
hintmask 0xD580 #無視
-108 -123 -88 -75 -84 hvcurveto 
  • 曲線: (666, 651)-(558, 651)-(435, 563)-(351, 488)
1 17 2 16 1 16 rrcurveto 
  • 曲線: (351, 488)-(352, 505)-(354, 521)-(355, 537)
hintmask 0xD380 #無視
45 callsubr

45+107=152番目のサブルーチンを呼び出す。
0xEFCAB5+152*2=0xEFCBE5番地から0x0B6E, 0x0B76と書かれているので、0xEFCAB5+(180+1)*2+0x0B6E-1=0xEFD78C番地から0xEFCAB5+(180+1)*2+0x0B76-1=0xEFD794番地までを読み出すと、

9A A3 9C A6 97 9D 08 0E

となっている。解読すると、

15 24 17 27 12 18 rrcurveto endchar
  • 曲線: (355, 537)-(370, 561)-(387, 588)-(399, 606)

となり、endcharで一つの文字のパスの定義が終了する。ここで、このパスの輪郭が(370, 641)から始まったことから、パスが閉じられていない。パスを閉じるためには次の直線が必要である。

  • 直線: (399, 606)-(370, 641)

以上で「ね」のアウトラインが得られた。

「こ」

続いて「こ」のcharstringは先掲のPythonスクリプトで解読すると次になる。

-20 82 546 80 hstem 176 82 vstem 237 619 rmoveto -7 79 84 -4 99 hhcurveto 91 109 
7 5 67 hvcurveto 82 vlineto -7 -71 -101 -47 callsubr -41 -310 rmoveto -9 -41 
64 callsubr -66 201 141 126 15 19 71 vhcurveto -1 86 rlineto -23 -43 callsubr 52 
74 37 8 36 12 40 hvcurveto -82 8 rlineto endchar

順に読んでいく。

-20 82 546 80 hstem #横stem
176 82 vstem #縦stem定義。stemは合計6個
237 619 rmoveto #(237, 619)

ヒントステムの合計は6個なので、hintmask, cntrmaskのマスクのバイト数は1バイトである。
rmovetoで(0, 0)から(237, 619)に移動、パスの定義をスタートさせる。

-7 79 84 -4 99 hhcurveto 
  • 曲線: (237, 619)-(316, 612)-(400, 608)-(499, 608)
91 109 7 5 67 hvcurveto 
  • 曲線: (499, 608)-(590, 608)-(699, 615)-(766, 620)
82 vlineto 

vlinetoは垂直な線を示す。

  • 直線: (766, 620)-(766, 702)
-7 -71 -101 -47 callsubr 

callsubrで-47+107=60番目のサブルーチンの呼び出し。
0xEFCAB5+60*2=0xEFCB2D番地から0x0776, 0x0782と書かれているので、0xEFCAB5+(180+1)*2+0x0776-1=0xEFD394番地から0xEFCAB5+(180+1)*2+0x0782-1=0xEFD3A0番地までを読み出すと、

84 2C 1B 28 30 8F 94 43 1F 39 07 0B

となっている。解読すると、

-7 -95 hhcurveto -99 -91 4 9 -72 hvcurveto -82 vlineto return

となる。
これを-7 -71 -101の引数をつけて呼び出すので、最初の命令は-7 -71 -101 -7 -95 hhcurveto ...となる。ここでは引数を[~]で囲んで表現しておく。

[-7 -71 -101] -7 -95 hhcurveto
  • 曲線: (766, 702)-(695, 695)-(594, 688)-(499, 688)
-99 -91 4 9 -72 hvcurveto
  • 曲線: (499, 688)-(400, 688)-(309, 692)-(237, 701)
-82 vlineto return
  • 直線: (237, 701)-(237, 619)

サブルーチンは終了。パスが始まった点に戻ってきた。


charstringに戻る。

-41 -310 rmoveto #次のパスへ (196, 309)
-9 -41 64 callsubr 

callsubrで64+107=171番目のサブルーチンの呼び出し。
0xEFCAB5+171*2=0xEFCC0B番地から0x0C12, 0x0C1Bと書かれているので、0xEFCAB5+(180+1)*2+0x0C12-1=0xEFD830番地から0xEFCAB5+(180+1)*2+0x0C1B-1=0xEFD839番地までを読み出すと、

80 5D 57 1A FB 10 F7 09 0B

となっている。解読すると、

-11 -46 -52 vvcurveto -124 117 return

である。

引数を合わせて読んでいくと、

[-9 -41] -11 -46 -52 vvcurveto
  • 曲線: (196, 309)-(187, 268)-(176, 222)-(176, 170)
-124 117 return

最後に数値が残った状態でサブルーチンがreturnされるので、戻った先でその値が使われることになる。


charstringに戻って、返された-124 117も併せて続きを読んでいく。

[-124 117] -66 201 141 126 15 19 71 vhcurveto 
  • 曲線: (176, 170)-(176, 46)-(293, -20)-(494, -20)
  • 曲線: (494, -20)-(635, -20)-(761, -5)-(832, 14)
-1 86 rlineto 
  • 直線: (832, 14)-(831, 100)
-23 -43 callsubr

callsubrで、-43+107=64番目のサブルーチンの呼び出し。
0xEFCAB5+64*2=0xEFCB35番地から0x0797, 0x07A2と書かれているので、0xEFCAB5+(180+1)*2+0x0797-1=0xEFD3B5番地から0xEFCAB5+(180+1)*2+0x07A2-1=0xEFD3C0番地までを読み出すと、

40 FB 13 7C FB 1D 1B FB 32 3F 0B

となっている。解読すると、

-75 -127 -15 -137 hhcurveto -158 -76 return

である。引数-23を合わせて読んでいくと、

[-23] -75 -127 -15 -137 hhcurveto
  • 曲線: (831, 100)-(756, 77)-(629, 62)-(492, 62)
-158 -76 return

サブルーチンは終了。使われなかった-158 -76が返される。


charstringに戻って、返された-158 -76も併せて読んでいく。

[-158 -76] 52 74 37 8 36 12 40 hvcurveto 
  • 曲線: (492, 62)-(334, 62)-(258, 114)-(258, 188)
  • 曲線: (258, 188)-(258, 225)-(266, 261)-(278, 301)
-82 8 rlineto endchar
  • 直線: (278, 301)-(196, 309)

endcharが来たので終了。パスの開始点へともどってきている。

プロット

直線と曲線の制御点の座標が求められたので、プロットしていく。「こ」は「ね」の幅1000 unitだけずらす。
f:id:nixeneko:20180610211334p:plain

Photoshopで実際のフォントのアウトラインを重ねてみると次のようになる。
f:id:nixeneko:20180610212135p:plain
曲線については、2つ飛ばしに制御点がアウトラインに乗っているように見え、良さそう。

適当に実線でアウトラインを描いていくと次のようになった。
f:id:nixeneko:20180610213110p:plain
実際のフォントのレンダリング結果を重ねてみると、おおよそ上手くいっているように見える。ずれは線の描画が雑なせいだと思う。
f:id:nixeneko:20180610214356p:plain

感想

  • OpenType/CFFは、TrueTypeと同様のテーブル構造(sfnt構造)にPostScriptフォントの一種であるCFFフォントを丸ごと突っ込んだ形をしている。フォントの中にフォントを突っ込むという気持ち悪い構造である。
  • CFFでアウトラインの定義に使われているType 2 Charstringはある種のプログラミング言語(PostScriptの仲間)である。なので、アウトラインを取得するためにはインタプリタを実装する必要がある。大変。
  • 仕様書によると、Type 2 Charstringにおいてはサブルーチン呼び出しを最大10回までネストすることができる。そのため、サブセットフォントをまともにつくろうとすると、サブルーチンがどこから呼び出されるかを全部辿って行かないといけないので、難しそう。
  • 歴史的な経緯でこうなったということは分かるのだが、現在新しくフォント形式を設計するとしたらこうはならないだろうな…という感じがする。

*1:実はplatformID=0x0000, encodingID=0x0003とplatformID=0x0003, encodingID=0x0001は同一のオフセットをもち、同じサブテーブルを指している。

*2:一方、OpenType 1.8でvariable font等に対応した際に追加された'CFF2'テーブルはOpenType仕様書にしっかり記載されている。基本は一緒のようなので、そちらを参照した方がアイデアはつかみやすいかもしれない。

*3:GIDは必ず隙間なく連続している必要があるが、CIDは空きがあって連続していなくてもよいので、CIDフォントにおいてはCID→GIDマッピングを定義しなければならない。

*4:なお、サブルーチンにおけるhintmask命令に後続するマスクの長さも、呼び出している側のcharstring先頭から解析していかないとわからない。