ウェブでデバイスによって画像を出し分ける技術

# まえがき
最近デバイスによって画像を出し分ける技術があるのは知ってたけど、具体的にどうやってるのか知らなかったので調べたことをまとめる。ウェブのフロントエンドについては全然詳しくない人が書いてるので間違いを含んでいる可能性が大いにあります。
またこの記事は以下のページをとても参考にしています。

* https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images

この記事に出てくるコードは調べた限りこうなるらしいと言う感じで書いていて動作確認してません。

# 解決したいことと手段
解決したいことが2つある。

* デバイスによって同じ画像で解像度の違う画像を出し分けて表示とトラフィックを最適化したい
    * これをresolution switchingと言う
* デバイスによって違う画像を出し分けたい
    * デスクトップなら横長、スマホならそれをクロッピングして真ん中縦長の画像を表示するとか
    * これをart directionと言う

基本的に

* resolution switchingするのにimgタグのsrcset, sizes属性が用いられる
* art directionを解決するのにpictureタグ、sourceタグが用いられる

でこれらについて説明する前に、 "pixel" について知る必要がある。

# Pixelとは
まずCSSで指定するpxはディスプレイの表示の最小単位であるドットとは別。CSSのpxをCSS pixel(あるいはreference pixel、論理ピクセル)と言い、ディスプレイのドットをdevice pixel(あるいは物理ピクセル)と言う。
昔は基本的にCSS pixelとdevice pixelは一致していたが、iPhone 4が登場した時に状況が変わった[^1]。 iPhone 4のディスプレイはiPhone 3GSとほぼ同じ大きさでDPIが2倍(ドットの数が4倍)あった。これだともし何も対策無しにiPhone 4でpixelでレイアウトしているウェブページを表示するとページが半分の大きさで表示されてしまうことになる。

<img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94/cc8e70d3-dad8-4498-49da-b2259de89cac.png' width='400px' alt='iPhone'>

これでは今まで快適に見れてたウェブページが見にくくて仕方がない。デバイス側で対策してくれなければウェブページ側でiPhone 4用の対応を入れなければならない。
で、Appleが取った対策がCSSで1pxと指定した長さをデバイスの2ドットに対応させると言うもの。それで同じような表示になる。で、前述の通りこのCSSの1pxをCSS pixelと言い、デバイスのドットをdevice pixelと言う。で、このdevice pixel/CSS pixelの比をdevice pixel ratio(デバイスピクセル比)と言う。

画像の描画に関して、例えばdevice pixel ratioが2の端末で400 CSS pixelの幅には横幅が800の画像がぴったり入る。個人的にここら辺で混乱した。pixelって名前に引きずられずCSS pixelはただの長さの単位でしかないと思うとすっきりした。

# imgタグとsrcset, sizes
resolution switchingにはimgタグとsrcset属性を使う。横幅320 CSS pxの領域にdevice pixel ratioが1のデバイスならwidthが320の画像[^3]、1.5なら480, 2ならwidthが640の画像を表示する場合、こんな感じになる。

```html
<img srcset="elva-fairy-320w.jpg,
             elva-fairy-480w.jpg 1.5x,
             elva-fairy-640w.jpg 2x"
     width="320px"
     src="elva-fairy-640w.jpg" alt="Elva dressed as a fairy">
```

srcset属性が説明した通り。src属性がsrcset属性をサポートしてない端末でちゃんと表示されるように必要。 

で、device pixel ratioが違うデスクトップディスプレイとかだとこれで十分だけど、デスクトップディスプレイとスマホで幅も変えたい場合には不十分。
そう言う場合はこんな感じに書く。

```html
<img srcset="elva-fairy-320w.jpg 320w,
             elva-fairy-480w.jpg 480w,
             elva-fairy-800w.jpg 800w"
     sizes="(max-width: 320px) 280px,
            (max-width: 480px) 440px,
            800px"
     src="elva-fairy-800w.jpg" alt="Elva dressed as a fairy"
```

srcsetのurlの次が2xとかじゃなくて画像の横幅を書いてある。これはCSS pixelじゃなくて画像自体の大きさ。device pixel ratioの時はwidthを指定していたが、今回はsizesと言う属性に置き換えて、ここにメディアクエリでデバイスの条件とマッチした場合の表示幅を指定している。sizesの条件は前から評価され最初にマッチしたサイズが採用される。これだけ書けばあとはブラウザがちょうど良い画像を選択して表示してくれる。
例えば、iPhone 3GSはdevice pixel ratioが1、横幅のCSS pixelが320、横幅のdevice pixelも３２０。この場合[^4]sizesは `(max-width: 320px) 280px` にマッチするので表示幅は280 CSS pixel=280 device pixel。これに一番近い320wのelva-fairy-320w.jpgが表示される。
iPhone 4の場合device pixel ratioが2、横幅のCSS pixelが320、横幅のdevice pixelが640。この場合sizesは `(max-width: 320px) 280px` にマッチするので表示幅は280 CSS pixel=560 device pixel。これに一番近い480wのelva-fairy-480w.jpgが表示される。
iPhone Xの場合device pixel ratioが3、横幅のCSS pixelが375、横幅のdevice pixelが1125。この場合、 `(max-width: 480px) 440px` にマッチするので表示幅は440 CSS pixel=1320 device pixel。これに一番近い800wのelva-fairy-800w.jpgが表示される。[^5]
ここでは「一番近い」と書いたが、どの画像を採用するかの判断はブラウザの実装によるようで、HTML StandardにはDPI、ズームレベル、ネットワーク速度などを考慮して選べる、と言うところまでしか書いてなかった。

> The user agent can choose any of the given resources depending on the user's screen's pixel density, zoom level, and possibly other factors such as the user's network conditions.

https://html.spec.whatwg.org/multipage/images.html#introduction-3

これらの処理はsrcset, sizesがない時代にメディアクエリを使って実現しようとするとかなり複雑になっていた。詳細はここを読むと良い。

* https://terkel.github.io/srcset-sizes/

# pictureタグ
art direction problemにはpictureタグを用いて対処する。
デバイスの横幅が400 CSS px以下、800 CSS px以下、801 CSS px以上でファイルを出し分ける場合こんな感じになる。

```html
<picture>
    <source media="(max-width: 400px)"
            srcset="elva-type1.jpg">
    <source media="(max-width: 800px)"
            srcset="elva-type2.jpg">
    <img src="elva-type3.jpg"
         alt="Chris standing up holding his daughter Elva">
</picture>
```

pictureタグの中の上から評価されマッチしたsourceタグ、imgタグが採用される。imgタグはデバイスが801 CSS px以上、あるいはpicture, sourceタグをサポートしていない場合に採用される。
さらに、sourceタグでも前述のimgタグのsrcsetタグが使える。よって、art directionしつつresolution switchingするとこうなる。

```html
<picture>
    <source media="(max-width: 400px)"
            srcset="elva-type1@1x.jpg,
                    elva-type1@2x.jpg 2x,
                    elva-type1@3x.jpg 3x">
    <source media="(max-width: 800px)"
            srcset="elva-type2@1x.jpg,
                    elva-type2@2x.jpg 2x,
                    elva-type2@3x.jpg 3x">
    <img srcset="elva-type3@1x.jpg,
                 elva-type3@2x.jpg 2x,
                 elva-type3@3x.jpg 3x"
         src="elva-type3@1x.jpg"
         alt="Chris standing up holding his daughter Elva">
</picture>
```

採用されたsource, imgタグのsrcsetの中のデバイスに最適なものが採用される。デバイスがpicture, source, srcset全部対応していない場合imgタグsrc属性が採用される。

# モダンな画像形式
ここまでではjpgファイルを例として使ってきたが、WebP, JPEG 2000, JPEG XRなどの新しい画像形式の方が小さいバイトサイズでより良い画質の画像が提供できる。
が、ブラウザによって対応している画像形式が違う。2019/07現在だいたいJPEG XRがIE, JPEG 2000がSafari、それ以外がWebPをサポートしてる。
最新情報はここら辺確認すると良い。

* https://caniuse.com/#feat=webp
* https://caniuse.com/#feat=jpeg2000
* https://caniuse.com/#feat=jpegxr

なのでこれらの画像を使う場合、ブラウザによって画像を出し分ける必要がある。これはいくつか手段があるがpictureタグとsourceタグで実現できる。

```html
<picture>
    <source type="image/webp" srcset="elva.webp">
    <source type="image/jp2" srcset="elva.jp2">
    <source type="image/jxr" srcset="elva.jxr">
    <img src="elva.jpg" alt="Chris standing up holding his daughter Elva">
</picture>
```

こう書くと上から評価され、ブラウザが扱える最初のtype属性のsourceタグ, あるいはimgタグが採用される。ここのtype属性は基本的に [IANAのMedia Type](https://www.iana.org/assignments/media-types/media-types.xhtml)にリストされてるんだけど、webpは何故かなかった。

これをpictureタグの章でart direction + resolution switchingしてるコードと組み合わせるとこんな感じ。

```html
<picture>
    <source media="(max-width: 400px)"
            type="image/webp"
            srcset="elva-type1@1x.webp,
                    elva-type1@2x.webp 2x,
                    elva-type1@3x.webp 3x">
    <source media="(max-width: 400px)"
            type="image/jp2"
            srcset="elva-type1@1x.jp2,
                    elva-type1@2x.jp2 2x,
                    elva-type1@3x.jp2 3x">
    <source media="(max-width: 400px)"
            type="image/jxr"
            srcset="elva-type1@1x.jxr,
                    elva-type1@2x.jxr 2x,
                    elva-type1@3x.jxr 3x">
    <source media="(max-width: 400px)"
            type="image/jpg"
            srcset="elva-type1@1x.jpg,
                    elva-type1@2x.jpg 2x,
                    elva-type1@3x.jpg 3x">

    <source media="(max-width: 800px)"
            type="image/webp"
            srcset="elva-type2@1x.webp,
                    elva-type2@2x.webp 2x,
                    elva-type2@3x.webp 3x">
    <source media="(max-width: 400px)"
            type="image/jp2"
            srcset="elva-type2@1x.jp2,
                    elva-type2@2x.jp2 2x,
                    elva-type2@3x.jp2 3x">
    <source media="(max-width: 400px)"
            type="image/jxr"
            srcset="elva-type2@1x.jxr,
                    elva-type2@2x.jxr 2x,
                    elva-type2@3x.jxr 3x">
    <source media="(max-width: 400px)"
            type="image/jpg"
            srcset="elva-type2@1x.jpg,
                    elva-type2@2x.jpg 2x,
                    elva-type2@3x.jpg 3x">

    <source type="image/webp"
            srcset="elva-type3@1x.webp,
                    elva-type3@2x.webp 2x,
                    elva-type3@3x.webp 3x">
    <source type="image/jp2"
            srcset="elva-type3@1x.jp2,
                    elva-type3@2x.jp2 2x,
                    elva-type3@3x.jp2 3x">
    <source type="image/jxr"
            srcset="elva-type3@1x.jxr,
                    elva-type3@2x.jxr 2x,
                    elva-type3@3x.jxr 3x">
    <img type="image/jpg"
         srcset="elva-type3@1x.jpg,
                 elva-type3@2x.jpg 2x,
                 elva-type3@3x.jpg 3x"
         src="elva-type3@1x.jpg"
         alt="Chris standing up holding his daughter Elva">
</picture>
```

imgタグだけだとこの画像形式を出し分ける機能がない。ので、resolution switchingだけを解決するのにはimgタグを使っていたが、新しい画像形式を出し分ける場合はpictureタグを使う必要がある。と言っても簡単で、上記コードのelva-type3の部分だけ抜き出したらそれでresolution switching版になる。

```html
<picture>
    <source type="image/webp"
            srcset="elva-type3@1x.webp,
                    elva-type3@2x.webp 2x,
                    elva-type3@3x.webp 3x">
    <source type="image/jp2"
            srcset="elva-type3@1x.jp2,
                    elva-type3@2x.jp2 2x,
                    elva-type3@3x.jp2 3x">
    <source type="image/jxr"
            srcset="elva-type3@1x.jxr,
                    elva-type3@2x.jxr 2x,
                    elva-type3@3x.jxr 3x">
    <img type="image/jpg"
         srcset="elva-type3@1x.jpg,
                 elva-type3@2x.jpg 2x,
                 elva-type3@3x.jpg 3x"
         src="elva-type3@1x.jpg"
         alt="Chris standing up holding his daughter Elva">
</picture>
```

デバイスによって画像フォーマットを出し分ける他の技術は、

* HTTPリクエストのAcceptヘッダを見て対応している画像フォーマットを返す
* JavaScriptで対応

などがある。詳細はここら辺参照

* https://developers.google.com/speed/webp/faq#how_can_i_detect_browser_support_for_webp
* https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/automating-image-optimization/?hl=ja#how-do-i-serve-webp

[^1]: iPhone 4以前からreference pixelの概念は存在する。少なくとも、ズームイン・ズームアウトした場合にこの概念が必要になる。 https://www.w3.org/TR/REC-CSS1-961217#units https://webkit.org/blog/57/css-units/
[^3]: ややこしくて仕方ないので、画像の横縦の大きさは単位無しで表記する。
[^4]: ブラウザが対応していれば、の話だけど
[^5]: [ここ](https://mdn.github.io/learning-area/html/multimedia-and-embedding/responsive-images/responsive.html)をChromeのdevtoolsでiPhone 4, iPhone Xをエミュレートして確認した。 iPhone 3GSは計算のみ。

# 参考
* https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images
* https://www.w3.org/TR/css3-values/#reference-pixel
* https://www.mydevice.io/
* https://developers.google.com/web/fundamentals/design-and-ux/responsive/images?hl=ja
* https://html.spec.whatwg.org/multipage/
* https://developers.google.com/web/updates/2015/09/automating-resource-selection-with-client-hints
* https://www.digitalocean.com/community/tutorials/how-to-create-and-serve-webp-images-to-speed-up-your-website