Vivliostyleで詳細なRGB色指定をおこなう

[CSS組版 Advent Calendar 2024](https://adventar.org/calendars/10448) 24日目の記事です。昨日はhidarumaさんでした。

Vivliostyleは、PDFのカラースペースとしてRGBを使用して出力します[^1]。RGBといえば、各チャンネルを符号なし8ビット整数（0〜255）で表す形式を想像しますが、PDFでは各チャンネルを0〜1の範囲の値として格納します。そのため、実のところPDFでは符号なし8ビット整数よりも細かい色の指定が可能です。

[^1]: Chromiumを前提とします。他のブラウザは未検証ですが、おおよそ同じと考えられます。

CSSにも符号なし8ビット整数を超える分解能でRGBを指定する方法が存在し、VivliostyleによるPDF出力にも反映させることができます[^2]。

[^2]: これは結局上手く実装できなかったけど、Vivliostyle.jsのコードを読む機会ができたし、Chromium Issue Trackerも初めて使ったのでよかった https://github.com/vivliostyle/vivliostyle.js/issues/1432

## 小数値を含む`rgb()`関数

CSSの`rgb()`関数は、`rgb(0, 128, 255)`のように整数値を用いる使用方法が一般的ですが、CSS Color Module Level 4では以下のように小数値で指定することも認められています。

```rgb.html
<html>
  <head>
    <style>
      @page {
        size: A5;
      }
      body {
        background-color: rgb(0 0.1 0.2);
      }
    </style>
  </head>
  <body></body>
</html>
```

このHTMLをChromiumで開き、PDFとして出力したものをGhostscriptで解析すると、次のような結果が得られるはずです。ここで`rg`はRGBの塗りつぶしを意味し、`0.000400`および`0.000800`は指定した値（それぞれ`0.1 / 255`、`0.2 / 255`に相当）が反映されていることを示しています。

```
$ echo -e 'from playwright.sync_api import sync_playwright\nwith (\n    sync_playwright() as playwright,\n    playwright.chromium.launch() as browser,\n    browser.new_context() as context,\n    context.new_page() as page,\n    open("rgb.html", encoding="utf-8") as f\n):\n    page.set_content(f.read())\n    page.pdf(path="rgb.pdf", print_background=True)' | python -
$ gs -dPDFDEBUG -dBATCH -dNOPAUSE -sDEVICE=nullpage rgb.pdf 2>&1 | grep rg
 0 0.000400 0.000800 rg
```

## インラインスタイルでの`rgb()`関数の制限

ところが、この記法は現時点のVivliostyleでは使用できません。以下の通り、先ほどの結果とは異なることが確認できます。

```
$ vivliostyle --version                           
cli: 8.17.1
core: 2.30.7
$ vivliostyle build rgb.html -o rgb-vivliostyle.pdf
$ gs -dPDFDEBUG -dBATCH -dNOPAUSE -sDEVICE=nullpage rgb-vivliostyle.pdf 2>&1 | grep rg
 0 0 0 rg
```

この現象はWebブラウザの挙動に起因します。私が理解している範囲で簡単に説明すると、Vivliostyleでは、スタイルの相関やページ分割を解析した後、適切なスタイルをインラインスタイルとして各要素に反映する部分があります。しかし、Chromium/Firefox/Safariでは、小数値を含む`rgb()`をインラインスタイルに設定すると丸めが発生します。この挙動はバグではない[^3]ものの、好ましい動作とは言えません。

[^3]: "The precision with which sRGB component values are retained, and thus the number of significant figures in the serialized value, is not defined in this specification, but must at least be sufficient to round-trip eight bit values. Values must be rounded towards +∞, not truncated." ― https://drafts.csswg.org/css-color-4/#css-serialization-of-srgb

```
> document.body.style.setProperty("background-color", "rgb(0 0.1 0.2)")
> document.body.style.getPropertyValue("background-color")
"rgb(0, 0, 0)"
```

:::note warn
`document.body.style.backgroundColor = "rgb(0 0.1 0.2)"`も同様に`rgb(0, 0, 0)`に変換されます。一方、`document.body.setAttribute("style", "background-color: rgb(0 0.1 0.2)")`とすると変換されず、DevToolsやPDF出力にも小数値が反映されます。ただし、その後に他のプロパティを`setProperty`または`style.prop = ...`で変更すると、`background-color: rgb(0, 0, 0)`に上書きされます。
:::

Webブラウザの挙動が将来的に改善される可能性はあります[^4]が、現在のところ、Vivliostyleでは小数値を含む`rgb()`関数は期待するように動作しません。

[^4]: https://issues.chromium.org/issues/385613195

## `color()`関数による色指定

代わりに`color()`関数を使用することで、Vivliostyleでも期待通りの色指定が可能です。以下のようにカラースペースに`srgb`を指定し、各チャンネルに0〜1の範囲の値を与えます。Ghostscriptの表示に近い表現で指定できますが、実際には小数点以下5桁付近で丸められているようです（詳細未確認）。

```color.html
<html>
  <head>
    <style>
      @page {
        size: A5;
      }
      body {
        background-color: color(srgb 0 0.0004 0.0008);
      }
    </style>
  </head>
  <body></body>
</html>
```

```
$ vivliostyle build color.html -o color-vivliostyle.pdf
$ gs -dPDFDEBUG -dBATCH -dNOPAUSE -sDEVICE=nullpage color-vivliostyle.pdf 2>&1 | grep rg
 0 0.000400 0.000800 rg
```

`color()`を使用することで、Vivliostyleでも高精度なRGB色指定が可能になります。画面表示に関しては、多くのPDFビューアが8ビット整数に丸めて表示していることが予想されますが、印刷用途では異なる結果が得られる場合があるかもしれません。……ないかもしれません。CMYKへの変換処理の実装次第でしょうか。