Skip to content

Commit

Permalink
feat: add fallback font support (#2640)
Browse files Browse the repository at this point in the history
  • Loading branch information
nikgraf committed Apr 23, 2024
1 parent 5e9a113 commit 67c265a
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 17 deletions.
7 changes: 7 additions & 0 deletions .changeset/hungry-zoos-sort.md
Original file line number Diff line number Diff line change
@@ -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']
Binary file not shown.
59 changes: 59 additions & 0 deletions packages/examples/src/fontFamilyFallback/index.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<Page style={styles.body}>
<Text style={styles.regular}>Test امتحان</Text>
</Page>
);
};

const App = () => {
return (
<Document>
<MyDoc />
</Document>
);
};

export default App;
2 changes: 1 addition & 1 deletion packages/examples/src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
17 changes: 12 additions & 5 deletions packages/font/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
30 changes: 23 additions & 7 deletions packages/layout/src/text/fontSubstitution.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
() =>
Expand Down Expand Up @@ -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
Expand Down
13 changes: 10 additions & 3 deletions packages/layout/src/text/getAttributedString.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/types/font.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface FontDescriptor {

interface FontSource {
src: string;
fontFamily: string;
fontFamily: string | string[];
fontStyle: FontStyle;
fontWeight: number;
data: any;
Expand Down

0 comments on commit 67c265a

Please sign in to comment.