diff --git a/.changeset/nine-ways-walk.md b/.changeset/nine-ways-walk.md new file mode 100644 index 000000000..d6a3b9a6d --- /dev/null +++ b/.changeset/nine-ways-walk.md @@ -0,0 +1,6 @@ +--- +"@react-pdf/examples": patch +"@react-pdf/layout": patch +--- + +fix: font selection regression diff --git a/packages/examples/src/fontFamilyFallback/index.jsx b/packages/examples/src/fontFamilyFallback/index.jsx index 148bdf034..ce38bddf7 100644 --- a/packages/examples/src/fontFamilyFallback/index.jsx +++ b/packages/examples/src/fontFamilyFallback/index.jsx @@ -5,6 +5,9 @@ import React from 'react'; import { Document, Page, Text, StyleSheet, Font } from '@react-pdf/renderer'; import RobotoFont from '../../public/Roboto-Regular.ttf'; +import RobotoBoldFont from '../../public/Roboto-Bold.ttf'; +import RobotItalicFont from '../../public/Roboto-Italic.ttf'; + import NotoSansArabicFont from '../../public/NotoSansArabic-Regular.ttf'; const styles = StyleSheet.create({ @@ -30,7 +33,18 @@ Font.register({ fonts: [ { src: RobotoFont, - fontWeight: 400, + fontStyle: 'normal', + fontWeight: 'normal', + }, + { + src: RobotItalicFont, + fontStyle: 'italic', + fontWeight: 'normal', + }, + { + src: RobotoBoldFont, + fontStyle: 'normal', + fontWeight: 'bold', }, ], }); @@ -48,19 +62,36 @@ Font.register({ const MyDoc = () => { return ( - + This font is default Courier + The following is partially Roboto and Noto Sans Arabic - + Roboto / امتحان + The following is partially Courier-Bold and Noto Sans Arabic - Courier-Bold / امتحان + + Courier-Bold / امتحان + + + + The following are multiple font families, weights, and styles all on the + same line + + + Roboto Normal{' / '} + Roboto Bold + {' / '} + Roboto Italic + {' / '} + Courier + ); }; diff --git a/packages/layout/src/text/fontSubstitution.js b/packages/layout/src/text/fontSubstitution.js index 9ddc982b6..40b8bafc8 100644 --- a/packages/layout/src/text/fontSubstitution.js +++ b/packages/layout/src/text/fontSubstitution.js @@ -20,10 +20,7 @@ const getOrCreateFont = (name) => { const getFallbackFont = () => getOrCreateFont('Helvetica'); const pickFontFromFontStack = (codePoint, fontStack, lastFont) => { - const fontStackWithFallback = [...fontStack, getFallbackFont()]; - if (lastFont) { - fontStackWithFallback.unshift(lastFont); - } + const fontStackWithFallback = [...fontStack, lastFont, getFallbackFont()]; for (let i = 0; i < fontStackWithFallback.length; i += 1) { const font = fontStackWithFallback[i]; if ( diff --git a/packages/layout/tests/text/fontSubstitution.test.js b/packages/layout/tests/text/fontSubstitution.test.js new file mode 100644 index 000000000..d6cc421df --- /dev/null +++ b/packages/layout/tests/text/fontSubstitution.test.js @@ -0,0 +1,90 @@ +import { describe, expect, test } from 'vitest'; +import fontSubstitution from '../../src/text/fontSubstitution'; + +const instance = fontSubstitution(); + +describe('FontSubstitution', () => { + test('should return empty array if no runs passed', () => { + const string = instance({ string: '', runs: [] }); + + expect(string).toHaveProperty('runs', []); + expect(string).toHaveProperty('string', ''); + }); + + 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 string = instance({ string: 'Lorem', runs: [run1, run2] }); + + expect(string).toHaveProperty('string', 'Lorem'); + expect(string.runs).toHaveLength(1); + expect(string.runs[0]).toHaveProperty('start', 0); + expect(string.runs[0]).toHaveProperty('end', 5); + expect(string.runs[0].attributes.font.name).toBe('Helvetica'); + }); + + test('should substitute many runs', () => { + const run1 = { start: 0, end: 3, attributes: { font: ['Courier'] } }; + const run2 = { start: 3, end: 5, attributes: { font: ['Helvetica'] } }; + const string = instance({ string: 'Lorem', runs: [run1, run2] }); + + expect(string).toHaveProperty('string', 'Lorem'); + expect(string.runs).toHaveLength(2); + expect(string.runs[0]).toHaveProperty('start', 0); + expect(string.runs[0]).toHaveProperty('end', 3); + expect(string.runs[0].attributes.font.name).toBe('Courier'); + expect(string.runs[1]).toHaveProperty('start', 3); + expect(string.runs[1]).toHaveProperty('end', 5); + expect(string.runs[1].attributes.font.name).toBe('Helvetica'); + }); + + describe('Fallback Font', () => { + const SimplifiedChineseFont = { + name: 'SimplifiedChineseFont', + hasGlyphForCodePoint: (codePoint) => codePoint === 20320, + }; + + test('should utilize a fallback font that supports the provided glyph', () => { + const run = { + start: 0, + end: 1, + attributes: { + font: ['Courier', SimplifiedChineseFont], + }, + }; + + const string = instance({ string: '你', runs: [run] }); + + expect(string).toHaveProperty('string', '你'); + expect(string.runs).toHaveLength(1); + expect(string.runs[0]).toHaveProperty('start', 0); + expect(string.runs[0]).toHaveProperty('end', 1); + expect(string.runs[0].attributes.font.name).toBe( + SimplifiedChineseFont.name, + ); + }); + + test('should split a run when fallback font is used on a portion of the run', () => { + const run = { + start: 0, + end: 2, + attributes: { + font: ['Courier', SimplifiedChineseFont], + }, + }; + + const string = instance({ string: 'A你', runs: [run] }); + + expect(string).toHaveProperty('string', 'A你'); + expect(string.runs).toHaveLength(2); + expect(string.runs[0]).toHaveProperty('start', 0); + expect(string.runs[0]).toHaveProperty('end', 1); + expect(string.runs[0].attributes.font.name).toBe('Courier'); + expect(string.runs[1]).toHaveProperty('start', 1); + expect(string.runs[1]).toHaveProperty('end', 2); + expect(string.runs[1].attributes.font.name).toBe( + SimplifiedChineseFont.name, + ); + }); + }); +});