文字の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>