dpi awareなimgを表示する 〜完結編〜
ブラウザで高解像度スクリーンショットを適切な論理サイズで表示する
Kyoto.js #16での発表資料です
https://gyazo.com/e673044c42b7fd8fd85a4f87687446c8
こんにちは、daiiz /daiiz/daiiz.icon です
NotaでScrapboxを作っています
/icons/twitter.icon @daizplus
/icons/scrapbox.icon https://scrapbox.io/daiiz
Nota
Gyazo、Scrapbox を開発している
https://gyazo.com/334156e5ddaec301eec92d6a8f2bf980
https://notainc.com/rebrand/
Scrapbox
https://gyazo.com/ed58dad9ca677c8ab54867fe08cf6817
Wiki、複数人で同時編集できるノート
リンク資産
時間を超えたのページの有効活用
そのままプレゼンモードにもなる
Pixel Slate
https://gyazo.com/1ba014cd7a772fa73c5ff9dd136c9fab https://store.google.com/gb/product/pixel_slate
最高 daiiz.icon*5
Pixel Penとキーボードが最高
Gyazo Crostini
Gyazo uploader for Pixel Slate
https://youtu.be/KtDG-SfLn9I
Chrome OS標準スクショ機能によって保存された画像ファイルを都度Gyazoにuploadする
はじめてRustを書いた
これはこれでいつか話したい
ブラウザで高解像度スクリーンショットを適切な論理サイズで表示する
解決したいこと
Retinaディスプレイで撮影した画像をimgタグで表示した時、縦横の大きさがそれぞれ2倍になる
<img src="screenshot.png">
https://gyazo.com/8e4556a979e216b5d9427ae0e6564bb8
前回までのあらすじ
dpi awareなimg CustomElementをつくる
png画像バイナリに含まれるpHYsを読む
ここにDPI情報 (単位メートルあたりのピクセル数) が書かれている
natural sizeを計算してCSS width, heightを与える
https://gyazo.com/490521889cc22cc419802f08a983bcaf/raw
svgのforeignObjectを使うのがポイント
ブラウザで一連の処理を行いDPI awareで表示するimg Custom Element
https://github.com/daiiz/dpi-aware-image
pHYsを読んでみて
うまくいけばnatural sizeをCSSで指定して表示
失敗したらimg要素にfallback
→ clientで画像のバイナリを読む力技
確かに動くが、これしか手はないものか?
dpi awareなimg要素は将来的にも登場しない?
devicePixelRatio > 1な環境で撮られるスクショ画像は増え続けるはず
ほかのアイデアも含めて、自前でサイズ決定して表示するのはだいぶ複雑
このあたりの議論どうなっているのだろう
課題
PNG以外のフォーマットどうするか問題
デバッグ大変
適切なCORS設定が必要
PNG以外のフォーマットどうするか
同様にバイナリ読めばいいのだけなのだが、大変
PNGで配信するのやめてWebPにしよう、となったら?
デバッグ大変
DPI awareで表示されないとき
pHYs読む過程でのbugなのか
そもそもDPI情報を持っていないのか
ぱっと見でわからない
便利な副生成物ができた
dpi-aware-image preview
DenoでCLIツールっぽいものも作った
https://github.com/daiiz/deno-png-dpi-reader-writer
$ 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で、画像のバイナリを読むために必要
https://gyazo.com/ebe013b773b69a37b775604b134d0c45
先にgyazo.comで画像を見た際にキャッシュされて、scrapboxでも使い回された
よく読むと、Access-Control-Allow-Originに gyazo.com が入っているのが分かる
一応 fetch(url, { cache: no-store })とすればこのエラーは起きない
が、cacheの活用が一切できなくなってしまう
さらに考えた
サーバーサイドでpHYsを読む?
ちなみに、GitHubのコメント欄にスクリーンショットをuploadするとdpi awareなサイズで表示される
width=80pxのRetina画像をuploadすると、<img width="40" ...>というimgタグが生成されて埋め込まれる
https://gyazo.com/71c2a7b98f39f625f0531f9cee1ef35d
読んだ結果をCustom HTTP Headerに載せて返す?
X-Image-Width: 1000
X-Image-DPI: 144
しかし、結局CORS縛りからは逃れられない
Chromiumを読んでみる
https://gyazo.com/ab3d56876cabf6aa617437a1eb954883
https://cs.chromium.org/chromium/
もしかしたらpHYsを読んでいる箇所があるかも
もしくは悪手である旨コメントがあるかも
そもそもnaturalWidth, naturalHeightをどうやって算出しているのか
ビューワが参照している情報を調べる
ここになければ解決手法はないのでは
Blinkでの画像のNaturalSizeの導出過程を追う
HTMLImageElementが実装すべき関数が定義されているヘッダファイル
https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/html/html_image_element.h
関係ありそうな関数の目星を付けて読んでいく
今日はダイジェスト版
導出過程1
LayoutSize定数を返す関数
code: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_;
このあたりが大事
code:cc
if (image_resource->HasDevicePixelRatioHeaderValue() &&
image_resource->DevicePixelRatioHeaderValue() > 0)
pixel_density = 1 / image_resource->DevicePixelRatioHeaderValue();
あとはLayoutSizeを取得して、px_density補正しているだけ
code: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() の実装
code: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というフィールドを読んでいる
code: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の定義
code:cc
const AtomicString& kContentDPR = reinterpret_cast<AtomicString*>(&names_storage)15;
導出過程5
HTTP HeaderがNameEntryとして列挙されているところに行き着いた
code:cc
struct NameEntry {
const char* name;
unsigned hash;
unsigned char length;
};
code:cc
...
{ "Cache-Control", 7757542, 13 },
{ "Content-DPR", 8569724, 11 },
{ "Content-Disposition", 362682, 19 },
...
つまり、HTTP HeaderにContent-DPRを付けて画像を配信すればよい!!
DPR?
$ DPR = \frac{DPI}{72}という関係が成り立つ
window.devicePixelRatioで得られる
具体例
table:DPRの例
MacBook Pro 2017 Retina 2.0
Pixel Slate 2.25
Content-DPR
諸々実験しているときは HTTP Client Hints に属していた仕様
いまは whatwg/htmlに移管されている
Remove image-related Client Hints by yoavweiss · Pull Request #775 · httpwg/http-extensions
Add image-related Client Hints headers removed from the IETF draft · yoavweiss/html@7bd134e
clientはこの値を考慮して各辺のCSSピクセル数を決定する
書き方
https://gyazo.com/f9481bc6bde770c205d7deea67275367 https://httpwg.org/http-extensions/client-hints.html#content-dpr
Retina画像ならContent-DPR: 2.0と書く
Akamaiによる解説
https://www.akamai.com/jp/ja/multimedia/documents/brochure/akamai-oreilly-high-performance-images-excerpts-rwd-brochure-japanese.pdf
対応ブラウザ
現時点ではChrome, Operaのみ。Blink以外に実装がない。
https://gyazo.com/185a6e6c63df9a33231bfa218a85fbb9 https://caniuse.com/#search=Content-DPR
Gyazoで対応!
Gyazoが配信する画像にContent-DPRヘッダーを付与するようになりました by /pastak-pub/pastak.icon
https://i.gyazo.com/7127a0c2a987ea50dbba0ebd6455c206.png
https://gyazo.com/7127a0c2a987ea50dbba0ebd6455c206/raw
Scrapboxでも
[https://gyazo.com/282d9be5cbc1f2d9a3c1f1b0eb5413d2/raw]
https://gyazo.com/282d9be5cbc1f2d9a3c1f1b0eb5413d2/raw
まとめ
Blinkのimg要素ではHTTP Header Content-DPRが考慮される
高解像度画像を、普通のimg要素で、DPR awareで表示できる
平成のうちに解決まで漕ぎ着けてよかった
/icons/hr.icon
Q&A
dpi awareなimgを表示する 〜完結編〜#5cc3d90eadf4e70000e28245に関して、SafariやFirefoxでのサポートの意向は?
Chrome Platform Statusによると、広くサポートの意向はありそう
https://svgscreenshot.appspot.com/o/ac293a455e495166ced6b9bf3ae8f3ed.svg https://www.chromestatus.com/feature/5504430086553600
Safari (WebKit)
https://bugs.webkit.org/show_bug.cgi?id=145380
停滞気味
Firefox
https://bugzilla.mozilla.org/show_bug.cgi?id=935216
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を用意するとよいかもしれない。
/icons/hr.icon
追記
探究 SVGとスクリーンショットにもまとめました