dpi awareなimgを表示する 〜完結編〜

dpi awareなimgを表示する 〜完結編〜

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



こんにちは、daiiz です
NotaでScrapboxを作っています


Nota
GyazoScrapbox を開発している


Scrapbox
Wiki、複数人で同時編集できるノート
リンク資産
時間を超えたのページの有効活用
そのままプレゼンモードにもなる


Pixel Slate
最高 daiizdaiizdaiizdaiizdaiiz
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ツールっぽいものも作った


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
Copied!
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
Copied!
if (image_resource->HasDevicePixelRatioHeaderValue() &&
image_resource->DevicePixelRatioHeaderValue() > 0)
pixel_density = 1 / image_resource->DevicePixelRatioHeaderValue();
あとはLayoutSizeを取得して、px_density補正しているだけ
cc
Copied!
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
Copied!
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
Copied!
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
Copied!
const AtomicString& kContentDPR = reinterpret_cast<AtomicString*>(&names_storage)[15];


導出過程5
HTTP HeaderがNameEntryとして列挙されているところに行き着いた
cc
Copied!
struct NameEntry {
const char* name;
unsigned hash;
unsigned char length;
};
cc
Copied!
...
{ "Cache-Control", 7757542, 13 },
{ "Content-DPR", 8569724, 11 },
{ "Content-Disposition", 362682, 19 },
...
つまり、HTTP Headerに Content-DPR を付けて画像を配信すればよい!!


DPR
という関係が成り立つ
window.devicePixelRatio で得られる
具体例
DPRの例
MacBook Pro 2017 Retina2.0
Pixel Slate2.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を用意するとよいかもしれない。

追記
Powered by Helpfeel