Skip to content

Commit 24690f5

Browse files
authored
feat: handle standard fonts in fonts package (#3102)
1 parent c7b7496 commit 24690f5

27 files changed

+1107
-317
lines changed

.changeset/shiny-wolves-deny.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@react-pdf/font": major
3+
"@react-pdf/textkit": minor
4+
"@react-pdf/layout": minor
5+
"@react-pdf/render": minor
6+
---
7+
8+
feat: handle standard fonts in fonts package

packages/examples/vite/src/examples/page-wrap/index.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const styles = StyleSheet.create({
2525
title: {
2626
fontSize: 24,
2727
textAlign: 'center',
28-
fontFamily: 'Oswald',
28+
fontFamily: ['Oswald', 'Helvetica'],
2929
},
3030
author: {
3131
fontSize: 12,

packages/font/declarations.d.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { Font, FontCollection } from 'fontkit';
2+
3+
declare module 'fontkit' {
4+
export function create(
5+
buffer: Uint8Array | Buffer,
6+
postscriptName?: string,
7+
): Font | FontCollection;
8+
}

packages/font/globals.d.ts

+2-9
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
1-
import type { Font, FontCollection } from 'fontkit';
2-
3-
declare module 'fontkit' {
4-
export function create(
5-
buffer: Uint8Array | Buffer,
6-
postscriptName?: string,
7-
): Font | FontCollection;
8-
}
9-
101
declare global {
112
const BROWSER: boolean;
123
}
4+
5+
export {};

packages/font/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"typecheck": "tsc --noEmit"
2424
},
2525
"dependencies": {
26+
"@react-pdf/pdfkit": "^4.0.2",
2627
"@react-pdf/types": "^2.8.0",
2728
"fontkit": "^2.0.2",
2829
"is-url": "^1.2.4"
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,5 @@
1-
/* eslint-disable max-classes-per-file */
2-
3-
import isUrl from 'is-url';
4-
import * as fontkit from 'fontkit';
5-
import {
6-
FontDescriptor,
7-
FontSourceOptions,
8-
FontStyle,
9-
FontWeight,
10-
RemoteOptions,
11-
SingleLoad,
12-
} from './types';
1+
import FontSource from './font-source';
2+
import { FontDescriptor, FontWeight, SingleLoad } from './types';
133

144
const FONT_WEIGHTS = {
155
thin: 100,
@@ -28,88 +18,19 @@ const FONT_WEIGHTS = {
2818
black: 900,
2919
};
3020

31-
const fetchFont = async (src: string, options: RemoteOptions) => {
32-
const response = await fetch(src, options);
33-
const data = await response.arrayBuffer();
34-
35-
return new Uint8Array(data);
36-
};
37-
38-
const isDataUrl = (dataUrl: string) => {
39-
const header = dataUrl.split(',')[0];
40-
const hasDataPrefix = header.substring(0, 5) === 'data:';
41-
const hasBase64Prefix = header.split(';')[1] === 'base64';
42-
43-
return hasDataPrefix && hasBase64Prefix;
44-
};
45-
4621
const resolveFontWeight = (value: FontWeight) => {
4722
return typeof value === 'string' ? FONT_WEIGHTS[value] : value;
4823
};
4924

5025
const sortByFontWeight = (a: FontSource, b: FontSource) =>
5126
a.fontWeight - b.fontWeight;
5227

53-
class FontSource {
54-
src: string;
55-
fontFamily: string;
56-
fontStyle: FontStyle;
57-
fontWeight: number;
58-
data: fontkit.Font | fontkit.FontCollection | null;
59-
options: FontSourceOptions;
60-
loadResultPromise: Promise<void> | null;
61-
62-
constructor(
63-
src: string,
64-
fontFamily: string,
65-
fontStyle?: FontStyle,
66-
fontWeight?: number,
67-
options?: FontSourceOptions,
68-
) {
69-
this.src = src;
70-
this.fontFamily = fontFamily;
71-
this.fontStyle = fontStyle || 'normal';
72-
this.fontWeight = fontWeight || 400;
73-
74-
this.data = null;
75-
this.options = options || {};
76-
this.loadResultPromise = null;
77-
}
78-
79-
async _load(): Promise<void> {
80-
const { postscriptName } = this.options;
81-
82-
if (isDataUrl(this.src)) {
83-
const raw = this.src.split(',')[1];
84-
const uint8Array = new Uint8Array(
85-
atob(raw)
86-
.split('')
87-
.map((c) => c.charCodeAt(0)),
88-
);
89-
this.data = fontkit.create(uint8Array, postscriptName);
90-
} else if (BROWSER || isUrl(this.src)) {
91-
const { headers, body, method = 'GET' } = this.options;
92-
const data = await fetchFont(this.src, { method, body, headers });
93-
this.data = fontkit.create(data, postscriptName);
94-
} else if (!BROWSER) {
95-
this.data = await fontkit.open(this.src, postscriptName);
96-
}
97-
}
98-
99-
async load() {
100-
if (this.loadResultPromise === null) {
101-
this.loadResultPromise = this._load();
102-
}
103-
return this.loadResultPromise;
104-
}
105-
}
106-
107-
class Font {
28+
class FontFamily {
10829
family: string;
10930
sources: FontSource[];
11031

11132
static create(family: string) {
112-
return new Font(family);
33+
return new FontFamily(family);
11334
}
11435

11536
constructor(family: string) {
@@ -163,6 +84,7 @@ class Font {
16384
const lt = styleSources
16485
.filter((s) => s.fontWeight < numericFontWeight)
16586
.sort(sortByFontWeight);
87+
16688
const gt = styleSources
16789
.filter((s) => s.fontWeight > numericFontWeight)
16890
.sort(sortByFontWeight);
@@ -185,4 +107,4 @@ class Font {
185107
}
186108
}
187109

188-
export default Font;
110+
export default FontFamily;

packages/font/src/font-source.ts

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import isUrl from 'is-url';
2+
import * as fontkit from 'fontkit';
3+
4+
import { Font, FontSourceOptions, FontStyle, RemoteOptions } from './types';
5+
import StandardFont, { STANDARD_FONTS } from './standard-font';
6+
7+
const fetchFont = async (src: string, options: RemoteOptions) => {
8+
const response = await fetch(src, options);
9+
const data = await response.arrayBuffer();
10+
11+
return new Uint8Array(data);
12+
};
13+
14+
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';
18+
19+
return hasDataPrefix && hasBase64Prefix;
20+
};
21+
22+
class FontSource {
23+
src: string;
24+
fontFamily: string;
25+
fontStyle: FontStyle;
26+
fontWeight: number;
27+
data: Font | null;
28+
options: FontSourceOptions;
29+
loadResultPromise: Promise<void> | null;
30+
31+
constructor(
32+
src: string,
33+
fontFamily: string,
34+
fontStyle?: FontStyle,
35+
fontWeight?: number,
36+
options?: FontSourceOptions,
37+
) {
38+
this.src = src;
39+
this.fontFamily = fontFamily;
40+
this.fontStyle = fontStyle || 'normal';
41+
this.fontWeight = fontWeight || 400;
42+
43+
this.data = null;
44+
this.options = options || {};
45+
this.loadResultPromise = null;
46+
}
47+
48+
async _load(): Promise<void> {
49+
const { postscriptName } = this.options;
50+
51+
let data = null;
52+
53+
if (STANDARD_FONTS.includes(this.src)) {
54+
data = new StandardFont(this.src);
55+
} else if (isDataUrl(this.src)) {
56+
const raw = this.src.split(',')[1];
57+
const uint8Array = new Uint8Array(
58+
atob(raw)
59+
.split('')
60+
.map((c) => c.charCodeAt(0)),
61+
);
62+
data = fontkit.create(uint8Array, postscriptName);
63+
} else if (BROWSER || isUrl(this.src)) {
64+
const { headers, body, method = 'GET' } = this.options;
65+
const buffer = await fetchFont(this.src, { method, body, headers });
66+
data = fontkit.create(buffer, postscriptName);
67+
} else if (!BROWSER) {
68+
data = await fontkit.open(this.src, postscriptName);
69+
}
70+
71+
if (data && 'fonts' in data) {
72+
throw new Error('Font collection is not supported');
73+
}
74+
75+
this.data = data;
76+
}
77+
78+
async load() {
79+
if (this.loadResultPromise === null) {
80+
this.loadResultPromise = this._load();
81+
}
82+
return this.loadResultPromise;
83+
}
84+
}
85+
86+
export default FontSource;

0 commit comments

Comments
 (0)