From 23a63b8ac80929f1027709bb0f264ebed17a050e Mon Sep 17 00:00:00 2001 From: Duncan MacKenzie Date: Thu, 18 Apr 2019 15:31:18 +0100 Subject: [PATCH 1/2] Font stack support: Load fonts and emojis together --- src/elements/Document.js | 22 ++-------------------- src/stylesheet/transformStyles.js | 3 +++ tests/layout.test.js | 1 - 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/src/elements/Document.js b/src/elements/Document.js index 694ea9aa2..fd80dc156 100644 --- a/src/elements/Document.js +++ b/src/elements/Document.js @@ -47,7 +47,7 @@ class Document { this.root.instance.info.Producer = producer || 'react-pdf'; } - async loadFonts() { + async loadFontsAndEmojis() { const promises = []; const listToExplore = this.children.slice(0); @@ -58,23 +58,6 @@ class Document { promises.push(Font.load(node.style, this.root.instance)); } - if (node.children) { - node.children.forEach(childNode => { - listToExplore.push(childNode); - }); - } - } - - await Promise.all(promises); - } - - async loadEmojis() { - const promises = []; - const listToExplore = this.children.slice(0); - - while (listToExplore.length > 0) { - const node = listToExplore.shift(); - if (typeof node === 'string') { promises.push(...fetchEmojis(node)); } else if (typeof node.value === 'string') { @@ -111,7 +94,7 @@ class Document { } async loadAssets() { - await Promise.all([this.loadFonts(), this.loadImages()]); + await Promise.all([this.loadFontsAndEmojis(), this.loadImages()]); } applyProps() { @@ -172,7 +155,6 @@ class Document { try { this.addMetaData(); this.applyProps(); - await this.loadEmojis(); await this.loadAssets(); await this.renderPages(); this.root.instance.end(); diff --git a/src/stylesheet/transformStyles.js b/src/stylesheet/transformStyles.js index d02c3dd19..ca750bef4 100644 --- a/src/stylesheet/transformStyles.js +++ b/src/stylesheet/transformStyles.js @@ -164,6 +164,9 @@ const expandStyles = style => { } } break; + /* case 'fontFamily': + resolvedStyle[key] = typeof value === 'string' ? value.split(',') : Array.isArray(value) ? value : [...value]; + break; */ default: resolvedStyle[key] = value; break; diff --git a/tests/layout.test.js b/tests/layout.test.js index c445bdb67..543479b9d 100644 --- a/tests/layout.test.js +++ b/tests/layout.test.js @@ -18,7 +18,6 @@ const testImage = fs.readFileSync(path.join(__dirname, 'assets/test.jpg')); const renderDocument = async doc => { doc.addMetaData(); doc.applyProps(); - await doc.loadEmojis(); await doc.loadAssets(); const subpages = await doc.renderPages(); doc.root.instance.end(); From b66b5474d1edf0e77834379298fd389f628a2955 Mon Sep 17 00:00:00 2001 From: Duncan MacKenzie Date: Wed, 24 Apr 2019 15:30:33 +0100 Subject: [PATCH 2/2] Load fonts as required by text in text nodes --- src/elements/Document.js | 23 ++++++++++++-------- src/font/font.js | 14 ++++++++++--- src/font/index.js | 35 +++++++++++++++++++++---------- src/stylesheet/transformStyles.js | 3 --- tests/document.test.js | 14 +++++++++++++ 5 files changed, 63 insertions(+), 26 deletions(-) diff --git a/src/elements/Document.js b/src/elements/Document.js index fd80dc156..4ea76585a 100644 --- a/src/elements/Document.js +++ b/src/elements/Document.js @@ -49,22 +49,27 @@ class Document { async loadFontsAndEmojis() { const promises = []; - const listToExplore = this.children.slice(0); + const listToExplore = this.children.map(node => [node, {}]); while (listToExplore.length > 0) { - const node = listToExplore.shift(); - - if (node.style && node.style.fontFamily) { - promises.push(Font.load(node.style, this.root.instance)); - } + const [node, { parentStyle = {} }] = listToExplore.shift(); if (typeof node === 'string') { - promises.push(...fetchEmojis(node)); + promises.push( + ...fetchEmojis(node), + Font.load(parentStyle, this.root.instance, node), + ); } else if (typeof node.value === 'string') { - promises.push(...fetchEmojis(node.value)); + promises.push( + ...fetchEmojis(node.value), + Font.load(node.style || parentStyle, this.root.instance, node.value), + ); } else if (node.children) { node.children.forEach(childNode => { - listToExplore.push(childNode); + listToExplore.push([ + childNode, + { parentStyle: { ...parentStyle, ...node.style } }, + ]); }); } } diff --git a/src/font/font.js b/src/font/font.js index e7dbe3a6c..caef9a0e5 100644 --- a/src/font/font.js +++ b/src/font/font.js @@ -21,11 +21,12 @@ const throwInvalidUrl = src => { }; class FontSource { - constructor(src, fontFamily, fontStyle, fontWeight, options) { + constructor(src, fontFamily, fontStyle, fontWeight, unicodeRange, options) { this.src = src; this.fontFamily = fontFamily; this.fontStyle = fontStyle || 'normal'; this.fontWeight = processFontWeight(fontWeight) || 400; + this.unicodeRange = unicodeRange instanceof RegExp ? unicodeRange : /./; this.data = null; this.loading = false; @@ -63,9 +64,16 @@ class Font { this.sources = []; } - register({ src, fontWeight, fontStyle, ...options }) { + register({ src, fontWeight, fontStyle, unicodeRange, ...options }) { this.sources.push( - new FontSource(src, this.fontFamily, fontStyle, fontWeight, options), + new FontSource( + src, + this.fontFamily, + fontStyle, + fontWeight, + unicodeRange, + options, + ), ); } diff --git a/src/font/index.js b/src/font/index.js index 34e185108..353b687c4 100644 --- a/src/font/index.js +++ b/src/font/index.js @@ -53,18 +53,31 @@ const getFont = descriptor => { return fonts[fontFamily].resolve(descriptor); }; -const load = async function(descriptor, doc) { - const { fontFamily } = descriptor; - const isStandard = standardFonts.includes(fontFamily); - - if (isStandard) return; - - const font = getFont(descriptor); - - // We cache the font to avoid fetching it many times - if (!font.data && !font.loading) { - await font.load(); +const load = function({ fontFamily, ...descriptor }, doc, text) { + const fontFamilies = + typeof fontFamily === 'string' + ? fontFamily.split(',').map(family => family.trim()) + : [...(fontFamily || [])]; + const promises = []; + + for (const family of fontFamilies) { + if (standardFonts.includes(family)) break; + + const font = getFont({ ...descriptor, fontFamily: family }); + + const textRequiresFont = + typeof text === 'string' ? text.search(font.unicodeRange) >= 0 : true; + + // We cache the font to avoid fetching it many times + if (textRequiresFont && !font.data && !font.loading) { + promises.push(font.load()); + if (font.unicodeRange.source === '.') { + break; + } + } } + + return Promise.all(promises); }; const reset = function() { diff --git a/src/stylesheet/transformStyles.js b/src/stylesheet/transformStyles.js index ca750bef4..d02c3dd19 100644 --- a/src/stylesheet/transformStyles.js +++ b/src/stylesheet/transformStyles.js @@ -164,9 +164,6 @@ const expandStyles = style => { } } break; - /* case 'fontFamily': - resolvedStyle[key] = typeof value === 'string' ? value.split(',') : Array.isArray(value) ? value : [...value]; - break; */ default: resolvedStyle[key] = value; break; diff --git a/tests/document.test.js b/tests/document.test.js index d599e1c49..6038cc6bc 100644 --- a/tests/document.test.js +++ b/tests/document.test.js @@ -74,11 +74,25 @@ describe('Document', () => { test('Should trigger available fonts loading', async () => { const doc = new Document(dummyRoot, {}); + const page1 = new Page(dummyRoot, { style: { fontFamily: 'Courier' } }); + const text1 = new Text(dummyRoot, {}); + const textInstance1 = new TextInstance(dummyRoot, 'sample text'); + const page2 = new Page(dummyRoot, { style: { fontFamily: 'Helvetica' } }); + const text2 = new Text(dummyRoot, {}); + const textInstance2 = new TextInstance(dummyRoot, 'sample text'); + + text1.layoutEngine = { layout: jest.fn() }; + text2.layoutEngine = { layout: jest.fn() }; doc.appendChild(page1); + page1.appendChild(text1); + text1.appendChild(textInstance1); + doc.appendChild(page2); + page2.appendChild(text2); + text2.appendChild(textInstance2); await doc.render();