diff --git a/.changeset/hungry-zoos-sort.md b/.changeset/hungry-zoos-sort.md new file mode 100644 index 000000000..a5e264321 --- /dev/null +++ b/.changeset/hungry-zoos-sort.md @@ -0,0 +1,7 @@ +--- +"@react-pdf/examples": minor +"@react-pdf/layout": minor +"@react-pdf/font": minor +--- + +Add support for fontFamily fallbacks e.g. fontFamily: ['Roboto', 'NotoSansArabic'] diff --git a/packages/examples/public/NotoSansArabic-Regular.ttf b/packages/examples/public/NotoSansArabic-Regular.ttf new file mode 100644 index 000000000..79359c460 Binary files /dev/null and b/packages/examples/public/NotoSansArabic-Regular.ttf differ diff --git a/packages/examples/src/fontFamilyFallback/index.jsx b/packages/examples/src/fontFamilyFallback/index.jsx new file mode 100644 index 000000000..bd249543a --- /dev/null +++ b/packages/examples/src/fontFamilyFallback/index.jsx @@ -0,0 +1,59 @@ +/* eslint react/prop-types: 0 */ +/* eslint react/jsx-sort-props: 0 */ + +import React from 'react'; +import { Document, Page, Text, StyleSheet, Font } from '@react-pdf/renderer'; + +import RobotoFont from '../../public/Roboto-Regular.ttf'; +import NotoSansArabicFont from '../../public/NotoSansArabic-Regular.ttf'; + +const styles = StyleSheet.create({ + body: { + paddingTop: 35, + paddingBottom: 45, + paddingHorizontal: 35, + position: 'relative', + }, + regular: { + fontFamily: ['Roboto', 'NotoSansArabic'], + fontWeight: 900, + }, +}); + +Font.register({ + family: 'Roboto', + fonts: [ + { + src: RobotoFont, + fontWeight: 400, + }, + ], +}); + +Font.register({ + family: 'NotoSansArabic', + fonts: [ + { + src: NotoSansArabicFont, + fontWeight: 400, + }, + ], +}); + +const MyDoc = () => { + return ( + + Test امتحان + + ); +}; + +const App = () => { + return ( + + + + ); +}; + +export default App; diff --git a/packages/examples/src/index.jsx b/packages/examples/src/index.jsx index f0a22de79..23d6c6b82 100644 --- a/packages/examples/src/index.jsx +++ b/packages/examples/src/index.jsx @@ -3,7 +3,7 @@ import { createRoot } from 'react-dom/client'; import { PDFViewer } from '@react-pdf/renderer'; -import Document from './pageWrap'; +import Document from './fontFamilyFallback'; import './index.css'; diff --git a/packages/font/src/index.js b/packages/font/src/index.js index a7dccf92c..bb48c5762 100644 --- a/packages/font/src/index.js +++ b/packages/font/src/index.js @@ -55,14 +55,21 @@ function FontStore() { this.load = async (descriptor) => { const { fontFamily } = descriptor; - const isStandard = standard.includes(fontFamily); + const fontFamilies = + typeof fontFamily === 'string' ? [fontFamily] : [...(fontFamily || [])]; + + const promises = []; - if (isStandard) return; + for (let len = fontFamilies.length, i = 0; i < len; i += 1) { + const family = fontFamilies[i]; + const isStandard = standard.includes(family); + if (isStandard) return; - const f = this.getFont(descriptor); + const f = this.getFont({ ...descriptor, fontFamily: family }); + promises.push(f.load()); + } - // We cache the font to avoid fetching it many times - await f.load(); + await Promise.all(promises); }; this.reset = () => { diff --git a/packages/layout/src/text/fontSubstitution.js b/packages/layout/src/text/fontSubstitution.js index 58a4f9825..967cc9985 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 = () => @@ -53,9 +66,12 @@ const fontSubstitution = 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.font, + 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 1a7625553..74cbc326f 100644 --- a/packages/layout/src/text/getAttributedString.js +++ b/packages/layout/src/text/getAttributedString.js @@ -44,9 +44,16 @@ const getFragments = (fontStore, instance, parentLink, level = 0) => { verticalAlign, } = instance.style; - const opts = { fontFamily, fontWeight, fontStyle }; - const obj = fontStore ? fontStore.getFont(opts) : null; - const font = obj ? obj.data : fontFamily; + const fontFamilies = + typeof fontFamily === 'string' ? [fontFamily] : [...(fontFamily || [])]; + + const font = fontFamilies.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; + }); // Don't pass main background color to textkit. Will be rendered by the render package instead const backgroundColor = level === 0 ? null : instance.style.backgroundColor; diff --git a/packages/types/font.d.ts b/packages/types/font.d.ts index e04d1ccb8..3e6b03731 100644 --- a/packages/types/font.d.ts +++ b/packages/types/font.d.ts @@ -20,7 +20,7 @@ export interface FontDescriptor { interface FontSource { src: string; - fontFamily: string; + fontFamily: string | string[]; fontStyle: FontStyle; fontWeight: number; data: any;