CSS組版 Advent Calendar 2024 17日目の記事です。昨日はhidarumaさんでした。明日はhidarumaさんです。この記事の内容はCSS組版というよりJavaScriptの領分です。
CSS組版で本格的な版下データを出力するには、CMYKへの対応が不可欠です。しかし、Vivliostyleをはじめとするレンダリングを既成のWebブラウザで行うCSS組版エンジンは、Webブラウザが対応しない限りCMYKカラーの出力ができません1。印刷というWebとは異なるドメインに属するカラースペースであるCMYKへの対応を、Webブラウザ側が積極的に進める可能性は高くないと考えられます。
過去には、CSS側で疑似的にCMYKを指定した後、RGBに変換して組版を行い、さらにRGBからCMYKを復元する情報を出力することで、実質的にCMYKを指定する方法を検討しました。そして、先日公開したu1f992/postcss-device-cmyk
により、当面の目標を達成できたと感じています。この記事では、u1f992/postcss-device-cmyk
を使用したCMYK指定とPDF出力について紹介します。
プロジェクト例
.
├── css
│ ├── JapanColor2001Coated.icc
│ ├── package.json
│ ├── postcss.config.js
│ └── src
│ └── style.css
├── manuscript.md
├── package.json
└── vivliostyle.config.js
> mkdir vivliostyle-cmyk-example && cd vivliostyle-cmyk-example
> npm init --yes
> npm pkg set type=module
> npm i --save-dev @vivliostyle/cli
> npx vivliostyle init
{
"name": "vivliostyle-cmyk-example",
"type": "module",
"scripts": {
"build": "cd css && npm run build && cd .. && vivliostyle build",
"preview": "node -e \"[['npm run watch','css'],['npx vivliostyle preview','']].map(([c,cwd])=>require('node:child_process').exec(c,{cwd})).forEach(p=>p.stdout.on('data',d=>process.stdout.write(d))||p.stderr.on('data',d=>process.stderr.write(d)))\""
},
"devDependencies": {
"@vivliostyle/cli": "^8.17.1"
}
}
// @ts-check
/** @type {import('@vivliostyle/cli').VivliostyleConfigSchema} */
const vivliostyleConfig = {
title: "...",
author: "...",
language: "ja",
theme: "./css",
entry: ["manuscript.md"],
output: ["./output.pdf"],
workspaceDir: ".vivliostyle",
};
export default vivliostyleConfig;
> mkdir css && cd css
> echo '{"name":"css","type":"module","main":"dest/style.css","scripts":{"build":"postcss src --verbose --dir dest","watch":"postcss src --verbose --dir dest --watch"}}' > package.json
> npm install --save-dev postcss-cli @u1f992/postcss-device-cmyk
> cd ..
// @ts-check
import fs from "node:fs";
import path from "node:path";
import deviceCMYK from "@u1f992/postcss-device-cmyk";
const outputDir = "dest";
/**
* @param {string} dir
* @returns {string[]}
*/
function collectFiles(dir) {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const filePath = path.join(dir, entry.name);
if (entry.isDirectory()) {
return collectFiles(filePath);
}
return entry.isFile() && filePath.endsWith(".css") ? [filePath] : [];
});
}
export default (ctx) => ({
plugins: [
deviceCMYK({
// https://www.adobe.com/support/downloads/iccprofiles/iccprofiles_win.html
cmykProfilePath: "JapanColor2001Coated.icc",
restoreJSONPath: path.join(outputDir, "device-cmyk.restore.json"),
otherFiles: collectFiles(ctx.file.dirname),
}),
],
});
package.json
の"scripts"
に記載されている通り、$ npm run preview
や$ npm run build
を実行することで、PostCSSを用いたCSSのビルドとVivliostyleによるビルドが一連の処理として実行されます。postcss-cliにはWatch機能があるため、device-cmyk
を含むCSSファイルを変更するだけで、変換を意識せずにVivliostyleのプレビューを更新することが可能です。
device-cmyk
を使用した色指定
u1f992/postcss-device-cmyk
は、CSS Color Module Level 5で定義されているdevice-cmyk
による色指定をサポートするPostCSSプラグインです。関数を単独で使用する場合に限らず、border: 1mm solid device-cmyk(0 1 0 0);
のように他のプロパティ値の一部として使用するケースにも対応しています。
@page {
size: A5;
}
h1 {
color: device-cmyk(0 0 0 1);
border: 1mm solid device-cmyk(0 1 0 0);
border-radius: 5mm;
padding: 10px;
font-size: 7mm;
}
p {
color: device-cmyk(0 0 0 1);
}
a {
color: device-cmyk(1 0 0 0);
}
code {
background-color: device-cmyk(0 0 0 25%);
}
strong {
text-decoration: underline;
text-decoration-color: device-cmyk(0, 0, 1, 0);
text-decoration-thickness: 3px;
text-underline-offset: 3px;
}
変換結果
@page {
size: A5;
}
h1 {
color: rgb(35 25 22);
border: 1mm solid rgb(228 0 127);
border-radius: 5mm;
padding: 10px;
font-size: 7mm;
}
p {
color: rgb(35 25 22);
}
a {
color: rgb(0 161 233);
}
code {
background-color: rgb(211 211 212);
}
strong {
text-decoration: underline;
text-decoration-color: rgb(255 241 0);
text-decoration-thickness: 3px;
text-underline-offset: 3px;
}
同時に、postcss.config.js
で指定したパスに復元情報を出力します。この復元情報は、RGBをキーとしてCMYKを関連付けた単純なJSONファイルです。キー自体がJSON形式である点に注意してください。JSONを処理できる環境であれば、キーのJSON文字列からオブジェクトを復元することもまた可能なはずであり、これは妥当な手段と考えています。
{
"[35,25,22]": [0, 0, 0, 1],
"[228,0,127]": [0, 1, 0, 0],
"[0,161,233]": [1, 0, 0, 0],
"[211,211,212]": [0, 0, 0, 0.25],
"[255,241,0]": [0, 0, 1, 0]
}
Scribusを使用したCMYKへの復元
さらなる調査と加筆が必要です。InDesignでも検証したい。
$ npm run build
で生成されるPDFは、特に変換を施していないRGB形式のPDFです。このPDFをCMYK形式に復元するには、生成されたJSONをもとにInDesignやScribusを使用する必要があります。比較的信頼性の高い方法はInDesignですが、私が所持していないので、ここではScribusを使用する方法を紹介します。
-
ファイル > 設定 > カラーマネジメント > Document Options > Activate Color Management
を有効化。よしなに設定 -
ファイル > 開く
からPDFを開く。Import Text As Vectors
で問題なし -
スクリプト > スクリプトを実行
で以下のPythonスクリプトを実行する - 適切な設定でエクスポートする
import importlib
import json
import typing
class ScribusModule(typing.Protocol):
def fileDialog(
self,
caption: str,
filter: str = ...,
defaultname: str = ...,
haspreview: bool = ...,
issave: bool = ...,
isdir: bool = ...,
) -> str: ...
def getColorNames(self) -> "list[str]": ...
def getColorAsRGB(self, name: str) -> "tuple[int, int, int]": ...
def changeColorCMYKFloat(
self, name: str, c: float, m: float, y: float, k: float
) -> None: ...
scribus = typing.cast(ScribusModule, importlib.import_module("scribus"))
def main():
json_path = scribus.fileDialog("Open", "JSON (*.json)")
if json_path == "":
# Cancel
return
with open(json_path, encoding="utf-8") as f:
restoration_table = typing.cast(
"dict[tuple[int, int, int], tuple[float, float, float, float]]",
{tuple(json.loads(k)): tuple(v) for k, v in json.loads(f.read()).items()},
)
colors_from_pdf = {
name: scribus.getColorAsRGB(name)
for name in scribus.getColorNames()
if name.startswith("FromPDF")
}
for name, color in colors_from_pdf.items():
# FIXME:
# try:
c, m, y, k = restoration_table[color]
# except KeyError:
# # It seems that Vivliostyle (or Chromium) internally uses rgb(0, 0, 0)
# if color == (0, 0, 0):
# c, m, y, k = 1, 1, 1, 1
# else:
# raise
scribus.changeColorCMYKFloat(name, c * 100, m * 100, y * 100, k * 100)
if __name__ == "__main__":
main()
補遺
言うまでもありませんが、このアイデアですべての場合に対応するのは困難です。例えばこんな問題があるかもしれません。
色数の制約
必ず元のCMYK値に復元できるよう、このPostCSSプラグインでは、どんなに近いCMYK値であっても異なるRGB値にマッピングします。たとえば、device-cmyk(1 0 0 0)
、device-cmyk(0.999 0 0 0)
、device-cmyk(0.998 0 0 0)
……のように使用すると、ビルド時間が延び、プレビュー時点での再現度も低下します。当然ながら、256^3色を超える指定はできません。ただし、手作業で指定する程度の個数であれば、まったく問題ありません。
図版
SVGで使用した色はScribus上で同様に列挙されているので、変換先のRGBのみを使用して図版を作成すればよいでしょう。
/* SVGで使う色をCSSに含めておく */
:root {
--cmyk-1-0-0-0: device-cmyk(1 0 0 0);
--cmyk-0_8-0-0-0: device-cmyk(0.8 0 0 0);
--cmyk-0_6-0-0-0: device-cmyk(0.6 0 0 0);
--cmyk-0_4-0-0-0: device-cmyk(0.4 0 0 0);
--cmyk-0_2-0-0-0: device-cmyk(0.2 0 0 0);
--cmyk-0-0-0-1: device-cmyk(0 0 0 1);
--cmyk-0-0-0-0_8: device-cmyk(0 0 0 0.8);
--cmyk-0-0-0-0_6: device-cmyk(0 0 0 0.6);
--cmyk-0-0-0-0_4: device-cmyk(0 0 0 0.4);
--cmyk-0-0-0-0_2: device-cmyk(0 0 0 0.2);
}
/* 変換後のRGBカラーのみを使用してSVG図版を作成する */
:root {
--cmyk-1-0-0-0: rgb(0 161 233);
--cmyk-0_8-0-0-0: rgb(0 175 236);
--cmyk-0_6-0-0-0: rgb(85 195 241);
--cmyk-0_4-0-0-0: rgb(159 217 246);
--cmyk-0_2-0-0-0: rgb(211 237 251);
--cmyk-0-0-0-1: rgb(35 25 22);
--cmyk-0-0-0-0_8: rgb(89 87 87);
--cmyk-0-0-0-0_6: rgb(137 137 137);
--cmyk-0-0-0-0_4: rgb(181 181 182);
--cmyk-0-0-0-0_2: rgb(220 221 221);
}
ラスター画像は手動で貼り替える以外に良い方法が思いつきません。何らかのメタデータを含められたら自動で貼り替えられるかもしれません。
画面キャプチャや写真は、元画像がCMYKである可能性は低いため、4Cであれば自動変換で妥当な結果を得られるのではないでしょうか。1~3Cの場合は体感としてモノクロで掲載することが多いことから、こちらも自動変換で十分対応できるかもしれません。シンプルなイラストの類については、ベクター画像への変換を許容する方が実用的かもしれません。
グラデーションなど
linear-gradient
などでdevice-cmyk
を使用すれば変換は行われますが、InDesignやScribus上でどのように取り込まれるのか、また操作可能なのかは不明です。仮に自動でCMYKに変換できたとしても、たとえば2色刷りでdevice-cmyk(1 0 0 0)
→device-cmyk(0 0 0 0)
を意図したグラデーションの中間色にシアン以外の色が含まれないとは限りません。結局、手動で貼り替える必要があるのではないでしょうか。
トンボ
Vivliostyleのトンボは、すべての版に何らかが表示されるよう、あえてblack
ではなくなっているということです2。
高度なトンボを付けたいなら、Vivliostyleからは「塗り足しあり・トンボなし」で出力して、InDesignやScribusでトンボを付けたほうがよさそうです。