フォントファイルの最初の部分(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>