Skip to content

Instantly share code, notes, and snippets.

@u1f992
Created December 24, 2025 14:48
Show Gist options
  • Select an option

  • Save u1f992/fe6eb2e2e47f161a4df9aaab3a3d5a0c to your computer and use it in GitHub Desktop.

Select an option

Save u1f992/fe6eb2e2e47f161a4df9aaab3a3d5a0c to your computer and use it in GitHub Desktop.

Chromium color(srgb ...) → PDF オペレータ 変換の調査

分析対象のコードバージョン

リポジトリ コミットハッシュ 日時
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

コードによる裏付け

1. 色のPDF出力エントリポイント

ファイル: 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 で出力する。

2. 精度の定義

ファイル: third_party/skia/src/pdf/SkPDFUtils.h:83

static constexpr unsigned kFloatColorDecimalCount = 4;

小数点以下4桁 の精度が定義されている。

3. 色値のフォーマット処理

ファイル: 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 * 10000sk_float_round2int整数に丸める
  • 結果を print_permil_as_decimal で小数文字列に変換

4. 丸め関数の実装

ファイル: 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)の実装。

5. rg オペレータの出力

ファイル: 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"

srgb-linear の場合の挙動

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出力パイプラインは以下の順序で処理を行う:

  1. CSS色値を解析
  2. 色空間変換(srgb-linear → sRGB など)
  3. ColorToDecimalF で小数点以下4桁に四捨五入
  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 関数によって実装されている:

  1. 精度: kFloatColorDecimalCount = 4 により小数点以下4桁
  2. 丸め方式: sk_double_round(x) = std::floor(x + 0.5) により round half up
  3. 色空間: 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 に変換される。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment