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

Use harfbuzzjs instead of fontkit #170

Merged
merged 6 commits into from
Apr 15, 2023
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
27 changes: 27 additions & 0 deletions lib/getFontInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const fontverter = require('fontverter');

async function getFontInfoFromBuffer(buffer) {
const harfbuzzJs = await require('harfbuzzjs');

const blob = harfbuzzJs.createBlob(await fontverter.convert(buffer, 'sfnt')); // Load the font data into something Harfbuzz can use
const face = harfbuzzJs.createFace(blob, 0); // Select the first font in the file (there's normally only one!)

const fontInfo = {
characterSet: Array.from(face.collectUnicodes()),
variationAxes: face.getAxisInfos(),
};

face.destroy();
blob.destroy();

return fontInfo;
}

const fontInfoPromiseByBuffer = new WeakMap();

module.exports = function getFontInfo(buffer) {
if (!fontInfoPromiseByBuffer.has(buffer)) {
fontInfoPromiseByBuffer.set(buffer, getFontInfoFromBuffer(buffer));
}
return fontInfoPromiseByBuffer.get(buffer);
};
70 changes: 31 additions & 39 deletions lib/subsetFonts.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ const injectSubsetDefinitions = require('./injectSubsetDefinitions');
const cssFontParser = require('css-font-parser');
const cssListHelpers = require('css-list-helpers');
const LinesAndColumns = require('lines-and-columns').default;
const fontkit = require('fontkit');
const crypto = require('crypto');

const unquote = require('./unquote');
const normalizeFontPropertyValue = require('./normalizeFontPropertyValue');
const getCssRulesByProperty = require('./getCssRulesByProperty');
const unicodeRange = require('./unicodeRange');
const getFontInfo = require('./getFontInfo');

const googleFontsCssUrlRegex = /^(?:https?:)?\/\/fonts\.googleapis\.com\/css/;

Expand Down Expand Up @@ -383,26 +383,17 @@ function getSubsetPromiseId(fontUsage, format, variationAxes = null) {
].join('\x1d');
}

function createFontkitMemoizer(assetGraph) {
return memoizeSync(function (url) {
return fontkit.create(assetGraph.findAssets({ url })[0].rawSrc);
});
}

function getFullyPinnedVariationAxes(
fontkitMemoizer,
async function getFullyPinnedVariationAxes(
assetGraph,
fontUrl,
seenAxisValuesByFontUrlAndAxisName
) {
let font;
try {
font = fontkitMemoizer(fontUrl);
} catch (err) {
// Don't break if we encounter an invalid font or one that's unsupported by fontkit
return;
}
const fontInfo = await getFontInfo(
assetGraph.findAssets({ url: fontUrl })[0].rawSrc
);

let variationAxes;
const fontVariationEntries = Object.entries(font.variationAxes);
const fontVariationEntries = Object.entries(fontInfo.variationAxes);
const seenAxisValuesByAxisName =
seenAxisValuesByFontUrlAndAxisName.get(fontUrl);
if (fontVariationEntries.length > 0 && seenAxisValuesByAxisName) {
Expand Down Expand Up @@ -435,7 +426,6 @@ async function getSubsetsForFontUsage(
htmlOrSvgAssetTextsWithProps,
formats,
seenAxisValuesByFontUrlAndAxisName,
fontkitMemoizer,
instance = false
) {
const allFonts = [];
Expand Down Expand Up @@ -480,8 +470,8 @@ async function getSubsetsForFontUsage(
const text = fontUsage.text;
let variationAxes;
if (instance) {
variationAxes = getFullyPinnedVariationAxes(
fontkitMemoizer,
variationAxes = await getFullyPinnedVariationAxes(
assetGraph,
fontUsage.fontUrl,
seenAxisValuesByFontUrlAndAxisName
);
Expand Down Expand Up @@ -681,7 +671,7 @@ async function createSelfHostedGoogleFontsCssAsset(
lines.push(` src: ${srcFragments.join(', ')};`);
lines.push(
` unicode-range: ${unicodeRange(
fontkit.create(cssFontFaceSrc.to.rawSrc).characterSet
(await getFontInfo(cssFontFaceSrc.to.rawSrc)).characterSet
)};`
);
lines.push('}');
Expand Down Expand Up @@ -761,7 +751,10 @@ function parseFontStretchRange(str) {
return [minFontStretch, maxFontStretch];
}

function warnAboutMissingGlyphs(htmlOrSvgAssetTextsWithProps, assetGraph) {
async function warnAboutMissingGlyphs(
htmlOrSvgAssetTextsWithProps,
assetGraph
) {
const missingGlyphsErrors = [];

for (const {
Expand All @@ -771,9 +764,9 @@ function warnAboutMissingGlyphs(htmlOrSvgAssetTextsWithProps, assetGraph) {
} of htmlOrSvgAssetTextsWithProps) {
for (const fontUsage of fontUsages) {
if (fontUsage.subsets) {
const characterSet = fontkit.create(
const { characterSet } = await getFontInfo(
Object.values(fontUsage.subsets)[0]
).characterSet;
);

let missedAny = false;
for (const char of [...fontUsage.pageText]) {
Expand Down Expand Up @@ -953,12 +946,11 @@ function getVariationAxisUsage(htmlOrSvgAssetTextsWithProps) {
return { seenAxisValuesByFontUrlAndAxisName, outOfBoundsAxesByFontUrl };
}

function warnAboutUnusedVariationAxes(
async function warnAboutUnusedVariationAxes(
assetGraph,
seenAxisValuesByFontUrlAndAxisName,
outOfBoundsAxesByFontUrl,
notFullyInstancedFontUrls,
fontkitMemoizer
notFullyInstancedFontUrls
) {
const warnings = [];
for (const [
Expand All @@ -969,17 +961,20 @@ function warnAboutUnusedVariationAxes(
continue;
}
const outOfBoundsAxes = outOfBoundsAxesByFontUrl.get(fontUrl) || new Set();
let font;
let fontInfo;
try {
font = fontkitMemoizer(fontUrl);
fontInfo = await getFontInfo(
assetGraph.findAssets({ url: fontUrl })[0].rawSrc
);
} catch (err) {
// Don't break if we encounter an invalid font or one that's unsupported by fontkit
// Don't break if we encounter an invalid font
continue;
}

const unusedAxes = [];
const underutilizedAxes = [];
for (const [name, { min, max, default: defaultValue }] of Object.entries(
font.variationAxes
fontInfo.variationAxes
)) {
if (ignoredVariationAxes.has(name)) {
continue;
Expand Down Expand Up @@ -1290,7 +1285,8 @@ async function subsetFonts(
let originalCodepoints;
try {
// Guard against 'Unknown font format' errors
originalCodepoints = fontkit.create(originalFont.rawSrc).characterSet;
originalCodepoints = (await getFontInfo(originalFont.rawSrc))
.characterSet;
} catch (err) {}
if (originalCodepoints) {
const usedCodepoints = getCodepoints(fontUsage.text);
Expand Down Expand Up @@ -1323,25 +1319,21 @@ async function subsetFonts(
const { seenAxisValuesByFontUrlAndAxisName, outOfBoundsAxesByFontUrl } =
getVariationAxisUsage(htmlOrSvgAssetTextsWithProps);

const fontkitMemoizer = createFontkitMemoizer(assetGraph);

// Generate subsets:
const { notFullyInstancedFontUrls } = await getSubsetsForFontUsage(
assetGraph,
htmlOrSvgAssetTextsWithProps,
formats,
seenAxisValuesByFontUrlAndAxisName,
fontkitMemoizer,
instance
);

warnAboutMissingGlyphs(htmlOrSvgAssetTextsWithProps, assetGraph);
warnAboutUnusedVariationAxes(
await warnAboutMissingGlyphs(htmlOrSvgAssetTextsWithProps, assetGraph);
await warnAboutUnusedVariationAxes(
assetGraph,
seenAxisValuesByFontUrlAndAxisName,
outOfBoundsAxesByFontUrl,
notFullyInstancedFontUrls,
fontkitMemoizer
notFullyInstancedFontUrls
);

// Insert subsets:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@
"css-list-helpers": "^2.0.0",
"font-snapper": "^1.2.0",
"font-tracer": "^3.6.0",
"fontkit": "^1.8.0",
"fontverter": "^2.0.0",
"gettemporaryfilepath": "^1.0.1",
"harfbuzzjs": "^0.3.3",
"lines-and-columns": "^1.1.6",
"lodash": "^4.17.15",
"memoizesync": "^1.1.1",
Expand Down
14 changes: 7 additions & 7 deletions test/subfont.js
Original file line number Diff line number Diff line change
Expand Up @@ -451,11 +451,11 @@ describe('subfont', function () {
);
expect(mockConsole.log, 'to have a call satisfying', () => {
mockConsole.log(
expect.it('to contain', '400 : 6/214 codepoints used (3 on this page),')
expect.it('to contain', '400 : 6/213 codepoints used (3 on this page),')
);
}).and('to have a call satisfying', () => {
mockConsole.log(
expect.it('to contain', '400 : 6/214 codepoints used (4 on this page),')
expect.it('to contain', '400 : 6/213 codepoints used (4 on this page),')
);
});
});
Expand All @@ -480,7 +480,7 @@ describe('subfont', function () {
mockConsole
);
expect(mockConsole.log, 'to have a call satisfying', () => {
mockConsole.log(expect.it('to contain', '400 : 3/214 codepoints used,'));
mockConsole.log(expect.it('to contain', '400 : 3/213 codepoints used,'));
});
});

Expand All @@ -507,7 +507,7 @@ describe('subfont', function () {
);
expect(mockConsole.log, 'to have a call satisfying', () => {
mockConsole.log(
expect.it('to contain', '400 : 14/214 codepoints used')
expect.it('to contain', '400 : 14/213 codepoints used')
);
});
});
Expand All @@ -534,7 +534,7 @@ describe('subfont', function () {
);
expect(mockConsole.log, 'to have a call satisfying', () => {
mockConsole.log(
expect.it('to contain', '400 : 16/214 codepoints used,')
expect.it('to contain', '400 : 16/213 codepoints used,')
);
});
});
Expand Down Expand Up @@ -562,7 +562,7 @@ describe('subfont', function () {
);
expect(mockConsole.log, 'to have a call satisfying', () => {
mockConsole.log(
expect.it('to contain', '400 : 14/214 codepoints used')
expect.it('to contain', '400 : 14/213 codepoints used')
);
});
});
Expand Down Expand Up @@ -590,7 +590,7 @@ describe('subfont', function () {
);
expect(mockConsole.log, 'to have a call satisfying', () => {
mockConsole.log(
expect.it('to contain', '400 : 14/214 codepoints used')
expect.it('to contain', '400 : 14/213 codepoints used')
);
});
});
Expand Down
46 changes: 21 additions & 25 deletions test/subsetFonts.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ const expect = require('unexpected')
const AssetGraph = require('assetgraph');
const pathModule = require('path');
const LinesAndColumns = require('lines-and-columns').default;
const fontkit = require('fontkit');

const httpception = require('httpception');
const sinon = require('sinon');
const fs = require('fs');
const subsetFonts = require('../lib/subsetFonts');
const getFontInfo = require('../lib/getFontInfo');

const defaultLocalSubsetMock = [
{
Expand Down Expand Up @@ -3206,11 +3206,8 @@ describe('subsetFonts', function () {

const subsetFontAssets = assetGraph.findAssets({ type: 'Woff2' });
expect(subsetFontAssets, 'to have length', 1);
expect(
fontkit.create(subsetFontAssets[0].rawSrc).variationAxes,
'to equal',
{}
);
const { variationAxes } = await getFontInfo(subsetFontAssets[0].rawSrc);
expect(variationAxes, 'to equal', {});
});
});

Expand All @@ -3231,25 +3228,24 @@ describe('subsetFonts', function () {

const subsetFontAssets = assetGraph.findAssets({ type: 'Woff2' });
expect(subsetFontAssets, 'to have length', 1);
expect(
fontkit.create(subsetFontAssets[0].rawSrc).variationAxes,
'to equal',
{
wght: { name: 'wght', min: 100, default: 400, max: 1000 },
wdth: { name: 'wdth', min: 25, default: 100, max: 151 },
opsz: { name: 'opsz', min: 8, default: 14, max: 144 },
GRAD: { name: 'GRAD', min: -200, default: 0, max: 150 },
slnt: { name: 'slnt', min: -10, default: 0, max: 0 },
XTRA: { name: 'XTRA', min: 323, default: 468, max: 603 },
XOPQ: { name: 'XOPQ', min: 27, default: 96, max: 175 },
YOPQ: { name: 'YOPQ', min: 25, default: 79, max: 135 },
YTLC: { name: 'YTLC', min: 416, default: 514, max: 570 },
YTUC: { name: 'YTUC', min: 528, default: 712, max: 760 },
YTAS: { name: 'YTAS', min: 649, default: 750, max: 854 },
YTDE: { name: 'YTDE', min: -305, default: -203, max: -98 },
YTFI: { name: 'YTFI', min: 560, default: 738, max: 788 },
}
);

const { variationAxes } = await getFontInfo(subsetFontAssets[0].rawSrc);

expect(variationAxes, 'to equal', {
wght: { min: 100, default: 400, max: 1000 },
wdth: { min: 25, default: 100, max: 151 },
opsz: { min: 8, default: 14, max: 144 },
GRAD: { min: -200, default: 0, max: 150 },
slnt: { min: -10, default: 0, max: 0 },
XTRA: { min: 323, default: 468, max: 603 },
XOPQ: { min: 27, default: 96, max: 175 },
YOPQ: { min: 25, default: 79, max: 135 },
YTLC: { min: 416, default: 514, max: 570 },
YTUC: { min: 528, default: 712, max: 760 },
YTAS: { min: 649, default: 750, max: 854 },
YTDE: { min: -305, default: -203, max: -98 },
YTFI: { min: 560, default: 738, max: 788 },
});
});
});
});
Expand Down