|
4 | 4 |
|
5 | 5 | # @react-pdf/textkit |
6 | 6 |
|
| 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 | + |
7 | 11 | ## Acknowledges |
8 | 12 |
|
9 | 13 | 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. |
10 | 14 |
|
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 |
0 commit comments