@jasy/pdf 1.0.0-alpha.8 → 1.0.0-alpha.9
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 +4 -3
- package/dist/api/content.d.ts +5 -4
- package/dist/api/content.js +13 -7
- package/dist/api/dimension.d.ts +56 -0
- package/dist/api/dimension.js +27 -0
- package/dist/api/font-url.d.ts +28 -0
- package/dist/api/font-url.js +134 -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 +5 -0
- package/dist/api/structure.js +16 -4
- package/dist/api/text.d.ts +42 -6
- package/dist/api/text.js +29 -5
- 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/container-element.d.ts +21 -1
- package/dist/elements/container-element.js +25 -10
- package/dist/elements/image-element.d.ts +18 -4
- package/dist/elements/image-element.js +27 -12
- package/dist/elements/layout/expanded-element.d.ts +3 -1
- package/dist/elements/layout/expanded-element.js +12 -3
- package/dist/elements/layout/padding-element.d.ts +4 -2
- package/dist/elements/layout/padding-element.js +4 -2
- package/dist/elements/layout/page-builder-element.js +2 -0
- package/dist/elements/pdf-element.d.ts +59 -1
- package/dist/elements/pdf-element.js +62 -1
- package/dist/elements/rectangle-element.d.ts +20 -6
- package/dist/elements/rectangle-element.js +25 -12
- package/dist/elements/row-element.d.ts +24 -1
- package/dist/elements/row-element.js +25 -10
- package/dist/elements/text-element.d.ts +60 -1
- package/dist/elements/text-element.js +132 -33
- package/dist/ir/display-list.d.ts +23 -6
- 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/renderer/image-renderer.js +2 -1
- package/dist/renderer/pdf-backend.d.ts +6 -0
- package/dist/renderer/pdf-backend.js +78 -31
- package/dist/renderer/pdf-document-renderer.js +8 -1
- package/dist/renderer/rectangle-renderer.js +8 -2
- package/dist/renderer/text-renderer.d.ts +2 -2
- package/dist/renderer/text-renderer.js +150 -42
- package/dist/text/advance.js +5 -2
- 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/line-breaker.d.ts +35 -1
- package/dist/text/line-breaker.js +76 -16
- 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 +67 -0
- package/dist/text/text-style.d.ts +25 -0
- package/dist/text/text-style.js +44 -1
- package/dist/utils/flex-layout.d.ts +24 -4
- package/dist/utils/flex-layout.js +169 -7
- package/dist/utils/font-metrics.d.ts +5 -0
- 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 +21 -0
- package/dist/utils/pdf-object-manager.js +107 -9
- 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/validators/element-validator.js +9 -2
- package/package.json +2 -1
|
@@ -1,6 +1,8 @@
|
|
|
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,6 +11,8 @@ export interface TextSegment {
|
|
|
9
11
|
fontStyle?: FontStyle;
|
|
10
12
|
fontColor?: Color;
|
|
11
13
|
fontFamily?: string;
|
|
14
|
+
/** The rest of the family stack for THIS run; unset inherits the Text's own. */
|
|
15
|
+
fontFallback?: string[];
|
|
12
16
|
fontSize?: number;
|
|
13
17
|
/** External URL: this segment becomes an inline hyperlink (a /Link annotation over its glyphs). */
|
|
14
18
|
href?: string;
|
|
@@ -19,7 +23,17 @@ export interface TextSegment {
|
|
|
19
23
|
strikethrough?: boolean;
|
|
20
24
|
/** Extra space after every glyph, in points; unset inherits the Text's own value. */
|
|
21
25
|
letterSpacing?: number;
|
|
26
|
+
/**
|
|
27
|
+
* CSS `vertical-align`, for a footnote marker or a formula index. Shifts THIS run's baseline;
|
|
28
|
+
* it does not resize the text, and it does not change the line's height - set `fontSize` yourself
|
|
29
|
+
* for the smaller look a browser's `<sup>` gets from its default stylesheet.
|
|
30
|
+
*/
|
|
31
|
+
verticalAlign?: VerticalTextAlign;
|
|
22
32
|
}
|
|
33
|
+
/** How far a run sits off the baseline, as a fraction of its font size (up is positive). */
|
|
34
|
+
export declare const VERTICAL_TEXT_SHIFT: Record<VerticalTextAlign, number>;
|
|
35
|
+
/** CSS `vertical-align`, the three values that mean something for a run of text. */
|
|
36
|
+
export type VerticalTextAlign = "baseline" | "super" | "sub";
|
|
23
37
|
/** Accessibility role for the tagged structure tree: a heading level or a paragraph (the default). */
|
|
24
38
|
export type TextRole = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p";
|
|
25
39
|
interface TextElementParams {
|
|
@@ -27,12 +41,18 @@ interface TextElementParams {
|
|
|
27
41
|
/** Unset (undefined) inherits the cascaded size; see ResolvedTextStyle. */
|
|
28
42
|
fontSize?: number;
|
|
29
43
|
fontFamily?: string;
|
|
44
|
+
/** The rest of the family stack, for code points `fontFamily` cannot draw. */
|
|
45
|
+
fontFallback?: string[];
|
|
30
46
|
fontStyle?: FontStyle;
|
|
31
47
|
content: string | TextSegment[];
|
|
32
48
|
color?: Color;
|
|
33
49
|
textAlignment?: HorizontalAlignment;
|
|
34
50
|
/** Cap the wrapped lines (default: unlimited / open-end). */
|
|
35
51
|
maxLines?: number;
|
|
52
|
+
/** Minimum lines that must stay behind at a page break (CSS `orphans`, default 2). */
|
|
53
|
+
orphans?: number;
|
|
54
|
+
/** Minimum lines that must carry over to the next page (CSS `widows`, default 2). */
|
|
55
|
+
widows?: number;
|
|
36
56
|
/** What to do past `maxLines`: `"clip"` (default) drops them, `"ellipsis"` ends with "…". */
|
|
37
57
|
overflow?: TextOverflow;
|
|
38
58
|
/** Line-height multiplier: each line is `fontSize * lineHeight` tall. Unset means the font's
|
|
@@ -46,6 +66,14 @@ interface TextElementParams {
|
|
|
46
66
|
skipInk?: boolean;
|
|
47
67
|
/** Extra space after every glyph, in points (CSS `letter-spacing`). Default 0. */
|
|
48
68
|
letterSpacing?: number;
|
|
69
|
+
/** Base writing direction (CSS `direction`); decides where a line starts. Default `"ltr"`. */
|
|
70
|
+
direction?: Direction;
|
|
71
|
+
/** CSS `text-transform`; applied before the text is measured or drawn. */
|
|
72
|
+
textTransform?: TextTransform;
|
|
73
|
+
/** CSS `word-spacing`, in points: extra advance at every space. */
|
|
74
|
+
wordSpacing?: number;
|
|
75
|
+
/** CSS `text-indent`, in points: how far the first line starts in. */
|
|
76
|
+
textIndent?: number;
|
|
49
77
|
/** Accessibility role for the tagged structure tree (heading level or paragraph; default `"p"`). */
|
|
50
78
|
role?: TextRole;
|
|
51
79
|
}
|
|
@@ -60,6 +88,11 @@ export declare class TextElement extends SizedPDFElement implements Fragmentable
|
|
|
60
88
|
private readonly rawStrikethrough?;
|
|
61
89
|
private readonly rawSkipInk?;
|
|
62
90
|
private readonly rawLetterSpacing?;
|
|
91
|
+
private readonly rawDirection?;
|
|
92
|
+
private readonly rawFontFallback?;
|
|
93
|
+
private readonly rawTextTransform?;
|
|
94
|
+
private readonly rawWordSpacing?;
|
|
95
|
+
private readonly rawTextIndent?;
|
|
63
96
|
private fontSize;
|
|
64
97
|
private fontFamily;
|
|
65
98
|
private fontStyle;
|
|
@@ -70,12 +103,31 @@ export declare class TextElement extends SizedPDFElement implements Fragmentable
|
|
|
70
103
|
private strikethrough;
|
|
71
104
|
private skipInk;
|
|
72
105
|
private letterSpacing;
|
|
106
|
+
private direction;
|
|
107
|
+
private fontFallback;
|
|
108
|
+
private resolved?;
|
|
109
|
+
private textTransform;
|
|
110
|
+
private wordSpacing;
|
|
111
|
+
private textIndent;
|
|
73
112
|
private content;
|
|
74
113
|
private maxLines?;
|
|
114
|
+
private orphans?;
|
|
115
|
+
private widows?;
|
|
75
116
|
private overflow;
|
|
76
117
|
private readonly role?;
|
|
77
|
-
constructor({ fontSize, content, fontFamily, fontStyle, color, textAlignment, maxLines, overflow, lineHeight, underline, strikethrough, skipInk, letterSpacing, role, }: TextElementParams);
|
|
118
|
+
constructor({ fontSize, content, fontFamily, fontStyle, color, textAlignment, maxLines, orphans, widows, overflow, lineHeight, underline, strikethrough, skipInk, letterSpacing, direction, fontFallback, textTransform, wordSpacing, textIndent, role, }: TextElementParams);
|
|
78
119
|
private resolveStyle;
|
|
120
|
+
/** Justified lines may be squeezed to keep a word; every other alignment gets no slack. Read by the
|
|
121
|
+
* breaker in BOTH passes, or a measured line and a drawn one would disagree about where it ends. */
|
|
122
|
+
/**
|
|
123
|
+
* The content as it is measured AND drawn - `text-transform` applied. One place, so a recased
|
|
124
|
+
* paragraph can never be measured in one casing and drawn in another (`ABC` and `abc` are not the
|
|
125
|
+
* same width in most fonts).
|
|
126
|
+
*/
|
|
127
|
+
private display;
|
|
128
|
+
/** The content with `text-transform` applied, and nothing else. */
|
|
129
|
+
private recased;
|
|
130
|
+
private lineOptions;
|
|
79
131
|
private applyStyle;
|
|
80
132
|
/**
|
|
81
133
|
* Splits the paragraph at line boxes (Slice 1). The lines that fit in `maxHeight` stay;
|
|
@@ -86,6 +138,8 @@ export declare class TextElement extends SizedPDFElement implements Fragmentable
|
|
|
86
138
|
fragment(maxHeight: number, width: number, ctx: LayoutContext): FragmentResult;
|
|
87
139
|
private fragmentString;
|
|
88
140
|
private fragmentSegments;
|
|
141
|
+
/** The orphan/widow minimums for this paragraph; unset means the CSS default of 2 lines each. */
|
|
142
|
+
private breakRule;
|
|
89
143
|
private cloneWithContent;
|
|
90
144
|
calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
|
|
91
145
|
/** The unwrapped single-line width of the content (used when width is unbounded, e.g. inside a Row).
|
|
@@ -109,12 +163,17 @@ export declare class TextElement extends SizedPDFElement implements Fragmentable
|
|
|
109
163
|
content: string | TextSegment[];
|
|
110
164
|
textAlignment: HorizontalAlignment;
|
|
111
165
|
maxLines: number | undefined;
|
|
166
|
+
orphans: number | undefined;
|
|
167
|
+
widows: number | undefined;
|
|
112
168
|
overflow: TextOverflow;
|
|
113
169
|
lineHeight: number | undefined;
|
|
114
170
|
underline: boolean;
|
|
115
171
|
strikethrough: boolean;
|
|
116
172
|
skipInk: boolean;
|
|
117
173
|
letterSpacing: number;
|
|
174
|
+
direction: Direction;
|
|
175
|
+
wordSpacing: number;
|
|
176
|
+
textIndent: number;
|
|
118
177
|
role: TextRole | undefined;
|
|
119
178
|
};
|
|
120
179
|
}
|
|
@@ -4,12 +4,31 @@
|
|
|
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 {
|
|
7
|
+
import { applyTextTransform } from "../text/text-style.js";
|
|
8
|
+
import { splitByFont } from "../text/font-fallback.js";
|
|
9
|
+
import { singleLineWidth, MAX_SPACE_SHRINK, wrapStringIntoLines, breakSegmentsIntoLines, segmentLinesToSegments, } from "../text/line-breaker.js";
|
|
10
|
+
import { adjustForOrphansWidows, DEFAULT_ORPHANS, DEFAULT_WIDOWS, } from "../text/orphans-widows.js";
|
|
8
11
|
import { lineBoxForSegmentLine, lineBoxForString } from "../text/line-metrics.js";
|
|
9
|
-
import {
|
|
10
|
-
|
|
12
|
+
import { HorizontalAlignment, SizedPDFElement } from "./pdf-element.js";
|
|
13
|
+
/** How far a run sits off the baseline, as a fraction of its font size (up is positive). */
|
|
14
|
+
export const VERTICAL_TEXT_SHIFT = {
|
|
15
|
+
baseline: 0,
|
|
16
|
+
super: 1 / 3,
|
|
17
|
+
sub: -1 / 5,
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* A measurement in points has to be a real number. A NaN or an Infinity would travel all the way into
|
|
21
|
+
* the content stream as a position - the backend refuses it there, but by then the message names a
|
|
22
|
+
* coordinate rather than the property that was wrong. Rejected here, where the caller can see it.
|
|
23
|
+
*/
|
|
24
|
+
function finitePoints(value, name) {
|
|
25
|
+
if (value !== undefined && !Number.isFinite(value)) {
|
|
26
|
+
throw new Error(`@jasy/pdf: Invalid ${name} ${value}: it must be a finite number of points.`);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
11
30
|
export class TextElement extends SizedPDFElement {
|
|
12
|
-
constructor({ fontSize, content, fontFamily, fontStyle, color, textAlignment, maxLines, overflow = "clip", lineHeight, underline, strikethrough, skipInk, letterSpacing, role, }) {
|
|
31
|
+
constructor({ fontSize, content, fontFamily, fontStyle, color, textAlignment, maxLines, orphans, widows, overflow = "clip", lineHeight, underline, strikethrough, skipInk, letterSpacing, direction, fontFallback, textTransform, wordSpacing, textIndent, role, }) {
|
|
13
32
|
super({ x: 0, y: 0 });
|
|
14
33
|
this.role = role;
|
|
15
34
|
this.rawFontSize = fontSize;
|
|
@@ -22,8 +41,15 @@ export class TextElement extends SizedPDFElement {
|
|
|
22
41
|
this.rawStrikethrough = strikethrough;
|
|
23
42
|
this.rawSkipInk = skipInk;
|
|
24
43
|
this.rawLetterSpacing = letterSpacing;
|
|
44
|
+
this.rawDirection = direction;
|
|
45
|
+
this.rawFontFallback = fontFallback;
|
|
46
|
+
this.rawTextTransform = textTransform;
|
|
47
|
+
this.rawWordSpacing = finitePoints(wordSpacing, "wordSpacing");
|
|
48
|
+
this.rawTextIndent = finitePoints(textIndent, "textIndent");
|
|
25
49
|
this.content = content;
|
|
26
50
|
this.maxLines = maxLines;
|
|
51
|
+
this.orphans = orphans;
|
|
52
|
+
this.widows = widows;
|
|
27
53
|
this.overflow = overflow;
|
|
28
54
|
this.applyStyle(DEFAULT_TEXT_STYLE);
|
|
29
55
|
}
|
|
@@ -31,6 +57,57 @@ export class TextElement extends SizedPDFElement {
|
|
|
31
57
|
resolveStyle(ctx) {
|
|
32
58
|
this.applyStyle(ctx.textStyle ?? DEFAULT_TEXT_STYLE);
|
|
33
59
|
}
|
|
60
|
+
/** Justified lines may be squeezed to keep a word; every other alignment gets no slack. Read by the
|
|
61
|
+
* breaker in BOTH passes, or a measured line and a drawn one would disagree about where it ends. */
|
|
62
|
+
/**
|
|
63
|
+
* The content as it is measured AND drawn - `text-transform` applied. One place, so a recased
|
|
64
|
+
* paragraph can never be measured in one casing and drawn in another (`ABC` and `abc` are not the
|
|
65
|
+
* same width in most fonts).
|
|
66
|
+
*/
|
|
67
|
+
display(metrics) {
|
|
68
|
+
const cased = this.recased();
|
|
69
|
+
// A SPAN may bring its own stack, so the element's being empty is not enough to skip the pass.
|
|
70
|
+
const anyStack = this.fontFallback.length > 0 ||
|
|
71
|
+
(typeof cased !== "string" && cased.some((seg) => (seg.fontFallback?.length ?? 0) > 0));
|
|
72
|
+
if (!anyStack || !metrics)
|
|
73
|
+
return cased;
|
|
74
|
+
// Font fallback turns the content into spans - one per family - which every later pass already
|
|
75
|
+
// knows how to handle. Nothing new downstream.
|
|
76
|
+
const pieces = typeof cased === "string" ? [{ content: cased }] : cased;
|
|
77
|
+
const out = [];
|
|
78
|
+
for (const seg of pieces) {
|
|
79
|
+
const runs = splitByFont(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontFallback ?? this.fontFallback, seg.fontStyle ?? this.fontStyle, metrics);
|
|
80
|
+
if (!runs)
|
|
81
|
+
out.push(seg);
|
|
82
|
+
else
|
|
83
|
+
for (const run of runs)
|
|
84
|
+
out.push({ ...seg, content: run.text, fontFamily: run.fontFamily });
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
/** The content with `text-transform` applied, and nothing else. */
|
|
89
|
+
recased() {
|
|
90
|
+
if (this.textTransform === "none")
|
|
91
|
+
return this.content;
|
|
92
|
+
if (typeof this.content === "string") {
|
|
93
|
+
return applyTextTransform(this.content, this.textTransform).text;
|
|
94
|
+
}
|
|
95
|
+
// The capitalisation state runs THROUGH the spans: `span("hel") + span("lo")` is one word.
|
|
96
|
+
let atWordStart = true;
|
|
97
|
+
return this.content.map((seg) => {
|
|
98
|
+
const done = applyTextTransform(seg.content, this.textTransform, atWordStart);
|
|
99
|
+
atWordStart = done.atWordStart;
|
|
100
|
+
return { ...seg, content: done.text };
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
lineOptions() {
|
|
104
|
+
return {
|
|
105
|
+
wordSpacing: this.wordSpacing,
|
|
106
|
+
indent: this.textIndent,
|
|
107
|
+
// Justified lines may be squeezed to keep a word; every other alignment gets no slack.
|
|
108
|
+
shrink: this.textAlignment === HorizontalAlignment.justify ? MAX_SPACE_SHRINK : 0,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
34
111
|
applyStyle(ts) {
|
|
35
112
|
this.fontSize = this.rawFontSize ?? ts.fontSize;
|
|
36
113
|
this.fontFamily = this.rawFontFamily ?? ts.fontFamily;
|
|
@@ -42,6 +119,11 @@ export class TextElement extends SizedPDFElement {
|
|
|
42
119
|
this.strikethrough = this.rawStrikethrough ?? ts.strikethrough;
|
|
43
120
|
this.skipInk = this.rawSkipInk ?? ts.skipInk;
|
|
44
121
|
this.letterSpacing = this.rawLetterSpacing ?? ts.letterSpacing;
|
|
122
|
+
this.direction = this.rawDirection ?? ts.direction;
|
|
123
|
+
this.fontFallback = this.rawFontFallback ?? ts.fontFallback;
|
|
124
|
+
this.textTransform = this.rawTextTransform ?? ts.textTransform;
|
|
125
|
+
this.wordSpacing = this.rawWordSpacing ?? ts.wordSpacing;
|
|
126
|
+
this.textIndent = this.rawTextIndent ?? ts.textIndent;
|
|
45
127
|
}
|
|
46
128
|
/**
|
|
47
129
|
* Splits the paragraph at line boxes (Slice 1). The lines that fit in `maxHeight` stay;
|
|
@@ -51,23 +133,25 @@ export class TextElement extends SizedPDFElement {
|
|
|
51
133
|
*/
|
|
52
134
|
fragment(maxHeight, width, ctx) {
|
|
53
135
|
this.resolveStyle(ctx);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
136
|
+
const content = this.display(ctx.metrics);
|
|
137
|
+
return typeof content === "string"
|
|
138
|
+
? this.fragmentString(content, maxHeight, width, ctx)
|
|
139
|
+
: this.fragmentSegments(content, maxHeight, width, ctx);
|
|
57
140
|
}
|
|
58
141
|
// Plain string: every wrapped line gets the same box (the same one calculateTextHeight uses),
|
|
59
142
|
// so `floor(maxHeight / lineBox)` lines fit.
|
|
60
143
|
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);
|
|
144
|
+
const lines = wrapStringIntoLines(content, this.fontFamily, this.fontSize, this.fontStyle, width, ctx.metrics, this.maxLines, this.overflow, this.letterSpacing, this.lineOptions());
|
|
62
145
|
const box = lineBoxForString(ctx.metrics, this.fontFamily, this.fontStyle, this.fontSize, this.lineHeight);
|
|
63
|
-
const fittedLineCount = Math.floor(maxHeight / box.height);
|
|
146
|
+
const fittedLineCount = adjustForOrphansWidows(Math.floor(maxHeight / box.height), lines.length, this.breakRule());
|
|
64
147
|
if (fittedLineCount <= 0)
|
|
65
148
|
return { fitted: null, remainder: this };
|
|
66
149
|
if (fittedLineCount >= lines.length)
|
|
67
150
|
return { fitted: this, remainder: null };
|
|
68
151
|
return {
|
|
69
152
|
fitted: this.cloneWithContent(lines.slice(0, fittedLineCount).join(" ")),
|
|
70
|
-
|
|
153
|
+
// A continuation is no longer the paragraph's FIRST line, so it carries no indent.
|
|
154
|
+
remainder: this.cloneWithContent(lines.slice(fittedLineCount).join(" "), 0),
|
|
71
155
|
};
|
|
72
156
|
}
|
|
73
157
|
// Styled segments: each line's height comes from the fonts on it, so pack lines by cumulative
|
|
@@ -78,29 +162,40 @@ export class TextElement extends SizedPDFElement {
|
|
|
78
162
|
fontSize: this.fontSize,
|
|
79
163
|
fontStyle: this.fontStyle,
|
|
80
164
|
letterSpacing: this.letterSpacing,
|
|
165
|
+
direction: this.direction,
|
|
166
|
+
wordSpacing: this.wordSpacing,
|
|
167
|
+
textIndent: this.textIndent,
|
|
81
168
|
};
|
|
82
169
|
const lines = breakSegmentsIntoLines(content, defaults, width, ctx.metrics, this.maxLines, this.overflow);
|
|
83
170
|
let used = 0;
|
|
84
|
-
let
|
|
171
|
+
let packed = 0;
|
|
85
172
|
for (const line of lines) {
|
|
86
173
|
const lineBox = lineBoxForSegmentLine(line, defaults, ctx.metrics, this.lineHeight).height;
|
|
87
174
|
if (used + lineBox > maxHeight)
|
|
88
175
|
break;
|
|
89
176
|
used += lineBox;
|
|
90
|
-
|
|
177
|
+
packed++;
|
|
91
178
|
}
|
|
179
|
+
const fittedLineCount = adjustForOrphansWidows(packed, lines.length, this.breakRule());
|
|
92
180
|
if (fittedLineCount <= 0)
|
|
93
181
|
return { fitted: null, remainder: this };
|
|
94
182
|
if (fittedLineCount >= lines.length)
|
|
95
183
|
return { fitted: this, remainder: null };
|
|
96
184
|
return {
|
|
97
185
|
fitted: this.cloneWithContent(segmentLinesToSegments(lines.slice(0, fittedLineCount))),
|
|
98
|
-
remainder: this.cloneWithContent(segmentLinesToSegments(lines.slice(fittedLineCount))),
|
|
186
|
+
remainder: this.cloneWithContent(segmentLinesToSegments(lines.slice(fittedLineCount)), 0),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/** The orphan/widow minimums for this paragraph; unset means the CSS default of 2 lines each. */
|
|
190
|
+
breakRule() {
|
|
191
|
+
return {
|
|
192
|
+
orphans: this.orphans ?? DEFAULT_ORPHANS,
|
|
193
|
+
widows: this.widows ?? DEFAULT_WIDOWS,
|
|
99
194
|
};
|
|
100
195
|
}
|
|
101
196
|
// A copy carrying the same style but different (already-wrapped) content. Re-wrapping at
|
|
102
197
|
// the same width reproduces exactly those lines (greedy is deterministic).
|
|
103
|
-
cloneWithContent(content) {
|
|
198
|
+
cloneWithContent(content, indent = this.textIndent) {
|
|
104
199
|
return new TextElement({
|
|
105
200
|
content,
|
|
106
201
|
fontSize: this.fontSize,
|
|
@@ -109,12 +204,17 @@ export class TextElement extends SizedPDFElement {
|
|
|
109
204
|
color: this.color,
|
|
110
205
|
textAlignment: this.textAlignment,
|
|
111
206
|
maxLines: this.maxLines,
|
|
207
|
+
orphans: this.orphans,
|
|
208
|
+
widows: this.widows,
|
|
112
209
|
overflow: this.overflow,
|
|
113
210
|
lineHeight: this.lineHeight,
|
|
114
211
|
underline: this.underline,
|
|
115
212
|
strikethrough: this.strikethrough,
|
|
116
213
|
skipInk: this.skipInk,
|
|
117
214
|
letterSpacing: this.letterSpacing,
|
|
215
|
+
direction: this.direction,
|
|
216
|
+
wordSpacing: this.wordSpacing,
|
|
217
|
+
textIndent: indent,
|
|
118
218
|
role: this.role,
|
|
119
219
|
}).adoptStructId(this); // a wrapped remainder is the SAME logical paragraph (one P across pages)
|
|
120
220
|
}
|
|
@@ -125,11 +225,12 @@ export class TextElement extends SizedPDFElement {
|
|
|
125
225
|
// Bounded width (e.g. inside a Column) wraps to that width; an unbounded width
|
|
126
226
|
// (e.g. inside a Row) means the text takes its natural single-line width and does
|
|
127
227
|
// not wrap. Columns always bound the width, so this leaves their layout untouched.
|
|
228
|
+
this.resolved = this.display(ctx.metrics);
|
|
128
229
|
this.width = constraints.hasBoundedWidth
|
|
129
230
|
? constraints.maxWidth
|
|
130
231
|
: this.naturalWidth(ctx.metrics);
|
|
131
232
|
const wrapWidth = this.width ?? 0;
|
|
132
|
-
this.height = TextRenderer.calculateTextHeight(this.
|
|
233
|
+
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
234
|
// Top-left coordinates (y = top of the text box). The baseline offset and the
|
|
134
235
|
// Y-flip are applied downstream (the line-builder positions baselines, the seam
|
|
135
236
|
// flips to PDF), so the element stays coordinate-system-blind.
|
|
@@ -144,24 +245,17 @@ export class TextElement extends SizedPDFElement {
|
|
|
144
245
|
* - enough to tip a borderline string (e.g. "20 Jun 2026", wider than "04 Jul 2026" only because
|
|
145
246
|
* 'n' beats 'l') one bit over its own width, dropping the last word onto a second line. */
|
|
146
247
|
naturalWidth(metrics) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// the same `runAdvance`, so the two agree bit for bit even with letterSpacing.
|
|
156
|
-
width += i < words.length - 1 ? w + space : w;
|
|
157
|
-
});
|
|
158
|
-
return width;
|
|
159
|
-
};
|
|
160
|
-
if (typeof this.content === "string") {
|
|
161
|
-
return oneLine(this.content, this.fontFamily, this.fontSize, this.fontStyle, this.letterSpacing);
|
|
248
|
+
// The breaker's own sum, to the last bit - see `singleLineWidth`.
|
|
249
|
+
const oneLine = (text, family, size, style, letterSpacing) => singleLineWidth(text, { fontFamily: family, fontSize: size, fontStyle: style }, metrics, letterSpacing, this.wordSpacing);
|
|
250
|
+
// The FIRST line starts `textIndent` in, so an unbounded box has to be that much wider.
|
|
251
|
+
const indent = Math.max(0, this.textIndent);
|
|
252
|
+
const content = this.display(metrics);
|
|
253
|
+
if (typeof content === "string") {
|
|
254
|
+
return (indent +
|
|
255
|
+
oneLine(content, this.fontFamily, this.fontSize, this.fontStyle, this.letterSpacing));
|
|
162
256
|
}
|
|
163
|
-
return
|
|
164
|
-
oneLine(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontSize ?? this.fontSize, seg.fontStyle ?? this.fontStyle, seg.letterSpacing ?? this.letterSpacing),
|
|
257
|
+
return content.reduce((sum, seg) => sum +
|
|
258
|
+
oneLine(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontSize ?? this.fontSize, seg.fontStyle ?? this.fontStyle, seg.letterSpacing ?? this.letterSpacing), indent);
|
|
165
259
|
}
|
|
166
260
|
getProps() {
|
|
167
261
|
return {
|
|
@@ -173,15 +267,20 @@ export class TextElement extends SizedPDFElement {
|
|
|
173
267
|
fontFamily: this.fontFamily,
|
|
174
268
|
fontStyle: this.fontStyle,
|
|
175
269
|
color: this.color,
|
|
176
|
-
content: this.content,
|
|
270
|
+
content: this.resolved ?? this.content,
|
|
177
271
|
textAlignment: this.textAlignment,
|
|
178
272
|
maxLines: this.maxLines,
|
|
273
|
+
orphans: this.orphans,
|
|
274
|
+
widows: this.widows,
|
|
179
275
|
overflow: this.overflow,
|
|
180
276
|
lineHeight: this.lineHeight,
|
|
181
277
|
underline: this.underline,
|
|
182
278
|
strikethrough: this.strikethrough,
|
|
183
279
|
skipInk: this.skipInk,
|
|
184
280
|
letterSpacing: this.letterSpacing,
|
|
281
|
+
direction: this.direction,
|
|
282
|
+
wordSpacing: this.wordSpacing,
|
|
283
|
+
textIndent: this.textIndent,
|
|
185
284
|
role: this.role,
|
|
186
285
|
};
|
|
187
286
|
}
|
|
@@ -32,6 +32,9 @@ export interface TextRun {
|
|
|
32
32
|
x: number;
|
|
33
33
|
y: number;
|
|
34
34
|
text: string;
|
|
35
|
+
/** The glyphs to draw, when the run was SHAPED. The backend emits these instead of encoding `text`,
|
|
36
|
+
* which stays logical for `ToUnicode` and the accessible tree. */
|
|
37
|
+
glyphs?: number[];
|
|
35
38
|
fontFamily: string;
|
|
36
39
|
fontStyle: FontStyle;
|
|
37
40
|
fontSize: number;
|
|
@@ -40,17 +43,31 @@ export interface TextRun {
|
|
|
40
43
|
tag?: StructTag;
|
|
41
44
|
}
|
|
42
45
|
/** An axis-aligned rectangle. An absent `fill` or `stroke` means that part is not drawn. */
|
|
46
|
+
/**
|
|
47
|
+
* Corner radii in points (CSS `border-radius`). Any corner left out falls back to 0. A single number
|
|
48
|
+
* everywhere is the common case and stays a plain `number` on the nodes, so nothing changed for a
|
|
49
|
+
* document that never asks for different corners.
|
|
50
|
+
*/
|
|
51
|
+
export interface Radii {
|
|
52
|
+
tl?: number;
|
|
53
|
+
tr?: number;
|
|
54
|
+
br?: number;
|
|
55
|
+
bl?: number;
|
|
56
|
+
}
|
|
57
|
+
/** True when any corner asks for a radius - the gate between a plain rect and a curved path. */
|
|
58
|
+
export declare function isRounded(radius: number | Radii | undefined): boolean;
|
|
43
59
|
export interface Rect {
|
|
44
60
|
type: "rect";
|
|
45
61
|
x: number;
|
|
46
62
|
y: number;
|
|
47
63
|
width: number;
|
|
48
64
|
height: number;
|
|
49
|
-
|
|
65
|
+
/** Solid colour, or a gradient whose anchor points are in absolute page coordinates. */
|
|
66
|
+
fill?: Color | Gradient;
|
|
50
67
|
stroke?: Color;
|
|
51
68
|
strokeWidth: number;
|
|
52
|
-
/** Corner radius in points
|
|
53
|
-
radius?: number;
|
|
69
|
+
/** Corner radius in points - one number for all four, or per corner. Absent/0 = sharp (plain `re`). */
|
|
70
|
+
radius?: number | Radii;
|
|
54
71
|
tag?: StructTag;
|
|
55
72
|
}
|
|
56
73
|
/** A straight line segment between two points. */
|
|
@@ -84,8 +101,8 @@ export interface Image {
|
|
|
84
101
|
width: number;
|
|
85
102
|
height: number;
|
|
86
103
|
};
|
|
87
|
-
/** Corner radius in points for the image box; absent/0 = sharp
|
|
88
|
-
radius?: number;
|
|
104
|
+
/** Corner radius in points for the image box - one number or per corner; absent/0 = sharp. */
|
|
105
|
+
radius?: number | Radii;
|
|
89
106
|
tag?: StructTag;
|
|
90
107
|
}
|
|
91
108
|
/**
|
|
@@ -99,7 +116,7 @@ export interface ClipPush {
|
|
|
99
116
|
y: number;
|
|
100
117
|
width: number;
|
|
101
118
|
height: number;
|
|
102
|
-
radius?: number;
|
|
119
|
+
radius?: number | Radii;
|
|
103
120
|
}
|
|
104
121
|
/** Closes the most recent `ClipPush` (restores the graphics state). */
|
|
105
122
|
export interface ClipPop {
|
package/dist/ir/display-list.js
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/** True when any corner asks for a radius - the gate between a plain rect and a curved path. */
|
|
2
|
+
export function isRounded(radius) {
|
|
3
|
+
if (radius === undefined)
|
|
4
|
+
return false;
|
|
5
|
+
if (typeof radius === "number")
|
|
6
|
+
return radius > 0;
|
|
7
|
+
return ((radius.tl ?? 0) > 0 || (radius.tr ?? 0) > 0 || (radius.br ?? 0) > 0 || (radius.bl ?? 0) > 0);
|
|
8
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flex alignment vocabulary. It lives here rather than in `utils/flex-layout.ts` because the base
|
|
3
|
+
* `PDFElement` needs `CrossAlign` for `alignSelf`, and flex-layout imports the elements - defining it
|
|
4
|
+
* there would close a cycle.
|
|
5
|
+
*/
|
|
6
|
+
/** Distribution along the MAIN axis (CSS `justify-content`). */
|
|
7
|
+
export type MainAlign = "start" | "center" | "end" | "between" | "around";
|
|
8
|
+
/** Position/size ACROSS the axis (CSS `align-items` / `align-self`). `stretch` fills the cross extent;
|
|
9
|
+
* the others place the child at its natural cross size. */
|
|
10
|
+
export type CrossAlign = "start" | "center" | "end" | "stretch";
|
|
@@ -41,6 +41,12 @@ export declare class BoxConstraints {
|
|
|
41
41
|
* what a padding/border element hands its child after reserving its own insets.
|
|
42
42
|
*/
|
|
43
43
|
deflate(horizontal: number, vertical: number): BoxConstraints;
|
|
44
|
+
/**
|
|
45
|
+
* Tightens this box by an element's own `min`/`max` on either axis. A max never widens the offered
|
|
46
|
+
* room, and a min may push past it - `minHeight: 500` in a 300pt region means 500, and pagination
|
|
47
|
+
* deals with the overflow, the same answer CSS gives.
|
|
48
|
+
*/
|
|
49
|
+
narrow(minWidth?: number, maxWidth?: number, minHeight?: number, maxHeight?: number): BoxConstraints;
|
|
44
50
|
/** Returns the constraints clamped to lie within `parent` (Flutter's enforce). */
|
|
45
51
|
enforce(parent: BoxConstraints): BoxConstraints;
|
|
46
52
|
}
|
|
@@ -52,6 +58,53 @@ export declare class BoxConstraints {
|
|
|
52
58
|
* Shared by every sized element so `width: "50%"` behaves identically across Box / Column / Row / Image.
|
|
53
59
|
*/
|
|
54
60
|
export declare function resolveExtent(fixed: number | undefined, factor: number | undefined, boundedMax: number, hasBounded: boolean): number | undefined;
|
|
61
|
+
/** What one axis of a sized element was asked for: a size, plus the bounds it must stay inside. */
|
|
62
|
+
export interface ExtentSpec {
|
|
63
|
+
fixed?: number;
|
|
64
|
+
factor?: number;
|
|
65
|
+
min?: number;
|
|
66
|
+
minFactor?: number;
|
|
67
|
+
max?: number;
|
|
68
|
+
maxFactor?: number;
|
|
69
|
+
}
|
|
70
|
+
/** The sizing options every sized element accepts. Each `*Factor` is the percentage form of its pair. */
|
|
71
|
+
export interface SizingParams {
|
|
72
|
+
width?: number;
|
|
73
|
+
height?: number;
|
|
74
|
+
widthFactor?: number;
|
|
75
|
+
heightFactor?: number;
|
|
76
|
+
minWidth?: number;
|
|
77
|
+
maxWidth?: number;
|
|
78
|
+
minHeight?: number;
|
|
79
|
+
maxHeight?: number;
|
|
80
|
+
minWidthFactor?: number;
|
|
81
|
+
maxWidthFactor?: number;
|
|
82
|
+
minHeightFactor?: number;
|
|
83
|
+
maxHeightFactor?: number;
|
|
84
|
+
/** width / height. Derives whichever axis is left open; see `resolveSize`. */
|
|
85
|
+
aspectRatio?: number;
|
|
86
|
+
}
|
|
87
|
+
/** Split `SizingParams` into the per-axis form `resolveSize` takes. */
|
|
88
|
+
export declare function extentSpecs(p: SizingParams): {
|
|
89
|
+
width: ExtentSpec;
|
|
90
|
+
height: ExtentSpec;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Resolve both axes of a sized element: relative sizing, then `aspectRatio`, then `min`/`max`.
|
|
94
|
+
*
|
|
95
|
+
* The order follows CSS: a ratio fills in the axis you left open, and min/max clamp afterwards - so an
|
|
96
|
+
* explicit bound wins over the ratio, exactly as `min-height` beats `aspect-ratio` in a browser.
|
|
97
|
+
*
|
|
98
|
+
* `min`/`max` also come back as NARROWED constraints, because an axis with no explicit size still has
|
|
99
|
+
* to obey them. `maxWidth` alone means "fill, but no wider than this", and the caller's own fill and
|
|
100
|
+
* shrink-wrap paths get that for free by using the returned constraints instead of the ones passed in.
|
|
101
|
+
* A `width` of `undefined` keeps its usual meaning: fill or shrink-wrap.
|
|
102
|
+
*/
|
|
103
|
+
export declare function resolveSize(width: ExtentSpec, height: ExtentSpec, aspectRatio: number | undefined, constraints: BoxConstraints): {
|
|
104
|
+
width?: number;
|
|
105
|
+
height?: number;
|
|
106
|
+
constraints: BoxConstraints;
|
|
107
|
+
};
|
|
55
108
|
/** The size an element resolves to and returns UP the tree. */
|
|
56
109
|
export interface Size {
|
|
57
110
|
width: number;
|