dpi awareなimg CustomElementをつくる
〜 ブラウザで高解像度スクリーンショットを適切な論理サイズで表示する 〜
完結編あります 2019/4
dpi awareなimgを表示する 〜完結編〜
Kyoto.js #15での発表資料
本日お話しすること
解決したいこと
ブラウザで、imgタグで、高解像度スクリーンショットを表示すると大きくなる問題
Retinaディスプレイで撮影した画像の縦横の大きさがそれぞれ2倍になる
https://gyazo.com/5ec2ecf355958e2049b52ac587297051
解決方法の提案
いくつか考えられる
理想的なimgタグをCustomElementとして実装してみる
問題
ブラウザで高解像度スクリーンショット画像を適切な論理サイズで表示したい
高解像度
ピクセル比 window.devicePixelRatio > 1
スクリーンショット画像
今回はpng画像について考える
他の画像フォーマットでも同様のアプローチを適用できるはず
論理サイズ
CSSで使う論理上のピクセル
Device Pixel Ratioで詳解
例. ブラウザではこうなる(再)
macOSのRetinaディスプレイで撮影したスクリーンショット (Retina画像) をimgタグで表示
<img src="screenshot.png">
https://gyazo.com/8e4556a979e216b5d9427ae0e6564bb8
width, height がともに論理サイズの2倍の大きさになる
論理1ピクセルが、物理2ピクセルで表現されている
https://gyazo.com/95837e153b2051ef12fda4ff146ddb3a
例. macOSのPreview.appでは?
全く同じ画像ファイルを開いている様子
正しい論理サイズで表示できている
https://gyazo.com/783a86a61b5f8b905fe98c4e1727290d
解像度は保持されている
正しく表示するための情報は揃っている
画像自身が”144 dpi”に相当する値を持っている!
https://gyazo.com/71686318cfe1167697577437fe5c8068
原因
HTMLのimg要素がDPIを考慮せずに縦横のサイズを決定しているため
画像の物理ピクセル数がそのままwidth, heightとして使われている
ビューワーの実装に依存する
これとは違う
表示デバイスのディスプレイ解像度に合わせて画像を出し分けたい
これは、picture要素やsrcsetを使って解決できる話題
今回は撮影時点でのデバイスの解像度が問題
解決アプローチ
3通り試した
最後の手が本日のメイン
ほかは最後におまけで紹介
サーバーから解像度情報を送ってやる
サーバーサイドで何らかの方法で画像の解像度を確定する
画像のHTTP Response Headerに載せて、クライアントでこれを読む
svg画像として配信する
svgのviewBoxにdpiを考慮した論理サイズを記述する
このsvg画像をimgタグで表示する
png画像のバイナリヘッダから解像度を読み取る
Preview.appと同様なことをブラウザでもやるアイデア
クライアントで完結できる
png画像のバイナリ構造
png signature
IHDRチャンク (= 画像ヘッダ)
https://www.setsuki.com/hsp/ext/chunk/IHDR.htm
画像のwidth, height (ピクセル数), color typeなどの必須情報
補助チャンク
いくつかある
pHYsチャンク
IDATチャンク
画像データの本体部分
IENDチャンク
画像ファイルの終端
pHYsチャンク
Physical pixel dimension
png画像の補助チャンクのひとつ
RFC 2083 - PNG (Portable Network Graphics) Specification
https://gyazo.com/02a27fccfc1b932244a5ca9556db82ed
物理的なピクセル寸法に関する情報が格納されている
X軸、Y軸上の1mあたりのピクセル数
符号なし8ビット整数列0 0 22 37 の読み方
32ビットの2進数として表現する
00000000 00000000 00010110 00100101
桁$ xでの値を$ bとするとき、$ \sum_{x=0}^{31}{b_x 2^x}.
上の例
=$ 2^0 + 2^2 + 2^5 + 2^9 + 2^{10} + 2^{12}
= 5669 px/㍍
≒ 144 px/㌅
必須項目ではないので、値の有無は画像を生成したアプリに依存する
png画像がこの値を持っていれば、ブラウザでも正しい論理サイズで表示できるはず
ブラウザでpng画像の解像度を読んで表示する流れ
pHYsチャンクから、撮影時の解像度情報を取得する
Retina画像の場合は、144 dpi と求まる (non Retina画像は72 dpi)
たしかに、縦横ともに論理サイズの2倍のピクセル数ある
縦横を算出して、CustomElementに内包するimg要素のwidth, heightとして指定
Retina画像の場合はそれぞれを半分にすればいい
クライアントで完結できる
ブラウザでpng画像の解像度を読み書き
png-dpi-reader-writer npm を作った daiiz.icon
https://www.npmjs.com/package/png-dpi-reader-writer https://gyazo.com/ee8098ea31f3ad9f54882d79f9459d4b
https://github.com/daiiz/png-dpi-reader-writer
png画像のpHYsの値からdpi (dots per inchi) を算出する
https://github.com/daiiz/png-dpi-reader-writer/blob/master/src/reader.js
基本的に、pHYsチャンクが現れるまでUint8Arrayを読み進めていくだけ
dpi = Math.floor(pixelsPerUnitXAxis / 39.3)
1mあたりのピクセル数よりも直感的に扱いやすい
dpiの書き込みもできる
https://github.com/daiiz/png-dpi-reader-writer/blob/master/src/writer.js
readerの逆をやる
dpiからpHYsチャンクのpixelsPerUnitXAxis値を算出する
table:pixelsPerUnitXAxis早見表
devicePixelRatio dpi pixels/meter
1 72 2835
2 144 5670
Uint8Arrayを読み進める
pHYsチャンクを発見
何もせずに直ちに処理を終える
IDATチャンクの開始位置に到達
この直前の位置にpHYsを入れた新たなUint8Arrayを生成する
解像度を考慮して表示サイズを決めるimg要素を作る
なぜ標準のimg要素はdpiを無視するのだろう
IHDRを読んだついでにpHYsも読めば実現できそう
そもそもIHDRチャンクすら読んでいない?
理想的なimg要素をCustomElementとして実装してみる
imgタグにdpiを考慮する属性を増やすとどうだろう
<img followdpi src='screenshot.png'>
もしくは常にdpiを考慮するimgタグもあり?
<dpi-aware-image src='screenshot.png'>
これを作った daiiz.icon
dpi-aware-image
https://github.com/daiiz/dpi-aware-image
https://daiiz.github.io/dpi-aware-image/demo/index.html
使い方
code:html
<dpi-aware-image src="screenshot.png"></dpi-aware-image>
CSS Variablesでmax-widthなどを設定できる
https://gyakky.herokuapp.com/svgyazo/3146f14a82f08a547470ef232ec42b58.svg
dpi-aware-image
DPIを考慮して画像を適切なサイズで表示できるimg要素相当のCustomElement
画像をfetchする
png画像であればバイナリヘッダを読みpHYs chunkを探す
png-dpi-reader-writer npmを使う
dpiを算出して論理サイズを決定し、img.styleとしてセット
width = width / (dpi / 72)
解像度を加味した縦横を画像のナチュラルサイズとして扱える
code:svg
<dpi-aware-image src="screenshot.png">
#shadow-root
<svg id="svg" width="608" height="509" viewBox="0 0 608 509">
<foreignObject x="0" y="0" width="100%" height="100%">
<img width="100%" height="100%" src="screenshot.png">
</foreignObject>
</svg>
</dpi-aware-image>
#svgに対して、max-width, width, max-height などを複数個同時に与えられる
svgのforeignObjectを使う
内部にimgタグを書ける
拡縮時に、viewBoxに従った縦横比を保てる
右クリックメニューのターゲットはimgタグ
png画像のURLを取得可能
https://gyazo.com/232bb714494a49dced36da6fbf032ac1
デモ
⌘ + Shift + 4 でRetina画像を撮る
dpi-aware-image previewにD&Dしてimgタグでの表示と比較
https://gyazo.com/602c9805dacc8725271ef5bb543d8b84
おまけ
ほかの2つのアイデアを軽く紹介
svg画像をimg要素で表示する
アイデア
Retinaディスプレイで撮ったscreenshotをsvgに包んで配信して、imgタグで原寸大で表示するで詳解
クライアントでの工夫が全く不要
svgを展開できる環境であれば汎用的に使える
サーバーサイド
svgのimage要素で外部画像を表示する条件を満たす、以下のようなsvg画像を生成して配信する
code:svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 571 506" width="571" height="506">
<image width="571" height="506" x="0" y="0"
xlink:href="data:png;base64,iVBORw0KGgoAAAANSUhE..."></image>
</svg>
width, height, viewBox にdpiを考慮した値を設定する
Retina画像なら半分の値にする
画像データをdata URIとしてsvg.image要素に与える
データサイズが大きくなるが、仕方ない
svgをimgタグで表示する場合、内部でリソースを外部参照できない為
クライアントサイド
imgタグで普通に表示するだけでOK
<img src="screenshot.svg">
https://gyazo.com/e4fe075c9e32b7ccbbaac4f28fd8ab05
画像のHTTPヘッダーに解像度情報を載せる
サーバーサイド
解像度情報を取得して画像のResponse Headerにつける
https://gyakky.herokuapp.com/svgyazo/35e38be09aeea60dbce2966089e8f662.svg
クライアントサイド
先程の<dpi-aware-image>の実装とほぼ同じ
画像バイナリの代わりにHTTPヘッダーを読んで適切なサイズで表示する
まとめ
高解像度ディスプレイで撮影したスクリーンショットを正しい論理サイズで表示したい
ブラウザで画像のdpiを読み書きする方法
png-dpi-reader-writer npm
CustomElementとして今回のケースにおける理想的なimg要素を作った
dpi awareなimg要素は将来的にも登場しない?
devicePixelRatio > 1な環境で撮られるスクショ画像は増え続けるはず
ほかのアイデアも含めて、自前でサイズ決定して表示するのはだいぶ複雑
このあたりの議論どうなっているのだろう
追記
探究 SVGとスクリーンショットにもまとめました
#作った