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

Author:

ビットマップ(PNG)形式のデータを取得し、SVGソースに埋め込む

カラー絵文字のフォントファイルは、ラスター(ビットマップ)形式だったりベクター形式だったりフォントごとに異なります😶‍🌫️。
ラスター(ビットマップ)形式のカラー絵文字データは、CBDTテーブルに埋め込まれています。カラー絵文字のデータが埋め込まれている場所(オフセット位置)は、CBLCテーブルから取得します。カラー絵文字のデータは、PNG形式となります。
フォントファイル内の画像データは、バイナリーデータのため、Base64エンコードによりテキストデータに変換した後、SVG内に記述する必要があります。

完成例

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

出力: 解析結果

使い方

フォントファイルのTable Directoryを確認し、CBDTおよびCBLCテーブルが存在することを確認します。
SVGで表示したい絵文字を「入力1: 文字列」のテキストボックスに入力します。
「ファイルを選択」ボタンをクリックして、フォントファイル(.ttfまたは.ttc)を開きます。
フォントファイルを開くイベントをきっかけにJavaScriptが動き、「出力: 解析結果」以下にSVGのソースおよびプレビュー画像が表示されます🤗。
(SVGが表示されるのは、Format 17(small metrics, PNG image data)のみとなります。)

動作確認は、Noto Color Emojiで行っています。GitHubから無料で入手することができます(NotoColorEmoji.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;
      }

      /**
        * バイナリーデータ(ArrayBufferオブジェクト)をBase64に変換
        * @param {ArrayBuffer} arrayBuffer
        * @returns {string}
        */
      function arrayBufferToBase64(arrayBuffer) {
        let base64Text = '';
        /** @type {DataView} */
        const dataView = new DataView(arrayBuffer);
        /** @type {string[]} */
        const base64Array = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/',];
        // 3バイト毎に処理
        for (let i = 0, li = arrayBuffer.byteLength / 3; i < li; i ++) {
          if (i <= li - 1 || arrayBuffer.byteLength % 3 === 0) {
            base64Text += base64Array[(dataView.getUint8(i * 3) & 0b11111100) >>> 2];
            base64Text += base64Array[(dataView.getUint16(i * 3) & 0b0000001111110000) >>> 4];
            base64Text += base64Array[(dataView.getUint16(i * 3 + 1) & 0b0000111111000000) >>> 6];
            base64Text += base64Array[dataView.getUint8(i * 3 + 2) & 0b00111111];
          } else if (i > li - 1 && arrayBuffer.byteLength % 3 === 1) {
            base64Text += base64Array[(dataView.getUint8(i * 3) & 0b11111100) >>> 2];
            base64Text += base64Array[dataView.getUint8(i * 3) & 0b00000011];
            base64Text += '==';
          } else if (i > li - 1 && arrayBuffer.byteLength % 3 === 2) {
            base64Text += base64Array[(dataView.getUint8(i * 3) & 0b11111100) >>> 2];
            base64Text += base64Array[(dataView.getUint16(i * 3) & 0b0000001111110000) >>> 4];
            base64Text += base64Array[(dataView.getUint8(i * 3 + 1) & 0b00001111) * 4];
            base64Text += '=';
          }
        }
        return base64Text;
      }

      /**
        * 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[]} */
          glyphs = [];
          height = 0;
          width = 0;
          x = 0;
          y = 0;
        }

        class Glyph {
          codePointString = '';
          /** @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
          //------------------------------------------------------------
          // フォントファイル内のCMapテーブルを使用してCode Pointの値からGlyph IDを取得
          // cmap - Character to Glyph Index Mapping Table
          {
            const cmap = { format4: { subtableOffset: 0 }, format12: { subtableOffset: 0 }, format14: { subtableOffset: 0 } };
            font.cmap = cmap;
            let offset = font.tableDirectory['cmap'].offset;
            let numTables = Valorize.uint16(offset + 2);
            for (let j = 0; j < numTables; j ++) {
              offset = font.tableDirectory['cmap'].offset + j * 8 + 4;
              let platformID = Valorize.uint16(offset);
              let encodingID = Valorize.uint16(offset + 2);
              // Unicode Platform (Platform ID = 0), Encoding ID 3 should be used in conjunction with 'cmap' subtable formats 4 or 6.
              // Windows Platform (Platform ID = 3), Fonts that support only Unicode BMP characters (U+0000 to U+FFFF) on the Windows platform must use encoding 1 with a format 4 subtable.
              if (cmap.format4.subtableOffset === 0 && (platformID === 0 && encodingID === 3) || (platformID === 3 && encodingID === 1)) {
                let subtableOffset = Valorize.Offset32(offset + 4);
                offset = font.tableDirectory['cmap'].offset + subtableOffset;
                let format = Valorize.uint16(offset);
                if (format === 4) {
                  cmap.format4.subtableOffset = font.tableDirectory['cmap'].offset + subtableOffset;
                }
              }
              // Unicode Platform (Platform ID = 0), Encoding ID 4 should be used in conjunction with subtable formats 10 or 12.
              // Windows Platform (Platform ID = 3), Fonts that support Unicode supplementary-plane characters (U+10000 to U+10FFFF) on the Windows platform must use encoding 10 with a format 12 subtable.
              if (cmap.format12.subtableOffset === 0 && (platformID === 0 && encodingID === 4) || (platformID === 3 && encodingID === 10)) {
                let subtableOffset = Valorize.Offset32(offset + 4);
                offset = font.tableDirectory['cmap'].offset + subtableOffset;
                let format = Valorize.uint16(offset);
                if (format === 12) {
                  cmap.format12.subtableOffset = font.tableDirectory['cmap'].offset + subtableOffset;
                }
              }
              // A format 14 subtable must only be used under platform ID 0 and encoding ID 5; and encoding ID 5 should only be used with a format 14 subtable.
              if (cmap.format14.subtableOffset === 0 && platformID === 0 && encodingID === 5) {
                let subtableOffset = Valorize.Offset32(offset + 4);
                offset = font.tableDirectory['cmap'].offset + subtableOffset;
                let format = Valorize.uint16(offset);
                if (format === 14) {
                  cmap.format14.subtableOffset = font.tableDirectory['cmap'].offset + subtableOffset;
                }
              }
            }
          }

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

          // name - Naming Table
          {
            // 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 } `));
          const defsElement = svgDocument.createElement('defs');
          svgElement.appendChild(defsElement);

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

            //------------------------------------------------------------
            // Glyph IDの取得
            //------------------------------------------------------------
            let codePoint = element1[0];
              /** @type {number|undefined} */
            let varSelector = element1[1] ? element1[1] : undefined;
            let glyphID = 0;
            glyph.glyphID = glyphID;
            // Format 4
            if (codePoint >= 0x0000 && codePoint <= 0xffff) {
              let offset = font.cmap.format4.subtableOffset;
              let segCountX2 = Valorize.uint16(offset + 6);
              for (let j = 0; j < segCountX2 / 2; j ++) {
                offset = font.cmap.format4.subtableOffset + j * 2 + 14;
                let startCode = Valorize.uint16(offset + segCountX2 + 2);
                if (codePoint <= Valorize.uint16(offset) && codePoint >= startCode) {
                  let idRangeOffset = Valorize.uint16(offset + 3 * segCountX2 + 2);
                  if (idRangeOffset === 0) {
                    offset = font.cmap.format4.subtableOffset + j * 2 + segCountX2 * 2 + 16;
                    glyphID = (codePoint + Valorize.uint16(offset)) & 0xffff;
                    break;
                  } else {
                    offset = font.cmap.format4.subtableOffset + j * 2 + segCountX2 * 3 + idRangeOffset + (codePoint - startCode) * 2 + 16;
                    glyphID = Valorize.uint16(offset);
                    break;
                  }
                }
              }
            }
            // Format 12
            if (glyphID === 0 && codePoint >= 0x0000 && codePoint <= 0x10ffff) {
              let offset = font.cmap.format12.subtableOffset;
              let numGroups = Valorize.uint32(offset + 12);
              for (let j = 0; j < numGroups; j ++) {
                offset = font.cmap.format12.subtableOffset + j * 12 + 16;
                let startCharCode = Valorize.uint32(offset);
                let endCharCode = Valorize.uint32(offset + 4);
                if (codePoint >= startCharCode && codePoint <= endCharCode) {
                    glyphID = (codePoint - startCharCode) + Valorize.uint32(offset + 8);
                    break;
                }
              }
            }
            // Format 14
            if (varSelector) {
              let offset = font.cmap.format14.subtableOffset;
              let numVarSelectorRecords = Valorize.uint32(offset + 6);
              for (let j = 0; j < numVarSelectorRecords; j ++) {
                offset = font.cmap.format14.subtableOffset + j * 11 + 10;
                if (Valorize.uint24(offset) === varSelector) {
                  let nonDefaultUVSOffset = Valorize.Offset32(offset + 7);
                  if (nonDefaultUVSOffset !== 0) {
                    offset = font.cmap.format14.subtableOffset + nonDefaultUVSOffset;
                    let numUVSMappings = Valorize.uint32(offset);
                    for (let k = 0; k < numUVSMappings; k++) {
                      offset = font.cmap.format14.subtableOffset + nonDefaultUVSOffset + k * 5 + 4;
                      let unicodeValue = Valorize.uint24(offset);
                      if (unicodeValue === codePoint) {
                        glyphID = Valorize.uint16(offset + 3);
                        break;
                      }
                    }
                  }
                }
              }
            }

            //------------------------------------------------------------
            // Font Tables その2 Glyph IDを使用
            //------------------------------------------------------------
            // CBLC — Color Bitmap Location Table
            // CblcHeader
            {
              glyph.CBLC = {};
              let offset = font.tableDirectory['CBLC'].offset;
              let numSizes = Valorize.uint32(offset + 4);
              // BitmapSize record
              for (let j = 0; j < numSizes; j ++) {
                offset = font.tableDirectory['CBLC'].offset + j * 48 + 8;
                let indexSubtableListOffset = Valorize.Offset32(offset);
                let numberOfIndexSubtables = Valorize.uint32(offset + 8);
                glyph.CBLC.ascender = Valorize.int8(offset + 16);
                glyph.CBLC.descender = Valorize.int8(offset + 17);
                // IndexSubtableList
                // IndexSubtableList table
                for (let k = 0; k < numberOfIndexSubtables; k ++) {
                  offset = font.tableDirectory['CBLC'].offset + indexSubtableListOffset + k * 8;
                  let firstGlyphIndex = Valorize.uint16(offset);
                  let lastGlyphIndex = Valorize.uint16(offset + 2);
                  let indexSubtableOffset = Valorize.Offset32(offset + 4);
                  if (glyphID >= firstGlyphIndex && glyphID <= lastGlyphIndex) {
                    offset = font.tableDirectory['CBLC'].offset + indexSubtableListOffset + indexSubtableOffset;
                    let indexFormat = Valorize.uint16(offset);
                    glyph.CBLC.imageFormat = Valorize.uint16(offset + 2);
                    glyph.CBLC.imageDataOffset = Valorize.Offset32(offset + 4);
                    // Noto Color Emoji は Format 1 のみ
                    if (indexFormat === 1) {
                      // IndexSubtableFormat1 table
                      offset = font.tableDirectory['CBLC'].offset + indexSubtableListOffset + indexSubtableOffset + (glyphID - firstGlyphIndex) * 4 + 8;
                      glyph.CBLC.sbitOffsets = Valorize.Offset32(offset);
                    } else {
                      // 対応していないIndexSubtableのFormatの場合、コンソールに出力
                      console.log(indexFormat);
                    }
                  }
                }
              }
            }
            // CBDT — Color Bitmap Data Table
            // Noto Color Emoji は Format 17 のみ
            // Format 17: small metrics, PNG image data
            {
              glyph.CBDT = {};
              let offset = font.tableDirectory['CBDT'].offset + glyph.CBLC.imageDataOffset + glyph.CBLC.sbitOffsets;
              glyph.CBDT.height = Valorize.uint8(offset);
              glyph.CBDT.width = Valorize.uint8(offset + 1);
              let dataLen = Valorize.uint32(offset + 5);

              //------------------------------------------------------------
              // SVG出力の準備 その2
              //------------------------------------------------------------
              // PNG画像データを取得し、SVGに追加
              defsElement.appendChild(svgDocument.createComment(` Character: \u0022${ stringData.characters[index1] }\u0022 `));
              glyph.svgElement = svgDocument.createElement('image');
              glyph.svgElement.setAttribute('id', glyph.codePointString);
              // Raw PNG dataをBase64に変換し、image要素のhref属性にセット
              glyph.svgElement.setAttribute('href', 'data:image/png;base64,' + arrayBufferToBase64(arrayBuffer.slice(offset + 9, offset + 9 + dataLen)));
              defsElement.appendChild(glyph.svgElement);
            }
          });

          //------------------------------------------------------------
          // SVG出力の準備 その3
          //------------------------------------------------------------
          // 入力文字列に対する処理
          // 入力文字列に関する要素を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 glyph = font.glyphs[index2];
            if (index1 === 0) {
              font.actualBoundingBoxLeft = 0;
            }
            font.actualBoundingBoxDescent = glyph.CBLC.descender < font.actualBoundingBoxDescent ? glyph.CBLC.descender : font.actualBoundingBoxDescent;
            if (index1 === array1.length - 1) {
              font.actualBoundingBoxRight = font.fontBoundingBoxWidth + glyph.CBDT.width;
            }
            font.actualBoundingBoxAscent = glyph.CBLC.ascender > font.actualBoundingBoxAscent ? glyph.CBLC.ascender : font.actualBoundingBoxAscent;

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

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

          defsElement.appendChild(svgDocument.createComment(` ${ stringData.inputString }, width: ${ font.fontBoundingBoxWidth }, height: ${ font.actualBoundingBoxAscent - font.actualBoundingBoxDescent } `));
          // フォントサイズの変更や中央揃えがしやすいように、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.width = font.actualBoundingBoxRight > font.fontBoundingBoxWidth ? font.actualBoundingBoxRight - font.x : font.fontBoundingBoxWidth - font.x;
          font.height = font.y - font.actualBoundingBoxDescent;
          svgElement.setAttribute('viewBox', `${ -1 * font.width / 2 } ${ -1 * font.y } ${ font.width } ${ font.height }`);

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

      });

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

参照


関連情報


コメントを残す

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