JavaScriptでフォントファイルを解析する その3 文字のSVGソースを作成し、SVG画像を表示する

Author:

文字列のSVGデータを取得する

フォントファイルを解析して取得した文字の座標データを元にSVGを作成します。
フォントファイル内の座標データを表の形式で確認したい場合は、文字のGlyph IDおよび座標データを取得するを使用してください。
座標データは、SVGの直線コマンド(L)および2次元ベジェコマンド(Q)を使用して文字のアウトラインを作成することができます。通過点(ON_CURVE_POINT)ではない座標は、2次元ベジェの制御点となります。
データ量を削減するため、2次元ベジェにおいて通過点が省略されることがあります😮。その場合は、連続する制御点の中間点を通過点として扱います🥳。

完成例

入力1: 文字列
入力2: フォントファイル

出力: 解析結果

使い方

SVGで表示したい文字を「入力1: 文字列」のテキストボックスに入力します。
「ファイルを選択」ボタンをクリックして、フォントファイル(.ttfまたは.ttc)を開きます。
フォントファイルを開くイベントをきっかけにJavaScriptが動き、「出力: 解析結果」以下にSVGのソースおよびプレビュー画像が表示されます🤗。
(SVGが表示されるのは、Simple Glyph Descriptionのみとなります。)

動作確認は、BIZ UDPゴシックで行っています。GitHubおよびGoogle Fontから無料で入手することができます。
BIZ UDPゴシックのライセンスは、「SIL Open Font License 1.1」(OFL-1.1)で個人利用・商用にかかわらず無償で利用できます。

ソース

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>フォントファイルを解析するJavaScript</title>
  </head>
  <body>
    入力1: 文字列 <input type="text" id="input1" value="stay awhile" /><br />
    入力2: フォントファイル <input type="file" id="input2" /><br />
    <br />
    出力: 解析結果<br />
    <br />
    <div id="output"></div>
    <script>

      'use strict';

      // @ts-check

      /**
      * 標準組み込みオブジェクト
      * @typeof {object} ArrayBuffer
      * @typeof {object} DataView
      * @typeof {object} Uint8Array
      * Web API
      * @typeof {object} FileList
      * @typeof {object} HTMLElement
      * @typeof {object} HTMLInputElement
      * @typeof {object} HTMLTextAreaElement
      * @typeof {object} TextDecoder
      * @typeof {object} XMLDocument
      */

      //------------------------------------------------------------
      // ライブラリー
      //------------------------------------------------------------

      /**
      * バイナリーデータ(ArrayBufferオブジェクト)を16進数テキストに変換
      * @param {ArrayBuffer} arrayBuffer 16進数テキストに変換するArrayBufferオブジェクトによるバイナリーデータ
      * @returns {string} 変換された16進数テキスト
      */
      function arrayBufferToHexText(arrayBuffer) {
        let hexText = '';
        /** @type {DataView} */
        const dataView = new DataView(arrayBuffer);
        for (let i = 0; i < arrayBuffer.byteLength; i ++) {
          hexText += dataView.getUint8(i).toString(16).padStart(2, '0');
        }
        return hexText;
      }

      /**
      * バイナリーデータ(ArrayBufferオブジェクト)をデータタイプに合わせて値に変換
      * @param {ArrayBuffer} arrayBuffer 値に変換するArrayBufferオブジェクトによるバイナリーデータ
      * @param {string} dataType データタイプ
      * @returns {(number|string)} 値
      */
      function arrayBufferToValue(arrayBuffer, dataType) {
        /** @type {DataView} */
        const dataView = new DataView(arrayBuffer);
        switch (dataType) {
          // 符号なし8ビット整数
          case 'uint8':
          // テーブルへの8ビットオフセット、符号無し8ビット整数と同じ、NULLオフセット = 0x00
          case 'Offset8':
            return dataView.getUint8(0);
          // 符号付き8ビット整数
          case 'int8':
            return dataView.getInt8(0);
          // 符号なし16ビット整数
          case 'uint16':
          // フォントデザインの単位で、数量を表す符号付き16ビット整数
          case 'UFWORD':
          // テーブルへの16ビットオフセット、符号無し16ビット整数と同じ、NULLオフセット = 0x00
          case 'Offset16':
            return dataView.getUint16(0);
          // 符号付き16ビット整数
          case 'int16':
          // フォントデザインの単位で、数量を表す符号なし16ビット整数
          case 'FWORD':
            return dataView.getInt16(0);
          // 符号なし24ビット整数
          case 'uint24':
          // テーブルへの24ビットオフセット、符号無し24ビット整数と同じ、NULLオフセット = 0x00
          case 'Offset24':
            return dataView.getUint16(0) * 256 + dataView.getUint8(2);
          // 符号なし32ビット整数
          case 'uint32':
          // テーブルへの32ビットオフセット、符号無し32ビット整数と同じ、NULLオフセット = 0x00
          case 'Offset32':
            return dataView.getUint32(0);
          // 符号付き32ビット整数
          case 'int32':
            return dataView.getInt32(0);
          // 符号付き32ビット固定小数点数 (16.16)
          case 'Fixed':
            return dataView.getInt16(0) + dataView.getUint16(2) / 2 ** 16;
          // 下位14ビットの仮数部を持つ符号付き16ビット固定小数点数 (2.14)
            case 'F2DOT14':
          {
            const UINT16 = dataView.getUint16(0);
            const UINT2 = (UINT16 & 0b1100000000000000) >>> 14;
            const INT2 = UINT2 >= 2 ** 1 ? UINT2 - 2 ** 2 : UINT2;
            return INT2 + (UINT16 & 0b0011111111111111) / 2 ** 14;
          }
          // 1904年1月1日午前0時 (UTC) からの秒数で表された日付と時刻、値は符号付き64ビット整数として表される
          case 'LONGDATETIME':
          {
            const BIG_INT64 = dataView.getBigInt64(0);
            const DATE = BigInt(new Date('January 1, 1904 0:0:0 GMT+00:00').getTime());
            return new Date(Number((BIG_INT64 + DATE / 1000n) * 1000n)).toDateString();
          }
          // テーブル、デザインバリエーションの軸、スクリプト、言語システム、フューチャーまたはベースラインを識別するために使用される4つの符号なし8ビット整数の配列 (長さ=32ビット)
          case 'Tag':
          {
            let asciiString ='';
            for (let i = 0; i < arrayBuffer.byteLength; i ++) {
              asciiString += String.fromCharCode(dataView.getUint8(i));
            }
            return asciiString;
          }
          // メジャーおよびマイナーバージョン番号がパックされた32ビットの値
          case 'Version16Dot16':
          {
            const HEX_TEXT = arrayBufferToHexText(arrayBuffer);
            return parseInt(HEX_TEXT.slice(0, 4)) + parseInt(HEX_TEXT.slice(4, 8)) / 10000;
          }
          default:
            console.log(`Data Type: ${ dataType } is not defined.`);
            return 0;
        }
      }

      /**
      * Naming Tableの解析 platformIDおよびlanguageIDに応じて文字列を返す
      * @param {ArrayBuffer} arrayBuffer
      * @param {number} platformID
      *  @param {number} languageID
      * @returns {string}
      */
      function nameMeaning(arrayBuffer, platformID, languageID) {
        let outputString ='';
        /** @type {Uint8Array} */
        const uint8Array = new Uint8Array(arrayBuffer);
        if (platformID === 0) {
          // Unicode platform (platform ID = 0)
          /** @type {TextDecoder} */
        const utf8Decoder = new TextDecoder('utf-8');
          outputString = utf8Decoder.decode(uint8Array);
        } else if (platformID === 1) {
          // Macintosh platform (platform ID = 1)
          if (languageID === 0) {
            // English (language ID = 0)
            /** @type {DataView} */
            const dataView = new DataView(arrayBuffer);
            for (let i = 0; i < arrayBuffer.byteLength; i ++) {
              outputString += String.fromCharCode(dataView.getUint8(i));
            }
          } else if (languageID === 11) {
            // Japanese (language ID = 11)
            /** @type {TextDecoder} */
            const utf8Decoder = new TextDecoder('sjis');
            outputString = utf8Decoder.decode(uint8Array);
          }
        } else if (platformID === 3) {
          // Windows platform (platform ID= 3)
          /** @type {TextDecoder} */
          const utf8Decoder = new TextDecoder('utf-16be');
          outputString = utf8Decoder.decode(uint8Array);
        }
          return outputString;
      }

      /**
      * ts-check エラー対策 関数を用意し、nullを排除し、HTMLElementではなくHTMLInputElementとする
      * @param {string} id input要素のID
      * @returns {HTMLInputElement}
      */
      function htmlInputElement(id) {
        return /** @type {!HTMLInputElement} */ (document.getElementById(id));
      }

      //------------------------------------------------------------
      // イベント  type="file"のインプット要素でファイルを選択時
      //------------------------------------------------------------
      /** @this HTMLInputElement */
      htmlInputElement('input2').addEventListener('change', async function() {

        //------------------------------------------------------------
        // 関数内ライブラリー
        //------------------------------------------------------------

        /** @classdesc データタイプに合わせてバイナリーデータを切り取って値に変換 */
        class Valorize {
          /** @param {number} offset 切り取りを開始するオフセット位置 @returns {number} 値 */
          static uint8(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 1), 'uint8')); }
          /** @param {number} offset @returns {number} */
          static Offset8(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 1), 'Offset8')); }
          /** @param {number} offset @returns {number} */
          static int8(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 1), 'int8')); }
          /** @param {number} offset @returns {number} */
          static uint16(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 2), 'uint16')); }
          /** @param {number} offset @returns {number} */
          static UFWORD(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 2), 'UFWORD')); }
          /** @param {number} offset @returns {number} */
          static Offset16(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 2), 'Offset16')); }
          /** @param {number} offset @returns {number} */
          static int16(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 2), 'int16')); }
          /** @param {number} offset @returns {number} */
          static FWORD(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 2), 'FWORD')); }
          /** @param {number} offset @returns {number} */
          static F2DOT14(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 2), 'F2DOT14')); }
          /** @param {number} offset @returns {number} */
          static uint24(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 3), 'uint24')); }
          /** @param {number} offset @returns {number} */
          static Offset24(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 3), 'Offset24')); }
          /** @param {number} offset @returns {number} */
          static uint32(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 4), 'uint32')); }
          /** @param {number} offset @returns {number} */
          static Offset32(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 4), 'Offset32')); }
          /** @param {number} offset @returns {number} */
          static int32(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 4), 'int32')); }
          /** @param {number} offset @returns {number} */
          static Fixed(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 4), 'Fixed')); }
          /** @param {number} offset @returns {string} */
          static Tag(offset) { return /** @type {string} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 4), 'Tag')); }
          /** @param {number} offset @returns {number} */
          static Version16Dot16(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 4), 'Version16Dot16')); }
          /** @param {number} offset @returns {string} */
          LONGDATETIME(offset) { return /** @type {string} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 8), 'LONGDATETIME')); }
        }

        //------------------------------------------------------------
        // クラスの設定
        //------------------------------------------------------------

        class Font {
          actualBoundingBoxAscent = 0;
          actualBoundingBoxDescent = 0;
          actualBoundingBoxLeft = 0;
          actualBoundingBoxRight = 0;
          fontBoundingBoxWidth = 0;
          /** @type {object[]} */
          glyphs = [];
          height = 0;
          width = 0;
          x = 0;
          y = 0;
        }

        class Glyph {
          codePointString = '';
          /** @type {object[]} */
          contours = [];
          /** @type {object} */
          svgElement = {};
        }

        //------------------------------------------------------------
        // ローカル変数の設定
        //------------------------------------------------------------

        /** @namespace */
        const stringData = {
          /** @type {string[]} */
          characters: [],
          codePointString: '',
          /** @type {number[][]} */
          codePoints: [],
          inputString: htmlInputElement('input1').value,
          /** @type {number[][]} */
          inputCodePoints: [],
          // ts-check エラー対策 Intl.Segmenterが存在しないバグがあるので無視
          /** @type {string[][]} 入力文字列を文字ごとに分割した配列 */
          // @ts-ignore
          inputStringArray: Array.from(new Intl.Segmenter('ja-JP').segment(htmlInputElement('input1').value), function (element) { return element.segment; }),
          // ts-check エラー対策 Intl.Segmenterが存在しないバグがあるので無視
          /** @type {string[][]} 入力文字列を文字ごとに分割した配列を作成後、重複する文字を除去した配列 */
          // @ts-ignore
          segmentArray: Array.from(new Set(Array.from(new Intl.Segmenter('ja-JP').segment(htmlInputElement('input1').value), function (element) { return element.segment; })))
        };

        /** @namespace */
        const fontCollection = {
          /** @type {object[]} */
          fonts: []
        };

        //------------------------------------------------------------
        // 入力文字列に対する処理
        //------------------------------------------------------------
        // 入力文字配列からCode Pointによる文字列を作成
        // SVGのid属性に使用
        stringData.inputStringArray.forEach(function (element1, index1) {
          stringData.inputCodePoints[index1] = [];
          const temporaryArray = Array.from(element1);
          temporaryArray.forEach(function (element2, index2) {
            let codePoint = /** @type {number} */ (element2.codePointAt(0));
            stringData.inputCodePoints[index1][index2] = codePoint;
            stringData.codePointString += codePoint.toString(16).toUpperCase().padStart(4, '0');
          });
        });

        // 重複する文字を除去した入力文字配列からCode Pointを取得する、異体字セレクターに対応するため多次元配列とする
        stringData.segmentArray.forEach(function (element1, index1) {
          stringData.codePoints[index1] = [];
          // 異体字セレクターを考慮して文字列を分割
          const temporaryArray = Array.from(element1);
          temporaryArray.forEach(function (element2, index2) {
            stringData.codePoints[index1][index2] = /** @type {number} */ (element2.codePointAt(0));
          });
        });

        // 取得したCode Pointの値をソート(多次元配列のソート)
        stringData.codePoints.sort(function (a, b) {
          for (let i = 0; i < b.length; i ++) {
            if (!a[i]) { return -1; } else if (a[i] === b[i]) { continue; } else { return a[i] - b[i]; }
          }
            return 0;
        });

        // Code Pointの値でソートした多次元配列の順番で文字の配列を作成
        stringData.codePoints.forEach(function (element1, index1) {
          stringData.characters[index1] = '';
          element1.forEach(function (element2) {
            stringData.characters[index1] += String.fromCodePoint(element2);
          });
        });

        //------------------------------------------------------------
        // フォントファイルを解析する
        //------------------------------------------------------------
        // フォントファイルからArrayBufferオブジェクトを取得
        const fileList = /** @type {FileList} */ (this.files);
        /** @type {ArrayBuffer} */
        const arrayBuffer = await fileList[0].arrayBuffer();

        //------------------------------------------------------------
        // Font Collections
        //------------------------------------------------------------
        // The Font Collection File Structure
        // TTC Header
        let numFonts = 1;
        const tableDirectoryOffsets = [];
        fontCollection.tableDirectoryOffsets = tableDirectoryOffsets;
        if (Valorize.Tag(0) === 'ttcf') {
          numFonts = Valorize.uint32(8);
          for (let i = 0; i < numFonts; i ++) {
            let offset = i * 4 + 12;
            tableDirectoryOffsets[i] = Valorize.Offset32(offset);
          }
        }

        //------------------------------------------------------------
        // Organization of an OpenType Font
        //------------------------------------------------------------
        for (let i = 0; i < numFonts; i ++) {
          /** @type {object} */
          const font = new Font();
          fontCollection.fonts[i] = font;
          // Table Directory
          {
            /** @type {object} */
            const tableDirectory = {};
            font.tableDirectory = tableDirectory;
            let offset = fontCollection.tableDirectoryOffsets[i] ? fontCollection.tableDirectoryOffsets[i] : 0;
            let numTables = Valorize.uint16(offset + 4);
            // Table Record
            for (let j = 0; j < numTables; j ++) {
              offset = fontCollection.tableDirectoryOffsets[i] ? fontCollection.tableDirectoryOffsets[i] + j * 16 + 12 : j * 16 + 12;
              const tableTag = Valorize.Tag(offset);
              tableDirectory[tableTag] = {};
              tableDirectory[tableTag].offset = Valorize.Offset32(offset + 8);
            }
          }

          //------------------------------------------------------------
          // Font Tables その1
          //------------------------------------------------------------
          // cmap - Character to Glyph Index Mapping Table
          // 主要フォーマットのサブテーブルのオフセット位置を取得
          {
            const cmap = { format4: { subtableOffset: 0 }, format12: { subtableOffset: 0 }, format14: { subtableOffset: 0 } };
            font.cmap = cmap;
            let offset = font.tableDirectory['cmap'].offset;
            let numTables = Valorize.uint16(offset + 2);
            for (let j = 0; j < numTables; j ++) {
              offset = font.tableDirectory['cmap'].offset + j * 8 + 4;
              let platformID = Valorize.uint16(offset);
              let encodingID = Valorize.uint16(offset + 2);
              // Unicode Platform (Platform ID = 0), Encoding ID 3 should be used in conjunction with 'cmap' subtable formats 4 or 6.
              // Windows Platform (Platform ID = 3), Fonts that support only Unicode BMP characters (U+0000 to U+FFFF) on the Windows platform must use encoding 1 with a format 4 subtable.
              if (cmap.format4.subtableOffset === 0 && (platformID === 0 && encodingID === 3) || (platformID === 3 && encodingID === 1)) {
                let subtableOffset = Valorize.Offset32(offset + 4);
                offset = font.tableDirectory['cmap'].offset + subtableOffset;
                let format = Valorize.uint16(offset);
                if (format === 4) {
                  cmap.format4.subtableOffset = font.tableDirectory['cmap'].offset + subtableOffset;
                }
              }
              // Unicode Platform (Platform ID = 0), Encoding ID 4 should be used in conjunction with subtable formats 10 or 12.
              // Windows Platform (Platform ID = 3), Fonts that support Unicode supplementary-plane characters (U+10000 to U+10FFFF) on the Windows platform must use encoding 10 with a format 12 subtable.
              if (cmap.format12.subtableOffset === 0 && (platformID === 0 && encodingID === 4) || (platformID === 3 && encodingID === 10)) {
                let subtableOffset = Valorize.Offset32(offset + 4);
                offset = font.tableDirectory['cmap'].offset + subtableOffset;
                let format = Valorize.uint16(offset);
                if (format === 12) {
                  cmap.format12.subtableOffset = font.tableDirectory['cmap'].offset + subtableOffset;
                }
              }
              // A format 14 subtable must only be used under platform ID 0 and encoding ID 5; and encoding ID 5 should only be used with a format 14 subtable.
              if (cmap.format14.subtableOffset === 0 && platformID === 0 && encodingID === 5) {
                let subtableOffset = Valorize.Offset32(offset + 4);
                offset = font.tableDirectory['cmap'].offset + subtableOffset;
                let format = Valorize.uint16(offset);
                if (format === 14) {
                  cmap.format14.subtableOffset = font.tableDirectory['cmap'].offset + subtableOffset;
                }
              }
            }
          }

          // head - Font Header Table
          {
            font.head = {};
            let offset = font.tableDirectory['head'].offset;
            font.head.unitsPerEm = Valorize.uint16(offset + 18);
            font.head.indexToLocFormat = Valorize.int16(offset + 50);
          }

          // hhea - Horizontal Header Table
          {
            font.hhea = {};
            let offset = font.tableDirectory['hhea'].offset;
            font.hhea.numberOfHMetrics = Valorize.uint16(offset + 34);
          }

          // name - Naming Table
          // ライセンス情報をコメントでSVG内に入れる
          {
            // Naming table header
            /** @type {string[]} */
            const name = [];
            font.name = name;
            let offset = font.tableDirectory['name'].offset;
            let count = Valorize.uint16(offset + 2);
            let storageOffset = Valorize.Offset16(offset + 4);
            for (let j = 0; j < count; j ++) {
              // Name records
              offset = font.tableDirectory['name'].offset + j * 12 + 6;
              let platformID = Valorize.uint16(offset);
              let languageID = Valorize.uint16(offset + 4);
              let nameID = Valorize.uint16(offset + 6);
              let length = Valorize.uint16(offset + 8);
              let stringOffset = Valorize.Offset16(offset + 10);
              if (platformID === 0 && languageID === 0) {
                offset = font.tableDirectory['name'].offset + storageOffset + stringOffset;
                name[nameID] = nameMeaning(arrayBuffer.slice(offset, offset + length), platformID, languageID);
              } else if (platformID === 1 && (languageID === 0 || languageID === 11)) {
                offset = font.tableDirectory['name'].offset + storageOffset + stringOffset;
                name[nameID] = nameMeaning(arrayBuffer.slice(offset, offset + length), platformID, languageID);
              } else if (platformID === 3 && languageID === 0x409) {
                offset = font.tableDirectory['name'].offset + storageOffset + stringOffset;
                name[nameID] = nameMeaning(arrayBuffer.slice(offset, offset + length), platformID, languageID);
              }
            }
          }

          // OS/2 - OS/2 and Windows Metrics Table
          {
            font.OS_2 = {};
            let offset = font.tableDirectory['OS/2'].offset;
            font.OS_2.fsType = Valorize.uint16(offset + 8);
            font.OS_2.sTypoAscender = Valorize.FWORD(offset + 68);
            font.OS_2.sTypoDescender = Valorize.FWORD(offset + 70);
            font.OS_2.usWinAscent = Valorize.uint16(offset + 74);
            font.OS_2.usWinDescent = Valorize.uint16(offset + 76);
          }

          //------------------------------------------------------------
          // SVG出力の準備 その1
          //------------------------------------------------------------
          // SVG出力用テキストエリア
          const htmlElement = /** @type {HTMLElement} */ (document.getElementById('output'));
          /** @type {HTMLTextAreaElement} */
          const textareaElement = document.createElement('textarea');
          htmlElement.appendChild(textareaElement);
          textareaElement.setAttribute('cols', '80');
          textareaElement.setAttribute('rows', '20');
          // XML DOM ツリーを作成
          /** @type {XMLDocument} */
          const svgDocument = document.implementation.createDocument('', '', null);
          /** @type {HTMLElement} */
          const svgElement = svgDocument.createElement('svg');
          svgElement.setAttribute('version', '1.1');
          svgElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
          svgDocument.appendChild(svgElement);
          svgElement.appendChild(svgDocument.createComment(` Copyright Notice: ${ font.name[0] }, Full Font Name: ${ font.name[4] } `));
          if ((font.OS_2.fsType & 0x000f) === 0) {
            svgElement.appendChild(svgDocument.createComment(` Type flags: \u00220\u0022 Installable embedding `));
          } else if ((font.OS_2.fsType & 0x000f) === 2) {
            svgElement.appendChild(svgDocument.createComment(` Type flags: \u00222\u0022 Restricted License embedding `));
          } else if ((font.OS_2.fsType & 0x000f) === 4) {
            svgElement.appendChild(svgDocument.createComment(` Type flags: \u00224\u0022 Preview & Print embedding `));
          } else if ((font.OS_2.fsType & 0x000f) === 8) {
            svgElement.appendChild(svgDocument.createComment(` Type flags: \u00228\u0022 Editable embedding `));
          }
          if ((font.OS_2.fsType & 0x0100)) {
            svgElement.appendChild(svgDocument.createComment(` Type flags: No subsetting `));
          }
          if ((font.OS_2.fsType & 0x0200)) {
            svgElement.appendChild(svgDocument.createComment(` Type flags: Bitmap embedding only `));
          }
          svgElement.appendChild(svgDocument.createComment(` unitsPerEm: ${ font.head.unitsPerEm }, usWinAscent: ${ font.OS_2.usWinAscent }, usWinDescent: ${ font.OS_2.usWinDescent }, sTypoAscender: ${ font.OS_2.sTypoAscender }, sTypoDescender: ${ font.OS_2.sTypoDescender } `));
          /** @type {HTMLElement} */
          const defsElement = svgDocument.createElement('defs');
          svgElement.appendChild(defsElement);

          //------------------------------------------------------------
          // SVGによる文字のアウトラインを作成
          //------------------------------------------------------------
          // 重複する文字を除去した入力文字配列からCode Pointごとに処理
          stringData.codePoints.forEach(function (element1, index1) {
            /** @type {object} */
            const glyph = new Glyph;
            font.glyphs[index1] = glyph;
            // Code Pointによる文字列を作成
            // SVGのid属性に使用
            element1.forEach(function (element2) {
              glyph.codePointString += element2.toString(16).toUpperCase().padStart(4, '0');
            });

            //------------------------------------------------------------
            // Glyph IDの取得
            //------------------------------------------------------------
            let codePoint = element1[0];
              /** @type {number|undefined} 異体字セレクター */
            let varSelector = element1[1] ? element1[1] : undefined;
            let glyphID = 0;
            glyph.glyphID = glyphID;
            // フォントファイル内のCMapテーブルを使用してCode Pointの値からGlyph IDを取得
            // Format 4
            if (codePoint >= 0x0000 && codePoint <= 0xffff) {
              let offset = font.cmap.format4.subtableOffset;
              let segCountX2 = Valorize.uint16(offset + 6);
              for (let j = 0; j < segCountX2 / 2; j ++) {
                offset = font.cmap.format4.subtableOffset + j * 2 + 14;
                let startCode = Valorize.uint16(offset + segCountX2 + 2);
                if (codePoint <= Valorize.uint16(offset) && codePoint >= startCode) {
                  let idRangeOffset = Valorize.uint16(offset + 3 * segCountX2 + 2);
                  if (idRangeOffset === 0) {
                    offset = font.cmap.format4.subtableOffset + j * 2 + segCountX2 * 2 + 16;
                    glyphID = (codePoint + Valorize.uint16(offset)) & 0xffff;
                    break;
                  } else {
                    offset = font.cmap.format4.subtableOffset + j * 2 + segCountX2 * 3 + idRangeOffset + (codePoint - startCode) * 2 + 16;
                    glyphID = Valorize.uint16(offset);
                    break;
                  }
                }
              }
            }
            // Format 12
            if (glyphID === 0 && codePoint >= 0x0000 && codePoint <= 0x10ffff) {
              let offset = font.cmap.format12.subtableOffset;
              let numGroups = Valorize.uint32(offset + 12);
              for (let j = 0; j < numGroups; j ++) {
                offset = font.cmap.format12.subtableOffset + j * 12 + 16;
                let startCharCode = Valorize.uint32(offset);
                let endCharCode = Valorize.uint32(offset + 4);
                if (codePoint >= startCharCode && codePoint <= endCharCode) {
                  glyphID = (codePoint - startCharCode) + Valorize.uint32(offset + 8);
                  break;
                }
              }
            }
            // Format 14
            if (varSelector) {
              let offset = font.cmap.format14.subtableOffset;
              let numVarSelectorRecords = Valorize.uint32(offset + 6);
              for (let j = 0; j < numVarSelectorRecords; j ++) {
                offset = font.cmap.format14.subtableOffset + j * 11 + 10;
                if (Valorize.uint24(offset) === varSelector) {
                  let nonDefaultUVSOffset = Valorize.Offset32(offset + 7);
                  if (nonDefaultUVSOffset !== 0) {
                    offset = font.cmap.format14.subtableOffset + nonDefaultUVSOffset;
                    let numUVSMappings = Valorize.uint32(offset);
                    for (let k = 0; k < numUVSMappings; k++) {
                      offset = font.cmap.format14.subtableOffset + nonDefaultUVSOffset + k * 5 + 4;
                      let unicodeValue = Valorize.uint24(offset);
                      if (unicodeValue === codePoint) {
                        glyphID = Valorize.uint16(offset + 3);
                        break;
                      }
                    }
                  }
                }
              }
            }

            //------------------------------------------------------------
            // Font Tables その2 Glyph IDを使用
            //------------------------------------------------------------
            // hmtx - Horizontal Metrics Table
            const hmtx = {}
            glyph.hmtx = hmtx;
            if (glyphID <= font.hhea.numberOfHMetrics) {
              let offset = font.tableDirectory['hmtx'].offset + glyphID * 4;
              hmtx.advanceWidth = Valorize.int16(offset);
            } else {
              let offset = font.tableDirectory['hmtx'].offset + font.hhea.numberOfHMetrics * 4;
              hmtx.advanceWidth = Valorize.uint16(offset);
            }
            // loca - Index to Location
            const loca = {};
            glyph.loca = loca;
            if (font.head.indexToLocFormat === 0) {
              let offset = font.tableDirectory['loca'].offset + glyphID * 2;
              loca.offsets = Valorize.Offset16(offset) * 2;
              loca.length = Valorize.Offset16(offset + 2) * 2 - loca.offsets * 2;
            } else if (font.head.indexToLocFormat === 1) {
              let offset = font.tableDirectory['loca'].offset + glyphID * 4;
              loca.offsets = Valorize.Offset32(offset);
              loca.length = Valorize.Offset32(offset + 4) - loca.offsets;
            }
            // glyf - Glyph Data
            const glyf = {};
            glyph.glyf = glyf;
            // Glyph Header
            let offset = font.tableDirectory['glyf'].offset + loca.offsets;
            glyf.numberOfContours = Valorize.int16(offset);
            glyf.xMin = Valorize.int16(offset + 2);
            glyf.yMin = Valorize.int16(offset + 4);
            glyf.xMax = Valorize.int16(offset + 6);
            glyf.yMax = Valorize.int16(offset + 8);

            //------------------------------------------------------------
            //  Simple Glyph Descriptionの解析
            //------------------------------------------------------------
            // Simple Glyph table
            glyf.endPtsOfContours = [];
            if (glyf.numberOfContours >= 0 && loca.length > 0) {
              for (let j = 0; j < glyf.numberOfContours; j ++) {
                offset = font.tableDirectory['glyf'].offset + loca.offsets + j * 2 + 10;
                glyf.endPtsOfContours[j] = Valorize.uint16(offset);
              }
              offset = font.tableDirectory['glyf'].offset + loca.offsets + glyf.numberOfContours * 2 + 10;
              let instructionLength = Valorize.uint16(offset);
              offset = font.tableDirectory['glyf'].offset + loca.offsets + glyf.numberOfContours * 2 + instructionLength + 12;
              /** @type {ArrayBuffer} Simple Glyph Descriptionのflags、xCoordinates、yCoordinatesを含むバイナリーデータ */
              const slicedArrayBuffer = arrayBuffer.slice(offset, font.tableDirectory['glyf'].offset + loca.offsets + loca.length);

              // バイナリーデータ(ArrayBufferオブジェクト)を解析してSVGによる文字のアウトラインを作成(Simple Glyph Description)
              {

                class Valorize {
                  /** @param {number} offset 切り取りを開始するオフセット位置 @returns {number} 値 */
                  static uint8(offset) { return /** @type {number }*/ (arrayBufferToValue(slicedArrayBuffer.slice(offset, offset + 1), 'uint8')); }
                  /** @param {number} offset @returns {number} */
                  static int16(offset) { return /** @type {number }*/ (arrayBufferToValue(slicedArrayBuffer.slice(offset, offset + 2), 'int16')); }
                }

                class Contour {
                  /** @type {number[]} */
                  flags = [];
                  /** @type {number[]} */
                  onCurvePoint = [];
                  /** @type {number[]} */
                  deltaX = [];
                  /** @type {number[]} */
                  deltaY = [];
                  points = 0;
                  /** @type {number[]} */
                  x = [];
                  /** @type {number[]} */
                  y = [];
                }

                let offset = 0;
                let repeatCount = 0;
                let repeatFlag = 0;
                let xCoordinates = 0;
                let yCoordinates = 0;

                
                for (let i = 0; i < glyph.glyf.numberOfContours; i ++) {
                  const contour = new Contour();
                  glyph.contours[i] = contour;
                  // 各contourのpointの数を計算
                  contour.points = i === 0 ? glyph.glyf.endPtsOfContours[0] + 1 : glyph.glyf.endPtsOfContours[i] - glyph.glyf.endPtsOfContours[i - 1];
                }

                //------------------------------------------------------------
                //  Simple Glyph Descriptionのflags、xCoordinates、yCoordinatesをの解析
                //------------------------------------------------------------
                // flagsの解析
                for (let i = 0; i < glyph.glyf.numberOfContours; i ++) {
                  for (let j = 0; j < glyph.contours[i].points; j ++) {
                    if (repeatCount > 0) {
                      glyph.contours[i].flags[j] = repeatFlag;
                      repeatCount --;
                    } else {
                      glyph.contours[i].flags[j] = Valorize.uint8(offset);
                      offset += 1;
                      if (glyph.contours[i].flags[j] & 0b00001000) {
                        repeatFlag = glyph.contours[i].flags[j];
                        repeatCount = Valorize.uint8(offset);
                        offset += 1;
                      }
                    }

                  if (glyph.contours[i].flags[j] & 0b00000001) {
                      glyph.contours[i].onCurvePoint[j] = 1;
                    } else {
                      glyph.contours[i].onCurvePoint[j] = 0;
                    }
                  }
                }

                // xCoordinatesの解析
                for (let i = 0; i < glyph.glyf.numberOfContours; i ++) {
                  for (let j = 0; j < glyph.contours[i].points; j ++) {
                    if (glyph.contours[i].flags[j] & 0b00000010) {
                      if (glyph.contours[i].flags[j] & 0b00010000) {
                        glyph.contours[i].deltaX[j] = Valorize.uint8(offset);
                      } else {
                        glyph.contours[i].deltaX[j] = -1 * Valorize.uint8(offset);
                      }
                      offset += 1;
                    } else {
                      if (glyph.contours[i].flags[j] & 0b00010000) {
                        glyph.contours[i].deltaX[j] = 0;
                      } else {
                        glyph.contours[i].deltaX[j] = Valorize.int16(offset);
                        offset += 2;
                      }
                    }
                    xCoordinates += glyph.contours[i].deltaX[j];
                    glyph.contours[i].x[j] = xCoordinates;
                  }
                }

                // yCoordinatesの解析
                for (let i = 0; i < glyph.glyf.numberOfContours; i ++) {
                  for (let j = 0; j < glyph.contours[i].points; j ++) {
                    if (glyph.contours[i].flags[j] & 0b00000100) {
                      if (glyph.contours[i].flags[j] & 0b00100000) {
                        glyph.contours[i].deltaY[j] = Valorize.uint8(offset);
                      } else {
                        glyph.contours[i].deltaY[j] = -1 * Valorize.uint8(offset);
                      }
                      offset += 1;
                    } else {
                      if (glyph.contours[i].flags[j] & 0b00100000) {
                        glyph.contours[i].deltaY[j] = 0;
                      } else {
                        glyph.contours[i].deltaY[j] = Valorize.int16(offset);
                        offset += 2;
                      }
                    }
                    yCoordinates += glyph.contours[i].deltaY[j];
                    glyph.contours[i].y[j] = yCoordinates;
                  }
                }

                //------------------------------------------------------------
                //  SVGによる文字のアウトラインを作成
                //------------------------------------------------------------
                glyph.svgElement = svgDocument.createElement('path');
                glyph.svgElement.setAttribute('id', glyph.codePointString);
                let pathData = '';
                let previousCommand = '';
                for (let i = 0; i < glyph.glyf.numberOfContours; i ++) {
                  for (let j = 0, lj = glyph.contours[i].points; j < lj; j ++) {
                    if (j === 0) {
                      if (glyph.contours[i].onCurvePoint[0] === 1 && glyph.contours[i].onCurvePoint[1] === 1) {
                        pathData += `M ${ glyph.contours[i].x[0] }, ${ glyph.contours[i].y[0] } L`;
                        previousCommand = 'L';
                      } else if (glyph.contours[i].onCurvePoint[0] === 1 && glyph.contours[i].onCurvePoint[1] === 0) {
                        pathData += `M ${ glyph.contours[i].x[0] }, ${ glyph.contours[i].y[0] } Q`;
                        previousCommand = 'Q';
                      } else if (glyph.contours[i].onCurvePoint[0] === 0) {
                        console.log(`Glyph: ${ glyph.unicodeNumber.toString(16).padStart(4, '0') }, Contour: ${ i }, Points: 0, Contour開始点にもかかわらずonCurvePointではありません。`);
                      }
                    } else if (j === lj - 1) {
                      if (glyph.contours[i].onCurvePoint[j] === 1) {
                        pathData += ` ${ glyph.contours[i].x[j] }, ${ glyph.contours[i].y[j] } Z`;
                      
                      } else {
                          pathData += ` ${ glyph.contours[i].x[j] }, ${ glyph.contours[i].y[j] } ${ glyph.contours[i].x[0] }, ${ glyph.contours[i].y[0] } Z`;
                      }
                      previousCommand = '';
                    } else {
                      if (glyph.contours[i].onCurvePoint[j] === 1 && glyph.contours[i].onCurvePoint[j + 1] === 1) {
                        if (previousCommand === 'L') {
                          pathData += ` ${ glyph.contours[i].x[j] }, ${ glyph.contours[i].y[j] }`;
                        } else {
                          pathData += ` ${ glyph.contours[i].x[j] }, ${ glyph.contours[i].y[j] } L`;
                          previousCommand = 'L';
                        }
                      } else if (glyph.contours[i].onCurvePoint[j] === 1 && glyph.contours[i].onCurvePoint[j + 1] === 0) {
                        if (previousCommand === 'Q') {
                          pathData += ` ${ glyph.contours[i].x[j] }, ${ glyph.contours[i].y[j] }`;
                        } else {
                          pathData += ` ${ glyph.contours[i].x[j] }, ${ glyph.contours[i].y[j] } Q`;
                          previousCommand = 'Q';
                        }
                      } else if (glyph.contours[i].onCurvePoint[j] === 0 && glyph.contours[i].onCurvePoint[j + 1] === 1) {
                        pathData += ` ${ glyph.contours[i].x[j] }, ${ glyph.contours[i].y[j] }`;
                      } else if (glyph.contours[i].onCurvePoint[j] === 0 && glyph.contours[i].onCurvePoint[j + 1] === 0) {
                        pathData += ` ${ glyph.contours[i].x[j] }, ${ glyph.contours[i].y[j] } ${ Math.round(((glyph.contours[i].x[j] + glyph.contours[i].x[j + 1]) / 2) * 10 ) / 10 }, ${ Math.round(((glyph.contours[i].y[j] + glyph.contours[i].y[j + 1]) / 2) * 10) / 10 } `;
                      }
                    }
                  }
                }

                if (pathData !== '') {
                  glyph.svgElement.setAttribute('d', pathData);
                }

              }

              //------------------------------------------------------------
              // SVG出力の準備 その2
              //------------------------------------------------------------
              // 作成した文字のアウトラインをSVGに追加
              defsElement.appendChild(svgDocument.createComment(` Character: \u0022${ stringData.characters[index1] }\u0022 `));
              defsElement.appendChild(glyph.svgElement);

            }
          });

          //------------------------------------------------------------
          // SVG出力の準備 その3
          //------------------------------------------------------------
          // 入力文字列に対する処理
          // 入力文字列に関する要素をSVGに追加
          /** @type {HTMLElement} */
          const gElement = svgDocument.createElement('g');
          gElement.setAttribute('id', stringData.inputCodePoints.length !== 1 ? stringData.codePointString : '_' + stringData.codePointString);
          {
            /** @type {HTMLElement} */
            const useElement = svgDocument.createElement('use');
            svgElement.appendChild(useElement);
            useElement.setAttribute('href', '#' + (stringData.inputCodePoints.length !== 1 ? stringData.codePointString : '_' + stringData.codePointString));
            useElement.setAttribute('transform', 'translate(0, 0)');
          }

          // 入力文字列内の文字ごとに処理
          stringData.inputCodePoints.forEach(function (element1, index1, array1) {
            // 重複する文字を除去した入力文字配列のソートされたCode Point配列から、入力文字のCode Pointに対応するインデックスを取得、試験
            const index2 = stringData.codePoints.findIndex(function (element2) {
              if (element2.length !== element1.length) { return false; }
              for (let j = 0, lj = element1.length; j < lj; j ++) {
                if (!element2[j] || element2[j] !== element1[j]) { return false; }
                if (j !== lj - 1) { continue; } else if (j === lj - 1) { return true; }
              }
              return false;
            });
            // 取得したインデックスを使用して、格納された文字に関するデータを取得
            const glyph = font.glyphs[index2];
            if (index1 === 0) {
              font.actualBoundingBoxLeft = glyph.glyf.xMin;
            }
            font.actualBoundingBoxDescent = glyph.glyf.yMin < font.actualBoundingBoxDescent ? glyph.glyf.yMin : font.actualBoundingBoxDescent;
            if (index1 === array1.length - 1) {
              font.actualBoundingBoxRight = font.fontBoundingBoxWidth + glyph.glyf.xMax;
            }
            font.actualBoundingBoxAscent = glyph.glyf.yMax > font.actualBoundingBoxAscent ? glyph.glyf.yMax : font.actualBoundingBoxAscent;

            // 入力文字列内の文字に関する要素をSVGに追加
            /** @type {HTMLElement} */
            const useElement = svgDocument.createElement('use');
            gElement.appendChild(useElement);
            useElement.setAttribute('href', '#' + glyph.codePointString);
            useElement.setAttribute('transform', `matrix(1 0 0 -1 ${ font.fontBoundingBoxWidth } 0)`);

            // 文字列全体の仮想ボディの幅
            font.fontBoundingBoxWidth += glyph.hmtx.advanceWidth;
          });

          defsElement.appendChild(svgDocument.createComment(` ${ stringData.inputString }, width: ${ font.fontBoundingBoxWidth }, height: ${ font.OS_2.usWinAscent + font.OS_2.usWinDescent } `));
          // フォントサイズの変更や中央揃えがしやすいように、scaleとtlanslateをセットしておく。
          gElement.setAttribute('transform', `scale(1), translate(-${ font.fontBoundingBoxWidth / 2 }, 0)`);
          defsElement.appendChild(gElement);

          // viewBox属性
          font.x = font.actualBoundingBoxLeft < 0 ? font.actualBoundingBoxLeft : 0;
          font.y = font.actualBoundingBoxAscent > font.OS_2.usWinAscent ? font.actualBoundingBoxAscent : font.OS_2.usWinAscent;
          font.width = font.actualBoundingBoxRight > font.fontBoundingBoxWidth ? font.actualBoundingBoxRight - font.x : font.fontBoundingBoxWidth - font.x;
          font.height = (font.y - font.actualBoundingBoxDescent) > (font.y + font.OS_2.usWinDescent) ? font.y - font.actualBoundingBoxDescent : font.y + font.OS_2.usWinDescent;
          svgElement.setAttribute('viewBox', `${ -1 * font.width / 2 } ${ -1 * font.y } ${ font.width } ${ font.height }`);

          //------------------------------------------------------------
          // SVG出力
          //------------------------------------------------------------
          // XMLDocumentを文字列に変換、読みやすいように改行を付加
          const outputText = new XMLSerializer().serializeToString(svgDocument).replace(/></g, '>\u000d\u000a<');
          // SVGソースをテキストエリア要素に出力
          textareaElement.appendChild(document.createTextNode(outputText));
          // SVG画像をコンテンツ区分要素に出力
          htmlElement.innerHTML += outputText;
        }

      });

    </script>
  </body>
</html>

参照


関連情報


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です