スキップしてメイン コンテンツに移動

svgo を使って SVG フォントを SVG 画像に変換する

はじめに

このブログなどで使用するアイコンを Pure CSS アイコンからベクターアイコンへ置き換えを進めています。新しいベクターアイコンでは、Web フォントと、フォールバックとして JavaScript で SVG を挿入する二段構えとしました。

Web フォントが適用されているか? の検出に関する記事は「Webフォントがブラウザで有効か?きっちり調べる」を参照ください。

フォールバックの恩恵を享ける環境、つまり SVG に対応するが Web フォントに非対応の環境は Gecko 1.8.1~1.9.0, Opera 9.x, Safari 1.3.2~1.3.3, Safari 2.0.4+ です。SVG Tiny をサポートする PC 版 Opera 8.xの為には、何らかの手段でパスデータの変更が必要です。(追記 2023/08/13)

インライン SVG について

いつの間にか見かけるようになった、HTML への SVG アイコンの直接記述(インライン SVG)は、HTML が大きくなってしまうので採用しませんでした。Web サーバへのリクエスト数は抑えられますが、SVG に非対応な、つまり非力なブラウザにも無駄に SVG 文字列をロードさせてしまうのはイケていませんね。

youtube.com のスクリーンショット。Firefox でスタイルシート使用しないにチェック。
twitter.com のスクリーンショットの比較。Firefox でスタイルシート使用しないにチェック。

ところで、この実装をしているサイトの中には、CSS が読み込まれないと、巨大なアイコンが無様に表示されてしまうものがあります。

インライン SVG の <svg> 要素には、きちんと width, height を設定しておきたいところです。

SVG フォントから SVG 画像を用意する

さて、このフォールバックの SVG 画像ですが、Web フォントの SVG から作ろうとすると上下に反転して上方向にズレた画像になってしまいます。これを svgo で transform してあげる方法をご紹介致します。

併せて1024x1024の論理サイズの viewPort を255x255に変更して、パスデータの少数を丸めてファイルサイズを減らします。ネットワークにも端末のリソースにも優しくなりますね。

web-doc-base ではここから更に、パス文字列だけを取り出して、unicode を key にするオブジェクトに格納しています。そして Web フォントに非対応でも SVG をサポートするブラウザに対して、SVG アイコンを挿入します。

Illustrator と Glyphs を使用すれば特に問題はないですが、Python を使った方法だと、上下が反転しまします。これは SVG が下向きプラスでグリフが上向きプラスだからです。これを修正してあげる必要があります。

node.js 用コード

const fs     = require( 'fs' );
const parser = require( 'xml2js' ).Parser();
const svgo   = require( 'svgo' );

fs.readFile(
  './.icomoon/fonts/web-doc-base.svg',
  function( error, buffer ){
    if( error ) throw error;

    parser.parseString(
      buffer,
      function( err, result ){
        if( err ) throw err;

        const font      = result.svg.defs[ 0 ].font[ 0 ];
        const fontFace  = font[ 'font-face' ][ 0 ].$;
        const width     = fontFace[ 'units-per-em' ];
        const ascent    = fontFace[ 'ascent' ];
        const descent   = fontFace[ 'descent' ];
        const glyphList = font.glyph;
        const glyphMap  = {};

        for( let i = 0, l = glyphList.length; i < l; ++i ){
          const glyph = glyphList[ i ].$;
          // 文字列を1文字ずつ配列化(サロゲートペアを考慮)
          //   https://qiita.com/sounisi5011/items/aa2d747322aad4850fe7
          // 合字を除外しています
          if( glyph.d && glyph.unicode && Array.from( glyph.unicode ).length === 1 ){
            glyphMap[ glyph.unicode ] = createOptimaizedPath( glyph.d, width, ascent, descent );
          };
        };
        fs.writeFileSync(
          './src/js-vector-icon-compat/vectorIconPathList.generated.js',
          Buffer.from(
            '// THIS SCRIPT IS GENERATED BY "gulp ico". DO NOT EDIT!\n' +
            'var VectorIconCompat_PATH_LIST = \n' + JSON.stringify( glyphMap, null, '  ' ) + ';'
          )
        );
      }
    );
  }
);

function createOptimaizedPath( path, width, ascent, descent ){
  const height = 255;
  const scale  = height / 1024;
  const result = svgo.optimize(
    '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0,0,' + width * scale + ',' + height + '">' +
      '<g transform="matrix(' + scale + ', 0, 0, -' + scale + ', 0, ' + ascent * scale + ')">' +
        '<path d="' + path + '"/>' +
      '</g>' +
    '</svg>',
    {
      plugins : [
        {
          name   : 'preset-default',
          params : {
            overrides : {
              convertPathData : { floatPrecision : 0 }
            }
          }
        }
      ]
    }
  );
  return result.data.split( ' d="' )[ 1 ].split( '"' )[ 0 ];
};

ポイント

  1. SVG フォントの JavaScript オブジェクトへの変換には、xml2js を使っています。
  2. svgo は 3.0.2 を使用しました。
  3. svgo の transform 属性をパスに apply するオプションはデフォルトで有効です。svgo / apply transforms to Path pata
  4. style="transform: ..." は無視されます。transform 属性を使います。
  5. matrix の各値は「SVG ⇄ グリフ をするときの座標変換」を参考にしました。
  6. SVG フォントから ascent を取得してズラしてあげます。