Chrome と Firefox では CSS の色にカラーマネジメントが適用されず、sRGB の画像と色をあわせることが基本的に無理です。
で、無理なんですが無理ではなくて、実は頑張れば sRGB にあわせることができることがわかりました。
デモ
以下のような感じになっていて、書いてある通りです。Chrome と Firefox だと上3つと下3つの色が異なります。Safari だと全て sRGB 扱いされるので全部同じに見えるはずです。ただしカラーマネジメントが有効かどうかの判定を入れてないので、カラーマネジメントに対応してないシステムでも全部同じになっています。
一番下の色が JS で CSS のルールを書きかえて sRGB にしたものです。
やっていること
要は使う色をあらかじめ sRGB プロファイルの PNG に描いておき、この画像のレンダリング結果を canvas に drawImage して、getPixelData を行うとディスプレイプロファイルが適用されたRGB値を取得することができるので、これを動的に CSS の色として適用すれば sRGB にできます。
sRGB の画像を canvas に描くってところがポイントです。
自動的にCSSから色を抽出して sRGB を適用する
が、これは手動でやろうとすると面倒なので、CSSから色を抽出し、動的に sRGB PNG を作ってRGB値を変換してやるというのをやってみました。実用的かというと実用的ではないんですが、できそうですねってところです。
- CSS から色を抽出 (cssRules から)
- canvas に全部の色を描く
- canvas から toDataURL で PNG を得る
- この PNG に sRGB プロファイルを埋めこむ
- img 要素で読みこませる
- canvas に img 要素を drawImage する
- canvas の色を読む
- CSS に色を書き戻す
という手順でやります。
document.styleSheets の内容を書きかえているので、要素個別に書かれたスタイルについては対応できてません。
CSS の色を抽出
雑に以下のようにしました。当然完璧ではありませんが…
function scanStyleSheetColors (cb) {
for (var i = 0, stylesheet; (stylesheet = document.styleSheets[i]); i++) {
for (var j = 0, rule; (rule = stylesheet.cssRules[j]); j++) {
for (var k = 0, name; (name = rule.style[k]); k++) {
var val = rule.style.getPropertyValue(name);
if (/rgb\((\d+),\s*(\d+),\s*(\d+)\)/.test(val)) {
var r = RegExp.$1;
var g = RegExp.$2;
var b = RegExp.$2;
var ret = cb(r, g, b);
if (ret) {
var newVal = 'rgb(' + ret.join(',') + ')';
console.log(rule, name, val, '->', newVal);
rule.style.setProperty(name, newVal, rule.style.getPropertyPriority(name));
}
}
}
}
}
}
canvas に sRGB プロファイルを適用して PNG の data URL 化
// to PNG data URL with sRGB profile
function applyProfile (canvas) {
function pngChunk (type, data) {
var LEN_LENGTH = 4
var LEN_TYPE = 4;
var LEN_CRC = 4;
var buf = new ArrayBuffer(LEN_LENGTH + LEN_TYPE + data.length + LEN_CRC);
var view = new DataView(buf);
var pos = 0;
view.setUint32(0, data.length);
pos += LEN_LENGTH;
for (var i = 0, len = type.length; i < len; i++) {
view.setUint8(pos++, type.charCodeAt(i));
}
for (var i = 0, len = data.length; i < len; i++) {
view.setUint8(pos++, data.charCodeAt(i));
}
var crc = CRC32.bstr(type + data);
view.setUint32(pos, crc);
return String.fromCharCode.apply(null, new Uint8Array(buf));
}
var dataURL = canvas.toDataURL('image/png');
var matched = dataURL.match(/^data:image\/png;base64,(.+)/);
var base64 = matched[1];
var png = atob(base64);
// sRGB with Perceptual (0) rendering intent
png = png.replace(/(....IDAT)/, pngChunk("sRGB", "\x00") + "$1");
return 'data:image/png;base64,' + btoa(png);
}
こんなんでできます。やってることは sRGB チャンクを追加してるだけです。PNG は sRGB に関しては ICC プロファイルを埋めこむ必要なく、13バイト追加するだけですみます。
備考
Safari だと drawImage がうまくいかないです。ちゃんと追ってません。Safari はそもそも sRGB なのでやる必要ないです。