JavaScriptでフォントファイルを解析する その1 TTC HeaderおよびTable Directoryを表示する

Author:

フォントファイルの最初の部分(TTC HeaderおよびTable Directory)を表示する

フォントファイルは通常、Table Directoryから始まります。Table Directoryには、各フォントテーブルの名前、オフセット位置、長さがまとめられており、フォントファイルのバイナリーデータを解析することで、フォントファイル内にどのテーブルが存在するか確認することができます。
また複数のフォントを1つのファイルにまとめたフォントコレクションファイルの場合は、TTC HeaderがTable Directoryの前に存在します。フォントコレクションファイルか否かは最初の4バイトの値で判断できます。
フォントファイルの最初の部分を解析し、TTC HeaderおよびTable Directoryを表示します。

完成例

入力: フォントファイル


出力: 解析結果

使い方

「ファイルを選択」ボタンをクリックして、フォントファイル(.ttfまたは.ttc)を開きます。フォントファイルを開くイベントをきっかけにTTC HeaderおよびTable Directoryを表示するJavaScriptが動き、「出力: 解析結果」以下に表示されます🤗。

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

      /**
        * 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'));
      }

      //------------------------------------------------------------
      // イベント  type="file"のインプット要素でファイルを選択時
      //------------------------------------------------------------
      // ts-check エラー対策 変数を用意し、nullを排除し、HTMLElementではなくHTMLInputElementとする
      const htmlInputElement = /** @type {!HTMLInputElement} */ (document.getElementById('input'));
      /** @this HTMLInputElement */
      htmlInputElement.addEventListener('change', async function() {

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

        /** @classdesc データタイプに合わせてバイナリーデータを切り取って値に変換 */
        class Valorize {
          /** @param {number} offset 切り取りを開始するオフセット位置 @return {number} 値 */
          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)); }
        }

        //------------------------------------------------------------
        // ローカル変数の設定 変更不要
        //------------------------------------------------------------
        // ts-check エラー対策 変数を用意し、nullを排除し、FileListとする
        const fileList = /** @type {!FileList} */ (this.files);
        // Fileオブジェクトは、Blobオブジェクトからメソッドを継承
        // Blob.prototype.arrayBuffer()
        // Fileをストリームに変換し、最後まで読み込む
        /** @type {ArrayBuffer} */
        const arrayBuffer = await fileList[0].arrayBuffer();

        //------------------------------------------------------------
        // フォントファイルを解析
        //------------------------------------------------------------

        //------------------------------------------------------------
        // Font Collection FileのTTC Headerの表示
        //------------------------------------------------------------
        // The Font Collection File Structure
        // TTC Header
        let numFonts = 1;
        /** @type {number[]} */
        const tableDirectoryOffsets = [];
        // Font Collection Fileか否かを最初の4バイトの値で判断
        if (Valorize.Tag(0) === 'ttcf') {
          /** @type {(number|string)[][]} */
          let table = [];
          let offset = 0;
          table[0] = ['#', '名前', 'データタイプ', '16進数文字列', '値'];
          table[1] = ['1', 'ttcTag', 'Tag', Textize.Tag(offset), Valorize.Tag(offset)];
          table[2] = ['2', 'majorVersion', 'uint16', Textize.uint16(offset + 4), Valorize.uint16(offset + 4)];
          table[3] = ['3', 'minorVersion', 'uint16', Textize.uint16(offset + 6), Valorize.uint16(offset + 6)];
          table[4] = ['4', 'numFonts', 'uint32', Textize.uint32(offset + 8), Valorize.uint32(offset + 8)];
          numFonts = Valorize.uint32(offset + 8);
          for (let i = 0; i < numFonts; i ++) {
            offset = i * 4 + 12;
            table[5 + i] = ['5-' + (i + 1).toString(), 'tableDirectoryOffsets', 'Offset32', Textize.Offset32(offset), Valorize.Offset32(offset)];
            tableDirectoryOffsets[i] = Valorize.Offset32(offset);
          }
          if (table[2][4] === 1) {
            outputTable(table, 'output', 'TTC Header Version 1.0');
          } else if (table[2][4] === 2) {
            offset = numFonts * 4 + 12;
            table[6 + numFonts - 1] = ['6', 'dsigTag', 'Tag', Textize.Tag(offset), Valorize.Tag(offset)];
            table[7 + numFonts - 1] = ['7', 'dsigLength', 'uint32', Textize.uint32(offset + 4), Valorize.uint32(offset + 4)];
            table[8 + numFonts - 1] = ['8', 'dsigOffset', 'Offset32', Textize.Offset32(offset + 8), Valorize.Offset32(offset + 8)];
            outputTable(table, 'output', 'TTC Header Version 2.0');
          }
        }

        //------------------------------------------------------------
        // Font FileのTable Directoryの表示
        //------------------------------------------------------------
        for (let i = 0; i < numFonts; i ++) {
          // Table Directory
          /** @type {(number|string)[][]} */
          let table = [];
          let offset = tableDirectoryOffsets[i] ? tableDirectoryOffsets[i] : 0;
          table[0] = ['#', '名前', 'データタイプ', '16進数文字列', '値'];
          table[1] = ['1', 'sfntVersion', 'uint32', Textize.uint32(offset), Valorize.uint32(offset)];
          table[2] = ['2', 'numTables', 'uint16', Textize.uint16(offset + 4), Valorize.uint16(offset + 4)];
          let numTables =  Valorize.uint16(offset + 4);
          table[3] = ['3', 'searchRange', 'uint16', Textize.uint16(offset + 6), Valorize.uint16(offset + 6)];
          table[4] = ['4', 'entrySelector', 'uint16', Textize.uint16(offset + 8), Valorize.uint16(offset + 8)];
          table[5] = ['5', 'rangeShift', 'uint16', Textize.uint16(offset + 10), Valorize.uint16(offset + 10)];
          outputTable(table, 'output', `Table Directory (Font No. ${ i + 1 })`);
          offset += 12;
          // Table Record (Font No #)
          table = [];
          table[0] = ['#', 'tableTag (Tag)', 'checksum (uint32)', 'offset (Offset32)', 'offset (値)', 'length (uint32)', 'length (値)'];
          for (let j = 0; j < numTables; j ++) {
            offset = tableDirectoryOffsets[i] ? tableDirectoryOffsets[i] + j * 16 + 12 : j * 16 + 12;
            table[j + 1] = [(j + 1).toString(), Valorize.Tag(offset), Textize.uint32(offset + 4), Textize.Offset32(offset + 8), Valorize.Offset32(offset + 8), Textize.uint32(offset + 12), Valorize.uint32(offset + 12)];
          }
          outputTable(table, 'output', `Table Record (Font No. ${ i + 1 })`);
        }

      });

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

参照


関連情報


コメントを残す

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