diff --git a/developer/src/kmc-keyboard-info/package.json b/developer/src/kmc-keyboard-info/package.json index 4c0914ee4f..f77402b483 100644 --- a/developer/src/kmc-keyboard-info/package.json +++ b/developer/src/kmc-keyboard-info/package.json @@ -25,8 +25,7 @@ "dependencies": { "@keymanapp/common-types": "*", "@keymanapp/developer-utils": "*", - "@keymanapp/kmc-package": "*", - "ttfmeta": "^1.1.2" + "@keymanapp/kmc-package": "*" }, "devDependencies": { "@types/chai": "^4.3.5", diff --git a/developer/src/kmc-keyboard-info/src/font-family.ts b/developer/src/kmc-keyboard-info/src/font-family.ts index 3bcb1365f1..a78e341de7 100644 --- a/developer/src/kmc-keyboard-info/src/font-family.ts +++ b/developer/src/kmc-keyboard-info/src/font-family.ts @@ -1,4 +1,4 @@ -import ttfMeta from 'ttfmeta'; +import ttfMeta from './ttfmeta/lib/index.js'; /** * Extracts the font-family from an in-memory TTF or WOFF blob in `source` @@ -7,7 +7,7 @@ import ttfMeta from 'ttfmeta'; * @param source In-memory TTF or WOFF font blob * * @throws Uncaught exceptions from ttfMeta.promise if the font file is invalid. - * + * * @returns If the file is invalid or cannot be parsed, returns `null`, * otherwise returns the font family as a string. */ diff --git a/developer/src/kmc-keyboard-info/src/ttfmeta/LICENSE b/developer/src/kmc-keyboard-info/src/ttfmeta/LICENSE new file mode 100644 index 0000000000..6bcff0d0e2 --- /dev/null +++ b/developer/src/kmc-keyboard-info/src/ttfmeta/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Khen Solomon Lethil + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/developer/src/kmc-keyboard-info/src/ttfmeta/README.md b/developer/src/kmc-keyboard-info/src/ttfmeta/README.md new file mode 100644 index 0000000000..779526d2ee --- /dev/null +++ b/developer/src/kmc-keyboard-info/src/ttfmeta/README.md @@ -0,0 +1 @@ +This was originally npmjs package ttfmeta. The source has been deleted from GitHub, so we will now using it locally so we can maintain it. \ No newline at end of file diff --git a/developer/src/kmc-keyboard-info/src/ttfmeta/lib/index.js b/developer/src/kmc-keyboard-info/src/ttfmeta/lib/index.js new file mode 100644 index 0000000000..eb94d53c1f --- /dev/null +++ b/developer/src/kmc-keyboard-info/src/ttfmeta/lib/index.js @@ -0,0 +1,52 @@ +/** + * ttfmeta + * Copyright(c) 2021-2022 Khen Solomon Lethil + * MIT Licensed + * v1.0.9 + */ + +import * as fs from "fs"; +import * as ttf from "./main.js"; + +/** + * @namespace + * @param {string | number | Buffer | URL} pathOrData + * @param {{(error:string|null,meta?:typeof ttf.result):void}} callback + */ +export function ttfInfo(pathOrData, callback) { + try { + if (pathOrData instanceof Buffer) { + ttf.ttfInfo(ttf.view(pathOrData), callback); + } else { + fs.readFile(pathOrData, function(error, data) { + if (error) { + callback(error.message || error.toString()); + } else { + ttf.ttfInfo(ttf.view(data), callback); + } + }); + } + } catch (/** @type {any}*/ error) { + callback(error.message || error.toString()); + } +} + +/** + * @param {string | number | Buffer | URL} pathOrData + * @returns {Promise} + */ +export function promise(pathOrData) { + return new Promise(function(res, rej) { + ttfInfo(pathOrData, function(e, d) { + if (d) { + res(d); + } else { + rej(e); + } + }); + }); +} + +/** @namespace */ +export const ttfMeta = { ttfInfo, promise }; +export default ttfMeta; diff --git a/developer/src/kmc-keyboard-info/src/ttfmeta/lib/main.js b/developer/src/kmc-keyboard-info/src/ttfmeta/lib/main.js new file mode 100644 index 0000000000..239a6244dc --- /dev/null +++ b/developer/src/kmc-keyboard-info/src/ttfmeta/lib/main.js @@ -0,0 +1,317 @@ +import property from './meta.js'; + +const TABLE_COUNT_OFFSET = 4, + TABLE_HEAD_OFFSET = 12, + TABLE_HEAD_SIZE = 16, + TAG_OFFSET = 0, + TAG_SIZE = 4, + CHECKSUM_OFFSET = TAG_OFFSET + TAG_SIZE, + CHECKSUM_SIZE = 4, + CONTENTS_PTR_OFFSET = CHECKSUM_OFFSET + CHECKSUM_SIZE, + CONTENTS_PTR_SIZE = 4, + LENGTH_OFFSET = TABLE_HEAD_SIZE + CONTENTS_PTR_OFFSET; + +/** + * org: count + * @param {*} data + */ +function offsetCount(data) { + return u16(data,TABLE_COUNT_OFFSET); +} + +/** + * org: offset + * @param {*} data + * @param {string} name + */ +function offsetContent(data, name) { + return offsetData(data, name).contents; +} + +/** + * @param {*} data + * @param {string} name + * @returns {{tag:any,checksum:any,contents:any,length:any}} + */ +function offsetData(data, name) { + var numTables = offsetCount(data); + var header={ + tag: '', + checksum: '', + contents: '', + length: '' + }; + + for (var i = 0; i < numTables; ++i) { + var o = TABLE_HEAD_OFFSET + i * TABLE_HEAD_SIZE; + var tag = utf8(data.buffer.slice(o, o + CONTENTS_PTR_SIZE)); + + if (tag === name) { + header.tag= tag, + header.checksum= u32(data,o + CHECKSUM_OFFSET), + header.contents= u32(data,o + CONTENTS_PTR_OFFSET), + header.length= u32(data,o + LENGTH_OFFSET) + return header; + } + } + return header +} + +/** + * org: tableName.js + * @param {*} data + */ +function name(data) { + var ntOffset = offsetContent(data, 'name'), + offsetStorage = u16(data,ntOffset+4), + numberNameRecords = u16(data,ntOffset+2); + + var storage = offsetStorage + ntOffset; + + /** + * @type {any} + */ + var info = {}; + + for (var j = 0; j < numberNameRecords; j++) { + var o = ntOffset + 6 + j*12; + + /** + * @type {string} + */ + var platformId = u16(data,o); + + /** + * @type {string} + */ + var encodingId = u16(data,o+2); + + /** + * @type {string} + */ + // var languageId = u16(data,o+4); + + /** + * @type {string} + */ + var nameId = u16(data,o+6); + /** + * @type {number} + */ + var stringLength = u16(data,o+8); + /** + * @type {string} + */ + var stringOffset = u16(data,o+10); + + if (!info.hasOwnProperty(nameId)) { + let decoder = null; + if(platformId == 0) { // unicode + decoder = utf16be; + } else if(platformId == 1) { // macintosh + if(encodingId == 0) decoder = macroman; + } else if(platformId == 3) { // windows + decoder = utf16be; + } + + if(!decoder) { + throw new Error(`Unspecified platform ${platformId} and encoding ${encodingId} for font`); + } else { + info[nameId] = decoder(data.buffer.slice(storage+stringOffset, storage+stringOffset+stringLength)); + } + + // info[nameId] = ''; + // for (var k = 0; k < stringLength; k++) { + // var charCode = data.getInt8(storage+stringOffset+k); + // if (charCode === 0) continue; + // info[nameId] += String.fromCharCode(charCode); + // } + } + } + return info; +} + +const VERSION_OFFSET = 0, WEIGHT_CLASS_OFFSET = 4; + +/** + * org: tableOS2.js + * @param {*} data + */ +function os2(data) { + var o = offsetContent(data, 'OS/2'); + return { + version : u16(data,o+VERSION_OFFSET), + weightClass : u16(data,o+WEIGHT_CLASS_OFFSET) + }; +} + +const FORMAT_OFFSET = 0, + ITALIC_ANGLE_OFFSET = FORMAT_OFFSET + 4, + UNDERLINE_POSITION_OFFSET = ITALIC_ANGLE_OFFSET + 8, + UNDERLINE_THICKNESS_OFFSET = UNDERLINE_POSITION_OFFSET + 2, + IS_FIXED_PITCH_OFFSET = UNDERLINE_THICKNESS_OFFSET + 2; + +export const result = { + meta:{ + /** + * @type {{name:string,text:string}[]} + */ + property:[], + /** + * @type {{name:string,text:string}[]} + */ + description:[], + /** + * @type {{name:string,text:string}[]} + */ + license:[], + /** + * @type {{name:string,text:string}[]} + */ + reference:[] + }, + tables: { + name: {}, + post: {}, + os2: { + version:'',weightClass:'' + } + } +}; + +/** + * @param {*} fixed + * org: fixed16dot16 + */ +function f32(fixed) { + if (fixed & 0x80000000) { + // negative number is stored in two's complement + fixed = -(~fixed + 1); + } + return fixed / 65536; +} + +/** + * @param {*} data + * @param {number} pos + */ +function i16(data,pos) { + // return data.readInt16BE(pos); + return data.getInt16(pos); +} + +/** + * @param {*} data + * @param {number} pos + */ +function u16(data,pos) { + // return data.readUInt16BE(pos); + return data.getUint16(pos); +} + +/** + * @param {*} data + * @param {number} pos + */ +function u32(data,pos) { + // return data.readUInt32BE(pos); + return data.getUint32(pos); +} + +/** + * @param {*} str + * @returns string + */ +function utf8(str) { + // return new TextDecoder("utf-8").decode(new Uint16Array(str)); + return new TextDecoder("utf-8").decode(new Uint8Array(str)); +} + +/** + * @param {*} str + * @returns string + */ +function utf16be(str) { + return new TextDecoder("utf-16be").decode(new Uint8Array(str)); +} + +/** + * @param {*} str + * @returns string + */ +function macroman(str) { + return new TextDecoder("mac").decode(new Uint8Array(str)); +} + +/** + * org: tablePost.js + * @param {*} data + */ +function post(data) { + var o = offsetContent(data, 'post'); + return { + format : f32(u32(data,o+FORMAT_OFFSET)), + italicAngle : f32(u32(data,o+ITALIC_ANGLE_OFFSET)), + + underlinePosition : i16(data,o+UNDERLINE_POSITION_OFFSET), + underlineThickness: i16(data,o+UNDERLINE_THICKNESS_OFFSET), + + isFixedPitch : u32(data,o+IS_FIXED_PITCH_OFFSET), + minMemType42 : u32(data,o+7), + maxMemType42 : u32(data,o+9), + minMemType1 : u32(data,o+11), + maxMemType1 : u32(data,o+13) + + }; +} + +/** + * @param {any} data + * param {CallableFunction} callback + */ +function resultTables(data) { + result.tables.name = name(data); + result.tables.post = post(data); + result.tables.os2 = os2(data); + result.meta = property(result.tables.name); + return result; +} + +/** + * @param {Buffer} data + * @return {DataView} + */ +export function view(data) { + return new DataView(data.buffer, 0, data.byteLength); +} + +/** + * @namespace + * @param {*} data + * @param {{(error:string|null,meta?:typeof result):void}} callback + */ +export function ttfInfo(data, callback) { + try { + // let dataview = new DataView(data.buffer, 0, data.length); + resultTables(data); + callback(null,result); + } catch (/** @type {any}*/error) { + callback(error.message || error.toString()); + } +} + +/** + * @param {string | number | Buffer | URL | DataView} pathOrData + * @returns {Promise} + */ +export function promise(pathOrData){ + return new Promise(function(res,rej){ + ttfInfo(pathOrData, function(e,d){ + if (d) { + res(d); + } else { + rej(e); + } + }) + }) +} diff --git a/developer/src/kmc-keyboard-info/src/ttfmeta/lib/meta.js b/developer/src/kmc-keyboard-info/src/ttfmeta/lib/meta.js new file mode 100644 index 0000000000..528d757e42 --- /dev/null +++ b/developer/src/kmc-keyboard-info/src/ttfmeta/lib/meta.js @@ -0,0 +1,113 @@ +/** + * @type {Object.} + */ +const tpl = { + 0: 'Copyright', + 1: 'Font Family', + 2: 'Font Subfamily', + 3: 'Unique identifier', + 4: 'Full name', + 5: 'Version', + 6: 'Postscript name', + 7: 'Note', + 8: 'Company', + 9: 'Author', + 10: 'Description', + 11: 'URL', + 12: 'URL', + 13: 'License', + 14: 'URL', + // 15: '', + 16: 'Name' + // 17: '' +}; + +const tagName = (text='') => /^[^a-z]*$/.test(text)?text.split(' ').length>4?'paragraph':'title':'paragraph'; +/** + * format meta.tables.name, property description license, reference + * @param {{[k: string]: string}} e + */ +export default function (e){ + var meta={ + /** + * @type {{name:string,text:string}[]} + */ + property:[], + /** + * @type {{name:string,text:string}[]} + */ + description:[], + /** + * @type {{name:string,text:string}[]} + */ + license:[], + /** + * @type {{name:string,text:string}[]} + */ + reference:[] + }; + + for (const key in e) { + if (e.hasOwnProperty(key)) { + const i = parseInt(key); + /** + * @type {keyof typeof tpl} + */ + var tplId = (i); + const context = e[i].trim(); + var pA = context.replace('~\r\n?~', "\n").split('\n').map(i=>i.trim()).filter( i => i); + if (pA.length > 1) { + /** + * @type {keyof typeof meta} + */ + var id = (i == 10)?'description':'license'; + meta[id]=[]; + for (const eP in pA) { + if (pA.hasOwnProperty(eP)) { + var text = pA[eP].trim(); + meta[id].push({name:tagName(text),text:text}); + } + } + } else if(context) { + if (/^s?https?:\/\/[-_.!~*'()a-zA-Z0-9;\/?:\@&=+\$,%#]+$/.test(context)){ + var has = meta.reference.findIndex(a => a.text == context); + if (has < 0) { + meta.reference.push({name:'url',text:context}); + } + } else if (i > 0 && i < 6) { + var name = tpl[tplId].replace(' ','-').toLowerCase(); + meta.property.push({name: name, text: context}); + } else { + if (tpl.hasOwnProperty(i)){ + if (i == 0 || i == 7) { + var pA = context.replace(/---+/, "\n").split('\n').map(i=>i.trim()).filter( i => i); + for (const eP in pA) { + if (pA.hasOwnProperty(eP)) { + var text = pA[eP].trim(); + meta.description.push({name:tagName(text),text:text}); + } + } + } else if (i == 13) { + meta.license.push({name:tagName(context), text: context}); + } else { + + var name = tpl[tplId].replace(' ','-').toLowerCase(); + meta.property.push({name:name, text: context}); + } + } + } + } + + // if (i == 1) { + // meta.title =context.replace('_',' '); + // meta.keywords = context.replace('_',','); + // meta.description = context; + // } else if (i == 7 && context) { + // meta.description = context; + // } else if (i == 4 && context) { + // meta.description = context; + // } + } + } + return meta; +} \ No newline at end of file diff --git a/developer/src/kmc-keyboard-info/src/ttfmeta/test/local.mjs b/developer/src/kmc-keyboard-info/src/ttfmeta/test/local.mjs new file mode 100644 index 0000000000..acea3942e0 --- /dev/null +++ b/developer/src/kmc-keyboard-info/src/ttfmeta/test/local.mjs @@ -0,0 +1,45 @@ +import fs from "fs"; +import ttfMeta from "../index.mjs"; + +// var fontFile = '/storage/media/fonts/secondary/winuniiw.ttf'; +// var fontFile = './assets/font/Myanmar3.ttf'; +var fontFile = "./assets/font/ttfmeta.ttf"; +// var fontFile = '/storage/media/fonts/secondary/m-myanmar1.TTF'; +// var fontFile = '/Windows/Fonts/AdobeHeitiStd-Regular.otf'; +// var fontFile = '/Windows/Fonts/BirchStd.otf'; + +/** + * send file + */ +ttfMeta + .promise(fontFile) + .then((e) => console.log(e)) + .catch((e) => console.log("error", e)); + +/** + * custom read and send buffer for callback + */ +fs.readFile(fontFile, function(err, buffer) { + if (err) { + console.log("err", err); + } else { + ttfMeta.ttfInfo(buffer, function(err, info) { + console.log("error", err); + console.log("info", info); + }); + } +}); + +/** + * custom read and send buffer for promise + */ +fs.readFile(fontFile, function(err, buffer) { + if (err) { + console.log("err", err); + } else { + ttfMeta + .promise(buffer) + .then((e) => console.log(e)) + .catch((e) => console.log("error", e)); + } +}); diff --git a/developer/src/kmc-keyboard-info/src/ttfmeta/test/mocha-index.mjs b/developer/src/kmc-keyboard-info/src/ttfmeta/test/mocha-index.mjs new file mode 100644 index 0000000000..f4e5bb8e8e --- /dev/null +++ b/developer/src/kmc-keyboard-info/src/ttfmeta/test/mocha-index.mjs @@ -0,0 +1,73 @@ +import "mocha"; +import fs from "fs"; +import assert from "assert"; +import ttfMeta from "../index.mjs"; + +const fontFile = "./assets/font/Myanmar3.ttf"; + +describe("ttfMeta", () => { + it("Using callback", () => { + ttfMeta.ttfInfo(fontFile, function(err, info) { + assert.strictEqual(null, err); + + // assert.strictEqual(7,Object.keys(info.tables.name).length); + assert.strictEqual(9, Object.keys(info.tables.post).length); + assert.strictEqual(2, Object.keys(info.tables.os2).length); + }); + }); + + it("Using promise", () => { + ttfMeta.promise(fontFile).then(function(info) { + // assert.strictEqual(7,Object.keys(info.tables.name).length); + assert.strictEqual(9, Object.keys(info.tables.post).length); + assert.strictEqual(2, Object.keys(info.tables.os2).length); + }); + }); + + it("Returning object has meta property", () => { + ttfMeta.promise(fontFile).then(function(info) { + assert.strictEqual(4, Object.keys(info.meta).length); + + assert.ok(info.meta.property); + assert.ok(info.meta.license); + assert.ok(info.meta.reference); + assert.ok(info.meta.description); + }); + }); + + it("Expecting error on ttfMeta.ttfInfo callback", () => { + ttfMeta.ttfInfo("./test/none.ttf", function(error) { + assert.ok(error); + assert.strictEqual("string", typeof error); + }); + }); + + it("Expecting error on ttfMeta.promise", () => { + ttfMeta + .promise("./test/none.ttf") + .then(function(info) { + assert.throws(info); + }) + .catch(function(error) { + assert.ok(error); + assert.strictEqual("string", typeof error); + }); + }); + + it("Reading from Buffer", () => { + fs.readFile(fontFile, function(err, buffer) { + if (err) { + throw err; + } else { + ttfMeta + .promise(buffer) + .then(function(info) { + assert.ok(info); + }) + .catch(function(error) { + throw error; + }); + } + }); + }); +}); diff --git a/developer/src/kmc-keyboard-info/test/fixtures/afghan_turkmen/Lateef-Bold.ttf b/developer/src/kmc-keyboard-info/test/fixtures/afghan_turkmen/Lateef-Bold.ttf new file mode 100644 index 0000000000..5059acd147 Binary files /dev/null and b/developer/src/kmc-keyboard-info/test/fixtures/afghan_turkmen/Lateef-Bold.ttf differ diff --git a/developer/src/kmc-keyboard-info/test/fixtures/afghan_turkmen/Lateef-Regular.ttf b/developer/src/kmc-keyboard-info/test/fixtures/afghan_turkmen/Lateef-Regular.ttf new file mode 100644 index 0000000000..b0c3d61479 Binary files /dev/null and b/developer/src/kmc-keyboard-info/test/fixtures/afghan_turkmen/Lateef-Regular.ttf differ diff --git a/developer/src/kmc-keyboard-info/test/test-font-family.ts b/developer/src/kmc-keyboard-info/test/test-font-family.ts new file mode 100644 index 0000000000..b27fad8d74 --- /dev/null +++ b/developer/src/kmc-keyboard-info/test/test-font-family.ts @@ -0,0 +1,50 @@ +import * as fs from 'fs'; +import { assert } from 'chai'; +import 'mocha'; +import { makePathToFixture } from './helpers/index.js'; +import { getFontFamily } from '../src/font-family.js'; + +const AFGHAN_TURKMEN_DISPLAY_FONT = makePathToFixture('afghan_turkmen', "Lateef-Regular.ttf"); +const AFGHAN_TURKMEN_OSK_FONT = makePathToFixture('afghan_turkmen', "Lateef-Bold.ttf"); + +const AFGHAN_TURKMEN_DISPLAY_FACE_NAME = "Lateef"; +const AFGHAN_TURKMEN_OSK_FACE_NAME = "Lateef"; + +describe('font-family', function () { + it('correctly reads font facename from TrueType font file', async function() { + // #11625 + const displayFontData: Uint8Array = fs.readFileSync(AFGHAN_TURKMEN_DISPLAY_FONT); + const displayFacename = await getFontFamily(displayFontData); + assert.equal(displayFacename, AFGHAN_TURKMEN_DISPLAY_FACE_NAME); + + const oskFontData: Uint8Array = fs.readFileSync(AFGHAN_TURKMEN_OSK_FONT); + const oskFacename = await getFontFamily(oskFontData); + assert.equal(oskFacename, AFGHAN_TURKMEN_OSK_FACE_NAME); + }); + + it.skip('can read all font files in keyboards repo', async function() { + this.timeout(100000); + async function testFonts(path: string) { + const files = fs.readdirSync(path); + for(const file of files) { + if(fs.statSync(path + file).isDirectory()) { + await testFonts(path + file + '/'); + } else if(file.match(/\.(ttf|otf)$/i)) { + await testFont(path + file); + } + } + } + + async function testFont(file: string) { + console.log(`Testing ${file}`); + const fontData: Uint8Array = fs.readFileSync(file); + const facename = await getFontFamily(fontData); + assert.isNotEmpty(facename); + assert.isFalse(facename.includes('\u0000')); + } + + // To enable this, we need to have access to the shared fonts in the + // keyboards repo + await testFonts('.../keyboards/release/shared/fonts/'); + }); +}); diff --git a/developer/src/kmc-keyboard-info/tsconfig.json b/developer/src/kmc-keyboard-info/tsconfig.json index bf1ad29bd9..414ff1f122 100644 --- a/developer/src/kmc-keyboard-info/tsconfig.json +++ b/developer/src/kmc-keyboard-info/tsconfig.json @@ -18,6 +18,7 @@ }, "include": [ "src/**/*.ts", + "src/ttfmeta/lib/*.js", "src/imports/langtags.js", ], "references": [ diff --git a/package-lock.json b/package-lock.json index 918bbca135..112abef83b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1272,8 +1272,7 @@ "dependencies": { "@keymanapp/common-types": "*", "@keymanapp/developer-utils": "*", - "@keymanapp/kmc-package": "*", - "ttfmeta": "^1.1.2" + "@keymanapp/kmc-package": "*" }, "devDependencies": { "@types/chai": "^4.3.5", @@ -11983,14 +11982,6 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, - "node_modules/ttfmeta": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/ttfmeta/-/ttfmeta-1.1.2.tgz", - "integrity": "sha512-x0mKp7IkWjEa6CpI4/6HdCA3lzk8PaSBF5hubq03G2HyIzuDSdZRrO1dYgW4XD7q4sFdEWXSMYw7j0pzl4Ty6Q==", - "engines": { - "node": ">=16.0" - } - }, "node_modules/tunnel": { "version": "0.0.6", "license": "MIT",