Skip to content

Commit 16cf5bd

Browse files
authored
feat(textkit): various improvements (#3273)
1 parent 2ca8ae8 commit 16cf5bd

59 files changed

Lines changed: 2191 additions & 118 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/dirty-rocks-jam.md

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

packages/textkit/README.md

Lines changed: 337 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,344 @@
44

55
# @react-pdf/textkit
66

7+
> An advanced text layout framework
8+
9+
A comprehensive text layout engine for react-pdf. Handles complex text rendering including bidirectional text, line breaking, hyphenation, justification, font substitution, and text decoration.
10+
711
## Acknowledges
812

913
This project is a fork of [textkit](https://github.com/foliojs/textkit) by @devongovett and continued under the scope of this project since it has react-pdf specific features. Any recongnition should go to him and the original project mantainers.
1014

11-
## Layout process
12-
13-
1. split into paragraphs
14-
2. get bidi runs and paragraph direction
15-
3. font substitution - map to resolved font runs
16-
4. script itemization
17-
5. font shaping - text to glyphs
18-
6. line breaking
19-
7. bidi reordering
20-
8. justification
21-
9. get a list of rectangles by intersecting path, line, and exclusion paths
22-
10. perform line breaking to get acceptable break points for each fragment
23-
11. ellipsize line if necessary
24-
12. bidi reordering
25-
13. justification
15+
## Installation
16+
17+
```bash
18+
yarn add @react-pdf/textkit
19+
```
20+
21+
## Usage
22+
23+
```js
24+
import layoutEngine, {
25+
bidi,
26+
linebreaker,
27+
justification,
28+
textDecoration,
29+
scriptItemizer,
30+
wordHyphenation,
31+
fontSubstitution,
32+
fromFragments,
33+
} from '@react-pdf/textkit';
34+
35+
// Create engines configuration
36+
const engines = {
37+
bidi: bidi(),
38+
linebreaker: linebreaker({}),
39+
justification: justification({}),
40+
textDecoration: textDecoration(),
41+
scriptItemizer: scriptItemizer(),
42+
wordHyphenation: wordHyphenation(),
43+
fontSubstitution: fontSubstitution(),
44+
};
45+
46+
// Create attributed string from fragments
47+
const attributedString = fromFragments([
48+
{ string: 'Hello ', attributes: { fontSize: 12, font: [myFont] } },
49+
{ string: 'World!', attributes: { fontSize: 12, font: [myFont] } },
50+
]);
51+
52+
// Define container
53+
const container = {
54+
x: 0,
55+
y: 0,
56+
width: 400,
57+
height: 600,
58+
};
59+
60+
// Layout text
61+
const layout = layoutEngine(engines);
62+
const paragraphs = layout(attributedString, container, {});
63+
```
64+
65+
## Layout Process
66+
67+
The layout engine processes text through the following steps:
68+
69+
1. Split into paragraphs
70+
2. Get bidi runs and paragraph direction
71+
3. Font substitution - map to resolved font runs
72+
4. Script itemization
73+
5. Font shaping - text to glyphs
74+
6. Line breaking
75+
7. Bidi reordering
76+
8. Justification
77+
9. Get a list of rectangles by intersecting path, line, and exclusion paths
78+
10. Perform line breaking to get acceptable break points for each fragment
79+
11. Ellipsize line if necessary
80+
12. Bidi reordering
81+
13. Justification
82+
83+
## Engines
84+
85+
The layout engine uses several specialized engines that can be customized:
86+
87+
### bidi
88+
89+
Handles bidirectional text analysis using the Unicode Bidirectional Algorithm. Determines text direction for mixed LTR/RTL content.
90+
91+
```js
92+
import { bidi } from '@react-pdf/textkit';
93+
94+
const bidiEngine = bidi();
95+
const result = bidiEngine(attributedString);
96+
```
97+
98+
### linebreaker
99+
100+
Performs line breaking using the Knuth-Plass algorithm with fallback to best-fit. Handles hyphenation points and produces optimal line breaks.
101+
102+
```js
103+
import { linebreaker } from '@react-pdf/textkit';
104+
105+
const linebreakerEngine = linebreaker({
106+
tolerance: 4,
107+
hyphenationPenalty: 100,
108+
});
109+
```
110+
111+
### justification
112+
113+
Adjusts character and word spacing to achieve justified text alignment. Based on Apple's justification algorithm.
114+
115+
```js
116+
import { justification } from '@react-pdf/textkit';
117+
118+
const justificationEngine = justification({
119+
expandCharFactor: { before: 0, after: 0 },
120+
shrinkCharFactor: { before: 0, after: 0 },
121+
expandWhitespaceFactor: { before: 0.5, after: 0.5 },
122+
shrinkWhitespaceFactor: { before: 0.5, after: 0.5 },
123+
});
124+
```
125+
126+
### fontSubstitution
127+
128+
Automatically substitutes fonts when the primary font doesn't have glyphs for certain characters. Picks the best font from the font stack.
129+
130+
```js
131+
import { fontSubstitution } from '@react-pdf/textkit';
132+
133+
const fontSubstitutionEngine = fontSubstitution();
134+
```
135+
136+
### scriptItemizer
137+
138+
Identifies Unicode script runs in text (Latin, Arabic, Han, etc.) to enable proper font selection and shaping.
139+
140+
```js
141+
import { scriptItemizer } from '@react-pdf/textkit';
142+
143+
const scriptItemizerEngine = scriptItemizer();
144+
```
145+
146+
### wordHyphenation
147+
148+
Provides word hyphenation using language-specific patterns. Supports soft hyphens and custom hyphenation callbacks.
149+
150+
```js
151+
import { wordHyphenation } from '@react-pdf/textkit';
152+
153+
const wordHyphenationEngine = wordHyphenation();
154+
const syllables = wordHyphenationEngine('hyphenation'); // ['hy', 'phen', 'a', 'tion']
155+
```
156+
157+
### textDecoration
158+
159+
Generates decoration lines (underline, strikethrough) for styled text runs.
160+
161+
```js
162+
import { textDecoration } from '@react-pdf/textkit';
163+
164+
const textDecorationEngine = textDecoration();
165+
```
166+
167+
## API Reference
168+
169+
### layoutEngine(engines)
170+
171+
Creates a layout function with the specified engines.
172+
173+
```js
174+
const layout = layoutEngine(engines);
175+
const paragraphs = layout(attributedString, container, options);
176+
```
177+
178+
### fromFragments(fragments)
179+
180+
Creates an AttributedString from text fragments.
181+
182+
```js
183+
import { fromFragments } from '@react-pdf/textkit';
184+
185+
const attributedString = fromFragments([
186+
{ string: 'Hello ', attributes: { fontSize: 14 } },
187+
{ string: 'World!', attributes: { fontSize: 14, color: 'blue' } },
188+
]);
189+
```
190+
191+
## Types
192+
193+
### AttributedString
194+
195+
The main data structure representing styled text:
196+
197+
```ts
198+
type AttributedString = {
199+
string: string;
200+
runs: Run[];
201+
syllables?: string[];
202+
box?: Rect;
203+
decorationLines?: DecorationLine[];
204+
};
205+
```
206+
207+
### Run
208+
209+
A styled segment of text:
210+
211+
```ts
212+
type Run = {
213+
start: number;
214+
end: number;
215+
attributes: Attributes;
216+
glyphs?: Glyph[];
217+
positions?: Position[];
218+
glyphIndices?: number[];
219+
};
220+
```
221+
222+
### Attributes
223+
224+
Style attributes for text runs:
225+
226+
```ts
227+
type Attributes = {
228+
align?: string;
229+
alignLastLine?: string;
230+
attachment?: Attachment;
231+
backgroundColor?: string;
232+
bidiLevel?: number;
233+
characterSpacing?: number;
234+
color?: string;
235+
direction?: 'rtl' | 'ltr';
236+
features?: unknown[];
237+
fill?: boolean;
238+
font?: Font[];
239+
fontSize?: number;
240+
hangingPunctuation?: boolean;
241+
hyphenationFactor?: number;
242+
indent?: number;
243+
justificationFactor?: number;
244+
lineHeight?: number;
245+
lineSpacing?: number;
246+
link?: string;
247+
margin?: number;
248+
marginLeft?: number;
249+
marginRight?: number;
250+
opacity?: number;
251+
padding?: number;
252+
paddingTop?: number;
253+
paragraphSpacing?: number;
254+
scale?: number;
255+
script?: unknown;
256+
shrinkFactor?: number;
257+
strike?: boolean;
258+
strikeColor?: string;
259+
strikeStyle?: string;
260+
stroke?: boolean;
261+
underline?: boolean;
262+
underlineColor?: string;
263+
underlineStyle?: string;
264+
verticalAlign?: string;
265+
wordSpacing?: number;
266+
yOffset?: number;
267+
};
268+
```
269+
270+
### Container
271+
272+
The area where text will be laid out:
273+
274+
```ts
275+
type Container = {
276+
x: number;
277+
y: number;
278+
width: number;
279+
height: number;
280+
truncateMode?: 'ellipsis';
281+
maxLines?: number;
282+
excludeRects?: Rect[];
283+
};
284+
```
285+
286+
### Rect
287+
288+
A rectangle definition:
289+
290+
```ts
291+
type Rect = {
292+
x: number;
293+
y: number;
294+
width: number;
295+
height: number;
296+
};
297+
```
298+
299+
### Fragment
300+
301+
Input format for creating attributed strings:
302+
303+
```ts
304+
type Fragment = {
305+
string: string;
306+
attributes?: Attributes;
307+
};
308+
```
309+
310+
### LayoutOptions
311+
312+
Options for the layout process:
313+
314+
```ts
315+
type LayoutOptions = {
316+
hyphenationCallback?: (
317+
word: string | null,
318+
fallback: (word: string | null) => string[],
319+
) => string[];
320+
tolerance?: number;
321+
hyphenationPenalty?: number;
322+
expandCharFactor?: JustificationFactor;
323+
shrinkCharFactor?: JustificationFactor;
324+
expandWhitespaceFactor?: JustificationFactor;
325+
shrinkWhitespaceFactor?: JustificationFactor;
326+
};
327+
```
328+
329+
### Engines
330+
331+
The engines configuration object:
332+
333+
```ts
334+
type Engines = {
335+
bidi: ReturnType<typeof bidi>;
336+
linebreaker: ReturnType<typeof linebreaker>;
337+
justification: ReturnType<typeof justification>;
338+
fontSubstitution: ReturnType<typeof fontSubstitution>;
339+
scriptItemizer: ReturnType<typeof scriptItemizer>;
340+
textDecoration: ReturnType<typeof textDecoration>;
341+
wordHyphenation?: ReturnType<typeof wordHyphenation>;
342+
};
343+
```
344+
345+
## License
346+
347+
MIT

packages/textkit/src/attributedString/ascent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { AttributedString, Run } from '../types';
88
* @returns Ascent
99
*/
1010
const ascent = (attributedString: AttributedString) => {
11-
const reducer = (acc, run: Run) => Math.max(acc, runAscent(run));
11+
const reducer = (acc: number, run: Run) => Math.max(acc, runAscent(run));
1212
return attributedString.runs.reduce(reducer, 0);
1313
};
1414

0 commit comments

Comments
 (0)