JavaScriptでフォントファイルを解析する その6 カラー絵文字のSVGソースを作成し、SVG画像を表示する(COLR/CPAL version 1)

Author:

ベクター形式のカラー絵文字のデータを取得し、SVGソースに埋め込む(COLR/CPAL version 1)

カラー絵文字のフォントファイルは、ラスター(ビットマップ)形式だったりベクター形式だったりフォントごとに異なります😶‍🌫️。
ベクター形式のカラー絵文字は、COLRテーブルにより、カラーグリフをレイヤー状に配置し、複合グラフィックとして表示されます。COLRテーブルには、version 0およびversion 1の2つのバージョンが定義されています。
下記のJavaScriptでは、version 1のフォントファイルを解析し、SVGソースを作成し、SVG画像を表示します。

完成例

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

出力: 解析結果

使い方

フォントファイルのTable Directoryを確認し、COLRおよびCPALテーブルが存在することを確認します。
SVGで表示したい文字を「入力1: 文字列」のテキストボックスに入力します。
「ファイルを選択」ボタンをクリックして、フォントファイル(.ttfまたは.ttc)を開きます。
フォントファイルを開くイベントをきっかけにJavaScriptが動き、「出力: 解析結果」以下にSVGのソースおよびプレビュー画像が表示されます🤗。
COLRおよびCPALテーブルが存在するのに、SVG画像が表示されない場合は、カラー絵文字のSVGソースを作成し、SVG画像を表示する(COLR/CPAL version 0)を試してください。

動作確認は、Noto Color Emojiで行っています。GitHubから無料で入手することができます(Noto-COLRv1.ttf)。
Noto Color Emojiのライセンスは、「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="😤😊📛🫠🎛🎟🎫👓😨🌠🌃🌆🌇" /><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) {
          case 'uint8':
          case 'Offset8':
            return dataView.getUint8(0);
          case 'int8':
            return dataView.getInt8(0);
          case 'uint16':
          case 'UFWORD':
          case 'Offset16':
            return dataView.getUint16(0);
          case 'int16':
          case 'FWORD':
            return dataView.getInt16(0);
          case 'uint24':
          case 'Offset24':
            return dataView.getUint16(0) * 256 + dataView.getUint8(2);
          case 'uint32':
          case 'Offset32':
            return dataView.getUint32(0);
          case 'int32':
            return dataView.getInt32(0);
          case 'Fixed':
            return dataView.getInt16(0) + dataView.getUint16(2) / 2 ** 16;
          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;
          }
          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();
          }
          case 'Tag':
          {
            let asciiString ='';
            for (let i = 0; i < arrayBuffer.byteLength; i ++) {
              asciiString += String.fromCharCode(dataView.getUint8(i));
            }
            return asciiString;
          }
          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) {
        /** @type {TextDecoder} */
        const utf8Decoder = new TextDecoder('utf-8');
          outputString = utf8Decoder.decode(uint8Array);
        } else if (platformID === 1) {
          if (languageID === 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) {
            /** @type {TextDecoder} */
            const utf8Decoder = new TextDecoder('sjis');
            outputString = utf8Decoder.decode(uint8Array);
          }
        } else if (platformID === 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(event) {

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

        /** @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;
          baseGlyphIDs = [];
          /** @type {object[]} */
          baseGlyphs = [];
          height = 0;
          width = 0;
          x = 0;
          y = 0;
        }

        class BaseGlyph {
          /** @type {object[]} */
          colorLayers = [];
          /** @type {object} */
          svgElement = {};
        }

        class ColorLayer {
          /** @type {number[]} */
          formats = [];
          /** @type {object} */
          svgElement = {};
        }

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

        //------------------------------------------------------------
        // ローカル変数の設定
        //------------------------------------------------------------
        /** @namespace */
        const stringData = {
          /** @type {string[]} */
          characters: [],
          codePointString: '',
          /** @type {number[][]} */
          codePoints: [],
          inputString: htmlInputElement('input1').value,
          /** @type {number[][]} */
          inputCodePoints: [],
          /** @type {string[][]} */
          // @ts-ignore
          inputStringArray: Array.from(new Intl.Segmenter('ja-JP').segment(htmlInputElement('input1').value), function (element) { return element.segment; }),
          /** @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: []
        };

        //------------------------------------------------------------
        // 入力文字列に対する処理
        //------------------------------------------------------------
        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');
          });
        });

        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));
          });
        });

        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;
        });

        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;
                }
              }
            }
          }

          // COLR — Color Table
          {
            // COLR header
            font.COLR = {};
            let offset = font.tableDirectory['COLR'].offset;
            font.COLR.baseGlyphListOffset = Valorize.Offset32(offset + 14);
            font.COLR.layerListOffset = Valorize.Offset32(offset + 18);
            font.COLR.clipListOffset = Valorize.Offset32(offset + 22);
            font.COLR.varIndexMapOffset = Valorize.Offset32(offset + 26);
            font.COLR.itemVariationStoreOffset = Valorize.Offset32(offset + 30);
          }

          // CPAL — Color Palette Table
          {
            font.CPAL = [];
            let offset = font.tableDirectory['CPAL'].offset;
            font.CPAL.version = Valorize.uint16(offset);
            font.CPAL.numPalettes = Valorize.uint16(offset + 4);
            font.CPAL.colorRecordsArrayOffset = Valorize.Offset32(offset + 8);
          }

          // 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) {
            let wrapperFlag = '';
            let wrapperCount = 0;
            let wrapperElement = {};
            /** @type {object} */
            const baseGlyph = new BaseGlyph();
            font.baseGlyphs[index1] = baseGlyph;
            baseGlyph.codePointString = '';
            element1.forEach(function (element2) {
              baseGlyph.codePointString += element2.toString(16).toUpperCase().padStart(4, '0');
            });
            baseGlyph.svgElement = svgDocument.createElement('g');
            baseGlyph.svgElement.setAttribute('id', baseGlyph.codePointString);

            //------------------------------------------------------------
            // Base Glyph IDの取得
            //------------------------------------------------------------
            let codePoint = element1[0];
              /** @type {number|undefined} 異体字セレクター */
            let varSelector = element1[1] ? element1[1] : undefined;
            let baseGlyphID = 0;
            baseGlyph.glyphID = baseGlyphID;
            // フォントファイル内のCMapテーブルを使用してCode Pointの値からBase 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;
                    baseGlyphID = (codePoint + Valorize.uint16(offset)) & 0xffff;
                    break;
                  } else {
                    offset = font.cmap.format4.subtableOffset + j * 2 + segCountX2 * 3 + idRangeOffset + (codePoint - startCode) * 2 + 16;
                    baseGlyphID = Valorize.uint16(offset);
                    break;
                  }
                }
              }
            }
            // Format 12
            if (baseGlyphID === 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) {
                  baseGlyphID = (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) {
                        baseGlyphID = Valorize.uint16(offset + 3);
                        break;
                      }
                    }
                  }
                }
              }
            }

            //------------------------------------------------------------
            // Font Tables その2 Base Glyph IDを使用
            //------------------------------------------------------------
            // hmtx - Horizontal Metrics Table
            baseGlyph.hmtx = {};
            if (baseGlyphID <= font.hhea.numberOfHMetrics) {
              let offset = font.tableDirectory['hmtx'].offset + baseGlyphID * 4;
              baseGlyph.hmtx.advanceWidth = Valorize.int16(offset);
            } else {
              let offset = font.tableDirectory['hmtx'].offset + font.hhea.numberOfHMetrics * 4;
              baseGlyph.hmtx.advanceWidth = Valorize.uint16(offset);
            }

            // loca - Index to Location
            baseGlyph.loca = {};
            if (font.head.indexToLocFormat === 0) {
              let offset = font.tableDirectory['loca'].offset + baseGlyphID * 2;
              baseGlyph.loca.offsets = Valorize.Offset16(offset) * 2;
            } else if (font.head.indexToLocFormat === 1) {
              let offset = font.tableDirectory['loca'].offset + baseGlyphID * 4;
              baseGlyph.loca.offsets = Valorize.Offset32(offset);
            }

            // glyf - Glyph Data
            baseGlyph.glyf = {};
            // Glyph Header
            let offset = font.tableDirectory['glyf'].offset + baseGlyph.loca.offsets;
            baseGlyph.glyf.numberOfContours = Valorize.int16(offset);
            baseGlyph.glyf.xMin = Valorize.int16(offset + 2);
            baseGlyph.glyf.yMin = Valorize.int16(offset + 4);
            baseGlyph.glyf.xMax = Valorize.int16(offset + 6);
            baseGlyph.glyf.yMax = Valorize.int16(offset + 8);

            //------------------------------------------------------------
            // BaseGlyph and Layer records
            //------------------------------------------------------------
            // COLR — Color Table
            baseGlyph.CORL = {};
            // BaseGlyphList table
            offset = font.tableDirectory['COLR'].offset + font.COLR.baseGlyphListOffset;
            let numBaseGlyphPaintRecords = Valorize.uint32(offset);
            for (let j = 0; j < numBaseGlyphPaintRecords; j ++) {
              // BaseGlyphPaintRecord
              offset = font.tableDirectory['COLR'].offset + font.COLR.baseGlyphListOffset + j * 6 + 4;
              if (Valorize.uint16(offset) === baseGlyphID) {
                let paintOffset = Valorize.Offset32(offset + 2);
                // Paint Recordを描画
                renderPaint(font.tableDirectory['COLR'].offset + font.COLR.baseGlyphListOffset + paintOffset);
              }
            }

            /**
              * Paint Recordを描画
              * @param {number} offset Paint Recordのオフセット位置
              * @param {object} colorLayer Paint Recordが属するLayer
              */
            function renderPaint(offset, colorLayer = {}) {
              let format = Valorize.uint8(offset);
              if (colorLayer.formats) {
                colorLayer.formats.push(format);
              }
              // 対応していないPaint RecordのFormatの場合、コンソールに出力
              if (format !== 1 && format !== 2 && format !== 4 && format !== 6 && format !== 10 && format !== 12 && format !== 14 && format !== 16 && format !== 18 && format !== 32) {
                console.log(format);
              }
              if (format === 1) {
                // Format 1: PaintColrLayers
                let numLayers = Valorize.uint8(offset + 1);
                let firstLayerIndex = Valorize.uint32(offset + 2);
                for (let j = 0; j < numLayers; j ++) {
                  /** @type {object} */
                  colorLayer = new ColorLayer();
                  colorLayer.formats.push(1);
                  colorLayer.CPALs = [];
                  colorLayer.numLayer = j;
                  font.baseGlyphs[index1].colorLayers.push(colorLayer);
                  offset = font.tableDirectory['COLR'].offset + font.COLR.layerListOffset + firstLayerIndex * 4 + j * 4 + 4;
                  let paintOffset = Valorize.Offset32(offset);
                  renderPaint(font.tableDirectory['COLR'].offset + font.COLR.layerListOffset + paintOffset, colorLayer);
                }
              } else if (format === 2) {
                // Formats 2: PaintSolid
                let paletteIndex = Valorize.uint16(offset + 1);
                colorLayer.alpha = Valorize.F2DOT14(offset + 3);
                colorLayer.type = 'solid';
                colorPalette(paletteIndex, colorLayer, 0);
                renderGlyph(colorLayer)
              } else if (format === 4) {
                // Formats 4: PaintLinearGradient
                colorLayer.type = 'linear';
                let colorLineOffset = Valorize.Offset24(offset + 1);
                let x0 = Valorize.FWORD(offset + 4);
                let y0 = Valorize.FWORD(offset + 6,);
                let x1 = Valorize.FWORD(offset + 8,);
                let y1 = Valorize.FWORD(offset + 10);
                let x2 = Valorize.FWORD(offset + 12);
                let y2 = Valorize.FWORD(offset + 14);
                colorLayer.x1 = x0;
                colorLayer.y1 = y0;
                colorLayer.x2 = x1;
                colorLayer.y2 = y1;
                let rotateAngle = (Math.atan2(y2 - y0, x2 - x0) - Math.atan2(y1 - y0, x1 - x0)) * 180 / Math.PI;
                rotateAngle = (rotateAngle >= 0 && rotateAngle <= 180) || (rotateAngle >= -360 && rotateAngle <= -180) ? rotateAngle - 90 : rotateAngle + 90;
                if (rotateAngle !== 0) {
                  colorLayer.transform = colorLayer.transform ? `rotate(${ rotateAngle } ${ x0 } ${ y0 }), ` + colorLayer.transform : `rotate(${ rotateAngle } ${ x0 } ${ y0 })`;
                }
                // ColorLine table
                offset += colorLineOffset;
                colorLayer.extend = Valorize.uint8(offset);
                colorLayer.numStops = Valorize.uint16(offset + 1);
                offset += 3;
                colorLayer.stopOffsets = [];
                colorLayer.alphas = [];
                for (let j = 0; j < colorLayer.numStops; j ++) {
                  // ColorStop record
                  colorLayer.stopOffsets[j] = Valorize.F2DOT14(offset);
                  let paletteIndex = Valorize.uint16(offset + 2);
                  colorLayer.alphas[j] = Valorize.F2DOT14(offset + 4);
                  colorPalette(paletteIndex, colorLayer, j);
                  offset += 6;
                }
                renderGlyph(colorLayer)
              } else if (format === 6) {
                // Formats 6: PaintRadialGradient
                colorLayer.type = 'radial';
                let colorLineOffset = Valorize.Offset24(offset + 1);
                colorLayer.fx = Valorize.FWORD(offset + 4);
                colorLayer.fy = Valorize.FWORD(offset + 6);
                colorLayer.fr = Valorize.UFWORD(offset + 8);
                colorLayer.cx = Valorize.FWORD(offset + 10);
                colorLayer.cy = Valorize.FWORD(offset + 12);
                colorLayer.r = Valorize.UFWORD(offset + 14);
                // ColorLine table
                offset += colorLineOffset;
                colorLayer.extend = Valorize.uint8(offset);
                colorLayer.numStops = Valorize.uint16(offset + 1);
                offset += 3;
                colorLayer.stopOffsets = [];
                colorLayer.alphas = [];
                for (let j = 0; j < colorLayer.numStops; j ++) {
                  // ColorStop record
                  colorLayer.stopOffsets[j] = Valorize.F2DOT14(offset);
                  let paletteIndex = Valorize.uint16(offset + 2);
                  colorLayer.alphas[j] = Valorize.F2DOT14(offset + 4);
                  colorPalette(paletteIndex, colorLayer, j);
                  offset += 6;
                }
                renderGlyph(colorLayer)
              } else if (format === 10) {
                // Format 10: PaintGlyph
                let paintOffset = Valorize.Offset24(offset + 1);
                colorLayer.glyphID = Valorize.uint16(offset + 4);
                renderPaint(offset + paintOffset, colorLayer);
              } else if (format === 12) {
                // Formats 12: PaintTransform
                let paintOffset = Valorize.Offset24(offset + 1);
                let transformOffset = Valorize.Offset24(offset + 4);
                // Affine2x3 table
                let xx = Valorize.Fixed(offset + transformOffset);
                let yx = Valorize.Fixed(offset + transformOffset + 4);
                let xy = Valorize.Fixed(offset + transformOffset + 8);
                let yy = Valorize.Fixed(offset + transformOffset + 12);
                let dx = Valorize.Fixed(offset + transformOffset + 16);
                let dy = Valorize.Fixed(offset + transformOffset + 20);
                colorLayer.transform = colorLayer.transform ? colorLayer.transform + `, matrix(${ xx } ${ yx } ${ xy } ${ yy } ${ dx } ${ dy })` : `matrix(${ xx } ${ yx } ${ xy } ${ yy } ${ dx } ${ dy })`;
                renderPaint(offset + paintOffset, colorLayer);
              } else if (format === 14) {
                // Formats 14: PaintTranslate
                let paintOffset = Valorize.Offset24(offset + 1);
                let dx = Valorize.FWORD(offset + 4);
                let dy = Valorize.FWORD(offset + 6);
                colorLayer.transform = colorLayer.transform ? colorLayer.transform + `, translate(${ dx } ${ dy })` : `translate(${ dx } ${ dy })`;
                renderPaint(offset + paintOffset, colorLayer);
              } else if (format === 16) {
                // Formats 16: scale in x or y directions relative to the origin.
                let paintOffset = Valorize.Offset24(offset + 1);
                let scaleX = Valorize.F2DOT14(offset + 4);
                let scaleY = Valorize.F2DOT14(offset + 6);
                colorLayer.transform = colorLayer.transform ? colorLayer.transform + `, scale(${ scaleX } ${ scaleY })` : `scale(${ scaleX } ${ scaleY })`;
                renderPaint(offset + paintOffset, colorLayer);
              } else if (format === 18) {
                // Formats 18: scale in x or y directions relative to a specified center.
                let paintOffset = Valorize.Offset24(offset + 1);
                let scaleX = Valorize.F2DOT14(offset + 4);
                let scaleY = Valorize.F2DOT14(offset + 6);
                let centerX = Valorize.FWORD(offset + 8);
                let centerY = Valorize.FWORD(offset + 10);
                colorLayer.transform = colorLayer.transform ? colorLayer.transform + `, translate(${ centerX } ${ centerY }), scale(${ scaleX } ${ scaleY }), translate(${ -1 * centerX } ${ -1 * centerY })` : `translate(${ centerX } ${ centerY }), scale(${ scaleX } ${ scaleY }), translate(${ -1 * centerX } ${ -1 * centerY })`;
                renderPaint(offset + paintOffset, colorLayer);
              } else if (format === 32) {
                // Format 32: PaintComposite
                let sourcePaintOffset = Valorize.Offset24(offset + 1);
                let compositeMode = Valorize.uint8(offset + 4);
                let backdropPaintOffset = Valorize.Offset24(offset + 5);
                if (compositeMode !== 5) {
                  // 対応していないComposite Modeの場合、コンソールに出力
                  console.log(compositeMode);
                }
                if (compositeMode === 5) {
                  // Source In
                  wrapperFlag = 'Source In';
                  wrapperElement = svgDocument.createElement('mask');
                  baseGlyph.svgElement.appendChild(wrapperElement);
                  wrapperElement.setAttribute('id', `${ baseGlyph.codePointString }_${ wrapperCount }`);
                  renderPaint(offset + backdropPaintOffset, colorLayer);
                  wrapperElement = svgDocument.createElement('g');
                  baseGlyph.svgElement.appendChild(wrapperElement);
                  wrapperElement.setAttribute('mask', `url(#${ baseGlyph.codePointString }_${ wrapperCount })`);
                  renderPaint(offset + sourcePaintOffset, colorLayer);
                  wrapperFlag = '';
                  wrapperCount ++;
                }

              }
            }


            /**
              * Color Paletteの色情報を取得
              * @param {number} paletteIndex ColorPalette内の色情報にアクセスするためのインデックス
              * @param {object} colorLayer Color Paletteが属するLayer
              * @param {number} index ColorStopに対応するため配列のインデックス
              */
            function colorPalette(paletteIndex, colorLayer, index) {
              if (font.CPAL.version === 0) {
                const CPAL = {};
                colorLayer.CPALs[index] = CPAL;
                offset = font.tableDirectory['CPAL'].offset + 12;
                let colorRecordIndex = Valorize.uint16(offset);
                offset = font.tableDirectory['CPAL'].offset + font.CPAL.colorRecordsArrayOffset + colorRecordIndex + paletteIndex * 4;
                CPAL.blue = Valorize.uint8(offset);
                CPAL.green = Valorize.uint8(offset + 1);
                CPAL.red = Valorize.uint8(offset + 2);
                CPAL.alpha = Valorize.uint8(offset + 3);
              }
            }

            /**
              * Color Glyphの描画
              * @param {object} colorLayer Color Glyphが属するLayer
              */
            function renderGlyph(colorLayer) {
              /** @type {object} */
              const glyph = new Glyph();
              colorLayer.glyph = glyph;
              if (colorLayer.glyphID) {
                glyph.glyphID = colorLayer.glyphID;
                glyph.glyphIDString = colorLayer.glyphID.toString(16).toUpperCase().padStart(4, '0');

                //------------------------------------------------------------
                // Font Tables その3 Glyph IDを使用
                //------------------------------------------------------------
                // loca - Index to Location
                glyph.loca = {};
                if (font.head.indexToLocFormat === 0) {
                  offset = font.tableDirectory['loca'].offset + glyph.glyphID * 2;
                  glyph.loca.offsets = Valorize.Offset16(offset) * 2;
                  glyph.loca.length = Valorize.Offset16(offset + 2) * 2 - glyph.loca.offsets * 2;
                } else if (font.head.indexToLocFormat === 1) {
                  offset = font.tableDirectory['loca'].offset + glyph.glyphID * 4;
                  glyph.loca.offsets = Valorize.Offset32(offset);
                  glyph.loca.length = Valorize.Offset32(offset + 4) - glyph.loca.offsets;
                }

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

                    // バイナリーデータ(ArrayBufferオブジェクト)を解析してColor Glyphの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;
                    const glyph = colorLayer.glyph;

                    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;
                      }
                    }

                    //------------------------------------------------------------
                    //  Color GlyphのSVGを作成
                    //------------------------------------------------------------
                    glyph.svgElement = svgDocument.createElement('path');
                    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 !== '') {
                      if (colorLayer.type === 'solid') {
                        if (colorLayer.CPALs[0]) {
                          glyph.svgElement.setAttribute('fill', `rgba(${ colorLayer.CPALs[0].red }, ${ colorLayer.CPALs[0].green }, ${ colorLayer.CPALs[0].blue }, ${ colorLayer.CPALs[0].alpha / 255 })`);
                          glyph.svgElement.setAttribute('fill-opacity', colorLayer.alpha);
                        }
                        if (colorLayer.transform) {
                          glyph.svgElement.setAttribute('transform', colorLayer.transform);
                        }
                      } else if (colorLayer.type === 'linear' || colorLayer.type === 'radial') {
                        glyph.svgElement.setAttribute('fill', `url(#${ glyph.glyphIDString }_${ colorLayer.numLayer })`);
                      }
                      glyph.svgElement.setAttribute('d', pathData);
                    }
                  }
                }
              } else {
                if (wrapperFlag === 'Source In') {
                  // Composite Mode 5(Source In)のマスク用
                  // Paint Record format 10を経ず、Glyph IDが指定されずにColor Glyphの描画が行われるため、四角に対してマスクを適用する。
                  glyph.svgElement = svgDocument.createElement('rect');
                  if (colorLayer.type === 'solid') {
                    if (colorLayer.CPALs[0]) {
                      glyph.svgElement.setAttribute('fill', `rgba(255, 255, 255, ${ colorLayer.alpha})`);
                    }
                    if (colorLayer.transform) {
                      glyph.svgElement.setAttribute('transform', colorLayer.transform);
                    }
                  } else if (colorLayer.type === 'linear' || colorLayer.type === 'radial') {
                    // Color Layer Type linearおよびradialに対応していないので、コンソールに出力
                    console.log(colorLayer.type);
                  }
                  glyph.svgElement.setAttribute('width', baseGlyph.hmtx.advanceWidth);
                  glyph.svgElement.setAttribute('height', font.OS_2.usWinAscent + font.OS_2.usWinDescent);
                }
              }
              //------------------------------------------------------------
              // SVG出力の準備 その2
              //------------------------------------------------------------
              // Color GlyphのSVGをLayerに追加
              colorLayer.svgElement = new DocumentFragment();
              if (glyph.glyphIDString) {
                colorLayer.svgElement.appendChild(svgDocument.createComment(` Glyph ID: GID+${ glyph.glyphIDString } `));
              }
              if (colorLayer.type === 'solid') {
                colorLayer.svgElement.appendChild(glyph.svgElement);
              } else if (colorLayer.type === 'linear' || colorLayer.type === 'radial') {
                let gradientElement = {};
                if (colorLayer.type === 'linear') {
                  gradientElement = svgDocument.createElement('linearGradient');
                  gradientElement.setAttribute('id', `${ glyph.glyphIDString }_${ colorLayer.numLayer }`);
                  gradientElement.setAttribute('gradientUnits', 'userSpaceOnUse');
                  gradientElement.setAttribute('x1', colorLayer.x1);
                  gradientElement.setAttribute('y1', colorLayer.y1);
                  gradientElement.setAttribute('x2', colorLayer.x2);
                  gradientElement.setAttribute('y2', colorLayer.y2);
                } else if (colorLayer.type === 'radial') {
                  gradientElement = svgDocument.createElement('radialGradient');
                  gradientElement.setAttribute('id', `${ glyph.glyphIDString }_${ colorLayer.numLayer }`);
                  gradientElement.setAttribute('gradientUnits', 'userSpaceOnUse');
                  gradientElement.setAttribute('fx', colorLayer.fx);
                  gradientElement.setAttribute('fy', colorLayer.fy);
                  gradientElement.setAttribute('fr', colorLayer.fr);
                  gradientElement.setAttribute('cx', colorLayer.cx);
                  gradientElement.setAttribute('cy', colorLayer.cy);
                  gradientElement.setAttribute('r', colorLayer.r);
                }
                if (colorLayer.extend === 1) {
                  gradientElement.setAttribute('spreadMethod', 'repeat');
                } else if (colorLayer.extend === 2) {
                  gradientElement.setAttribute('spreadMethod', 'reflect');
                }
                if (colorLayer.transform) {
                  gradientElement.setAttribute('gradientTransform', colorLayer.transform);
                }
                for (let j = 0; j < colorLayer.numStops; j ++) {
                  const stopElement = svgDocument.createElement('stop');
                  gradientElement.appendChild(stopElement);
                  stopElement.setAttribute('offset', colorLayer.stopOffsets[j]);
                  stopElement.setAttribute('stop-color', `rgba(${ colorLayer.CPALs[j].red }, ${ colorLayer.CPALs[j].green }, ${ colorLayer.CPALs[j].blue }, ${ colorLayer.CPALs[j].alpha / 255 })`);
                  stopElement.setAttribute('stop-opacity', colorLayer.alphas[j]);
                }
                colorLayer.svgElement.appendChild(gradientElement);
                colorLayer.svgElement.appendChild(glyph.svgElement);
              }
              if (wrapperFlag === 'Source In') {
                wrapperElement.appendChild(colorLayer.svgElement);
              } else {
                // LayerのSVGをBase Glyphに追加
                baseGlyph.svgElement.appendChild(colorLayer.svgElement);
              }
            }

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

          //------------------------------------------------------------
          // SVG出力の準備 その4
          //------------------------------------------------------------
          // 入力文字列に対する処理
          // 入力文字列に関する要素をSVGに追加
          /** @type {HTMLElement} */
          const gElement = svgDocument.createElement('g');
          gElement.setAttribute('id', stringData.codePoints.length !== 1 ? stringData.codePointString : '_' + stringData.codePointString);
          {
            /** @type {HTMLElement} */
            const useElement = svgDocument.createElement('use');
            svgElement.appendChild(useElement);
            useElement.setAttribute('href', '#' + (stringData.codePoints.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 baseGlyph = font.baseGlyphs[index2];
            if (index1 === 0) {
              font.actualBoundingBoxLeft = baseGlyph.glyf.xMin;
            }
            font.actualBoundingBoxDescent = baseGlyph.glyf.yMin < font.actualBoundingBoxDescent ? baseGlyph.glyf.yMin : font.actualBoundingBoxDescent;
            if (index1 === array1.length - 1) {
              font.actualBoundingBoxRight = font.fontBoundingBoxWidth + baseGlyph.glyf.xMax;
            }
            font.actualBoundingBoxAscent = baseGlyph.glyf.yMax > font.actualBoundingBoxAscent ? baseGlyph.glyf.yMax : font.actualBoundingBoxAscent;

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

            // 文字列全体の仮想ボディの幅
            font.fontBoundingBoxWidth += baseGlyph.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.fontBoundingBoxWidth;
          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', `${ font.x - font.fontBoundingBoxWidth / 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>

参照


関連情報


コメントを残す

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