From 9753581042520657631101657e4bc899007f97c3 Mon Sep 17 00:00:00 2001 From: Diego Muracciole Date: Sat, 16 Mar 2019 20:03:20 -0300 Subject: [PATCH 1/7] Add font-weight support --- src/elements/Document.js | 2 +- src/font/emoji.js | 12 +++ src/font/font.js | 121 ++++++++++++++++++++++++++ src/font/hyphenation.js | 12 +++ src/font/index.js | 108 +++++++---------------- src/stylesheet/transformFontWeight.js | 22 +++++ src/stylesheet/transformStyles.js | 3 + src/utils/attributedString.js | 4 +- 8 files changed, 207 insertions(+), 77 deletions(-) create mode 100644 src/font/emoji.js create mode 100644 src/font/font.js create mode 100644 src/font/hyphenation.js create mode 100644 src/stylesheet/transformFontWeight.js diff --git a/src/elements/Document.js b/src/elements/Document.js index a05ce0cc9..694ea9aa2 100644 --- a/src/elements/Document.js +++ b/src/elements/Document.js @@ -55,7 +55,7 @@ class Document { const node = listToExplore.shift(); if (node.style && node.style.fontFamily) { - promises.push(Font.load(node.style.fontFamily, this.root.instance)); + promises.push(Font.load(node.style, this.root.instance)); } if (node.children) { diff --git a/src/font/emoji.js b/src/font/emoji.js new file mode 100644 index 000000000..b71ff05e6 --- /dev/null +++ b/src/font/emoji.js @@ -0,0 +1,12 @@ +let emojiSource; + +export const registerEmojiSource = ({ url, format = 'png' }) => { + emojiSource = { url, format }; +}; + +export const getEmojiSource = () => emojiSource; + +export default { + registerEmojiSource, + getEmojiSource, +}; diff --git a/src/font/font.js b/src/font/font.js new file mode 100644 index 000000000..7e132d004 --- /dev/null +++ b/src/font/font.js @@ -0,0 +1,121 @@ +import isUrl from 'is-url'; +import fontkit from '@react-pdf/fontkit'; +import fetch from 'cross-fetch'; + +import { processFontWeight } from '../stylesheet/transformFontWeight'; + +const fetchFont = async (src, options) => { + const response = await fetch(src, options); + + const buffer = await (response.buffer + ? response.buffer() + : response.arrayBuffer()); + + return buffer.constructor.name === 'Buffer' ? buffer : Buffer.from(buffer); +}; + +const throwInvalidUrl = src => { + throw new Error( + `Invalid font url: ${src}. If you use relative url please replace it with absolute one (ex. /font.ttf -> http://localhost:3000/font.ttf)`, + ); +}; + +class FontSource { + constructor(src, fontFamily, fontStyle, fontWeight, options) { + this.src = src; + this.fontFamily = fontFamily; + this.fontStyle = fontStyle || 'normal'; + this.fontWeight = processFontWeight(fontWeight) || 400; + + this.data = null; + this.loaded = false; + this.loading = false; + this.options = options; + } + + async load() { + if (isUrl(this.src)) { + const { headers, body, method = 'GET' } = this.options; + const data = await fetchFont(this.src, { method, body, headers }); + this.data = fontkit.create(data); + } else { + if (BROWSER) throwInvalidUrl(this.src); // Can't load a non-url font in browser + + this.data = await new Promise((resolve, reject) => + fontkit.open(this.src, (err, data) => + err ? reject(err) : resolve(data), + ), + ); + } + } + + register(doc) { + this.loaded = true; + this.loading = false; + + // TODO: Should be different for each style? + doc.registerFont(this.fontFamily, this.data); + } +} + +class Font { + static create(family) { + return new Font(family); + } + + constructor(family) { + this.family = family; + this.sources = []; + } + + register({ src, fontWeight, fontStyle, ...options }) { + this.sources.push( + new FontSource(src, this.fontFamily, fontStyle, fontWeight, options), + ); + } + + resolve(descriptor) { + const { fontWeight = 400, fontStyle = 'normal' } = descriptor; + const styleSources = this.sources.filter(s => s.fontStyle === fontStyle); + + // Weight resolution. https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#Fallback_weights + const exactFit = styleSources.find(s => s.fontWeight === fontWeight); + + if (exactFit) return exactFit; + + let res; + + if (fontWeight >= 400 && fontWeight <= 500) { + const leftOffset = styleSources.filter(s => s.fontWeight <= fontWeight); + const rightOffset = styleSources.filter(s => s.fontWeight > 500); + const fit = styleSources.filter( + s => s.fontWeight >= fontWeight && s.fontWeight < 500, + ); + + res = fit[0] || leftOffset[leftOffset.length - 1] || rightOffset[0]; + } + + const lt = styleSources.filter(s => s.fontWeight < fontWeight); + const gt = styleSources.filter(s => s.fontWeight > fontWeight); + + if (fontWeight < 400) { + res = lt[lt.length - 1] || gt[0]; + } + + if (fontWeight > 500) { + res = gt[0] || lt[lt.length - 1]; + } + + if (!res) { + throw new Error( + `Could not resolve font for ${ + this.fontFamily + }, fontWeight ${fontWeight}`, + ); + } + + return res; + } +} + +export default Font; diff --git a/src/font/hyphenation.js b/src/font/hyphenation.js new file mode 100644 index 000000000..f55beeac0 --- /dev/null +++ b/src/font/hyphenation.js @@ -0,0 +1,12 @@ +let hyphenationCallback; + +export const registerHyphenationCallback = callback => { + hyphenationCallback = callback; +}; + +export const getHyphenationCallback = () => hyphenationCallback; + +export default { + registerHyphenationCallback, + getHyphenationCallback, +}; diff --git a/src/font/index.js b/src/font/index.js index 8ca74fbd1..5b131ffd5 100644 --- a/src/font/index.js +++ b/src/font/index.js @@ -1,91 +1,51 @@ -import isUrl from 'is-url'; -import fetch from 'cross-fetch'; -import fontkit from '@react-pdf/fontkit'; - +import font from './font'; +import emoji from './emoji'; import standardFonts from './standard'; +import hyphenation from './hyphenation'; let fonts = {}; -let emojiSource; -let hyphenationCallback; - -const fetchFont = async (src, options) => { - const response = await fetch(src, options); - - const buffer = await (response.buffer - ? response.buffer() - : response.arrayBuffer()); - return buffer.constructor.name === 'Buffer' ? buffer : Buffer.from(buffer); -}; - -const register = (src, { family, ...otherOptions }) => { - fonts[family] = { - src, - loaded: false, - loading: false, - data: null, - ...otherOptions, - }; -}; +const register = (src, data) => { + const { family } = data; -const registerHyphenationCallback = callback => { - hyphenationCallback = callback; -}; + if (!fonts[family]) { + fonts[family] = font.create(family); + } -const registerEmojiSource = ({ url, format = 'png' }) => { - emojiSource = { url, format }; + fonts[family].register({ src, ...data }); }; const getRegisteredFonts = () => Object.keys(fonts); -const getFont = family => fonts[family]; - -const getEmojiSource = () => emojiSource; +const getFont = descriptor => { + const { fontFamily } = descriptor; + const isStandard = standardFonts.includes(fontFamily); -const getHyphenationCallback = () => hyphenationCallback; + if (isStandard) return standardFonts[fontFamily]; -const load = async function(fontFamily, doc) { - const font = getFont(fontFamily); + if (!fonts[fontFamily]) { + throw new Error( + `Font family not registered: ${fontFamily}. Please register it calling Font.register() method.`, + ); + } - // We cache the font to avoid fetching it many times - if (font && !font.data && !font.loading) { - font.loading = true; + return fonts[fontFamily].resolve(descriptor); +}; - if (isUrl(font.src)) { - const { src, headers, body, method = 'GET' } = font; - const data = await fetchFont(src, { headers, method, body }); - font.data = fontkit.create(data); - } else { - if (BROWSER) { - throw new Error( - `Invalid font url: ${ - font.src - }. If you use relative url please replace it with absolute one (ex. /font.ttf -> http://localhost:3000/font.ttf)`, - ); - } +const load = async function(descriptor, doc) { + const font = getFont(descriptor); - font.data = await new Promise((resolve, reject) => - fontkit.open(font.src, (err, data) => - err ? reject(err) : resolve(data), - ), - ); + // We cache the font to avoid fetching it many times + if (!font.data && !font.loading) { + await font.load(); + + // If the font wasn't added to the document yet (aka. loaded), we add it. + // This prevents calling `registerFont` many times for the same font. + // Fonts loaded state will be reset after the document is closed. + if (!font.loaded) { + font.register(doc); } } - - // If the font wasn't added to the document yet (aka. loaded), we add it. - // This prevents calling `registerFont` many times for the same font. - // Fonts loaded state will be reset after the document is closed. - if (font && !font.loaded) { - font.loaded = true; - font.loading = false; - doc.registerFont(fontFamily, font.data); - } - - if (!font && !standardFonts.includes(fontFamily)) { - throw new Error( - `Font family not registered: ${fontFamily}. Please register it calling Font.register() method.`, - ); - } }; const reset = function() { @@ -102,13 +62,11 @@ const clear = function() { export default { register, - getEmojiSource, getRegisteredFonts, - registerEmojiSource, - registerHyphenationCallback, - getHyphenationCallback, getFont, load, clear, reset, + ...emoji, + ...hyphenation, }; diff --git a/src/stylesheet/transformFontWeight.js b/src/stylesheet/transformFontWeight.js new file mode 100644 index 000000000..1073b0329 --- /dev/null +++ b/src/stylesheet/transformFontWeight.js @@ -0,0 +1,22 @@ +// https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#Common_weight_name_mapping +const FONT_WEIGHTS = { + thin: 100, + ultralight: 200, + light: 300, + normal: 400, + medium: 500, + semibold: 600, + bold: 700, + ultrabold: 800, + heavy: 900, +}; + +export const isFontWeightStyle = key => key.match(/^fontWeight/); + +export const processFontWeight = value => { + console.log(value); + + if (!value) return FONT_WEIGHTS.normal; + if (typeof value === 'number') return value; + return FONT_WEIGHTS[value.toLowerCase()]; +}; diff --git a/src/stylesheet/transformStyles.js b/src/stylesheet/transformStyles.js index 967a6c4f8..d02c3dd19 100644 --- a/src/stylesheet/transformStyles.js +++ b/src/stylesheet/transformStyles.js @@ -2,6 +2,7 @@ import yogaValue from './yogaValue'; import parseScalar from './transformUnits'; import { isBorderStyle, processBorders } from './borders'; import { isBoxModelStyle, processBoxModel } from './boxModel'; +import { isFontWeightStyle, processFontWeight } from './transformFontWeight'; import { isObjectPositionStyle, processObjectPosition } from './objectPosition'; import { isTransformOriginStyle, @@ -190,6 +191,8 @@ const transformStyles = style => { resolved = processObjectPosition(key, value); } else if (isTransformOriginStyle(key, value)) { resolved = processTransformOrigin(key, value); + } else if (isFontWeightStyle(key, value)) { + resolved = processFontWeight(value); } else { resolved = value; } diff --git a/src/utils/attributedString.js b/src/utils/attributedString.js index 4db28b069..f5ec42641 100644 --- a/src/utils/attributedString.js +++ b/src/utils/attributedString.js @@ -28,6 +28,8 @@ export const getFragments = instance => { color = 'black', backgroundColor, fontFamily = 'Helvetica', + fontWeight, + fontStyle, fontSize = 18, textAlign = 'left', position, @@ -44,7 +46,7 @@ export const getFragments = instance => { instance.children.forEach(child => { if (child.value !== null && child.value !== undefined) { - const obj = Font.getFont(fontFamily); + const obj = Font.getFont({ fontFamily, fontWeight, fontStyle }); const font = obj ? obj.data : fontFamily; const string = transformText(child.value, textTransform); From 2ea8a91580e96d5ab156023c6943ab93c3705311 Mon Sep 17 00:00:00 2001 From: Diego Muracciole Date: Tue, 19 Mar 2019 14:51:19 -0300 Subject: [PATCH 2/7] Change register API --- src/font/index.js | 14 +++++++++++++- src/stylesheet/transformFontWeight.js | 2 -- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/font/index.js b/src/font/index.js index 5b131ffd5..f222e39ab 100644 --- a/src/font/index.js +++ b/src/font/index.js @@ -2,17 +2,29 @@ import font from './font'; import emoji from './emoji'; import standardFonts from './standard'; import hyphenation from './hyphenation'; +import warning from '../utils/warning'; let fonts = {}; const register = (src, data) => { + if (typeof src === 'object') { + data = src; + } else { + warning( + false, + 'Font.register will not longer accept the font source as first argument. Please move it into the data object. For more info refer to https://react-pdf.org/fonts', + ); + + data.src = src; + } + const { family } = data; if (!fonts[family]) { fonts[family] = font.create(family); } - fonts[family].register({ src, ...data }); + fonts[family].register(data); }; const getRegisteredFonts = () => Object.keys(fonts); diff --git a/src/stylesheet/transformFontWeight.js b/src/stylesheet/transformFontWeight.js index 1073b0329..b2813ffbd 100644 --- a/src/stylesheet/transformFontWeight.js +++ b/src/stylesheet/transformFontWeight.js @@ -14,8 +14,6 @@ const FONT_WEIGHTS = { export const isFontWeightStyle = key => key.match(/^fontWeight/); export const processFontWeight = value => { - console.log(value); - if (!value) return FONT_WEIGHTS.normal; if (typeof value === 'number') return value; return FONT_WEIGHTS[value.toLowerCase()]; From 28248a4d5d5683e7fd897f34015669de46646b41 Mon Sep 17 00:00:00 2001 From: Diego Muracciole Date: Tue, 19 Mar 2019 14:56:30 -0300 Subject: [PATCH 3/7] Add bulk loading feature --- src/font/index.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/font/index.js b/src/font/index.js index f222e39ab..a3fd4d156 100644 --- a/src/font/index.js +++ b/src/font/index.js @@ -24,7 +24,14 @@ const register = (src, data) => { fonts[family] = font.create(family); } - fonts[family].register(data); + // Bulk loading + if (data.fonts) { + for (let i = 0; i < data.fonts.length; i++) { + fonts[family].register({ family, ...data.fonts[i] }); + } + } else { + fonts[family].register(data); + } }; const getRegisteredFonts = () => Object.keys(fonts); From edff33f3936546c995cc4cf704ab37c81333a5b8 Mon Sep 17 00:00:00 2001 From: Diego Muracciole Date: Tue, 19 Mar 2019 16:11:20 -0300 Subject: [PATCH 4/7] Fix tests --- src/font/index.js | 7 +++- tests/document.test.js | 4 +- tests/font.test.js | 75 ++++++++++++++++++++++++------------- tests/stylesInherit.test.js | 37 +++++++++--------- 4 files changed, 77 insertions(+), 46 deletions(-) diff --git a/src/font/index.js b/src/font/index.js index a3fd4d156..f0548a042 100644 --- a/src/font/index.js +++ b/src/font/index.js @@ -40,7 +40,7 @@ const getFont = descriptor => { const { fontFamily } = descriptor; const isStandard = standardFonts.includes(fontFamily); - if (isStandard) return standardFonts[fontFamily]; + if (isStandard) return null; if (!fonts[fontFamily]) { throw new Error( @@ -52,6 +52,11 @@ const getFont = 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 diff --git a/tests/document.test.js b/tests/document.test.js index d3ef794ea..d599e1c49 100644 --- a/tests/document.test.js +++ b/tests/document.test.js @@ -83,8 +83,8 @@ describe('Document', () => { await doc.render(); expect(Font.load.mock.calls).toHaveLength(2); - expect(Font.load.mock.calls[0][0]).toBe('Courier'); - expect(Font.load.mock.calls[1][0]).toBe('Helvetica'); + expect(Font.load.mock.calls[0][0].fontFamily).toBe('Courier'); + expect(Font.load.mock.calls[1][0].fontFamily).toBe('Helvetica'); }); test('Should trigger available images loading', async () => { diff --git a/tests/font.test.js b/tests/font.test.js index 3d11cd66c..7720ac0e4 100644 --- a/tests/font.test.js +++ b/tests/font.test.js @@ -20,14 +20,14 @@ describe('Font', () => { }); test('should be able to register font families', () => { - Font.register('src', { family: 'MyFont' }); - Font.register('src', { family: 'MyOtherFont' }); + Font.register({ family: 'MyFont', src: 'src' }); + Font.register({ family: 'MyOtherFont', src: 'src' }); expect(Font.getRegisteredFonts()).toEqual(['MyFont', 'MyOtherFont']); }); test('should be able to clear registered fonts', () => { - Font.register('src', { family: 'MyFont' }); + Font.register({ family: 'MyFont', src: 'src' }); expect(Font.getRegisteredFonts()).toEqual(['MyFont']); @@ -36,13 +36,30 @@ describe('Font', () => { expect(Font.getRegisteredFonts()).toEqual([]); }); + test.skip('should show warning when old register API used', async () => { + fetch.once(localFont); + + const descriptor = { fontFamily: 'Oswald' }; + + Font.register({ family: 'Oswald', src: oswaldUrl }); + await Font.load(descriptor, dummyRoot.instance); + + const font = Font.getFont(descriptor); + + expect(font.loaded).toBeTruthy(); + expect(font.loading).toBeFalsy(); + expect(font.data).toBeTruthy(); + }); + test('should be able to load font from url', async () => { fetch.once(localFont); - Font.register(oswaldUrl, { family: 'Oswald' }); - await Font.load('Oswald', dummyRoot.instance); + const descriptor = { fontFamily: 'Oswald' }; + + Font.register({ family: 'Oswald', src: oswaldUrl }); + await Font.load(descriptor, dummyRoot.instance); - const font = Font.getFont('Oswald'); + const font = Font.getFont(descriptor); expect(font.loaded).toBeTruthy(); expect(font.loading).toBeFalsy(); @@ -52,8 +69,10 @@ describe('Font', () => { test('should fetch remote font using GET method by default', async () => { fetch.once(localFont); - Font.register(oswaldUrl, { family: 'Oswald' }); - await Font.load('Oswald', dummyRoot.instance); + const descriptor = { fontFamily: 'Oswald' }; + + Font.register({ family: 'Oswald', src: oswaldUrl }); + await Font.load(descriptor, dummyRoot.instance); expect(fetch.mock.calls[0][1].method).toBe('GET'); }); @@ -61,8 +80,10 @@ describe('Font', () => { test('Should fetch remote font using passed method', async () => { fetch.once(localFont); - Font.register(oswaldUrl, { family: 'Oswald', method: 'POST' }); - await Font.load('Oswald', dummyRoot.instance); + const descriptor = { fontFamily: 'Oswald' }; + + Font.register({ family: 'Oswald', src: oswaldUrl, method: 'POST' }); + await Font.load(descriptor, dummyRoot.instance); expect(fetch.mock.calls[0][1].method).toBe('POST'); }); @@ -70,10 +91,11 @@ describe('Font', () => { test('Should fetch remote font using passed headers', async () => { fetch.once(localFont); + const descriptor = { fontFamily: 'Oswald' }; const headers = { Authorization: 'Bearer qwerty' }; - Font.register(oswaldUrl, { family: 'Oswald', headers }); - await Font.load('Oswald', dummyRoot.instance); + Font.register({ family: 'Oswald', src: oswaldUrl, headers }); + await Font.load(descriptor, dummyRoot.instance); expect(fetch.mock.calls[0][1].headers).toBe(headers); }); @@ -81,20 +103,23 @@ describe('Font', () => { test('Should fetch remote font using passed body', async () => { fetch.once(localFont); + const descriptor = { fontFamily: 'Oswald' }; const body = 'qwerty'; - Font.register(oswaldUrl, { family: 'Oswald', body }); - await Font.load('Oswald', dummyRoot.instance); + Font.register({ family: 'Oswald', src: oswaldUrl, body }); + await Font.load(descriptor, dummyRoot.instance); expect(fetch.mock.calls[0][1].body).toBe(body); }); test('should be able to load a font from file', async () => { - Font.register(`${__dirname}/assets/font.ttf`, { family: 'Roboto' }); + Font.register({ family: 'Roboto', src: `${__dirname}/assets/font.ttf` }); + + const descriptor = { fontFamily: 'Roboto' }; - await Font.load('Roboto', dummyRoot.instance); + await Font.load(descriptor, dummyRoot.instance); - const font = Font.getFont('Roboto'); + const font = Font.getFont(descriptor); expect(font.loaded).toBeTruthy(); expect(font.loading).toBeFalsy(); @@ -125,11 +150,11 @@ describe('Font', () => { describe('invalid url', () => { test('should throw `no such file or directory` error', async () => { - Font.register('/roboto.ttf', { family: 'Roboto' }); + Font.register({ family: 'Roboto', src: '/roboto.ttf' }); - expect(Font.load('Roboto', dummyRoot.instance)).rejects.toThrow( - 'no such file or directory', - ); + expect( + Font.load({ fontFamily: 'Roboto' }, dummyRoot.instance), + ).rejects.toThrow('no such file or directory'); }); describe('in browser', () => { @@ -142,11 +167,11 @@ describe('Font', () => { }); test('should throw `Invalid font url` error', async () => { - Font.register('/roboto.ttf', { family: 'Roboto' }); + Font.register({ family: 'Roboto', src: '/roboto.ttf' }); - expect(Font.load('Roboto', dummyRoot.instance)).rejects.toThrow( - 'Invalid font url', - ); + expect( + Font.load({ fontFamily: 'Roboto' }, dummyRoot.instance), + ).rejects.toThrow('Invalid font url'); }); }); }); diff --git a/tests/stylesInherit.test.js b/tests/stylesInherit.test.js index c8127b5c8..0fa0ab138 100644 --- a/tests/stylesInherit.test.js +++ b/tests/stylesInherit.test.js @@ -10,9 +10,7 @@ describe('Styles inherit', () => { dummyRoot = root.reset(); }); - const shouldInherit = attribute => async () => { - const value = 'Courier'; - + const shouldInherit = (attribute, value) => async () => { const doc = new Document(dummyRoot, {}); const page = new Page(dummyRoot, {}); const parent = new View(dummyRoot, { style: { [attribute]: value } }); @@ -50,9 +48,9 @@ describe('Styles inherit', () => { expect(child2.style[attribute]).not.toBe(value); }; - const shouldOverride = (attribute, value) => async () => { - const parentValue = 'Courier'; - const childValue = 'Helvetica'; + const shouldOverride = (attribute, value1, value2) => async () => { + const parentValue = value1 || 'Courier'; + const childValue = value2 || 'Helvetica'; const doc = new Document(dummyRoot, {}); const page = new Page(dummyRoot, {}); @@ -71,23 +69,26 @@ describe('Styles inherit', () => { expect(child2.style[attribute]).toBe(childValue); }; - test('Should inherit color', shouldInherit('color')); - test('Should inherit opacity', shouldInherit('opacity')); - test('Should inherit font size', shouldInherit('fontSize')); - test('Should inherit text align', shouldInherit('textAlign')); - test('Should inherit visibility', shouldInherit('visibility')); - test('Should inherit font weight', shouldInherit('fontWeight')); - test('Should inherit line height', shouldInherit('lineHeight')); - test('Should inherit font family', shouldInherit('fontFamily')); - test('Should inherit word spacing', shouldInherit('wordSpacing')); - test('Should inherit letter spacing', shouldInherit('letterSpacing')); - test('Should inherit text decoration', shouldInherit('textDecoration')); + test('Should inherit color', shouldInherit('color', 'red')); + test('Should inherit opacity', shouldInherit('opacity', 0.5)); + test('Should inherit font size', shouldInherit('fontSize', 12)); + test('Should inherit text align', shouldInherit('textAlign', 'center')); + test('Should inherit visibility', shouldInherit('visibility', 'none')); + test('Should inherit font weight', shouldInherit('fontWeight', 700)); + test('Should inherit line height', shouldInherit('lineHeight', 12)); + test('Should inherit font family', shouldInherit('fontFamily', 'Courier')); + test('Should inherit word spacing', shouldInherit('wordSpacing', 10)); + test('Should inherit letter spacing', shouldInherit('letterSpacing', 10)); + test( + 'Should inherit text decoration', + shouldInherit('textDecoration', 'underline'), + ); test('Should override color', shouldOverride('color')); test('Should override font size', shouldOverride('fontSize')); test('Should override text align', shouldOverride('textAlign')); test('Should override visibility', shouldOverride('visibility')); - test('Should override font weight', shouldOverride('fontWeight')); + test('Should override font weight', shouldOverride('fontWeight', 700, 100)); test('Should override line height', shouldOverride('lineHeight')); test('Should override font family', shouldOverride('fontFamily')); test('Should override word spacing', shouldOverride('wordSpacing')); From dd8d0f5561ffabb886ef795dfc9afdf957ad3a53 Mon Sep 17 00:00:00 2001 From: Diego Muracciole Date: Tue, 19 Mar 2019 16:16:27 -0300 Subject: [PATCH 5/7] Update typings --- index.d.ts | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/index.d.ts b/index.d.ts index ac5c10f59..6f234ac10 100644 --- a/index.d.ts +++ b/index.d.ts @@ -94,7 +94,7 @@ declare module '@react-pdf/renderer' { type SourceObject = | string | { data: Buffer; format: 'png' | 'jpg' } - | { uri: string; method: HTTPMethod, body: any, headers: any } + | { uri: string; method: HTTPMethod; body: any; headers: any }; interface BaseImageProps extends NodeProps { debug?: boolean; @@ -216,11 +216,31 @@ declare module '@react-pdf/renderer' { */ class PDFDownloadLink extends React.Component {} + type FontStyle = 'normal' | 'italic' | 'oblique'; + + type FontWeight = + | number + | 'thin' + | 'ultralight' + | 'light' + | 'normal' + | 'medium' + | 'semibold' + | 'bold' + | 'ultrabold' + | 'heavy'; + interface EmojiSource { url: string; format: string; } + interface FontDescriptor { + family: string; + fontStyle: FontStyle; + fontWeight: FontWeight; + } + interface RegisteredFont { src: string; loaded: boolean; @@ -235,10 +255,11 @@ declare module '@react-pdf/renderer' { ) => string[]; const Font: { - register: ( - src: string, - options: { family: string; [key: string]: any }, - ) => void; + register: (options: { + family: string; + src: string; + [key: string]: any; + }) => void; getEmojiSource: () => EmojiSource; getRegisteredFonts: () => string[]; registerEmojiSource: (emojiSource: EmojiSource) => void; @@ -246,9 +267,9 @@ declare module '@react-pdf/renderer' { hyphenationCallback: HyphenationCallback, ) => void; getHyphenationCallback: () => HyphenationCallback; - getFont: (fontFamily: string) => RegisteredFont | undefined; + getFont: (fontDescriptor: FontDescriptor) => RegisteredFont | undefined; load: ( - fontFamily: string, + fontDescriptor: FontDescriptor, document: React.ReactElement, ) => Promise; clear: () => void; From 9c0a074dd2f4cbcb0d7098ba22b9bfee7b448804 Mon Sep 17 00:00:00 2001 From: Diego Muracciole Date: Wed, 20 Mar 2019 00:08:29 -0300 Subject: [PATCH 6/7] Remove unnecesary register --- src/font/font.js | 7 ------- src/font/index.js | 9 +-------- tests/font.test.js | 3 --- 3 files changed, 1 insertion(+), 18 deletions(-) diff --git a/src/font/font.js b/src/font/font.js index 7e132d004..bc0848749 100644 --- a/src/font/font.js +++ b/src/font/font.js @@ -28,7 +28,6 @@ class FontSource { this.fontWeight = processFontWeight(fontWeight) || 400; this.data = null; - this.loaded = false; this.loading = false; this.options = options; } @@ -47,14 +46,8 @@ class FontSource { ), ); } - } - register(doc) { - this.loaded = true; this.loading = false; - - // TODO: Should be different for each style? - doc.registerFont(this.fontFamily, this.data); } } diff --git a/src/font/index.js b/src/font/index.js index f0548a042..927f5936f 100644 --- a/src/font/index.js +++ b/src/font/index.js @@ -62,20 +62,13 @@ const load = async function(descriptor, doc) { // We cache the font to avoid fetching it many times if (!font.data && !font.loading) { await font.load(); - - // If the font wasn't added to the document yet (aka. loaded), we add it. - // This prevents calling `registerFont` many times for the same font. - // Fonts loaded state will be reset after the document is closed. - if (!font.loaded) { - font.register(doc); - } } }; const reset = function() { for (const font in fonts) { if (fonts.hasOwnProperty(font)) { - fonts[font].loaded = false; + fonts[font].data = null; } } }; diff --git a/tests/font.test.js b/tests/font.test.js index 7720ac0e4..15cb5cb55 100644 --- a/tests/font.test.js +++ b/tests/font.test.js @@ -46,7 +46,6 @@ describe('Font', () => { const font = Font.getFont(descriptor); - expect(font.loaded).toBeTruthy(); expect(font.loading).toBeFalsy(); expect(font.data).toBeTruthy(); }); @@ -61,7 +60,6 @@ describe('Font', () => { const font = Font.getFont(descriptor); - expect(font.loaded).toBeTruthy(); expect(font.loading).toBeFalsy(); expect(font.data).toBeTruthy(); }); @@ -121,7 +119,6 @@ describe('Font', () => { const font = Font.getFont(descriptor); - expect(font.loaded).toBeTruthy(); expect(font.loading).toBeFalsy(); expect(font.data).toBeTruthy(); }); From 1e9dc1daccbaf8afd6d312d360f26d287f2b1569 Mon Sep 17 00:00:00 2001 From: Diego Muracciole Date: Wed, 20 Mar 2019 00:59:32 -0300 Subject: [PATCH 7/7] Add tests --- index.d.ts | 18 +- src/font/index.js | 5 +- src/stylesheet/transformFontWeight.js | 5 + tests/font.test.js | 228 ++++++++++++++++++++++++-- tests/styleShorthands.test.js | 64 ++++++++ 5 files changed, 302 insertions(+), 18 deletions(-) diff --git a/index.d.ts b/index.d.ts index 6f234ac10..093875e25 100644 --- a/index.d.ts +++ b/index.d.ts @@ -230,6 +230,21 @@ declare module '@react-pdf/renderer' { | 'ultrabold' | 'heavy'; + interface FontSource { + src: string; + fontFamily: string; + fontStyle: FontStyle; + fontWeight: number; + data: any; + loading: boolean; + options: any; + } + + interface FontInstance { + family: string; + sources: FontSource[]; + } + interface EmojiSource { url: string; format: string; @@ -261,7 +276,8 @@ declare module '@react-pdf/renderer' { [key: string]: any; }) => void; getEmojiSource: () => EmojiSource; - getRegisteredFonts: () => string[]; + getRegisteredFonts: () => FontInstance[]; + getRegisteredFontFamilies: () => string[]; registerEmojiSource: (emojiSource: EmojiSource) => void; registerHyphenationCallback: ( hyphenationCallback: HyphenationCallback, diff --git a/src/font/index.js b/src/font/index.js index 927f5936f..34e185108 100644 --- a/src/font/index.js +++ b/src/font/index.js @@ -34,7 +34,9 @@ const register = (src, data) => { } }; -const getRegisteredFonts = () => Object.keys(fonts); +const getRegisteredFonts = () => fonts; + +const getRegisteredFontFamilies = () => Object.keys(fonts); const getFont = descriptor => { const { fontFamily } = descriptor; @@ -80,6 +82,7 @@ const clear = function() { export default { register, getRegisteredFonts, + getRegisteredFontFamilies, getFont, load, clear, diff --git a/src/stylesheet/transformFontWeight.js b/src/stylesheet/transformFontWeight.js index b2813ffbd..52b7e1309 100644 --- a/src/stylesheet/transformFontWeight.js +++ b/src/stylesheet/transformFontWeight.js @@ -1,14 +1,19 @@ // https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#Common_weight_name_mapping const FONT_WEIGHTS = { thin: 100, + hairline: 100, ultralight: 200, + extralight: 200, light: 300, normal: 400, medium: 500, semibold: 600, + demibold: 600, bold: 700, ultrabold: 800, + extrabold: 800, heavy: 900, + black: 900, }; export const isFontWeightStyle = key => key.match(/^fontWeight/); diff --git a/tests/font.test.js b/tests/font.test.js index 15cb5cb55..c756b4389 100644 --- a/tests/font.test.js +++ b/tests/font.test.js @@ -3,15 +3,19 @@ import path from 'path'; import Font from '../src/font'; import root from './utils/dummyRoot'; +import warning from '../src/utils/warning'; + +jest.mock('../src/utils/warning'); let dummyRoot; +const localFont = fs.readFileSync(path.join(__dirname, 'assets/font.ttf')); const oswaldUrl = 'https://fonts.gstatic.com/s/oswald/v13/Y_TKV6o8WovbUd3m_X9aAA.ttf'; -const localFont = fs.readFileSync(path.join(__dirname, 'assets/font.ttf')); describe('Font', () => { beforeEach(() => { fetch.resetMocks(); + warning.mockReset(); dummyRoot = root.reset(); }); @@ -19,35 +23,85 @@ describe('Font', () => { Font.clear(); }); - test('should be able to register font families', () => { + test('should be able to clear registered fonts', () => { Font.register({ family: 'MyFont', src: 'src' }); - Font.register({ family: 'MyOtherFont', src: 'src' }); - expect(Font.getRegisteredFonts()).toEqual(['MyFont', 'MyOtherFont']); + expect(Font.getRegisteredFontFamilies()).toEqual(['MyFont']); + + Font.clear(); + + expect(Font.getRegisteredFontFamilies()).toEqual([]); }); - test('should be able to clear registered fonts', () => { + test('should show warning when old register API used', () => { + fetch.once(localFont); + + Font.register(oswaldUrl, { family: 'Oswald' }); + + expect(warning.mock.calls).toHaveLength(1); + }); + + test('should be able to register one font family', () => { Font.register({ family: 'MyFont', src: 'src' }); - expect(Font.getRegisteredFonts()).toEqual(['MyFont']); + expect(Font.getRegisteredFontFamilies()).toEqual(['MyFont']); + }); - Font.clear(); + test('should be able to register many font families', () => { + Font.register({ family: 'MyFont', src: 'src' }); + Font.register({ family: 'MyOtherFont', src: 'src' }); - expect(Font.getRegisteredFonts()).toEqual([]); + expect(Font.getRegisteredFontFamilies()).toEqual(['MyFont', 'MyOtherFont']); }); - test.skip('should show warning when old register API used', async () => { - fetch.once(localFont); + test('should be able to register many sources of one font family individually', () => { + Font.register({ family: 'MyFont', src: 'src' }); + Font.register({ family: 'MyFont', src: 'src', fontStyle: 'italic' }); + Font.register({ + family: 'MyFont', + src: 'src', + fontStyle: 'italic', + fontWeight: 700, + }); - const descriptor = { fontFamily: 'Oswald' }; + expect(Font.getRegisteredFontFamilies()).toEqual(['MyFont']); - Font.register({ family: 'Oswald', src: oswaldUrl }); - await Font.load(descriptor, dummyRoot.instance); + const fontInstance = Font.getRegisteredFonts()['MyFont']; - const font = Font.getFont(descriptor); + expect(fontInstance.sources).toHaveLength(3); + expect(fontInstance.sources[0]).toHaveProperty('fontStyle', 'normal'); + expect(fontInstance.sources[0]).toHaveProperty('fontWeight', 400); + expect(fontInstance.sources[1]).toHaveProperty('fontStyle', 'italic'); + expect(fontInstance.sources[1]).toHaveProperty('fontWeight', 400); + expect(fontInstance.sources[2]).toHaveProperty('fontStyle', 'italic'); + expect(fontInstance.sources[2]).toHaveProperty('fontWeight', 700); + }); - expect(font.loading).toBeFalsy(); - expect(font.data).toBeTruthy(); + test('should be able to register many sources of one font family in bulk', () => { + Font.register({ + family: 'MyFont', + fonts: [ + { src: 'src' }, + { src: 'src', fontStyle: 'italic' }, + { + src: 'src', + fontStyle: 'italic', + fontWeight: 700, + }, + ], + }); + + expect(Font.getRegisteredFontFamilies()).toEqual(['MyFont']); + + const fontInstance = Font.getRegisteredFonts()['MyFont']; + + expect(fontInstance.sources).toHaveLength(3); + expect(fontInstance.sources[0]).toHaveProperty('fontStyle', 'normal'); + expect(fontInstance.sources[0]).toHaveProperty('fontWeight', 400); + expect(fontInstance.sources[1]).toHaveProperty('fontStyle', 'italic'); + expect(fontInstance.sources[1]).toHaveProperty('fontWeight', 400); + expect(fontInstance.sources[2]).toHaveProperty('fontStyle', 'italic'); + expect(fontInstance.sources[2]).toHaveProperty('fontWeight', 700); }); test('should be able to load font from url', async () => { @@ -123,6 +177,148 @@ describe('Font', () => { expect(font.data).toBeTruthy(); }); + test('should throw error if missing font style is requested', async () => { + Font.register({ family: 'Roboto', src: `${__dirname}/assets/font.ttf` }); // normal + + await Font.load({ fontFamily: 'Roboto' }, dummyRoot.instance); + + expect(() => + Font.getFont({ fontFamily: 'Roboto', fontStyle: 'italic' }), + ).toThrow(); + }); + + test('should be able to load requested font style source', async () => { + Font.register({ + family: 'Roboto', + src: `${__dirname}/assets/font.ttf`, + fontStyle: 'italic', + }); + + const descriptor = { fontFamily: 'Roboto', fontStyle: 'italic' }; + + await Font.load(descriptor, dummyRoot.instance); + + const font = Font.getFont(descriptor); + + expect(font.data).toBeTruthy(); + expect(font.loading).toBeFalsy(); + expect(font.fontStyle).toEqual('italic'); + }); + + test('should correctly resolve exact font weight if present', async () => { + const src = `${__dirname}/assets/font.ttf`; + + Font.register({ + src, + family: 'Roboto', + fontWeight: 600, + }); + + const font = Font.getFont({ fontFamily: 'Roboto', fontWeight: 600 }); + + expect(font.fontWeight).toEqual(600); + }); + + test('should correctly resolve font between target and 500 when target between 400 and 500', async () => { + const src = `${__dirname}/assets/font.ttf`; + + Font.register({ + family: 'Roboto', + fonts: [{ src, fontWeight: 430 }, { src, fontWeight: 470 }], + }); + + expect( + Font.getFont({ fontFamily: 'Roboto', fontWeight: 420 }).fontWeight, + ).toEqual(430); + expect( + Font.getFont({ fontFamily: 'Roboto', fontWeight: 450 }).fontWeight, + ).toEqual(470); + }); + + test('should correctly resolve font less than target when target between 400 and 500', async () => { + const src = `${__dirname}/assets/font.ttf`; + + Font.register({ + family: 'Roboto', + fonts: [{ src, fontWeight: 300 }, { src, fontWeight: 600 }], + }); + + expect( + Font.getFont({ fontFamily: 'Roboto', fontWeight: 420 }).fontWeight, + ).toEqual(300); + expect( + Font.getFont({ fontFamily: 'Roboto', fontWeight: 450 }).fontWeight, + ).toEqual(300); + }); + + test('should correctly resolve font greater than target when target between 400 and 500', async () => { + const src = `${__dirname}/assets/font.ttf`; + + Font.register({ + family: 'Roboto', + fonts: [{ src, fontWeight: 600 }], + }); + + expect( + Font.getFont({ fontFamily: 'Roboto', fontWeight: 420 }).fontWeight, + ).toEqual(600); + expect( + Font.getFont({ fontFamily: 'Roboto', fontWeight: 450 }).fontWeight, + ).toEqual(600); + }); + + test('should correctly resolve font less than target when target below 400', async () => { + const src = `${__dirname}/assets/font.ttf`; + + Font.register({ + family: 'Roboto', + fonts: [{ src, fontWeight: 100 }, { src, fontWeight: 200 }], + }); + + expect( + Font.getFont({ fontFamily: 'Roboto', fontWeight: 300 }).fontWeight, + ).toEqual(200); + }); + + test('should correctly resolve font greater than target when target below 400', async () => { + const src = `${__dirname}/assets/font.ttf`; + + Font.register({ + family: 'Roboto', + fonts: [{ src, fontWeight: 600 }, { src, fontWeight: 700 }], + }); + + expect( + Font.getFont({ fontFamily: 'Roboto', fontWeight: 300 }).fontWeight, + ).toEqual(600); + }); + + test('should correctly resolve font greater than target when target above 500', async () => { + const src = `${__dirname}/assets/font.ttf`; + + Font.register({ + family: 'Roboto', + fonts: [{ src, fontWeight: 600 }, { src, fontWeight: 700 }], + }); + + expect( + Font.getFont({ fontFamily: 'Roboto', fontWeight: 550 }).fontWeight, + ).toEqual(600); + }); + + test('should correctly resolve font less than target when target above 500', async () => { + const src = `${__dirname}/assets/font.ttf`; + + Font.register({ + family: 'Roboto', + fonts: [{ src, fontWeight: 200 }, { src, fontWeight: 300 }], + }); + + expect( + Font.getFont({ fontFamily: 'Roboto', fontWeight: 550 }).fontWeight, + ).toEqual(300); + }); + test('should get undefined hyphenation callback if not registered', () => { expect(Font.getHyphenationCallback()).toBe(undefined); }); diff --git a/tests/styleShorthands.test.js b/tests/styleShorthands.test.js index df36dea5f..33f2c2d1f 100644 --- a/tests/styleShorthands.test.js +++ b/tests/styleShorthands.test.js @@ -159,4 +159,68 @@ describe('shorthands', () => { expect(stylesheet.transformOriginX).toBe('50%'); expect(stylesheet.transformOriginY).toBe('50%'); }); + + test('should process font-weight thin shorthand', () => { + const stylesheet1 = StyleSheet.resolve({ fontWeight: 'thin' }); + const stylesheet2 = StyleSheet.resolve({ fontWeight: 'hairline' }); + + expect(stylesheet1.fontWeight).toBe(100); + expect(stylesheet2.fontWeight).toBe(100); + }); + + test('should process font-weight ultralight shorthand', () => { + const stylesheet1 = StyleSheet.resolve({ fontWeight: 'ultralight' }); + const stylesheet2 = StyleSheet.resolve({ fontWeight: 'extralight' }); + + expect(stylesheet1.fontWeight).toBe(200); + expect(stylesheet2.fontWeight).toBe(200); + }); + + test('should process font-weight light shorthand', () => { + const stylesheet = StyleSheet.resolve({ fontWeight: 'light' }); + + expect(stylesheet.fontWeight).toBe(300); + }); + + test('should process font-weight normal shorthand', () => { + const stylesheet = StyleSheet.resolve({ fontWeight: 'normal' }); + + expect(stylesheet.fontWeight).toBe(400); + }); + + test('should process font-weight medium shorthand', () => { + const stylesheet = StyleSheet.resolve({ fontWeight: 'medium' }); + + expect(stylesheet.fontWeight).toBe(500); + }); + + test('should process font-weight semibold shorthand', () => { + const stylesheet1 = StyleSheet.resolve({ fontWeight: 'semibold' }); + const stylesheet2 = StyleSheet.resolve({ fontWeight: 'demibold' }); + + expect(stylesheet1.fontWeight).toBe(600); + expect(stylesheet2.fontWeight).toBe(600); + }); + + test('should process font-weight bold shorthand', () => { + const stylesheet = StyleSheet.resolve({ fontWeight: 'bold' }); + + expect(stylesheet.fontWeight).toBe(700); + }); + + test('should process font-weight ultrabold shorthand', () => { + const stylesheet1 = StyleSheet.resolve({ fontWeight: 'ultrabold' }); + const stylesheet2 = StyleSheet.resolve({ fontWeight: 'extraBold' }); + + expect(stylesheet1.fontWeight).toBe(800); + expect(stylesheet2.fontWeight).toBe(800); + }); + + test('should process font-weight heavy shorthand', () => { + const stylesheet1 = StyleSheet.resolve({ fontWeight: 'heavy' }); + const stylesheet2 = StyleSheet.resolve({ fontWeight: 'black' }); + + expect(stylesheet1.fontWeight).toBe(900); + expect(stylesheet2.fontWeight).toBe(900); + }); });