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

Only download truetype files from Google Web Fonts and convert them locally #115

Merged
merged 4 commits into from
Jul 26, 2020
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
36 changes: 36 additions & 0 deletions lib/convertFontBuffer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const wawoff2 = require('wawoff2');
const woffTool = require('woff2sfnt-sfnt2woff');

const supportedFormats = new Set(['truetype', 'woff', 'woff2']);
const detectFontFormat = require('./detectFontFormat');

async function convertFontBuffer(buffer, toFormat, fromFormat) {
if (!supportedFormats.has(toFormat)) {
throw new Error(`Cannot convert to ${toFormat}`);
}
if (fromFormat) {
if (!supportedFormats.has(fromFormat)) {
throw new Error(`Cannot convert from ${toFormat}`);
}
} else {
fromFormat = detectFontFormat(buffer);
}

if (fromFormat === toFormat) {
return buffer;
}
if (fromFormat === 'woff') {
buffer = woffTool.toSfnt(buffer);
} else if (fromFormat === 'woff2') {
buffer = Buffer.from(await wawoff2.decompress(buffer));
}

if (toFormat === 'woff') {
buffer = woffTool.toWoff(buffer);
} else if (toFormat === 'woff2') {
buffer = Buffer.from(await wawoff2.compress(buffer));
}
return buffer;
}

module.exports = convertFontBuffer;
18 changes: 18 additions & 0 deletions lib/detectFontFormat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
function detectFontFormat(buffer) {
const signature = buffer.toString('ascii', 0, 4);
if (signature === 'wOFF') {
return 'woff';
} else if (signature === 'wOF2') {
return 'woff2';
} else if (
signature === 'true' ||
signature === 'OTTO' ||
signature === '\x00\x01\x00\x00'
) {
return 'truetype';
} else {
throw new Error(`Unrecognized font signature: ${signature}`);
}
}

module.exports = detectFontFormat;
47 changes: 19 additions & 28 deletions lib/subsetFonts.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const unquote = require('./unquote');
const normalizeFontPropertyValue = require('./normalizeFontPropertyValue');
const getCssRulesByProperty = require('./getCssRulesByProperty');
const unicodeRange = require('./unicodeRange');
const convertFontBuffer = require('./convertFontBuffer');

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

Expand Down Expand Up @@ -382,15 +383,13 @@ async function getSubsetsForFontUsage(
continue;
}

for (const format of formats) {
const mapId = getSubsetPromiseId(fontUsage, format);
const mapId = getSubsetPromiseId(fontUsage, 'truetype');

if (!fontCssUrlMap[mapId]) {
fontCssUrlMap[mapId] = `${getGoogleFontSubsetCssUrl(
fontUsage.props,
fontUsage.text
)}&format=${format}`;
}
if (!fontCssUrlMap[mapId]) {
fontCssUrlMap[mapId] = `${getGoogleFontSubsetCssUrl(
fontUsage.props,
fontUsage.text
)}`;
}
}
}
Expand All @@ -411,16 +410,8 @@ async function getSubsetsForFontUsage(

const assetGraphForLoadingFonts = new AssetGraph();

for (const format of formats) {
assetGraphForLoadingFonts.teepee.headers['User-Agent'] =
fontFormatUA[format];
const formatUrls = _.uniq(
Object.values(fontCssUrlMap).filter((url) =>
url.endsWith(`format=${format}`)
)
);
await assetGraphForLoadingFonts.loadAssets(Object.values(formatUrls));
}
const formatUrls = _.uniq(Object.values(fontCssUrlMap));
await assetGraphForLoadingFonts.loadAssets(Object.values(formatUrls));

await assetGraphForLoadingFonts.populate({
followRelations: {
Expand All @@ -431,7 +422,8 @@ async function getSubsetsForFontUsage(
for (const item of htmlAssetTextsWithProps) {
for (const fontUsage of item.fontUsages) {
for (const format of formats) {
const cssUrl = fontCssUrlMap[getSubsetPromiseId(fontUsage, format)];
const cssUrl =
fontCssUrlMap[getSubsetPromiseId(fontUsage, 'truetype')];
const cssAsset = assetGraphForLoadingFonts.findAssets({
url: cssUrl,
isLoaded: true,
Expand All @@ -445,8 +437,14 @@ async function getSubsetsForFontUsage(
fontUsage.subsets = {};
}

fontUsage.subsets[format] = fontAsset.rawSrc;
const size = fontAsset.rawSrc.length;
const buffer = await convertFontBuffer(
fontAsset.rawSrc,
format,
'truetype'
);

fontUsage.subsets[format] = buffer;
const size = buffer.length;
if (
!fontUsage.smallestSubsetSize ||
size < fontUsage.smallestSubsetSize
Expand Down Expand Up @@ -554,13 +552,6 @@ function getFontUsageStylesheet(fontUsages) {
.join('\n\n');
}

const fontFormatUA = {
woff:
'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0',
woff2:
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.835.2 Safari/537.36',
};

const validFontDisplayValues = [
'auto',
'block',
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
"puppeteer-core": "^3.1.0",
"specificity": "^0.4.1",
"urltools": "^0.4.1",
"wawoff2": "^1.0.2",
"woff2sfnt-sfnt2woff": "^1.0.0",
"yargs": "^15.4.0"
},
"devDependencies": {
Expand Down
153 changes: 153 additions & 0 deletions test/convertFontBuffer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
const convertFontBuffer = require('../lib/convertFontBuffer');
const detectFontFormat = require('../lib/detectFontFormat');
const fs = require('fs').promises;
const pathModule = require('path');
const expect = require('unexpected').clone();

describe('convertFontBuffer', function () {
before(async function () {
this.truetype = await fs.readFile(
pathModule.resolve(
__dirname,
'..',
'testdata',
'subsetFonts',
'Roboto-400.ttf'
)
);
this.woff = await fs.readFile(
pathModule.resolve(
__dirname,
'..',
'testdata',
'subsetFonts',
'Roboto-400.woff'
)
);
this.woff2 = await fs.readFile(
pathModule.resolve(
__dirname,
'..',
'testdata',
'subsetFonts',
'Roboto-400.woff2'
)
);
});

describe('when the source format is not given', function () {
it('should throw if the source format could not be detected', async function () {
expect(
() => convertFontBuffer(Buffer.from('abcd'), 'truetype'),
'to error',
'Unrecognized font signature: abcd'
);
});

it('should convert a truetype font to truetype', async function () {
const buffer = await convertFontBuffer(this.truetype, 'truetype');
expect(detectFontFormat(buffer), 'to equal', 'truetype');
expect(buffer, 'to be', this.truetype); // Should be a noop
});

it('should convert a truetype font to woff', async function () {
const buffer = await convertFontBuffer(this.truetype, 'woff');
expect(detectFontFormat(buffer), 'to equal', 'woff');
});

it('should convert a truetype font to woff2', async function () {
const buffer = await convertFontBuffer(this.truetype, 'woff2');
expect(detectFontFormat(buffer), 'to equal', 'woff2');
});

it('should convert a woff font to truetype', async function () {
const buffer = await convertFontBuffer(this.woff, 'truetype');
expect(detectFontFormat(buffer), 'to equal', 'truetype');
});

it('should convert a woff font to woff', async function () {
const buffer = await convertFontBuffer(this.woff, 'woff');
expect(detectFontFormat(buffer), 'to equal', 'woff');
expect(buffer, 'to be', this.woff); // Should be a noop
});

it('should convert a woff font to woff2', async function () {
const buffer = await convertFontBuffer(this.woff, 'woff2');
expect(detectFontFormat(buffer), 'to equal', 'woff2');
});

it('should convert a woff2 font to truetype', async function () {
const buffer = await convertFontBuffer(this.woff2, 'truetype');
expect(detectFontFormat(buffer), 'to equal', 'truetype');
});

it('should convert a woff2 font to woff', async function () {
const buffer = await convertFontBuffer(this.woff2, 'woff');
expect(detectFontFormat(buffer), 'to equal', 'woff');
});

it('should convert a woff2 font to woff2', async function () {
const buffer = await convertFontBuffer(this.woff2, 'woff2');
expect(detectFontFormat(buffer), 'to equal', 'woff2');
expect(buffer, 'to be', this.woff2); // Should be a noop
});
});

describe('when the source format is given', function () {
it('should convert a truetype font to truetype', async function () {
const buffer = await convertFontBuffer(
this.truetype,
'truetype',
'truetype'
);
expect(detectFontFormat(buffer), 'to equal', 'truetype');
expect(buffer, 'to be', this.truetype); // Should be a noop
});

it('should convert a truetype font to woff', async function () {
const buffer = await convertFontBuffer(this.truetype, 'woff', 'truetype');
expect(detectFontFormat(buffer), 'to equal', 'woff');
});

it('should convert a truetype font to woff2', async function () {
const buffer = await convertFontBuffer(
this.truetype,
'woff2',
'truetype'
);
expect(detectFontFormat(buffer), 'to equal', 'woff2');
});

it('should convert a woff font to truetype', async function () {
const buffer = await convertFontBuffer(this.woff, 'truetype', 'woff');
expect(detectFontFormat(buffer), 'to equal', 'truetype');
});

it('should convert a woff font to woff', async function () {
const buffer = await convertFontBuffer(this.woff, 'woff', 'woff');
expect(detectFontFormat(buffer), 'to equal', 'woff');
expect(buffer, 'to be', this.woff); // Should be a noop
});

it('should convert a woff font to woff2', async function () {
const buffer = await convertFontBuffer(this.woff, 'woff2', 'woff');
expect(detectFontFormat(buffer), 'to equal', 'woff2');
});

it('should convert a woff2 font to truetype', async function () {
const buffer = await convertFontBuffer(this.woff2, 'truetype', 'woff2');
expect(detectFontFormat(buffer), 'to equal', 'truetype');
});

it('should convert a woff2 font to woff', async function () {
const buffer = await convertFontBuffer(this.woff2, 'woff', 'woff2');
expect(detectFontFormat(buffer), 'to equal', 'woff');
});

it('should convert a woff2 font to woff2', async function () {
const buffer = await convertFontBuffer(this.woff2, 'woff2', 'woff2');
expect(detectFontFormat(buffer), 'to equal', 'woff2');
expect(buffer, 'to be', this.woff2); // Should be a noop
});
});
});
54 changes: 54 additions & 0 deletions test/detectFontFormat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const convertFontBuffer = require('../lib/convertFontBuffer');
const detectFontFormat = require('../lib/detectFontFormat');
const fs = require('fs').promises;
const pathModule = require('path');
const expect = require('unexpected').clone();

describe('detectFontFormat', function () {
it('should throw if the contents of the buffer could not be recognized', async function () {
expect(
() => convertFontBuffer(Buffer.from('abcd'), 'truetype'),
'to error',
'Unrecognized font signature: abcd'
);
});

it('should detect a truetype font', async function () {
const buffer = await fs.readFile(
pathModule.resolve(
__dirname,
'..',
'testdata',
'subsetFonts',
'Roboto-400.ttf'
)
);
expect(detectFontFormat(buffer), 'to equal', 'truetype');
});

it('should detect a woff font', async function () {
const buffer = await fs.readFile(
pathModule.resolve(
__dirname,
'..',
'testdata',
'subsetFonts',
'Roboto-400.woff'
)
);
expect(detectFontFormat(buffer), 'to equal', 'woff');
});

it('should detect a woff2 font', async function () {
const buffer = await fs.readFile(
pathModule.resolve(
__dirname,
'..',
'testdata',
'subsetFonts',
'Roboto-400.woff2'
)
);
expect(detectFontFormat(buffer), 'to equal', 'woff2');
});
});
Loading