Skip to content

Commit e27cdb4

Browse files
authored
feat(font): various improvements (#3271)
1 parent 5b6a6a4 commit e27cdb4

9 files changed

Lines changed: 513 additions & 6 deletions

File tree

.changeset/pretty-carrots-obey.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@react-pdf/font": patch
3+
---
4+
5+
feat(font): various improvements

packages/font/README.md

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,311 @@
33
</p>
44

55
# @react-pdf/font
6+
7+
Font registration, loading, and resolution library for react-pdf. Handles TTF, WOFF, and WOFF2 fonts from various sources including local files, remote URLs, and base64 data URIs. Includes built-in support for PDF standard fonts.
8+
9+
## Installation
10+
11+
```bash
12+
yarn add @react-pdf/font
13+
```
14+
15+
## Usage
16+
17+
```js
18+
import FontStore from '@react-pdf/font';
19+
20+
const fontStore = new FontStore();
21+
22+
// Register a custom font
23+
fontStore.register({
24+
family: 'Roboto',
25+
src: 'https://example.com/fonts/Roboto-Regular.ttf',
26+
});
27+
28+
// Load the font
29+
await fontStore.load({
30+
fontFamily: 'Roboto',
31+
fontStyle: 'normal',
32+
fontWeight: 400,
33+
});
34+
```
35+
36+
## Font Sources
37+
38+
The library supports multiple font source types:
39+
40+
### Remote URL
41+
42+
```js
43+
fontStore.register({
44+
family: 'Open Sans',
45+
src: 'https://example.com/fonts/OpenSans-Regular.ttf',
46+
});
47+
```
48+
49+
With custom request options:
50+
51+
```js
52+
fontStore.register({
53+
family: 'Open Sans',
54+
src: 'https://example.com/fonts/OpenSans-Regular.ttf',
55+
method: 'GET',
56+
headers: {
57+
Authorization: 'Bearer token',
58+
},
59+
body: null,
60+
});
61+
```
62+
63+
### Local File (Node.js)
64+
65+
```js
66+
fontStore.register({
67+
family: 'Custom Font',
68+
src: '/path/to/font.ttf',
69+
});
70+
```
71+
72+
> **Note:** Local file resolution is only available in Node.js environments.
73+
74+
### Base64 Data URI
75+
76+
```js
77+
fontStore.register({
78+
family: 'Embedded Font',
79+
src: 'data:font/ttf;base64,AAEAAAALAIAAAwAwT1MvMg...',
80+
});
81+
```
82+
83+
## Registering Font Families
84+
85+
### Single Font
86+
87+
```js
88+
fontStore.register({
89+
family: 'Roboto',
90+
src: 'https://example.com/fonts/Roboto-Regular.ttf',
91+
fontWeight: 400,
92+
fontStyle: 'normal',
93+
});
94+
```
95+
96+
### Multiple Weights and Styles (Bulk Registration)
97+
98+
```js
99+
fontStore.register({
100+
family: 'Roboto',
101+
fonts: [
102+
{ src: 'https://example.com/fonts/Roboto-Regular.ttf', fontWeight: 400 },
103+
{ src: 'https://example.com/fonts/Roboto-Bold.ttf', fontWeight: 700 },
104+
{
105+
src: 'https://example.com/fonts/Roboto-Italic.ttf',
106+
fontWeight: 400,
107+
fontStyle: 'italic',
108+
},
109+
{
110+
src: 'https://example.com/fonts/Roboto-BoldItalic.ttf',
111+
fontWeight: 700,
112+
fontStyle: 'italic',
113+
},
114+
],
115+
});
116+
```
117+
118+
## Standard Fonts
119+
120+
The following PDF standard fonts are pre-registered and available without any additional setup:
121+
122+
- **Helvetica** (with Bold, Oblique, and BoldOblique variants)
123+
- **Courier** (with Bold, Oblique, and BoldOblique variants)
124+
- **Times-Roman** (with Bold, Italic, and BoldItalic variants)
125+
126+
Helvetica variants are pre-loaded by default, so no explicit `load()` call is needed for them.
127+
128+
```js
129+
// Standard fonts are ready to use immediately
130+
const font = fontStore.getFont({
131+
fontFamily: 'Helvetica',
132+
fontWeight: 700,
133+
fontStyle: 'normal',
134+
});
135+
```
136+
137+
## Font Weight Resolution
138+
139+
The library implements CSS font-weight resolution rules. You can specify font weights as numbers or keywords:
140+
141+
| Keyword | Numeric Value |
142+
| -------------------------- | ------------- |
143+
| `thin`, `hairline` | 100 |
144+
| `ultralight`, `extralight` | 200 |
145+
| `light` | 300 |
146+
| `normal` | 400 |
147+
| `medium` | 500 |
148+
| `semibold`, `demibold` | 600 |
149+
| `bold` | 700 |
150+
| `ultrabold`, `extrabold` | 800 |
151+
| `heavy`, `black` | 900 |
152+
153+
When an exact weight match isn't available, the library uses CSS fallback rules to find the closest available weight.
154+
155+
## Emoji Support
156+
157+
Register an emoji source to enable emoji rendering:
158+
159+
```js
160+
// Using a URL pattern (must end with trailing slash)
161+
fontStore.registerEmojiSource({
162+
url: 'https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/72x72/',
163+
format: 'png',
164+
});
165+
166+
// Using a custom builder function
167+
fontStore.registerEmojiSource({
168+
builder: (code) => `https://example.com/emojis/${code}.png`,
169+
});
170+
```
171+
172+
The `code` parameter passed to the builder is a hyphen-separated string of hex code points (e.g., `1f44d` for 👍 or `1f44d-1f3ff` for 👍🏿).
173+
174+
Set `withVariationSelectors: true` if your emoji source requires variation selectors in the code points.
175+
176+
## Hyphenation
177+
178+
Register a hyphenation callback for text wrapping:
179+
180+
```js
181+
import hyphenationCallback from 'hyphen/en';
182+
183+
fontStore.registerHyphenationCallback(hyphenationCallback);
184+
```
185+
186+
## API Reference
187+
188+
### FontStore
189+
190+
#### `register(data: SingleLoad | BulkLoad)`
191+
192+
Register a font or font family.
193+
194+
#### `load(descriptor: FontDescriptor): Promise<void>`
195+
196+
Load a specific font variant.
197+
198+
#### `getFont(descriptor: FontDescriptor): FontSource`
199+
200+
Get a font source matching the descriptor.
201+
202+
#### `registerEmojiSource(source: EmojiSource)`
203+
204+
Register an emoji image source.
205+
206+
#### `registerHyphenationCallback(callback: HyphenationCallback)`
207+
208+
Register a hyphenation callback function.
209+
210+
#### `reset()`
211+
212+
Reset all loaded font data (keeps registrations).
213+
214+
#### `clear()`
215+
216+
Clear all font registrations, emoji source, and hyphenation callback.
217+
218+
#### `getRegisteredFontFamilies(): string[]`
219+
220+
Get list of registered font family names.
221+
222+
#### `getRegisteredFonts(): Record<string, FontFamily>`
223+
224+
Get all registered font families with their sources.
225+
226+
#### `getEmojiSource(): EmojiSource | null`
227+
228+
Get the registered emoji source, if any.
229+
230+
#### `getHyphenationCallback(): HyphenationCallback | null`
231+
232+
Get the registered hyphenation callback, if any.
233+
234+
## Types
235+
236+
### FontDescriptor
237+
238+
```ts
239+
type FontDescriptor = {
240+
fontFamily: string;
241+
fontStyle?: 'normal' | 'italic' | 'oblique';
242+
fontWeight?: FontWeight;
243+
};
244+
```
245+
246+
### FontWeight
247+
248+
```ts
249+
type FontWeight =
250+
| number
251+
| 'thin'
252+
| 'hairline'
253+
| 'ultralight'
254+
| 'extralight'
255+
| 'light'
256+
| 'normal'
257+
| 'medium'
258+
| 'semibold'
259+
| 'demibold'
260+
| 'bold'
261+
| 'ultrabold'
262+
| 'extrabold'
263+
| 'heavy'
264+
| 'black';
265+
```
266+
267+
### SingleLoad
268+
269+
```ts
270+
type SingleLoad = {
271+
family: string;
272+
src: string;
273+
fontStyle?: 'normal' | 'italic' | 'oblique';
274+
fontWeight?: FontWeight;
275+
postscriptName?: string;
276+
method?: 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
277+
headers?: Record<string, string>;
278+
body?: any;
279+
};
280+
```
281+
282+
### BulkLoad
283+
284+
```ts
285+
type BulkLoad = {
286+
family: string;
287+
fonts: FontSource[];
288+
};
289+
```
290+
291+
### EmojiSource
292+
293+
```ts
294+
type EmojiSource =
295+
| { url: string; format?: string; withVariationSelectors?: boolean }
296+
| { builder: (code: string) => string; withVariationSelectors?: boolean };
297+
```
298+
299+
### HyphenationCallback
300+
301+
```ts
302+
type HyphenationCallback = (word: string) => string[];
303+
```
304+
305+
## Supported Font Formats
306+
307+
- **TTF** - TrueType Font
308+
- **WOFF** - Web Open Font Format
309+
- **WOFF2** - Web Open Font Format 2.0
310+
311+
## License
312+
313+
MIT

packages/font/src/font-family.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ class FontFamily {
7575
const rightOffset = styleSources.filter((s) => s.fontWeight > 500);
7676

7777
const fit = styleSources.filter(
78-
(s) => s.fontWeight >= numericFontWeight && s.fontWeight < 500,
78+
(s) => s.fontWeight >= numericFontWeight && s.fontWeight <= 500,
7979
);
8080

8181
font = fit[0] || leftOffset[leftOffset.length - 1] || rightOffset[0];

packages/font/src/font-source.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,25 @@ import StandardFont, { STANDARD_FONTS } from './standard-font';
66

77
const fetchFont = async (src: string, options: RemoteOptions) => {
88
const response = await fetch(src, options);
9+
10+
if (!response.ok) {
11+
throw new Error(
12+
`Failed to fetch font from ${src}: ${response.status} ${response.statusText}`,
13+
);
14+
}
15+
916
const data = await response.arrayBuffer();
1017

1118
return new Uint8Array(data);
1219
};
1320

1421
const isDataUrl = (dataUrl: string) => {
15-
const header = dataUrl.split(',')[0];
16-
const hasDataPrefix = header.substring(0, 5) === 'data:';
17-
const hasBase64Prefix = header.split(';')[1] === 'base64';
22+
const commaIndex = dataUrl.indexOf(',');
23+
if (commaIndex === -1) return false;
24+
25+
const header = dataUrl.substring(0, commaIndex);
26+
const hasDataPrefix = header.startsWith('data:');
27+
const hasBase64Prefix = header.includes(';base64');
1828

1929
return hasDataPrefix && hasBase64Prefix;
2030
};

packages/font/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ class FontStore {
188188

189189
clear = () => {
190190
this.fontFamilies = {};
191+
this.emojiSource = null;
192+
this.hyphenationCallback = null;
191193
};
192194

193195
getRegisteredFonts = () => this.fontFamilies;

packages/font/src/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,19 @@ export type FontStyle = 'normal' | 'italic' | 'oblique';
1010
export type FontWeight =
1111
| number
1212
| 'thin'
13+
| 'hairline'
1314
| 'ultralight'
15+
| 'extralight'
1416
| 'light'
1517
| 'normal'
1618
| 'medium'
1719
| 'semibold'
20+
| 'demibold'
1821
| 'bold'
1922
| 'ultrabold'
20-
| 'heavy';
23+
| 'extrabold'
24+
| 'heavy'
25+
| 'black';
2126

2227
export type FontDescriptor = {
2328
fontFamily: string;

0 commit comments

Comments
 (0)