@jasy/pdf 1.0.0-alpha.6 → 1.0.0-alpha.7
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/dist/api/descriptor.js +3 -1
- package/dist/api/layout.d.ts +47 -14
- package/dist/api/layout.js +47 -11
- package/dist/api/structure.d.ts +5 -0
- package/dist/api/structure.js +1 -0
- package/dist/api/text.d.ts +18 -2
- package/dist/api/text.js +15 -0
- package/dist/elements/container-element.d.ts +10 -1
- package/dist/elements/container-element.js +26 -8
- package/dist/elements/index.d.ts +2 -0
- package/dist/elements/index.js +2 -0
- package/dist/elements/layout/keep-together-element.d.ts +42 -0
- package/dist/elements/layout/keep-together-element.js +85 -0
- package/dist/elements/layout/padding-element.d.ts +3 -0
- package/dist/elements/layout/padding-element.js +12 -0
- package/dist/elements/layout/page-break-element.d.ts +17 -0
- package/dist/elements/layout/page-break-element.js +24 -0
- package/dist/elements/layout/positioned-element.d.ts +10 -7
- package/dist/elements/layout/positioned-element.js +21 -9
- package/dist/elements/page-element.d.ts +11 -1
- package/dist/elements/page-element.js +41 -16
- package/dist/elements/pdf-element.d.ts +28 -0
- package/dist/elements/pdf-element.js +32 -0
- package/dist/elements/rectangle-element.d.ts +10 -1
- package/dist/elements/rectangle-element.js +27 -6
- package/dist/elements/row-element.d.ts +9 -1
- package/dist/elements/row-element.js +9 -1
- package/dist/elements/text-element.d.ts +30 -4
- package/dist/elements/text-element.js +40 -17
- package/dist/ir/display-list.d.ts +1 -0
- package/dist/layout/fragmentation.d.ts +12 -0
- package/dist/layout/fragmentation.js +45 -8
- package/dist/renderer/keep-together-renderer.d.ts +10 -0
- package/dist/renderer/keep-together-renderer.js +12 -0
- package/dist/renderer/page-break-renderer.d.ts +14 -0
- package/dist/renderer/page-break-renderer.js +15 -0
- package/dist/renderer/pdf-backend.d.ts +7 -0
- package/dist/renderer/pdf-backend.js +40 -6
- package/dist/renderer/pdf-document-class.js +3 -0
- package/dist/renderer/pdf-document-renderer.js +26 -7
- package/dist/renderer/pdf-renderer.js +6 -0
- package/dist/renderer/text-renderer.d.ts +1 -1
- package/dist/renderer/text-renderer.js +107 -59
- package/dist/text/advance.d.ts +34 -0
- package/dist/text/advance.js +42 -0
- package/dist/text/line-breaker.d.ts +9 -8
- package/dist/text/line-breaker.js +28 -21
- package/dist/text/line-metrics.d.ts +49 -0
- package/dist/text/line-metrics.js +57 -0
- package/dist/text/text-decoration.d.ts +50 -0
- package/dist/text/text-decoration.js +62 -0
- package/dist/text/text-style.d.ts +13 -3
- package/dist/text/text-style.js +10 -3
- package/dist/utils/afm-parser.d.ts +34 -0
- package/dist/utils/afm-parser.js +67 -0
- package/dist/utils/font-metrics.d.ts +14 -0
- package/dist/utils/pdf-object-manager.d.ts +27 -1
- package/dist/utils/pdf-object-manager.js +134 -20
- package/dist/utils/ttf-parser.d.ts +53 -0
- package/dist/utils/ttf-parser.js +368 -0
- package/package.json +1 -1
package/dist/api/descriptor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Text, Paragraph, span } from "./text.js";
|
|
2
|
-
import { Column, Row, Box, Padding, Spacer, Expanded, Positioned, Link, Anchor, Bookmark, Rotated, RotatedBox, } from "./layout.js";
|
|
2
|
+
import { Column, Row, Box, Padding, Spacer, PageBreak, keepTogether, Expanded, Positioned, Link, Anchor, Bookmark, Rotated, RotatedBox, } from "./layout.js";
|
|
3
3
|
import { Divider, Image } from "./content.js";
|
|
4
4
|
import { Page, Document, DefaultTextStyle } from "./structure.js";
|
|
5
5
|
import { PageNumber, PageCount } from "./page-builder.js";
|
|
@@ -85,6 +85,8 @@ const REGISTRY = {
|
|
|
85
85
|
padding: (props, children) => Padding(props.insets ?? 0, elementChildren(children)[0]),
|
|
86
86
|
expanded: (props, children) => Expanded(props, elementChildren(children)[0]),
|
|
87
87
|
spacer: (props) => Spacer(props?.flex),
|
|
88
|
+
"page-break": () => PageBreak(),
|
|
89
|
+
"keep-together": (_props, children) => keepTogether(elementChildren(children)),
|
|
88
90
|
divider: (props) => Divider(props),
|
|
89
91
|
image: (props) => Image(props.src, props),
|
|
90
92
|
text: (props, children) => Text(textContent(children), props),
|
package/dist/api/layout.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { ContainerElement } from "../elements/container-element.js";
|
|
2
|
-
import { RowElement } from "../elements/row-element.js";
|
|
3
|
-
import { RectangleElement } from "../elements/rectangle-element.js";
|
|
4
1
|
import { ExpandedElement } from "../elements/layout/expanded-element.js";
|
|
5
2
|
import { PaddingElement } from "../elements/layout/padding-element.js";
|
|
3
|
+
import { PageBreakElement } from "../elements/layout/page-break-element.js";
|
|
4
|
+
import { KeepTogetherElement } from "../elements/layout/keep-together-element.js";
|
|
6
5
|
import { PositionedElement, PositionedInsets } from "../elements/layout/positioned-element.js";
|
|
7
6
|
import { LinkElement } from "../elements/layout/link-element.js";
|
|
8
7
|
import { BookmarkElement } from "../elements/layout/bookmark-element.js";
|
|
@@ -28,13 +27,28 @@ export interface StackOptions {
|
|
|
28
27
|
* axis (`height: "50%"` only resolves where the parent bounds the height). */
|
|
29
28
|
width?: SizeInput;
|
|
30
29
|
height?: SizeInput;
|
|
30
|
+
/** Start this stack on a fresh page (CSS `break-before: page`, react-pdf's `break`). Ignored at the
|
|
31
|
+
* top of a page. */
|
|
32
|
+
breakBefore?: boolean;
|
|
33
|
+
/** Start everything after this stack on a fresh page (CSS `break-after: page`). */
|
|
34
|
+
breakAfter?: boolean;
|
|
35
|
+
/** Try to keep this stack on one page (CSS `break-inside: avoid`). If it does not fit here but would
|
|
36
|
+
* fit on a fresh page, it moves there whole; if it is taller than a whole page, it splits anyway. */
|
|
37
|
+
keepTogether?: boolean;
|
|
31
38
|
}
|
|
32
39
|
/** A vertical stack. `Column(children)` or `Column(opts, children)`. */
|
|
33
|
-
export declare function Column(children: PDFElement[]):
|
|
34
|
-
export declare function Column(opts: StackOptions, children: PDFElement[]):
|
|
40
|
+
export declare function Column(children: PDFElement[]): PDFElement;
|
|
41
|
+
export declare function Column(opts: StackOptions, children: PDFElement[]): PDFElement;
|
|
35
42
|
/** A horizontal stack. `Row(children)` or `Row(opts, children)`. */
|
|
36
|
-
export declare function Row(children: PDFElement[]):
|
|
37
|
-
export declare function Row(opts: StackOptions, children: PDFElement[]):
|
|
43
|
+
export declare function Row(children: PDFElement[]): PDFElement;
|
|
44
|
+
export declare function Row(opts: StackOptions, children: PDFElement[]): PDFElement;
|
|
45
|
+
/**
|
|
46
|
+
* Keep a group of elements on ONE page (CSS `break-inside: avoid`). `keepTogether([...])` stacks the
|
|
47
|
+
* children in a `Column` and refuses to split them across a page break - unless they do not fit on a
|
|
48
|
+
* fresh page at all, in which case they split anyway (so pagination always terminates). Equivalent to
|
|
49
|
+
* the `keepTogether` prop on `Box`/`Column`/`Row`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function keepTogether(children: PDFElement[]): KeepTogetherElement;
|
|
38
52
|
/** A bordered / filled box that wraps its children (locked §4). */
|
|
39
53
|
export interface BoxOptions {
|
|
40
54
|
/** Border (stroke) color. A box has a border only when `border` or `borderWidth` is set. */
|
|
@@ -65,22 +79,35 @@ export interface BoxOptions {
|
|
|
65
79
|
/** `"hidden"` crops children to the box (rounded corners included); `"visible"` (default) lets a
|
|
66
80
|
* `Positioned` child spill over the edge. */
|
|
67
81
|
overflow?: "hidden" | "visible";
|
|
82
|
+
/** Start this box on a fresh page (CSS `break-before: page`, react-pdf's `break`). Ignored at the top
|
|
83
|
+
* of a page. */
|
|
84
|
+
breakBefore?: boolean;
|
|
85
|
+
/** Start everything after this box on a fresh page (CSS `break-after: page`). */
|
|
86
|
+
breakAfter?: boolean;
|
|
87
|
+
/** Try to keep this box on one page (CSS `break-inside: avoid`). Moves whole to a fresh page if it
|
|
88
|
+
* would fit there; splits anyway if it is taller than a whole page. */
|
|
89
|
+
keepTogether?: boolean;
|
|
68
90
|
}
|
|
69
91
|
/**
|
|
70
92
|
* A box: maps to a `RectangleElement` (fill + border + radius) whose children are stacked
|
|
71
93
|
* inside it, optionally inset by `padding` (a `PaddingElement` wrapping a `Column` of the
|
|
72
94
|
* children). With no `border`/`borderWidth` the box has no outline (`borderWidth` 0).
|
|
73
95
|
*/
|
|
74
|
-
export declare function Box(children: PDFElement[]):
|
|
75
|
-
export declare function Box(opts: BoxOptions, children: PDFElement[]):
|
|
96
|
+
export declare function Box(children: PDFElement[]): PDFElement;
|
|
97
|
+
export declare function Box(opts: BoxOptions, children: PDFElement[]): PDFElement;
|
|
76
98
|
/** Insets a single child by `padding` (a number / `{x,y}` / `{top,…}` / 4-tuple). */
|
|
77
99
|
export declare function Padding(padding: Insets, child: PDFElement): PaddingElement;
|
|
78
100
|
/**
|
|
79
|
-
* Places a child OUT OF FLOW, relative to the nearest enclosing `relative` Box
|
|
80
|
-
* axis: pin to EDGES - `Positioned({ top, left, right,
|
|
81
|
-
* pokes into / out of the corner (a badge, a tab, a
|
|
82
|
-
* ANCHOR + nudge - `Positioned({ h: "center", v: "end",
|
|
83
|
-
* end-aligned with a pixel offset. An edge wins over an
|
|
101
|
+
* Places a child OUT OF FLOW, relative to the nearest enclosing `relative` Box - or, with none, to
|
|
102
|
+
* the page's content box. Two ways, pick per axis: pin to EDGES - `Positioned({ top, left, right,
|
|
103
|
+
* bottom }, child)`, where a negative value pokes into / out of the corner (a badge, a tab, a
|
|
104
|
+
* ribbon) and pinning both sides stretches; or ANCHOR + nudge - `Positioned({ h: "center", v: "end",
|
|
105
|
+
* x: -10, y: -8 }, child)`, i.e. centered / end-aligned with a pixel offset. An edge wins over an
|
|
106
|
+
* anchor on the same axis.
|
|
107
|
+
*
|
|
108
|
+
* A `Positioned` in a `Page`'s `header` or `footer` repeats on EVERY page and takes no space in the
|
|
109
|
+
* band, which is how you draw a watermark, a draft stamp or a corner ribbon across a document. It
|
|
110
|
+
* anchors to the page, not to the band, so `bottom: 0` is the foot of the page.
|
|
84
111
|
*/
|
|
85
112
|
export declare function Positioned(opts: PositionedInsets, child: PDFElement): PositionedElement;
|
|
86
113
|
/**
|
|
@@ -137,6 +164,12 @@ export declare function RotatedBox(opts: {
|
|
|
137
164
|
* and `b` to the edges. `flex` weights it against other flex children (default 1).
|
|
138
165
|
*/
|
|
139
166
|
export declare function Spacer(flex?: number): ExpandedElement;
|
|
167
|
+
/**
|
|
168
|
+
* A forced page break. Put it in a `Column` (or any vertical flow) and everything AFTER it starts on
|
|
169
|
+
* a fresh page, no matter how much space is left: `Column([intro, PageBreak(), chapterTwo])`. It takes
|
|
170
|
+
* no space and draws nothing. Works nested, and a break wins over `keepTogether`.
|
|
171
|
+
*/
|
|
172
|
+
export declare function PageBreak(): PageBreakElement;
|
|
140
173
|
export interface ExpandedOptions {
|
|
141
174
|
/** Share of the leftover space vs other flex children (default 1). */
|
|
142
175
|
flex?: number;
|
package/dist/api/layout.js
CHANGED
|
@@ -3,6 +3,8 @@ import { RowElement } from "../elements/row-element.js";
|
|
|
3
3
|
import { RectangleElement } from "../elements/rectangle-element.js";
|
|
4
4
|
import { ExpandedElement } from "../elements/layout/expanded-element.js";
|
|
5
5
|
import { PaddingElement } from "../elements/layout/padding-element.js";
|
|
6
|
+
import { PageBreakElement } from "../elements/layout/page-break-element.js";
|
|
7
|
+
import { KeepTogetherElement } from "../elements/layout/keep-together-element.js";
|
|
6
8
|
import { PositionedElement } from "../elements/layout/positioned-element.js";
|
|
7
9
|
import { LinkElement } from "../elements/layout/link-element.js";
|
|
8
10
|
import { BookmarkElement } from "../elements/layout/bookmark-element.js";
|
|
@@ -24,25 +26,44 @@ function stackSize(opts) {
|
|
|
24
26
|
const DEFAULT_CROSS = "start";
|
|
25
27
|
export function Column(a, b) {
|
|
26
28
|
const { opts, children } = splitArgs(a, b);
|
|
27
|
-
return new ContainerElement({
|
|
29
|
+
return maybeKeepTogether(opts, new ContainerElement({
|
|
28
30
|
x: 0,
|
|
29
31
|
y: 0,
|
|
30
32
|
children,
|
|
31
33
|
gap: opts.gap,
|
|
32
34
|
main: opts.justify, // undefined → engine default `start` (matches §5)
|
|
33
35
|
cross: opts.align ?? DEFAULT_CROSS,
|
|
36
|
+
breakBefore: opts.breakBefore,
|
|
37
|
+
breakAfter: opts.breakAfter,
|
|
34
38
|
...stackSize(opts),
|
|
35
|
-
});
|
|
39
|
+
}));
|
|
36
40
|
}
|
|
37
41
|
export function Row(a, b) {
|
|
38
42
|
const { opts, children } = splitArgs(a, b);
|
|
39
|
-
return new RowElement({
|
|
43
|
+
return maybeKeepTogether(opts, new RowElement({
|
|
40
44
|
children,
|
|
41
45
|
gap: opts.gap,
|
|
42
46
|
main: opts.justify,
|
|
43
47
|
cross: opts.align ?? DEFAULT_CROSS,
|
|
48
|
+
breakBefore: opts.breakBefore,
|
|
49
|
+
breakAfter: opts.breakAfter,
|
|
44
50
|
...stackSize(opts),
|
|
45
|
-
});
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
/** Wraps `element` in a `keepTogether` group when the `keepTogether` option is set; otherwise returns it
|
|
54
|
+
* unchanged (byte-identical). Shared by the `Box`/`Column`/`Row` prop shortcut. */
|
|
55
|
+
function maybeKeepTogether(opts, element) {
|
|
56
|
+
return opts.keepTogether ? new KeepTogetherElement({ child: element }) : element;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Keep a group of elements on ONE page (CSS `break-inside: avoid`). `keepTogether([...])` stacks the
|
|
60
|
+
* children in a `Column` and refuses to split them across a page break - unless they do not fit on a
|
|
61
|
+
* fresh page at all, in which case they split anyway (so pagination always terminates). Equivalent to
|
|
62
|
+
* the `keepTogether` prop on `Box`/`Column`/`Row`.
|
|
63
|
+
*/
|
|
64
|
+
export function keepTogether(children) {
|
|
65
|
+
const child = children.length === 1 ? children[0] : Column(children);
|
|
66
|
+
return new KeepTogetherElement({ child });
|
|
46
67
|
}
|
|
47
68
|
export function Box(a, b) {
|
|
48
69
|
const { opts, children } = splitArgs(a, b);
|
|
@@ -66,7 +87,7 @@ export function Box(a, b) {
|
|
|
66
87
|
// A point size fills `width`/`height`; a percentage fills `widthFactor`/`heightFactor`.
|
|
67
88
|
const w = opts.width !== undefined ? toDimension(opts.width) : undefined;
|
|
68
89
|
const h = opts.height !== undefined ? toDimension(opts.height) : undefined;
|
|
69
|
-
return new RectangleElement({
|
|
90
|
+
return maybeKeepTogether(opts, new RectangleElement({
|
|
70
91
|
x: 0,
|
|
71
92
|
y: 0,
|
|
72
93
|
children: content,
|
|
@@ -88,18 +109,25 @@ export function Box(a, b) {
|
|
|
88
109
|
: undefined,
|
|
89
110
|
relative: opts.relative,
|
|
90
111
|
overflow: opts.overflow,
|
|
91
|
-
|
|
112
|
+
breakBefore: opts.breakBefore,
|
|
113
|
+
breakAfter: opts.breakAfter,
|
|
114
|
+
}));
|
|
92
115
|
}
|
|
93
116
|
/** Insets a single child by `padding` (a number / `{x,y}` / `{top,…}` / 4-tuple). */
|
|
94
117
|
export function Padding(padding, child) {
|
|
95
118
|
return new PaddingElement({ margin: toEdges(padding), child });
|
|
96
119
|
}
|
|
97
120
|
/**
|
|
98
|
-
* Places a child OUT OF FLOW, relative to the nearest enclosing `relative` Box
|
|
99
|
-
* axis: pin to EDGES - `Positioned({ top, left, right,
|
|
100
|
-
* pokes into / out of the corner (a badge, a tab, a
|
|
101
|
-
* ANCHOR + nudge - `Positioned({ h: "center", v: "end",
|
|
102
|
-
* end-aligned with a pixel offset. An edge wins over an
|
|
121
|
+
* Places a child OUT OF FLOW, relative to the nearest enclosing `relative` Box - or, with none, to
|
|
122
|
+
* the page's content box. Two ways, pick per axis: pin to EDGES - `Positioned({ top, left, right,
|
|
123
|
+
* bottom }, child)`, where a negative value pokes into / out of the corner (a badge, a tab, a
|
|
124
|
+
* ribbon) and pinning both sides stretches; or ANCHOR + nudge - `Positioned({ h: "center", v: "end",
|
|
125
|
+
* x: -10, y: -8 }, child)`, i.e. centered / end-aligned with a pixel offset. An edge wins over an
|
|
126
|
+
* anchor on the same axis.
|
|
127
|
+
*
|
|
128
|
+
* A `Positioned` in a `Page`'s `header` or `footer` repeats on EVERY page and takes no space in the
|
|
129
|
+
* band, which is how you draw a watermark, a draft stamp or a corner ribbon across a document. It
|
|
130
|
+
* anchors to the page, not to the band, so `bottom: 0` is the foot of the page.
|
|
103
131
|
*/
|
|
104
132
|
export function Positioned(opts, child) {
|
|
105
133
|
return new PositionedElement({ child, ...opts });
|
|
@@ -161,6 +189,14 @@ export function RotatedBox(opts, child) {
|
|
|
161
189
|
export function Spacer(flex = 1) {
|
|
162
190
|
return new ExpandedElement({ flex, child: Column([]) });
|
|
163
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* A forced page break. Put it in a `Column` (or any vertical flow) and everything AFTER it starts on
|
|
194
|
+
* a fresh page, no matter how much space is left: `Column([intro, PageBreak(), chapterTwo])`. It takes
|
|
195
|
+
* no space and draws nothing. Works nested, and a break wins over `keepTogether`.
|
|
196
|
+
*/
|
|
197
|
+
export function PageBreak() {
|
|
198
|
+
return new PageBreakElement();
|
|
199
|
+
}
|
|
164
200
|
export function Expanded(a, b) {
|
|
165
201
|
const isOptsForm = b !== undefined;
|
|
166
202
|
const opts = (isOptsForm ? a : {});
|
package/dist/api/structure.d.ts
CHANGED
|
@@ -132,6 +132,11 @@ export interface RenderOptions {
|
|
|
132
132
|
standardFonts?: boolean;
|
|
133
133
|
/** FlateDecode-compress the streams (default true). Set false for a greppable, uncompressed PDF. */
|
|
134
134
|
compress?: boolean;
|
|
135
|
+
/** Kern the text - ON by default (still labelled experimental while we validate against react-pdf).
|
|
136
|
+
* Emits `TJ` adjustments from the font's own pairs: standard-14 (AFM), embedded `kern` table, and
|
|
137
|
+
* embedded `GPOS` Type 2, so every font path kerns. Set `false` to opt out (byte-identical to the
|
|
138
|
+
* pre-kerning output). Verified against headless Chrome and measured == drawn to the thousandth. */
|
|
139
|
+
kerning?: boolean;
|
|
135
140
|
/** What to do when content is taller than a page and cannot break: `"error"` throws (default),
|
|
136
141
|
* `"warn"` logs and clips, `"ignore"` clips silently. It is always clipped either way. */
|
|
137
142
|
onOverflow?: OverflowPolicy;
|
package/dist/api/structure.js
CHANGED
|
@@ -137,6 +137,7 @@ export async function renderPdf(doc, options) {
|
|
|
137
137
|
const om = this.objectManager;
|
|
138
138
|
om.setCompress(options?.compress !== false); // FlateDecode streams by default
|
|
139
139
|
om.setOverflowPolicy(options?.onOverflow ?? "error");
|
|
140
|
+
om.setKerning(options?.kerning !== false); // on by default (experimental); `kerning:false` opts out
|
|
140
141
|
for (const [name, value] of Object.entries(fonts)) {
|
|
141
142
|
if (isFontBytes(value)) {
|
|
142
143
|
om.registerCustomFont(name, value);
|
package/dist/api/text.d.ts
CHANGED
|
@@ -11,6 +11,17 @@ export interface TextStyle {
|
|
|
11
11
|
bold?: boolean;
|
|
12
12
|
italic?: boolean;
|
|
13
13
|
color?: ColorInput;
|
|
14
|
+
/** Draw a line under the text. Its position and thickness come from the font, not from a guess.
|
|
15
|
+
* A `Link` is NOT underlined automatically - say so if you want it. */
|
|
16
|
+
underline?: boolean;
|
|
17
|
+
/** Draw a line through the text, at half its x-height (where a browser puts it). */
|
|
18
|
+
strikethrough?: boolean;
|
|
19
|
+
/** Let the underline step around descenders, like a browser (`text-decoration-skip-ink`). Needs an
|
|
20
|
+
* EMBEDDED font - the standard-14 outlines live in the viewer, so we cannot see where their ink
|
|
21
|
+
* is. Asking for it with a standard font is an error, not a silent no-op. */
|
|
22
|
+
skipInk?: boolean;
|
|
23
|
+
/** Extra space after every glyph, in points (CSS `letter-spacing`). Negative tightens. Default 0. */
|
|
24
|
+
letterSpacing?: number;
|
|
14
25
|
/** External URL - makes this run an inline hyperlink (a /Link annotation over its glyphs). On a
|
|
15
26
|
* `span` it links just that run; on a whole `Text` (plain string) it links the whole text. */
|
|
16
27
|
href?: string;
|
|
@@ -26,8 +37,9 @@ export interface TextOptions extends TextStyle {
|
|
|
26
37
|
maxLines?: number;
|
|
27
38
|
/** What happens past `maxLines`: `"clip"` (default) cuts hard, `"ellipsis"` ends with "…". */
|
|
28
39
|
overflow?: TextOverflow;
|
|
29
|
-
/** Line-height multiplier
|
|
30
|
-
* `
|
|
40
|
+
/** Line-height multiplier: each line is `size * lineHeight` tall. Unset means the font's own
|
|
41
|
+
* natural line height (`ascent + descent + lineGap`), like CSS `line-height: normal` - which is
|
|
42
|
+
* what you want for body copy. `1` gives exactly one em, which most faces overflow slightly. */
|
|
31
43
|
lineHeight?: number;
|
|
32
44
|
/** Accessibility role for the tagged structure tree (only when rendered with `accessible`): a heading
|
|
33
45
|
* level `"h1"`..`"h6"` or `"p"` (default). Purely semantic - it does not change the visual style. */
|
|
@@ -56,6 +68,10 @@ export interface TextDefaults {
|
|
|
56
68
|
color?: ColorInput;
|
|
57
69
|
align?: "left" | "center" | "right";
|
|
58
70
|
lineHeight?: number;
|
|
71
|
+
underline?: boolean;
|
|
72
|
+
strikethrough?: boolean;
|
|
73
|
+
skipInk?: boolean;
|
|
74
|
+
letterSpacing?: number;
|
|
59
75
|
}
|
|
60
76
|
/** Maps the `Text`-style option names onto a partial engine `ResolvedTextStyle` (only the set
|
|
61
77
|
* fields), for seeding the inheritance cascade. */
|
package/dist/api/text.js
CHANGED
|
@@ -50,6 +50,9 @@ export function span(text, style = {}) {
|
|
|
50
50
|
? toFontStyle(style.bold, style.italic)
|
|
51
51
|
: undefined,
|
|
52
52
|
fontColor: style.color !== undefined ? toColor(style.color) : undefined,
|
|
53
|
+
underline: style.underline,
|
|
54
|
+
strikethrough: style.strikethrough,
|
|
55
|
+
letterSpacing: style.letterSpacing,
|
|
53
56
|
href: style.href,
|
|
54
57
|
dest: style.to,
|
|
55
58
|
};
|
|
@@ -79,6 +82,10 @@ export function Text(content, opts = {}) {
|
|
|
79
82
|
maxLines: opts.maxLines,
|
|
80
83
|
overflow: opts.overflow,
|
|
81
84
|
lineHeight: opts.lineHeight,
|
|
85
|
+
underline: opts.underline,
|
|
86
|
+
strikethrough: opts.strikethrough,
|
|
87
|
+
skipInk: opts.skipInk,
|
|
88
|
+
letterSpacing: opts.letterSpacing,
|
|
82
89
|
role: opts.role,
|
|
83
90
|
});
|
|
84
91
|
}
|
|
@@ -103,5 +110,13 @@ export function toTextStyleOverride(opts) {
|
|
|
103
110
|
style.textAlignment = ALIGN[opts.align];
|
|
104
111
|
if (opts.lineHeight !== undefined)
|
|
105
112
|
style.lineHeight = opts.lineHeight;
|
|
113
|
+
if (opts.underline !== undefined)
|
|
114
|
+
style.underline = opts.underline;
|
|
115
|
+
if (opts.strikethrough !== undefined)
|
|
116
|
+
style.strikethrough = opts.strikethrough;
|
|
117
|
+
if (opts.skipInk !== undefined)
|
|
118
|
+
style.skipInk = opts.skipInk;
|
|
119
|
+
if (opts.letterSpacing !== undefined)
|
|
120
|
+
style.letterSpacing = opts.letterSpacing;
|
|
106
121
|
return style;
|
|
107
122
|
}
|
|
@@ -12,14 +12,22 @@ interface ContainerElementParams extends SizedElement, WithChildren {
|
|
|
12
12
|
/** Width/height as a fraction (0..1) of the offered box instead of `width`/`height` (relative sizing). */
|
|
13
13
|
widthFactor?: number;
|
|
14
14
|
heightFactor?: number;
|
|
15
|
+
/** Start this stack on a fresh page (CSS `break-before: page`). */
|
|
16
|
+
breakBefore?: boolean;
|
|
17
|
+
/** Start everything after this stack on a fresh page (CSS `break-after: page`). */
|
|
18
|
+
breakAfter?: boolean;
|
|
15
19
|
}
|
|
16
20
|
export declare class ContainerElement extends SizedPDFElement implements Fragmentable {
|
|
17
21
|
private children;
|
|
18
22
|
private gap;
|
|
19
23
|
private main;
|
|
20
24
|
private cross;
|
|
25
|
+
private breakBefore;
|
|
26
|
+
private breakAfter;
|
|
21
27
|
private requested;
|
|
22
|
-
constructor({ x, y, width, height, children, gap, main, cross, widthFactor, heightFactor, }: ContainerElementParams);
|
|
28
|
+
constructor({ x, y, width, height, children, gap, main, cross, widthFactor, heightFactor, breakBefore, breakAfter, }: ContainerElementParams);
|
|
29
|
+
breaksBefore(): boolean;
|
|
30
|
+
breaksAfter(): boolean;
|
|
23
31
|
/**
|
|
24
32
|
* Splits the vertical stack across pages. Children are measured against the content
|
|
25
33
|
* width and packed until one would exceed `maxHeight`. That straddling child is itself
|
|
@@ -31,6 +39,7 @@ export declare class ContainerElement extends SizedPDFElement implements Fragmen
|
|
|
31
39
|
* Flex children make the container absorb the leftover space, so it never overflows -
|
|
32
40
|
* in that case we don't fragment and hand the whole container back as `fitted`.
|
|
33
41
|
*/
|
|
42
|
+
hasForcedBreak(): boolean;
|
|
34
43
|
fragment(maxHeight: number, width: number, ctx: LayoutContext): FragmentResult;
|
|
35
44
|
private cloneWithChildren;
|
|
36
45
|
relativeSizeFactor(horizontal: boolean): number | undefined;
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { FlexLayoutHelper, VERTICAL_AXIS } from "../utils/flex-layout.js";
|
|
2
2
|
import { resolveExtent } from "../layout/box-constraints.js";
|
|
3
|
-
import { packChildren } from "../layout/fragmentation.js";
|
|
3
|
+
import { childrenForceBreak, packChildren, } from "../layout/fragmentation.js";
|
|
4
4
|
import { FlexiblePDFElement, SizedPDFElement, } from "./pdf-element.js";
|
|
5
5
|
export class ContainerElement extends SizedPDFElement {
|
|
6
|
-
constructor({ x, y, width, height, children, gap, main, cross, widthFactor, heightFactor, }) {
|
|
6
|
+
constructor({ x, y, width, height, children, gap, main, cross, widthFactor, heightFactor, breakBefore, breakAfter, }) {
|
|
7
7
|
super({ x, y, width, height });
|
|
8
8
|
this.children = children;
|
|
9
9
|
this.gap = gap ?? 0;
|
|
10
10
|
this.main = main ?? "start";
|
|
11
11
|
this.cross = cross ?? "stretch";
|
|
12
12
|
this.requested = { width, height, widthFactor, heightFactor };
|
|
13
|
+
this.breakBefore = breakBefore ?? false;
|
|
14
|
+
this.breakAfter = breakAfter ?? false;
|
|
15
|
+
}
|
|
16
|
+
breaksBefore() {
|
|
17
|
+
return this.breakBefore;
|
|
18
|
+
}
|
|
19
|
+
breaksAfter() {
|
|
20
|
+
return this.breakAfter;
|
|
13
21
|
}
|
|
14
22
|
/**
|
|
15
23
|
* Splits the vertical stack across pages. Children are measured against the content
|
|
@@ -22,18 +30,27 @@ export class ContainerElement extends SizedPDFElement {
|
|
|
22
30
|
* Flex children make the container absorb the leftover space, so it never overflows -
|
|
23
31
|
* in that case we don't fragment and hand the whole container back as `fitted`.
|
|
24
32
|
*/
|
|
33
|
+
hasForcedBreak() {
|
|
34
|
+
return childrenForceBreak(this.children);
|
|
35
|
+
}
|
|
25
36
|
fragment(maxHeight, width, ctx) {
|
|
26
|
-
const { fitted, remainder } = packChildren(this.children, maxHeight, width, ctx, this.gap);
|
|
27
|
-
// Fits as one region: hand the whole container back so the page renders
|
|
28
|
-
// (its normal layout distributes flex / fills the page).
|
|
29
|
-
|
|
37
|
+
const { fitted, remainder, forceBreak } = packChildren(this.children, maxHeight, width, ctx, this.gap);
|
|
38
|
+
// Fits as one region with no forced cut: hand the whole container back so the page renders
|
|
39
|
+
// unchanged (its normal layout distributes flex / fills the page). A forced break is excluded
|
|
40
|
+
// here even when nothing follows it (a trailing `PageBreak`): return the packed children, which
|
|
41
|
+
// no longer hold the consumed break marker, so it does not leak into the render pass.
|
|
42
|
+
if (remainder.length === 0 && !forceBreak)
|
|
30
43
|
return { fitted: this, remainder: null };
|
|
31
44
|
return {
|
|
32
45
|
fitted: this.cloneWithChildren(fitted),
|
|
33
|
-
|
|
46
|
+
// Only the CONTINUATION carries `breakAfter`, so a stack that splits across pages still breaks
|
|
47
|
+
// after its LAST fragment (not after the first one). `breakBefore` is dropped on both: it was
|
|
48
|
+
// already honoured by the parent before this stack was ever fragmented.
|
|
49
|
+
remainder: remainder.length === 0 ? null : this.cloneWithChildren(remainder, this.breakAfter),
|
|
50
|
+
forceBreak,
|
|
34
51
|
};
|
|
35
52
|
}
|
|
36
|
-
cloneWithChildren(children) {
|
|
53
|
+
cloneWithChildren(children, breakAfter = false) {
|
|
37
54
|
return new ContainerElement({
|
|
38
55
|
x: this.x,
|
|
39
56
|
y: this.y,
|
|
@@ -45,6 +62,7 @@ export class ContainerElement extends SizedPDFElement {
|
|
|
45
62
|
gap: this.gap,
|
|
46
63
|
main: this.main,
|
|
47
64
|
cross: this.cross,
|
|
65
|
+
breakAfter,
|
|
48
66
|
});
|
|
49
67
|
}
|
|
50
68
|
relativeSizeFactor(horizontal) {
|
package/dist/elements/index.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export * from "./rectangle-element.js";
|
|
|
6
6
|
export * from "./image-element.js";
|
|
7
7
|
export * from "./layout/expanded-element.js";
|
|
8
8
|
export * from "./layout/padding-element.js";
|
|
9
|
+
export * from "./layout/page-break-element.js";
|
|
10
|
+
export * from "./layout/keep-together-element.js";
|
|
9
11
|
export * from "./layout/default-text-style-element.js";
|
|
10
12
|
export * from "./layout/positioned-element.js";
|
|
11
13
|
export * from "./layout/link-element.js";
|
package/dist/elements/index.js
CHANGED
|
@@ -6,6 +6,8 @@ export * from "./rectangle-element.js";
|
|
|
6
6
|
export * from "./image-element.js";
|
|
7
7
|
export * from "./layout/expanded-element.js";
|
|
8
8
|
export * from "./layout/padding-element.js";
|
|
9
|
+
export * from "./layout/page-break-element.js";
|
|
10
|
+
export * from "./layout/keep-together-element.js";
|
|
9
11
|
export * from "./layout/default-text-style-element.js";
|
|
10
12
|
export * from "./layout/positioned-element.js";
|
|
11
13
|
export * from "./layout/link-element.js";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
|
|
2
|
+
import { Fragmentable, FragmentResult } from "../../layout/fragmentation.js";
|
|
3
|
+
import { LayoutContext, PDFElement } from "../pdf-element.js";
|
|
4
|
+
/**
|
|
5
|
+
* A layout-TRANSPARENT wrapper that tries to keep its content on ONE page (CSS `break-inside: avoid`).
|
|
6
|
+
* It delegates all layout and rendering to its child, so the visual result is byte-identical to the
|
|
7
|
+
* unwrapped content; only pagination behaves differently.
|
|
8
|
+
*
|
|
9
|
+
* The rule, decided in `fragment`:
|
|
10
|
+
* 1. Fits in the space offered here -> place it whole.
|
|
11
|
+
* 2. Does not fit here but fits on a FRESH -> veto the split: defer the whole group to the next
|
|
12
|
+
* full page page (`fitted: null`), so it lands together there.
|
|
13
|
+
* 3. Taller than a whole page -> drop the veto and split normally. A group that can
|
|
14
|
+
* never fit on one page must break, or pagination
|
|
15
|
+
* could not terminate. Inner `keepTogether`s survive
|
|
16
|
+
* this (they are separate elements, re-evaluated when
|
|
17
|
+
* the child splits) - graceful degrade, outer->inner.
|
|
18
|
+
*
|
|
19
|
+
* A forced page break inside the group (a `PageBreak`, or a `breakBefore`/`breakAfter`) contradicts
|
|
20
|
+
* keeping it together: the break wins (CSS behaves the same), and we WARN once rather than silently
|
|
21
|
+
* swallowing either intent.
|
|
22
|
+
*/
|
|
23
|
+
export declare class KeepTogetherElement extends PDFElement implements Fragmentable {
|
|
24
|
+
private child;
|
|
25
|
+
private warnedBreakConflict;
|
|
26
|
+
constructor({ child }: {
|
|
27
|
+
child: PDFElement;
|
|
28
|
+
});
|
|
29
|
+
calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
|
|
30
|
+
relativeSizeFactor(horizontal: boolean): number | undefined;
|
|
31
|
+
needsBoundedMain(horizontal: boolean): boolean;
|
|
32
|
+
hasForcedBreak(): boolean;
|
|
33
|
+
breaksBefore(): boolean;
|
|
34
|
+
breaksAfter(): boolean;
|
|
35
|
+
fragment(maxHeight: number, width: number, ctx: LayoutContext): FragmentResult;
|
|
36
|
+
/** Delegate the actual split to the child, unwrapping - once we have decided to break the group, the
|
|
37
|
+
* pieces just flow (any INNER keepTogether lives in the child subtree and still vetoes there). */
|
|
38
|
+
private splitChild;
|
|
39
|
+
getProps(): {
|
|
40
|
+
child: PDFElement;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { BoxConstraints } from "../../layout/box-constraints.js";
|
|
2
|
+
import { isFragmentable } from "../../layout/fragmentation.js";
|
|
3
|
+
import { PDFElement } from "../pdf-element.js";
|
|
4
|
+
/**
|
|
5
|
+
* A layout-TRANSPARENT wrapper that tries to keep its content on ONE page (CSS `break-inside: avoid`).
|
|
6
|
+
* It delegates all layout and rendering to its child, so the visual result is byte-identical to the
|
|
7
|
+
* unwrapped content; only pagination behaves differently.
|
|
8
|
+
*
|
|
9
|
+
* The rule, decided in `fragment`:
|
|
10
|
+
* 1. Fits in the space offered here -> place it whole.
|
|
11
|
+
* 2. Does not fit here but fits on a FRESH -> veto the split: defer the whole group to the next
|
|
12
|
+
* full page page (`fitted: null`), so it lands together there.
|
|
13
|
+
* 3. Taller than a whole page -> drop the veto and split normally. A group that can
|
|
14
|
+
* never fit on one page must break, or pagination
|
|
15
|
+
* could not terminate. Inner `keepTogether`s survive
|
|
16
|
+
* this (they are separate elements, re-evaluated when
|
|
17
|
+
* the child splits) - graceful degrade, outer->inner.
|
|
18
|
+
*
|
|
19
|
+
* A forced page break inside the group (a `PageBreak`, or a `breakBefore`/`breakAfter`) contradicts
|
|
20
|
+
* keeping it together: the break wins (CSS behaves the same), and we WARN once rather than silently
|
|
21
|
+
* swallowing either intent.
|
|
22
|
+
*/
|
|
23
|
+
export class KeepTogetherElement extends PDFElement {
|
|
24
|
+
constructor({ child }) {
|
|
25
|
+
super();
|
|
26
|
+
this.warnedBreakConflict = false;
|
|
27
|
+
this.child = child;
|
|
28
|
+
}
|
|
29
|
+
calculateLayout(constraints, offset, ctx) {
|
|
30
|
+
return this.child.calculateLayout(constraints, offset, ctx);
|
|
31
|
+
}
|
|
32
|
+
// Transparent: forward the sizing hints the flex parent reads, so a %-sized or Spacer-holding child
|
|
33
|
+
// keeps working when it is wrapped (the wrapper must not swallow these).
|
|
34
|
+
relativeSizeFactor(horizontal) {
|
|
35
|
+
return this.child.relativeSizeFactor(horizontal);
|
|
36
|
+
}
|
|
37
|
+
needsBoundedMain(horizontal) {
|
|
38
|
+
return this.child.needsBoundedMain(horizontal);
|
|
39
|
+
}
|
|
40
|
+
// Transparent to break control: a break-before/after on the wrapped element still applies to the group
|
|
41
|
+
// as a whole, and an inner forced break still forces the parent to fragment us (so `fragment` runs).
|
|
42
|
+
hasForcedBreak() {
|
|
43
|
+
return this.child.hasForcedBreak();
|
|
44
|
+
}
|
|
45
|
+
breaksBefore() {
|
|
46
|
+
return this.child.breaksBefore();
|
|
47
|
+
}
|
|
48
|
+
breaksAfter() {
|
|
49
|
+
return this.child.breaksAfter();
|
|
50
|
+
}
|
|
51
|
+
fragment(maxHeight, width, ctx) {
|
|
52
|
+
// Contradiction: a forced break inside the group beats keeping it together. Split at the break by
|
|
53
|
+
// delegating to the child (which honours it), and say so once - never keep such a group whole.
|
|
54
|
+
if (this.child.hasForcedBreak()) {
|
|
55
|
+
if (!this.warnedBreakConflict) {
|
|
56
|
+
this.warnedBreakConflict = true;
|
|
57
|
+
console.warn("keepTogether contains a forced page break (PageBreak / breakBefore / breakAfter). The break " +
|
|
58
|
+
"wins and the group is split. Remove the break, or the keepTogether, to resolve this.");
|
|
59
|
+
}
|
|
60
|
+
return this.splitChild(maxHeight, width, ctx);
|
|
61
|
+
}
|
|
62
|
+
const natural = this.child.calculateLayout(BoxConstraints.loose(width, Infinity), { x: 0, y: 0 }, ctx).height;
|
|
63
|
+
// 1. Fits in the offered space: keep whole.
|
|
64
|
+
if (natural <= maxHeight)
|
|
65
|
+
return { fitted: this, remainder: null };
|
|
66
|
+
// 2. Would fit on a fresh full page: veto the split, defer the whole group to the next page. Only
|
|
67
|
+
// reachable in a PARTIAL region (something is already on this page); alone on a full page,
|
|
68
|
+
// `maxHeight` IS the page body, so `natural > maxHeight` means case 3, never this.
|
|
69
|
+
const pageBody = ctx.pageBodyHeight ?? maxHeight;
|
|
70
|
+
if (natural <= pageBody)
|
|
71
|
+
return { fitted: null, remainder: this };
|
|
72
|
+
// 3. Bigger than a whole page: it can never be kept together, so split it.
|
|
73
|
+
return this.splitChild(maxHeight, width, ctx);
|
|
74
|
+
}
|
|
75
|
+
/** Delegate the actual split to the child, unwrapping - once we have decided to break the group, the
|
|
76
|
+
* pieces just flow (any INNER keepTogether lives in the child subtree and still vetoes there). */
|
|
77
|
+
splitChild(maxHeight, width, ctx) {
|
|
78
|
+
if (!isFragmentable(this.child))
|
|
79
|
+
return { fitted: null, remainder: this };
|
|
80
|
+
return this.child.fragment(maxHeight, width, ctx);
|
|
81
|
+
}
|
|
82
|
+
getProps() {
|
|
83
|
+
return { child: this.child };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -15,6 +15,9 @@ export declare class PaddingElement extends SizedPDFElement implements Fragmenta
|
|
|
15
15
|
* child can't be split, the whole padding moves on as the remainder.
|
|
16
16
|
*/
|
|
17
17
|
fragment(maxHeight: number, width: number, ctx: LayoutContext): FragmentResult;
|
|
18
|
+
hasForcedBreak(): boolean;
|
|
19
|
+
breaksBefore(): boolean;
|
|
20
|
+
breaksAfter(): boolean;
|
|
18
21
|
private cloneWithChild;
|
|
19
22
|
calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
|
|
20
23
|
getProps(): {
|
|
@@ -25,8 +25,20 @@ export class PaddingElement extends SizedPDFElement {
|
|
|
25
25
|
return {
|
|
26
26
|
fitted: split.fitted ? this.cloneWithChild(split.fitted) : null,
|
|
27
27
|
remainder: split.remainder ? this.cloneWithChild(split.remainder) : null,
|
|
28
|
+
forceBreak: split.forceBreak,
|
|
28
29
|
};
|
|
29
30
|
}
|
|
31
|
+
hasForcedBreak() {
|
|
32
|
+
return this.child.hasForcedBreak();
|
|
33
|
+
}
|
|
34
|
+
// Padding is a transparent single-child wrapper, so a break-before/after on the wrapped element must
|
|
35
|
+
// still be seen by the parent packer at this boundary (otherwise it is swallowed).
|
|
36
|
+
breaksBefore() {
|
|
37
|
+
return this.child.breaksBefore();
|
|
38
|
+
}
|
|
39
|
+
breaksAfter() {
|
|
40
|
+
return this.child.breaksAfter();
|
|
41
|
+
}
|
|
30
42
|
cloneWithChild(child) {
|
|
31
43
|
return new PaddingElement({ margin: this.margin, child });
|
|
32
44
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
|
|
2
|
+
import { LayoutContext, PDFElement } from "../pdf-element.js";
|
|
3
|
+
/**
|
|
4
|
+
* A forced page break. Placed in a flow (a `Column`/`Row`/`Box`), it makes everything AFTER it start
|
|
5
|
+
* on a fresh page, no matter how much space is left. It takes zero space and draws nothing - the
|
|
6
|
+
* pagination packer consumes it at the cut, so it never reaches the render pass.
|
|
7
|
+
*
|
|
8
|
+
* Nesting works: a break deep inside a container is honoured because the packer fragments any child
|
|
9
|
+
* whose subtree `hasForcedBreak()`, and the resulting `forceBreak` bubbles up so the outer flow stops
|
|
10
|
+
* too. A break wins over a `keepTogether` (a forced break cannot be avoided) - CSS behaves the same.
|
|
11
|
+
*/
|
|
12
|
+
export declare class PageBreakElement extends PDFElement {
|
|
13
|
+
calculateLayout(_constraints: BoxConstraints, _offset: Offset, _ctx: LayoutContext): Size;
|
|
14
|
+
isPageBreak(): boolean;
|
|
15
|
+
hasForcedBreak(): boolean;
|
|
16
|
+
getProps(): Record<string, never>;
|
|
17
|
+
}
|