Skip to content

Commit

Permalink
Merge pull request #23 from papandreou/tech/partialInstancing
Browse files Browse the repository at this point in the history
Support reducing the variation space of individual axes (partial instancing)
  • Loading branch information
papandreou authored Mar 24, 2024
2 parents 8118084 + e473a67 commit f53dfc9
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 8 deletions.
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?'
);
});
});
});
});
});

0 comments on commit f53dfc9

Please sign in to comment.