Windows・Chrome で游ゴシックフォントを少しだけ太く見せる JavaScript と CSS

以前、「ウェブサイトに適用する游ゴシックフォントを見直しまくった最終解」という記事を書いた。

おかげさまで、はてブで77ブクマ (本稿執筆時点) いただき、皆様もこの件について苦労なさっていることが伺い知れた。

今回はこの続報。Windows・Chrome 環境で、游ゴシックフォントが適用されている場合のみ、フォントを少しだけ太く表示させる JavaScript と CSS を作ったので紹介する。開発者が自分のサイトに組み込んで調整いただけるだろう。

目次

前回のおさらい

「Windows の游ゴシックフォントが細くて見づらい問題」の概要と、前回の記事で紹介した対処法をおさらいする。

問題の概要

ウェブサイト開発者ができる対処法

/* Chrome でのみフォントを太めに表示する */
@media screen and (-webkit-min-device-pixel-ratio: 0) {
  * {
    text-shadow: transparent 0 0 0, rgba(0, 0, 0, .7) 0 0 0 !important;
  }
}

クライアントサイドでできる対処法


おさらいココまで。

text-shadow ではなく -webkit-text-stroke-width を使う

途中で紹介した text-shadow による太字化は、若干レンダリング結果が汚く見えてしまう。

そこで調べていると、-webkit-text-stroke-width という、WebKit 向けのプロパティを使うテクニックを見つけた。

body {
  -webkit-text-stroke-width: .4px !important;
}

コレがなかなかキレイに見える。要素に指定した color を利用して上手く縁取りしてくれるので、text-shadow のように文字色が濁ったりはしない。クライアントサイドの拡張機能で text-shadow 指定を使っていた人は、よりキレイに見える -webkit-text-stroke-width 指定に切り替えることをオススメする。

.4px (= 0.4px) 部分の指定を少しだけ増やせば、もう少し太く見えるが、やりすぎは禁物。

text-shadow 指定とどのくらい違いがあるのかは、以下のデモページで確認できるだろう。

ついでに、各ブラウザで text-shadow 指定と -webkit-text-stroke-width 指定とを比較してみた。text-shadow 指定はギザギザしがちなのに対して、-webkit-text-stroke-width はもう少しなめらかに表示される。

このとおり、-webkit-text-stroke-width はなかなか太く・キレイに見えるのだが、このような指定をしなくても普通に見えている Firefox や、なぜか -webkit なプロパティを認識できる Edge でも有効になってしまう他、游ゴシックフォント以外に適用すると今度は太すぎて文字が潰れて見えてしまう。

ということは、この CSS を適用する OS・ブラウザを特定し、さらに「実際に游ゴシックフォントでレンダリングされているのかどうか」を調べてからでないと、-webkit-text-stroke-width を指定して良いかどうか、判断できないということだ。

window.getComputedStyle() では使われているフォントが分からない

OS・ブラウザ判定は、navigator.userAgent の文字列を見ていけば分かりそうだが、実際に使われているフォント名を知るのは、一筋縄では行かない。

以前紹介したが、DOM 要素に実際に指定されているスタイル定義を取得できる、window.getComputedStyle() という API がある。しかし、font-family については、この API を使っても正確な情報が分からないのだ。

どういうことか確認できるサンプルを用意した。

上のデモページの「チェック 1」ボタンを押下すると、window.getComputedStyle() を使って、ある要素の font-family 値を取得する。対象の要素の CSS は、font-family: "デタラメゴシック", "游ゴシック"; と指定されており、window.getComputedStyle()"デタラメゴシック", "游ゴシック" という文字列を取得してしまう。

もちろん、「デタラメゴシック」なんてフォントは存在しない。それなのにこのフォント名が取得できてしまうし、「游ゴシック」という文字列も一緒に取れてしまう。それぞれのフォントが実際に使われてレンダリングされているのかどうかは、window.getComputedStyle() では分からないのだ。

canvas 要素を使ってフォントレンダリングをチェックする

そこで見つけたのが、canvas 要素にそのフォント名を使ってテキストを描画し、ピクセル単位でレンダリング状況をチェックするというコード。

少し今っぽいコードに調整したのが以下。

/**
 * 指定の要素に実際に適用されているフォント名を特定する
 * 
 * https://stackoverflow.com/a/38910481
 * 
 * @param {*} element DOM 要素
 * @return {string} フォント名
 */
function detectFontName(element) {
  // スタイル未指定時のブラウザデフォルトフォント名を取得する
  const detectDefaultFonts = () => {
    const iframe = document.createElement('iframe');
    document.body.appendChild(iframe);
    iframe.contentWindow.document.open();
    iframe.contentWindow.document.write('<html><body>');
    const subElement = iframe.contentWindow.document.createElement(element.tagName);
    iframe.contentWindow.document.body.appendChild(subElement);
    const defaultFonts = getComputedStyle(subElement)['font-family'];
    document.body.removeChild(iframe);
    return defaultFonts;
  };
  
  const fonts = getComputedStyle(element)['font-family'] + ',' + detectDefaultFonts();
  const fontsArray = fonts.split(',');
  const canvas = document.createElement('canvas');
  const context = canvas.getContext("2d");
  const testString = "abcdefghijklmnopqrstuvwxyz!@#$%^&*()ñ";
  let prevImageData;
  document.body.appendChild(canvas);
  canvas.width = 500;
  canvas.height = 300;
  fontsArray.unshift('"Font That Doesnt Exists ' + Math.random() + '"');
  
  for(let i = 0; i < fontsArray.length; i++) {
    const fontName = fontsArray[i].trim();
    context.clearRect(0, 0, canvas.width, canvas.height);
    context.font = '16px ' + fontName + ', monospace';
    context.fillText(testString, 10, 100);
    const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
    const data = imageData.data;
    if(prevImageData) {
      for(let j = 0; j < data.length; j += 3) {
        if(prevImageData[j + 3] !== data[j + 3]) {
          document.body.removeChild(canvas);
          return fontName;
        }
      }
    }
    prevImageData = data;
  }
  
  document.body.removeChild(canvas);
  return 'monospace';
}

使い方は以下のとおり。

/* こんな CSS 指定があったとして */
.example {
  font-family: "デタラメゴシック", "游ゴシック", sans-serif;
}
<!-- こんな要素が配置されていたとして -->
<div class="example">テキストテキスト</div>
// 前述の関数を以下のように呼び出す
const targetElement = document.querySelector('.example');
const actualFontName = detectFontName(targetElement);
// → '游ゴシック' と取得できる

このように、「デタラメゴシック」という指定は効いていないので無効、次の「游ゴシック」という指定はちゃんとフォントが変わって適用されているので、このフォントが有効だろう、と上手く判断できている。

このコードも完璧ではなく、対象の要素の CSS 指定によっては、sans-serif など総称ファミリが返ってくる場合もあるし、完璧ではないのだが、「実際に游ゴシックでレンダリングされているか否か」を判断するシチュエーションに限れば、今のところ特に問題はない。

コードを見て分かるとおり、canvas 要素や iframe 要素を埋め込んで各種判定をするので、DOM を一時的に汚す点はご容赦。

UA から OS とブラウザを判定する

さて、実際に游ゴシックでレンダリングされているのかどうかは、以上の方法で調べられそうだ。あとは OS とブラウザ判定である。

お手軽なのは ua-parser-js などの npm パッケージを使う方法だが、navigator.userAgent から取得した文字列から String.prototype.match() で判定するだけでも十分だ。

function bolderYuGothicOnWindowsChrome() {
  // User-Agent 文字列を取得する
  const ua = navigator.userAgent;
  
  // Windows かどうかの判定
  const isWindows  = ua.match(/Windows/);
  // Chrome ブラウザかどうかの判定
  const isChrome   = ua.match(/Chrome/) && !ua.match(/Edge/);
  
  // 前述の関数を使って、body 要素に「Yu Gothic」か「游ゴシック」(いずれも Windows 向けの游ゴシックフォント名) が適用されているか判定する
  const fontName = detectFontName(document.body);
  const isYuGothic = fontName.match(/Yu Gothic/) || fontName.match(/游ゴシック/);
  
  // Windows・Chrome ブラウザで游ゴシックフォントが使われていれば、文字を太くする
  if(isWindows && isChrome && isYuGothic) {
    // 文字を太くするためのコード…
  }

Chrome ブラウザかどうかの判定がちょっと変わっているところ。実は、Edge のユーザエージェント文字列は、次のようになっている。

Mozilla/5.0 (Windows NT 10.0; Win64; x64; ServiceUI 13.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134

なんと、Edge ブラウザの UA なのに、AppleWebKit やら Chrome やら Safari やらという文言が登場する。コレは、WebKit 系のブラウザと互換性を持たせるための策らしいが、なんとも鬱陶しい。

とりあえず、コレで、クライアント環境を「Windows かつ Chrome ブラウザかつ、指定の要素に游ゴシックフォントが適用されていること」という条件で絞り込むことができた。

Windows・Chrome・游ゴシック適用済の場合のみ、フォントを少し太くするコード

ということで、次のコードを突っ込めば、Windows Chrome で游ゴシックフォントが使われている時のみ、-webkit-text-stroke-width プロパティを利用して文字を少し太く表示することができるようになる。

/* ページ全体には游ゴシックを指定しておく */
body {
  font-family: YuGothic, "游ゴシック体", "Yu Gothic", "游ゴシック", sans-serif;
}

/* 等幅フォント指定など… */
code, kbd, samp, var, pre, textarea {
  font-family: MeiryoKe_Gothic, "Ricty Diminished", Osaka-mono, "MS Gothic", "Courier New", monospace;
}
document.addEventListener('DOMContentLoaded', () => {
  // Windows・Chrome で游ゴシックを指定していれば太く見せる
  bolderYuGothicOnWindowsChrome();
});

/**
 * Windows・Chrome で游ゴシックを指定している場合に
 * -webkit-text-stroke-width を指定してフォントを少し太くする
 * 
 * OS・ブラウザ・body 要素に指定しているフォントが条件に合致しなかった場合は何もしない
 */
function bolderYuGothicOnWindowsChrome() {
  const ua = navigator.userAgent;
  
  const isWindows  = ua.match(/Windows/);
  const isChrome   = ua.match(/Chrome/) && !ua.match(/Edge/);
  
  // Windows 環境ではないか、Chrome ブラウザでない場合は処理を中止する
  if(!isWindows || !isChrome) {
    return;
  }
  
  const fontName = detectFontName(document.body);  // body 要素に游ゴシックが適用されているかどうかで判定する
  const isYuGothic = fontName.match(/Yu Gothic/) || fontName.match(/游ゴシック/);
  
  // 游ゴシックフォントが適用されていないようであれば太字化しないで終了する
  if(!isYuGothic) {
    return;
  }
  
  const bolderStyle = `
    <style id="bolder-style">
      /* 太字化する */
      body {
        -webkit-text-stroke-width: .4px;
      }
      
      /* 等幅フォントなど、游ゴシックを使わない要素には指定しない (この辺の調整はお好みで…) */
      code, kbd, samp, var, pre, textarea {
        -webkit-text-stroke-width: 0;
      }
    </style>
  `;
  // head 要素の末尾に style 要素を追加する
  document.getElementsByTagName('head')[0].insertAdjacentHTML('beforeend', bolderStyle);
}

/**
 * 指定の要素に実際に適用されているフォント名を特定する
 * 
 * https://stackoverflow.com/a/38910481
 * 
 * @param {*} element DOM 要素
 * @return {string} フォント名
 */
function detectFontName(element) {
  // スタイル未指定時のブラウザデフォルトフォント名を取得する
  const detectDefaultFonts = () => {
    const iframe = document.createElement('iframe');
    document.body.appendChild(iframe);
    iframe.contentWindow.document.open();
    iframe.contentWindow.document.write('<html><body>');
    const subElement = iframe.contentWindow.document.createElement(element.tagName);
    iframe.contentWindow.document.body.appendChild(subElement);
    const defaultFonts = getComputedStyle(subElement)['font-family'];
    document.body.removeChild(iframe);
    return defaultFonts;
  };
  
  const fonts = getComputedStyle(element)['font-family'] + ',' + detectDefaultFonts();
  const fontsArray = fonts.split(',');
  const canvas = document.createElement('canvas');
  const context = canvas.getContext("2d");
  const testString = "abcdefghijklmnopqrstuvwxyz!@#$%^&*()ñ";
  let prevImageData;
  document.body.appendChild(canvas);
  canvas.width = 500;
  canvas.height = 300;
  fontsArray.unshift('"Font That Doesnt Exists ' + Math.random() + '"');
  
  for(let i = 0; i < fontsArray.length; i++) {
    const fontName = fontsArray[i].trim();
    context.clearRect(0, 0, canvas.width, canvas.height);
    context.font = '16px ' + fontName + ', monospace';
    context.fillText(testString, 10, 100);
    const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
    const data = imageData.data;
    if(prevImageData) {
      for(let j = 0; j < data.length; j += 3) {
        if(prevImageData[j + 3] !== data[j + 3]) {
          document.body.removeChild(canvas);
          return fontName;
        }
      }
    }
    prevImageData = data;
  }
  
  document.body.removeChild(canvas);
  return 'monospace';
}

実際にこのようなコードを埋め込んだサンプルを以下に作った。「有効にする」ボタンで効き目をトグルできるので、確認してほしい (Windows マシンで、Chrome ブラウザ上で確認すること)。游ゴシックフォントが使われていない pre 要素は、ボタンを押しても文字が太くならないことを確認できるだろう。

以上

結局、JavaScript を組み合わせないとうまいことフォントの判定ができなかったが、ココまで組み込めば、游ゴシックフォントが細く見えてしまう Windows 環境向けに、かなり良い感じの調整を効かせることはできた。

今回は特に細く見えてしまう Chrome ブラウザに絞ったが、UA 判定を調整すれば IE でも Edge でも Firefox でも適用することはできる。-webkit-text-stroke-width で指定する数値についても、お好みで太さを調整してもらえればと思う。