dpi awareなimgを表示する 〜完結編〜
ブラウザで高解像度スクリーンショットを適切な論理サイズで表示する
Kyoto.js #16での発表資料です
こんにちは、daiiz です
NotaでScrapboxを作っています
Nota
Scrapbox
Wiki、複数人で同時編集できるノート
リンク資産
時間を超えたのページの有効活用
そのままプレゼンモードにもなる
Pixel Slate
最高 




Pixel Penとキーボードが最高
Gyazo uploader for Pixel Slate
Chrome OS標準スクショ機能によって保存された画像ファイルを都度Gyazoにuploadする
はじめてRustを書いた
これはこれでいつか話したい
ブラウザで高解像度スクリーンショットを適切な論理サイズで表示する
解決したいこと
Retinaディスプレイで撮影した画像をimgタグで表示した時、縦横の大きさがそれぞれ2倍になる
<img src="screenshot.png"> 前回までのあらすじ
png画像バイナリに含まれるpHYsを読む
ここにDPI情報 (単位メートルあたりのピクセル数) が書かれている
natural sizeを計算してCSS width, heightを与える
svgのforeignObjectを使うのがポイント
ブラウザで一連の処理を行いDPI awareで表示するimg Custom Element
pHYsを読んでみて
うまくいけばnatural sizeをCSSで指定して表示
失敗したらimg要素にfallback
→ clientで画像のバイナリを読む力技
確かに動くが、これしか手はないものか?
dpi awareなimg要素は将来的にも登場しない?
devicePixelRatio > 1 な環境で撮られるスクショ画像は増え続けるはず ほかのアイデアも含めて、自前でサイズ決定して表示するのはだいぶ複雑
このあたりの議論どうなっているのだろう
課題
PNG以外のフォーマットどうするか問題
デバッグ大変
適切なCORS設定が必要
PNG以外のフォーマットどうするか
同様にバイナリ読めばいいのだけなのだが、大変
PNGで配信するのやめてWebPにしよう、となったら?
デバッグ大変
DPI awareで表示されないとき
pHYs読む過程でのbugなのか
そもそもDPI情報を持っていないのか
ぱっと見でわからない
便利な副生成物ができた
DenoでCLIツールっぽいものも作った
$ deno https://denopkg.com/daiiz/deno-png-dpi-reader/examples/reader.ts --allow-net https://i.gyazo.com/7127a0c2a987ea50dbba0ebd6455c206.png
CORS設定
gyazo.com から配信される画像を scrapbox.io で読む例
Gyazoからのresponse header
Access-Control-Allow-Origin: scrapbox.io Scrapboxで、画像のバイナリを読むために必要
先にgyazo.comで画像を見た際にキャッシュされて、scrapboxでも使い回された
よく読むと、Access-Control-Allow-Originに gyazo.com が入っているのが分かる
一応
fetch(url, { cache: no-store }) とすればこのエラーは起きない が、cacheの活用が一切できなくなってしまう
さらに考えた
サーバーサイドでpHYsを読む?
width=80pxのRetina画像をuploadすると、
<img width="40" ...> というimgタグが生成されて埋め込まれる 読んだ結果をCustom HTTP Headerに載せて返す?
X-Image-Width: 1000 X-Image-DPI: 144 しかし、結局CORS縛りからは逃れられない
Chromiumを読んでみる
もしかしたらpHYsを読んでいる箇所があるかも
もしくは悪手である旨コメントがあるかも
そもそもnaturalWidth, naturalHeightをどうやって算出しているのか
ビューワが参照している情報を調べる
ここになければ解決手法はないのでは
HTMLImageElementが実装すべき関数が定義されているヘッダファイル
関係ありそうな関数の目星を付けて読んでいく
今日はダイジェスト版
導出過程1
LayoutSize定数を返す関数
ccLayoutSize HTMLImageElement::DensityCorrectedIntrinsicDimensions() const { IntSize overridden_intrinsic_size = GetOverriddenIntrinsicSize(); if (!overridden_intrinsic_size.IsEmpty()) return LayoutSize(overridden_intrinsic_size); ImageResourceContent* image_resource = GetImageLoader().GetContent(); if (!image_resource || !image_resource->HasImage()) return LayoutSize();
float pixel_density = image_device_pixel_ratio_; このあたりが大事
cc if (image_resource->HasDevicePixelRatioHeaderValue() && image_resource->DevicePixelRatioHeaderValue() > 0) pixel_density = 1 / image_resource->DevicePixelRatioHeaderValue(); あとはLayoutSizeを取得して、px_density補正しているだけ
cc RespectImageOrientationEnum respect_image_orientation = LayoutObject::ShouldRespectImageOrientation(GetLayoutObject());
LayoutSize natural_size( image_resource->IntrinsicSize(respect_image_orientation)); natural_size.Scale(pixel_density); return natural_size;} 最終的にサイズを決定しているのはnatural_size
natural_size.Scale(pixel_density) することで論理サイズを決定してる導出過程2
DevicePixelRatioHeaderValueを読み解く
image_resource->DevicePixelRatioHeaderValue() の実装
ccfloat ImageResourceContent::DevicePixelRatioHeaderValue() const { return device_pixel_ratio_header_value_;} すでにどこかで
device_pixel_ratio_header_value_ は確定しているらしい導出過程3
device_pixel_ratio_header_value_ をセットするところ HeaderとはHTTP Response Headerのこと
http_names::kContentDPR というフィールドを読んでいるccscoped_refptr<Image> ImageResourceContent::CreateImage(bool is_multipart) { String content_dpr_value = info_->GetResponse().HttpHeaderField(http_names::kContentDPR); この後 content_dpr_valueを加工して
device_pixel_ratio_header_value_ を確定している導出過程4
http_names::kContentDPR の定義ccconst AtomicString& kContentDPR = reinterpret_cast<AtomicString*>(&names_storage)[15];導出過程5
HTTP HeaderがNameEntryとして列挙されているところに行き着いた
cc struct NameEntry { const char* name; unsigned hash; unsigned char length; };cc ... { "Cache-Control", 7757542, 13 }, { "Content-DPR", 8569724, 11 }, { "Content-Disposition", 362682, 19 }, ... つまり、HTTP Headerに
Content-DPR を付けて画像を配信すればよい!!DPR?
という関係が成り立つ
window.devicePixelRatio で得られる 具体例
| MacBook Pro 2017 Retina | 2.0 |
| Pixel Slate | 2.25 |
諸々実験しているときは HTTP Client Hints に属していた仕様
いまは whatwg/htmlに移管されている
clientはこの値を考慮して各辺のCSSピクセル数を決定する
書き方
Retina画像なら
Content-DPR: 2.0 と書く Akamaiによる解説
対応ブラウザ
現時点ではChrome, Operaのみ。Blink以外に実装がない。
Gyazoで対応!
https://i.gyazo.com/7127a0c2a987ea50dbba0ebd6455c206.png https://gyazo.com/7127a0c2a987ea50dbba0ebd6455c206/raw Scrapboxでも
[https://gyazo.com/282d9be5cbc1f2d9a3c1f1b0eb5413d2/raw] まとめ
Blinkのimg要素ではHTTP Header Content-DPRが考慮される
高解像度画像を、普通のimg要素で、DPR awareで表示できる
平成のうちに解決まで漕ぎ着けてよかった
Q&A
dpi awareなimgを表示する 〜完結編〜#5cc3d90eadf4e70000e28245に関して、SafariやFirefoxでのサポートの意向は?
Chrome Platform Statusによると、広くサポートの意向はありそう
Safari (WebKit)
停滞気味
Firefox
dpi awareなimgを表示する 〜完結編〜#5cc3d90eadf4e70000e2824fに関して、
/raw を指定せずに、GyazoのURLをScrapboxにペーストした場合はDPI awareにならない? はい。この場合は
/thumb/1000 を参照し、適当なサイズにresizeされたサムネイルが配信されるのですが、これにはContent-DPR headerは載せていないです。 /thumb/1000 をDPI awareで、naturalWidth=1000pxと解釈した時、(1000 * DPR)pxの画像を配信することになるが、直感的に理解しづらいため。 /dpr-aware-thumb/1000 など、別のendpointを用意するとよいかもしれない。追記
探究 SVGとスクリーンショットにもまとめました