Vivliostyle でテーマのフォントを変更する

技術書典などで技術同人誌を書くとき、私は [Vivliostyle](https://vivliostyle.org/ja/) [^vivliostyle] を利用しています。Web 技術や CSS を利用して組版する OSS です。本や原稿の製作に、日々の仕事で使っている Web 知識を活用できるので、とても重宝しています。技術書典のほかに、iOSDC Japan のパンフレット寄稿記事にも利用しており、いくつか執筆テンプレートを Vivliostyle で作成して、いつでも書けるようにしています。

https://github.com/mitsuharu/iosdc-pamphlet-template

https://github.com/mitsuharu/techbook-template

[^vivliostyle]: https://vivliostyle.org/ja/

## 生成される PDF のフォント

Vivliostyle で生成される PDF は、利用するテーマにもよりますが、一般にビルドコマンドを実行する環境のフォントが利用されます。MacBook であれば macOS のシステムフォント、Linux マシンであれば Linux のシステムフォントが使われます。執筆を MacBook で行い、印刷用 PDF の生成を公式推奨の Docker で実行すると、それぞれで利用されるフォントは異なります。これまで段落やページ数が変わるほどの大きな違いや問題は無かったので、「フォントが違うなぁ」と軽く思う程度でした。

>2024/11/04 追記
>気付いていなかっただけで、段落の配置が実際に異なってました。全体のページ数が変わらなかったので気付かず、見落としてた。フォントを揃えるのは大事！

執筆テンプレートを作成する際に、記事追加の PR で PDF が自動作成ができたら、レビューの助けになるだろうなと GitHub Actions 上で PDF を作成しました。無事に PDF が作成されて満足してたところ、実際にファイルを開いたらフォントが豆腐（文字化け）になっていました。困った。GitHub Actions 上で動作させる Ubuntu に日本語フォントが入っていないからです。

Actions を実行するたびに `sudo apt install fonts-noto-cjk` などフォントインストールを実行すれば解決しそうですが、毎回インストールするのはさすがにパフォーマンスが悪い。CI や自動化などを考えている場合に、フォントがコマンドを実行する環境に依存するのは、あまりよろしくないですね。それならば、執筆のプロジェクトにフォントを組み込んで、執筆環境が異なっても同じフォントを利用しましょう。

## ローカルフォントの選定

フォントデータは Google Fonts の Noto Sans JP および Noto Sans Mono を利用しました。通常の本文には Noto Sans JP、ソースコードなどの等倍フォントには Noto Sans Mono です。Google Fonts は商用利用できる、印刷物にも利用できると、ライセンス的に問題のないフォントです。ありがとうございます。

https://developers.google.com/fonts/faq?hl=ja

## ローカルフォントを適用させる（失敗した例）

さっそくフォントをプロジェクト内に保存して、CSS でそのフォントを設定します。

```css:fonts.css
@font-face {
  font-family: 'Noto Sans JP';
  src: url('./Noto_Sans_JP/static/NotoSansJP-Regular.ttf') format('truetype');
}

* {
  font-family: 'Noto Sans JP';
}
```

そして、この `fonts.css` をビルド設定に加えました。

```js:vivliostyle.config.js
module.exports = {
  theme: [
    '@vivliostyle/theme-techbook',
    'fonts.css',
  ],
```

対応終わった！と意気揚々にビルドしたところ、このフォント設定は有効にはなりませんでした。それどころ、コンソールに `A network error occurred.` と不吉なエラーメッセージが表示されました。なお、フォント適用を除いて PDF は正常に生成されています。

ちなみに、ローカルフォントではなく、Web フォントで指定するとエラーメッセージは表示されることなく、そのフォントが PDFに適用されました。不思議。

```css:web-fonts.css
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');

* {
  font-family: "Roboto", sans-serif;
}
```

### ローカルフォント適用失敗の原因

この問題を呟いたところ、Vivliostyle 開発者の [MurakamiShinyu](https://x.com/MurakamiShinyu) さん達に原因を教えてもらい、無事に解決しました。

https://github.com/vivliostyle/vivliostyle-cli/issues/537

theme オプションで CSS ファイルを指定する方法は、必要な外部ファイルの場所を解決できないため、フォントファイルの参照に失敗していました。そこで、フォントファイルを含めたローカルのパッケージとして定義する必要があると、教えてもらいました。

その理由から Web フォントが適用できたのは、Web URL という絶対パスなので、フォントファイルを問題なく参照できたからと考えられます。ちなみに、ローカルパスでも `/Users/user/path/font.ttf` のようなルートからの絶対パスなら表示できていました。

## ローカルフォントを適用させる（成功した例）

私は次のような構成からなるフォントのローカルパッケージを作成しました。

```text:フォントのローカルパッケージ
fonts
┣ package.json
┣ index.css
┣ noto-sans-jp.css
┣ Noto_Sans_JP/
┣ noto-sans-mono.css
┗ Noto_Sans_Mono/
```

`pakage.json` は次のとおりで、メインとなる `index.css` を参照しています。

```json:pakage.json
{
  "name": "my-theme-fonts",
  "main": "index.css"
}
```

`index.css` は次のように、フォントの適用を行っています。このファイル内でフォント設定してもよいのですが、ファイルが肥大化してしまい可読性が下がるので、フォント設定用の CSS を作成して、それを import しています。なお、前節ではフォント適用に HTML タグを利用していましたが、Vivliostyle のフォント用の変数に修正しました。HTML タグでは柱（ページ右上）などの本文以外の文字列のフォントが適用されなかったためです。

```css:index.css
@import url(./noto-sans-jp.css); 
@import url(./noto-sans-mono.css);

/* フォントを変更する */
:root {
  /* 本文のフォント */
  --vs-font-family: 'Noto Sans JP', sans-serif;

  /* 等幅フォント（コードブロックで利用される）のフォント */
  --vs--monospace-font-family: 'Noto Sans Mono', 'Noto Sans JP', monospace;
}
```

さて、フォントを設定している CSS は長くなるので後回しとして、準備が揃ったので、このローカルパッケージをビルド設定に追加しました。これでビルドすると、無事に Noto Sans JP になった PDF が生成されました。よかった。

```js:vivliostyle.config.js
module.exports = {
  theme: [
  '@vivliostyle/theme-techbook',
  'fonts',
  ],
```

フォントを設定している `noto-sans-jp.css` と `noto-sans-mono.css` は以下のとおりです。Noto Sans JP は本文に利用されるため、normal の font-weight だけでは対応できない場合があるので、font-weight それぞれのフォントを設定しました。約 50 MB ほどファイルサイズが大きくなりました。VariableFont を利用すれば、１つのフォントファイルで済みますが、それを利用して生成した PDF がフォントを埋め込んでいなかったので、やめました。

```css:noto-sans-jp.css
@font-face {
  font-family: 'Noto Sans JP';
  src: url('./Noto_Sans_JP/static/NotoSansJP-Thin.ttf') format('truetype');
  font-weight: 100;
  font-style: normal;
}

@font-face {
  font-family: 'Noto Sans JP';
  src: url('./Noto_Sans_JP/static/NotoSansJP-ExtraLight.ttf') format('truetype');
  font-weight: 200;
  font-style: normal;
}

@font-face {
  font-family: 'Noto Sans JP';
  src: url('./Noto_Sans_JP/static/NotoSansJP-Light.ttf') format('truetype');
  font-weight: 300;
  font-style: normal;
}

@font-face {
  font-family: 'Noto Sans JP';
  src: url('./Noto_Sans_JP/static/NotoSansJP-Regular.ttf') format('truetype');
  font-weight: 400;
  font-style: normal;
}

@font-face {
  font-family: 'Noto Sans JP';
  src: url('./Noto_Sans_JP/static/NotoSansJP-Medium.ttf') format('truetype');
  font-weight: 500;
  font-style: normal;
}

@font-face {
  font-family: 'Noto Sans JP';
  src: url('./Noto_Sans_JP/static/NotoSansJP-SemiBold.ttf') format('truetype');
  font-weight: 600;
  font-style: normal;
}

@font-face {
  font-family: 'Noto Sans JP';
  src: url('./Noto_Sans_JP/static/NotoSansJP-Bold.ttf') format('truetype');
  font-weight: 700;
  font-style: normal;
}

@font-face {
  font-family: 'Noto Sans JP';
  src: url('./Noto_Sans_JP/static/NotoSansJP-ExtraBold.ttf') format('truetype');
  font-weight: 800;
  font-style: normal;
}

@font-face {
  font-family: 'Noto Sans JP';
  src: url('./Noto_Sans_JP/static/NotoSansJP-Black.ttf') format('truetype');
  font-weight: 900;
  font-style: normal;
}
```

Noto Sans Mono は主にソースコードの表示にしか使われない想定なので、font-weight が normal のフォントのみを設定しました。すべての font-weight を設定した方が安全ですが、Noto Sans JP で増えたファイルサイズがさらに増えるのか…と日和ったのが半分本音です。

```css:noto-sans-mono.css
@font-face {
  font-family: 'Noto Sans Mono';
  src: url('./Noto_Sans_Mono/static/NotoSansMono-Regular.ttf') format('truetype');
  font-weight: normal;
  font-style: normal;
}
```

## まとめ

Vivliostyle は雰囲気で利用していたので、theme オプションでパッケージの概念があったとは知りませんした。勉強になりました。

今回作成したフォントのローカルパッケージは Vivliostyle のテーマとして公開しようかとも思いましたが、再配布可能な Google Fonts とはいえ、さすがに埋め込んでパッケージとして配るのはやり過ぎかなと止めました。が、実際はどうなんでしょうかね。毎回プロジェクトごとに設定するのは面倒なので、パッケージ化した方がよいのかな？

## フォントを設定するテーマを作りました（2024/11/05 追記）

フォントを設定するテーマを作りました。その詳細は次の記事でまとめました。

https://qiita.com/mitsuharu_e/items/09c3b7dfbf49f4608d52