Fontconfig (14日目) - キャッシュの改竄

あけましておめでとうございます。 今年はfontconfigをネタに書こうかなと思います。

ええ、はい……放置ごめんなさい。 連日何か書くのは辛くかつそろそろネタ切れのところで、動作が思い通りに行かなくてですね。 1日さぼったらずるずると……。 ようやく狙った動作をさせられるようになりました。 (どうせなら1月1日から再開したかったのに日付が変わってしまいました。)

フォントファイルの収録している言語

引き続きAsana Mathネタです。

ここまでAsan Mathを選ばせるためにFreeSerifをアンインストールしたり無効にしたりしてきましたが、そもそもAsana Mathがフォントパターンに挙げられてすらいないFreeSerifやDejaVu Math TeX Gyreに負けて選ばれない理由はund-zmthを収録していないからでした。

$ fc-query /usr/share/fonts/opentype/asana-math/Asana-Math.otf  | grep '\blang:'
        lang: aa|ay|bi|br|ch|da|de|el|en|es|eu|fj|fo|fur|fy|gd|gl|gv|ho|ia|id|ie|io|is|it|lb|mg|nb|nds|nl|nn|no|nr|oc|om|pt|rm|sma|smj|so|sq|ss|st|sv|sw|tl|ts|uz|vo|wa|xh|yap|zu|an|fil|ht|jv|kj|kwm|li|ms|ng|pap-an|pap-aw|rn|rw|sc|sg|sn|su|za(s)

あるフォントファイルが特定言語を収録しているとFontconfigにみなされる条件は fc-lang ディレクトリの各 *.orth ファイルに書かれています。 und-zmthであればこれだけの文字を全部収録している必要があります。

フォントファイルの側でもWindowsのコードページ等の単位で収録言語を自己申告できますがFontconfigはja、zh-ch、ko、zh-twの4つしか見てくれておらず、それも取り除く方向のみです。 つまり日本語の文字を全て収録している繁体字フォントであっても Windows-31J 用と自己申告されていなければjaを収録している扱いにはなりませんし、Windows-31J用と自己申告しているフォントであっても収録文字が足らないとやはりjaを収録している扱いにはなりません。(恐らく) 自己申告を全くしていないフォントであればja、zh-ch、ko、zh-twに関しても通常の判定が行われるようです。(多分)

実際問題、特定言語向けに作られているフォントファイルであっても実際にその言語の文字を全て網羅していることは少ないと思います。 使用頻度の低そうな漢字1文字が無いだけでCJKフォントじゃない扱いされたらふざけんなってなりますよね。 Fontconfigの *.orth ファイルは現存するフォントを誤判定しないよう恣意的に条件が設定されている節があってなんだかなあです。

というわけでFontconfigによる判定を上書きしてAsana Mathがund-zmthを収録していることにしてしまえば解決できるはずです。

フォントファイルとキャッシュファイルと設定ファイル

Fontconfigは毎回フォントファイルをスキャンしているわけではなく普段はキャッシュを用いています。 フォントファイルに対する判定を上書きできるとすればキャッシュファイル作成時点ということになります。

キャッシュファイルはシステムグローバルなものとユーザー毎のものがありました。 Debianパッケージを用いてインストールしたAsana Mathは /usr/share/fonts/ 以下にありますからグローバルなキャッシュに含まれます。 グローバルなキャッシュはroot権限で更新されますのでユーザー毎の設定ファイルは反映されません。 設定ファイルも /etc/fonts/conf.d/ に置く必要があります。

<match target="scan">

<match> タグの target アトリビュートにscanを指定することでキャッシュ作成時への介入になります。 (<match target="font"> では fc-match が返す結果には反映されますがFontconfigがフォントを選ぶ動作には影響を与えられません。)

<langset>

それで lang 属性を上書きするわけですが、これがリストそのままでもなければ普段表示されている | 区切り文字列でもなく <langset> タグを使った表記になります。

/etc/fonts/conf.d/99-userscan.conf
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
 <match target="scan">
  <test name="family" compare="eq">
   <string>Asana Math</string>
  </test>
  <edit name="lang">
   <langset>
    <string>en</string>
    <string>und-zmth</string>
    <!-- その他諸々は省略 -->
   </langset>
  </edit>
 </match>
</fontconfig>

動作するでしょうか……。

$ sudo fc-cache -rs
$ fc-list -v 'Asana Math' | grep '\blang:'
        lang: en|und-zmth(s)

うまくいきました!

何も変わらない場合はユーザー側でキャッシュされているかもしれませんので ~/.cache/fontconfig/ 以下を削除してみてください。

キャッシュの内容はおなじみ fc-match -v でも見ることができますがここでは fc-list を使っています。 fc-list は設定ファイルを処理しないようですので <match target="font"> の影響を考えなくて済みます。 本当はキャッシュの内容を見るのは fc-cat なのですがこちらは出力が見辛いので。

この <langset><plus><minus> で集合演算もできるようです。

Asana Mathはund-zmthこそ判定から漏れていますが上で見ましたように結構多くの言語の文字を収録していて、全部書くのは手間ですから省略してしまいました。 集合演算を使えば実際の値に追加できます。

/etc/fonts/conf.d/99-userscan.conf (修正)
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
 <match target="scan">
  <test name="family" compare="eq">
   <string>Asana Math</string>
  </test>
  <edit name="lang">
   <plus>
    <name>lang</name>
    <langset>
     <string>und-zmth</string>
    </langset>
   </plus>
  </edit>
 </match>
</fontconfig>
$ sudo fc-cache -sr
$ fc-list -v 'Asana Math' | grep '\blang:'
        lang: aa|ay|bi|br|ch|da|de|el|en|es|eu|fj|fo|fur|fy|gd|gl|gv|ho|ia|id|ie|io|is|it|lb|mg|nb|nds|nl|nn|no|nr|oc|om|pt|rm|sma|smj|so|sq|ss|st|sv|sw|tl|ts|uz|vo|wa|xh|yap|zu|an|fil|ht|jv|kj|kwm|li|ms|ng|pap-an|pap-aw|rn|rw|sc|sg|sn|su|za|und-zmth(s)

動作確認

上手くキャッシュを上書きできましたら12日目の例に戻って確認してみましょう。 XITS Mathが存在しない場合のフォールバックとしてAsana Mathが選ばれるはずなのでした。

もしFreeSerifをアンインストールしていればインストールしなおして、もし無効化の設定(13日目の 10-reject.conf)をしていれば消しておいてください。

$ LANG=ja fc-match -s 'XITS Math' | head
NotoSansCJK-Regular.ttc: "Noto Sans CJK JP" "Regular"
Asana-Math.otf: "Asana Math" "Regular"
NotoSansMath-Regular.ttf: "Noto Sans Math" "Regular"
Ubuntu-R.ttf: "Ubuntu" "Regular"
Ubuntu-Th.ttf: "Ubuntu" "Thin"
NotoSans-Regular.ttf: "Noto Sans" "Regular"
NotoSansArabic-Regular.ttf: "Noto Sans Arabic" "Regular"
NotoSansThai-Regular.ttf: "Noto Sans Thai" "Regular"
NotoSansDevanagari-Regular.ttf: "Noto Sans Devanagari" "Regular"
NotoSansTamil-Regular.ttf: "Noto Sans Tamil" "Regular"
$ LANG=en fc-match -s 'XITS Math' | head
Asana-Math.otf: "Asana Math" "Regular"
FreeSerif.ttf: "FreeSerif" "Regular"
NotoSansMath-Regular.ttf: "Noto Sans Math" "Regular"
Ubuntu-R.ttf: "Ubuntu" "Regular"
Ubuntu-Th.ttf: "Ubuntu" "Thin"
NotoSans-Regular.ttf: "Noto Sans" "Regular"
NotoSansCJK-Regular.ttc: "Noto Sans CJK SC" "Regular"
NotoSansArabic-Regular.ttf: "Noto Sans Arabic" "Regular"
NotoSansDevanagari-Regular.ttf: "Noto Sans Devanagari" "Regular"
NotoSansTamil-Regular.ttf: "Noto Sans Tamil" "Regular"

ついに……ついにAsana MathがFreeSerifに打ち勝ちました!!

長かった……。

正直言ってスキャンに手を出す気はありませんでした。 普通の用途では必要ないと思ってました。 しかし、うまいこと丁度いい問題に出くわすものですね……。

lang だけではなく weight の調整等にも応用できると思います。 Light、Regular、Boldが用意されているフォントファミリーでRegularが太すぎる場合にLightをRegularにすることができたら嬉しい場面ありそうですよね。