HEIF形式の画像をpngやjpegにしてブラウザで扱う

画像合成機能を実装するにあたって、ユーザーの画像を取り扱いたい、というケースは多々あると思います。

実際今僕が携わっている案件もまさにそうで、inputtype='file'で受けてそれを canvas 上に描画して、最終的に画像合成…という流れです。

ただこれには問題があって、iOS11 以降でデフォルトとなっている HEIF 形式(.heic.heif)は、そのまま WEB 上で取り扱うことが出来ません。普通に出来ると思ってましたが、完全に甘ちゃんでした。大甘です。

今回はなんとか HEIF を WEB で取り扱えるようにしていきます。

結論だけ先に述べると、JavaScript 実装があるにはあるんですが、ちょっと今回のプロジェクトにいれるのはどうなんだ…という結果になりました。ただ問題になるのは PC からのアップロード時のみなので、特に制約のないプロジェクトであれば、普通に導入して問題ないと思います。

ゴール

  • HEIF 形式の画像を png もしくは jpeg にして ブラウザ上に描画する
    • iPhone から上げる場合は勝手に(?)変換されるので png や jpeg と同じように取り扱えますが、PC からでも取り扱いたい

前提条件

  • バックエンド(ImageMagick)は触りたくない
    • ImageMagick なら簡単に実装できるが、本番環境の ImageMagick が古く…
    • バージョンアップするには検証が大変で…

HEIF とは

まず先に HEIF1とはなにか、です。

High Efficiency Image File Format (ハイ・エフィシエンシー・イメージ・ファイル・フォーマット、略称:HEIF、ヒーフ) は、Moving Picture Experts Group (MPEG) によって開発され、 MPEG-H Part 12 (ISO/IEC 23008-12) で定義された画像ファイルフォーマットである。

まぁ小難しいことを考えてもしょうがないので、png や jpeg と同じく画像ファイルフォーマット(MIME type がimage/*)とだけ捉えておけばいいでしょう。

聞き慣れないですが、冒頭でも書いた通り、iOS11 以降は HEIF 形式がデフォルトのフォーマットになっています(勉強不足ですいません…)。

ブラウザの対応状況

次にブラウザの対応状況を見ていきます。

僕が知らなかっただけで、iOS11 は 2017 年リリースなわけで、HEIF そのものは実は結構昔から使われているわけです。もう 2 年も前ですね。

となると、普通にimgタグのsrcに食わせれば表示されるだろ…と思わせといて、実はブラウザでは HEIF を取り扱うことが出来ません。衝撃の事実です。なんだか政治の匂いがプンプンしますね。

なお、現在サポートされている MIME type は以下の種類だけです2

image/apngimage/bmpimage/gifimage/x-iconimage/jpegimage/pngimage/svg+xmlimage/tiffimage/webp

一通り取り扱えますが、肝心の HEIF が…。

対応の方向性

マイクロサービス的に画像変換サーバーを Lambda とかで書いていいのであれば話は早いんですが、まぁ色々やると怒られそうなので、フロントエンドだけでなんとかならないかチャレンジしてみます。

方向性としては、以下のいずれかが考えられます。

  • JavaScript で実装された既存の converter を使う
  • ImageMagick の convert を リバースエンジニアリングして独自実装する
  • iPhone から上げる場合の挙動をリバースエンジニアリングして独自実装する

1 つ目でいいのがあればその瞬間に優勝です。さらにそれが CDN で配布されていれば、言うことなしです。

2 つ目の場合は、コードリーディングのコストも高そうだし、そこそこ重い処理になりそうだから wasm か…?という感じで暗雲が立ち込めています。まぁ wasm は書いてみたいので魅力的な案ですが、そもそも wasm を勝手に導入するのも(ry

最後の 3 つ目はリバースエンジニアリングが出来ればまぁ悪くない案だとは思いますが、どうやってリバースエンジニアリングすればいいのか検討すらつきません。雰囲気的に iOS 側がやってる感じがありますし、仮にリバースエンジニアリングができてもそこから再実装となると、上記の ImageMagick と同じ末路を辿りそうです。

1 つ目の案をメインに、残り 2 つはおまけ程度に見ていくことにします。

JavaScript で実装された既存の converter を使う

まずは一番現実的な案である、既存の converter を探してみます。考えることは皆同じで、探してみるといくつかヒットしました。

nokiatech/heif

スッと見つかったのは、Qiita の記事3で、Nokia の JavaScript 実装4が紹介されていました。

GitHub Pages 用に build されているブランチがあるのでそれを使って試してみると、問題なく表示されました。すごい。

JS ファイルを読み込んで、img の src に当ててやれば OK で、img が canvas に置き換えられて描画されます。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <script src="js/libde265-selector.js"></script>
    <script src="js/heif-api.js"></script>
    <script src="js/heif-extension.js"></script>
    <script src="js/hevc-decoder.js"></script>
    <script src="js/image-provider.js"></script>
    <title>Document</title>
  </head>
  <body>
    <img src="./content/images/autumn_1440x960.heic" alt="" srcset="" />
  </body>
</html>

じゃあもうこれでいいじゃん?と言いたいところですが、プロダクトに投入するにはライセンスがネックになりそうな感じでした。残念。

strukturag/libheif

次に見つけたのが libheif5 というやつで、これもまた JavaScript にコンパイル可能です。

libheif can also be compiled to JavaScript using emscripten. See the build-emscripten.sh for further information.

オンラインデモ6があるので試しに HEIF 形式の画層を突っ込んでみると、見事に表示されました。ライセンスは GNU Lesser General Public License なので、問題はなさそうです。

ただ npm でパッケージングされているわけでもなく「自前でビルドしてね」スタイルなので、その後の保守のことまで考えると、若干ハードルが高い選択肢です。

alexcorvi/heic2any

heic2any7という、上記の libheif に依存しているラッパーのようなライブラリも見つかりました。

こちらは npm でも配布されている、かつ、人気はなさそうですが更新もされているようなので、今まで見た中では一番有力な選択肢のように感じました。

と、いうことで、試しにこれを使って実装してみます。

distディレクトリに最終成果物としてindex.js上がっているので、これをダウンロードして読み込めば OK です。

inputで読み込まれた.heicを食わせて、previewpreview2の src に流し込んでみます。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <script src="./index.js"></script>
    <title>Document</title>
  </head>
  <body>
    <input type="file" id="user-file" accept="image/heic" />
    <hr />
    <img id="preview" width="300" />
    <img id="preview2" width="300" />
  </body>

  <script>
    document.getElementById("user-file").addEventListener("change", ev => {
      const blob = ev.target.files[0];
      heic2any({
        blob,
        toType: "image/jpeg",
        quality: 0.5
      }).then(resultBlob => {
        // createObjectURLで埋め込む
        const img = document.getElementById("preview");
        img.src = URL.createObjectURL(resultBlob);

        // readAsDataURLで埋め込む
        const img2 = document.getElementById("preview2");
        const reader = new FileReader();
        reader.onload = function(d) {
          img2.src = reader.result;
        };
        reader.readAsDataURL(resultBlob);
      });
    });
  </script>
</html>

heic2anyは戻り値としてPromiseで Blob オブジェクトを返してきます。

previewの方はデモ実装に合わせてcreateObjectURL()に食わせて src に流し込み、preview2の方は今自分がやっている案件に合わせてreadAsDataURL()に食わせて src に流し込んでみました。

どちらも、問題なく動作します。ただ、処理としてはやっぱり結構重ためです。まぁ基本は PC からしか使われない変換処理なので、そこまでシビアに考える必要はないと思います。

一方でこのライブラリを実案件で入れてよいか、と言われるとなかなか判断が難しいです(下手なライブラリに依存させちゃうと後々大変になるので)。

ImageMagick の convert を リバースエンジニアリングして独自実装する

では続いて、ImageMagick の convert 処理をみていきましょう。

まず、手元の Mac に ImageMagick を入れます。brew で install 出来るので楽です。執筆時点の最新版は7.0.9-2だったので、これをベースに進めていきます。

$ brew install imagemagick
$ magick --version
Version: ImageMagick 7.0.9-2 Q16 x86_64 2019-10-31 https://imagemagick.org
Copyright: © 1999-2019 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI Modules OpenMP(3.1)
Delegates (built-in): bzlib freetype heic jng jp2 jpeg lcms ltdl lzma openexr png tiff webp xml zlib

その前に、HEIF 形式のサポートがいつから始まったのかを ChangeLog から追っておきましょう。時期的に 6 系と 7 系が並列して更新されている時期だったようで、それぞれ以下のようになっていました。

6 系の ChangeLog はこちら。

2018-01-06  6.9.9-34 Cristy  <quetzlzacatenango@image...>
  * Support aspect ratio geometry, e.g. -crop 3:2.
  * Add support for reading the HEIC image format (reference
    https://github.com/ImageMagick/ImageMagick/issues/507).
  * Fix IM build when HEIC is enabled.
  * Fixed numerous memory leaks, credit to OSS Fuzz.

7 系の ChangeLog はこちら。

2018-01-06  7.0.7-22 Cristy  <quetzlzacatenango@image...>
  * Support aspect ratio geometry, e.g. -crop 3:2.
  * Add support for reading the HEIC image format (reference
    https://github.com/ImageMagick/ImageMagick/issues/507).
  * Fixed numerous memory leaks, credit to OSS Fuzz.

いずれも 2018 年 1 月 6 日にリリースされたバージョンから HEIF 形式のサポートが入っているようです。少なくとも、先程入れた最新版であれば対応していますね。

ImageMagick が入ったら、サブコマンドの convert8で変換してみます。引数色々指定できますが、ここでは単に png 画像に変換してみました。

$ magick convert hoge.HEIC hoge.png

identifyで画像の情報を見てやると、HEIC から、

$ identify hoge.HEIC
hoge.HEIC HEIC 3024x4032 3024x4032+0+0 8-bit YCbCr 0.000u 0:00.005

png に変換されていることがわかります。

$ identify hoge.png
hoge.png PNG 3024x4032 3024x4032+0+0 8-bit sRGB 13.63MiB 0.000u 0:00.001

HEIC の方で容量の表示が出ていませんが、Finder で覗くと 2M 超だったので、6 倍近くの容量まで膨れ上がってしまいました。こんなに膨れ上がるはずはないんですが…まぁ今回は本題ではないので無視します。

さてここからが本題です。

convert コマンドで変換できることがわかったので、convert の処理をリバースエンジニアリングできれば優勝です。

まずはビルド前のソースコードを取得しておきます。それを grep した感じ、今回使った convert 処理は以下の 2 ファイルを中心に関係してそうでした(実際はヘッダファイルとか色々あります)。

./coders/heic.c
./MagickWand/convert.c

まぁ予感はしてたんですが C ですね。C 実装ですね。C こわい。

で、これを軽く読んでみたんですが、正直めちゃくちゃ難しかったので、これはコスト合いそうにないし、かつ、JavaScript で再実装出来るかと聞かれれば出来そうにありません。

はい終了。

※再実装せずとも、ImageMagick をラップした Rust 実装9があるので、これを使えば wasm を作ってそれを利用する、という手段は取れそうです。ちょっと面白そうなので、気が向いたら作ってみます。

iPhone から上げる場合の挙動をリバースエンジニアリングして独自実装する

さて最後です。

冒頭でも述べましたが、iPhone から上げた場合、HEIF 形式そのままでは上がらずに誰かが jpeg に変換してくれる10んですよね。なんなんでしょう、この謎の力…。

一体誰が変換してくれているのか突き止めていきます。

まずは本当に上記の記事通り変換されているのかを確かめていきます。localhost で立てているので、実機からの確認には ngrok11を使いました。

ngrok は localhost で立てているサーバーを外部からもアクセスできるようにトンネルを掘ってくれる便利なやつです。わざわざ実機を Mac に繋いだり、あるいは Hosting する手間が省けるので楽ですね。

console.log()でもいいですが、実機確認が手間なので適当に p タグを作ってその innerHTML に突っ込んでログ代わりにしています。

const logger = document.getElementById("logger");
logger.innerHTML = ev.target.files.length + " : " + ev.target.files[0].type;

PC から上げると1 : image/heic、iPhone からだと1 : image/jpegとなっていることが確認できます。つまり、changeイベントが発火したタイミングではすでに変換された画像がブラウザに渡ってきているので、これはもう iOS 側でなんかやってる気配が濃厚です。

ただどこで何をやってるのかは、完全に謎でした。

iOS シミュレーターを使ってシステムログを覗きながら選択前後のログを追ってみたんですが、特に何も吐かれないんですよね。それにリアルタイムで変換しているにしてはあまりにもタイムラグがないので、ほんと謎…。

結局よくわからずじまいで終了です。

P.S.

JavaScript 実装があったので完全に詰みってことにはなりませんでしたが、これ結構ハードル高いですね。普通にバックエンドで変換してあげるってのがやっぱり常套手段なのかなという印象です。

ただそれだとなんか負けた気になるし、フロントエンドで完結させたいってニーズはあると思うので、時間のある時に Rust で wasm でも作ってみようと思います。

Avatar
nabeen
Coder

仕事したくないc(`Д´と⌒c)つ彡 ヤダヤダ

comments powered by Disqus
Next
Previous

Related