ベクター形式のカラー絵文字のデータを取得し、SVGソースに埋め込む(COLR/CPAL version 1)
カラー絵文字のフォントファイルは、ラスター(ビットマップ)形式だったりベクター形式だったりフォントごとに異なります😶🌫️。
ベクター形式のカラー絵文字は、COLRテーブルにより、カラーグリフをレイヤー状に配置し、複合グラフィックとして表示されます。COLRテーブルには、version 0およびversion 1の2つのバージョンが定義されています。
下記のJavaScriptでは、version 1のフォントファイルを解析し、SVGソースを作成し、SVG画像を表示します。
完成例
入力1: 文字列入力2: フォントファイル
出力: 解析結果
使い方
フォントファイルのTable Directoryを確認し、COLRおよびCPALテーブルが存在することを確認します。SVGで表示したい文字を「入力1: 文字列」のテキストボックスに入力します。
「ファイルを選択」ボタンをクリックして、フォントファイル(.ttfまたは.ttc)を開きます。
フォントファイルを開くイベントをきっかけにJavaScriptが動き、「出力: 解析結果」以下にSVGのソースおよびプレビュー画像が表示されます🤗。
COLRおよびCPALテーブルが存在するのに、SVG画像が表示されない場合は、カラー絵文字のSVGソースを作成し、SVG画像を表示する(COLR/CPAL version 0)を試してください。
動作確認は、Noto Color Emojiで行っています。GitHubから無料で入手することができます(Noto-COLRv1.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; } /** * 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; baseGlyphIDs = []; /** @type {object[]} */ baseGlyphs = []; height = 0; width = 0; x = 0; y = 0; } class BaseGlyph { /** @type {object[]} */ colorLayers = []; /** @type {object} */ svgElement = {}; } class ColorLayer { /** @type {number[]} */ formats = []; /** @type {object} */ svgElement = {}; } class Glyph { /** @type {object[]} */ contours = []; /** @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 その1 //------------------------------------------------------------ // 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; } } } } // COLR — Color Table { // COLR header font.COLR = {}; let offset = font.tableDirectory['COLR'].offset; font.COLR.baseGlyphListOffset = Valorize.Offset32(offset + 14); font.COLR.layerListOffset = Valorize.Offset32(offset + 18); font.COLR.clipListOffset = Valorize.Offset32(offset + 22); font.COLR.varIndexMapOffset = Valorize.Offset32(offset + 26); font.COLR.itemVariationStoreOffset = Valorize.Offset32(offset + 30); } // CPAL — Color Palette Table { font.CPAL = []; let offset = font.tableDirectory['CPAL'].offset; font.CPAL.version = Valorize.uint16(offset); font.CPAL.numPalettes = Valorize.uint16(offset + 4); font.CPAL.colorRecordsArrayOffset = Valorize.Offset32(offset + 8); } // head - Font Header Table { font.head = {}; let offset = font.tableDirectory['head'].offset; font.head.unitsPerEm = Valorize.uint16(offset + 18); 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); } // name - Naming Table // ライセンス情報をコメントでSVG内に入れる { // 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 } `)); /** @type {HTMLElement} */ const defsElement = svgDocument.createElement('defs'); svgElement.appendChild(defsElement); //------------------------------------------------------------ // SVGによるカラー絵文字の作成 //------------------------------------------------------------ // 重複する文字を除去した入力文字配列からCode Pointごとに処理 stringData.codePoints.forEach(function (element1, index1) { let wrapperFlag = ''; let wrapperCount = 0; let wrapperElement = {}; /** @type {object} */ const baseGlyph = new BaseGlyph(); font.baseGlyphs[index1] = baseGlyph; baseGlyph.codePointString = ''; element1.forEach(function (element2) { baseGlyph.codePointString += element2.toString(16).toUpperCase().padStart(4, '0'); }); baseGlyph.svgElement = svgDocument.createElement('g'); baseGlyph.svgElement.setAttribute('id', baseGlyph.codePointString); //------------------------------------------------------------ // Base Glyph IDの取得 //------------------------------------------------------------ let codePoint = element1[0]; /** @type {number|undefined} 異体字セレクター */ let varSelector = element1[1] ? element1[1] : undefined; let baseGlyphID = 0; baseGlyph.glyphID = baseGlyphID; // フォントファイル内のCMapテーブルを使用してCode Pointの値からBase Glyph IDを取得 // 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; baseGlyphID = (codePoint + Valorize.uint16(offset)) & 0xffff; break; } else { offset = font.cmap.format4.subtableOffset + j * 2 + segCountX2 * 3 + idRangeOffset + (codePoint - startCode) * 2 + 16; baseGlyphID = Valorize.uint16(offset); break; } } } } // Format 12 if (baseGlyphID === 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) { baseGlyphID = (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) { baseGlyphID = Valorize.uint16(offset + 3); break; } } } } } } //------------------------------------------------------------ // Font Tables その2 Base Glyph IDを使用 //------------------------------------------------------------ // hmtx - Horizontal Metrics Table baseGlyph.hmtx = {}; if (baseGlyphID <= font.hhea.numberOfHMetrics) { let offset = font.tableDirectory['hmtx'].offset + baseGlyphID * 4; baseGlyph.hmtx.advanceWidth = Valorize.int16(offset); } else { let offset = font.tableDirectory['hmtx'].offset + font.hhea.numberOfHMetrics * 4; baseGlyph.hmtx.advanceWidth = Valorize.uint16(offset); } // loca - Index to Location baseGlyph.loca = {}; if (font.head.indexToLocFormat === 0) { let offset = font.tableDirectory['loca'].offset + baseGlyphID * 2; baseGlyph.loca.offsets = Valorize.Offset16(offset) * 2; } else if (font.head.indexToLocFormat === 1) { let offset = font.tableDirectory['loca'].offset + baseGlyphID * 4; baseGlyph.loca.offsets = Valorize.Offset32(offset); } // glyf - Glyph Data baseGlyph.glyf = {}; // Glyph Header let offset = font.tableDirectory['glyf'].offset + baseGlyph.loca.offsets; baseGlyph.glyf.numberOfContours = Valorize.int16(offset); baseGlyph.glyf.xMin = Valorize.int16(offset + 2); baseGlyph.glyf.yMin = Valorize.int16(offset + 4); baseGlyph.glyf.xMax = Valorize.int16(offset + 6); baseGlyph.glyf.yMax = Valorize.int16(offset + 8); //------------------------------------------------------------ // BaseGlyph and Layer records //------------------------------------------------------------ // COLR — Color Table baseGlyph.CORL = {}; // BaseGlyphList table offset = font.tableDirectory['COLR'].offset + font.COLR.baseGlyphListOffset; let numBaseGlyphPaintRecords = Valorize.uint32(offset); for (let j = 0; j < numBaseGlyphPaintRecords; j ++) { // BaseGlyphPaintRecord offset = font.tableDirectory['COLR'].offset + font.COLR.baseGlyphListOffset + j * 6 + 4; if (Valorize.uint16(offset) === baseGlyphID) { let paintOffset = Valorize.Offset32(offset + 2); // Paint Recordを描画 renderPaint(font.tableDirectory['COLR'].offset + font.COLR.baseGlyphListOffset + paintOffset); } } /** * Paint Recordを描画 * @param {number} offset Paint Recordのオフセット位置 * @param {object} colorLayer Paint Recordが属するLayer */ function renderPaint(offset, colorLayer = {}) { let format = Valorize.uint8(offset); if (colorLayer.formats) { colorLayer.formats.push(format); } // 対応していないPaint RecordのFormatの場合、コンソールに出力 if (format !== 1 && format !== 2 && format !== 4 && format !== 6 && format !== 10 && format !== 12 && format !== 14 && format !== 16 && format !== 18 && format !== 32) { console.log(format); } if (format === 1) { // Format 1: PaintColrLayers let numLayers = Valorize.uint8(offset + 1); let firstLayerIndex = Valorize.uint32(offset + 2); for (let j = 0; j < numLayers; j ++) { /** @type {object} */ colorLayer = new ColorLayer(); colorLayer.formats.push(1); colorLayer.CPALs = []; colorLayer.numLayer = j; font.baseGlyphs[index1].colorLayers.push(colorLayer); offset = font.tableDirectory['COLR'].offset + font.COLR.layerListOffset + firstLayerIndex * 4 + j * 4 + 4; let paintOffset = Valorize.Offset32(offset); renderPaint(font.tableDirectory['COLR'].offset + font.COLR.layerListOffset + paintOffset, colorLayer); } } else if (format === 2) { // Formats 2: PaintSolid let paletteIndex = Valorize.uint16(offset + 1); colorLayer.alpha = Valorize.F2DOT14(offset + 3); colorLayer.type = 'solid'; colorPalette(paletteIndex, colorLayer, 0); renderGlyph(colorLayer) } else if (format === 4) { // Formats 4: PaintLinearGradient colorLayer.type = 'linear'; let colorLineOffset = Valorize.Offset24(offset + 1); let x0 = Valorize.FWORD(offset + 4); let y0 = Valorize.FWORD(offset + 6,); let x1 = Valorize.FWORD(offset + 8,); let y1 = Valorize.FWORD(offset + 10); let x2 = Valorize.FWORD(offset + 12); let y2 = Valorize.FWORD(offset + 14); colorLayer.x1 = x0; colorLayer.y1 = y0; colorLayer.x2 = x1; colorLayer.y2 = y1; let rotateAngle = (Math.atan2(y2 - y0, x2 - x0) - Math.atan2(y1 - y0, x1 - x0)) * 180 / Math.PI; rotateAngle = (rotateAngle >= 0 && rotateAngle <= 180) || (rotateAngle >= -360 && rotateAngle <= -180) ? rotateAngle - 90 : rotateAngle + 90; if (rotateAngle !== 0) { colorLayer.transform = colorLayer.transform ? `rotate(${ rotateAngle } ${ x0 } ${ y0 }), ` + colorLayer.transform : `rotate(${ rotateAngle } ${ x0 } ${ y0 })`; } // ColorLine table offset += colorLineOffset; colorLayer.extend = Valorize.uint8(offset); colorLayer.numStops = Valorize.uint16(offset + 1); offset += 3; colorLayer.stopOffsets = []; colorLayer.alphas = []; for (let j = 0; j < colorLayer.numStops; j ++) { // ColorStop record colorLayer.stopOffsets[j] = Valorize.F2DOT14(offset); let paletteIndex = Valorize.uint16(offset + 2); colorLayer.alphas[j] = Valorize.F2DOT14(offset + 4); colorPalette(paletteIndex, colorLayer, j); offset += 6; } renderGlyph(colorLayer) } else if (format === 6) { // Formats 6: PaintRadialGradient colorLayer.type = 'radial'; let colorLineOffset = Valorize.Offset24(offset + 1); colorLayer.fx = Valorize.FWORD(offset + 4); colorLayer.fy = Valorize.FWORD(offset + 6); colorLayer.fr = Valorize.UFWORD(offset + 8); colorLayer.cx = Valorize.FWORD(offset + 10); colorLayer.cy = Valorize.FWORD(offset + 12); colorLayer.r = Valorize.UFWORD(offset + 14); // ColorLine table offset += colorLineOffset; colorLayer.extend = Valorize.uint8(offset); colorLayer.numStops = Valorize.uint16(offset + 1); offset += 3; colorLayer.stopOffsets = []; colorLayer.alphas = []; for (let j = 0; j < colorLayer.numStops; j ++) { // ColorStop record colorLayer.stopOffsets[j] = Valorize.F2DOT14(offset); let paletteIndex = Valorize.uint16(offset + 2); colorLayer.alphas[j] = Valorize.F2DOT14(offset + 4); colorPalette(paletteIndex, colorLayer, j); offset += 6; } renderGlyph(colorLayer) } else if (format === 10) { // Format 10: PaintGlyph let paintOffset = Valorize.Offset24(offset + 1); colorLayer.glyphID = Valorize.uint16(offset + 4); renderPaint(offset + paintOffset, colorLayer); } else if (format === 12) { // Formats 12: PaintTransform let paintOffset = Valorize.Offset24(offset + 1); let transformOffset = Valorize.Offset24(offset + 4); // Affine2x3 table let xx = Valorize.Fixed(offset + transformOffset); let yx = Valorize.Fixed(offset + transformOffset + 4); let xy = Valorize.Fixed(offset + transformOffset + 8); let yy = Valorize.Fixed(offset + transformOffset + 12); let dx = Valorize.Fixed(offset + transformOffset + 16); let dy = Valorize.Fixed(offset + transformOffset + 20); colorLayer.transform = colorLayer.transform ? colorLayer.transform + `, matrix(${ xx } ${ yx } ${ xy } ${ yy } ${ dx } ${ dy })` : `matrix(${ xx } ${ yx } ${ xy } ${ yy } ${ dx } ${ dy })`; renderPaint(offset + paintOffset, colorLayer); } else if (format === 14) { // Formats 14: PaintTranslate let paintOffset = Valorize.Offset24(offset + 1); let dx = Valorize.FWORD(offset + 4); let dy = Valorize.FWORD(offset + 6); colorLayer.transform = colorLayer.transform ? colorLayer.transform + `, translate(${ dx } ${ dy })` : `translate(${ dx } ${ dy })`; renderPaint(offset + paintOffset, colorLayer); } else if (format === 16) { // Formats 16: scale in x or y directions relative to the origin. let paintOffset = Valorize.Offset24(offset + 1); let scaleX = Valorize.F2DOT14(offset + 4); let scaleY = Valorize.F2DOT14(offset + 6); colorLayer.transform = colorLayer.transform ? colorLayer.transform + `, scale(${ scaleX } ${ scaleY })` : `scale(${ scaleX } ${ scaleY })`; renderPaint(offset + paintOffset, colorLayer); } else if (format === 18) { // Formats 18: scale in x or y directions relative to a specified center. let paintOffset = Valorize.Offset24(offset + 1); let scaleX = Valorize.F2DOT14(offset + 4); let scaleY = Valorize.F2DOT14(offset + 6); let centerX = Valorize.FWORD(offset + 8); let centerY = Valorize.FWORD(offset + 10); colorLayer.transform = colorLayer.transform ? colorLayer.transform + `, translate(${ centerX } ${ centerY }), scale(${ scaleX } ${ scaleY }), translate(${ -1 * centerX } ${ -1 * centerY })` : `translate(${ centerX } ${ centerY }), scale(${ scaleX } ${ scaleY }), translate(${ -1 * centerX } ${ -1 * centerY })`; renderPaint(offset + paintOffset, colorLayer); } else if (format === 32) { // Format 32: PaintComposite let sourcePaintOffset = Valorize.Offset24(offset + 1); let compositeMode = Valorize.uint8(offset + 4); let backdropPaintOffset = Valorize.Offset24(offset + 5); if (compositeMode !== 5) { // 対応していないComposite Modeの場合、コンソールに出力 console.log(compositeMode); } if (compositeMode === 5) { // Source In wrapperFlag = 'Source In'; wrapperElement = svgDocument.createElement('mask'); baseGlyph.svgElement.appendChild(wrapperElement); wrapperElement.setAttribute('id', `${ baseGlyph.codePointString }_${ wrapperCount }`); renderPaint(offset + backdropPaintOffset, colorLayer); wrapperElement = svgDocument.createElement('g'); baseGlyph.svgElement.appendChild(wrapperElement); wrapperElement.setAttribute('mask', `url(#${ baseGlyph.codePointString }_${ wrapperCount })`); renderPaint(offset + sourcePaintOffset, colorLayer); wrapperFlag = ''; wrapperCount ++; } } } /** * Color Paletteの色情報を取得 * @param {number} paletteIndex ColorPalette内の色情報にアクセスするためのインデックス * @param {object} colorLayer Color Paletteが属するLayer * @param {number} index ColorStopに対応するため配列のインデックス */ function colorPalette(paletteIndex, colorLayer, index) { if (font.CPAL.version === 0) { const CPAL = {}; colorLayer.CPALs[index] = CPAL; offset = font.tableDirectory['CPAL'].offset + 12; let colorRecordIndex = Valorize.uint16(offset); offset = font.tableDirectory['CPAL'].offset + font.CPAL.colorRecordsArrayOffset + colorRecordIndex + paletteIndex * 4; CPAL.blue = Valorize.uint8(offset); CPAL.green = Valorize.uint8(offset + 1); CPAL.red = Valorize.uint8(offset + 2); CPAL.alpha = Valorize.uint8(offset + 3); } } /** * Color Glyphの描画 * @param {object} colorLayer Color Glyphが属するLayer */ function renderGlyph(colorLayer) { /** @type {object} */ const glyph = new Glyph(); colorLayer.glyph = glyph; if (colorLayer.glyphID) { glyph.glyphID = colorLayer.glyphID; glyph.glyphIDString = colorLayer.glyphID.toString(16).toUpperCase().padStart(4, '0'); //------------------------------------------------------------ // Font Tables その3 Glyph IDを使用 //------------------------------------------------------------ // loca - Index to Location glyph.loca = {}; if (font.head.indexToLocFormat === 0) { offset = font.tableDirectory['loca'].offset + glyph.glyphID * 2; glyph.loca.offsets = Valorize.Offset16(offset) * 2; glyph.loca.length = Valorize.Offset16(offset + 2) * 2 - glyph.loca.offsets * 2; } else if (font.head.indexToLocFormat === 1) { offset = font.tableDirectory['loca'].offset + glyph.glyphID * 4; glyph.loca.offsets = Valorize.Offset32(offset); glyph.loca.length = Valorize.Offset32(offset + 4) - glyph.loca.offsets; } // glyf - Glyph Data // Glyph Header glyph.glyf = {}; offset = font.tableDirectory['glyf'].offset + glyph.loca.offsets; glyph.glyf.numberOfContours = Valorize.int16(offset); //------------------------------------------------------------ // Simple Glyph Descriptionの解析 //------------------------------------------------------------ // Simple Glyph table glyph.glyf.endPtsOfContours = []; if (glyph.glyf.numberOfContours >= 0 && glyph.loca.length > 0) { for (let j = 0; j < glyph.glyf.numberOfContours; j ++) { offset = font.tableDirectory['glyf'].offset + glyph.loca.offsets + j * 2 + 10; glyph.glyf.endPtsOfContours[j] = Valorize.uint16(offset); } offset = font.tableDirectory['glyf'].offset + glyph.loca.offsets + glyph.glyf.numberOfContours * 2 + 10; let instructionLength = Valorize.uint16(offset); offset = font.tableDirectory['glyf'].offset + glyph.loca.offsets + glyph.glyf.numberOfContours * 2 + instructionLength + 12; /** @type {ArrayBuffer} Simple Glyph Descriptionのflags、xCoordinates、yCoordinatesを含むバイナリーデータ */ const slicedArrayBuffer = arrayBuffer.slice(offset, font.tableDirectory['glyf'].offset + glyph.loca.offsets + glyph.loca.length); // バイナリーデータ(ArrayBufferオブジェクト)を解析してColor GlyphのSVGを作成(Simple Glyph Description) { class Valorize { /** @param {number} offset @returns {number} */ static uint8(offset) { return /** @type {number }*/ (arrayBufferToValue(slicedArrayBuffer.slice(offset, offset + 1), 'uint8')); } /** @param {number} offset @returns {number} */ static int16(offset) { return /** @type {number }*/ (arrayBufferToValue(slicedArrayBuffer.slice(offset, offset + 2), 'int16')); } } class Contour { /** @type {number[]} */ flags = []; /** @type {number[]} */ onCurvePoint = []; /** @type {number[]} */ deltaX = []; /** @type {number[]} */ deltaY = []; points = 0; /** @type {number[]} */ x = []; /** @type {number[]} */ y = []; } let offset = 0; let repeatCount = 0; let repeatFlag = 0; let xCoordinates = 0; let yCoordinates = 0; const glyph = colorLayer.glyph; for (let i = 0; i < glyph.glyf.numberOfContours; i ++) { const contour = new Contour() glyph.contours[i] = contour; // 各contourのpointの数を計算 contour.points = i === 0 ? glyph.glyf.endPtsOfContours[0] + 1 : glyph.glyf.endPtsOfContours[i] - glyph.glyf.endPtsOfContours[i - 1]; } //------------------------------------------------------------ // Simple Glyph Descriptionのflags、xCoordinates、yCoordinatesをの解析 //------------------------------------------------------------ // flagsの解析 for (let i = 0; i < glyph.glyf.numberOfContours; i ++) { for (let j = 0; j < glyph.contours[i].points; j ++) { if (repeatCount > 0) { glyph.contours[i].flags[j] = repeatFlag; repeatCount --; } else { glyph.contours[i].flags[j] = Valorize.uint8(offset); offset += 1; if (glyph.contours[i].flags[j] & 0b00001000) { repeatFlag = glyph.contours[i].flags[j]; repeatCount = Valorize.uint8(offset); offset += 1; } } if (glyph.contours[i].flags[j] & 0b00000001) { glyph.contours[i].onCurvePoint[j] = 1; } else { glyph.contours[i].onCurvePoint[j] = 0; } } } // xCoordinatesの解析 for (let i = 0; i < glyph.glyf.numberOfContours; i ++) { for (let j = 0; j < glyph.contours[i].points; j ++) { if (glyph.contours[i].flags[j] & 0b00000010) { if (glyph.contours[i].flags[j] & 0b00010000) { glyph.contours[i].deltaX[j] = Valorize.uint8(offset); } else { glyph.contours[i].deltaX[j] = -1 * Valorize.uint8(offset); } offset += 1; } else { if (glyph.contours[i].flags[j] & 0b00010000) { glyph.contours[i].deltaX[j] = 0; } else { glyph.contours[i].deltaX[j] = Valorize.int16(offset); offset += 2; } } xCoordinates += glyph.contours[i].deltaX[j]; glyph.contours[i].x[j] = xCoordinates; } } // yCoordinatesの解析 for (let i = 0; i < glyph.glyf.numberOfContours; i ++) { for (let j = 0; j < glyph.contours[i].points; j ++) { if (glyph.contours[i].flags[j] & 0b00000100) { if (glyph.contours[i].flags[j] & 0b00100000) { glyph.contours[i].deltaY[j] = Valorize.uint8(offset); } else { glyph.contours[i].deltaY[j] = -1 * Valorize.uint8(offset); } offset += 1; } else { if (glyph.contours[i].flags[j] & 0b00100000) { glyph.contours[i].deltaY[j] = 0; } else { glyph.contours[i].deltaY[j] = Valorize.int16(offset); offset += 2; } } yCoordinates += glyph.contours[i].deltaY[j]; glyph.contours[i].y[j] = yCoordinates; } } //------------------------------------------------------------ // Color GlyphのSVGを作成 //------------------------------------------------------------ glyph.svgElement = svgDocument.createElement('path'); let pathData = ''; let previousCommand = ''; for (let i = 0; i < glyph.glyf.numberOfContours; i ++) { for (let j = 0, lj = glyph.contours[i].points; j < lj; j ++) { if (j === 0) { if (glyph.contours[i].onCurvePoint[0] === 1 && glyph.contours[i].onCurvePoint[1] === 1) { pathData += `M ${ glyph.contours[i].x[0] }, ${ glyph.contours[i].y[0] } L`; previousCommand = 'L'; } else if (glyph.contours[i].onCurvePoint[0] === 1 && glyph.contours[i].onCurvePoint[1] === 0) { pathData += `M ${ glyph.contours[i].x[0] }, ${ glyph.contours[i].y[0] } Q`; previousCommand = 'Q'; } else if (glyph.contours[i].onCurvePoint[0] === 0) { console.log(`Glyph: ${ glyph.unicodeNumber.toString(16).padStart(4, '0') }, Contour: ${ i }, Points: 0, Contour開始点にもかかわらずonCurvePointではありません。`); } } else if (j === lj - 1) { if (glyph.contours[i].onCurvePoint[j] === 1) { pathData += ` ${ glyph.contours[i].x[j] }, ${ glyph.contours[i].y[j] } Z`; } else { pathData += ` ${ glyph.contours[i].x[j] }, ${ glyph.contours[i].y[j] } ${ glyph.contours[i].x[0] }, ${ glyph.contours[i].y[0] } Z`; } previousCommand = ''; } else { if (glyph.contours[i].onCurvePoint[j] === 1 && glyph.contours[i].onCurvePoint[j + 1] === 1) { if (previousCommand === 'L') { pathData += ` ${ glyph.contours[i].x[j] }, ${ glyph.contours[i].y[j] }`; } else { pathData += ` ${ glyph.contours[i].x[j] }, ${ glyph.contours[i].y[j] } L`; previousCommand = 'L'; } } else if (glyph.contours[i].onCurvePoint[j] === 1 && glyph.contours[i].onCurvePoint[j + 1] === 0) { if (previousCommand === 'Q') { pathData += ` ${ glyph.contours[i].x[j] }, ${ glyph.contours[i].y[j] }`; } else { pathData += ` ${ glyph.contours[i].x[j] }, ${ glyph.contours[i].y[j] } Q`; previousCommand = 'Q'; } } else if (glyph.contours[i].onCurvePoint[j] === 0 && glyph.contours[i].onCurvePoint[j + 1] === 1) { pathData += ` ${ glyph.contours[i].x[j] }, ${ glyph.contours[i].y[j] }`; } else if (glyph.contours[i].onCurvePoint[j] === 0 && glyph.contours[i].onCurvePoint[j + 1] === 0) { pathData += ` ${ glyph.contours[i].x[j] }, ${ glyph.contours[i].y[j] } ${ Math.round(((glyph.contours[i].x[j] + glyph.contours[i].x[j + 1]) / 2) * 10) / 10 }, ${ Math.round(((glyph.contours[i].y[j] + glyph.contours[i].y[j + 1]) / 2) * 10) / 10 } `; } } } } if (pathData !== '') { if (colorLayer.type === 'solid') { if (colorLayer.CPALs[0]) { glyph.svgElement.setAttribute('fill', `rgba(${ colorLayer.CPALs[0].red }, ${ colorLayer.CPALs[0].green }, ${ colorLayer.CPALs[0].blue }, ${ colorLayer.CPALs[0].alpha / 255 })`); glyph.svgElement.setAttribute('fill-opacity', colorLayer.alpha); } if (colorLayer.transform) { glyph.svgElement.setAttribute('transform', colorLayer.transform); } } else if (colorLayer.type === 'linear' || colorLayer.type === 'radial') { glyph.svgElement.setAttribute('fill', `url(#${ glyph.glyphIDString }_${ colorLayer.numLayer })`); } glyph.svgElement.setAttribute('d', pathData); } } } } else { if (wrapperFlag === 'Source In') { // Composite Mode 5(Source In)のマスク用 // Paint Record format 10を経ず、Glyph IDが指定されずにColor Glyphの描画が行われるため、四角に対してマスクを適用する。 glyph.svgElement = svgDocument.createElement('rect'); if (colorLayer.type === 'solid') { if (colorLayer.CPALs[0]) { glyph.svgElement.setAttribute('fill', `rgba(255, 255, 255, ${ colorLayer.alpha})`); } if (colorLayer.transform) { glyph.svgElement.setAttribute('transform', colorLayer.transform); } } else if (colorLayer.type === 'linear' || colorLayer.type === 'radial') { // Color Layer Type linearおよびradialに対応していないので、コンソールに出力 console.log(colorLayer.type); } glyph.svgElement.setAttribute('width', baseGlyph.hmtx.advanceWidth); glyph.svgElement.setAttribute('height', font.OS_2.usWinAscent + font.OS_2.usWinDescent); } } //------------------------------------------------------------ // SVG出力の準備 その2 //------------------------------------------------------------ // Color GlyphのSVGをLayerに追加 colorLayer.svgElement = new DocumentFragment(); if (glyph.glyphIDString) { colorLayer.svgElement.appendChild(svgDocument.createComment(` Glyph ID: GID+${ glyph.glyphIDString } `)); } if (colorLayer.type === 'solid') { colorLayer.svgElement.appendChild(glyph.svgElement); } else if (colorLayer.type === 'linear' || colorLayer.type === 'radial') { let gradientElement = {}; if (colorLayer.type === 'linear') { gradientElement = svgDocument.createElement('linearGradient'); gradientElement.setAttribute('id', `${ glyph.glyphIDString }_${ colorLayer.numLayer }`); gradientElement.setAttribute('gradientUnits', 'userSpaceOnUse'); gradientElement.setAttribute('x1', colorLayer.x1); gradientElement.setAttribute('y1', colorLayer.y1); gradientElement.setAttribute('x2', colorLayer.x2); gradientElement.setAttribute('y2', colorLayer.y2); } else if (colorLayer.type === 'radial') { gradientElement = svgDocument.createElement('radialGradient'); gradientElement.setAttribute('id', `${ glyph.glyphIDString }_${ colorLayer.numLayer }`); gradientElement.setAttribute('gradientUnits', 'userSpaceOnUse'); gradientElement.setAttribute('fx', colorLayer.fx); gradientElement.setAttribute('fy', colorLayer.fy); gradientElement.setAttribute('fr', colorLayer.fr); gradientElement.setAttribute('cx', colorLayer.cx); gradientElement.setAttribute('cy', colorLayer.cy); gradientElement.setAttribute('r', colorLayer.r); } if (colorLayer.extend === 1) { gradientElement.setAttribute('spreadMethod', 'repeat'); } else if (colorLayer.extend === 2) { gradientElement.setAttribute('spreadMethod', 'reflect'); } if (colorLayer.transform) { gradientElement.setAttribute('gradientTransform', colorLayer.transform); } for (let j = 0; j < colorLayer.numStops; j ++) { const stopElement = svgDocument.createElement('stop'); gradientElement.appendChild(stopElement); stopElement.setAttribute('offset', colorLayer.stopOffsets[j]); stopElement.setAttribute('stop-color', `rgba(${ colorLayer.CPALs[j].red }, ${ colorLayer.CPALs[j].green }, ${ colorLayer.CPALs[j].blue }, ${ colorLayer.CPALs[j].alpha / 255 })`); stopElement.setAttribute('stop-opacity', colorLayer.alphas[j]); } colorLayer.svgElement.appendChild(gradientElement); colorLayer.svgElement.appendChild(glyph.svgElement); } if (wrapperFlag === 'Source In') { wrapperElement.appendChild(colorLayer.svgElement); } else { // LayerのSVGをBase Glyphに追加 baseGlyph.svgElement.appendChild(colorLayer.svgElement); } } //------------------------------------------------------------ // SVG出力の準備 その3 //------------------------------------------------------------ // 作成したBase GlyphのアウトラインをSVGに追加 defsElement.appendChild(svgDocument.createComment(` Character: \u0022${ stringData.characters[index1] }\u0022 `)); defsElement.appendChild(baseGlyph.svgElement); }); //------------------------------------------------------------ // SVG出力の準備 その4 //------------------------------------------------------------ // 入力文字列に対する処理 // 入力文字列に関する要素を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 baseGlyph = font.baseGlyphs[index2]; if (index1 === 0) { font.actualBoundingBoxLeft = baseGlyph.glyf.xMin; } font.actualBoundingBoxDescent = baseGlyph.glyf.yMin < font.actualBoundingBoxDescent ? baseGlyph.glyf.yMin : font.actualBoundingBoxDescent; if (index1 === array1.length - 1) { font.actualBoundingBoxRight = font.fontBoundingBoxWidth + baseGlyph.glyf.xMax; } font.actualBoundingBoxAscent = baseGlyph.glyf.yMax > font.actualBoundingBoxAscent ? baseGlyph.glyf.yMax : font.actualBoundingBoxAscent; // 入力文字列内の文字に関する要素をSVGに追加 /** @type {HTMLElement} */ const useElement = svgDocument.createElement('use'); useElement.setAttribute('href', '#' + baseGlyph.codePointString); useElement.setAttribute('transform', `matrix(1 0 0 -1 ${ font.fontBoundingBoxWidth } 0)`); gElement.appendChild(useElement); // 文字列全体の仮想ボディの幅 font.fontBoundingBoxWidth += baseGlyph.hmtx.advanceWidth; }); defsElement.appendChild(svgDocument.createComment(` ${ stringData.inputString }, width: ${ font.fontBoundingBoxWidth }, height: ${ font.OS_2.usWinAscent + font.OS_2.usWinDescent } `)); // フォントサイズの変更や中央揃えがしやすいように、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.OS_2.usWinAscent ? font.actualBoundingBoxAscent : font.OS_2.usWinAscent; font.width = font.actualBoundingBoxRight > font.fontBoundingBoxWidth ? font.actualBoundingBoxRight : font.fontBoundingBoxWidth; font.height = (font.y - font.actualBoundingBoxDescent) > (font.y + font.OS_2.usWinDescent) ? font.y - font.actualBoundingBoxDescent : font.y + font.OS_2.usWinDescent; svgElement.setAttribute('viewBox', `${ font.x - font.fontBoundingBoxWidth / 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>