前置き / Apple Color Emoji on iOS 10+
Apple Color EmojiはmacOS / iOSで使われているカラー絵文字フォントです。
ここではその中でもiOS 10以降に搭載されているApple Color Emojiのフォントの中身の画像を解析してデコーダを公開した話をします。
解析期間: 2018/01/14, 2018/07/18 - 2018/08/13
デコーダ: https://github.com/cc4966/emjc-decoder
カラー絵文字フォント (OpenType)
世には4種類のカラー絵文字フォントの仕様がある
-
COLR
/CPAL
- Windowsのやつ
-
CBDT
/CBLC
- Androidのやつ
-
SVG␣
/CPAL
- Adobe/Mozillaが推進してるやつ
-
sbix
| 今回の話- iOS / macOSのやつ
- Standard Bitmap Graphics Table の略
sbix
仕様書/ドキュメントはこう言っている。
- Appleが運用しているドキュメント
- 対応形式:
jpg␣
,pdf␣
,png␣
,tiff
,mask
,dupe
- 参考: https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6sbix.html
- 対応形式:
- OpenTypeの仕様
- 対応形式:
jpg␣
,png␣
,tiff
,dupe
- 参考: https://docs.microsoft.com/en-us/typography/opentype/spec/sbix
- 対応形式:
pdf␣
とmask
はAppleのみの独自仕様なのです。
sbix (真)
だけど! 実際にはこうなってる。
- Appleが運用しているドキュメント
- 対応形式:
jpg␣
,pdf␣
,png␣
,tiff
,mask
,dupe
- 対応形式:
- Appleが運用している仕様(真)
- 対応形式:
jpg␣
,pdf␣
,png␣
,tiff
,mask
,dupe
,emjc
- 対応形式:
sbix/emjcとは……?
emjc
…?
-
"sbix" "emjc"
でググっても情報はない!- macOSでは相変わらず
png␣
が使用されている - iOSのApple Color Emojiのみ
sbix
が使用されている
- macOSでは相変わらず
-
sbix
- Standard Bitmap Graphics Table の略
- なーにがStandardじゃい!
……というわけで謎のバイナリを解析して、emjc画像のデコーダを作りました。
sbix/emjcの解析結果
sbix/emjcの概要
sbix/emjcの画像のデータ構造
- ヘッダー
- 圧縮データ
sbix/emjcの概要 (ヘッダー)
ヘッダーのデータ構造
Type | Name | Description |
---|---|---|
Tag | format | おそらく画像フォーマット(常にemj1 ) |
uint16 | version | おそらくバージョン(常に0x0100 ) |
uint16 | unknown | 不明(常に0xa101 ) |
uint16 | width | 画像の幅 [pixel] |
uint16 | height | 画像の高さ [pixel] |
uint16 | offsetArrayLength | 可変長データ部分のバイト数 |
uint16 | padding | paddingもしくはoffsetArrayLengthの上位2バイト |
widthとheight
実際のデータは全て正方形の画像しか含んでいないためwidthとheightの順番は、解析過程でmacOS/iOSの文字描画エンジンに長方形のデータを与えて確認したものです。
sbix/emjcの概要 (圧縮データ)
圧縮データ
Type | Name | Description |
---|---|---|
byte[] | data | lzfseで圧縮されている |
lzfse
lzfse/lzfse はAppleが開発した圧縮形式です。
Apple Color Emoji on iOS 10+内の圧縮データはbvx2
から始まってbvx$
で終わるのでそこでlzfseで圧縮されていることを確認できます。
Constant | Name | Description |
---|---|---|
0x24787662 | bvx$ |
end of stream |
0x2d787662 | bvx- |
raw data |
0x31787662 | bvx1 |
lzfse compressed, uncompressed tables |
0x32787662 | bvx2 |
lzfse compressed, compressed tables |
0x6e787662 | bvxn |
lzvn compressed |
sbix/emjcの概要 (解凍後の圧縮データ)
Type | Name | Description |
---|---|---|
byte[width*height] | alphaArray | 画像のアルファ値 |
byte[height] | filterArray | 各行のフィルタの種類 (0/1/2/3/4) |
uint24[width*height] | operandArray | 操作量の配列 |
byte[offsetArrayLength] | offsetArray | 操作量のオフセット量の情報 |
RGBA32画像の復元方法
- alphaArrayがそのまま$[0,256)$のアルファ値の配列になっている
- 値の変化が境界のみで発生するためこのままで十分圧縮できると思われる
- RGBデータはfilterArray / operandArray / offsetArrayから復元する
RGBデータの復元方法
- filterArray / operandArray / offsetArrayからwidth*heightピクセル分の符号付整数の3つ組の配列を生成する
- 各ピクセルについて、符号付整数の3つ組をRGBデータに変換する
符号付整数の3つ組の配列の生成方法
- 画像の各行について対応する
filter
を適用するfilter = filterArray[y]
- 各行の各ピクセルに対応する3つ組の操作量に
filter
の操作を適用する
3つ組の操作量の計算方法
-
operand
とoffsetArrayから各ピクセルの3つ組の操作量を計算するoperand = operandArray[y+width+x]
-
operand
の各バイトは下位1bitが符号、上位7bitが操作量の絶対値- 例: 100なら+50 / 255なら-127
-
operand
は3バイトなので3つ組の操作量になる - 通常の符号付整数を使わない理由(推測)
- $0$付近の操作が多い($0$付近の操作が全て$0$付近で表現できて圧縮率が上がる)
- $-0$という表現が欲しい(後述する)
offsetArrayの利用方法
- 最小構成は
0xFC
を operandArrayのバイト数 / 64 [byte] 並べたもの -
0xFC
は0b11111100
-
0xFC
は(63 << 6) + 0
- 63 [byte] スキップして、64 [byte] 目で0という操作をする
- 上位6bitは何バイトスキップするかという情報
- 下位2bitは対応する操作量をどれだけ増やすかという情報
- 0: 操作量の絶対値を0大きくする=何もしない
- 1: 操作量の絶対値を128大きくする
- 2: 操作量の絶対値を256大きくする
- 3: 操作量の絶対値を384大きくする
- ここで$-0$という操作量も意味を持つ
フィルタの操作
Filter | Description |
---|---|
0 | オペランドがそのまま内部表現になる |
1 | オペランドは左または上の内部表現からの差分, 左上から見て左または上の変化量の多い方が選択される, 左端なら上からの差分, 変化量が同じなら左を優先 |
2 | オペランドは左の内部表現からの差分, 左端ならオペランドがそのまま内部表現になる |
3 | オペランドは上の内部表現からの差分 |
4 | オペランドは左と上の内部表現の平均からの差分, 左端なら上からの差分 |
フィルタの操作の注意事項
- 基本的に3つ組の操作量それぞれに独立して適用する
- ただしfilter 1のみ、3つ組の最初の比較で左と上どちらが選択されるかが決まる
- 1, 3, 4は前の行の内部表現が必要になる
- macOSのレンダラはそこを初期化しないので、画像の1行目にこれを使うと描画するたびに結果が変わる(意図せず初期化前のメモリを読めるのまずそう)
符号付整数の3つ組からRGBへの変換方法
ここまでで各ピクセルについて、符号付整数の3つ組$(x_1,x_2,x_3)^\top\in\mathbb{N}^3$がわかっている。
\begin{bmatrix}
r \\
g \\
b \\
\end{bmatrix}=
\begin{bmatrix}
1 & \frac{1}{2} & -\frac{1}{2} \\
1 & 0 & \frac{1}{2} \\
1 & -\frac{1}{2} & -\frac{1}{2} \\
\end{bmatrix}
\begin{bmatrix}
x_1 \\
x_2 \\
x_3 \\
\end{bmatrix}
r,g,bからRGBへの変換
-
rgb
は最後に$\mathbb{N}\mapsto [0, 256)$する- 変換は$\mathbb{R}→S^1$っぽい写像
R = (r % 257) & 0xFF
G = (g % 257) & 0xFF
B = (b % 257) & 0xFF
- 先ほどの変換行列はちゃんと正則
- r, g, bは独立に指定できる
付録
- 平均の計算やRGBへの変換における$\frac{1}{2}$の操作
- 正なら絶対値を切り上げ
- 負なら絶対値を切り捨て
まとめ
sbix/emjcをデコードしたいとき
- 画像サイズと補正情報の長さからlzfseで画像情報をデコードする
- ピクセル分の符号付整数の3つ組の配列を用意(実際には2行分で十分だけど)
- 符号付整数の3つ組の配列をoffsetArrayで初期化
- 符号付整数の3つ組の配列にoperandArrayを足す
- 各行についてfilterを適用する
- 各行について符号付整数の3つ組の配列をRGBに変換する
- RGBデータ+アルファ値, width, heightを返す
emjcデコーダのライブラリ
細かい実装についてはGitHubにデコーダを置いたのでそちらをご参照ください。
今後
- FreeTypeあたりにsbix/emjcについての情報を流して取り込まれたい
- これを使ったAndroid版 / iOS版のTATEditorを公開したい
- そもそもこのデコーダは自作の文字描画系で描画できないカラー絵文字フォントを見つけたので作った
- iOS版を出せたらこのデコーダが初めて実用される
- (アプリのプライバシーポリシーが作り終わらない……)