dpi awareなimgを表示する 〜完結編〜
ブラウザで高解像度スクリーンショットを適切な論理サイズで表示する
こんにちは、daiiz

です
NotaでScrapboxを作っています
Nota
Scrapbox
Wiki、複数人で同時編集できるノート
リンク資産
時間を超えたのページの有効活用
そのままプレゼンモードにもなる
Pixel Slate
Gyazo uploader for Pixel Slate
Chrome OS標準スクショ機能によって保存された画像ファイルを都度Gyazoにuploadする
はじめてRustを書いた
これはこれでいつか話したい
ブラウザで高解像度スクリーンショットを適切な論理サイズで表示する
解決したいこと
Retinaディスプレイで撮影した画像をimgタグで表示した時、縦横の大きさがそれぞれ2倍になる
<img src="screenshot.png">
前回までのあらすじ
ここにDPI情報 (単位メートルあたりのピクセル数) が書かれている
natural sizeを計算してCSS width, heightを与える
ブラウザで一連の処理を行い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 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でも使い回された
一応 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 = \frac{DPI}{72}という関係が成り立つ
window.devicePixelRatio
で得られる
具体例
諸々実験しているときは HTTP Client Hints に属していた仕様
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]
まとめ
高解像度画像を、普通のimg要素で、DPR awareで表示できる
平成のうちに解決まで漕ぎ着けてよかった
Q&A
Chrome Platform Statusによると、広くサポートの意向はありそう
Safari (WebKit)
停滞気味
Firefox
はい。この場合は /thumb/1000
を参照し、適当なサイズにresizeされたサムネイルが配信されるのですが、これにはContent-DPR headerは載せていないです。
/thumb/1000
をDPI awareで、naturalWidth=1000pxと解釈した時、(1000 * DPR)pxの画像を配信することになるが、直感的に理解しづらいため。
/dpr-aware-thumb/1000
など、別のendpointを用意するとよいかもしれない。
追記