Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support reducing the variation space of individual axes (partial instancing) #23

Merged
merged 4 commits into from
Mar 24, 2024
Merged
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
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
# subset-font

Create a subset font from an existing font in SFNT (TrueType/OpenType), WOFF, or WOFF2 format. Uses [`harfbuzzjs`](https://github.com/harfbuzz/harfbuzzjs), which is a WebAssembly build of [HarfBuzz](https://harfbuzz.github.io/).
Create a subset font from an existing font in SFNT (TrueType/OpenType), WOFF, or WOFF2 format. When subsetting a variable font, you can also reduce the variation space at the individual axis level.

These operations are implemented using [`harfbuzzjs`](https://github.com/harfbuzz/harfbuzzjs), which is a WebAssembly build of [HarfBuzz](https://harfbuzz.github.io/).

## Basic example

```js
const subsetFont = require('subset-font');

const mySfntFontBuffer = Buffer.from(/*...*/);

// Create a new font with only the characters required to render "Hello, world!" in WOFF2 format:
const subsetBuffer = await subsetFont(mySfntFontBuffer, 'Hello, world!', {
targetFormat: 'woff2',
});
```

## Reducing the variation space

```js
const subsetFont = require('subset-font');
Expand All @@ -10,6 +27,15 @@ const mySfntFontBuffer = Buffer.from(/*...*/);
// Create a new font with only the characters required to render "Hello, world!" in WOFF2 format:
const subsetBuffer = await subsetFont(mySfntFontBuffer, 'Hello, world!', {
targetFormat: 'woff2',
variationAxes: {
// Pin the axis to 200:
wght: 200,
// Reduce the variation space, explicitly setting a new default value:
GRAD: { min: -50, max: 50, default: 25 },
// Reduce the variation space. A new default value will be inferred by clamping the old default to the new range:
slnt: { min: -9, max: 0 },
// The remaining axes will be kept as-is
},
});
```

Expand All @@ -25,6 +51,7 @@ Options:

- `targetFormat` - the format to output, can be either `'sfnt'`, `'woff'`, or `'woff2'`.
- `preserveNameIds` - an array of numbers specifying the extra name ids to preserve in the `name` table. By default the harfbuzz subsetter drops most of these. Use case described [here](https://github.com/papandreou/subset-font/issues/7).
- `variationAxes` - an object specifying a full or partial instancing of variation axes in the font. Only works with variable fonts. See the example above.

For backwards compatibility reasons, `'truetype'` is supported as an alias for `'sfnt'`.

Expand Down
52 changes: 46 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,52 @@ async function subsetFont(

if (variationAxes) {
for (const [axisName, value] of Object.entries(variationAxes)) {
exports.hb_subset_input_pin_axis_location(
input,
face,
HB_TAG(axisName),
value
);
if (typeof value === 'number') {
// Simple case: Pin/instance the variation axis to a single value
if (
!exports.hb_subset_input_pin_axis_location(
input,
face,
HB_TAG(axisName),
value
)
) {
exports.hb_face_destroy(face);
exports.free(fontBuffer);
throw new Error(
`hb_subset_input_pin_axis_location (harfbuzz) returned zero when pinning ${axisName} to ${value}, indicating failure. Maybe the axis does not exist in the font?`
);
}
} else if (value && typeof value === 'object') {
// Complex case: Reduce the variation space of the axis
if (
typeof value.min === 'undefined' ||
typeof value.max === 'undefined'
) {
exports.hb_face_destroy(face);
exports.free(fontBuffer);
throw new Error(
`${axisName}: You must provide both a min and a max value when setting the axis range`
);
}
if (
!exports.hb_subset_input_set_axis_range(
input,
face,
HB_TAG(axisName),
value.min,
value.max,
// An explicit NaN makes harfbuzz use the existing default value, clamping to the new range if necessary
value.default ?? NaN
)
) {
exports.hb_face_destroy(face);
exports.free(fontBuffer);
throw new Error(
`hb_subset_input_set_axis_range (harfbuzz) returned zero when setting the range of ${axisName} to [${value.min}; ${value.max}] and a default value of ${value.default}, indicating failure. Maybe the axis does not exist in the font?`
);
}
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "index.js",
"dependencies": {
"fontverter": "^2.0.0",
"harfbuzzjs": "^0.3.4",
"harfbuzzjs": "^0.3.5",
"lodash": "^4.17.21",
"p-limit": "^3.1.0"
},
Expand Down
99 changes: 99 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,9 +407,108 @@ describe('subset-font', function () {
slnt: { name: 'slnt', min: -10, default: 0, max: 0 },
});

// When not instancing the subset font is about 29 KB
expect(result.length, 'to be less than', 26000);
});
});

describe('when reducing the ranges of some variation axes', function () {
it('should perform a partial instancing', async function () {
const result = await subsetFont(this.variableRobotoFont, 'abcd', {
variationAxes: {
GRAD: { min: -50, max: 50, default: 25 },
slnt: { min: -9, max: 0 },
YTDE: { min: -100, max: -98 },
opsz: 14,
XTRA: 468,
XOPQ: 96,
YOPQ: 79,
YTLC: 514,
YTUC: 712,
YTAS: 750,
YTFI: 738,
// Leaving out wght and wdth so that the full variation space is preserved
},
});

expect(
fontkit.create(result).variationAxes,
'to exhaustively satisfy',
{
GRAD: { name: 'GRAD', min: -50, max: 50, default: 25 },
slnt: { name: 'slnt', min: -9, max: 0, default: 0 },
YTDE: { name: 'YTDE', min: -100, max: -98, default: -100 },
wght: { name: 'wght', min: 100, max: 1000, default: 400 },
wdth: { name: 'wdth', min: 25, max: 151, default: 100 },
}
);

// When not instancing the subset font is about 29 KB
expect(result.length, 'to be less than', 25000);
});

describe('when leaving out a min value', function () {
it('should error', async function () {
await expect(
() =>
subsetFont(this.variableRobotoFont, 'abcd', {
variationAxes: {
wght: { max: 300 },
},
}),
'to error',
'wght: You must provide both a min and a max value when setting the axis range'
);
});
});

describe('when leaving out a max value', function () {
it('should error', async function () {
await expect(
() =>
subsetFont(this.variableRobotoFont, 'abcd', {
variationAxes: {
wght: { min: 300 },
},
}),
'to error',
'wght: You must provide both a min and a max value when setting the axis range'
);
});
});

describe('when pinning a non-existent axis', function () {
it('should error', async function () {
await expect(
() =>
subsetFont(this.variableRobotoFont, 'abcd', {
variationAxes: {
foob: 123,
},
}),
'to error',
'hb_subset_input_pin_axis_location (harfbuzz) returned zero when pinning foob to 123, indicating failure. Maybe the axis does not exist in the font?'
);
});
});

describe('when reducing the variation space of a non-existent axis', function () {
it('should error', async function () {
await expect(
() =>
subsetFont(this.variableRobotoFont, 'abcd', {
variationAxes: {
foob: {
min: 123,
max: 456,
},
},
}),
'to error',
'hb_subset_input_set_axis_range (harfbuzz) returned zero when setting the range of foob to [123; 456] and a default value of undefined, indicating failure. Maybe the axis does not exist in the font?'
);
});
});
});
});
});
Loading