Vivliostyle+ScribusでCMYK色指定を実現する

Vivliostyleは出力にChromiumを使用する都合上、色の指定方法は現状RGBに限られており、CMYKで直接指定することはできません。[press-ready](https://github.com/vibranthq/press-ready)やInDesignで変換することはできても、特定の版のみを使用するデータの作成は困難です。『Web技術で「本」が作れるCSS組版Vivliostyle入門』では、いくつかのRGB値がInDesignでキリのよいCMYK値に変換されることを利用して2色刷りのPDFを作成する試みが紹介されています[^1]。

[^1]: 大津雄一郎（2023）Web技術で「本」が作れるCSS組版Vivliostyle入門　P.240

考えてみると、図版を除けば本文フォーマットデザイン自体にはそれほど多くの色を使用していないように見えます。色数がRGBの256^3色より十分少ないなら、素朴な発想として、「このRGB値は後処理でこのCMYK値に置き換える」というルールをあらかじめ決めておけば実質的なCMYK色指定ができそうです。

使用するCMYK値の一覧を渡すと、CSS変数定義と、Scribusで使用するPythonスクリプトを生成するスクリプトを用意しました。

[https://gist.github.com/u1f992/724dbef7875f265900738cf2259d684a](https://gist.github.com/u1f992/724dbef7875f265900738cf2259d684a)

CK2色刷りで0, 20, 40, 60, 80, 100の掛け合わせなら以下のように使用します。

```py
main(
    frozenset(
        itertools.product(
            map(lambda x: float(x / 10), range(0, 11, 2)),
            [0.0],
            [0.0],
            map(lambda x: float(x / 10), range(0, 11, 2)),
        ),
    )
)
```

<details>
<summary>ck.css</summary>

```ck.css
:root {
    --cmyk-60-0-0-60: rgb(36, 106, 132);
    --cmyk-40-0-0-0: rgb(158, 217, 246);
    --cmyk-80-0-0-20: rgb(0, 154, 206);
    --cmyk-0-0-0-40: rgb(181, 181, 182);
    --cmyk-80-0-0-100: rgb(0, 8, 33);
    --cmyk-20-0-0-0: rgb(211, 237, 251);
    --cmyk-100-0-0-0: rgb(0, 161, 233);
    --cmyk-80-0-0-60: rgb(0, 97, 131);
    --cmyk-60-0-0-0: rgb(84, 195, 241);
    --cmyk-40-0-0-40: rgb(112, 155, 176);
    --cmyk-0-0-0-80: rgb(89, 87, 87);
    --cmyk-20-0-0-40: rgb(150, 169, 179);
    --cmyk-100-0-0-40: rgb(0, 117, 169);
    --cmyk-80-0-0-0: rgb(0, 175, 236);
    --cmyk-60-0-0-40: rgb(56, 141, 173);
    --cmyk-40-0-0-80: rgb(50, 75, 87);
    --cmyk-0-0-0-20: rgb(220, 221, 221);
    --cmyk-0-0-0-100: rgb(0, 0, 0);
    --cmyk-20-0-0-80: rgb(72, 81, 87);
    --cmyk-0-0-0-60: rgb(137, 137, 137);
    --cmyk-40-0-0-20: rgb(137, 188, 214);
    --cmyk-80-0-0-40: rgb(0, 128, 171);
    --cmyk-100-0-0-80: rgb(0, 56, 86);
    --cmyk-60-0-0-80: rgb(9, 68, 86);
    --cmyk-40-0-0-100: rgb(3, 19, 29);
    --cmyk-20-0-0-20: rgb(183, 205, 218);
    --cmyk-40-0-0-60: rgb(82, 117, 134);
    --cmyk-100-0-0-20: rgb(0, 141, 203);
    --cmyk-20-0-0-100: rgb(23, 22, 26);
    --cmyk-60-0-0-20: rgb(72, 170, 210);
    --cmyk-0-0-0-0: rgb(255, 255, 255);
    --cmyk-80-0-0-80: rgb(0, 61, 86);
    --cmyk-100-0-0-100: rgb(0, 1, 34);
    --cmyk-60-0-0-100: rgb(0, 15, 31);
    --cmyk-20-0-0-60: rgb(113, 127, 135);
    --cmyk-100-0-0-60: rgb(0, 89, 130);
}
```

</details>
<details>
<summary>ck.py</summary>

```ck.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import division
import sys

try:
    import scribus
except ImportError as err:
    print("This Python script is written for the Scribus scripting interface.")
    print("It can only be run from within Scribus.")
    sys.exit(1)

                    
TABLE = {(0, 0, 0): (0.0, 0.0, 0.0, 1.0),
 (0, 1, 34): (1.0, 0.0, 0.0, 1.0),
 (0, 8, 33): (0.8, 0.0, 0.0, 1.0),
 (0, 15, 31): (0.6, 0.0, 0.0, 1.0),
 (0, 56, 86): (1.0, 0.0, 0.0, 0.8),
 (0, 61, 86): (0.8, 0.0, 0.0, 0.8),
 (0, 89, 130): (1.0, 0.0, 0.0, 0.6),
 (0, 97, 131): (0.8, 0.0, 0.0, 0.6),
 (0, 117, 169): (1.0, 0.0, 0.0, 0.4),
 (0, 128, 171): (0.8, 0.0, 0.0, 0.4),
 (0, 141, 203): (1.0, 0.0, 0.0, 0.2),
 (0, 154, 206): (0.8, 0.0, 0.0, 0.2),
 (0, 161, 233): (1.0, 0.0, 0.0, 0.0),
 (0, 175, 236): (0.8, 0.0, 0.0, 0.0),
 (3, 19, 29): (0.4, 0.0, 0.0, 1.0),
 (9, 68, 86): (0.6, 0.0, 0.0, 0.8),
 (23, 22, 26): (0.2, 0.0, 0.0, 1.0),
 (36, 106, 132): (0.6, 0.0, 0.0, 0.6),
 (50, 75, 87): (0.4, 0.0, 0.0, 0.8),
 (56, 141, 173): (0.6, 0.0, 0.0, 0.4),
 (72, 81, 87): (0.2, 0.0, 0.0, 0.8),
 (72, 170, 210): (0.6, 0.0, 0.0, 0.2),
 (82, 117, 134): (0.4, 0.0, 0.0, 0.6),
 (84, 195, 241): (0.6, 0.0, 0.0, 0.0),
 (89, 87, 87): (0.0, 0.0, 0.0, 0.8),
 (112, 155, 176): (0.4, 0.0, 0.0, 0.4),
 (113, 127, 135): (0.2, 0.0, 0.0, 0.6),
 (137, 137, 137): (0.0, 0.0, 0.0, 0.6),
 (137, 188, 214): (0.4, 0.0, 0.0, 0.2),
 (150, 169, 179): (0.2, 0.0, 0.0, 0.4),
 (158, 217, 246): (0.4, 0.0, 0.0, 0.0),
 (181, 181, 182): (0.0, 0.0, 0.0, 0.4),
 (183, 205, 218): (0.2, 0.0, 0.0, 0.2),
 (211, 237, 251): (0.2, 0.0, 0.0, 0.0),
 (220, 221, 221): (0.0, 0.0, 0.0, 0.2),
 (255, 255, 255): (0.0, 0.0, 0.0, 0.0)}


def main(argv):
    """This is a documentation string. Write a description of what your code
    does here. You should generally put documentation strings ("docstrings")
    on all your Python functions."""
    for color_name in scribus.getColorNames():
        rgb = scribus.getColorAsRGB(color_name)
        if rgb in TABLE.keys():
            c, m, y, k = map(lambda x: x * 100, TABLE[rgb])
            scribus.changeColorCMYKFloat(color_name, c, m, y, k)



def main_wrapper(argv):
    """The main_wrapper() function disables redrawing, sets a sensible generic
    status bar message, and optionally sets up the progress bar. It then runs
    the main() function. Once everything finishes it cleans up after the main()
    function, making sure everything is sane before the script terminates."""
    try:
        scribus.statusMessage("Running script...")
        scribus.progressReset()
        main(argv)
    finally:
        # Exit neatly even if the script terminated with an exception,
        # so we leave the progress bar and status bar blank and make sure
        # drawing is enabled.
        if scribus.haveDoc():
            scribus.setRedraw(True)
        scribus.statusMessage("")
        scribus.progressReset()

# This code detects if the script is being run as a script, or imported as a module.
# It only runs main() if being run as a script. This permits you to import your script
# and control it manually for debugging.
if __name__ == '__main__':
    main_wrapper(sys.argv)
```

</details>

Vivliostyle側では、定義されたCSS変数で色を指定します。

```test.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <style>
        @import url("ck.css");

        @page {
            size: 210mm 297mm;
            margin: 0;
        }

        body {
            display: grid;
            grid-template-columns: repeat(6, 1fr);
            grid-template-rows: repeat(6, 1fr);
            height: 297mm;
        }
    </style>
</head>

<body>
    <div style="background-color: var(--cmyk-0-0-0-0);">C0 K0</div>
    <div style="background-color: var(--cmyk-20-0-0-0);">C20 K0</div>
    <div style="background-color: var(--cmyk-40-0-0-0);">C40 K0</div>
    <div style="background-color: var(--cmyk-60-0-0-0);">C60 K0</div>
    <div style="background-color: var(--cmyk-80-0-0-0);">C80 K0</div>
    <div style="background-color: var(--cmyk-100-0-0-0);">C100 K0</div>
    <div style="background-color: var(--cmyk-0-0-0-20);">C0 K20</div>
    <div style="background-color: var(--cmyk-20-0-0-20);">C20 K20</div>
    <div style="background-color: var(--cmyk-40-0-0-20);">C40 K20</div>
    <div style="background-color: var(--cmyk-60-0-0-20);">C60 K20</div>
    <div style="background-color: var(--cmyk-80-0-0-20);">C80 K20</div>
    <div style="background-color: var(--cmyk-100-0-0-20);">C100 K20</div>
    <div style="background-color: var(--cmyk-0-0-0-40);">C0 K40</div>
    <div style="background-color: var(--cmyk-20-0-0-40);">C20 K40</div>
    <div style="background-color: var(--cmyk-40-0-0-40);">C40 K40</div>
    <div style="background-color: var(--cmyk-60-0-0-40);">C60 K40</div>
    <div style="background-color: var(--cmyk-80-0-0-40);">C80 K40</div>
    <div style="background-color: var(--cmyk-100-0-0-40);">C100 K40</div>
    <div style="background-color: var(--cmyk-0-0-0-60); color: var(--cmyk-0-0-0-0);">C0 K60</div>
    <div style="background-color: var(--cmyk-20-0-0-60); color: var(--cmyk-0-0-0-0);">C20 K60</div>
    <div style="background-color: var(--cmyk-40-0-0-60); color: var(--cmyk-0-0-0-0);">C40 K60</div>
    <div style="background-color: var(--cmyk-60-0-0-60); color: var(--cmyk-0-0-0-0);">C60 K60</div>
    <div style="background-color: var(--cmyk-80-0-0-60); color: var(--cmyk-0-0-0-0);">C80 K60</div>
    <div style="background-color: var(--cmyk-100-0-0-60); color: var(--cmyk-0-0-0-0);">C100 K60</div>
    <div style="background-color: var(--cmyk-0-0-0-80); color: var(--cmyk-0-0-0-0);">C0 K80</div>
    <div style="background-color: var(--cmyk-20-0-0-80); color: var(--cmyk-0-0-0-0);">C20 K80</div>
    <div style="background-color: var(--cmyk-40-0-0-80); color: var(--cmyk-0-0-0-0);">C40 K80</div>
    <div style="background-color: var(--cmyk-60-0-0-80); color: var(--cmyk-0-0-0-0);">C60 K80</div>
    <div style="background-color: var(--cmyk-80-0-0-80); color: var(--cmyk-0-0-0-0);">C80 K80</div>
    <div style="background-color: var(--cmyk-100-0-0-80); color: var(--cmyk-0-0-0-0);">C100 K80</div>
    <div style="background-color: var(--cmyk-0-0-0-100); color: var(--cmyk-0-0-0-0);">C0 K100</div>
    <div style="background-color: var(--cmyk-20-0-0-100); color: var(--cmyk-0-0-0-0);">C20 K100</div>
    <div style="background-color: var(--cmyk-40-0-0-100); color: var(--cmyk-0-0-0-0);">C40 K100</div>
    <div style="background-color: var(--cmyk-60-0-0-100); color: var(--cmyk-0-0-0-0);">C60 K100</div>
    <div style="background-color: var(--cmyk-80-0-0-100); color: var(--cmyk-0-0-0-0);">C80 K100</div>
    <div style="background-color: var(--cmyk-100-0-0-100); color: var(--cmyk-0-0-0-0);">C100 K100</div>
</body>

</html>
```

変換後のPDFは目論見通りCK版しか使っていないことがわかります。

```shell-session
> docker run --mount "type=bind,source=$PWD,target=/workdir" --workdir /workdir --interactive --tty --rm registry.gitlab.com/islandoftex/images/texlive:TL2023-historic gs -dSAFER -dNOPAUSE -dBATCH -o- -sDEVICE=ink_cov test-cmyk.pdf
GPL Ghostscript 10.03.1 (2024-05-02)
Copyright (C) 2024 Artifex Software, Inc.  All rights reserved.
This software is supplied under the GNU AGPLv3 and comes with NO WARRANTY:
see the file COPYING for details.
Processing pages 1 through 1.
Page 1
49.03447  0.00000  0.00000 49.92754 CMYK OK
```

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1550728/1c67839f-d9a1-8e66-3c96-73484ca5cf83.png)

プレビューでもそれなりに色を再現できています。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1550728/31a3d0c1-df2c-f9a0-a1b3-b9dc7832ccb5.png)

使う色だけで変換テーブルを生成するのが肝です。CSSが長くなれば組版処理も時間がかかるので、2色刷り各色0.5刻みで全色定義するような使い方は現実的ではありません。また当然256^3色以上のCMYK値を定義することはできません。

今回は図版については何も考えていません。SVGの色はScribusから同じように見えているので同様に変換できそうです。ビットマップ画像は手動で貼り替えることになるかもしれません。