base イメージは、文字を描くときの既定を日本語にする。総称ファミリ(sans-serif / sans /
serif / monospace)と、イメージに無い書体名は Noto CJK の JP フェイスへ解決し、欧文の
metric 互換(Arial / Times New Roman / Courier New / Calibri / Cambria)と、中国語・
韓国語を明示した指定は、それぞれの書体・言語・様式のままにする。あわせて、PDF を画像に
する・調べる、OOXML を壊さずに読み書きする道具を base の中に持つ。
fontconfig は Chromium / Playwright のスクリーンショット、PDF の生成、画像の生成がすべて 参照するため、この既定は「文書を描く」以外の用途にも一様に効く。
規則は containers/base/fonts-local.conf の 1 ファイルで表し、イメージの中では
/etc/fonts/local.conf に置く。追加のパッケージを要さず、設定そのもののサイズは 0 である。
利用者向けの読み方は コンテナ操作ガイド: 文字の描画と、文書を扱う道具 にある。
- base イメージの fontconfig の設定の置き場所と内容、フォントキャッシュの作り直し
- base イメージに同梱する、文書を扱う道具のパッケージ
- base から派生するイメージ(
general/go/php/php85/bi-tools/latex/trygroup)への伝播の規則 containers/lfmは base 由来ではないため対象に含まないENV LANGは設定しない。LibreOffice とpipは同梱しない
| 用語 | 意味 |
|---|---|
| 総称ファミリ | sans-serif / sans / serif / monospace。具体の書体名ではなく様式を指す指定 |
| スロット | /etc/fonts/conf.d/ のファイル名の先頭の番号。fontconfig はこの番号順に設定を読む |
| metric 互換 | 字幅・行送りが元の書体と一致する代替の書体(Arial → Liberation Sans など) |
| 受け皿 | イメージに実在しない書体名を指定されたときに末尾へ足すフェイス |
| 要素 | 置き場所 | 責務 |
|---|---|---|
| フォントの規則 | containers/base/fonts-local.conf |
4 つの <alias>、未導入の書体の受け皿 1 つ、中国語・韓国語を明示した指定を守る 8 つの <match>。先頭のコメントに置き場所を動かせない理由を持つ |
| 規則の配置 | containers/base/Dockerfile の末尾の COPY 群 |
COPY --chmod=0644 fonts-local.conf /etc/fonts/local.conf。直後の RUN sudo fc-cache -f |
| 道具のパッケージ | containers/base/Dockerfile の 1 つ目の RUN の 1 回目の apt-get install |
metric 互換の 2 つと、文書を扱う 4 つ |
| 形の検査 | tests/containers/test_base_dockerfile_fonts.py |
Docker を起動せずに Dockerfile と fonts-local.conf の形を固定する |
| 解決先の検査 | tests/containers/test_base_image_font_matching.py |
建てたイメージの中の fc-match の解決先を固定する |
型(クラス)は持たない。設定ファイルと Dockerfile の命令だけで構成する。
規則は /etc/fonts/local.conf に置く。/etc/fonts/conf.d/ には 1 つも置かない。
/etc/fonts/fonts.conf は <include>conf.d</include> の 1 行しか持たず、conf.d を
ファイル名の番号順に読む。local.conf は conf.d/51-local.conf 経由で読まれる ── つまり
スロット 51 で、sans-serif を WenQuanYi Zen Hei(中国語)へ向けている
64-wqy-zenhei.conf と 65-nonlatin.conf より先である。
<prefer> は、一致した総称ファミリの直前へ挿入する(prepend)。そのため最も早く読まれた
<prefer> が先頭に残る。この設定が効くのは順序が後だからではなく、先だからである。
同じ内容を conf.d/99-devbase-fonts.conf へ置くと効かない。
| 置き場所 | fc-match sans-serif |
fc-match sans-serif:lang=ja |
|---|---|---|
/etc/fonts/local.conf |
Noto Sans CJK JP | Noto Sans CJK JP |
/etc/fonts/conf.d/99-devbase-fonts.conf |
WenQuanYi Zen Hei | WenQuanYi Zen Hei |
規則の種類によって順序への依存が違う。 99- へ置いた場合でも <match target="pattern">
(下の zh-cn / ko)は効いている。効かないのは <alias><prefer> だけである。部分的に直ったのを
見て置き場所の問題を見落とさないよう、この違いを fonts-local.conf の先頭のコメントにも残す。
conf.d/00-*.conf のように小さい番号を使う形は採らない。local.conf は fontconfig が
「システムの局所的な設定」のために用意した場所で、番号の付け替えで順序を取りに行くと他の
パッケージの番号と競合する。
利用者の上書きの口は塞がない。 conf.d/50-user.conf が ~/.config/fontconfig/fonts.conf
を読む。スロット 50 は 51-local.conf より先なので、利用者が自分の fonts.conf で別の
<prefer> を書けばそちらが勝つ。この性質は /etc/fonts/local.conf を選んだことの帰結で、
conf.d/00-*.conf を使っていれば利用者の設定より先に読まれて上書きを塞いでいた。
4 つの <alias> が、総称ファミリの先頭を日本語のフェイスにする。
| 総称ファミリ | <prefer> するフェイス |
|---|---|
sans-serif |
Noto Sans CJK JP |
sans |
Noto Sans CJK JP |
serif |
Noto Serif CJK JP |
monospace |
Noto Sans Mono CJK JP |
言語を明示しない中国語は、日本語の字形で描かれる。 lang を伴わない sans-serif は
どちらかの言語を選ばざるをえず、base の利用者は日本語話者で扱う文書も日本語が多いため、
日本語を既定に置く。中国語を明示した指定(lang=zh-cn)と、中国語の書体を名指しした指定
(WenQuanYi Zen Hei)は変わらない。Chromium は <html lang="zh-CN"> のページで lang を
載せる。
未導入の書体の受け皿は、条件を付けない <match target="pattern"> 1 つで足りる。
<match target="pattern">
<edit name="family" mode="append" binding="weak"><string>Noto Sans CJK JP</string></edit>
</match>append はすべてのパターンの末尾へ Noto Sans CJK JP を足し、弱い結合(binding="weak")
なので実在する指定を妨げない。Arial は 30-metric-aliases.conf(スロット 30、この設定より
先)の強い結合で Liberation Sans へ向くため、受け皿は末尾に付くだけで結果を変えない。
条件(<test>)を付けて「未導入のときだけ」足す形は採らない。fontconfig に「その書体が実在
するか」を問う <test> は無く、弱い結合がまさにその働きをする。
zh-cn / ko の <match> は、総称ファミリを名指ししたときだけ効かせる。<test name="lang">
に加えて <test name="family"> を必ず持つ。
| 総称ファミリ | lang=zh-cn で前置する |
lang=ko で前置する |
|---|---|---|
sans-serif |
Noto Sans CJK SC |
Noto Sans CJK KR |
sans |
Noto Sans CJK SC |
Noto Sans CJK KR |
serif |
Noto Serif CJK SC |
Noto Serif CJK KR |
monospace |
Noto Sans Mono CJK SC |
Noto Sans Mono CJK KR |
<test name="family"> を省いて lang だけを条件にすると壊れる。 binding="strong" の
前置は名指しの書体よりも強いため、lang=zh-cn が付いたすべてのパターン ── Arial や
Times New Roman を名指しした欧文の指定を含む ── から、指定した書体を奪う。
| 指定 | lang だけを条件にした場合 |
family + lang を条件にした場合 |
|---|---|---|
serif:lang=zh-cn |
Noto Sans CJK SC(様式が崩れる) | Noto Serif CJK SC |
monospace:lang=zh-cn |
Noto Sans CJK SC(等幅でなくなる) | Noto Sans Mono CJK SC |
Arial:lang=zh-cn |
Noto Sans CJK SC(欧文が奪われる) | Liberation Sans |
Times New Roman:lang=zh-cn |
Noto Sans CJK SC(同上) | Liberation Serif |
ko の規則を省く形も採らない。 zh-cn は OS 既定の 65-nonlatin.conf と
70-fonts-noto-cjk.conf が正しく扱うので規則が無くても合うが、ko は合わない ── 上の
<alias> が JP を先頭にするため、韓国語が日本語の字形になる。ko だけ書くと非対称で、なぜ
zh-cn が無いのかが後から読めないため、総称ファミリ 4 つ × 言語 2 つ = 8 つを並べて対称にする。
これらのフェイスは fonts-noto-cjk / fonts-noto-cjk-extra に既にあり、追加の導入を要さない。
建てた base イメージの中で fc-match が返すフェイス。
| 指定 | 解決先 |
|---|---|
sans-serif |
Noto Sans CJK JP |
sans-serif:lang=ja |
Noto Sans CJK JP |
sans |
Noto Sans CJK JP |
serif |
Noto Serif CJK JP |
monospace |
Noto Sans Mono CJK JP |
Noto Sans JP |
Noto Sans CJK JP |
Meiryo |
Noto Sans CJK JP |
Yu Gothic |
Noto Sans CJK JP |
MS PGothic |
Noto Sans CJK JP |
Zen Kaku Gothic New(イメージに無い) |
Noto Sans CJK JP |
Arial |
Liberation Sans |
Times New Roman |
Liberation Serif |
Courier New |
Liberation Mono |
Calibri |
Carlito |
Cambria |
Caladea |
sans-serif:lang=zh-cn |
Noto Sans CJK SC |
sans:lang=zh-cn |
Noto Sans CJK SC |
serif:lang=zh-cn |
Noto Serif CJK SC |
monospace:lang=zh-cn |
Noto Sans Mono CJK SC |
sans-serif:lang=ko |
Noto Sans CJK KR |
sans:lang=ko |
Noto Sans CJK KR |
serif:lang=ko |
Noto Serif CJK KR |
monospace:lang=ko |
Noto Sans Mono CJK KR |
Arial:lang=zh-cn |
Liberation Sans |
Arial:lang=ko |
Liberation Sans |
Times New Roman:lang=zh-cn |
Liberation Serif |
WenQuanYi Zen Hei(名指し) |
WenQuanYi Zen Hei |
IPAPGothic(名指し) |
IPAPGothic |
並び順を返す fc-match -s sans-serif:lang=ja も、1 件目が Noto Sans CJK JP になる
(2 件目以降に WenQuanYi Zen Hei / IPAPGothic が続く)。
欧文を名指しした 3 行(Arial:lang=zh-cn / Arial:lang=ko /
Times New Roman:lang=zh-cn)と、実在する書体を名指しした 2 行(WenQuanYi Zen Hei /
IPAPGothic)は、<test name="family"> を欠いた規則の壊れ方を捕まえる行である。
RUN sudo fc-cache -f は、COPY の直後かつPlaywright がフォントを入れる RUN より後に
置く。その RUN の npx playwright install --with-deps chromium が fonts-wqy-zenhei /
fonts-ipafont-gothic / fonts-liberation などを入れ、直後に ~/.cache を消す。前で走らせる
と、後から入った書体を知らないキャッシュが残る。
fc-cache は設定を反映するためのものではない(設定は照合のたびに読まれる)。走らせる
のは、~/.cache を消した後に /var/cache/fontconfig を作り直し、コンテナの初回起動時の
キャッシュ生成を避けるためである。
末尾へ置くことにはキャッシュの上の利点もある。fonts-local.conf を書き換えたとき、無効に
なるのは末尾の数層だけで、巨大な 1 つ目の RUN は建て直されない。
| 対象 | 理由 |
|---|---|
| LibreOffice | 展開 372〜459MB で、base の規律に見合わない |
pip / pip3 |
既にある uv / uvx で賄う |
ENV LANG |
設定するとコンテナの中のすべてのコマンドの出力・ソート順・日付の書式が変わり、影響がフォントの外へ出る。総称ファミリの解決先そのものを日本語にすれば lang のヒントは要らない |
fonts-wqy-zenhei の削除 |
削除しても OS 既定の 65-nonlatin.conf が sans-serif の prefer 一覧にこの書体を含むため日本語にはならず、中国語のページを豆腐にするだけになる。Dockerfile に導入の行は無く、Playwright が依存として入れる |
Dockerfile は fonts-wqy-zenhei を対象とする apt-get remove / apt-get purge / dpkg -r を
持たない。
同梱するパッケージは 6 つで、いずれも Ubuntu の標準のアーカイブにある。外部のリポジトリを
要さないため、1 つ目の RUN の1 回目の apt-get install の一覧へ、既にあるフォントの行の
隣に置く(2 回目の一覧は、後から足したリポジトリから取るもののためにある。どちらも同じ RUN
の中にありキャッシュの鍵は 1 つなので、置き場所でキャッシュの効き方は変わらない)。
| パッケージ | 何が使えるようになるか |
|---|---|
poppler-utils |
pdftoppm / pdftocairo(PDF を画像にする)、pdfinfo / pdffonts(ページ数・寸法・埋め込みフォントを調べる) |
python3-pil |
import PIL(画像の読み書き・変換) |
python3-lxml |
import lxml(OOXML の XML の読み書き) |
python3-defusedxml |
import defusedxml(外部実体を無効にした XML の解析) |
fonts-crosextra-carlito |
Calibri の metric 互換の実体 |
fonts-crosextra-caladea |
Cambria の metric 互換の実体 |
metric 互換の 2 つは、30-metric-aliases.conf が既に持っている対応の実体である。実体が
無いと Calibri / Cambria の指定は行き先を失い、中国語のフォントへ落ちる。
設定は Dockerfile のヒアドキュメントではなく独立したファイルにする。containers/base/ は
ビルドコンテキストそのものなので、ファイルを置けば COPY で届く(ai-cli-aliases.sh /
tmux.conf と同じ形)。独立したファイルにすると XML として検査でき、差分が読め、置き場所の
理由のコメントを長く書ける。
- 変更はイメージを建て直すまで反映されない。
devbase build base --no-cacheで base を 建て直し、使っている派生イメージ(general/go/php/php85/bi-tools/latex/trygroup。いずれもFROM devbase-base:latest)も建て直し、稼働中のコンテナはdevbase down→devbase upで作り直す。この 3 段はいずれも省けない devbase rebuildはここでは使えない。devbase build --expires=7のシノニム (lib/devbase/commands/container.pyのcmd_rebuild)で、イメージのビルドしか行わず コンテナを作り直さないうえ、期限内ならビルドそのものを飛ばす- 既定を戻したい利用者は、コンテナの中の
~/.config/fontconfig/fonts.conf(スロット 50)で 上書きできる。イメージを触る必要は無い containers/lfmはこの仕様の対象外である。FROM nvidia/cuda:...で base 由来ではなく、fonts-noto-cjkを自前で入れている(containers/lfm/Dockerfile)- 確かめてあるのは
fc-matchの水準までで、LibreOffice と Chromium での実際の描画は未検証 である。どちらも base に無く、LibreOffice は fontconfig とは別の照合も持つ - 解決先の表は arm64 で採ったものである。amd64 では
google-chrome-stableが追加で入るため--with-depsが入れるフォントの顔ぶれが違いうる。同じ表になるかは amd64 の端末で建てるまで 分からない
回帰テストは 2 段に分ける。Dockerfile の文字列検査だけでは足りない。 固定したいのは
「COPY の行があること」ではなく「fc-match sans-serif が日本語を返すこと」で、後者は文字列
からは分からない。<test name="family"> を欠いた壊れ方は、Dockerfile を読んでも見えない。
tests/containers/test_base_dockerfile_fonts.py(Docker を要さない):
- 6 パッケージが 1 回目の
apt-get installの一覧にあること。 COPYの宛先が/etc/fonts/local.confであり、conf.d/を宛先にするCOPYが無いこと。fc-cache -fが 1 度だけで、COPYより後かつ Playwright のRUNより後にあること。fonts-local.confが整形式の XML で、4 つの<alias>と 9 つの<match>(受け皿 1 つと、 総称ファミリ 4 つ × 言語 2 つの 8 つ)を持つこと。langの<match>がfamilyの<test>を必ず持つこと。- 先頭のコメントが置き場所の理由(
51-local.conf/conf.d/99)に触れていること。 libreoffice/soffice/pipを入れていないこと。fonts-wqy-zenheiを消す命令が無いこと。
tests/containers/test_base_image_font_matching.py(Docker を要する):
- 「解決先」の表の各行。
fc-match -s sans-serif:lang=jaの 1 件目。 pdftoppm/pdfinfo/pdffonts/pdftocairo/uvがPATHにあり、soffice/libreoffice/pip/pip3が無いこと。python3 -c "import PIL, defusedxml, lxml"が 0 で 終わること。docker runはセッションで 1 回に抑える。scope="session"の fixture が 1 つの スクリプトを走らせ、fc-match・command -v・importの結果をまとめて辞書で返す。
skip は 4 段で見る。shutil.which('docker') → docker info →
docker image inspect devbase-base:latest → イメージの中に /etc/fonts/local.conf があるか。
4 段目は、この設定より前に建てたイメージを持つ人が全員 pytest tests/ で赤くなるのを避ける
ためにある。 古いイメージを「失敗」として知らせると、赤の意味が「壊れている」と「イメージが
古い」で混ざる。skip の文言には devbase build base --no-cache を書き、建て直せば検査が効く
ようにする。Docker もイメージもある状態でスクリプトが失敗したときは skip せず、失敗として
知らせる。
CI はイメージを建てるジョブを持たないため、このテストは CI では常に skip になる。解決先の 証跡は手元で建てたイメージから採る。