ビットマップ(PNG)形式のデータを取得し、SVGソースに埋め込む
カラー絵文字のフォントファイルは、ラスター(ビットマップ)形式だったりベクター形式だったりフォントごとに異なります😶🌫️。
ラスター(ビットマップ)形式のカラー絵文字データは、CBDTテーブルに埋め込まれています。カラー絵文字のデータが埋め込まれている場所(オフセット位置)は、CBLCテーブルから取得します。カラー絵文字のデータは、PNG形式となります。
フォントファイル内の画像データは、バイナリーデータのため、Base64エンコードによりテキストデータに変換した後、SVG内に記述する必要があります。
完成例
入力1: 文字列入力2: フォントファイル
出力: 解析結果
使い方
フォントファイルのTable Directoryを確認し、CBDTおよびCBLCテーブルが存在することを確認します。SVGで表示したい絵文字を「入力1: 文字列」のテキストボックスに入力します。
「ファイルを選択」ボタンをクリックして、フォントファイル(.ttfまたは.ttc)を開きます。
フォントファイルを開くイベントをきっかけにJavaScriptが動き、「出力: 解析結果」以下にSVGのソースおよびプレビュー画像が表示されます🤗。
(SVGが表示されるのは、Format 17(small metrics, PNG image data)のみとなります。)
動作確認は、Noto Color Emojiで行っています。GitHubから無料で入手することができます(NotoColorEmoji.ttf)。
Noto Color Emojiのライセンスは、「SIL Open Font License 1.1」(OFL-1.1)で個人利用・商用にかかわらず無償で利用できます。
ソース
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>フォントファイルを解析するJavaScript</title> </head> <body> 入力1: 文字列 <input type="text" id="input1" value="😤😊📛🫠🎛🎟🎫👓😨🌠🌃🌆🌇" /><br /> 入力2: フォントファイル <input type="file" id="input2" /><br /> <br /> 出力: 解析結果<br /> <br /> <div id="output"></div> <script>
'use strict'; // @ts-check /** * 標準組み込みオブジェクト * @typeof {object} ArrayBuffer * @typeof {object} DataView * @typeof {object} Uint8Array * Web API * @typeof {object} FileList * @typeof {object} HTMLElement * @typeof {object} HTMLInputElement * @typeof {object} HTMLTextAreaElement * @typeof {object} TextDecoder * @typeof {object} XMLDocument */ //------------------------------------------------------------ // ライブラリー //------------------------------------------------------------ /** * バイナリーデータ(ArrayBufferオブジェクト)を16進数テキストに変換 * @param {ArrayBuffer} arrayBuffer 16進数テキストに変換するArrayBufferオブジェクトによるバイナリーデータ * @returns {string} 変換された16進数テキスト */ function arrayBufferToHexText(arrayBuffer) { let hexText = ''; /** @type {DataView} */ const dataView = new DataView(arrayBuffer); for (let i = 0; i < arrayBuffer.byteLength; i ++) { hexText += dataView.getUint8(i).toString(16).padStart(2, '0'); } return hexText; } /** * バイナリーデータ(ArrayBufferオブジェクト)をデータタイプに合わせて値に変換 * @param {ArrayBuffer} arrayBuffer 値に変換するArrayBufferオブジェクトによるバイナリーデータ * @param {string} dataType データタイプ * @returns {(number|string)} 値 */ function arrayBufferToValue(arrayBuffer, dataType) { /** @type {DataView} */ const dataView = new DataView(arrayBuffer); switch (dataType) { case 'uint8': case 'Offset8': return dataView.getUint8(0); case 'int8': return dataView.getInt8(0); case 'uint16': case 'UFWORD': case 'Offset16': return dataView.getUint16(0); case 'int16': case 'FWORD': return dataView.getInt16(0); case 'uint24': case 'Offset24': return dataView.getUint16(0) * 256 + dataView.getUint8(2); case 'uint32': case 'Offset32': return dataView.getUint32(0); case 'int32': return dataView.getInt32(0); case 'Fixed': return dataView.getInt16(0) + dataView.getUint16(2) / 2 ** 16; case 'F2DOT14': { const UINT16 = dataView.getUint16(0); const UINT2 = (UINT16 & 0b1100000000000000) >>> 14; const INT2 = UINT2 >= 2 ** 1 ? UINT2 - 2 ** 2 : UINT2; return INT2 + (UINT16 & 0b0011111111111111) / 2 ** 14; } case 'LONGDATETIME': { const BIG_INT64 = dataView.getBigInt64(0); const DATE = BigInt(new Date('January 1, 1904 0:0:0 GMT+00:00').getTime()); return new Date(Number((BIG_INT64 + DATE / 1000n) * 1000n)).toDateString(); } case 'Tag': { let asciiString =''; for (let i = 0; i < arrayBuffer.byteLength; i ++) { asciiString += String.fromCharCode(dataView.getUint8(i)); } return asciiString; } case 'Version16Dot16': { const HEX_TEXT = arrayBufferToHexText(arrayBuffer); return parseInt(HEX_TEXT.slice(0, 4)) + parseInt(HEX_TEXT.slice(4, 8)) / 10000; } default: console.log(`Data Type: ${ dataType } is not defined.`); return 0; } } /** * Naming Tableの解析 platformIDおよびlanguageIDに応じて文字列を返す * @param {ArrayBuffer} arrayBuffer * @param {number} platformID * @param {number} languageID * @returns {string} */ function nameMeaning(arrayBuffer, platformID, languageID) { let outputString =''; /** @type {Uint8Array} */ const uint8Array = new Uint8Array(arrayBuffer); if (platformID === 0) { /** @type {TextDecoder} */ const utf8Decoder = new TextDecoder('utf-8'); outputString = utf8Decoder.decode(uint8Array); } else if (platformID === 1) { if (languageID === 0) { /** @type {DataView} */ const dataView = new DataView(arrayBuffer); for (let i = 0; i < arrayBuffer.byteLength; i ++) { outputString += String.fromCharCode(dataView.getUint8(i)); } } else if (languageID === 11) { /** @type {TextDecoder} */ const utf8Decoder = new TextDecoder('sjis'); outputString = utf8Decoder.decode(uint8Array); } } else if (platformID === 3) { /** @type {TextDecoder} */ const utf8Decoder = new TextDecoder('utf-16be'); outputString = utf8Decoder.decode(uint8Array); } return outputString; } /** * バイナリーデータ(ArrayBufferオブジェクト)をBase64に変換 * @param {ArrayBuffer} arrayBuffer * @returns {string} */ function arrayBufferToBase64(arrayBuffer) { let base64Text = ''; /** @type {DataView} */ const dataView = new DataView(arrayBuffer); /** @type {string[]} */ const base64Array = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/',]; // 3バイト毎に処理 for (let i = 0, li = arrayBuffer.byteLength / 3; i < li; i ++) { if (i <= li - 1 || arrayBuffer.byteLength % 3 === 0) { base64Text += base64Array[(dataView.getUint8(i * 3) & 0b11111100) >>> 2]; base64Text += base64Array[(dataView.getUint16(i * 3) & 0b0000001111110000) >>> 4]; base64Text += base64Array[(dataView.getUint16(i * 3 + 1) & 0b0000111111000000) >>> 6]; base64Text += base64Array[dataView.getUint8(i * 3 + 2) & 0b00111111]; } else if (i > li - 1 && arrayBuffer.byteLength % 3 === 1) { base64Text += base64Array[(dataView.getUint8(i * 3) & 0b11111100) >>> 2]; base64Text += base64Array[dataView.getUint8(i * 3) & 0b00000011]; base64Text += '=='; } else if (i > li - 1 && arrayBuffer.byteLength % 3 === 2) { base64Text += base64Array[(dataView.getUint8(i * 3) & 0b11111100) >>> 2]; base64Text += base64Array[(dataView.getUint16(i * 3) & 0b0000001111110000) >>> 4]; base64Text += base64Array[(dataView.getUint8(i * 3 + 1) & 0b00001111) * 4]; base64Text += '='; } } return base64Text; } /** * ts-check エラー対策 関数を用意し、nullを排除し、HTMLElementではなくHTMLInputElementとする * @param {string} id input要素のID * @returns {HTMLInputElement} */ function htmlInputElement(id) { return /** @type {!HTMLInputElement} */ (document.getElementById(id)); } //------------------------------------------------------------ // イベント type="file"のインプット要素でファイルを選択時 //------------------------------------------------------------ /** @this HTMLInputElement */ htmlInputElement('input2').addEventListener('change', async function(event) { //------------------------------------------------------------ // 関数内ライブラリー //------------------------------------------------------------ /** @classdesc データタイプに合わせてバイナリーデータを切り取って値に変換 */ class Valorize { /** @param {number} offset @returns {number} */ static uint8(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 1), 'uint8')); } /** @param {number} offset @returns {number} */ static Offset8(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 1), 'Offset8')); } /** @param {number} offset @returns {number} */ static int8(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 1), 'int8')); } /** @param {number} offset @returns {number} */ static uint16(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 2), 'uint16')); } /** @param {number} offset @returns {number} */ static UFWORD(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 2), 'UFWORD')); } /** @param {number} offset @returns {number} */ static Offset16(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 2), 'Offset16')); } /** @param {number} offset @returns {number} */ static int16(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 2), 'int16')); } /** @param {number} offset @returns {number} */ static FWORD(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 2), 'FWORD')); } /** @param {number} offset @returns {number} */ static F2DOT14(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 2), 'F2DOT14')); } /** @param {number} offset @returns {number} */ static uint24(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 3), 'uint24')); } /** @param {number} offset @returns {number} */ static Offset24(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 3), 'Offset24')); } /** @param {number} offset @returns {number} */ static uint32(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 4), 'uint32')); } /** @param {number} offset @returns {number} */ static Offset32(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 4), 'Offset32')); } /** @param {number} offset @returns {number} */ static int32(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 4), 'int32')); } /** @param {number} offset @returns {number} */ static Fixed(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 4), 'Fixed')); } /** @param {number} offset @returns {string} */ static Tag(offset) { return /** @type {string} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 4), 'Tag')); } /** @param {number} offset @returns {number} */ static Version16Dot16(offset) { return /** @type {number} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 4), 'Version16Dot16')); } /** @param {number} offset @returns {string} */ LONGDATETIME(offset) { return /** @type {string} */ (arrayBufferToValue(arrayBuffer.slice(offset, offset + 8), 'LONGDATETIME')); } } //------------------------------------------------------------ // クラスの設定 //------------------------------------------------------------ class Font { actualBoundingBoxAscent = 0; actualBoundingBoxDescent = 0; actualBoundingBoxLeft = 0; actualBoundingBoxRight = 0; fontBoundingBoxWidth = 0; /** @type {object[]} */ glyphs = []; height = 0; width = 0; x = 0; y = 0; } class Glyph { codePointString = ''; /** @type {object} */ svgElement = {}; } //------------------------------------------------------------ // ローカル変数の設定 //------------------------------------------------------------ /** @namespace */ const stringData = { /** @type {string[]} */ characters: [], codePointString: '', /** @type {number[][]} */ codePoints: [], inputString: htmlInputElement('input1').value, /** @type {number[][]} */ inputCodePoints: [], /** @type {string[][]} */ // @ts-ignore inputStringArray: Array.from(new Intl.Segmenter('ja-JP').segment(htmlInputElement('input1').value), function (element) { return element.segment; }), /** @type {string[][]} */ // @ts-ignore segmentArray: Array.from(new Set(Array.from(new Intl.Segmenter('ja-JP').segment(htmlInputElement('input1').value), function (element) { return element.segment; }))) }; /** @namespace */ const fontCollection = { /** @type {object[]} */ fonts: [] }; //------------------------------------------------------------ // 入力文字列に対する処理 //------------------------------------------------------------ stringData.inputStringArray.forEach(function (element1, index1) { stringData.inputCodePoints[index1] = []; const temporaryArray = Array.from(element1); temporaryArray.forEach(function (element2, index2) { let codePoint = /** @type {number} */ (element2.codePointAt(0)); stringData.inputCodePoints[index1][index2] = codePoint; stringData.codePointString += codePoint.toString(16).toUpperCase().padStart(4, '0'); }); }); stringData.segmentArray.forEach(function (element1, index1) { stringData.codePoints[index1] = []; const temporaryArray = Array.from(element1); temporaryArray.forEach(function (element2, index2) { stringData.codePoints[index1][index2] = /** @type {number} */ (element2.codePointAt(0)); }); }); stringData.codePoints.sort(function (a, b) { for (let i = 0; i < b.length; i ++) { if (!a[i]) { return -1; } else if (a[i] === b[i]) { continue; } else { return a[i] - b[i]; } } return 0; }); stringData.codePoints.forEach(function (element1, index1) { stringData.characters[index1] = ''; element1.forEach(function (element2) { stringData.characters[index1] += String.fromCodePoint(element2); }); }); //------------------------------------------------------------ // フォントファイルを解析する //------------------------------------------------------------ // フォントファイルからArrayBufferオブジェクトを取得 const fileList = /** @type {FileList} */ (this.files); /** @type {ArrayBuffer} */ const arrayBuffer = await fileList[0].arrayBuffer(); //------------------------------------------------------------ // Font Collections //------------------------------------------------------------ // The Font Collection File Structure // TTC Header let numFonts = 1; const tableDirectoryOffsets = []; fontCollection.tableDirectoryOffsets = tableDirectoryOffsets; if (Valorize.Tag(0) === 'ttcf') { numFonts = Valorize.uint32(8); for (let i = 0; i < numFonts; i ++) { let offset = i * 4 + 12; tableDirectoryOffsets[i] = Valorize.Offset32(offset); } } //------------------------------------------------------------ // Organization of an OpenType Font //------------------------------------------------------------ for (let i = 0; i < numFonts; i ++) { /** @type {object} */ const font = new Font(); fontCollection.fonts[i] = font; // Table Directory { /** @type {object} */ const tableDirectory = {}; font.tableDirectory = tableDirectory; let offset = fontCollection.tableDirectoryOffsets[i] ? fontCollection.tableDirectoryOffsets[i] : 0; let numTables = Valorize.uint16(offset + 4); // Table Record for (let j = 0; j < numTables; j ++) { offset = fontCollection.tableDirectoryOffsets[i] ? fontCollection.tableDirectoryOffsets[i] + j * 16 + 12 : j * 16 + 12; const tableTag = Valorize.Tag(offset); tableDirectory[tableTag] = {}; tableDirectory[tableTag].offset = Valorize.Offset32(offset + 8); } } //------------------------------------------------------------ // Font Tables //------------------------------------------------------------ // フォントファイル内のCMapテーブルを使用してCode Pointの値からGlyph IDを取得 // cmap - Character to Glyph Index Mapping Table { const cmap = { format4: { subtableOffset: 0 }, format12: { subtableOffset: 0 }, format14: { subtableOffset: 0 } }; font.cmap = cmap; let offset = font.tableDirectory['cmap'].offset; let numTables = Valorize.uint16(offset + 2); for (let j = 0; j < numTables; j ++) { offset = font.tableDirectory['cmap'].offset + j * 8 + 4; let platformID = Valorize.uint16(offset); let encodingID = Valorize.uint16(offset + 2); // Unicode Platform (Platform ID = 0), Encoding ID 3 should be used in conjunction with 'cmap' subtable formats 4 or 6. // Windows Platform (Platform ID = 3), Fonts that support only Unicode BMP characters (U+0000 to U+FFFF) on the Windows platform must use encoding 1 with a format 4 subtable. if (cmap.format4.subtableOffset === 0 && (platformID === 0 && encodingID === 3) || (platformID === 3 && encodingID === 1)) { let subtableOffset = Valorize.Offset32(offset + 4); offset = font.tableDirectory['cmap'].offset + subtableOffset; let format = Valorize.uint16(offset); if (format === 4) { cmap.format4.subtableOffset = font.tableDirectory['cmap'].offset + subtableOffset; } } // Unicode Platform (Platform ID = 0), Encoding ID 4 should be used in conjunction with subtable formats 10 or 12. // Windows Platform (Platform ID = 3), Fonts that support Unicode supplementary-plane characters (U+10000 to U+10FFFF) on the Windows platform must use encoding 10 with a format 12 subtable. if (cmap.format12.subtableOffset === 0 && (platformID === 0 && encodingID === 4) || (platformID === 3 && encodingID === 10)) { let subtableOffset = Valorize.Offset32(offset + 4); offset = font.tableDirectory['cmap'].offset + subtableOffset; let format = Valorize.uint16(offset); if (format === 12) { cmap.format12.subtableOffset = font.tableDirectory['cmap'].offset + subtableOffset; } } // A format 14 subtable must only be used under platform ID 0 and encoding ID 5; and encoding ID 5 should only be used with a format 14 subtable. if (cmap.format14.subtableOffset === 0 && platformID === 0 && encodingID === 5) { let subtableOffset = Valorize.Offset32(offset + 4); offset = font.tableDirectory['cmap'].offset + subtableOffset; let format = Valorize.uint16(offset); if (format === 14) { cmap.format14.subtableOffset = font.tableDirectory['cmap'].offset + subtableOffset; } } } } // head - Font Header Table { font.head = {}; let offset = font.tableDirectory['head'].offset; font.head.unitsPerEm = Valorize.uint16(offset + 18); } // name - Naming Table { // Naming table header /** @type {string[]} */ const name = []; font.name = name; let offset = font.tableDirectory['name'].offset; let count = Valorize.uint16(offset + 2); let storageOffset = Valorize.Offset16(offset + 4); for (let j = 0; j < count; j ++) { // Name records offset = font.tableDirectory['name'].offset + j * 12 + 6; let platformID = Valorize.uint16(offset); let languageID = Valorize.uint16(offset + 4); let nameID = Valorize.uint16(offset + 6); let length = Valorize.uint16(offset + 8); let stringOffset = Valorize.Offset16(offset + 10); if (platformID === 0 && languageID === 0) { offset = font.tableDirectory['name'].offset + storageOffset + stringOffset; name[nameID] = nameMeaning(arrayBuffer.slice(offset, offset + length), platformID, languageID); } else if (platformID === 1 && (languageID === 0 || languageID === 11)) { offset = font.tableDirectory['name'].offset + storageOffset + stringOffset; name[nameID] = nameMeaning(arrayBuffer.slice(offset, offset + length), platformID, languageID); } else if (platformID === 3 && languageID === 0x409) { offset = font.tableDirectory['name'].offset + storageOffset + stringOffset; name[nameID] = nameMeaning(arrayBuffer.slice(offset, offset + length), platformID, languageID); } } } // OS/2 - OS/2 and Windows Metrics Table { font.OS_2 = {}; let offset = font.tableDirectory['OS/2'].offset; font.OS_2.fsType = Valorize.uint16(offset + 8); font.OS_2.sTypoAscender = Valorize.FWORD(offset + 68); font.OS_2.sTypoDescender = Valorize.FWORD(offset + 70); font.OS_2.usWinAscent = Valorize.uint16(offset + 74); font.OS_2.usWinDescent = Valorize.uint16(offset + 76); } //------------------------------------------------------------ // SVG出力の準備 その1 //------------------------------------------------------------ // SVG出力用テキストエリア const htmlElement = /** @type {HTMLElement} */ (document.getElementById('output')); /** @type {HTMLTextAreaElement} */ const textareaElement = document.createElement('textarea'); htmlElement.appendChild(textareaElement); textareaElement.setAttribute('cols', '80'); textareaElement.setAttribute('rows', '20'); // XML DOM ツリーを作成 /** @type {XMLDocument} */ const svgDocument = document.implementation.createDocument('', '', null); /** @type {HTMLElement} */ const svgElement = svgDocument.createElement('svg'); svgElement.setAttribute('version', '1.1'); svgElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); svgDocument.appendChild(svgElement); svgElement.appendChild(svgDocument.createComment(` Copyright Notice: ${ font.name[0] }, Full Font Name: ${ font.name[4] } `)); if ((font.OS_2.fsType & 0x000f) === 0) { svgElement.appendChild(svgDocument.createComment(` Type flags: \u00220\u0022 Installable embedding `)); } else if ((font.OS_2.fsType & 0x000f) === 2) { svgElement.appendChild(svgDocument.createComment(` Type flags: \u00222\u0022 Restricted License embedding `)); } else if ((font.OS_2.fsType & 0x000f) === 4) { svgElement.appendChild(svgDocument.createComment(` Type flags: \u00224\u0022 Preview & Print embedding `)); } else if ((font.OS_2.fsType & 0x000f) === 8) { svgElement.appendChild(svgDocument.createComment(` Type flags: \u00228\u0022 Editable embedding `)); } if ((font.OS_2.fsType & 0x0100)) { svgElement.appendChild(svgDocument.createComment(` Type flags: No subsetting `)); } if ((font.OS_2.fsType & 0x0200)) { svgElement.appendChild(svgDocument.createComment(` Type flags: Bitmap embedding only `)); } svgElement.appendChild(svgDocument.createComment(` unitsPerEm: ${ font.head.unitsPerEm }, usWinAscent: ${ font.OS_2.usWinAscent }, usWinDescent: ${ font.OS_2.usWinDescent }, sTypoAscender: ${ font.OS_2.sTypoAscender }, sTypoDescender: ${ font.OS_2.sTypoDescender } `)); const defsElement = svgDocument.createElement('defs'); svgElement.appendChild(defsElement); //------------------------------------------------------------ // PNG画像データを取得 //------------------------------------------------------------ // 重複する文字を除去した入力文字配列からCode Pointごとに処理 stringData.codePoints.forEach(function (element1, index1) { /** @type {object} */ const glyph = new Glyph; font.glyphs[index1] = glyph; // Code Pointによる文字列を作成 // SVGのid属性に使用 element1.forEach(function (element2) { glyph.codePointString += element2.toString(16).toUpperCase().padStart(4, '0'); }); //------------------------------------------------------------ // Glyph IDの取得 //------------------------------------------------------------ let codePoint = element1[0]; /** @type {number|undefined} */ let varSelector = element1[1] ? element1[1] : undefined; let glyphID = 0; glyph.glyphID = glyphID; // Format 4 if (codePoint >= 0x0000 && codePoint <= 0xffff) { let offset = font.cmap.format4.subtableOffset; let segCountX2 = Valorize.uint16(offset + 6); for (let j = 0; j < segCountX2 / 2; j ++) { offset = font.cmap.format4.subtableOffset + j * 2 + 14; let startCode = Valorize.uint16(offset + segCountX2 + 2); if (codePoint <= Valorize.uint16(offset) && codePoint >= startCode) { let idRangeOffset = Valorize.uint16(offset + 3 * segCountX2 + 2); if (idRangeOffset === 0) { offset = font.cmap.format4.subtableOffset + j * 2 + segCountX2 * 2 + 16; glyphID = (codePoint + Valorize.uint16(offset)) & 0xffff; break; } else { offset = font.cmap.format4.subtableOffset + j * 2 + segCountX2 * 3 + idRangeOffset + (codePoint - startCode) * 2 + 16; glyphID = Valorize.uint16(offset); break; } } } } // Format 12 if (glyphID === 0 && codePoint >= 0x0000 && codePoint <= 0x10ffff) { let offset = font.cmap.format12.subtableOffset; let numGroups = Valorize.uint32(offset + 12); for (let j = 0; j < numGroups; j ++) { offset = font.cmap.format12.subtableOffset + j * 12 + 16; let startCharCode = Valorize.uint32(offset); let endCharCode = Valorize.uint32(offset + 4); if (codePoint >= startCharCode && codePoint <= endCharCode) { glyphID = (codePoint - startCharCode) + Valorize.uint32(offset + 8); break; } } } // Format 14 if (varSelector) { let offset = font.cmap.format14.subtableOffset; let numVarSelectorRecords = Valorize.uint32(offset + 6); for (let j = 0; j < numVarSelectorRecords; j ++) { offset = font.cmap.format14.subtableOffset + j * 11 + 10; if (Valorize.uint24(offset) === varSelector) { let nonDefaultUVSOffset = Valorize.Offset32(offset + 7); if (nonDefaultUVSOffset !== 0) { offset = font.cmap.format14.subtableOffset + nonDefaultUVSOffset; let numUVSMappings = Valorize.uint32(offset); for (let k = 0; k < numUVSMappings; k++) { offset = font.cmap.format14.subtableOffset + nonDefaultUVSOffset + k * 5 + 4; let unicodeValue = Valorize.uint24(offset); if (unicodeValue === codePoint) { glyphID = Valorize.uint16(offset + 3); break; } } } } } } //------------------------------------------------------------ // Font Tables その2 Glyph IDを使用 //------------------------------------------------------------ // CBLC — Color Bitmap Location Table // CblcHeader { glyph.CBLC = {}; let offset = font.tableDirectory['CBLC'].offset; let numSizes = Valorize.uint32(offset + 4); // BitmapSize record for (let j = 0; j < numSizes; j ++) { offset = font.tableDirectory['CBLC'].offset + j * 48 + 8; let indexSubtableListOffset = Valorize.Offset32(offset); let numberOfIndexSubtables = Valorize.uint32(offset + 8); glyph.CBLC.ascender = Valorize.int8(offset + 16); glyph.CBLC.descender = Valorize.int8(offset + 17); // IndexSubtableList // IndexSubtableList table for (let k = 0; k < numberOfIndexSubtables; k ++) { offset = font.tableDirectory['CBLC'].offset + indexSubtableListOffset + k * 8; let firstGlyphIndex = Valorize.uint16(offset); let lastGlyphIndex = Valorize.uint16(offset + 2); let indexSubtableOffset = Valorize.Offset32(offset + 4); if (glyphID >= firstGlyphIndex && glyphID <= lastGlyphIndex) { offset = font.tableDirectory['CBLC'].offset + indexSubtableListOffset + indexSubtableOffset; let indexFormat = Valorize.uint16(offset); glyph.CBLC.imageFormat = Valorize.uint16(offset + 2); glyph.CBLC.imageDataOffset = Valorize.Offset32(offset + 4); // Noto Color Emoji は Format 1 のみ if (indexFormat === 1) { // IndexSubtableFormat1 table offset = font.tableDirectory['CBLC'].offset + indexSubtableListOffset + indexSubtableOffset + (glyphID - firstGlyphIndex) * 4 + 8; glyph.CBLC.sbitOffsets = Valorize.Offset32(offset); } else { // 対応していないIndexSubtableのFormatの場合、コンソールに出力 console.log(indexFormat); } } } } } // CBDT — Color Bitmap Data Table // Noto Color Emoji は Format 17 のみ // Format 17: small metrics, PNG image data { glyph.CBDT = {}; let offset = font.tableDirectory['CBDT'].offset + glyph.CBLC.imageDataOffset + glyph.CBLC.sbitOffsets; glyph.CBDT.height = Valorize.uint8(offset); glyph.CBDT.width = Valorize.uint8(offset + 1); let dataLen = Valorize.uint32(offset + 5); //------------------------------------------------------------ // SVG出力の準備 その2 //------------------------------------------------------------ // PNG画像データを取得し、SVGに追加 defsElement.appendChild(svgDocument.createComment(` Character: \u0022${ stringData.characters[index1] }\u0022 `)); glyph.svgElement = svgDocument.createElement('image'); glyph.svgElement.setAttribute('id', glyph.codePointString); // Raw PNG dataをBase64に変換し、image要素のhref属性にセット glyph.svgElement.setAttribute('href', 'data:image/png;base64,' + arrayBufferToBase64(arrayBuffer.slice(offset + 9, offset + 9 + dataLen))); defsElement.appendChild(glyph.svgElement); } }); //------------------------------------------------------------ // SVG出力の準備 その3 //------------------------------------------------------------ // 入力文字列に対する処理 // 入力文字列に関する要素をSVGに追加 /** @type {HTMLElement} */ const gElement = svgDocument.createElement('g'); gElement.setAttribute('id', stringData.codePoints.length !== 1 ? stringData.codePointString : '_' + stringData.codePointString); { /** @type {HTMLElement} */ const useElement = svgDocument.createElement('use'); svgElement.appendChild(useElement); useElement.setAttribute('href', '#' + (stringData.codePoints.length !== 1 ? stringData.codePointString : '_' + stringData.codePointString)); useElement.setAttribute('transform', 'translate(0, 0)'); } // 入力文字列内の文字ごとに処理 stringData.inputCodePoints.forEach(function (element1, index1, array1) { // 重複する文字を除去した入力文字配列のソートされたCode Point配列から、入力文字のCode Pointに対応するインデックスを取得、試験 const index2 = stringData.codePoints.findIndex(function (element2) { if (element2.length !== element1.length) { return false; } for (let j = 0, lj = element1.length; j < lj; j ++) { if (!element2[j] || element2[j] !== element1[j]) { return false; } if (j !== lj - 1) { continue; } else if (j === lj - 1) { return true; } } return false; }); // 取得したインデックスを使用して、格納された文字に関するデータを取得 const glyph = font.glyphs[index2]; if (index1 === 0) { font.actualBoundingBoxLeft = 0; } font.actualBoundingBoxDescent = glyph.CBLC.descender < font.actualBoundingBoxDescent ? glyph.CBLC.descender : font.actualBoundingBoxDescent; if (index1 === array1.length - 1) { font.actualBoundingBoxRight = font.fontBoundingBoxWidth + glyph.CBDT.width; } font.actualBoundingBoxAscent = glyph.CBLC.ascender > font.actualBoundingBoxAscent ? glyph.CBLC.ascender : font.actualBoundingBoxAscent; // 入力文字列内の文字に関する要素をSVGに追加 /** @type {HTMLElement} */ const useElement = svgDocument.createElement('use'); gElement.appendChild(useElement); useElement.setAttribute('href', '#' + glyph.codePointString); useElement.setAttribute('transform', `matrix(1 0 0 1 ${ font.fontBoundingBoxWidth } ${ -1 * glyph.CBLC.ascender })`); // 文字列全体の仮想ボディの幅 font.fontBoundingBoxWidth += glyph.CBDT.width; }); defsElement.appendChild(svgDocument.createComment(` ${ stringData.inputString }, width: ${ font.fontBoundingBoxWidth }, height: ${ font.actualBoundingBoxAscent - font.actualBoundingBoxDescent } `)); // フォントサイズの変更や中央揃えがしやすいように、scaleとtlanslateをセットしておく。 gElement.setAttribute('transform', `scale(1), translate(-${ font.fontBoundingBoxWidth / 2 }, 0)`); defsElement.appendChild(gElement); // viewBox属性 font.x = font.actualBoundingBoxLeft < 0 ? font.actualBoundingBoxLeft : 0; font.y = font.actualBoundingBoxAscent; font.width = font.actualBoundingBoxRight > font.fontBoundingBoxWidth ? font.actualBoundingBoxRight - font.x : font.fontBoundingBoxWidth - font.x; font.height = font.y - font.actualBoundingBoxDescent; svgElement.setAttribute('viewBox', `${ -1 * font.width / 2 } ${ -1 * font.y } ${ font.width } ${ font.height }`); //------------------------------------------------------------ // SVG出力 //------------------------------------------------------------ // XMLDocumentを文字列に変換、読みやすいように改行を付加 const outputText = new XMLSerializer().serializeToString(svgDocument).replace(/></g, '>\u000d\u000a<'); // SVGソースをテキストエリア要素に出力 textareaElement.appendChild(document.createTextNode(outputText)); // SVG画像をコンテンツ区分要素に出力 htmlElement.innerHTML += outputText; } });
</script> </body> </html>