dpi awareなimg CustomElementをつくる

dpi awareなimg CustomElementをつくる

〜 ブラウザで高解像度スクリーンショットを適切な論理サイズで表示する 〜

完結編あります 2019/4


本日お話しすること
解決したいこと
ブラウザで、imgタグで、高解像度スクリーンショットを表示すると大きくなる問題
Retinaディスプレイで撮影した画像の縦横の大きさがそれぞれ2倍になる

解決方法の提案
いくつか考えられる
理想的なimgタグをCustomElementとして実装してみる


問題
ブラウザで高解像度スクリーンショット画像を適切な論理サイズで表示したい

高解像度
ピクセル比 window.devicePixelRatio > 1

スクリーンショット画像
今回はpng画像について考える
他の画像フォーマットでも同様のアプローチを適用できるはず

論理サイズ
CSSで使う論理上のピクセル


例. ブラウザではこうなる(再)
macOSのRetinaディスプレイで撮影したスクリーンショット (Retina画像) をimgタグで表示
<img src="screenshot.png">

width, height がともに論理サイズの2倍の大きさになる
論理1ピクセルが、物理2ピクセルで表現されている


例. macOSのPreview.appでは?
全く同じ画像ファイルを開いている様子
正しい論理サイズで表示できている

解像度は保持されている
正しく表示するための情報は揃っている
画像自身が”144 dpi”に相当する値を持っている!


原因
HTMLのimg要素がDPIを考慮せずに縦横のサイズを決定しているため
画像の物理ピクセル数がそのままwidth, heightとして使われている
ビューワーの実装に依存する


これとは違う
表示デバイスのディスプレイ解像度に合わせて画像を出し分けたい
これは、picture要素やsrcsetを使って解決できる話題
今回は撮影時点でのデバイスの解像度が問題


解決アプローチ
3通り試した
最後の手が本日のメイン
ほかは最後におまけで紹介

サーバーから解像度情報を送ってやる
サーバーサイドで何らかの方法で画像の解像度を確定する
画像のHTTP Response Headerに載せて、クライアントでこれを読む

svg画像として配信する
svgのviewBoxにdpiを考慮した論理サイズを記述する
このsvg画像をimgタグで表示する

png画像のバイナリヘッダから解像度を読み取る
Preview.appと同様なことをブラウザでもやるアイデア
クライアントで完結できる


png画像のバイナリ構造
png signature
IHDRチャンク (= 画像ヘッダ)
画像のwidth, height (ピクセル数), color typeなどの必須情報
補助チャンク
いくつかある
pHYsチャンク
IDATチャンク
画像データの本体部分
IENDチャンク
画像ファイルの終端


pHYsチャンク
Physical pixel dimension
png画像の補助チャンクのひとつ

物理的なピクセル寸法に関する情報が格納されている
X軸、Y軸上の1mあたりのピクセル数
符号なし8ビット整数列 0 0 22 37 の読み方
32ビットの2進数として表現する
00000000 00000000 00010110 00100101
での値をとするとき、.
上の例
= 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

png画像のpHYsの値からdpi (dots per inchi) を算出する
基本的に、pHYsチャンクが現れるまでUint8Arrayを読み進めていくだけ
dpi = Math.floor(pixelsPerUnitXAxis / 39.3)
1mあたりのピクセル数よりも直感的に扱いやすい

dpiの書き込みもできる
readerの逆をやる
dpiからpHYsチャンクのpixelsPerUnitXAxis値を算出する
pixelsPerUnitXAxis早見表
devicePixelRatiodpipixels/meter
1722835
21445670
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


使い方
html
Copied!
<dpi-aware-image src="screenshot.png"></dpi-aware-image>

CSS Variablesでmax-widthなどを設定できる


dpi-aware-image
DPIを考慮して画像を適切なサイズで表示できるimg要素相当のCustomElement
画像をfetchする
png画像であればバイナリヘッダを読みpHYs chunkを探す
png-dpi-reader-writer npmを使う
dpiを算出して論理サイズを決定し、img.styleとしてセット
width = width / (dpi / 72)

解像度を加味した縦横を画像のナチュラルサイズとして扱える
svg
Copied!
<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を取得可能

デモ
⌘ + Shift + 4 でRetina画像を撮る
dpi-aware-image previewにD&Dしてimgタグでの表示と比較


おまけ
ほかの2つのアイデアを軽く紹介


svg画像をimg要素で表示する
アイデア
クライアントでの工夫が全く不要
svgを展開できる環境であれば汎用的に使える

サーバーサイド
svgのimage要素で外部画像を表示する条件を満たす、以下のようなsvg画像を生成して配信する
svg
Copied!
<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">


画像のHTTPヘッダーに解像度情報を載せる
サーバーサイド
解像度情報を取得して画像のResponse Headerにつける

クライアントサイド
先程の <dpi-aware-image> の実装とほぼ同じ
画像バイナリの代わりにHTTPヘッダーを読んで適切なサイズで表示する


まとめ
高解像度ディスプレイで撮影したスクリーンショットを正しい論理サイズで表示したい
ブラウザで画像のdpiを読み書きする方法
png-dpi-reader-writer npm
CustomElementとして今回のケースにおける理想的なimg要素を作った
dpi awareなimg要素は将来的にも登場しない?
devicePixelRatio > 1 な環境で撮られるスクショ画像は増え続けるはず
ほかのアイデアも含めて、自前でサイズ決定して表示するのはだいぶ複雑
このあたりの議論どうなっているのだろう

追記

Powered by Helpfeel