技術書典などで技術同人誌を書くとき、私は Vivliostyle 1 を利用しています。Web 技術や CSS を利用して組版する OSS です。本や原稿の製作に、日々の仕事で使っている Web 知識を活用できるので、とても重宝しています。技術書典のほかに、iOSDC Japan のパンフレット寄稿記事にも利用しており、いくつか執筆テンプレートを Vivliostyle で作成して、いつでも書けるようにしています。
生成される 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 は商用利用できる、印刷物にも利用できると、ライセンス的に問題のないフォントです。ありがとうございます。
ローカルフォントを適用させる(失敗した例)
さっそくフォントをプロジェクト内に保存して、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
をビルド設定に加えました。
module.exports = {
theme: [
'@vivliostyle/theme-techbook',
'fonts.css',
],
対応終わった!と意気揚々にビルドしたところ、このフォント設定は有効にはなりませんでした。それどころ、コンソールに A network error occurred.
と不吉なエラーメッセージが表示されました。なお、フォント適用を除いて PDF は正常に生成されています。
ちなみに、ローカルフォントではなく、Web フォントで指定するとエラーメッセージは表示されることなく、そのフォントが PDFに適用されました。不思議。
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
* {
font-family: "Roboto", sans-serif;
}
ローカルフォント適用失敗の原因
この問題を呟いたところ、Vivliostyle 開発者の MurakamiShinyu さん達に原因を教えてもらい、無事に解決しました。
theme オプションで CSS ファイルを指定する方法は、必要な外部ファイルの場所を解決できないため、フォントファイルの参照に失敗していました。そこで、フォントファイルを含めたローカルのパッケージとして定義する必要があると、教えてもらいました。
その理由から Web フォントが適用できたのは、Web URL という絶対パスなので、フォントファイルを問題なく参照できたからと考えられます。ちなみに、ローカルパスでも /Users/user/path/font.ttf
のようなルートからの絶対パスなら表示できていました。
ローカルフォントを適用させる(成功した例)
私は次のような構成からなるフォントのローカルパッケージを作成しました。
fonts
┣ package.json
┣ index.css
┣ noto-sans-jp.css
┣ Noto_Sans_JP/
┣ noto-sans-mono.css
┗ Noto_Sans_Mono/
pakage.json
は次のとおりで、メインとなる index.css
を参照しています。
{
"name": "my-theme-fonts",
"main": "index.css"
}
index.css
は次のように、フォントの適用を行っています。このファイル内でフォント設定してもよいのですが、ファイルが肥大化してしまい可読性が下がるので、フォント設定用の CSS を作成して、それを import しています。なお、前節ではフォント適用に HTML タグを利用していましたが、Vivliostyle のフォント用の変数に修正しました。HTML タグでは柱(ページ右上)などの本文以外の文字列のフォントが適用されなかったためです。
@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 が生成されました。よかった。
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 を利用すれば、1つのフォントファイルで済みますが、それを利用して生成した PDF がフォントを埋め込んでいなかったので、やめました。
@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 で増えたファイルサイズがさらに増えるのか…と日和ったのが半分本音です。
@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 追記)
フォントを設定するテーマを作りました。その詳細は次の記事でまとめました。