@jasy/pdf 1.0.0-alpha.8 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -10
- package/dist/api/color.js +69 -0
- package/dist/api/content.d.ts +45 -5
- package/dist/api/content.js +96 -7
- package/dist/api/descriptor.js +6 -1
- package/dist/api/dimension.d.ts +57 -0
- package/dist/api/dimension.js +27 -0
- package/dist/api/font-url.d.ts +28 -0
- package/dist/api/font-url.js +135 -0
- package/dist/api/gradient.d.ts +53 -0
- package/dist/api/gradient.js +97 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/insets.d.ts +19 -12
- package/dist/api/insets.js +18 -7
- package/dist/api/layout.d.ts +20 -6
- package/dist/api/layout.js +31 -6
- package/dist/api/page-builder.d.ts +8 -0
- package/dist/api/page-builder.js +12 -0
- package/dist/api/structure.d.ts +12 -0
- package/dist/api/structure.js +54 -7
- package/dist/api/text.d.ts +65 -8
- package/dist/api/text.js +38 -5
- package/dist/canvas/painter.d.ts +102 -0
- package/dist/canvas/painter.js +183 -0
- package/dist/edit/document.d.ts +14 -4
- package/dist/edit/document.js +15 -22
- package/dist/edit/fill.d.ts +19 -2
- package/dist/edit/fill.js +63 -34
- package/dist/edit/flatten.d.ts +13 -1
- package/dist/edit/flatten.js +38 -13
- package/dist/edit/index.d.ts +2 -0
- package/dist/edit/index.js +3 -1
- package/dist/edit/objects.d.ts +5 -0
- package/dist/edit/objects.js +27 -0
- package/dist/edit/writer.d.ts +7 -0
- package/dist/edit/writer.js +23 -1
- package/dist/elements/canvas-element.d.ts +38 -0
- package/dist/elements/canvas-element.js +47 -0
- package/dist/elements/container-element.d.ts +23 -1
- package/dist/elements/container-element.js +31 -10
- package/dist/elements/image-element.d.ts +18 -4
- package/dist/elements/image-element.js +27 -12
- package/dist/elements/index.d.ts +2 -0
- package/dist/elements/index.js +2 -0
- package/dist/elements/layout/expanded-element.d.ts +5 -1
- package/dist/elements/layout/expanded-element.js +16 -3
- package/dist/elements/layout/keep-together-element.d.ts +2 -0
- package/dist/elements/layout/keep-together-element.js +4 -0
- package/dist/elements/layout/padding-element.d.ts +9 -2
- package/dist/elements/layout/padding-element.js +12 -2
- package/dist/elements/layout/page-builder-element.js +2 -0
- package/dist/elements/layout/struct-group.d.ts +2 -0
- package/dist/elements/layout/struct-group.js +4 -0
- package/dist/elements/pdf-element.d.ts +75 -1
- package/dist/elements/pdf-element.js +80 -1
- package/dist/elements/rectangle-element.d.ts +22 -6
- package/dist/elements/rectangle-element.js +31 -12
- package/dist/elements/row-element.d.ts +26 -1
- package/dist/elements/row-element.js +31 -10
- package/dist/elements/svg-element.d.ts +41 -0
- package/dist/elements/svg-element.js +93 -0
- package/dist/elements/text-element.d.ts +92 -1
- package/dist/elements/text-element.js +208 -33
- package/dist/ir/display-list.d.ts +67 -11
- package/dist/ir/display-list.js +8 -1
- package/dist/layout/alignment.d.ts +10 -0
- package/dist/layout/alignment.js +6 -0
- package/dist/layout/box-constraints.d.ts +53 -0
- package/dist/layout/box-constraints.js +72 -0
- package/dist/layout/insets.d.ts +28 -0
- package/dist/layout/insets.js +20 -0
- package/dist/layout/min-intrinsic.d.ts +29 -0
- package/dist/layout/min-intrinsic.js +37 -0
- package/dist/renderer/anchor-renderer.js +1 -1
- package/dist/renderer/bookmark-renderer.js +1 -1
- package/dist/renderer/canvas-renderer.d.ts +14 -0
- package/dist/renderer/canvas-renderer.js +36 -0
- package/dist/renderer/container-renderer.js +1 -3
- package/dist/renderer/default-text-style-renderer.js +1 -1
- package/dist/renderer/deferred-renderer.js +1 -1
- package/dist/renderer/expanded-renderer.js +1 -1
- package/dist/renderer/image-renderer.js +2 -1
- package/dist/renderer/index.d.ts +2 -0
- package/dist/renderer/index.js +2 -0
- package/dist/renderer/keep-together-renderer.js +1 -1
- package/dist/renderer/link-renderer.js +1 -1
- package/dist/renderer/padding-renderer.js +1 -1
- package/dist/renderer/page-builder-renderer.js +1 -1
- package/dist/renderer/page-renderer.js +1 -3
- package/dist/renderer/pdf-backend.d.ts +21 -4
- package/dist/renderer/pdf-backend.js +198 -61
- package/dist/renderer/pdf-document-renderer.js +8 -1
- package/dist/renderer/pdf-renderer.js +5 -1
- package/dist/renderer/positioned-renderer.js +1 -1
- package/dist/renderer/rectangle-renderer.js +9 -5
- package/dist/renderer/repeating-header-renderer.js +1 -2
- package/dist/renderer/rotated-renderer.js +1 -1
- package/dist/renderer/row-renderer.js +1 -3
- package/dist/renderer/struct-group-renderer.js +1 -1
- package/dist/renderer/svg-renderer.d.ts +11 -0
- package/dist/renderer/svg-renderer.js +27 -0
- package/dist/renderer/text-renderer.d.ts +2 -2
- package/dist/renderer/text-renderer.js +176 -57
- package/dist/svg/css.d.ts +38 -0
- package/dist/svg/css.js +115 -0
- package/dist/svg/errors.d.ts +24 -0
- package/dist/svg/errors.js +30 -0
- package/dist/svg/gradients.d.ts +34 -0
- package/dist/svg/gradients.js +156 -0
- package/dist/svg/index.d.ts +22 -0
- package/dist/svg/index.js +331 -0
- package/dist/svg/shapes.d.ts +6 -0
- package/dist/svg/shapes.js +177 -0
- package/dist/svg/style.d.ts +61 -0
- package/dist/svg/style.js +144 -0
- package/dist/svg/transform.d.ts +13 -0
- package/dist/svg/transform.js +68 -0
- package/dist/svg/units.d.ts +11 -0
- package/dist/svg/units.js +73 -0
- package/dist/svg/xml.d.ts +24 -0
- package/dist/svg/xml.js +158 -0
- package/dist/text/advance.d.ts +3 -0
- package/dist/text/advance.js +8 -4
- package/dist/text/arabic.d.ts +20 -0
- package/dist/text/arabic.js +101 -0
- package/dist/text/bidi.d.ts +33 -0
- package/dist/text/bidi.js +94 -0
- package/dist/text/font-fallback.d.ts +25 -0
- package/dist/text/font-fallback.js +38 -0
- package/dist/text/glyph-coverage.d.ts +15 -0
- package/dist/text/glyph-coverage.js +77 -0
- package/dist/text/line-breaker.d.ts +43 -1
- package/dist/text/line-breaker.js +181 -45
- package/dist/text/orphans-widows.d.ts +27 -0
- package/dist/text/orphans-widows.js +53 -0
- package/dist/text/shape.d.ts +29 -0
- package/dist/text/shape.js +91 -0
- package/dist/text/text-style.d.ts +38 -0
- package/dist/text/text-style.js +50 -1
- package/dist/text/whitespace.d.ts +4 -0
- package/dist/text/whitespace.js +43 -0
- package/dist/text/word-splitting.d.ts +42 -0
- package/dist/text/word-splitting.js +72 -0
- package/dist/utils/flex-layout.d.ts +24 -4
- package/dist/utils/flex-layout.js +218 -7
- package/dist/utils/font-metrics.d.ts +13 -2
- package/dist/utils/gsub.d.ts +36 -0
- package/dist/utils/gsub.js +178 -0
- package/dist/utils/inflate.d.ts +16 -0
- package/dist/utils/inflate.js +52 -0
- package/dist/utils/pdf-object-manager.d.ts +44 -3
- package/dist/utils/pdf-object-manager.js +176 -14
- package/dist/utils/renderer-registry.d.ts +13 -1
- package/dist/utils/renderer-registry.js +22 -1
- package/dist/utils/ttf-parser.d.ts +6 -0
- package/dist/utils/ttf-parser.js +19 -0
- package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
- package/dist/utils/utf8-to-windows1252-encoder.js +10 -0
- package/dist/utils/woff.d.ts +10 -0
- package/dist/utils/woff.js +116 -0
- package/dist/utils/woff2.d.ts +6 -0
- package/dist/utils/woff2.js +651 -0
- package/dist/validators/element-validator.js +9 -2
- package/dist/vector/path.d.ts +33 -0
- package/dist/vector/path.js +81 -0
- package/package.json +6 -2
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { Color } from "../common/color.js";
|
|
2
2
|
import { FontStyle } from "../utils/pdf-object-manager.js";
|
|
3
3
|
import { BoxConstraints, Offset, Size } from "../layout/box-constraints.js";
|
|
4
|
+
import type { Direction } from "../text/bidi.js";
|
|
5
|
+
import { type TextTransform } from "../text/text-style.js";
|
|
4
6
|
import { Fragmentable, FragmentResult } from "../layout/fragmentation.js";
|
|
5
7
|
import { TextOverflow } from "../text/line-breaker.js";
|
|
6
8
|
import { HorizontalAlignment, LayoutContext, SizedPDFElement } from "./pdf-element.js";
|
|
9
|
+
import type { Hyphenator } from "../text/word-splitting.js";
|
|
7
10
|
export interface TextSegment {
|
|
8
11
|
content: string;
|
|
9
12
|
fontStyle?: FontStyle;
|
|
10
13
|
fontColor?: Color;
|
|
11
14
|
fontFamily?: string;
|
|
15
|
+
/** The rest of the family stack for THIS run; unset inherits the Text's own. */
|
|
16
|
+
fontFallback?: string[];
|
|
12
17
|
fontSize?: number;
|
|
13
18
|
/** External URL: this segment becomes an inline hyperlink (a /Link annotation over its glyphs). */
|
|
14
19
|
href?: string;
|
|
@@ -19,7 +24,17 @@ export interface TextSegment {
|
|
|
19
24
|
strikethrough?: boolean;
|
|
20
25
|
/** Extra space after every glyph, in points; unset inherits the Text's own value. */
|
|
21
26
|
letterSpacing?: number;
|
|
27
|
+
/**
|
|
28
|
+
* CSS `vertical-align`, for a footnote marker or a formula index. Shifts THIS run's baseline;
|
|
29
|
+
* it does not resize the text, and it does not change the line's height - set `fontSize` yourself
|
|
30
|
+
* for the smaller look a browser's `<sup>` gets from its default stylesheet.
|
|
31
|
+
*/
|
|
32
|
+
verticalAlign?: VerticalTextAlign;
|
|
22
33
|
}
|
|
34
|
+
/** How far a run sits off the baseline, as a fraction of its font size (up is positive). */
|
|
35
|
+
export declare const VERTICAL_TEXT_SHIFT: Record<VerticalTextAlign, number>;
|
|
36
|
+
/** CSS `vertical-align`, the three values that mean something for a run of text. */
|
|
37
|
+
export type VerticalTextAlign = "baseline" | "super" | "sub";
|
|
23
38
|
/** Accessibility role for the tagged structure tree: a heading level or a paragraph (the default). */
|
|
24
39
|
export type TextRole = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p";
|
|
25
40
|
interface TextElementParams {
|
|
@@ -27,12 +42,18 @@ interface TextElementParams {
|
|
|
27
42
|
/** Unset (undefined) inherits the cascaded size; see ResolvedTextStyle. */
|
|
28
43
|
fontSize?: number;
|
|
29
44
|
fontFamily?: string;
|
|
45
|
+
/** The rest of the family stack, for code points `fontFamily` cannot draw. */
|
|
46
|
+
fontFallback?: string[];
|
|
30
47
|
fontStyle?: FontStyle;
|
|
31
48
|
content: string | TextSegment[];
|
|
32
49
|
color?: Color;
|
|
33
50
|
textAlignment?: HorizontalAlignment;
|
|
34
51
|
/** Cap the wrapped lines (default: unlimited / open-end). */
|
|
35
52
|
maxLines?: number;
|
|
53
|
+
/** Minimum lines that must stay behind at a page break (CSS `orphans`, default 2). */
|
|
54
|
+
orphans?: number;
|
|
55
|
+
/** Minimum lines that must carry over to the next page (CSS `widows`, default 2). */
|
|
56
|
+
widows?: number;
|
|
36
57
|
/** What to do past `maxLines`: `"clip"` (default) drops them, `"ellipsis"` ends with "…". */
|
|
37
58
|
overflow?: TextOverflow;
|
|
38
59
|
/** Line-height multiplier: each line is `fontSize * lineHeight` tall. Unset means the font's
|
|
@@ -46,6 +67,20 @@ interface TextElementParams {
|
|
|
46
67
|
skipInk?: boolean;
|
|
47
68
|
/** Extra space after every glyph, in points (CSS `letter-spacing`). Default 0. */
|
|
48
69
|
letterSpacing?: number;
|
|
70
|
+
/** Base writing direction (CSS `direction`); decides where a line starts. Default `"ltr"`. */
|
|
71
|
+
direction?: Direction;
|
|
72
|
+
/** CSS `text-transform`; applied before the text is measured or drawn. */
|
|
73
|
+
textTransform?: TextTransform;
|
|
74
|
+
/** CSS `word-spacing`, in points: extra advance at every space. */
|
|
75
|
+
wordSpacing?: number;
|
|
76
|
+
/** CSS `text-indent`, in points: how far the first line starts in. */
|
|
77
|
+
textIndent?: number;
|
|
78
|
+
/** CSS `overflow-wrap: break-word` - split a word wider than its box, anywhere. */
|
|
79
|
+
breakWord?: boolean;
|
|
80
|
+
/** The font's ligatures; on unless switched off. */
|
|
81
|
+
ligatures?: boolean;
|
|
82
|
+
/** Language-aware splitting; a hyphen is drawn at the break. See text/word-splitting.ts. */
|
|
83
|
+
hyphenate?: Hyphenator;
|
|
49
84
|
/** Accessibility role for the tagged structure tree (heading level or paragraph; default `"p"`). */
|
|
50
85
|
role?: TextRole;
|
|
51
86
|
}
|
|
@@ -60,6 +95,14 @@ export declare class TextElement extends SizedPDFElement implements Fragmentable
|
|
|
60
95
|
private readonly rawStrikethrough?;
|
|
61
96
|
private readonly rawSkipInk?;
|
|
62
97
|
private readonly rawLetterSpacing?;
|
|
98
|
+
private readonly rawDirection?;
|
|
99
|
+
private readonly rawFontFallback?;
|
|
100
|
+
private readonly rawTextTransform?;
|
|
101
|
+
private readonly rawWordSpacing?;
|
|
102
|
+
private readonly rawTextIndent?;
|
|
103
|
+
private readonly rawBreakWord?;
|
|
104
|
+
private readonly rawLigatures?;
|
|
105
|
+
private readonly rawHyphenate?;
|
|
63
106
|
private fontSize;
|
|
64
107
|
private fontFamily;
|
|
65
108
|
private fontStyle;
|
|
@@ -70,12 +113,41 @@ export declare class TextElement extends SizedPDFElement implements Fragmentable
|
|
|
70
113
|
private strikethrough;
|
|
71
114
|
private skipInk;
|
|
72
115
|
private letterSpacing;
|
|
116
|
+
private direction;
|
|
117
|
+
private fontFallback;
|
|
118
|
+
private resolved?;
|
|
119
|
+
private textTransform;
|
|
120
|
+
private wordSpacing;
|
|
121
|
+
private textIndent;
|
|
122
|
+
private breakWord;
|
|
123
|
+
private ligatures;
|
|
124
|
+
private hyphenate?;
|
|
73
125
|
private content;
|
|
74
126
|
private maxLines?;
|
|
127
|
+
private orphans?;
|
|
128
|
+
private widows?;
|
|
75
129
|
private overflow;
|
|
76
130
|
private readonly role?;
|
|
77
|
-
constructor({ fontSize, content, fontFamily, fontStyle, color, textAlignment, maxLines, overflow, lineHeight, underline, strikethrough, skipInk, letterSpacing, role, }: TextElementParams);
|
|
131
|
+
constructor({ fontSize, content, fontFamily, fontStyle, color, textAlignment, maxLines, orphans, widows, overflow, lineHeight, underline, strikethrough, skipInk, letterSpacing, direction, fontFallback, textTransform, wordSpacing, textIndent, breakWord, hyphenate, ligatures, role, }: TextElementParams);
|
|
78
132
|
private resolveStyle;
|
|
133
|
+
/** Justified lines may be squeezed to keep a word; every other alignment gets no slack. Read by the
|
|
134
|
+
* breaker in BOTH passes, or a measured line and a drawn one would disagree about where it ends. */
|
|
135
|
+
/**
|
|
136
|
+
* The content as it is measured AND drawn - `text-transform` applied. One place, so a recased
|
|
137
|
+
* paragraph can never be measured in one casing and drawn in another (`ABC` and `abc` are not the
|
|
138
|
+
* same width in most fonts).
|
|
139
|
+
*/
|
|
140
|
+
private display;
|
|
141
|
+
/**
|
|
142
|
+
* Drops or substitutes whatever the RESOLVED font cannot draw. Runs last, after the fallback stack
|
|
143
|
+
* has had its say, and before anything is measured - a character removed at draw time only would
|
|
144
|
+
* make the measured line and the drawn one disagree. A font that draws everything gets its input
|
|
145
|
+
* back unchanged, so an ordinary document is byte-identical.
|
|
146
|
+
*/
|
|
147
|
+
private covered;
|
|
148
|
+
/** The content with `text-transform` applied, and nothing else. */
|
|
149
|
+
private recased;
|
|
150
|
+
private lineOptions;
|
|
79
151
|
private applyStyle;
|
|
80
152
|
/**
|
|
81
153
|
* Splits the paragraph at line boxes (Slice 1). The lines that fit in `maxHeight` stay;
|
|
@@ -86,6 +158,8 @@ export declare class TextElement extends SizedPDFElement implements Fragmentable
|
|
|
86
158
|
fragment(maxHeight: number, width: number, ctx: LayoutContext): FragmentResult;
|
|
87
159
|
private fragmentString;
|
|
88
160
|
private fragmentSegments;
|
|
161
|
+
/** The orphan/widow minimums for this paragraph; unset means the CSS default of 2 lines each. */
|
|
162
|
+
private breakRule;
|
|
89
163
|
private cloneWithContent;
|
|
90
164
|
calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
|
|
91
165
|
/** The unwrapped single-line width of the content (used when width is unbounded, e.g. inside a Row).
|
|
@@ -97,6 +171,15 @@ export declare class TextElement extends SizedPDFElement implements Fragmentable
|
|
|
97
171
|
* - enough to tip a borderline string (e.g. "20 Jun 2026", wider than "04 Jul 2026" only because
|
|
98
172
|
* 'n' beats 'l') one bit over its own width, dropping the last word onto a second line. */
|
|
99
173
|
private naturalWidth;
|
|
174
|
+
/**
|
|
175
|
+
* The widest piece of this text that cannot be broken - CSS's `min-content` width. A shrinking flex
|
|
176
|
+
* parent floors the width it hands us here, so a squeezed paragraph stops at its longest word
|
|
177
|
+
* instead of becoming one word per line.
|
|
178
|
+
*
|
|
179
|
+
* With `breakWord` or `hyphenate` on, the text may be cut mid-word, so the unbreakable piece is a
|
|
180
|
+
* single character - the same reasoning CSS applies to `word-break: break-all`.
|
|
181
|
+
*/
|
|
182
|
+
minIntrinsicMain(horizontal: boolean, ctx: LayoutContext): number;
|
|
100
183
|
getProps(): {
|
|
101
184
|
x: number;
|
|
102
185
|
y: number;
|
|
@@ -109,12 +192,20 @@ export declare class TextElement extends SizedPDFElement implements Fragmentable
|
|
|
109
192
|
content: string | TextSegment[];
|
|
110
193
|
textAlignment: HorizontalAlignment;
|
|
111
194
|
maxLines: number | undefined;
|
|
195
|
+
orphans: number | undefined;
|
|
196
|
+
widows: number | undefined;
|
|
112
197
|
overflow: TextOverflow;
|
|
113
198
|
lineHeight: number | undefined;
|
|
114
199
|
underline: boolean;
|
|
115
200
|
strikethrough: boolean;
|
|
116
201
|
skipInk: boolean;
|
|
117
202
|
letterSpacing: number;
|
|
203
|
+
direction: Direction;
|
|
204
|
+
wordSpacing: number;
|
|
205
|
+
textIndent: number;
|
|
206
|
+
breakWord: boolean;
|
|
207
|
+
ligatures: boolean;
|
|
208
|
+
hyphenate: Hyphenator | undefined;
|
|
118
209
|
role: TextRole | undefined;
|
|
119
210
|
};
|
|
120
211
|
}
|
|
@@ -4,12 +4,34 @@
|
|
|
4
4
|
// breaks the constructor-keyed RendererRegistry. A direct import keeps the graph acyclic.
|
|
5
5
|
import { TextRenderer } from "../renderer/text-renderer.js";
|
|
6
6
|
import { DEFAULT_TEXT_STYLE } from "../text/text-style.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { applyTextTransform } from "../text/text-style.js";
|
|
8
|
+
import { splitByFont } from "../text/font-fallback.js";
|
|
9
9
|
import { runAdvance } from "../text/advance.js";
|
|
10
|
-
import {
|
|
10
|
+
import { singleLineWidth, MAX_SPACE_SHRINK, wrapStringIntoLines, breakSegmentsIntoLines, segmentLinesToSegments, } from "../text/line-breaker.js";
|
|
11
|
+
import { adjustForOrphansWidows, DEFAULT_ORPHANS, DEFAULT_WIDOWS, } from "../text/orphans-widows.js";
|
|
12
|
+
import { lineBoxForSegmentLine, lineBoxForString } from "../text/line-metrics.js";
|
|
13
|
+
import { HorizontalAlignment, SizedPDFElement } from "./pdf-element.js";
|
|
14
|
+
import { normalizeContent } from "../text/whitespace.js";
|
|
15
|
+
import { coverText } from "../text/glyph-coverage.js";
|
|
16
|
+
/** How far a run sits off the baseline, as a fraction of its font size (up is positive). */
|
|
17
|
+
export const VERTICAL_TEXT_SHIFT = {
|
|
18
|
+
baseline: 0,
|
|
19
|
+
super: 1 / 3,
|
|
20
|
+
sub: -1 / 5,
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* A measurement in points has to be a real number. A NaN or an Infinity would travel all the way into
|
|
24
|
+
* the content stream as a position - the backend refuses it there, but by then the message names a
|
|
25
|
+
* coordinate rather than the property that was wrong. Rejected here, where the caller can see it.
|
|
26
|
+
*/
|
|
27
|
+
function finitePoints(value, name) {
|
|
28
|
+
if (value !== undefined && !Number.isFinite(value)) {
|
|
29
|
+
throw new Error(`@jasy/pdf: Invalid ${name} ${value}: it must be a finite number of points.`);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
11
33
|
export class TextElement extends SizedPDFElement {
|
|
12
|
-
constructor({ fontSize, content, fontFamily, fontStyle, color, textAlignment, maxLines, overflow = "clip", lineHeight, underline, strikethrough, skipInk, letterSpacing, role, }) {
|
|
34
|
+
constructor({ fontSize, content, fontFamily, fontStyle, color, textAlignment, maxLines, orphans, widows, overflow = "clip", lineHeight, underline, strikethrough, skipInk, letterSpacing, direction, fontFallback, textTransform, wordSpacing, textIndent, breakWord, hyphenate, ligatures, role, }) {
|
|
13
35
|
super({ x: 0, y: 0 });
|
|
14
36
|
this.role = role;
|
|
15
37
|
this.rawFontSize = fontSize;
|
|
@@ -22,8 +44,20 @@ export class TextElement extends SizedPDFElement {
|
|
|
22
44
|
this.rawStrikethrough = strikethrough;
|
|
23
45
|
this.rawSkipInk = skipInk;
|
|
24
46
|
this.rawLetterSpacing = letterSpacing;
|
|
25
|
-
this.
|
|
47
|
+
this.rawDirection = direction;
|
|
48
|
+
this.rawFontFallback = fontFallback;
|
|
49
|
+
this.rawTextTransform = textTransform;
|
|
50
|
+
this.rawWordSpacing = finitePoints(wordSpacing, "wordSpacing");
|
|
51
|
+
this.rawTextIndent = finitePoints(textIndent, "textIndent");
|
|
52
|
+
this.rawBreakWord = breakWord;
|
|
53
|
+
this.rawLigatures = ligatures;
|
|
54
|
+
this.rawHyphenate = hyphenate;
|
|
55
|
+
// Control characters have no glyph and would be DRAWN as .notdef boxes; `\n` survives
|
|
56
|
+
// because the breaker treats it as a hard break. See text/whitespace.ts.
|
|
57
|
+
this.content = normalizeContent(content);
|
|
26
58
|
this.maxLines = maxLines;
|
|
59
|
+
this.orphans = orphans;
|
|
60
|
+
this.widows = widows;
|
|
27
61
|
this.overflow = overflow;
|
|
28
62
|
this.applyStyle(DEFAULT_TEXT_STYLE);
|
|
29
63
|
}
|
|
@@ -31,6 +65,82 @@ export class TextElement extends SizedPDFElement {
|
|
|
31
65
|
resolveStyle(ctx) {
|
|
32
66
|
this.applyStyle(ctx.textStyle ?? DEFAULT_TEXT_STYLE);
|
|
33
67
|
}
|
|
68
|
+
/** Justified lines may be squeezed to keep a word; every other alignment gets no slack. Read by the
|
|
69
|
+
* breaker in BOTH passes, or a measured line and a drawn one would disagree about where it ends. */
|
|
70
|
+
/**
|
|
71
|
+
* The content as it is measured AND drawn - `text-transform` applied. One place, so a recased
|
|
72
|
+
* paragraph can never be measured in one casing and drawn in another (`ABC` and `abc` are not the
|
|
73
|
+
* same width in most fonts).
|
|
74
|
+
*/
|
|
75
|
+
display(metrics) {
|
|
76
|
+
const cased = this.recased();
|
|
77
|
+
// A SPAN may bring its own stack, so the element's being empty is not enough to skip the pass.
|
|
78
|
+
const anyStack = this.fontFallback.length > 0 ||
|
|
79
|
+
(typeof cased !== "string" && cased.some((seg) => (seg.fontFallback?.length ?? 0) > 0));
|
|
80
|
+
if (!metrics)
|
|
81
|
+
return cased;
|
|
82
|
+
if (!anyStack)
|
|
83
|
+
return this.covered(cased, metrics);
|
|
84
|
+
// Font fallback turns the content into spans - one per family - which every later pass already
|
|
85
|
+
// knows how to handle. Nothing new downstream.
|
|
86
|
+
const pieces = typeof cased === "string" ? [{ content: cased }] : cased;
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const seg of pieces) {
|
|
89
|
+
const runs = splitByFont(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontFallback ?? this.fontFallback, seg.fontStyle ?? this.fontStyle, metrics);
|
|
90
|
+
if (!runs)
|
|
91
|
+
out.push(seg);
|
|
92
|
+
else
|
|
93
|
+
for (const run of runs)
|
|
94
|
+
out.push({ ...seg, content: run.text, fontFamily: run.fontFamily });
|
|
95
|
+
}
|
|
96
|
+
return this.covered(out, metrics);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Drops or substitutes whatever the RESOLVED font cannot draw. Runs last, after the fallback stack
|
|
100
|
+
* has had its say, and before anything is measured - a character removed at draw time only would
|
|
101
|
+
* make the measured line and the drawn one disagree. A font that draws everything gets its input
|
|
102
|
+
* back unchanged, so an ordinary document is byte-identical.
|
|
103
|
+
*/
|
|
104
|
+
covered(content, metrics) {
|
|
105
|
+
const report = (dropped) => metrics.reportMissingGlyph?.(dropped);
|
|
106
|
+
if (typeof content === "string") {
|
|
107
|
+
const { text, dropped } = coverText(content, this.fontFamily, this.fontStyle, metrics);
|
|
108
|
+
if (dropped.length > 0)
|
|
109
|
+
report(dropped);
|
|
110
|
+
return text;
|
|
111
|
+
}
|
|
112
|
+
return content.map((seg) => {
|
|
113
|
+
const { text, dropped } = coverText(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontStyle ?? this.fontStyle, metrics);
|
|
114
|
+
if (dropped.length > 0)
|
|
115
|
+
report(dropped);
|
|
116
|
+
return text === seg.content ? seg : { ...seg, content: text };
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/** The content with `text-transform` applied, and nothing else. */
|
|
120
|
+
recased() {
|
|
121
|
+
if (this.textTransform === "none")
|
|
122
|
+
return this.content;
|
|
123
|
+
if (typeof this.content === "string") {
|
|
124
|
+
return applyTextTransform(this.content, this.textTransform).text;
|
|
125
|
+
}
|
|
126
|
+
// The capitalisation state runs THROUGH the spans: `span("hel") + span("lo")` is one word.
|
|
127
|
+
let atWordStart = true;
|
|
128
|
+
return this.content.map((seg) => {
|
|
129
|
+
const done = applyTextTransform(seg.content, this.textTransform, atWordStart);
|
|
130
|
+
atWordStart = done.atWordStart;
|
|
131
|
+
return { ...seg, content: done.text };
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
lineOptions() {
|
|
135
|
+
return {
|
|
136
|
+
wordSpacing: this.wordSpacing,
|
|
137
|
+
indent: this.textIndent,
|
|
138
|
+
splitting: { breakWord: this.breakWord, hyphenate: this.hyphenate },
|
|
139
|
+
ligatures: this.ligatures,
|
|
140
|
+
// Justified lines may be squeezed to keep a word; every other alignment gets no slack.
|
|
141
|
+
shrink: this.textAlignment === HorizontalAlignment.justify ? MAX_SPACE_SHRINK : 0,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
34
144
|
applyStyle(ts) {
|
|
35
145
|
this.fontSize = this.rawFontSize ?? ts.fontSize;
|
|
36
146
|
this.fontFamily = this.rawFontFamily ?? ts.fontFamily;
|
|
@@ -42,6 +152,14 @@ export class TextElement extends SizedPDFElement {
|
|
|
42
152
|
this.strikethrough = this.rawStrikethrough ?? ts.strikethrough;
|
|
43
153
|
this.skipInk = this.rawSkipInk ?? ts.skipInk;
|
|
44
154
|
this.letterSpacing = this.rawLetterSpacing ?? ts.letterSpacing;
|
|
155
|
+
this.direction = this.rawDirection ?? ts.direction;
|
|
156
|
+
this.fontFallback = this.rawFontFallback ?? ts.fontFallback;
|
|
157
|
+
this.textTransform = this.rawTextTransform ?? ts.textTransform;
|
|
158
|
+
this.wordSpacing = this.rawWordSpacing ?? ts.wordSpacing;
|
|
159
|
+
this.textIndent = this.rawTextIndent ?? ts.textIndent;
|
|
160
|
+
this.breakWord = this.rawBreakWord ?? ts.breakWord;
|
|
161
|
+
this.ligatures = this.rawLigatures ?? ts.ligatures;
|
|
162
|
+
this.hyphenate = this.rawHyphenate ?? ts.hyphenate;
|
|
45
163
|
}
|
|
46
164
|
/**
|
|
47
165
|
* Splits the paragraph at line boxes (Slice 1). The lines that fit in `maxHeight` stay;
|
|
@@ -51,23 +169,25 @@ export class TextElement extends SizedPDFElement {
|
|
|
51
169
|
*/
|
|
52
170
|
fragment(maxHeight, width, ctx) {
|
|
53
171
|
this.resolveStyle(ctx);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
172
|
+
const content = this.display(ctx.metrics);
|
|
173
|
+
return typeof content === "string"
|
|
174
|
+
? this.fragmentString(content, maxHeight, width, ctx)
|
|
175
|
+
: this.fragmentSegments(content, maxHeight, width, ctx);
|
|
57
176
|
}
|
|
58
177
|
// Plain string: every wrapped line gets the same box (the same one calculateTextHeight uses),
|
|
59
178
|
// so `floor(maxHeight / lineBox)` lines fit.
|
|
60
179
|
fragmentString(content, maxHeight, width, ctx) {
|
|
61
|
-
const lines = wrapStringIntoLines(content, this.fontFamily, this.fontSize, this.fontStyle, width, ctx.metrics, this.maxLines, this.overflow, this.letterSpacing);
|
|
180
|
+
const lines = wrapStringIntoLines(content, this.fontFamily, this.fontSize, this.fontStyle, width, ctx.metrics, this.maxLines, this.overflow, this.letterSpacing, this.lineOptions());
|
|
62
181
|
const box = lineBoxForString(ctx.metrics, this.fontFamily, this.fontStyle, this.fontSize, this.lineHeight);
|
|
63
|
-
const fittedLineCount = Math.floor(maxHeight / box.height);
|
|
182
|
+
const fittedLineCount = adjustForOrphansWidows(Math.floor(maxHeight / box.height), lines.length, this.breakRule());
|
|
64
183
|
if (fittedLineCount <= 0)
|
|
65
184
|
return { fitted: null, remainder: this };
|
|
66
185
|
if (fittedLineCount >= lines.length)
|
|
67
186
|
return { fitted: this, remainder: null };
|
|
68
187
|
return {
|
|
69
188
|
fitted: this.cloneWithContent(lines.slice(0, fittedLineCount).join(" ")),
|
|
70
|
-
|
|
189
|
+
// A continuation is no longer the paragraph's FIRST line, so it carries no indent.
|
|
190
|
+
remainder: this.cloneWithContent(lines.slice(fittedLineCount).join(" "), 0),
|
|
71
191
|
};
|
|
72
192
|
}
|
|
73
193
|
// Styled segments: each line's height comes from the fonts on it, so pack lines by cumulative
|
|
@@ -78,29 +198,40 @@ export class TextElement extends SizedPDFElement {
|
|
|
78
198
|
fontSize: this.fontSize,
|
|
79
199
|
fontStyle: this.fontStyle,
|
|
80
200
|
letterSpacing: this.letterSpacing,
|
|
201
|
+
direction: this.direction,
|
|
202
|
+
wordSpacing: this.wordSpacing,
|
|
203
|
+
textIndent: this.textIndent,
|
|
81
204
|
};
|
|
82
205
|
const lines = breakSegmentsIntoLines(content, defaults, width, ctx.metrics, this.maxLines, this.overflow);
|
|
83
206
|
let used = 0;
|
|
84
|
-
let
|
|
207
|
+
let packed = 0;
|
|
85
208
|
for (const line of lines) {
|
|
86
209
|
const lineBox = lineBoxForSegmentLine(line, defaults, ctx.metrics, this.lineHeight).height;
|
|
87
210
|
if (used + lineBox > maxHeight)
|
|
88
211
|
break;
|
|
89
212
|
used += lineBox;
|
|
90
|
-
|
|
213
|
+
packed++;
|
|
91
214
|
}
|
|
215
|
+
const fittedLineCount = adjustForOrphansWidows(packed, lines.length, this.breakRule());
|
|
92
216
|
if (fittedLineCount <= 0)
|
|
93
217
|
return { fitted: null, remainder: this };
|
|
94
218
|
if (fittedLineCount >= lines.length)
|
|
95
219
|
return { fitted: this, remainder: null };
|
|
96
220
|
return {
|
|
97
221
|
fitted: this.cloneWithContent(segmentLinesToSegments(lines.slice(0, fittedLineCount))),
|
|
98
|
-
remainder: this.cloneWithContent(segmentLinesToSegments(lines.slice(fittedLineCount))),
|
|
222
|
+
remainder: this.cloneWithContent(segmentLinesToSegments(lines.slice(fittedLineCount)), 0),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/** The orphan/widow minimums for this paragraph; unset means the CSS default of 2 lines each. */
|
|
226
|
+
breakRule() {
|
|
227
|
+
return {
|
|
228
|
+
orphans: this.orphans ?? DEFAULT_ORPHANS,
|
|
229
|
+
widows: this.widows ?? DEFAULT_WIDOWS,
|
|
99
230
|
};
|
|
100
231
|
}
|
|
101
232
|
// A copy carrying the same style but different (already-wrapped) content. Re-wrapping at
|
|
102
233
|
// the same width reproduces exactly those lines (greedy is deterministic).
|
|
103
|
-
cloneWithContent(content) {
|
|
234
|
+
cloneWithContent(content, indent = this.textIndent) {
|
|
104
235
|
return new TextElement({
|
|
105
236
|
content,
|
|
106
237
|
fontSize: this.fontSize,
|
|
@@ -109,12 +240,17 @@ export class TextElement extends SizedPDFElement {
|
|
|
109
240
|
color: this.color,
|
|
110
241
|
textAlignment: this.textAlignment,
|
|
111
242
|
maxLines: this.maxLines,
|
|
243
|
+
orphans: this.orphans,
|
|
244
|
+
widows: this.widows,
|
|
112
245
|
overflow: this.overflow,
|
|
113
246
|
lineHeight: this.lineHeight,
|
|
114
247
|
underline: this.underline,
|
|
115
248
|
strikethrough: this.strikethrough,
|
|
116
249
|
skipInk: this.skipInk,
|
|
117
250
|
letterSpacing: this.letterSpacing,
|
|
251
|
+
direction: this.direction,
|
|
252
|
+
wordSpacing: this.wordSpacing,
|
|
253
|
+
textIndent: indent,
|
|
118
254
|
role: this.role,
|
|
119
255
|
}).adoptStructId(this); // a wrapped remainder is the SAME logical paragraph (one P across pages)
|
|
120
256
|
}
|
|
@@ -125,11 +261,12 @@ export class TextElement extends SizedPDFElement {
|
|
|
125
261
|
// Bounded width (e.g. inside a Column) wraps to that width; an unbounded width
|
|
126
262
|
// (e.g. inside a Row) means the text takes its natural single-line width and does
|
|
127
263
|
// not wrap. Columns always bound the width, so this leaves their layout untouched.
|
|
264
|
+
this.resolved = this.display(ctx.metrics);
|
|
128
265
|
this.width = constraints.hasBoundedWidth
|
|
129
266
|
? constraints.maxWidth
|
|
130
267
|
: this.naturalWidth(ctx.metrics);
|
|
131
268
|
const wrapWidth = this.width ?? 0;
|
|
132
|
-
this.height = TextRenderer.calculateTextHeight(this.
|
|
269
|
+
this.height = TextRenderer.calculateTextHeight(this.resolved, this.fontSize, this.fontFamily, this.fontStyle, ctx.metrics, wrapWidth, this.maxLines, this.overflow, this.lineHeight, this.letterSpacing, this.lineOptions());
|
|
133
270
|
// Top-left coordinates (y = top of the text box). The baseline offset and the
|
|
134
271
|
// Y-flip are applied downstream (the line-builder positions baselines, the seam
|
|
135
272
|
// flips to PDF), so the element stays coordinate-system-blind.
|
|
@@ -144,24 +281,54 @@ export class TextElement extends SizedPDFElement {
|
|
|
144
281
|
* - enough to tip a borderline string (e.g. "20 Jun 2026", wider than "04 Jul 2026" only because
|
|
145
282
|
* 'n' beats 'l') one bit over its own width, dropping the last word onto a second line. */
|
|
146
283
|
naturalWidth(metrics) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
284
|
+
// The breaker's own sum, to the last bit - see `singleLineWidth`.
|
|
285
|
+
const oneLine = (text, family, size, style, letterSpacing) => singleLineWidth(text, { fontFamily: family, fontSize: size, fontStyle: style }, metrics, letterSpacing, this.wordSpacing);
|
|
286
|
+
// The FIRST line starts `textIndent` in, so an unbounded box has to be that much wider.
|
|
287
|
+
const indent = Math.max(0, this.textIndent);
|
|
288
|
+
const content = this.display(metrics);
|
|
289
|
+
if (typeof content === "string") {
|
|
290
|
+
return (indent +
|
|
291
|
+
oneLine(content, this.fontFamily, this.fontSize, this.fontStyle, this.letterSpacing));
|
|
292
|
+
}
|
|
293
|
+
return content.reduce((sum, seg) => sum +
|
|
294
|
+
oneLine(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontSize ?? this.fontSize, seg.fontStyle ?? this.fontStyle, seg.letterSpacing ?? this.letterSpacing), indent);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The widest piece of this text that cannot be broken - CSS's `min-content` width. A shrinking flex
|
|
298
|
+
* parent floors the width it hands us here, so a squeezed paragraph stops at its longest word
|
|
299
|
+
* instead of becoming one word per line.
|
|
300
|
+
*
|
|
301
|
+
* With `breakWord` or `hyphenate` on, the text may be cut mid-word, so the unbreakable piece is a
|
|
302
|
+
* single character - the same reasoning CSS applies to `word-break: break-all`.
|
|
303
|
+
*/
|
|
304
|
+
minIntrinsicMain(horizontal, ctx) {
|
|
305
|
+
// Only the width is a question here: the height of a text is whatever its lines add up to, and a
|
|
306
|
+
// column squeezing it vertically is the paginator's business, not the shrink pass's.
|
|
307
|
+
if (!horizontal)
|
|
308
|
+
return 0;
|
|
309
|
+
this.resolveStyle(ctx);
|
|
310
|
+
const metrics = ctx.metrics;
|
|
311
|
+
const widest = (text, family, size, style, letterSpacing) => {
|
|
312
|
+
// `ligatures` belongs to the FONT: it decides which glyphs are drawn and therefore how wide the
|
|
313
|
+
// piece is. Leaving it out here would measure a floor the drawing never matches.
|
|
314
|
+
const font = {
|
|
315
|
+
fontFamily: family,
|
|
316
|
+
fontSize: size,
|
|
317
|
+
fontStyle: style,
|
|
318
|
+
ligatures: this.ligatures,
|
|
319
|
+
};
|
|
320
|
+
// Split on the same boundaries the breaker uses - a hard break ends a line too.
|
|
321
|
+
const pieces = this.breakWord || this.hyphenate ? [...text] : text.split(/[ \n]/);
|
|
322
|
+
return pieces.reduce((max, piece) => Math.max(max, runAdvance(metrics, piece, font, letterSpacing)), 0);
|
|
159
323
|
};
|
|
160
|
-
|
|
161
|
-
|
|
324
|
+
const content = this.display(metrics);
|
|
325
|
+
if (typeof content === "string") {
|
|
326
|
+
return widest(content, this.fontFamily, this.fontSize, this.fontStyle, this.letterSpacing);
|
|
162
327
|
}
|
|
163
|
-
|
|
164
|
-
|
|
328
|
+
// Segments join without a space, so a word may straddle two of them; taking the widest piece per
|
|
329
|
+
// segment can therefore UNDER-estimate. That errs towards squeezing a little too far, which lands
|
|
330
|
+
// back on the old overflow behaviour rather than on something new.
|
|
331
|
+
return content.reduce((max, seg) => Math.max(max, widest(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontSize ?? this.fontSize, seg.fontStyle ?? this.fontStyle, seg.letterSpacing ?? this.letterSpacing)), 0);
|
|
165
332
|
}
|
|
166
333
|
getProps() {
|
|
167
334
|
return {
|
|
@@ -173,15 +340,23 @@ export class TextElement extends SizedPDFElement {
|
|
|
173
340
|
fontFamily: this.fontFamily,
|
|
174
341
|
fontStyle: this.fontStyle,
|
|
175
342
|
color: this.color,
|
|
176
|
-
content: this.content,
|
|
343
|
+
content: this.resolved ?? this.content,
|
|
177
344
|
textAlignment: this.textAlignment,
|
|
178
345
|
maxLines: this.maxLines,
|
|
346
|
+
orphans: this.orphans,
|
|
347
|
+
widows: this.widows,
|
|
179
348
|
overflow: this.overflow,
|
|
180
349
|
lineHeight: this.lineHeight,
|
|
181
350
|
underline: this.underline,
|
|
182
351
|
strikethrough: this.strikethrough,
|
|
183
352
|
skipInk: this.skipInk,
|
|
184
353
|
letterSpacing: this.letterSpacing,
|
|
354
|
+
direction: this.direction,
|
|
355
|
+
wordSpacing: this.wordSpacing,
|
|
356
|
+
textIndent: this.textIndent,
|
|
357
|
+
breakWord: this.breakWord,
|
|
358
|
+
ligatures: this.ligatures,
|
|
359
|
+
hyphenate: this.hyphenate,
|
|
185
360
|
role: this.role,
|
|
186
361
|
};
|
|
187
362
|
}
|