にせねこメモ

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

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を弄ると太さや細い部分がどれだけ細いかを変化させることができる。

平成の次の新元号の文字列を取得するコード

※これはネタですが、エイプリルフールとは無関係です。


まず実用性はないのだが、新元号の文字列を取得するJavascriptコードを思いついたので書いておく。

新元号 = "㋿".normalize("NFKD");

今は何も意味のあるものは得られないが、新元号が発表されてしばらくすれば、最新のブラウザで上記のコードを実行すると新元号を表す文字列が取得できるようになるはずである。

実行例:

何をやっているのか

去年の12月に、日本のNational Bodyから、(「㍾」「㍽」「㍼」「㍻」のように)新元号の合字の独立したコードポイントを、古いITシステムのためにBMP*1に確保してくれという要請が出された。

これに対し、Unicodeコンソーシアムは、U+32FFを新元号として確保するようにした。

つまり、U+32FF “㋿”は新元号が発表された後、(フォントが対応すると)新元号で表示されるようになり、Unicodeにも正式に採録されるだろうと考えられる。
一方でUnicodeは、検索などの利便性のために、Unicode正規化という仕組みを用意している。この仕組みを利用すると、例えば「平成」で検索して「㍻」をヒットさせることができる。正規化の挙動はUnicode Character Database (UCD)として提供されている。

この仕組みを利用して、"㍻"から"平成"が得られる。

heisei = "㍻".normalize("NFKD"); //"平成"が返る

実行例:

同様に新元号“㋿”についても、ブラウザの利用するUCDが新元号の分解に対応したものに更新された暁には、Unicode正規化を利用して新元号を表す文字列が取得できる様になるはずである。


とはいっても、新元号が発表されてからUCDの新版が出るまではラグがあるはずであり、結局はUCDが対応するより前に手動で対応することになるのだと思う。そもそも、UCDの更新を行っていないシステムもあるだろうので、あまり期待しない方がよさそうである。

*1:U+0000~U+FFFF。UTF-16サロゲートペアを使わずに表現できる範囲。

はてなブログ+さくらのレンタルサーバをHTTPS化する

このブログを全面的にhttpsに切り替えたので、何をやったかを書いておく。

経緯

次のページに詳しいが、ブログやWebサイトをhttpsにせずhttpのままで運用していると、不都合があるっぽい:
はてなブログSSL暗号化通信(https)対応問題、10月から起こるかもしれない問題を解りやすく書いてみました。 - 神戸グルメゲリラ


一方ではてなブログは、2017年10月から2018年初頭にかけて、ユーザーが設定することで全面HTTPS化ができるよう実装予定だとのことだった:
はてなブログへの接続をすべてHTTPSにできる機能の実装予定と、利用を検討するユーザー様に準備いただきたいこと - はてなブログ開発ブログ

そして、2018/02/22から、はてなの提供するドメインによるブログ(つまり、独自ドメインでないブログ)のhttpsによる配信ができるようになったとのこと。
はてなが提供するドメインのブログで、HTTPSで配信できる仕組みの提供を開始しました(追記あり) - はてなブログ開発ブログ


記事内でも書かれているが、HTTPSで配信されるページにHTTPで配信されるコンテンツ(CSS, Javascript, img, iframeなど)が混在している場合(mixed content)に問題になる。

ここで、私ははてなブログはてなが提供するドメインで運用しているため、HTTPS配信への移行は簡単に達成できることが予想されるが、一方でWebフォントなどのリソースをさくらのレンタルサーバ上に置いているため、そのHTTPS化を行わないとそれらのリソースは読み込むことができなくなる。

さくらのレンタルサーバSSL

という訳で、ブログで利用するリソースを置いているさくらのレンタルサーバ(ライト)のHTTPS化を行う。

一つの方法として、共有SSLというものをさくらで提供している。これは、設定をするとexample.sakura.ne.jpの形の初期ドメインであればそのままHTTPS化ができるというものである。初期ドメインのまま利用しているばあいはこれを使うのが簡単で良いと思うが、一方でさくらのサブドメインを含む独自ドメインについては共有SSLはセキュリティ的によろしくなく*1、非推奨とのことである。
共有SSL|さくらのレンタルサーバ

他の方法として、独自でSSL証明書を取得して、HTTPS化する方法があるのだが、これは独自ドメインのためのもので、さくらのサブドメインでは使うことができないようである。

結局、今回は共有SSLを設定し、example.sakura.ne.jpの形の初期ドメインで使うことにして、ブログ記事で参照しているURLを書き換えた。
SSLの設定はサーバコントロールパネルから行う:
【共有SSL】設定方法 – さくらのサポート情報

さくらの共有SSLはプロキシとして動作しているため、細やかな設定が難しい場合があるらしい:
さくらのレンタルサーバでHTTPS(SNI SSL)な独自ドメインのWordpressサイトを構築する際の注意点 - Qiita

はてなブログの設定

最終的にはてなブログHTTPS化したのは2018年3月28日であるが、Mixed contentの問題があるため、HTTPで埋め込んでいるものは事前に書き換えておいた。
特にさくらのレンタルサーバSSL化したため、そこに置いていたリソースのURLがすべて変更になったので、はてなブログの記事で参照しているリソースをすべてHTTPSのものに書き換えた。

HTTPS

f:id:nixeneko:20180328231517p:plain
はてなブログダッシュボードの設定を開いたら、HTTPS配信が利用できる様になっていたため、切り替えた。
切替は一度確認が出るくらいで一瞬だった。
f:id:nixeneko:20180328231718p:plain

Mixed content対策

さくらのレンタルサーバに置いてたリソースはHTTPSに移行したが、他に、はてな記法の[~:embed]で埋め込んだものなどがちゃんと読み込めるか記事を見ていって確認する。
Mixed contentがある場合、ブラウザのコンソールにエラーが表示されるらしい。それも参考に潰していく。

サイト自体はhttp配信であるもののoEmbedなどで埋め込まれるページはhttpsで配信しているサービスも多く、割とそのままでもなんとかなる様であった。

mixed contentの制限があるのでhttpで埋め込んでるコンテンツがないか探していく。

  • リンクを張っているのみ→そのままでよい
  • カスタマイズCSSなどで参照している外部ファイル→HTTPSへの対応が必要
  • はてなフォトライフの画像の埋め込み→はてな記法で埋め込んでる限り、はてな側でよしなにしてくれるっぽい。
  • はてなhttp記法[~:sound]で音声ファイルのプレーヤを表示するのはhttpsに対応していないので、HTML5の<audio>タグ等で置き換える。
  • はてなhttp記法[~:embed]で埋め込み→埋め込み先がhttpsに対応していない場合、困るかもしれない。
    • oEmbedによるブログカード等の埋め込みが表示されなくなっていた場合、記事の編集画面を開いて保存し直したら表示される様になったことがある。

埋め込んでいた他サイトの動画やブログカードについて次のような感じであった。次のようなサイトは、URLがhttp://~のままでもはてな記法の埋め込み[http://~:embed]でうまくいった。一旦記事の編集画面を開いて保存し直すと表示されるようになるものもあった。

  • ニコニコ動画: サイトはhttpであるが、埋め込み用のページはhttpsで配信されている。
  • はてなブログ: ブログがhttp/httpsに関わらず、oEmbedで指定された埋め込み用のページはhttpsで配信されている。
  • Qiita: httpでアクセスするとhttpsに飛ばされる。


はてなブログtex記法を使っている場合、mixed content (混在表示コンテンツ)の警告が表示されることがあった。これは、一度記事の編集画面を開いて保存し直すと直るらしい。
はてなが提供するドメインのブログで、HTTPSで配信できる仕組みの提供を開始しました(追記あり) - はてなブログ開発ブログの追記を参照。



とりあえずしばらくは様子見。とはいってもhttpでの配信に戻すことはできないのだけど。

*1:××××.comというドメインであれば、共有SSLのURLは https://secure○○.sakura.ne.jp/××××.com のようになり、他のブログと同一ドメインにまとめられてしまう。

Pleromaのお一人様インスタンスをUbuntu 16.04で立てた作業ログ

https://nixeneko.infoにPleromaのおひとり様インスタンスを立ててみた。
Mastodon等やっている方は、よければ@nixeneko@nixeneko.infoをリモートフォローしてください。
(※20181227追記: この記事は古くなってるので、多分今はそのまま使えません。)

概要

Pleromaという、GNU SocialやMastodon互換のマイクロブログSNSソフトウェアがある。機能としてはTwitterに近い。
これは自分でサーバを用意して動かす(インスタンスを立てる)こともでき、インスタンスが異なっても互いにフォローできる*1ので、自分専用のインスタンスとして使っても問題ない。
Twitterという一企業のサービスに依存しないのも良いなあと思って、VPSを借りて立ててみた。


以降は2018/3/6に作業したログという感じなので、まとまっていないし、現在では変わっている部分もあるかもしれない。

実際に立ててみたい場合には、Pleroma公式Wikiのインストールガイドや次のページなどを参照するとよいと思う。

下準備

  • ドメインを取得した(nixeneko.info)。
  • ConoHaで一番安いメモリ512MBのVPSを借りた。
    • OSはUbuntu 16.04(.4 LTS)にし、SSH用にローカルで作成した公開鍵を登録しておいた(とはいえ、別に作業用のユーザを作成したので後で削除した)。接続許可ポートはすべて許可にした。
  • VPSIPアドレスを確認し、取得したドメインVPSIPアドレスを指すようにドメインのAレコードを設定した。

作業ログ(2018/3/6)

初期設定

上のページを参考にサーバの初期設定を行った。

rootでSSHでログインして(公開鍵を設定してない場合はConoHaのコンソールから作業することになると思う)、ソフトウェアを更新する。

apt update
apt upgrade

と更新したら、設定がリポジトリでインストールしたデフォルトから変更されてるよって言われて置き換えるか聞かれたが、/etc/cloud/cloud.cfgはそのまま残した(これは初回起動時に実行されるものらしいので、たぶんどっちでもよい)。

作業ユーザの追加

rootユーザで

adduser workuser
gpasswd -a workuser sudo

とし、作業ユーザを追加しsudoできるようにした。

su workuser

rootから作業ユーザに切り替える。

sudo: unable to resolve host回避

その後、sudoする度に“sudo: unable to resolve host (ホスト名)”と表示されるので、/etc/hostsに

127.0.1.1       (ホスト名)

を追記したら表示されなくなった。
参考: sudo: unable to resolve host が表示されたら - Qiita

作業ユーザへのSSH公開鍵の設定
cd ~
mkdir .ssh
chmod 700 .ssh
cd .ssh
touch authorized_keys
chmod 600 authorized_keys
nano authorized_keys

authorized_keysにローカルで作成した公開鍵をペーストし保存した。

sshdの設定
sudo nano /etc/ssh/sshd_config

/etc/ssh/sshd_configを開き、ルートログイン禁止とsshのポートの変更を行った。
ルートログイン禁止:

PermitRootLogin no

SSHポートの変更:

Port 10022

以上のように書き換え(sudo sshd -tで構文チェックができる)、sshdを再起動。

sudo service sshd restart
rootユーザに対して追加されていた公開鍵を消す
sudo su root
rm ~/.ssh/ authorized_keys

設定が済んだら作業用ユーザでログインし直す(ポートも変更したものに合わせる)

Pleromaのインストール

公式のWikiに載ってるのに従ってやっていく。

PostgreSQL 9.6のインストール

9.6以上のバージョンが標準リポジトリにないので、PostgreSQLの公式ページに従ってインストールする。

sudo nano /etc/apt/sources.list.d/pgdg.list

以下を書いて保存する。

deb http://apt.postgresql.org/pub/repos/apt/ xenial-pgdg main

その後、次のコマンドを実行してインストールする。

wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt update 
sudo apt install postgresql-9.6 postgresql-contrib-9.6
Elixir 1.5+

参照: Installing Elixir - Elixir

wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb && sudo dpkg -i erlang-solutions_1.0_all.deb
sudo apt update
sudo apt install elixir
他に必要なものをインストール

他の必要なパッケージをインストールする。

sudo apt install erlang-dev erlang-parsetools erlang-xmerl git build-essential nginx
ユーザ作成
sudo adduser pleroma
sudo usermod -aG sudo pleroma
su pleroma
ソースコードを持ってきて必要なプログラムをインストールしたり設定をする
cd ~
git clone https://git.pleroma.social/pleroma/pleroma
cd pleroma
mix deps.get

Hexをインストールするか聞かれたらyを入力。

設定ファイルを作成
mix generate_config

rebar3をインストールするか聞かれるのでy。
他にも、ドメイン名、インスタンス名、adminのEメールアドレスを入力する。
mediaproxyを有効にするか聞かれ、nとした。

作成された設定ファイルをリネームする。

mv config/generated_config.exs config/prod.secret.exs
データベースの作成・マイグレーション
sudo su postgres -c 'psql -f config/setup_db.psql'
MIX_ENV=prod mix ecto.migrate
起動テスト
MIX_ENV=prod mix phx.server

これで起動するが確かめたらCtrl-Cで停止する。

SSL証明書取得

Let's EncryptでSSL証明書を取得する。(ここらへんよくわかってない)

sudo add-apt-repository ppa:certbot/certbot
sudo apt update
sudo apt install python-certbot-nginx
sudo certbot certonly -d (ドメイン名)

すると色々聞かれるので入力すると、 /etc/letsencrypt/live/(ドメイン名)/ 以下に証明書が生成された。
(Pleromaのサイトによると証明書の取得についてより先にnginxの設定がきているので、その順番でやっていたが、certbot --nginxというふうに証明書を取得しようとしたところ、nginxの設定で指定されている証明書がないよ、とエラーが出て動かせなかった。最初は--standaloneで証明書取得を行った方が楽かもしれない)

nginxの設定

設定ファイルをコピーし、設定を変更する。

sudo cp /home/pleroma/pleroma/installation/pleroma.nginx /etc/nginx/sites-enabled/pleroma.nginx
sudo nano /etc/nginx/sites-enabled/pleroma.nginx

サーバのドメインを指定し、cert pathsも取得した証明書にあわせて修正する。

sudo systemctl restart nginx.service
サービスの設定
sudo cp /home/pleroma/pleroma/installation/pleroma.service /lib/systemd/system/pleroma.service
sudo nano /lib/systemd/system/pleroma.service

/lib/systemd/system/pleroma.serviceを開き、[Service]のところに

Environment="MIX_ENV=prod"

を書き加える。

sudo systemctl start pleroma.service

サービスを起動する。

自分用のアカウントを作る

設定したドメインにアクセスし、自分で使うためのPleromaのユーザを作成する。

登録の停止
nano ~/pleroma/config/prod.secret.exs

を編集し

  limit: 400,
  registrations_open: false

に変更して登録を停止した。またついでに投稿辺りの文字数の上限を何となく400文字とした。

設定の反映・自動起動設定

再起動してPleromaの設定を反映

sudo systemctl restart pleroma.service

Pleromaのサービスを自動起動するように設定

sudo systemctl enable pleroma.service
iptablesの設定

firewall有効化

sudo ufw enable

Ubuntuファイアウォールはデフォルトでufwなのであるが、iptablesの方が設定ファイルの流用がしやすいのでiptables使って設定ファイルを編集した。ufwコマンドで設定する方が筋がいいかもしれない。

このサイトの記述を元に設定を書いたテキストファイル(~/iptables)を作成する。sshのポートをsshd_configで設定したものに合わせて変える。(最終的に、http, https, sshの接続を許可した)

設定を反映し、永続化させる。

sudo iptables-restore < ~/iptables
sudo apt install iptables-persistent

ここで、iptablesの設定の反映は次のコマンドでできるっぽい。

sudo /etc/init.d/iptables-persistent save 

参考: Ubuntuでiptablesの設定をiptables-persistentで永続化する

let'sencryptの自動更新
/usr/bin/certbot renew --post-hook "service httpd restart"

を動かしてみてちゃんと動くか確認する。(証明書取得してすぐだと更新されないが、Cert not yet due for renewalと出れば問題ない)

cronに更新用のコマンドを設定する。

sudo nano /etc/cron.d/letsencrypt

で開き、内容は次のようにする。これは毎週火曜日16時33分に発動する設定になっている。

33 16 * * 2 root /usr/bin/certbot renew --post-hook "service httpd restart"

参考: Let&#39;s Encryptを使ってSSL証明書を自動更新する(AWS/Amazon Linux/Apache) - Qiita

こまごまとした設定

Instance specific panelの表示
nano ~/pleroma/priv/static/static/config.json

  "showInstanceSpecificPanel": true

のようにtrueに変更する。

Instance specific panelの内容の設定は~/pleroma/priv/static/instance/panel.htmlを弄るとできる。

nano ~/pleroma/priv/static/instance/panel.html

として開いて、適当に書き換える。

インスタンスのサムネイル画像の差し替え

また、 ~/pleroma/priv/static/instance/thumbnail.jpeg を置き換えると、 Pleroma Instances などで表示されるアイコンが、デフォルトの暗灰色にπλήρωμαの文字の画像から変更できる。

ロゴの変更

ロゴ、つまりブラウザでPleromaを開いたとき上部真ん中に表示される画像が標準で ~/pleroma/priv/static/static/logo.png となっているので、これを入れ替えるとPleromaフロントエンドの真ん中上部に表示される画像を変更できる。
なお、ロゴ画像ファイルの位置は ~/pleroma/priv/static/static/config.json で指定されている。

チャットの削除

チャットはお一人様インスタンスでは意味がないので消す。 ~/pleroma/config/prod.secret.exs を開き、

nano ~/pleroma/config/prod.secret.exs

次の内容を最後に書き加える。

config :pleroma, :chat,
  enabled: false

設定が終わったらPleromaを再起動する。

sudo systemctl restart pleroma.service

Pleroma最新版への更新(2018/3/17)

2018/3/8あたりでPleromaにActivityPubによるfederationが追加された。私がインストールしたのは3/6なので更新したが、やや面倒だった。更新でなくて一から入れ直すのであれば必要ない手順だと思う。

Pleromaの更新

このページに書かれているように、Pleromaのサービスを止め、最新版のコードにし、マイグレーションを行った。

sudo systemctl stop pleroma.service
cd ~/pleroma/
git pull
MIX_ENV=prod mix ecto.migrate
Nginxの設定を変える

に従って、

sudo nano /etc/nginx/sites-enabled/pleroma.nginx

と /etc/nginx/sites-enabled/pleroma.nginx に次のように書き加える。

        proxy_set_header Host $http_host;
ActivityPub対応

に従って

MIX_ENV=prod mix deps.clean --build mime

を実行し、NginxとPleromaを再起動する。

sudo systemctl restart nginx.service
sudo systemctl restart pleroma.service

感想

PleromaはRaspberryPiでも動かせる! という触れ込みだったのでメモリ512MBのVPSにインストールしたみたのだが、今のところ問題なく動いているようだ。メモリ2GB程度無いと安定動作が難しいMastodonと比べると軽さが際立つ感じがする。

リソース使用量

2018/3/20現在、11アカウントをフォローし、10アカウントからフォローされている。
基本的に、リソース使用料はインストール時を除いて安定しているように思う。しばらく様子見。

CPU

f:id:nixeneko:20180320145001p:plain

ディスクIO

f:id:nixeneko:20180320145127p:plain

転送量

f:id:nixeneko:20180320145043p:plain

*1:このようにサーバーやインスタンスが異なっても「連合」(federation)して互いにフォローすることができる。連合しているSNSネットワークのことをfediverseという。