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定数を返す関数
cc
LayoutSize 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() の実装
cc
float 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
というフィールドを読んでいるcc
scoped_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
の定義cc
const 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とスクリーンショットにもまとめました