次世代画像フォーマットJPEG XLは、JPEGと違ってアニメーションもアルファチャンネルもサポートしています。つまりこれひとつでJPEG/PNG/GIFがカバーしている領域を奪えるっていう野心的なフォーマットですね。これに乗らない手はない!ってことで、WebKit に JPEG XL decoderを追加してみました。まだアニメーションには未対応なので、どうやってアニメーションさせればいいかを見てみようと思います。
libjxl
JPEG XLのデコードには libjxl ライブラリを使用します。libjxl API の設計として
- (部分)データをデコーダにセットする
- セットしたデータを処理しろと指示する。
ということの繰り返しでデコードが進むようになっています。
実際のAPIで言うと以下のような流れになります。
JxlDecoder*dec = JxlDecoderCreate(nullptr);
JxlDecoderSetInput(dec, data, datasize); // 入力データのセット
while (true) {
JxlDecoderStatus status = JxlDecoderProcessInput(dec);
if (status == JXL_DEC_ERROR || status == JXL_DEC_SUCCESS || status == JXL_DEC_NEED_MORE_INPUT) {
// JXL_DEC_ERRORかJXL_DEC_SUCCESSの場合はデコード終了。
// JXL_DEC_NEED_MORE_INPUT の場合は後続データ待ち。
break;
}
// status に応じた処理
}
size_t remaining = JxlDecoderReleaseInput(dec);
// JxlDecoderReleaseInput()を呼ぶと処理しきれなかったデータサイズが返るので、
// JXL_DEC_NEED_MORE_INPUTの場合は、残データと後続データを結合して、
// 再度 JxlDecoderSetInput()から繰り返す。
JxlDecoderProcessInput()
は区切りのいいところで中断させることができます。例えば画像サイズが確定したところでいったんJxlDecoderProcessInput()
を抜けてほしい場合は、
JxlDecoderSubscribeEvents(dec, JXL_DEC_BASIC_INFO);
とすると、画像の基本情報(BasicInfo)がパースできた段階で JxlDecoderProcessInput()
が終了し、JXL_DEC_BASIC_INFO
が関数から返されます。このタイミングでBasicInfoが取得できるようになっているので、JxlDecoderGetBasicInfo()
を使って取得したりします。
フレームのデコード
アニメーションの場合は複数のフレームがJPEG XLに格納されています。静止画の場合はフレームは一つです。デコーダがフレームを検出すると、JxlDecoderProcessInput()
がJXL_DEC_FRAME
を返します。(前もってSubscribeする必要があります)
このタイミングで JxlDecoderSetImageOutCallback()
を使ってコールバックを登録すると、実際のピクセルデータが得られたタイミングでデコーダがコールバックを呼び出し、ピクセルデータが渡されます。コールバックの形式は
typedef void (*JxlImageOutCallback)(void* opaque, size_t x, size_t y,
size_t num_pixels, const void* pixels);
のようになっています。
一つのフレームの処理が完了すると、JXL_DEC_FULL_IMAGE
がJxlDecoderProcessInput()
から返ります。
フレームのデコード処理をまとめると
- JXL_DEC_FRAME: フレーム開始
- ImageOutCallback 経由でピクセルデータ回収
- JXL_DEC_FULL_IMAGE: フレーム完了
となります。
アニメーション
上記流れでフレーム画像自体は取得できるのですが、実際のアニメーションにあたっては時間軸の情報が必要です。
まずBasicInfoには
/** Indicates animation frames exist in the codestream. The animation
* information is not included in the basic info.
*/
JXL_BOOL have_animation;
とアニメーションがあるかないかのフラグがありますね。さらに、これがTRUEの場合は
/** Animation header with global animation properties for all frames, only
* used if have_animation is JXL_TRUE.
*/
JxlAnimationHeader animation;
とAnimationHeaderもBasicInfoに設定されます。
AnimationHeaderは
/** The codestream animation header, optionally present in the beginning of
* the codestream, and if it is it applies to all animation frames, unlike
* JxlFrameHeader which applies to an individual frame.
*/
typedef struct {
/** Numerator of ticks per second of a single animation frame time unit */
uint32_t tps_numerator;
/** Denominator of ticks per second of a single animation frame time unit */
uint32_t tps_denominator;
/** Amount of animation loops, or 0 to repeat infinitely */
uint32_t num_loops;
/** Whether animation time codes are present at animation frames in the
* codestream */
JXL_BOOL have_timecodes;
} JxlAnimationHeader;
となっています。こちらはグローバルな設定のようです。
JXL_DEC_FRAME の説明は
/** Informative event by JxlDecoderProcessInput: Beginning of a frame.
* JxlDecoderGetFrameHeader can be used at this point. A note on frames:
* a JPEG XL image can have internal frames that are not intended to be
* displayed (e.g. used for compositing a final frame), but this only returns
* displayed frames. A displayed frame either has an animation duration or is
* the only or last frame in the image. This event occurs max once per
* displayed frame, always later than JXL_DEC_COLOR_ENCODING, and always
* earlier than any pixel data. While JPEG XL supports encoding a single frame
* as the composition of multiple internal sub-frames also called frames, this
* event is not indicated for the internal frames.
*/
JXL_DEC_FRAME = 0x400,
となっています。
A displayed frame either has an animation duration or isthe only or last frame in the image.
と言っているので、最終フレーム以外には animation duration が設定されているようです。
フレーム情報は JxlDecoderGetFrameHeader()
として取得可能です。
/** The header of one displayed frame. */
typedef struct {
/** How long to wait after rendering in ticks. The duration in seconds of a
* tick is given by tps_numerator and tps_denominator in JxlAnimationHeader.
*/
uint32_t duration;
/** SMPTE timecode of the current frame in form 0xHHMMSSFF, or 0. The bits are
* interpreted from most-significant to least-significant as hour, minute,
* second, and frame. If timecode is nonzero, it is strictly larger than that
* of a previous frame with nonzero duration. These values are only available
* if have_timecodes in JxlAnimationHeader is JXL_TRUE.
* This value is only used if have_timecodes in JxlAnimationHeader is
* JXL_TRUE.
*/
uint32_t timecode;
/** Length of the frame name in bytes, or 0 if no name.
* Excludes null termination character.
*/
uint32_t name_length;
/** Indicates this is the last animation frame.
*/
JXL_BOOL is_last;
} JxlFrameHeader;
frame の duration は tick 単位で表現され、それが何秒にあたるか(ticks per second) はグローバルなAnimationHeaderを参照しろってことですね。
そして SMPTE timecode なるものが出てきました。がオプショナルっぽいので無視しておきます。
では実データで見てみましょう。conformance testからanimation_icos4d のinput.jxl を使ってみます。
BasicInfo は
num_loops = 0
tps_numerator = 1000
tps_denominator = 1
となっています。 num_loops = 0 なので、無限ループです。
tick per sec は tps_numerator / tps_denominator = 1000 になります。
各フレームは
duration = 50, is_last = 0
duration = 50, is_last = 0
...
duration = 50, is_last = 0
duration = 50, is_last = 1
と、すべて duration = 50 ticks となっています。ですので、フレーム当たり 50 / 1000 sec = 50 ms でアニメーションすることになります。
また、is_last フラグは最終フレームのみセットされていますね。
メタデータとしてフレーム数情報が取得できないようなので、事前にフレーム数が必要な場合は1パス目でフレームヘッダだけ読んでいく、といった処理が必要になってくるのかもしれません。
フレームの読み飛ばしはJxlDecoderSkipFrames()
で行えます。また、先頭に巻き戻すにはJxlDecoderRewind()
が使えます。JxlDecoderSkipFrames()
はキャッシュを使っているようで、初回走査時はデコードしていく場合と速度的には大差がないようです。
WebKit 実装
といったあたりを踏まえてWebKit側で対応をいれました!
Support Animated JPEG-XL images