| リポジトリ | コミットハッシュ | 日時 |
|---|---|---|
| Chromium | a2652c6fc5817d5cc643c3935e1363ddc48abb6e |
2025-12-24 04:32:10 -0800 |
| Skia (third_party/skia) | 3544942c9d424d37d82d756d6dbbbc04327e8dbb |
2025-12-24 02:08:10 -0800 |
test.js
// @ts-check
import { chromium } from "playwright";
import { gs } from "@u1f992/gs-wasm";
const toColorValue = (/** @type {number} */ n) => {
const s = n.toString().padStart(6, "0");
return s.slice(0, -5) + "." + s.slice(-5);
};
const html = (
/** @type {string} */ r,
/** @type {string} */ g,
/** @type {string} */ b
) =>
`<html>
<head>
<style>
body {
background-color: color(srgb ${r} ${g} ${b});
}
</style>
</head>
<body>
</body>
</html>`;
const browser = await chromium.launch();
try {
const page = await browser.newPage();
for (let r = 0; r < 21; r++) {
const red = toColorValue(r);
await page.setContent(html(red, "0", "0"));
const pdf = await page.pdf({ printBackground: true });
/** @type {number[]} */
const stdout = [];
await gs({
args: ["-dBATCH", "-dNOPAUSE", "-dPDFDEBUG", "-sDEVICE=nullpage", "-"],
onStdin: (() => {
let i = 0;
return () => {
return i < pdf.length ? pdf[i++] : null;
};
})(),
onStdout(charCode) {
if (charCode !== null) stdout.push(charCode);
},
});
const decoded = new TextDecoder().decode(new Uint8Array(stdout));
const lines = decoded
.split("\n")
.map((line) => line.trim())
.filter((line) => line.endsWith("rg")); // fill color
console.log({ input: red, output: lines });
}
} finally {
await browser.close();
}color(srgb r g b) の値は、PDF出力時に 小数点以下第4位まで 、四捨五入 で丸められる。
| 入力 | 出力 |
|---|---|
| 0.00000 ~ 0.00004 | 0 0 0 rg |
| 0.00005 ~ 0.00014 | 0.000100 0 0 rg |
| 0.00015 ~ 0.00020 | 0.000200 0 0 rg |
ファイル: third_party/skia/src/pdf/SkPDFGraphicStackState.cpp:17-25
static void emit_pdf_color(SkColor4f color, SkWStream* result) {
SkASSERT(color.fA == 1); // We handle alpha elsewhere.
SkPDFUtils::AppendColorComponentF(color.fR, result);
result->writeText(" ");
SkPDFUtils::AppendColorComponentF(color.fG, result);
result->writeText(" ");
SkPDFUtils::AppendColorComponentF(color.fB, result);
result->writeText(" ");
}この関数は SkColor4f(float型のRGBA)を受け取り、各成分を AppendColorComponentF で出力する。
ファイル: third_party/skia/src/pdf/SkPDFUtils.h:83
static constexpr unsigned kFloatColorDecimalCount = 4;小数点以下4桁 の精度が定義されている。
ファイル: third_party/skia/src/pdf/SkPDFUtils.cpp:300-308
size_t SkPDFUtils::ColorToDecimalF(float value, char (&result)[kFloatColorDecimalCount + 2]) {
static constexpr int kFactor = int_pow(10, kFloatColorDecimalCount); // 10^4 = 10000
int x = sk_float_round2int(value * kFactor); // ← 丸め処理
if (x >= kFactor || x <= 0) { // clamp to 0-1
result[0] = x > 0 ? '1' : '0';
result[1] = '\0';
return 1;
}
return print_permil_as_decimal(x, result, kFloatColorDecimalCount);
}kFactor = 10000(10^4)value * 10000をsk_float_round2intで 整数に丸める- 結果を
print_permil_as_decimalで小数文字列に変換
ファイル: third_party/skia/include/private/base/SkFloatingPoint.h
// 38行目
#define sk_float_round(x) (float)sk_double_round((double)(x))
// 119行目
#define sk_float_round2int(x) sk_float_saturate2int(sk_float_round(x))
// 126行目
#define sk_double_round(x) (std::floor((x) + 0.5))sk_double_round(x) = std::floor(x + 0.5) は round half up(0.5を足してfloor)の実装。
ファイル: third_party/skia/src/pdf/SkPDFGraphicStackState.cpp:200-204
} else if (state.fColor != currentEntry()->fColor || currentEntry()->fShaderIndex >= 0) {
emit_pdf_color(state.fColor, fContentStream);
fContentStream->writeText("RG ");
emit_pdf_color(state.fColor, fContentStream);
fContentStream->writeText("rg\n");
...
}RG はストローク色、rg はフィル色のPDFオペレータ。
CSS: color(srgb r g b)
↓
SkColor4f (Blink/Skia境界)
↓
emit_pdf_color()
↓
SkPDFUtils::AppendColorComponentF()
↓
SkPDFUtils::ColorToDecimalF()
↓
sk_float_round2int(value * 10000)
↓
sk_float_round() → sk_double_round()
↓
std::floor(x + 0.5) ← round half up
↓
print_permil_as_decimal()
↓
PDF: "0.0001 0 0 rg"
color(srgb-linear ...) を使用した場合、異なる挙動が観察される:
| 入力 (linear) | 出力 (PDF) | 備考 |
|---|---|---|
| 0.00000 | 0 | |
| 0.00001 | 0.0001 | 12.92 × 0.00001 = 0.0001292 → 0.0001 |
| 0.00002 | 0.0003 | 12.92 × 0.00002 = 0.0002584 → 0.0003 |
| 0.00005 | 0.0006 | 12.92 × 0.00005 = 0.000646 → 0.0006 |
| 0.00010 | 0.0013 | 12.92 × 0.00010 = 0.001292 → 0.0013 |
これは sRGB-linear → sRGB へのガンマ変換 が適用されていることを示す。
sRGBのガンマ変換式(線形領域、値 ≤ 0.0031308 の場合):
sRGB = 12.92 × linear
つまり、PDF出力パイプラインは以下の順序で処理を行う:
- CSS色値を解析
- 色空間変換(srgb-linear → sRGB など)
ColorToDecimalFで小数点以下4桁に四捨五入- PDFオペレータとして出力
PDFのrgオペレータはDeviceRGB色空間を使用する。DeviceRGBは本来デバイス依存であり、物理的な光の強度を表す線形値(sRGB-linear)が対応するように思えるかもしれない。
しかし、Chromium/SkiaのPDF出力ではsRGBをOutputIntentとして指定している:
ファイル: third_party/skia/src/pdf/SkPDFDocument.cpp:532-543
static std::unique_ptr<SkPDFArray> make_srgb_output_intents(SkPDFDocument* doc) {
// sRGB is specified by HTML, CSS, and SVG.
auto outputIntent = SkPDFMakeDict("OutputIntent");
outputIntent->insertTextString("Info", "sRGB IEC61966-2.1");
...
}これにより、PDFビューアはDeviceRGB値をsRGBとして解釈する。したがって:
| CSS色空間 | 処理 | 理由 |
|---|---|---|
color(srgb ...) |
そのまま出力 | 入力が既にsRGB。ビューアもsRGBとして解釈するため変換不要 |
color(srgb-linear ...) |
sRGBに変換して出力 | 入力は線形値。ビューアがsRGBとして解釈するため、ガンマ変換が必要 |
sRGB値をDeviceRGBとして出力し、OutputIntentでsRGBを指定することで、PDFビューアは正しい色を再現できる。
観察された挙動は、Skiaライブラリの SkPDFUtils::ColorToDecimalF 関数によって実装されている:
- 精度:
kFloatColorDecimalCount = 4により小数点以下4桁 - 丸め方式:
sk_double_round(x) = std::floor(x + 0.5)により round half up - 色空間: OutputIntent で sRGB を指定し、sRGB 値を DeviceRGB として出力。sRGB-linear は sRGB に変換後に出力
この実装により、color(srgb 0.00005 0 0) は 0.0001 0 0 rg に、color(srgb 0.00004 0 0) は 0 0 0 rg に変換される。