にせねこメモ

はてなダイアリーが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

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

前に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先頭から解析していかないとわからない。

耳を塞がないイヤホンambieを一ヶ月でぶっ壊してしまった話

はじめに

前にHoloLensを体験したとき、着用している人には音声が聞こえるが、それ以外の周りの人にはその音が聞こえないという状態を体験した。
このような、自分にだけ音が聞こえて、かつ周りの音を聞くことを阻害しないイヤホンあるいはヘッドフォンのようなものがあればなあと思い、探していると、Ambie sound earcuffという耳を塞がないイヤホンが去年から発売されていたことを知った。
今年の3月、ヨドバシカメラの店舗で視聴してみて、いい感じだったのですぐ買ってしまった*1

感想

一ヶ月ほど使ってみた上での感想は、日常生活に自分だけのBGMを重畳する、という感覚がすごくよかった。

メリットとデメリットを挙げる。

利点

  • 耳を塞がないので周りの音が聞こえ、これが自分だけに聞こえるBGMといった感じでとてもよい。散歩するにも周りの音が聞こえるので気楽。
  • 耳を塞がない構造から、しばらく聞いていてもあまり疲れない。ただし耳のそばで鳴っているわけなので、全く疲れないわけではない。
  • イヤホンでありがちなコードが擦れる音がしない。

欠点

  • 周りの音が聞こえるといっても耳元で音を鳴らしている分、小さな物音とかは聞こえなくなる。あと周りの音がどこからするのかといった周囲の音の定位が曖昧になる気がする。
  • 大きな音を出すと音漏れはする。なので、静かな図書館とかで使うには向かない。
  • BGM感覚で聞いているとつけていることを忘れるので、机に置いたスマホに接続して聞いてた時に立ち上がったり後ろ向いたりしてスマホを落っことすことが何回もあった。散歩とかしてる場合は問題ないのだけれど。
  • イヤーピースが簡単に外れるようになっていて、油断するとなくす。鞄とかに雑に突っ込んでおくと次に取り出した時にはなくなっていたりする。

f:id:nixeneko:20180506110943p:plain

結局最後は足でコードを踏んづけたまま引っ張ってしまったために、イヤホンのプラグが破損してしまった。
f:id:nixeneko:20180506110612p:plain
まだ一ヶ月しか使ってないのに…。有線は散歩にはいいが作業用には危ないように感じた。

自分で修理

さすがに一ヶ月しか使用してないのにこれは参ってしまった。引っ張って壊れたため保証による無料修理も効かないだろうと判断して、プラグ部分を入れ替えて何とか自分で修理しようとした。

f:id:nixeneko:20180506111142p:plain
プラグの辺りで切断し、被覆を剥くと銅色のものが2本あり、片方は白で被覆されたマイク線が中心に入っている。銅色はGND、緑と赤がLとRの信号線である。緑がL、赤がRだったと思う。
白色の線を取り出し、銅色のものはまとめてしまってGNDとする。イヤホンの銅線はリッツ線という名前らしく、絶縁の為に塗料がついているので、ライターで炙って先端を溶かし、紙やすりで磨いたりした。
f:id:nixeneko:20180506112436p:plain
初めはこれを汎用的なプラグにはんだ付けすればいいと思ったのだが、はんだ付けが細かく、かつ絶縁が難しそうだったので一度あきらめた。


その後、別の(生きている)プラグ付きのケーブルがあったので、切断して導線の被覆を剥き、イヤホンの導線とはんだ付けして繋げることでイヤホンとしての機能を復活させた。この時、接続の前に絶縁のため熱収縮チューブを通しておき、テスターでどの線がプラグのどの位置に来るかを調べ(プラグの先端からL, R, GND, Micを接続する)、それに従って線をよじってつなげ、ちゃんと音が出るかをはんだ付け前に確認している。
f:id:nixeneko:20180506112755p:plain
はんだ付けが上手くいかずてこずった。一応繋がってはいるので良しとしたが、素人仕事なので問題があるかもしれない。(繋がった導線は余分な部分を適当な長さで切っといた方が良かった気がする)。

あとは熱収縮チューブを2重にして、内側に一本ずつチューブで挟むことで絶縁をした。最良ではなさそうな気もするが一先ず動いてるのでよしとした。
f:id:nixeneko:20180506114700p:plain
f:id:nixeneko:20180506114910p:plain
熱収縮チューブはライターで炙って縮ませた。外側の導線がはみ出てるやんけ……別の信号の導線同士でショートしてはないようなのでまあいいかという感じ。音もちゃんと左右分離して出るし、リモコン操作も動く。

おわりに

ところが今年2018年4月にBluetooth無線版の物が発売されていたらしい。注文してしまって届いた。
f:id:nixeneko:20180512212846p:plain
充電が必要なのは面倒ではあるのだが、2時間半ほど充電して6時間ほど持つらしい。作業中にコード引っ掛けることもなさそうだし、よい。あと、イヤーピースが外れづらい構造になったのもよい。

*1:2018年5月現在では無線版も発売されているが、当時は有線版しかなかった。

粉末状完全食・日米対決: COMP vs. Soylent飲み比べ

f:id:nixeneko:20180407222131p:plain

完全食について

完全食とは、健康を維持するために必要な栄養をすべて含んだ食品のこと。ここでは、SoylentとCOMPをとりあげる。


Soylentという、食事を代替することを目的とした粉末状の食料がある。粉末のプロテインのように水などに溶かして飲む。2013年にアメリカでクラウドファンディングが実施され、2014年から最初の商品であるSoylent v1.0が出荷された。

公式の通販サイトでは日本への発送はしていないのだが、このたび知人がアメリカに行くとのことだったので、お土産にSoylentをリクエストしてみた。


COMPはSoylentの日本版とでもいうべきものであり、Soylentが日本への配送を行っていないことが開発の一つのきっかけであったらしい*1
成分についても、日本の厚生労働省が出している必要な栄養素を元にしていて、材料も日本で手に入るものに変えているとのことである。

実食

実際に飲み比べたのは

  • COMP v.4
  • Soylentのformlula v.1.8

である。食品なのにバージョン表記がなされているのが工学っぽい。

見た目比較

f:id:nixeneko:20180407224215p:plain
左がCOMP、右がSoylentである。

  • 粉の色: Soylentの方が黄色っぽく、COMPの方は白っぽい
  • 体積ベースで、粉:水=1:1になるようにシェーカーに投入し、振り混ぜた。
  • 液体の色: 混ぜた後の液体の色としてはほとんど変わらないが、Soylentの方がやや白っぽい

飲み比べ

f:id:nixeneko:20180407222802p:plain

COMP V.4
  • きな粉っぽさが強い
    • 全体として粉っぽさがあり、舌触りがざらざらしている
  • シナモンの香りがする
  • 甘く、飲みやすい
    • なんとなく、飲む八つ橋といった感じもある

COMPはv.3時代に購入して飲んでたのだが、v.3は豆乳臭さが残っていた。それに比べて、v.4は初めて飲んだのだが、凄く飲みやすく、おいしくなっていて驚いた。

Soylent v1.8
  • 塩味が強めに感じられる
  • 甘味もあるが、薄めな気がする
    • 全体として、「甘い」というより「しょっぱい」という印象
  • 液はなめらかであり、粉っぽさはないが、噛むとじゃりじゃりした感覚が少しある。

COMPと比較するとしょっぱさが際立つという感想だったが、Soylent単体で飲んでみたところ、そこまでしょっぱさが気になる訳でもなかった。味の雰囲気を牛乳に近づけているような気がする。
(2018-04-16追記)濃いめだと「しょっぱい」という印象だったが、もう少し水で薄めると塩味も気にならなくなり、さらに薄めると豆乳っぽさが感じられた。(追記終)


全体として、以前自分がCOMP v.3を飲んでたための慣れもあるのだろうけれど、COMP v.4の方がSoylentより飲みやすく感じた。

成分量比較

甘味や塩味について、400kcalあたりの分量で比較してみると、

ナトリウム 炭水化物 食物繊維 糖質
Soylent 320mg 39g 5g 34g
COMP 264mg 59.3g 4.3g 55g

となっている。

ナトリウムについてはCOMPは少なめになっている。これはナトリウムの基準量は上限であり、他の食事で多めに摂取されることが予想されるからだろう。逆に糖質についてはCOMPの方が多い*2。これらは実食したときの味の感覚と合っているように思う。

*1:食に関心のないミレニアルが「完全食」で半年生活してみた | BUSINESS INSIDER JAPAN

*2:とはいえ、甘さに寄与しない糖質もあるし、糖質以外でも甘さをもつものもあり(人工甘味料など)、甘味への寄与は何ともわからない。

Ubuntu 16.04でgooglei18n/fontviewをビルド

OpenTypeのvariable fontが発表され、デモフォントとvariable font対応のフォントビューアが公開された。そのフォントビューアがgoogle18n/fontviewである。

たぶん最初の対応ビューアなので、variable font開発でも確認に使われているだろうと思われる。

これは、主にMac向けらしい(Mac向けバイナリも配布されている)が、Linuxでもビルドできるようなので、メモしておく。なお、一筋縄ではいかない模様。

環境

依存するライブラリ等のインストール

さて、README.mdを読んでいくと、次のような記述がある。

Building on Linux

You need to first install wxWidgets as well as latest versions of FreeType, HarfBuzz and FriBiDi.

https://github.com/googlei18n/fontview

これに従って、wxWidgets, FreeType, HarfBuzz, FriBiDiをインストールしていく。

wxWidgets

sudo apt install libwxgtk3.0-dev

FreeType

コンパイル面倒なのでStefan GlasenhardtによるPPAレポジトリを追加して入れる。

sudo add-apt-repository ppa:glasen/freetype2
sudo apt update
sudo apt install libfreetype6-dev

入ったバージョンは2.8.1。

HarfBuzz

harfbuzz/BUILD.md at master · harfbuzz/harfbuzz · GitHubに従ってインストールする。

sudo apt install libicu-dev libcairo2-dev libgraphite2-dev gcc g++ libfreetype6-dev libglib2.0-dev libcairo2-dev
wget https://www.freedesktop.org/software/harfbuzz/release/harfbuzz-1.5.1.tar.bz2
tar xvf harfbuzz-1.5.1.tar.bz2
cd harfbuzz-1.5.1
./configure
make
sudo make install

FriBiDi

リポジトリに入っているバージョンが0.19.7だったので、そのままリポジトリから入れてしまう。

sudo apt install libfribidi-dev 

Raqm

fontviewが必要とするのでこれも入れる。

sudo apt install libglib2.0-dev gtk-doc-tools
cd ~
wget https://github.com/HOST-Oman/libraqm/releases/download/v0.5.0/raqm-0.5.0.tar.gz
tar xvf raqm-0.5.0.tar.gz
cd raqm-0.5.0
./configure
make
sudo make install

FontViewのインストール

cd ~
sudo apt install git
git clone https://github.com/googlei18n/fontview.git
cd fontview/src/fontview
g++ *.cpp --std=c++11 `pkg-config --cflags --libs harfbuzz freetype2 fribidi raqm` `wx-config --cflags --libs` -o fontview

すると、~/fontview/src/fontview/fontviewにバイナリが生成される。コンパイルコマンドはREADMEの方法と異なっているので注意。

適当にパスの通ったところにコピーしておく。

cd ~
mkdir bin
cp fontview/src/fontview/fontview bin/

再起動する。

テスト

Adobeのvariable fontのプロトタイプをダウンロードして動作を確認してみる。

cd ~
wget https://github.com/adobe-fonts/adobe-variable-font-prototype/releases/download/1.003/AdobeVFPrototype.ttf
fontview AdobeVFPrototype.ttf

f:id:nixeneko:20180406232134p:plain
Weight/Contrastを弄ると太さや細い部分がどれだけ細いかを変化させることができる。