From d8cf34d2bd46c3a1f7096678a63df0eb6588fcf5 Mon Sep 17 00:00:00 2001 From: Andreas Lind Date: Thu, 16 Jul 2020 14:22:40 +0200 Subject: [PATCH] Add some utilities for detecting and converting fonts --- lib/convertFontBuffer.js | 36 +++++++++ lib/detectFontFormat.js | 18 +++++ lib/subsetFonts.js | 19 ++--- test/convertFontBuffer.js | 153 ++++++++++++++++++++++++++++++++++++++ test/detectFontFormat.js | 54 ++++++++++++++ 5 files changed, 269 insertions(+), 11 deletions(-) create mode 100644 lib/convertFontBuffer.js create mode 100644 lib/detectFontFormat.js create mode 100644 test/convertFontBuffer.js create mode 100644 test/detectFontFormat.js diff --git a/lib/convertFontBuffer.js b/lib/convertFontBuffer.js new file mode 100644 index 00000000..07c22d30 --- /dev/null +++ b/lib/convertFontBuffer.js @@ -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; diff --git a/lib/detectFontFormat.js b/lib/detectFontFormat.js new file mode 100644 index 00000000..6bedc8d1 --- /dev/null +++ b/lib/detectFontFormat.js @@ -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; diff --git a/lib/subsetFonts.js b/lib/subsetFonts.js index dd3dc3ad..2b2aaaf3 100644 --- a/lib/subsetFonts.js +++ b/lib/subsetFonts.js @@ -25,9 +25,7 @@ const unquote = require('./unquote'); const normalizeFontPropertyValue = require('./normalizeFontPropertyValue'); const getCssRulesByProperty = require('./getCssRulesByProperty'); const unicodeRange = require('./unicodeRange'); - -const wawoff2 = require('wawoff2'); -const woffTool = require('woff2sfnt-sfnt2woff'); +const convertFontBuffer = require('./convertFontBuffer'); const googleFontsCssUrlRegex = /^(?:https?:)?\/\/fonts\.googleapis\.com\/css/; @@ -439,15 +437,14 @@ async function getSubsetsForFontUsage( fontUsage.subsets = {}; } - let rawSrc = fontAsset.rawSrc; - if (format === 'woff') { - rawSrc = woffTool.toWoff(rawSrc); - } else if (format === 'woff2') { - rawSrc = Buffer.from(await wawoff2.compress(rawSrc)); - } + const buffer = await convertFontBuffer( + fontAsset.rawSrc, + format, + 'truetype' + ); - fontUsage.subsets[format] = rawSrc; - const size = rawSrc.length; + fontUsage.subsets[format] = buffer; + const size = buffer.length; if ( !fontUsage.smallestSubsetSize || size < fontUsage.smallestSubsetSize diff --git a/test/convertFontBuffer.js b/test/convertFontBuffer.js new file mode 100644 index 00000000..1dbcafdb --- /dev/null +++ b/test/convertFontBuffer.js @@ -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 + }); + }); +}); diff --git a/test/detectFontFormat.js b/test/detectFontFormat.js new file mode 100644 index 00000000..1d483828 --- /dev/null +++ b/test/detectFontFormat.js @@ -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'); + }); +});