【image-orientation】Exif情報で回転している縦画像をCanvasで正しい向きで表示できなくなった件へ対処する

この記事は最終更新日から1年以上が経過しています。情報が古くなっている可能性があります。

最近、1 年近く前に作ったジェネレーターサイトをメンテナンスする機会があったのですが、その途中で「特定の画像(写真)だけ、向きが 90 度回転して表示される」という不具合を見つけました。

その理由を調べていくうちに土壺にはまってしまったので、いろいろ調べたことを書き残しておきます。

Sponsored Link

Exif 回転画像

そのジェネレーターはユーザーが <input type=”file”> で選択した画像ファイルを受け取り、Canvas でその画像を予め用意したフレームの中に描画する、という比較的単純なもので、toBlob() をつかって Canvas で描画した画像を保存できるようにしていました。

ところで、一部のスマホや一眼レフで縦で撮影した JPEG 画像には、ピクセル情報を横長で記録し、JPEG の Exif 情報に含まれている回転 (Orientation) 情報を使って画像ビューア側で正しい向きで表示させるようになっているものが存在します。(以下便宜的に「Exif 回転画像」と呼ぶことにします・参考記事

スマホや一眼のセンサー自体は横長で配置されているので、縦向きにして撮った場合、多くのカメラは処理速度的な観点からか横長の画像として記録した後、Exif に「この画像は右に90度回転すると正しい向きで表示できる」といった情報を書き込むようになっているようです。
ただし、実際に Exif 回転情報を読み取って正しい向きで表示するかは画像ビューア側次第で、正しい向きで表示されるものもあれば Exif を無視するものもあります。

開発当時のブラウザは Canvas に描画する画像に限らず Exif 情報を無視して画像を表示していたため、Exif 回転画像をブラウザで表示させると横長で表示されてしまう場合がありました。
このため、Canvas で Exif 回転画像を描画する際に横長(左90度回転)で表示されないよう、こちらの記事を参考にしてジェネレーターに JavaScript で Exif の回転 (Orientation) 情報を読み取り、もし Orientation の値が未定義か 1 (0°) であれば Canvas の rotate() を使って一旦画像を回転させてから描画するような処理を入れていました。

ところが、メンテナンスで試しに手元にあった適当な画像をいくつか選択して試してみたら、スマホで撮った縦長の画像だけ横長で表示されてしまいました。何故横長で表示されるのか見当もつきませんでしたが、いろいろ調べると、
「画像の回転処理は正しく動作しているが、何故かブラウザ側が画像内の Exif 情報を読み取り既に正しい向きで描画しているため、画像回転処理を挟むと右に 90 度余計に回転してしまい、結果的に正しい向きより右 90 度回転した横長の状態で表示されてしまう」
ということが判明。

画像の回転処理を行わない場合は正しい向きより左90°回転した状態で表示されるので、画像の回転処理を行わない場合と比べて右に 180 度回転してしまっていることになります。
「ブラウザ側が画像内の Exif 情報を読み取り正しい向きで描画している」ことと、以前の記憶が正しければ画像の回転処理は正常に動作していたことから推測される原因は…「ブラウザの仕様変更」。

image-orientation プロパティ

この問題は、ここ数ヶ月( 2020 年 6 月当時)で主要ブラウザに初期値を from-image とした image-orientation という CSS プロパティが追加され、デフォルトで Exif 回転画像が正しい向きで表示されるようになった事、それに伴いブラウザ全体で Exif 回転画像の扱いが変更されたことに起因します。

Moziila いわく、元々 image-orientation プロパティは画像の向きを image-orientation: 90deg; image-orientation: flip; のように変えたり反転させたり、image-orientation: from-image; と指定して Exif 情報に合わせて正しい向きで画像を表示できる CSS プロパティでした。
ただし Firefox 以外では採用されなかったため普及せず、このうち90deg といった角度指定と flip のような反転指定は transform プロパティで代替できることから Firefox 63 で廃止され、from-image のみが残ったようです。

その後、2020年3月公開の Chrome 81 より 初期値を from-image とした image-orientation プロパティが実装され、Chrome では明示的に image-orientation: none; を指定しない限り、Exif 回転画像を常に正しい向きで表示するようになりました。
この挙動に追従するように、2020年6月公開の Firefox 77(Firefox 75 からの予定がコロナの影響で遅れたらしい)から image-orientation プロパティの初期値が none から from-image に変更され、Firefox でも Chrome 同様に Exif 回転画像を常に正しい向きで表示する(Exif の回転情報を尊重する)ようになっています(参考記事)。
さらに、Safari も Safari 14 から image-orientation プロパティをサポートするようです。

さて、この変更に併せて、background-image や border-image などの CSS で指定する画像、CanvasRenderingContext2D.drawImage() で描画する画像も Exif の回転情報を尊重し、常に正しい向きで描画されるようになりました。
この変更
自体は喜ばしいことですが、これらの挙動は image-orientation プロパティで制御できる img 要素とは異なり、無効化する手段がありません。
https://github.com/whatwg/html/issues/4495 にて html に autorotate 属性をつけることが提案されていますが、「キャンバスや他の場所で画像の正しい方向を無効にするための合理的なユースケースは考えられません」と一蹴されてしまっています(それはそう)。

先程の右 90 度余計に回転してしまっていたのも、これが原因とみて間違いないでしょう。
ブラウザ側で正しい向きで表示する機能を無効化できれば JavaScript 上での画像回転処理に一本化できるのですがそうもいかず、なかなかに面倒……。。

Chrome では Canvas 要素に image-orientation: none; を指定すると CanvasRenderingContext2D.drawImage() で描画する画像も Exif 情報を尊重しなくなりますが、残念ながら Firefox では機能しません(リリース予定の Safari も効かなさそう)。
iOS の Safari は image-orientation: from-image; 自体は実装していないものの、以前から Exif 回転画像を Exif 情報を尊重して正しい向きで表示する仕様だったらしい…?

全てのブラウザ側で最初から正しい向きで表示してくれればいいのですが、現状 IE や旧 Edge などの古いブラウザ、Chromium ベースだがまだ最新の Chromium に更新されていないブラウザがあったりと、まだフォールバックが必要です。そのため、ブラウザ側で正しい向きで描画できればそのまま描画し、使えなければ JavaScript 側で回転させてから描画する、といった処理を行う必要があります。

判定方法

Safari はリリース前なのでわかりませんが、Chrome も Firefox もimage-orientation プロパティが実装され既定値が from-image に変更される」タイミングでブラウザ全体での Exif 回転画像の扱いが変更になっているので、そのあたりで判定すればいいかなーと調べていたところ、同様の問題が Stack Overflow で取り上げられているのを発見(こういうのは英語で調べると大体出てくる傾向がある)。

様々な回答がありますが、最も確実なのは JasonWoof 氏の回答 でしょう。

// returns a promise that resolves to true  if the browser automatically
// rotates images based on exif data and false otherwise
function browserAutoRotates () {
    return new Promise((resolve, reject) => {
        // load an image with exif rotation and see if the browser rotates it
        const image = new Image();
        image.onload = () => {
            resolve(image.naturalWidth === 1);
        };
        image.onerror = reject;
        // this jpeg is 2x1 with orientation=6 so it should rotate to 1x2
        image.src = 'data:image/jpeg;base64,/9j/4QBiRXhpZgAATU0AKgAAAAgABQESAAMAAAABAAYAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAAITAAMAAAABAAEAAAAAAAAAAABIAAAAAQAAAEgAAAAB/9sAQwAEAwMEAwMEBAMEBQQEBQYKBwYGBgYNCQoICg8NEBAPDQ8OERMYFBESFxIODxUcFRcZGRsbGxAUHR8dGh8YGhsa/9sAQwEEBQUGBQYMBwcMGhEPERoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoa/8IAEQgAAQACAwERAAIRAQMRAf/EABQAAQAAAAAAAAAAAAAAAAAAAAf/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAAF/P//EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAQUCf//EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQMBAT8Bf//EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQIBAT8Bf//EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEABj8Cf//EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAT8hf//aAAwDAQACAAMAAAAQH//EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQMBAT8Qf//EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQIBAT8Qf//EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAT8Qf//Z';
    });
}

2×1 の画像を読み込んでみて縦横どちらの長さが 1px かを判定しているようです(すごい)。
ただし、テスト用の画像を読み込む関係上 Promise で返ってきてしまうため、少し使いづらい…。。

function browserImageRotationSupport(){
    let imgTag = document.createElement('img');
    return imgTag.style.imageOrientation !== undefined;
}

Chase R Lewis 氏の回答document.createElement() で生成した画像の CSS プロパティに image-orientation プロパティかあるかないかで確認していますが、これだと image-orientation が以前から実装されていた Firefox では機能しない、と David Barnes 氏がコメントしています。
Firefox 68(ESR 版)をインストールして試してみましたが、確かに image-orientation プロパティ自体は存在するため機能しませんでした。

ここまでくると UserAgent で判定するのが良さそうにすら見えますが、Chrome が UserAgent のバージョン情報を凍結させる方向なほか、有象無象の Chromium・Firefox 派生ブラウザの UserAgent を全て控えるのは非現実的なので、できれば避けたいところです。

let browserAutoRotates = getComputedStyle(document.body)['imageOrientation'] == 'from-image';

David Barnes 氏は先程のコメントに「次のコードは、ブラウザーが画像を自動的に回転させるかどうかを検出できることがわかりました」と別の実装例を提案しています。
body 要素の image-orientation プロパティの初期値を getComputedStyle() で取得した(CSS にそのプロパティが指定されていない場合はブラウザの初期値が返ってくる)値が from-image になっているかで判定する方法で、これなら手軽に使えそうです。

body 要素の image-orientation プロパティを CSS で明示的に none に指定している場合は使えませんが、かなりレアケースでさほど問題にはならないと思います。
// ブラウザが画像の自動回転機能をサポートしているか
// サポートしていれば true・サポートしていなければ false を返す
function isSupportedImageAutoRotation() {
    // image-orientation: from-image; がサポートされた環境では image-orientation の既定値が from-image になっていることを利用
    return getComputedStyle(document.body)['imageOrientation'] === 'from-image';
}

というわけで、(関数にするほどでもないと思うけど)ブラウザの自動回転機能がサポートされているかどうかを返す関数を作りました。これでブラウザが自動回転している画像をそのまま描画するか、JavaScript で画像を回転させてから描画するかを判定できます。

対策例

See the Pen Canvas に正しい向きで画像を描画する by tsukumi (@tsukumijima) on CodePen.

ここまで調べるのにかなり時間がかかってしまいましたが、先ほどの isSupportedImageAutoRotation() でブラウザが画像の自動回転をサポートしているかを判定し、ブラウザが画像の自動回転をサポートしていないときだけ JavaScript で画像を自動回転してから描画することで、ようやくどんなブラウザでも画像を正しい向きで Canvas に描画できるようになりました!長かった…
CodePen に載せておくので、スマホのカメラで縦で撮った写真を選択してみて、正しい向きで描画されるかどうか試してみてください。

jQuery 使ってたり前時代的な書き方なのはご愛嬌…

コードの解説ですが、画像を FileReader で読み込んだあと、clearOrientation() に読み込んだ画像の DataURL を投げ、clearOrientation() 内で Exif の Orientation が 1 以外 & ブラウザが画像の自動回転に対応していない場合だけ画像を正しい向きに回転し、返ってきた DataURL を Image でロードして Canvas に描画するような構成になっています。

回転している画像を正しい向きに直す処理は こちらの記事 を参考にさせていただきました。
DataURL で実装しているためかブラウザが画像の自動回転に対応していない場合に描画に時間がかかってしまうので、本来は blob で実装したほうがいいのかもしれません。

まとめ

今日は、スマホ📱で縦向きに撮った写真🖼を Canvas で描画するとどうなるか、知っておこう😊🧐

  • 多くのスマホや一眼レフで縦で撮影した JPEG 画像には、ピクセル情報を横長で記録し、JPEG の Exif 情報に含まれている回転 (Orientation) 情報を使って画像ビューア側で正しい向きで表示させる特殊な画像が存在する
  • 今までのブラウザは Canvas に描画する画像に限らず全ての画像を Exif 情報を無視して表示していた
  • 最近になって主要ブラウザに初期値を from-image とした image-orientation という CSS プロパティが追加され、デフォルトで Exif 回転画像が正しい向きで表示されるようになった
  • それに伴いブラウザ全体で Exif 回転画像の扱いが変更され、CanvasRenderingContext2D.drawImage() でも Exif 情報を尊重し、画像を常に正しい向きで描画するようになった
  • この影響で今まで JavaScript で Exif 情報を読み取り画像を回転していた場合は余計に右 90 度回転してしまうようになってしまった
  • image-orientation: from-image; がデフォルトになっているかは body 要素の image-orientation プロパティの初期値が from-image かどうかで判定できる
  • ブラウザが Exif 回転画像の自動回転をサポートしているかを判定し、サポートされていない時だけ JavaScript で画像を回転することで、どんなブラウザでも画像を正しい向きで Canvas に描画することができる

うーん…😑💭
最初から Exif 回転画像を全部ブラウザ🌐が自動回転していればこんな事にはならなかったよね!🤔🤔

じゃ!✋🙂

コメント