diff --git a/packages/font/src/font.js b/packages/font/src/font.js index ff5dd14ae..563dad5f9 100644 --- a/packages/font/src/font.js +++ b/packages/font/src/font.js @@ -21,6 +21,8 @@ const FONT_WEIGHTS = { black: 900, }; +const ALL_CHARS_REGEX = /./u; + const fetchFont = async (src, options) => { const response = await fetch(src, options); const data = await response.arrayBuffer(); @@ -43,11 +45,15 @@ const resolveFontWeight = value => { const sortByFontWeight = (a, b) => a.fontWeight - b.fontWeight; 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 = fontWeight || 400; + this.unicodeRange = new RegExp( + unicodeRange instanceof RegExp ? unicodeRange : ALL_CHARS_REGEX, + 'gu', + ); this.data = null; this.options = options; @@ -92,11 +98,18 @@ class Font { this.sources = []; } - register({ src, fontWeight, fontStyle, ...options }) { + register({ src, fontWeight, fontStyle, unicodeRange, ...options }) { const numericFontWeight = resolveFontWeight(fontWeight); this.sources.push( - new FontSource(src, this.family, fontStyle, numericFontWeight, options), + new FontSource( + src, + this.family, + fontStyle, + numericFontWeight, + unicodeRange, + options, + ), ); } diff --git a/packages/font/src/index.js b/packages/font/src/index.js index 09a818121..de3c4665d 100644 --- a/packages/font/src/index.js +++ b/packages/font/src/index.js @@ -1,4 +1,4 @@ -import font from './font'; +import Font from './font'; import standard from './standard'; function FontStore() { @@ -12,13 +12,15 @@ function FontStore() { const { family } = data; if (!fonts[family]) { - fonts[family] = font.create(family); + fonts[family] = Font.create(family); } - // Bulk loading if (data.fonts) { for (let i = 0; i < data.fonts.length; i += 1) { - fonts[family].register({ family, ...data.fonts[i] }); + const defaultValues = { ...data }; + delete defaultValues.fonts; + + fonts[family].register({ ...defaultValues, ...data.fonts[i] }); } } else { fonts[family].register(data); @@ -48,16 +50,31 @@ function FontStore() { return fonts[fontFamily].resolve(descriptor); }; - this.load = async descriptor => { + this.load = async (descriptor, text = '') => { const { fontFamily } = descriptor; - const isStandard = standard.includes(fontFamily); + const fontFamilies = + typeof fontFamily === 'string' + ? fontFamily.split(',').map(family => family.trim()) + : [...(fontFamily || [])]; + + const promises = []; + + for (let len = fontFamilies.length, i = 0; i < len; i += 1) { + const family = fontFamilies[i]; - if (isStandard) return; + const isStandard = standard.includes(family); + if (isStandard) return; - const f = this.getFont(descriptor); + const font = this.getFont({ ...descriptor, fontFamily: family }); + + const didMatch = !font.unicodeRange || font.unicodeRange.test(text); + + if (didMatch) { + promises.push(font.load()); + } + } - // We cache the font to avoid fetching it many times - await f.load(); + await Promise.all(promises); }; this.reset = () => { diff --git a/packages/font/tests/font.test.js b/packages/font/tests/font.test.js new file mode 100644 index 000000000..1d093f824 --- /dev/null +++ b/packages/font/tests/font.test.js @@ -0,0 +1,137 @@ +import Font from '../src/font'; + +// should load the font only once (no race condition) +// should not load loading font +// should not load loaded font + +describe('Font', () => { + test('Should register font', async () => { + const font = Font.create('MyFont1'); + + font.register({ + family: 'MyFont1', + src: '', + fontWeight: 'normal', + unicodeRange: /\u0048/gu, + }); + + font.register({ + family: 'MyFont1', + src: '', + fontWeight: 'bold', + unicodeRange: /\u0048/gu, + }); + + expect(font.sources).toHaveLength(2); + expect(font.sources[0].fontFamily).toBe('MyFont1'); + expect(font.sources[0].src).toBe(''); + expect(font.sources[0].fontWeight).toBe(400); + expect(font.sources[0].unicodeRange.source).toBe(/\u0048/gu.source); + + expect(font.sources).toHaveLength(2); + expect(font.sources[1].fontFamily).toBe('MyFont1'); + expect(font.sources[1].src).toBe(''); + expect(font.sources[1].fontWeight).toBe(700); + expect(font.sources[1].unicodeRange.source).toBe(/\u0048/gu.source); + }); + + test('Should resolve font to closest weight', async () => { + const font = Font.create('MyFont1'); + + font.register({ + family: 'MyFont1', + src: '', + fontWeight: 'normal', + unicodeRange: /\u0048/gu, + }); + + font.register({ + family: 'MyFont1', + src: '', + fontWeight: 'bold', + unicodeRange: /\u0048/gu, + }); + font.register({ + family: 'MyFont1', + src: '', + fontWeight: 'thin', + unicodeRange: /\u0048/gu, + }); + + expect(font.resolve({ fontWeight: 700 })).toBe(font.sources[1]); + expect(font.resolve({ fontWeight: 400 })).toBe(font.sources[0]); + + expect(font.resolve({ fontWeight: 100 })).toBe(font.sources[2]); + expect(font.resolve({ fontWeight: 300 })).toBe(font.sources[2]); + expect(font.resolve({ fontWeight: 200 })).toBe(font.sources[2]); + }); +}); + +const mockLoadFont = jest.fn(async function() { + this.data = {}; +}); +describe('FontSource', () => { + beforeEach(() => { + mockLoadFont.mockClear(); + }); + + test('Should only try to load font once', async () => { + const font = Font.create('MyFont1'); + + font.register({ + family: 'MyFont1', + src: '', + fontWeight: 'normal', + unicodeRange: /\u0048/gu, + }); + + const fontResolved = font.resolve({ fontWeight: 400 }); + fontResolved.loadFont = mockLoadFont; + + await Promise.all([ + fontResolved.load(), + fontResolved.load(), + fontResolved.load(), + fontResolved.load(), + fontResolved.load(), + ]); + expect(fontResolved.loadFont).toBeCalledTimes(1); + }); + + test('All calls should await font to be loaded', async () => { + const font = Font.create('MyFont1'); + + font.register({ + family: 'MyFont1', + src: '', + fontWeight: 'normal', + unicodeRange: /\u0048/gu, + }); + + const fontResolved = font.resolve({ fontWeight: 400 }); + fontResolved.loadFont = mockLoadFont; + const promise1 = fontResolved.load(); + const promise2 = fontResolved.load(); + + expect(promise1).toStrictEqual(promise2); + await expect(promise1).resolves.toBe(await promise2); + }); + + test('Should not load already loaded font', async () => { + const font = Font.create('MyFont1'); + + font.register({ + family: 'MyFont1', + src: '', + fontWeight: 'normal', + unicodeRange: /\u0048/gu, + }); + + const fontResolved = font.resolve({ fontWeight: 400 }); + fontResolved.loadFont = mockLoadFont; + await fontResolved.load(); + await fontResolved.load(); + + expect(fontResolved.loadFont).toBeCalledTimes(1); + }); +}); diff --git a/packages/font/tests/index.test.js b/packages/font/tests/index.test.js new file mode 100644 index 000000000..ff13df67e --- /dev/null +++ b/packages/font/tests/index.test.js @@ -0,0 +1,324 @@ +import FontStore from '../src'; +import Font from '../src/font'; + +const mockRegister = jest.fn(); +const mockResolve = jest.fn(); + +jest.mock('../src/font', () => ({ + create: jest.fn(() => { + return { + register: mockRegister, + resolve: mockResolve, + }; + }), +})); + +describe('font store', () => { + beforeEach(() => { + Font.create.mockClear(); + mockRegister.mockClear(); + mockResolve.mockClear(); + }); + test('Should register a single font', () => { + const fontStore = new FontStore(); + const font = { + family: 'Arial', + src: '', + fontWeight: 'normal', + unicodeRange: /[\u0600-\u06FF]/gu, + }; + fontStore.register(font); + + expect(Font.create).toBeCalledTimes(1); + expect(mockRegister).toBeCalledTimes(1); + + expect(mockRegister).toBeCalledWith(font); + + expect(fontStore.getRegisteredFontFamilies()).toEqual(['Arial']); + }); + + test('Should register multiple font families', () => { + const fontStore = new FontStore(); + const myFont1 = { + family: 'MyFont1', + src: '', + fontWeight: 'Bold', + unicodeRange: /[\u0600-\u06FF]/gu, + }; + const myFont2 = { + family: 'MyFont2', + src: '', + fontWeight: 'normal', + unicodeRange: /[\u0600-\u06FF]/gu, + }; + fontStore.register(myFont1); + fontStore.register(myFont2); + + expect(Font.create).toBeCalledTimes(2); + expect(mockRegister).toBeCalledTimes(2); + + expect(mockRegister).toBeCalledWith(myFont1); + expect(mockRegister).toBeCalledWith(myFont2); + + expect(fontStore.getRegisteredFontFamilies()).toEqual([ + 'MyFont1', + 'MyFont2', + ]); + }); + + test('Should register a multiple fonts', () => { + expect(Font.create).toBeCalledTimes(0); + const fontStore = new FontStore(); + const fontFamily = { + family: 'Arial', + unicodeRange: /[\u0600-\u06FF]/gu, + fonts: [ + { src: '', fontWeight: 'normal' }, + { src: '', fontWeight: 'bold' }, + ], + }; + fontStore.register(fontFamily); + + expect(Font.create).toBeCalledTimes(1); + expect(mockRegister).toBeCalledTimes(fontFamily.fonts.length); + + const defaultValues = { ...fontFamily }; + delete defaultValues.fonts; + + expect(mockRegister).toBeCalledWith({ + ...defaultValues, + ...fontFamily.fonts[0], + }); + expect(mockRegister).toBeCalledWith({ + ...defaultValues, + ...fontFamily.fonts[1], + }); + }); + + test('Should register override family properties by font properties', () => { + expect(Font.create).toBeCalledTimes(0); + const fontStore = new FontStore(); + const fontFamily = { + family: 'Arial', + unicodeRange: /[\u0600-\u06FF]/gu, + fonts: [ + { src: '', fontWeight: 'normal', unicodeRange: /[\u0600-\u06DD]/gu }, + { src: '', fontWeight: 'bold', unicodeRange: /[\u0600-\u06CC]/gu }, + ], + }; + fontStore.register(fontFamily); + + expect(Font.create).toBeCalledTimes(1); + expect(mockRegister).toBeCalledTimes(fontFamily.fonts.length); + + const defaultValues = { ...fontFamily }; + delete defaultValues.fonts; + + expect(mockRegister).toBeCalledWith({ + ...defaultValues, + ...fontFamily.fonts[0], + }); + expect(mockRegister).toBeCalledWith({ + ...defaultValues, + ...fontFamily.fonts[1], + }); + }); + + test('Should clear fonts', () => { + const fontStore = new FontStore(); + const font = { + family: 'Arial', + src: '', + fontWeight: 'normal', + unicodeRange: /[\u0600-\u06FF]/gu, + }; + fontStore.register(font); + + expect(fontStore.getRegisteredFontFamilies()).toEqual(['Arial']); + fontStore.clear(); + expect(fontStore.getRegisteredFontFamilies()).toEqual([]); + }); + + test('should resolve the font', () => { + const fontStore = new FontStore(); + const fontFamily = { + family: 'Arial', + fontFamily: 'Arial', + src: '', + fontWeight: 'normal', + unicodeRange: /[\u0600-\u06FF]/gu, + }; + fontStore.register(fontFamily); + + fontStore.getFont(fontFamily); + expect(mockResolve).toBeCalledTimes(1); + expect(mockResolve).toBeCalledWith(fontFamily); + }); + + test('should load the font', async () => { + const fontStore = new FontStore(); + + fontStore.getFont = jest.fn(() => ({ + unicodeRange: /\u0048/gu, // H + data: null, + loading: false, + load: jest.fn(), + })); + + await fontStore.load( + { + fontFamily: 'MyFont1', + }, + 'H', + ); + expect(fontStore.getFont.mock.results[0].value.load).toBeCalledTimes(1); + + expect(fontStore.getFont).toBeCalledTimes(1); + expect(fontStore.getFont).toBeCalledWith({ + fontFamily: 'MyFont1', + }); + }); + + test('should not load any font', async () => { + const fontStore = new FontStore(); + + fontStore.getFont = jest.fn(() => ({ + unicodeRange: /\u0049/gu, // I + data: null, + loading: false, + load: jest.fn(), + })); + + await fontStore.load( + { + fontFamily: 'MyFont1, MyFont2, MyFont3', + }, + 'ZXC', + ); + expect(fontStore.getFont).toBeCalledTimes(3); + expect(fontStore.getFont).toBeCalledWith({ + fontFamily: 'MyFont1', + }); + expect(fontStore.getFont).toBeCalledWith({ + fontFamily: 'MyFont2', + }); + expect(fontStore.getFont).toBeCalledWith({ + fontFamily: 'MyFont3', + }); + + expect(fontStore.getFont.mock.results[0].value.load).toBeCalledTimes(0); + expect(fontStore.getFont.mock.results[1].value.load).toBeCalledTimes(0); + expect(fontStore.getFont.mock.results[2].value.load).toBeCalledTimes(0); + }); + + test('should load only one font', async () => { + const fontStore = new FontStore(); + fontStore.getFont = jest + .fn() + .mockImplementationOnce(() => ({ + unicodeRange: /\u0049/gu, // I + data: null, + loading: false, + load: jest.fn(), + })) + .mockImplementation(() => ({ + unicodeRange: /\u0048/gu, // H + data: null, + loading: false, + load: jest.fn(), + })); + + await fontStore.load( + { + fontFamily: 'MyFont1', + }, + 'HI', + ); + expect(fontStore.getFont).toBeCalledTimes(1); + expect(fontStore.getFont).toBeCalledWith({ + fontFamily: 'MyFont1', + }); + + expect(fontStore.getFont.mock.results[0].value.load).toBeCalledTimes(1); + }); + + test('should load two fonts', async () => { + const fontStore = new FontStore(); + fontStore.getFont = jest + .fn() + .mockImplementationOnce(() => ({ + unicodeRange: /\u0049/gu, // I + data: null, + loading: false, + load: jest.fn(), + })) + .mockImplementation(() => ({ + unicodeRange: /\u0048/gu, // H + data: null, + loading: false, + load: jest.fn(), + })); + + await fontStore.load( + { + fontFamily: 'MyFont1, MyFont2', + }, + 'HI', + ); + expect(fontStore.getFont).toBeCalledTimes(2); + expect(fontStore.getFont).toBeCalledWith({ + fontFamily: 'MyFont1', + }); + expect(fontStore.getFont).toBeCalledWith({ + fontFamily: 'MyFont2', + }); + + expect(fontStore.getFont.mock.results[0].value.load).toBeCalledTimes(1); + expect(fontStore.getFont.mock.results[1].value.load).toBeCalledTimes(1); + }); + + test('should load only two fonts', async () => { + const fontStore = new FontStore(); + fontStore.getFont = jest + .fn() + .mockImplementationOnce(() => ({ + unicodeRange: /\u0049/gu, // I + data: null, + loading: false, + load: jest.fn(), + })) + .mockImplementationOnce(() => ({ + unicodeRange: /\u0048/gu, // H + data: null, + loading: false, + load: jest.fn(), + })) + .mockImplementationOnce(() => ({ + unicodeRange: /\u0050/gu, // P + data: null, + loading: false, + load: jest.fn(), + })); + + await fontStore.load( + { + fontFamily: 'MyFont1, MyFont2, MyFont3', + }, + 'HIJ', + ); + expect(fontStore.getFont).toBeCalledTimes(3); + expect(fontStore.getFont).toBeCalledWith({ + fontFamily: 'MyFont1', + }); + expect(fontStore.getFont).toBeCalledWith({ + fontFamily: 'MyFont2', + }); + expect(fontStore.getFont).toBeCalledWith({ + fontFamily: 'MyFont3', + }); + + expect(fontStore.getFont.mock.results[0].value.load).toBeCalledTimes(1); + expect(fontStore.getFont.mock.results[1].value.load).toBeCalledTimes(1); + expect(fontStore.getFont.mock.results[2].value.load).toBeCalledTimes(0); + }); +}); diff --git a/packages/layout/src/steps/resolveAssets.js b/packages/layout/src/steps/resolveAssets.js index 89a3b3412..090ad5bb4 100644 --- a/packages/layout/src/steps/resolveAssets.js +++ b/packages/layout/src/steps/resolveAssets.js @@ -13,31 +13,33 @@ const isImage = node => node.type === P.Image; */ const fetchAssets = (fontStore, node) => { const promises = []; - const listToExplore = node.children?.slice(0) || []; + const listToExplore = node.children ? node.children.map(n => [n, {}]) : []; + const emojiSource = fontStore ? fontStore.getEmojiSource() : null; while (listToExplore.length > 0) { - const n = listToExplore.shift(); + const [n, { parentStyle = {} }] = listToExplore.shift(); if (isImage(n)) { promises.push(fetchImage(n)); } - if (fontStore && n.style?.fontFamily) { - promises.push(fontStore.load(n.style)); - } - if (typeof n === 'string') { promises.push(...fetchEmojis(n, emojiSource)); + promises.push(fontStore.load(parentStyle, n)); } if (typeof n.value === 'string') { promises.push(...fetchEmojis(n.value, emojiSource)); + promises.push(fontStore.load(n.style || parentStyle, n.value)); } if (n.children) { n.children.forEach(childNode => { - listToExplore.push(childNode); + listToExplore.push([ + childNode, + { parentStyle: { ...parentStyle, ...n.style } }, + ]); }); } } diff --git a/packages/layout/src/svg/layoutText.js b/packages/layout/src/svg/layoutText.js index aabf49549..49b5ff89f 100644 --- a/packages/layout/src/svg/layoutText.js +++ b/packages/layout/src/svg/layoutText.js @@ -22,6 +22,28 @@ const engines = { fontSubstitution, }; +const toFontNameStack = (...fontFamilyObjects) => + fontFamilyObjects + .map(fontFamilies => + typeof fontFamilies === 'string' + ? fontFamilies.split(',').map(f => f.trim()) + : Array.from(fontFamilies || []), + ) + .flat() + .reduce( + (fonts, font) => (fonts.includes(font) ? fonts : [...fonts, font]), + [], + ); + +const getFontStack = (fontStore, { fontFamily, fontStyle, fontWeight }) => + toFontNameStack(fontFamily).map(fontFamilyName => { + if (typeof fontFamilyName !== 'string') return fontFamilyName; + + const opts = { fontFamily: fontFamilyName, fontWeight, fontStyle }; + const obj = fontStore ? fontStore.getFont(opts) : null; + return obj ? obj.data : fontFamilyName; + }); + const engine = layoutEngine(engines); const getFragments = (fontStore, instance) => { @@ -43,13 +65,14 @@ const getFragments = (fontStore, instance) => { const _textDecoration = instance.props.textDecoration; - const obj = fontStore - ? fontStore.getFont({ fontFamily, fontWeight, fontStyle }) - : null; - const font = obj ? obj.data : fontFamily; + const fontStack = getFontStack(fontStore, { + fontFamily, + fontStyle, + fontWeight, + }); const attributes = { - font, + fontStack, opacity, fontSize, color: fill, diff --git a/packages/layout/src/text/fontSubstitution.js b/packages/layout/src/text/fontSubstitution.js index fb9ef8c74..be7059c53 100644 --- a/packages/layout/src/text/fontSubstitution.js +++ b/packages/layout/src/text/fontSubstitution.js @@ -19,11 +19,24 @@ const getOrCreateFont = name => { const getFallbackFont = () => getOrCreateFont('Helvetica'); -const shouldFallbackToFont = (codePoint, font) => - !font || - (!IGNORED_CODE_POINTS.includes(codePoint) && - !font.hasGlyphForCodePoint(codePoint) && - getFallbackFont().hasGlyphForCodePoint(codePoint)); +const pickFontFromFontStack = (codePoint, fontStack, lastFont) => { + const fontStackWithFallback = [...fontStack, getFallbackFont()]; + if (lastFont) { + fontStackWithFallback.unshift(lastFont); + } + for (let i = 0; i < fontStackWithFallback.length; i += 1) { + const font = fontStackWithFallback[i]; + if ( + !IGNORED_CODE_POINTS.includes(codePoint) && + font && + font.hasGlyphForCodePoint && + font.hasGlyphForCodePoint(codePoint) + ) { + return font; + } + } + return getFallbackFont(); +}; const fontSubstitution = () => ({ string, runs }) => { let lastFont = null; @@ -37,9 +50,9 @@ const fontSubstitution = () => ({ string, runs }) => { const run = runs[i]; const defaultFont = - typeof run.attributes.font === 'string' - ? getOrCreateFont(run.attributes.font) - : run.attributes.font; + run.attributes.fontStack?.[0] === 'string' + ? getOrCreateFont(run.attributes.fontStack[0]) + : run.attributes.fontStack[0]; if (string.length === 0) { res.push({ start: 0, end: 0, attributes: { font: defaultFont } }); @@ -47,13 +60,15 @@ const fontSubstitution = () => ({ string, runs }) => { } const chars = string.slice(run.start, run.end); - for (let j = 0; j < chars.length; j += 1) { const char = chars[j]; const codePoint = char.codePointAt(); - const shouldFallback = shouldFallbackToFont(codePoint, defaultFont); // If the default font does not have a glyph and the fallback font does, we use it - const font = shouldFallback ? getFallbackFont() : defaultFont; + const font = pickFontFromFontStack( + codePoint, + run.attributes.fontStack, + lastFont, + ); const fontSize = getFontSize(run); // If anything that would impact res has changed, update it diff --git a/packages/layout/src/text/getAttributedString.js b/packages/layout/src/text/getAttributedString.js index cce85ba0f..336027a10 100644 --- a/packages/layout/src/text/getAttributedString.js +++ b/packages/layout/src/text/getAttributedString.js @@ -11,6 +11,28 @@ const isImage = node => node.type === P.Image; const isTextInstance = node => node.type === P.TextInstance; +const toFontNameStack = (...fontFamilyObjects) => + fontFamilyObjects + .map(fontFamilies => + typeof fontFamilies === 'string' + ? fontFamilies.split(',').map(f => f.trim()) + : Array.from(fontFamilies || []), + ) + .flat() + .reduce( + (fonts, font) => (fonts.includes(font) ? fonts : [...fonts, font]), + [], + ); + +const getFontStack = (fontStore, { fontFamily, fontStyle, fontWeight }) => + toFontNameStack(fontFamily).map(fontFamilyName => { + if (typeof fontFamilyName !== 'string') return fontFamilyName; + + const opts = { fontFamily: fontFamilyName, fontWeight, fontStyle }; + const obj = fontStore ? fontStore.getFont(opts) : null; + return obj ? obj.data : fontFamilyName; + }); + /** * Get textkit fragments of given node object * @@ -40,15 +62,17 @@ const getFragments = (fontStore, instance, parentLink, level = 0) => { opacity, } = instance.style; - const opts = { fontFamily, fontWeight, fontStyle }; - const obj = fontStore ? fontStore.getFont(opts) : null; - const font = obj ? obj.data : fontFamily; + const fontStack = getFontStack(fontStore, { + fontFamily, + fontStyle, + fontWeight, + }); // Don't pass main background color to textkit. Will be rendered by the render package instead const backgroundColor = level === 0 ? null : instance.style.backgroundColor; const attributes = { - font, + fontStack, color, opacity, fontSize, diff --git a/packages/layout/src/text/ignoreChars.js b/packages/layout/src/text/ignoreChars.js index 5b86ef33c..a43ffa303 100644 --- a/packages/layout/src/text/ignoreChars.js +++ b/packages/layout/src/text/ignoreChars.js @@ -3,17 +3,24 @@ const IGNORABLE_CODEPOINTS = [ 8233, // PARAGRAPH_SEPARATOR ]; -const buildSubsetForFont = font => +const buildSubsetForFontStack = fonts => IGNORABLE_CODEPOINTS.reduce((acc, codePoint) => { - if (font && font.hasGlyphForCodePoint && font.hasGlyphForCodePoint(codePoint)) { - return acc; + for (let i = 0; i < fonts.length; i += 1) { + const font = fonts[i]; + if ( + font && + font.hasGlyphForCodePoint && + font.hasGlyphForCodePoint(codePoint) + ) { + return acc; + } } return [...acc, String.fromCharCode(codePoint)]; }, []); const ignoreChars = fragments => fragments.map(fragment => { - const charSubset = buildSubsetForFont(fragment.attributes.font); + const charSubset = buildSubsetForFontStack(fragment.attributes.fontStack); const subsetRegex = new RegExp(charSubset.join('|')); return { diff --git a/packages/textkit/src/engines/fontSubstitution/index.js b/packages/textkit/src/engines/fontSubstitution/index.js index 1ebd863ae..16bebc99a 100644 --- a/packages/textkit/src/engines/fontSubstitution/index.js +++ b/packages/textkit/src/engines/fontSubstitution/index.js @@ -26,7 +26,7 @@ const fontSubstitution = () => attributedString => { for (const run of runs) { const fontSize = getFontSize(run); - const defaultFont = run.attributes.font; + const defaultFont = run.attributes.fontStack?.[0]; if (string.length === 0) { res.push({ start: 0, end: 0, attributes: { font: defaultFont } }); diff --git a/packages/textkit/src/layout/applyDefaultStyles.js b/packages/textkit/src/layout/applyDefaultStyles.js index d91bca8ce..a3f570305 100644 --- a/packages/textkit/src/layout/applyDefaultStyles.js +++ b/packages/textkit/src/layout/applyDefaultStyles.js @@ -10,7 +10,7 @@ const applyAttributes = a => ({ color: a.color || 'black', features: a.features || [], fill: a.fill !== false, - font: a.font || null, + fontStack: a.fontStack || [], fontSize: a.fontSize || 12, hangingPunctuation: a.hangingPunctuation || false, hyphenationFactor: a.hyphenationFactor || 0, diff --git a/packages/textkit/tests/engines/fontSubstitution.test.js b/packages/textkit/tests/engines/fontSubstitution.test.js index 3bc6507a7..0a8521e13 100644 --- a/packages/textkit/tests/engines/fontSubstitution.test.js +++ b/packages/textkit/tests/engines/fontSubstitution.test.js @@ -20,8 +20,8 @@ describe('FontSubstitution', () => { }); test('should merge consecutive runs with same font', () => { - const run1 = { start: 0, end: 3, attributes: { font: 'Helvetica' } }; - const run2 = { start: 3, end: 5, attributes: { font: 'Helvetica' } }; + const run1 = { start: 0, end: 3, attributes: { fontStack: ['Helvetica'] } }; + const run2 = { start: 3, end: 5, attributes: { fontStack: ['Helvetica'] } }; const string = instance({ string: 'Lorem', runs: [run1, run2] }); expect(string).toHaveProperty('string', 'Lorem'); @@ -31,8 +31,8 @@ describe('FontSubstitution', () => { }); test('should substitute many runs', () => { - const run1 = { start: 0, end: 3, attributes: { font: 'Courier' } }; - const run2 = { start: 3, end: 5, attributes: { font: 'Helvetica' } }; + const run1 = { start: 0, end: 3, attributes: { fontStack: ['Courier'] } }; + const run2 = { start: 3, end: 5, attributes: { fontStack: ['Helvetica'] } }; const string = instance({ string: 'Lorem', runs: [run1, run2] }); expect(string).toHaveProperty('string', 'Lorem'); diff --git a/packages/textkit/tests/layout/applyDefaultStyles.test.js b/packages/textkit/tests/layout/applyDefaultStyles.test.js index 215355184..2520e87ca 100644 --- a/packages/textkit/tests/layout/applyDefaultStyles.test.js +++ b/packages/textkit/tests/layout/applyDefaultStyles.test.js @@ -10,7 +10,7 @@ const DEFAULTS = { color: 'black', features: [], fill: true, - font: null, + fontStack: [], fontSize: 12, hangingPunctuation: false, hyphenationFactor: 0, @@ -44,7 +44,7 @@ const OVERRIDES = { color: 'red', features: ['test'], fill: false, - font: { font: true }, + fontStack: { font: true }, fontSize: 15, hangingPunctuation: true, hyphenationFactor: 5, diff --git a/packages/types/font.d.ts b/packages/types/font.d.ts index ea098c625..2bda73c4e 100644 --- a/packages/types/font.d.ts +++ b/packages/types/font.d.ts @@ -23,6 +23,7 @@ interface FontSource { fontFamily: string; fontStyle: FontStyle; fontWeight: number; + unicodeRange?: RegExp; data: any; loading: boolean; options: any; @@ -54,6 +55,7 @@ interface EmojiSource { interface SingleLoad { family: string; src: string; + unicodeRange: RegExp, fontStyle?: string; fontWeight?: string | number; [key: string]: any; @@ -61,10 +63,12 @@ interface SingleLoad { interface BulkLoad { family: string; + unicodeRange?: RegExp, fonts: { src: string; fontStyle?: string; fontWeight?: string | number; + unicodeRange?: RegExp, [key: string]: any; }[]; }