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;