diff --git a/harfbuzz.symbols b/harfbuzz.symbols index 00fdc5c..9b54ef0 100644 --- a/harfbuzz.symbols +++ b/harfbuzz.symbols @@ -129,6 +129,10 @@ _hb_ot_tag_to_script _hb_ot_tags_from_script_and_language _hb_ot_tags_to_script_and_language _hb_ot_var_get_axis_infos +_hb_ot_var_get_named_instance_count +_hb_ot_var_named_instance_get_design_coords +_hb_ot_var_named_instance_get_postscript_name_id +_hb_ot_var_named_instance_get_subfamily_name_id _hb_script_from_string _hb_feature_from_string _hb_feature_to_string diff --git a/src/face.ts b/src/face.ts index 203aa35..df2b47f 100644 --- a/src/face.ts +++ b/src/face.ts @@ -14,6 +14,7 @@ import { } from "./helpers"; import type { AxisInfo, + NamedInstance, NameEntry, FeatureNameIds, PaletteColor, @@ -102,6 +103,57 @@ export class Face { return result; } + /** + * Return the named instances of a variable font. + * @returns An array of {@link NamedInstance} values, empty for a non-variable + * font. + */ + getNamedInstances(): NamedInstance[] { + const count = exports.hb_ot_var_get_named_instance_count(this.ptr); + if (!count) { + return []; + } + // getAxisInfos() keeps the last axis of a repeated tag, leaving holes + // for the others, whose coordinates are skipped + const axisTags: string[] = []; + for (const axis of Object.values(this.getAxisInfos())) + axisTags[axis.axisIndex] = axis.tag; + const sp = Module.stackSave(); + const coordsLengthPtr = Module.stackAlloc(4); + const coordsPtr = Module.stackAlloc(axisTags.length * 4); + const instances: NamedInstance[] = []; + for (let i = 0; i < count; i++) { + Module.HEAPU32[coordsLengthPtr / 4] = axisTags.length; + exports.hb_ot_var_named_instance_get_design_coords( + this.ptr, + i, + coordsLengthPtr, + coordsPtr, + ); + const coords: Record = {}; + const coordsLength = Module.HEAPU32[coordsLengthPtr / 4]; + for (let j = 0; j < coordsLength; j++) { + if (axisTags[j] !== undefined) + coords[axisTags[j]] = Module.HEAPF32[coordsPtr / 4 + j]; + } + const instance: NamedInstance = { + instanceIndex: i, + subfamilyNameId: exports.hb_ot_var_named_instance_get_subfamily_name_id( + this.ptr, + i, + ), + coords, + }; + const postScriptNameId = + exports.hb_ot_var_named_instance_get_postscript_name_id(this.ptr, i); + if (postScriptNameId != HB_OT_NAME_ID_INVALID) + instance.postScriptNameId = postScriptNameId; + instances.push(instance); + } + Module.stackRestore(sp); + return instances; + } + /** * Return unicodes the face supports. * @returns A Uint32Array of supported Unicode code points. diff --git a/src/types.ts b/src/types.ts index 19c5d55..ce86e15 100644 --- a/src/types.ts +++ b/src/types.ts @@ -307,6 +307,27 @@ export interface AxisInfo { max: number; } +/** + * Data type for holding a named instance of a variable font, such as "Bold" + * or "Condensed Light". + */ +export interface NamedInstance { + /** Index of the instance in the named-instance array. */ + instanceIndex: number; + /** The `name` table Name ID that provides display names for the instance. */ + subfamilyNameId: number; + /** + * The `name` table Name ID that provides the PostScript name of the + * instance, if the font has one. + */ + postScriptNameId?: number; + /** + * The design coordinates of the instance, in un-normalized, user scales, + * keyed by axis tag. + */ + coords: Record; +} + export interface NameEntry { nameId: number; language: string; diff --git a/test/index.test.js b/test/index.test.js index 6ed2abe..b02eb6b 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -62,6 +62,59 @@ describe("Face", function () { expect(Object.keys(face.getAxisInfos())).to.have.lengthOf(0); }); + it("getNamedInstances returns the named instances of a variable font", function () { + let blob = new hb.Blob( + fs.readFileSync( + path.join(__dirname, "fonts/noto/NotoSansArabic-Variable.ttf"), + ), + ); + let face = new hb.Face(blob); + const instances = face.getNamedInstances(); + expect(instances).to.have.lengthOf(36); + expect(instances[0]).to.deep.equal({ + instanceIndex: 0, + subfamilyNameId: 258, + coords: { wght: 100, wdth: 100 }, + }); + expect(instances[11]).to.deep.equal({ + instanceIndex: 11, + subfamilyNameId: 269, + coords: { wght: 300, wdth: 75 }, + }); + expect(face.getName(instances[11].subfamilyNameId, "en")).to.equal( + "Condensed Light", + ); + expect(instances[35].coords).to.deep.equal({ wght: 900, wdth: 62.5 }); + }); + + it("getNamedInstances keys coordinates like getAxisInfos for repeated axis tags", function () { + const data = fs.readFileSync( + path.join(__dirname, "fonts/noto/NotoSansArabic-Variable.ttf"), + ); + // Rename the second axis (wdth) to wght + const numTables = data.readUInt16BE(4); + for (let i = 0; i < numTables; i++) { + const record = 12 + i * 16; + if (data.toString("latin1", record, record + 4) === "fvar") { + const fvar = data.readUInt32BE(record + 8); + const axes = fvar + data.readUInt16BE(fvar + 4); + data.write("wght", axes + 20, "latin1"); + } + } + let face = new hb.Face(new hb.Blob(data)); + expect(Object.keys(face.getAxisInfos())).to.deep.equal(["wght"]); + expect(face.getAxisInfos().wght.axisIndex).to.equal(1); + expect(face.getNamedInstances()[11].coords).to.deep.equal({ wght: 75 }); + }); + + it("getNamedInstances returns an empty array for a non-variable font", function () { + let blob = new hb.Blob( + fs.readFileSync(path.join(__dirname, "fonts/noto/NotoSans-Regular.ttf")), + ); + let face = new hb.Face(blob); + expect(face.getNamedInstances()).to.deep.equal([]); + }); + it("getTableScriptTags returns tags for a font", function () { let blob = new hb.Blob( fs.readFileSync(path.join(__dirname, "fonts/noto/NotoSans-Regular.ttf")),