Javascript+CSS でフォントが有効か検査する

2020/01/22, 01/29, 2021/06/18 山田 泰司

この技術は諸刃の剣かもしれません。あるフォントがシステムにインストールされているか検査出来ると、オペレーティングシステムの種類・バージョンが推測できてしまいます。現に、そうした技術の援用でクラックなどに使われているようです。

しかし、既に Adobe からそうした目的にも使えるフォントがオープンソースでリリース済みの現在、隠し立てすることの方がデメリットが大きいので、ここでは単に技術的な見地でこの手法を紹介します。その手法は既に公知ですが、私なりに実用的になるようにアレンジしています。

AdobeBlank フォントの準備、改め、GetaBlank フォントの準備

Adobe はオープンソースの取り組みとして様々な有用なフォントを公開しています。その一つに、AdobeBlank と名付けられた各種フォント形式、他に AND(Adobe NotDef) フォントなど特殊な用途のフォントが github.com/adobe-fonts/ にて公開されています。

これを HTML で使うには head タグ内に以下の CSS を記述します。

<style>
@font-face {
  font-family: 'AdobeBlank';
  src: url("data:font/woff;base64,ここに
`curl -L 'https://github.com/adobe-fonts/adobe-blank/blob/master/AdobeBlank.otf.woff?raw=true' | base64 | pbcopy`
のように base64 エンコードした AdobeBlank.otf.woff のコピーをペースト");
}
</style>

ちなみに、font/opentype ではなく font/woff にしたので半分以下のサイズに抑えられているものの、約 10KB とサイズが大きいので SVG フォントで代替したいところですが、成功していません。

このフォントでもいいのですが、やはりサイズが大きいので、検査用の最小限の Unicode ポイントのグリフの幅をゼロにしたフォントを GetaBlank.woff または GetaBlank.otf を用意しました。これは GetaBlank.svg フォントから FontForge で変換したもので、下駄記号「〓」および等号「=」のみのコードポイントを持ちます。

これを HTML で使うには head タグ内に以下の CSS を記述します。

<style>
@font-face {
  font-family: 'GetaBlank';
  src: url("data:font/woff;base64,ここに
`curl -L 'https://www.aihara.co.jp/~taiji/browser-security/js/GetaBlank.woff' | base64 | pbcopy`
のように base64 エンコードした GetaBlank.woff のコピーをペースト");
}
</style>

または、

<style>
@font-face {
  font-family: 'GetaBlank';
  src: url("data:font/opentype;base64,ここに
`curl -L 'https://www.aihara.co.jp/~taiji/browser-security/js/GetaBlank.otf' | base64 | pbcopy`
のように base64 エンコードした GetaBlank.otf のコピーをペースト");
}
</style>

これは、約 1 KB とサイズが小さいので使いでがよろしいかと思われます。ページの公開元に置くなら、GetaBlank.css の方が良いでしょう。

検査用 Javascript コード

検査用の関数の定義

head タグ内の script タグ内で以下の関数を定義しておきます。

function is_installed_ja_font_family(names)
{
  const name_list = names.map(v=>v.replace(/^([^']*)$/, '"$1"').replace(/^'(.*)'$/, '$1')).join(', ');
  //const string = '=';
  const string = '〓';
  const span_tag = document.createElement('span');
  //span_tag.style.fontFamily = `${name_list}, AdobeBlank`;
  span_tag.style.fontFamily = `${name_list}, GetaBlank`;
  span_tag.innerHTML = `${string}`;
  document.body.appendChild(span_tag);
  const is_not_blank = span_tag.offsetWidth > .0;
  document.body.removeChild(span_tag);
  return is_not_blank;
}

指定するフォントのフォールバック AdobeBlank GetaBlank において下駄印「〓」を表示してみて、幅がゼロより大きければ、指定したフォントが有効ということになります。AdobeBlank もしくは GetaBlank をフォールバックとして指定しないと Firefox, Safari, Chrome において、こういった判定はできないようです。

検査用の関数の実行

検査用の関数は必ず、ドキュメントの読み込みが完了した後に行います。次の例は、MS Windows, Apple macOS, ubuntu GNU/Linux, Solaris それぞれでインストール済みが期待されるフォント名を検査しています。

<script>
window.addEventListener('load', function () {
for (const font_names of [
  [ '游ゴシック',	'Yu Gothic' ],	// windows
  [ '游ゴシック体',	'YuGothic' ],	// macOS
  [ 'Noto Sans CJK JP' ],		// ubuntu
  [ 'HeiseiMin' ],			// solaris
]) {
  const is_installed = is_installed_ja_font_family(font_names);
  if (is_installed) {
    console.log(font_names.join(', '));
    // 有効なフォント名の場合の処理
  }
}
});
</script>

検査結果の例

例えば、手元の Windows だと以下のような結果がブラウザのコンソールに印字されます。

游ゴシック, Yu Gothic

ユーザ試料の検査

ユーザ試料の日本語フォント検査

フォントが無効(「〓」が未定義)なら以下にフォント名が赤で印字されます。そして、グリフがない場合未定義が 🅇 となる AND-Regular フォントで代替されます。

tab () か enter () で実行

このように好みのフォントファミリ名で、下記で行っているような試験結果を表示できます。

ユーザ試料のラテンフォント検査

フォントが無効(「=」が未定義)なら以下にフォント名が赤で印字されます。そして、グリフがない場合未定義が 🅇 となる AND-Regular フォントで代替されます。

tab () か enter () で実行

このように好みのフォントファミリ名で、下記で行っているような試験結果を表示できます。

既定の試料の検査結果

フォントが無効(「〓」が未定義)なら以下にフォント名が赤で印字されます。そして、グリフがない場合未定義が 🅇 となる AND-Regular フォントで代替されます。また、ウェブフォントなどの確実な判定には至らないときもあります。

あくまで日本語フォントとして利用が見込めるかをたった一文字「〓」から判断しているので、単にフォントが存在するかであればイコール記号「=」が未定義かを調べれば十分かと思います。ちなみに、なぜゲタ「〓」を選んだのかと言えば、日本にはある環境で印字できない文字を表すとき、このゲタが使われる独特な慣わしがあり、かつ JIS X 0208 定義の文字だからです。より新しい JIS X 0213 対応日本語フォントであれば「〼」あたりを判定基準にしてもよいでしょう。

おわりに

AdobeBlank フォントで試みが成功して、のちに拙作 GetaBlank で成功して、しかし、ここの記事としては(フォントの MIME タイプを別のを試したりして)中途半端な状態で一年以上放置してしまっていたようです。もし、読者に混乱を与えたなら申し訳ありません。現状の記事は少なくともキリのよい状態にはなっています。

以下、他所の記事の転載ですが、ちょっとした実験で、本文と関係ありません。すいません。

table[border] の廃止と代替スタイルについて

ここの話題は CSS 変数(カスタムプロパティ)とその寿命に関係する。

さて、table[border] つまり、テーブルの border 属性が HTML Live Standard で廃止ということで、ブラウザが事実上サポートを続けるかもしれないが、準備はしておきたいと思った。

本節の末尾で紹介するクラス付きテーブルスタイル table.border を使うと、border 属性付きテーブルとほぼ等価な見栄えになる。左がブラウザ既定、右がこのスタイルによる実現である。

table border
0,00,10,20,30,40,50,60,7
1,01,11,21,31,41,5
2,02,12,22,32,42,5
table.border
0,00,10,20,30,40,50,60,7
1,01,11,21,31,41,5
2,02,12,22,32,42,5

これで実用的には十分なのかもしれないが、多少汎用性を持たせよう。border="2" と指定したときのようにスタイルファイルで見栄えを実現する。そのために CSS 変数を活用してみよう。インラインスタイル定義としてテーブルの属性 style="--table-border-width: 2px;" を付記すればよい。左がブラウザ既定、右がこのスタイルによる実現である。

table border="2"
0,00,10,20,30,40,50,60,7
1,01,11,21,31,41,5
2,02,12,22,32,42,5
table.border 2px
0,00,10,20,30,40,50,60,7
1,01,11,21,31,41,5
2,02,12,22,32,42,5

Firefox, Chrome, Safari それぞれが異なるのでまったく同じにはならないが、むしろより自然な様相になってると思う。

さて、せっかくここまで再現できたので、少し調子に乗って他によく使われる table[border][rules='all'] だけはサポートしておこうと思った。

本節の末尾で紹介するクラス付きテーブルスタイル table.border_rules-all を使うと、border, rules='all' 属性付きテーブルとほぼ等価な見栄えになる。左がブラウザ既定、右がこのスタイルによる実現である。

table border rules="all"
0,00,10,20,30,40,50,60,7
1,01,11,21,31,41,5
2,02,12,22,32,42,5
table.border_rules-all
0,00,10,20,30,40,50,60,7
1,01,11,21,31,41,5
2,02,12,22,32,42,5

素晴らしい!実用上、これらの属性(但し、rules='all' のときのみ)がブラウザで廃止されても困らない。

一応、border="5" としたときの対応として、インラインスタイル定義としてテーブルの属性 style="--table-border-width: 5px;" を付記したものも実現しておこう。

table border="5" rules="all"
0,00,10,20,30,40,50,60,7
1,01,11,21,31,41,5
2,02,12,22,32,42,5
table.border_rules-all 5px
0,00,10,20,30,40,50,60,7
1,01,11,21,31,41,5
2,02,12,22,32,42,5

rules='all' 以外のクラスもいずれ作成しておくかもしれない。

さて、ここで style="--table-border-width: 5px;" などを付記しているので、万が一次のテーブルで枠線が太いままだったら困る。最初の例の表をそのまま載せてみる。

table border
0,00,10,20,30,40,50,60,7
1,01,11,21,31,41,5
2,02,12,22,32,42,5
table.border
0,00,10,20,30,40,50,60,7
1,01,11,21,31,41,5
2,02,12,22,32,42,5

流石に変数が上書きされてしまうようなことはない。からくりは CSS 変数はスタイルの継承によって参照・定義されるので、この場合、これらは要素の継承関係にはないので、意図しない変数の変更には至らない。至極当たり前のことなのだが、実際に確認してみて納得できた次第である。

では、最後にこのスタイルファイルを紹介しておく。

table.border {
  border-color: darkgray;
  border-style: outset;
  border-width: var(--table-border-width, 1px);
  border-collapse: separate;
  border-spacing: var(--table-border-spacing, 2px);
}
table.border > :is(thead, tbody, tfoot) > tr > :is(th, td) {
  border-color: darkgray;
  border-style: inset;
  border-width: var(--table-td-border-width, 1px);
}
table.border_rules-all {
  border-style: outset;
  border-width: var(--table-border-width, 1px);
  border-collapse: collapse;
}
table.border_rules-all > :is(thead, tbody, tfoot) > tr > :is(th, td) {
  border-style: solid;
  border-width: var(--table-td-border-width, 1px);
}

但し、SeaMonkey ではセレクタ :is() が働かないので多少工夫を要するのだが、時間が解決する問題だと思うのでそのままにしておく。

Written by Taiji Yamada <taiji@aihara.co.jp>