LoginSignup
11
11

More than 5 years have passed since last update.

iOS 10以降のApple Color Emojiで使用される独自画像フォーマット"emjc"の仕様解析とデコーダ公開について

Last updated at Posted at 2018-08-24

前置き / 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

仕様書/ドキュメントはこう言っている。

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が使用されている
  • sbix
    • Standard Bitmap Graphics Table の略
    • なーにがStandardじゃい!

……というわけで謎のバイナリを解析して、emjc画像のデコーダを作りました。


sbix/emjcの解析結果


sbix/emjcの概要

sbix/emjcの画像のデータ構造

  1. ヘッダー
  2. 圧縮データ

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] 並べたもの
  • 0xFC0b11111100
  • 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をデコードしたいとき

  1. 画像サイズと補正情報の長さからlzfseで画像情報をデコードする
  2. ピクセル分の符号付整数の3つ組の配列を用意(実際には2行分で十分だけど)
  3. 符号付整数の3つ組の配列をoffsetArrayで初期化
  4. 符号付整数の3つ組の配列にoperandArrayを足す
  5. 各行についてfilterを適用する
  6. 各行について符号付整数の3つ組の配列をRGBに変換する
  7. RGBデータ+アルファ値, width, heightを返す

emjcデコーダのライブラリ

細かい実装についてはGitHubにデコーダを置いたのでそちらをご参照ください。


今後

  • FreeTypeあたりにsbix/emjcについての情報を流して取り込まれたい
  • これを使ったAndroid版 / iOS版のTATEditorを公開したい
    • そもそもこのデコーダは自作の文字描画系で描画できないカラー絵文字フォントを見つけたので作った
    • iOS版を出せたらこのデコーダが初めて実用される
    • (アプリのプライバシーポリシーが作り終わらない……)
11
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
11