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

Author:

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

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

完成例

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

出力: 解析結果

使い方

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

動作確認は、Segoe UI Emoji(2021年版、2D Fluent Design System)で行っています。

ソース

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

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

        class ColorLayer {
          /** @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.numBaseGlyphRecords = Valorize.uint16(offset + 2);
            font.COLR.baseGlyphRecordsOffset = Valorize.Offset32(offset + 4);
            font.COLR.layerRecordsOffset = Valorize.Offset32(offset + 8);
            font.COLR.numLayerRecords = Valorize.uint16(offset + 12);
          }

          // 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) {
            /** @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 = {};
            for (let j = 0; j < font.COLR.numBaseGlyphRecords; j ++) {
              // BaseGlyph record
              offset = font.tableDirectory['COLR'].offset + font.COLR.baseGlyphRecordsOffset + j * 6;
              if (Valorize.uint16(offset) === baseGlyphID) {
                let firstLayerIndex = Valorize.uint16(offset + 2);
                let numLayers = Valorize.uint16(offset + 4);
                for (let k = 0; k < numLayers; k ++) {
                  //------------------------------------------------------------
                  // Layer
                  //------------------------------------------------------------
                  /** @type {object} */
                  const colorLayer = new ColorLayer();
                  baseGlyph.colorLayers[k] = colorLayer;
                  // Layer record
                  offset = font.tableDirectory['COLR'].offset + font.COLR.layerRecordsOffset + firstLayerIndex * 4 + k * 4;
                  colorLayer.glyphID = Valorize.uint16(offset);
                  let paletteIndex = Valorize.uint16(offset + 2);
                  //------------------------------------------------------------
                  // Color Palette
                  //------------------------------------------------------------
                  if (font.CPAL.version === 0) {
                    const CPAL = {};
                    colorLayer.CPAL = 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
                  //------------------------------------------------------------
                  /** @type {object} */
                  const glyph = new Glyph();
                  colorLayer.glyph = glyph;
                  glyph.glyphID = colorLayer.glyphID;
                  glyph.glyphIDString = glyph.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.CPAL) {
                          glyph.svgElement.setAttribute('fill', `rgba(${ colorLayer.CPAL.red }, ${ colorLayer.CPAL.green }, ${ colorLayer.CPAL.blue }, ${ colorLayer.CPAL.alpha / 255 })`);
                        }
                        glyph.svgElement.setAttribute('d', pathData);
                      }

                    }
                  }

                  //------------------------------------------------------------
                  // SVG出力の準備 その2
                  //------------------------------------------------------------
                  // Color GlyphのSVGをLayerに追加
                  colorLayer.svgElement = new DocumentFragment();
                  colorLayer.svgElement.appendChild(svgDocument.createComment(` Glyph ID: GID+${ glyph.glyphIDString } `));
                  colorLayer.svgElement.appendChild(glyph.svgElement);
                  // 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>

参照


関連情報


コメントを残す

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