Merge pull request #11634 from keymanapp/fix/developer/cherry-pick/11625-11630-ttfmeta-to-stable

fix(developer): support Windows and Unicode names in .ttf 🍒 🏠
This commit is contained in:
Marc Durdin 2024-06-02 17:41:29 +07:00 • committed by GitHub
commit 8fa82ac999
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 677 additions and 14 deletions

View file

@ -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",

View file

@ -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.
*/

View file

@ -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.

View file

@ -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.

View file

@ -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<typeof ttf.result>}
*/
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;

View file

@ -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<typeof result>}
*/
export function promise(pathOrData){
return new Promise(function(res,rej){
ttfInfo(pathOrData, function(e,d){
if (d) {
res(d);
} else {
rej(e);
}
})
})
}

View file

@ -0,0 +1,113 @@
/**
* @type {Object.<number, string>}
*/
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;
}

View file

@ -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));
}
});

View file

@ -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;
});
}
});
});
});

View file

@ -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/');
});
});

View file

@ -18,6 +18,7 @@
},
"include": [
"src/**/*.ts",
"src/ttfmeta/lib/*.js",
"src/imports/langtags.js",
],
"references": [

11
package-lock.json generated
View file

@ -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",