JavaScriptでフォントファイルを解析する その2 文字のGlyph IDおよび座標データを取得する

Author:

文字のGlyph IDおよび座標データを取得する

フォントファイル内には、文字(Glyph)ごとに書体デザインを規定したデータが存在しますので、JavaScriptを使用してデータを確認したいと思います😤。 フォントファイル内のCMapテーブル(Character to Glyph Index Mapping Table)を使用し、文字のCode PointからGlyph IDを取得します。
取得したGlyph IDおよびglyfテーブル(Glyph Data)から、文字の座標データを取得します。
「Code PointとGlyph IDの対応表」およびCode Pointごとに「座標の解析結果の表」を表示します。

完成例

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

出力: 解析結果

使い方

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

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

ソース

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>フォントファイルを解析するJavaScript グリフの座標データを取得する</title>
  </head>
  <body>
    入力1: 文字列 <input type="text" id="input1" value="stay awhile𩸽葛葛󠄀厩厩󠄀厩󠄁厩󠄃😤😊" /><br />
    入力2: フォントファイル <input type="file" id="input2" /><br />
    <br />
    出力: 解析結果<br />
    <br />
    <div id="output1"></div>
    <div id="output2"></div>
    <script>

      // @ts-check

      /**
      * 標準組み込みオブジェクト
      * @typeof {object} ArrayBuffer
      * @typeof {object} DataView
      * Web API
      * @typeof {object} FileList
      * @typeof {object} HTMLElement
      * @typeof {object} HTMLInputElement
      * @typeof {object} HTMLTableElement
      * @typeof {object} HTMLTableCaptionElement
      * @typeof {object} HTMLTableCellElement
      * @typeof {object} HTMLTableRowElement
      * @typeof {object} HTMLTableSectionElement
      */

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

      /**
      * バイナリーデータ(ArrayBufferオブジェクト)を16進数テキストに変換
      * @param {ArrayBuffer} arrayBuffer 16進数テキストに変換するArrayBufferオブジェクトによるバイナリーデータ
      * @return {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 データタイプ
      * @return {(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;
        }
      }

      /**
      * 2次元配列を表として出力
      * @param {(string|number)[][]} array 表として出力する2次元配列
      * @param {string} id 表の出力先のHTML要素のID
      * @param {string} caption 表のタイトル
      */
      function outputTable(array, id, caption = '') {
        // ts-check エラー対策 変数を用意し、nullを排除し、HTMLElementとする
        const htmlElement = /** @type {!HTMLElement} */ (document.getElementById(id));
        /** @type {HTMLTableElement} */
        const tableElement = document.createElement('table');
        htmlElement.appendChild(tableElement);
        if (caption) {
          /** @type {HTMLTableCaptionElement} */
          const captionElement = document.createElement('caption');
          tableElement.appendChild(captionElement);
          captionElement.appendChild(document.createTextNode(caption));
        }
        /** @type {HTMLTableSectionElement} */
        const theadElement = document.createElement('thead');
        tableElement.appendChild(theadElement);
        /** @type {HTMLTableSectionElement} */
        const tbodyElement = document.createElement('tbody');
        tableElement.appendChild(tbodyElement);
        array.forEach(function (element1, index1) {
          /** @type {HTMLTableRowElement} */
          const trElement = document.createElement('tr');
          if (index1 === 0) {
            theadElement.appendChild(trElement);
          } else {
            tbodyElement.appendChild(trElement);
          }
          element1.forEach(function (element2) {
            /** @type {HTMLTableCellElement} */
            let tdElement;
            if (index1 === 0) {
              tdElement = document.createElement('th');
            } else {
              tdElement = document.createElement('td');
            }
            trElement.appendChild(tdElement);
            tdElement.appendChild(document.createTextNode(String(element2)));
          });
        });
        htmlElement.appendChild(document.createElement('br'));
      }

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

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

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

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

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

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

        class Font {
          /** @type {number[]} */
          glyphIDs = [];
          /** @type {object[]} */
          glyphs = [];
        }

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

        // ts-check エラー対策 変数を用意し、nullを排除し、FileListとする
        const fileList = /** @type {!FileList} */ (this.files);
        /** @type {ArrayBuffer} */
        const arrayBuffer = await fileList[0].arrayBuffer();

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

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

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

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

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

        //------------------------------------------------------------
        // フォントファイルを解析する
        //------------------------------------------------------------
        //------------------------------------------------------------
        // Font Collections
        //------------------------------------------------------------
        // The Font Collection File Structure
        // TTC Header
        let numFonts = 1;
        /** @type {number[]} */
        const tableDirectoryOffsets = [];
        /** @memberof fontCollection */
        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テーブルを使用してCode Pointの値からGlyph IDを取得
          // cmap - Character to Glyph Index Mapping Table
          // 主要フォーマットのサブテーブルのオフセット位置を取得
          {
            /** @type {object} */
            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;
                }
              }
            }
          }

          // フォントファイル内のglyfテーブルを使用して文字の座標データを取得
          // head - Font Header Table
          {
            font.head = {};
            let offset = font.tableDirectory['head'].offset;
            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);
          }

          {
            //------------------------------------------------------------
            // Code PointとGlyph IDの対応表を作成
            //------------------------------------------------------------
            /** @type {(number|string)[][]} */
            let table = [];
            table[0] = ['#', 'Character', 'Code Point', 'Variation Selector', 'Glyph ID', '', 'length', 'advanceWidth (uint16)', 'lsb(int16)', 'rsb', 'numberOfContours (int16)', 'xMin (int16)', 'yMin (int16)', 'xMax (int16)', 'yMax (int16)'];
            if (font.head.indexToLocFormat === 0) {
              table[0][5] = 'offsets (Offset16)';
            } else if (font.head.indexToLocFormat === 1) {
              table[0][5] = 'offsets (Offset32)';
            }

            // 重複する文字を除去した入力文字配列からCode Pointごとに処理
            stringData.codePoints.forEach(function (element, index) {
              /** @type {object} */
              const glyph = {};
              font.glyphs[index] = glyph;

              //------------------------------------------------------------
              // Glyph IDの取得
              //------------------------------------------------------------
              let codePoint = element[0];
                /** @type {number|undefined} 異体字セレクター */
              let varSelector = element[1] ? element[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;
                        }
                      }
                    }
                  }
                }
              }

              table[index + 1] = [];
              table[index + 1][0] = (index + 1).toString();
              table[index + 1][1] = stringData.characters[index];
              table[index + 1][2] = codePoint.toString(16).toUpperCase().padStart(4, '0');
              table[index + 1][3] = varSelector ? varSelector.toString(16).toUpperCase().padStart(4, '0') : '-';
              table[index + 1][4] = glyphID.toString(16).toUpperCase().padStart(4, '0');
              // loca - Index to Location
              /** @type {object} */
              const loca = {}
              glyph.loca = loca;
              if (font.head.indexToLocFormat === 0) {
                /** @type {number} */
                let offset = font.tableDirectory['loca'].offset + glyphID * 2;
                table[index + 1][5] = Textize.Offset16(offset);
                loca.offsets = Valorize.Offset16(offset) * 2;
                table[index + 1][6] = Valorize.Offset16(offset + 2) * 2 - loca.offsets * 2;
                loca.length = Valorize.Offset16(offset + 2) * 2 - loca.offsets * 2;
              } else if (font.head.indexToLocFormat === 1) {
                let offset = font.tableDirectory['loca'].offset + glyphID * 4;
                table[index + 1][5] = Textize.Offset32(offset);
                loca.offsets = Valorize.Offset32(offset);
                table[index + 1][6] = Valorize.Offset32(offset + 4) - loca.offsets;
                loca.length = Valorize.Offset32(offset + 4) - loca.offsets;
              }
              // hmtx - Horizontal Metrics Table
              /** @type {object} */
              const hmtx = {};
              glyph.hmtx = hmtx;
              if (glyphID <= font.hhea.numberOfHMetrics) {
                let offset = font.tableDirectory['hmtx'].offset + glyphID * 4;
                table[index + 1][7] = Valorize.UFWORD(offset);
                hmtx.advanceWidth = Valorize.UFWORD(offset);
                table[index + 1][8] = Valorize.FWORD(offset + 2);
                hmtx.lsb = Valorize.FWORD(offset + 2);
              } else {
                let offset = font.tableDirectory['hmtx'].offset + font.hhea.numberOfHMetrics * 4;
                table[index + 1][7] = Valorize.UFWORD(offset);
                hmtx.advanceWidth = Valorize.UFWORD(offset);
                table[index + 1][8] = Valorize.FWORD(offset + (font.hhea.numberOfHMetrics - glyphID) * 2);
                hmtx.lsb = Valorize.FWORD(offset + (font.hhea.numberOfHMetrics - glyphID) * 2);
              }
              // glyf - Glyph Data
              /** @type {object} */
              const glyf = {};
              glyph.glyf = glyf;
              // Glyph Header
              let offset = font.tableDirectory['glyf'].offset + loca.offsets;
              table[index + 1][10] = Valorize.int16(offset);
              glyf.numberOfContours = Valorize.int16(offset);
              table[index + 1][11] = Valorize.int16(offset + 2);
              glyf.xMin = Valorize.int16(offset + 2);
              table[index + 1][12] = Valorize.int16(offset + 4);
              glyf.yMin = Valorize.int16(offset + 4);
              table[index + 1][13] = Valorize.int16(offset + 6);
              glyf.xMax = Valorize.int16(offset + 6);
              table[index + 1][14] = Valorize.int16(offset + 8);
              glyf.yMax = Valorize.int16(offset + 8);
              // rsbの計算 rsb = advanceWidth - (lsb + xMax - xMin)
              table[index + 1][9] = glyph.hmtx.advanceWidth - glyph.hmtx.lsb - (glyf.xMax - glyf.xMin);
              //------------------------------------------------------------
              //  Simple Glyph Descriptionの解析
              //------------------------------------------------------------
              // Simple Glyph table
              glyf.endPtsOfContours = [];
              if (glyf.numberOfContours >= 0 && loca.length > 0) {
                let offset = font.tableDirectory['glyf'].offset + loca.offsets + 10;
                for (let j = 0; j < glyf.numberOfContours; j ++) {
                  glyf.endPtsOfContours[j] = Valorize.uint16(offset + j * 2);
                }
                offset = font.tableDirectory['glyf'].offset + loca.offsets + glyf.numberOfContours * 2 + 10;
                let instructionLength = Valorize.uint16(offset);
                offset = font.tableDirectory['glyf'].offset + loca.offsets + glyf.numberOfContours * 2 + instructionLength + 12;

                /**
                * 即時実行関数
                * バイナリーデータ(ArrayBufferオブジェクト)内のflags、xCoordinates、yCoordinatesを解析し、グリフの座標データを取得し、表示する
                * @param {ArrayBuffer} arrayBuffer Simple glyph descriptionのflags、xCoordinates、yCoordinatesを含むバイナリーデータ
                */
                (function (arrayBuffer) {

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

                  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 int16(offset) { return /** @type {number }*/ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 2), 'int16')); }
                  }

                  // 座標の解析結果の表を作成
                  /** @type {(number|string)[][]} */
                  let table = [];
                  let offset = 0;
                  /** @type {number[]} */
                  let flags = [];
                  let repeatCount = 0;
                  let repeatFlag = 0;
                  let xCoordinate = 0;
                  let yCoordinate = 0;
                  table[0] = ['#', 'flags (2進数)', 'ON_CURVE_POINT', 'X_SHORT_VECTOR', 'Y_SHORT_VECTOR', 'REPEAT_FLAG', 'X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR', 'Y_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR', 'OVERLAP_SIMPLE', 'Delta X', 'Delta Y', 'X', 'Y'];

                  // flagsの解析
                  for (let i = 0; i <= glyf.endPtsOfContours[glyf.numberOfContours - 1]; i ++) {
                    table[i + 1] = [];
                    table[i + 1][0] = i.toString();
                    if (repeatCount > 0) {
                      table[i + 1][1] = `(${ repeatFlag.toString(2).padStart(8, '0') })`;
                      table[i + 1][5] = '-';
                      flags[i] = repeatFlag;
                      repeatCount --;
                    } else {
                      table[i + 1][1] = Valorize.uint8(offset).toString(2).padStart(8, '0');
                      flags[i] = Valorize.uint8(offset);
                      offset += 1;
                      if (flags[i] & 0b00001000) {
                        repeatFlag = flags[i];
                        repeatCount = Valorize.uint8(offset);
                        offset += 1;
                        table[i + 1][5] = `True (${ repeatCount })`;
                      } else {
                        table[i + 1][5] = '-';
                      }
                    }

                    if (flags[i] & 0b00000001) {
                      table[i + 1][2] = 'True (On the Curve)';
                    } else {
                      table[i + 1][2] = '-';
                    }

                    if (flags[i] & 0b01000000) {
                      table[i + 1][8] = 'True (Overlap)';
                    } else {
                      table[i + 1][8] = '-';
                    }
                  }

                  // xCoordinatesの解析
                  for (let i = 0; i <= glyf.endPtsOfContours[glyf.numberOfContours - 1]; i ++) {
                    let deltaX = 0;
                    if (flags[i] & 0b00000010) {
                      table[i + 1][3] = 'True (uint8)';
                      if (flags[i] & 0b00010000) {
                        table[i + 1][6] = 'True (Positive)';
                        table[i + 1][9] = Valorize.uint8(offset);
                        deltaX = Valorize.uint8(offset);
                      } else {
                        table[i + 1][6] = 'False (Negative)';
                        table[i + 1][9] = -1 * Valorize.uint8(offset);
                        deltaX = -1 * Valorize.uint8(offset);
                      }
                      offset += 1;
                    } else {
                      table[i + 1][3] = 'False (int16)';
                      if (flags[i] & 0b00010000) {
                        table[i + 1][6] = 'True (Same)';
                        table[i + 1][9] = 0;
                      } else {
                        table[i + 1][6] = 'False (Signed Delta)';
                        table[i + 1][9] = Valorize.int16(offset);
                        deltaX = Valorize.int16(offset);
                        offset += 2;
                      }
                    }
                    xCoordinate += deltaX;
                    table[i + 1][11] = xCoordinate;
                  }

                  // yCoordinatesの解析
                  for (let i = 0; i <= glyf.endPtsOfContours[glyf.numberOfContours - 1]; i ++) {
                    let deltaY = 0;
                    if (flags[i] & 0b00000100) {
                      table[i + 1][4] = 'True (uint8)';
                      if (flags[i] & 0b00100000) {
                        table[i + 1][7] = 'True (Positive)';
                        table[i + 1][10] = Valorize.uint8(offset);
                        deltaY = Valorize.uint8(offset);
                      } else {
                        table[i + 1][7] = 'False (Negative)';
                        table[i + 1][10] = -1 * Valorize.uint8(offset);
                        deltaY = -1 * Valorize.uint8(offset);
                      }
                      offset += 1;
                    } else {
                      table[i + 1][4] = 'False (int16)';
                      if (flags[i] & 0b00100000) {
                        table[i + 1][7] = 'True (Same)';
                        table[i + 1][10] = 0;
                      } else {
                        table[i + 1][7] = 'False (Signed Delta)';
                        table[i + 1][10] = Valorize.int16(offset);
                        deltaY = Valorize.int16(offset);
                        offset += 2;
                      }
                    }
                    yCoordinate += deltaY;
                    table[i + 1][12] = yCoordinate;
                  }

                  // 座標の解析結果の表の出力
                  outputTable(table, 'output2', `Simple Glyph Description (Character: ${ stringData.characters[index] }, Glyph ID: G+${ glyphID.toString(16).padStart(4, '0') })`);

                } (arrayBuffer.slice(offset, font.tableDirectory['glyf'].offset + loca.offsets + loca.length)));

              }
            });

            // Code PointとGlyph IDの対応表を出力
            outputTable(table, 'output1', `Code PointとGlyph IDの対応表 (Font No. ${ i + 1 })`);

          }

        }

      });

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

参照


関連情報


コメントを残す

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