Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions harfbuzz.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions src/face.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from "./helpers";
import type {
AxisInfo,
NamedInstance,
NameEntry,
FeatureNameIds,
PaletteColor,
Expand Down Expand Up @@ -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<string, number> = {};
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.
Expand Down
21 changes: 21 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number>;
}

export interface NameEntry {
nameId: number;
language: string;
Expand Down
53 changes: 53 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
Expand Down