LoginSignup
2
1

More than 1 year has passed since last update.

JPEG XL animation を実装してみた

Posted at

次世代画像フォーマットJPEG XLは、JPEGと違ってアニメーションもアルファチャンネルもサポートしています。つまりこれひとつでJPEG/PNG/GIFがカバーしている領域を奪えるっていう野心的なフォーマットですね。これに乗らない手はない!ってことで、WebKit に JPEG XL decoderを追加してみました。まだアニメーションには未対応なので、どうやってアニメーションさせればいいかを見てみようと思います。

libjxl

JPEG XLのデコードには libjxl ライブラリを使用します。libjxl API の設計として

  1. (部分)データをデコーダにセットする
  2. セットしたデータを処理しろと指示する。
    ということの繰り返しでデコードが進むようになっています。

実際の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_IMAGEJxlDecoderProcessInput()から返ります。

フレームのデコード処理をまとめると

  1. JXL_DEC_FRAME: フレーム開始
  2. ImageOutCallback 経由でピクセルデータ回収
  3. 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 を使ってみます。
ref.apng

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

2
1
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
2
1