@jasy/pdf 1.0.0-alpha.1 → 1.0.0-alpha.2
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/layout.d.ts +13 -0
- package/dist/api/layout.js +12 -0
- package/dist/elements/index.d.ts +1 -0
- package/dist/elements/index.js +1 -0
- package/dist/elements/layout/positioned-element.d.ts +31 -0
- package/dist/elements/layout/positioned-element.js +57 -0
- package/dist/elements/page-element.js +12 -1
- package/dist/elements/pdf-element.d.ts +16 -0
- package/dist/elements/rectangle-element.d.ts +9 -1
- package/dist/elements/rectangle-element.js +35 -6
- package/dist/ir/display-list.d.ts +18 -1
- package/dist/renderer/pdf-backend.js +16 -1
- package/dist/renderer/pdf-renderer.js +3 -0
- package/dist/renderer/positioned-renderer.d.ts +6 -0
- package/dist/renderer/positioned-renderer.js +25 -0
- package/dist/renderer/rectangle-renderer.js +10 -2
- package/dist/validators/element-validator.js +8 -10
- package/package.json +1 -1
package/dist/api/layout.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { RowElement } from "../elements/row-element";
|
|
|
3
3
|
import { RectangleElement } from "../elements/rectangle-element";
|
|
4
4
|
import { ExpandedElement } from "../elements/layout/expanded-element";
|
|
5
5
|
import { PaddingElement } from "../elements/layout/padding-element";
|
|
6
|
+
import { PositionedElement, PositionedInsets } from "../elements/layout/positioned-element";
|
|
6
7
|
import { PDFElement } from "../elements/pdf-element";
|
|
7
8
|
import { MainAlign, CrossAlign } from "../utils/flex-layout";
|
|
8
9
|
import { ColorInput } from "./color";
|
|
@@ -45,6 +46,12 @@ export interface BoxOptions {
|
|
|
45
46
|
height?: number;
|
|
46
47
|
/** Corner radius in points. */
|
|
47
48
|
radius?: number;
|
|
49
|
+
/** Make this box a positioning frame: `Positioned` children placed inside it resolve their
|
|
50
|
+
* offsets against this box (CSS `position: relative`). */
|
|
51
|
+
relative?: boolean;
|
|
52
|
+
/** `"hidden"` crops children to the box (rounded corners included); `"visible"` (default) lets a
|
|
53
|
+
* `Positioned` child spill over the edge. */
|
|
54
|
+
overflow?: "hidden" | "visible";
|
|
48
55
|
}
|
|
49
56
|
/**
|
|
50
57
|
* A box: maps to a `RectangleElement` (fill + border + radius) whose children are stacked
|
|
@@ -55,6 +62,12 @@ export declare function Box(children: PDFElement[]): RectangleElement;
|
|
|
55
62
|
export declare function Box(opts: BoxOptions, children: PDFElement[]): RectangleElement;
|
|
56
63
|
/** Insets a single child by `padding` (a number / `{x,y}` / `{top,…}` / 4-tuple). */
|
|
57
64
|
export declare function Padding(padding: Insets, child: PDFElement): PaddingElement;
|
|
65
|
+
/**
|
|
66
|
+
* Places a child OUT OF FLOW, relative to the nearest enclosing `relative` Box. Offsets are from
|
|
67
|
+
* the frame's edges (points); a negative `top`/`left` lets the child poke into or out of the corner
|
|
68
|
+
* - a badge, a tab, a ribbon. `Positioned({ top, left, right, bottom }, child)`.
|
|
69
|
+
*/
|
|
70
|
+
export declare function Positioned(opts: PositionedInsets, child: PDFElement): PositionedElement;
|
|
58
71
|
/**
|
|
59
72
|
* A flexible empty gap that pushes its siblings apart - `Row([a, Spacer(), b])` sends `a`
|
|
60
73
|
* and `b` to the edges. `flex` weights it against other flex children (default 1).
|
package/dist/api/layout.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.Column = Column;
|
|
|
4
4
|
exports.Row = Row;
|
|
5
5
|
exports.Box = Box;
|
|
6
6
|
exports.Padding = Padding;
|
|
7
|
+
exports.Positioned = Positioned;
|
|
7
8
|
exports.Spacer = Spacer;
|
|
8
9
|
exports.Expanded = Expanded;
|
|
9
10
|
const container_element_1 = require("../elements/container-element");
|
|
@@ -11,6 +12,7 @@ const row_element_1 = require("../elements/row-element");
|
|
|
11
12
|
const rectangle_element_1 = require("../elements/rectangle-element");
|
|
12
13
|
const expanded_element_1 = require("../elements/layout/expanded-element");
|
|
13
14
|
const padding_element_1 = require("../elements/layout/padding-element");
|
|
15
|
+
const positioned_element_1 = require("../elements/layout/positioned-element");
|
|
14
16
|
const color_1 = require("./color");
|
|
15
17
|
const insets_1 = require("./insets");
|
|
16
18
|
const args_1 = require("./args");
|
|
@@ -77,12 +79,22 @@ function Box(a, b) {
|
|
|
77
79
|
left: side(opts.borderLeft),
|
|
78
80
|
}
|
|
79
81
|
: undefined,
|
|
82
|
+
relative: opts.relative,
|
|
83
|
+
overflow: opts.overflow,
|
|
80
84
|
});
|
|
81
85
|
}
|
|
82
86
|
/** Insets a single child by `padding` (a number / `{x,y}` / `{top,…}` / 4-tuple). */
|
|
83
87
|
function Padding(padding, child) {
|
|
84
88
|
return new padding_element_1.PaddingElement({ margin: (0, insets_1.toEdges)(padding), child });
|
|
85
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Places a child OUT OF FLOW, relative to the nearest enclosing `relative` Box. Offsets are from
|
|
92
|
+
* the frame's edges (points); a negative `top`/`left` lets the child poke into or out of the corner
|
|
93
|
+
* - a badge, a tab, a ribbon. `Positioned({ top, left, right, bottom }, child)`.
|
|
94
|
+
*/
|
|
95
|
+
function Positioned(opts, child) {
|
|
96
|
+
return new positioned_element_1.PositionedElement(Object.assign({ child }, opts));
|
|
97
|
+
}
|
|
86
98
|
/**
|
|
87
99
|
* A flexible empty gap that pushes its siblings apart - `Row([a, Spacer(), b])` sends `a`
|
|
88
100
|
* and `b` to the edges. `flex` weights it against other flex children (default 1).
|
package/dist/elements/index.d.ts
CHANGED
|
@@ -6,5 +6,6 @@ export * from "./rectangle-element";
|
|
|
6
6
|
export * from "./image-element";
|
|
7
7
|
export * from "./layout/expanded-element";
|
|
8
8
|
export * from "./layout/padding-element";
|
|
9
|
+
export * from "./layout/positioned-element";
|
|
9
10
|
export * from "./line-element";
|
|
10
11
|
export * from "./row-element";
|
package/dist/elements/index.js
CHANGED
|
@@ -24,5 +24,6 @@ __exportStar(require("./rectangle-element"), exports);
|
|
|
24
24
|
__exportStar(require("./image-element"), exports);
|
|
25
25
|
__exportStar(require("./layout/expanded-element"), exports);
|
|
26
26
|
__exportStar(require("./layout/padding-element"), exports);
|
|
27
|
+
__exportStar(require("./layout/positioned-element"), exports);
|
|
27
28
|
__exportStar(require("./line-element"), exports);
|
|
28
29
|
__exportStar(require("./row-element"), exports);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { BoxConstraints, Offset, Size } from "../../layout/box-constraints";
|
|
2
|
+
import { LayoutContext, PDFElement, WithChild } from "../pdf-element";
|
|
3
|
+
/** Offsets from the frame's box edges, in points. Negative lets the child poke outside. */
|
|
4
|
+
export interface PositionedInsets {
|
|
5
|
+
top?: number;
|
|
6
|
+
right?: number;
|
|
7
|
+
bottom?: number;
|
|
8
|
+
left?: number;
|
|
9
|
+
}
|
|
10
|
+
interface PositionedElementParams extends WithChild, PositionedInsets {
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* An out-of-flow child, placed relative to the nearest enclosing positioning frame (a `relative`
|
|
14
|
+
* Box). It takes ZERO space in the normal flow - `calculateLayout` returns `Size(0,0)` and instead
|
|
15
|
+
* registers a placement closure on the frame. The frame runs that closure once it knows its own
|
|
16
|
+
* size, so `right`/`bottom` resolve against the final box and the child can overflow it (a negative
|
|
17
|
+
* `top`/`left` makes it poke into / out of the corner, e.g. a badge or tab).
|
|
18
|
+
*
|
|
19
|
+
* With no frame in scope the element is a no-op (nothing is drawn) - `Positioned` only makes sense
|
|
20
|
+
* inside a `relative` Box.
|
|
21
|
+
*/
|
|
22
|
+
export declare class PositionedElement extends PDFElement {
|
|
23
|
+
private child;
|
|
24
|
+
private insets;
|
|
25
|
+
constructor({ child, top, right, bottom, left }: PositionedElementParams);
|
|
26
|
+
calculateLayout(_constraints: BoxConstraints, _offset: Offset, ctx: LayoutContext): Size;
|
|
27
|
+
/** Lays the child out at the resolved position inside (or overflowing) the frame box. */
|
|
28
|
+
private placeInFrame;
|
|
29
|
+
getProps(): WithChild;
|
|
30
|
+
}
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PositionedElement = void 0;
|
|
4
|
+
const box_constraints_1 = require("../../layout/box-constraints");
|
|
5
|
+
const pdf_element_1 = require("../pdf-element");
|
|
6
|
+
/**
|
|
7
|
+
* An out-of-flow child, placed relative to the nearest enclosing positioning frame (a `relative`
|
|
8
|
+
* Box). It takes ZERO space in the normal flow - `calculateLayout` returns `Size(0,0)` and instead
|
|
9
|
+
* registers a placement closure on the frame. The frame runs that closure once it knows its own
|
|
10
|
+
* size, so `right`/`bottom` resolve against the final box and the child can overflow it (a negative
|
|
11
|
+
* `top`/`left` makes it poke into / out of the corner, e.g. a badge or tab).
|
|
12
|
+
*
|
|
13
|
+
* With no frame in scope the element is a no-op (nothing is drawn) - `Positioned` only makes sense
|
|
14
|
+
* inside a `relative` Box.
|
|
15
|
+
*/
|
|
16
|
+
class PositionedElement extends pdf_element_1.PDFElement {
|
|
17
|
+
constructor({ child, top, right, bottom, left }) {
|
|
18
|
+
super();
|
|
19
|
+
this.child = child;
|
|
20
|
+
this.insets = { top, right, bottom, left };
|
|
21
|
+
}
|
|
22
|
+
calculateLayout(_constraints, _offset, ctx) {
|
|
23
|
+
var _a;
|
|
24
|
+
// Defer to the frame: it calls back once it has sized itself. Out of flow either way.
|
|
25
|
+
(_a = ctx.frame) === null || _a === void 0 ? void 0 : _a.place.push((frame, frameCtx) => this.placeInFrame(frame, frameCtx));
|
|
26
|
+
return { width: 0, height: 0 };
|
|
27
|
+
}
|
|
28
|
+
/** Lays the child out at the resolved position inside (or overflowing) the frame box. */
|
|
29
|
+
placeInFrame(frame, ctx) {
|
|
30
|
+
const { top, right, bottom, left } = this.insets;
|
|
31
|
+
// An axis is PINNED (stretched) only when BOTH of its insets are given; otherwise the child
|
|
32
|
+
// shrink-wraps its content (unbounded), the CSS rule for an absolutely-positioned element.
|
|
33
|
+
const width = left !== undefined && right !== undefined
|
|
34
|
+
? Math.max(0, frame.size.width - left - right)
|
|
35
|
+
: Infinity;
|
|
36
|
+
const height = top !== undefined && bottom !== undefined
|
|
37
|
+
? Math.max(0, frame.size.height - top - bottom)
|
|
38
|
+
: Infinity;
|
|
39
|
+
const constraints = box_constraints_1.BoxConstraints.loose(width, height);
|
|
40
|
+
// Measure first (so right/bottom can resolve against the child's own size).
|
|
41
|
+
const measured = this.child.calculateLayout(constraints, { x: 0, y: 0 }, ctx);
|
|
42
|
+
let x = frame.origin.x + (left !== null && left !== void 0 ? left : 0);
|
|
43
|
+
if (left === undefined && right !== undefined) {
|
|
44
|
+
x = frame.origin.x + frame.size.width - measured.width - right;
|
|
45
|
+
}
|
|
46
|
+
let y = frame.origin.y + (top !== null && top !== void 0 ? top : 0);
|
|
47
|
+
if (top === undefined && bottom !== undefined) {
|
|
48
|
+
y = frame.origin.y + frame.size.height - measured.height - bottom;
|
|
49
|
+
}
|
|
50
|
+
// Place it for real at the resolved corner.
|
|
51
|
+
this.child.calculateLayout(constraints, { x, y }, ctx);
|
|
52
|
+
}
|
|
53
|
+
getProps() {
|
|
54
|
+
return { child: this.child };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.PositionedElement = PositionedElement;
|
|
@@ -70,7 +70,18 @@ class PageElement extends pdf_element_1.PDFElement {
|
|
|
70
70
|
// content box when there is neither - byte-identical to a plain page).
|
|
71
71
|
const bands = layoutPageBands(this.config, this.header, this.footer, pageCtx);
|
|
72
72
|
const childConstraints = box_constraints_1.BoxConstraints.loose(bands.bodyWidth, bands.bodyHeight);
|
|
73
|
-
|
|
73
|
+
// The page body is itself a positioning frame: a `Positioned` with no `relative` ancestor
|
|
74
|
+
// resolves against the content box (a `relative` Box overrides it for its own subtree). Drained
|
|
75
|
+
// after the body is laid out, so a page-level Positioned isn't a silent no-op.
|
|
76
|
+
const frame = {
|
|
77
|
+
origin: bands.bodyOrigin,
|
|
78
|
+
size: { width: bands.bodyWidth, height: bands.bodyHeight },
|
|
79
|
+
place: [],
|
|
80
|
+
};
|
|
81
|
+
const bodyCtx = Object.assign(Object.assign({}, pageCtx), { frame });
|
|
82
|
+
this.children.forEach((child) => child.calculateLayout(childConstraints, bands.bodyOrigin, bodyCtx));
|
|
83
|
+
for (const place of frame.place)
|
|
84
|
+
place(frame, pageCtx);
|
|
74
85
|
const { width, height } = resolvePageContentBox(this.config);
|
|
75
86
|
return { width, height };
|
|
76
87
|
}
|
|
@@ -7,9 +7,25 @@ import type { BoxConstraints, Offset, Size } from "../layout/box-constraints";
|
|
|
7
7
|
* `PageElement` sets `pageConfig` for its subtree, so each page flips against its own
|
|
8
8
|
* height. The PDF byte writer is deliberately absent - layout must not touch it.
|
|
9
9
|
*/
|
|
10
|
+
/**
|
|
11
|
+
* A positioning frame - the CSS "containing block" that `Positioned` children resolve their
|
|
12
|
+
* offsets against. A `relative` Box (later the page) creates one and threads it to its subtree;
|
|
13
|
+
* `Positioned` descendants register a placement closure in `place`, which the frame drains once it
|
|
14
|
+
* has finished sizing itself (so `right`/`bottom` can resolve against the final box).
|
|
15
|
+
*/
|
|
16
|
+
export interface PositioningFrame {
|
|
17
|
+
origin: Offset;
|
|
18
|
+
size: Size;
|
|
19
|
+
place: Array<(frame: {
|
|
20
|
+
origin: Offset;
|
|
21
|
+
size: Size;
|
|
22
|
+
}, ctx: LayoutContext) => void>;
|
|
23
|
+
}
|
|
10
24
|
export interface LayoutContext {
|
|
11
25
|
metrics: FontMetrics;
|
|
12
26
|
pageConfig: PDFPageConfig;
|
|
27
|
+
/** The nearest enclosing positioning frame, if any (set by a `relative` Box). */
|
|
28
|
+
frame?: PositioningFrame;
|
|
13
29
|
}
|
|
14
30
|
export declare abstract class PDFElement {
|
|
15
31
|
abstract getProps(): unknown;
|
|
@@ -18,6 +18,11 @@ interface RectangleElementParams extends SizedElement, WithChildren {
|
|
|
18
18
|
radius?: number;
|
|
19
19
|
/** Individual side borders; overrides the uniform `color` border when present. */
|
|
20
20
|
sideBorders?: SideBorders;
|
|
21
|
+
/** When true, this box is a positioning frame for `Positioned` descendants (CSS `relative`). */
|
|
22
|
+
relative?: boolean;
|
|
23
|
+
/** `"hidden"` crops children to the box (CSS `overflow: hidden`); `"visible"` (default) lets a
|
|
24
|
+
* `Positioned` child spill over the edge. */
|
|
25
|
+
overflow?: "hidden" | "visible";
|
|
21
26
|
}
|
|
22
27
|
export declare class RectangleElement extends SizedPDFElement implements Fragmentable {
|
|
23
28
|
private children;
|
|
@@ -26,8 +31,10 @@ export declare class RectangleElement extends SizedPDFElement implements Fragmen
|
|
|
26
31
|
private borderWidth;
|
|
27
32
|
private radius;
|
|
28
33
|
private sideBorders?;
|
|
34
|
+
private relative;
|
|
35
|
+
private overflow;
|
|
29
36
|
private sizeMemory;
|
|
30
|
-
constructor({ children, color, backgroundColor, borderWidth, width, height, radius, sideBorders, }: RectangleElementParams);
|
|
37
|
+
constructor({ children, color, backgroundColor, borderWidth, width, height, radius, sideBorders, relative, overflow, }: RectangleElementParams);
|
|
31
38
|
/**
|
|
32
39
|
* Splits the bordered box across pages (box-decoration-break: clone - every fragment
|
|
33
40
|
* gets its own full border). The children stack is packed into the space left after
|
|
@@ -50,6 +57,7 @@ export declare class RectangleElement extends SizedPDFElement implements Fragmen
|
|
|
50
57
|
borderWidth: number;
|
|
51
58
|
radius: number;
|
|
52
59
|
sideBorders: SideBorders | undefined;
|
|
60
|
+
overflow: "hidden" | "visible";
|
|
53
61
|
};
|
|
54
62
|
}
|
|
55
63
|
export {};
|
|
@@ -6,7 +6,7 @@ const box_constraints_1 = require("../layout/box-constraints");
|
|
|
6
6
|
const fragmentation_1 = require("../layout/fragmentation");
|
|
7
7
|
const pdf_element_1 = require("./pdf-element");
|
|
8
8
|
class RectangleElement extends pdf_element_1.SizedPDFElement {
|
|
9
|
-
constructor({ children = [], color = new color_1.Color(0, 0, 0), backgroundColor, borderWidth, width, height, radius, sideBorders, }) {
|
|
9
|
+
constructor({ children = [], color = new color_1.Color(0, 0, 0), backgroundColor, borderWidth, width, height, radius, sideBorders, relative, overflow, }) {
|
|
10
10
|
super({ x: 0, y: 0, width, height });
|
|
11
11
|
this.children = [];
|
|
12
12
|
this.children = children;
|
|
@@ -16,6 +16,8 @@ class RectangleElement extends pdf_element_1.SizedPDFElement {
|
|
|
16
16
|
this.borderWidth = borderWidth !== null && borderWidth !== void 0 ? borderWidth : 1;
|
|
17
17
|
this.radius = radius !== null && radius !== void 0 ? radius : 0;
|
|
18
18
|
this.sideBorders = sideBorders;
|
|
19
|
+
this.relative = relative !== null && relative !== void 0 ? relative : false;
|
|
20
|
+
this.overflow = overflow !== null && overflow !== void 0 ? overflow : "visible";
|
|
19
21
|
this.sizeMemory = { x: 0, y: 0, width, height };
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
@@ -56,10 +58,12 @@ class RectangleElement extends pdf_element_1.SizedPDFElement {
|
|
|
56
58
|
borderWidth: this.borderWidth,
|
|
57
59
|
radius: this.radius,
|
|
58
60
|
sideBorders: this.sideBorders,
|
|
61
|
+
relative: this.relative,
|
|
62
|
+
overflow: this.overflow,
|
|
59
63
|
});
|
|
60
64
|
}
|
|
61
65
|
calculateLayout(constraints, offset, ctx) {
|
|
62
|
-
var _a, _b, _c;
|
|
66
|
+
var _a, _b, _c, _d, _e;
|
|
63
67
|
// Width: an explicit size wins (clamped), else fill the offered box. (Without this a
|
|
64
68
|
// fixed box would balloon to the parent's size.)
|
|
65
69
|
this.width =
|
|
@@ -68,6 +72,9 @@ class RectangleElement extends pdf_element_1.SizedPDFElement {
|
|
|
68
72
|
: constraints.hasBoundedWidth
|
|
69
73
|
? constraints.maxWidth
|
|
70
74
|
: this.width;
|
|
75
|
+
// Width shrink-wrap: no explicit width AND an unbounded region (e.g. a `Box` badge inside a
|
|
76
|
+
// `Positioned`). Resolved after the children are measured, just below.
|
|
77
|
+
const shrinkWrapWidth = this.sizeMemory.width === undefined && !constraints.hasBoundedWidth;
|
|
71
78
|
// Height: explicit wins; otherwise FILL a bounded region (e.g. inside an Expanded) but
|
|
72
79
|
// SHRINK-WRAP the content in an unbounded one (a note box in a stack). Shrink-wrap is
|
|
73
80
|
// resolved after the children are measured, just below.
|
|
@@ -80,26 +87,47 @@ class RectangleElement extends pdf_element_1.SizedPDFElement {
|
|
|
80
87
|
: this.height;
|
|
81
88
|
this.x = this.sizeMemory.x + offset.x;
|
|
82
89
|
this.y = this.sizeMemory.y + offset.y;
|
|
90
|
+
// A `relative` box is a positioning frame: thread a fresh frame to the subtree so any
|
|
91
|
+
// `Positioned` descendant registers against it (out of flow), then drain it below once the
|
|
92
|
+
// box is sized. A plain box leaves `ctx` untouched -> byte-identical.
|
|
93
|
+
const frame = this.relative
|
|
94
|
+
? { origin: { x: 0, y: 0 }, size: { width: 0, height: 0 }, place: [] }
|
|
95
|
+
: undefined;
|
|
96
|
+
const childCtx = frame ? Object.assign(Object.assign({}, ctx), { frame }) : ctx;
|
|
83
97
|
// Lay out children stacked inside the border (inset by the border width). Width is
|
|
84
98
|
// finalized here; height is left unbounded so each child sizes to its own content.
|
|
85
|
-
const innerWidth =
|
|
99
|
+
const innerWidth = shrinkWrapWidth
|
|
100
|
+
? Infinity
|
|
101
|
+
: Math.max(0, ((_a = this.width) !== null && _a !== void 0 ? _a : 0) - 2 * this.borderWidth);
|
|
102
|
+
let contentWidth = 0;
|
|
86
103
|
let contentHeight = 0;
|
|
87
104
|
let yCursor = this.y + this.borderWidth;
|
|
88
105
|
for (const child of this.children) {
|
|
89
|
-
const childSize = child.calculateLayout(box_constraints_1.BoxConstraints.loose(innerWidth, Infinity), { x: this.x + this.borderWidth, y: yCursor },
|
|
106
|
+
const childSize = child.calculateLayout(box_constraints_1.BoxConstraints.loose(innerWidth, Infinity), { x: this.x + this.borderWidth, y: yCursor }, childCtx);
|
|
90
107
|
yCursor += childSize.height;
|
|
91
108
|
contentHeight += childSize.height;
|
|
109
|
+
contentWidth = Math.max(contentWidth, childSize.width);
|
|
92
110
|
}
|
|
111
|
+
// No explicit width and no bounded region: shrink-wrap to the widest child.
|
|
112
|
+
if (shrinkWrapWidth)
|
|
113
|
+
this.width = contentWidth + 2 * this.borderWidth;
|
|
93
114
|
// No explicit height and no bounded region: the border hugs its content.
|
|
94
115
|
if (shrinkWrapHeight)
|
|
95
116
|
this.height = contentHeight + 2 * this.borderWidth;
|
|
117
|
+
// The box is sized now: place any out-of-flow `Positioned` descendants against its box.
|
|
118
|
+
if (frame) {
|
|
119
|
+
frame.origin = { x: this.x, y: this.y };
|
|
120
|
+
frame.size = { width: (_b = this.width) !== null && _b !== void 0 ? _b : 0, height: (_c = this.height) !== null && _c !== void 0 ? _c : 0 };
|
|
121
|
+
for (const place of frame.place)
|
|
122
|
+
place(frame, ctx);
|
|
123
|
+
}
|
|
96
124
|
// Border-box model: width/height ARE the outer box (the rect we draw); the content
|
|
97
125
|
// is inset by the border on every side. Report the honest box size - no phantom
|
|
98
126
|
// border added on (that asymmetric "+border" was the source of the magic-3 fudge in
|
|
99
127
|
// fragment). Top-left coordinates; the Y-flip happens at the IR -> backend seam.
|
|
100
128
|
return {
|
|
101
|
-
width: (
|
|
102
|
-
height: (
|
|
129
|
+
width: (_d = this.width) !== null && _d !== void 0 ? _d : 0,
|
|
130
|
+
height: (_e = this.height) !== null && _e !== void 0 ? _e : 0,
|
|
103
131
|
};
|
|
104
132
|
}
|
|
105
133
|
getProps() {
|
|
@@ -114,6 +142,7 @@ class RectangleElement extends pdf_element_1.SizedPDFElement {
|
|
|
114
142
|
borderWidth: this.borderWidth,
|
|
115
143
|
radius: this.radius,
|
|
116
144
|
sideBorders: this.sideBorders,
|
|
145
|
+
overflow: this.overflow,
|
|
117
146
|
};
|
|
118
147
|
}
|
|
119
148
|
}
|
|
@@ -70,5 +70,22 @@ export interface Image {
|
|
|
70
70
|
/** Corner radius in points for the image box; absent/0 = sharp corners. */
|
|
71
71
|
radius?: number;
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Pushes a clipping region (a rectangle, rounded when `radius` is set). Everything between this and
|
|
75
|
+
* the matching `ClipPop` is clipped to it - what a Box with `overflow: "hidden"` wraps its children
|
|
76
|
+
* in, so a `Positioned` child gets cropped at the box edge instead of spilling over.
|
|
77
|
+
*/
|
|
78
|
+
export interface ClipPush {
|
|
79
|
+
type: "clip-push";
|
|
80
|
+
x: number;
|
|
81
|
+
y: number;
|
|
82
|
+
width: number;
|
|
83
|
+
height: number;
|
|
84
|
+
radius?: number;
|
|
85
|
+
}
|
|
86
|
+
/** Closes the most recent `ClipPush` (restores the graphics state). */
|
|
87
|
+
export interface ClipPop {
|
|
88
|
+
type: "clip-pop";
|
|
89
|
+
}
|
|
73
90
|
/** The closed set of primitives the PDF backend knows how to draw. */
|
|
74
|
-
export type IRNode = TextRun | Rect | Line | Image;
|
|
91
|
+
export type IRNode = TextRun | Rect | Line | Image | ClipPush | ClipPop;
|
|
@@ -35,6 +35,11 @@ class PdfBackend {
|
|
|
35
35
|
}
|
|
36
36
|
return flipped;
|
|
37
37
|
}
|
|
38
|
+
case "clip-push":
|
|
39
|
+
// Flip the clip rect around its bottom edge, like a rect.
|
|
40
|
+
return Object.assign(Object.assign({}, node), { y: pageHeight - node.y - node.height });
|
|
41
|
+
case "clip-pop":
|
|
42
|
+
return node;
|
|
38
43
|
default: {
|
|
39
44
|
const unknown = node;
|
|
40
45
|
return unknown;
|
|
@@ -95,7 +100,7 @@ class PdfBackend {
|
|
|
95
100
|
* `om` is used only by primitives that allocate PDF resources (images, fonts).
|
|
96
101
|
*/
|
|
97
102
|
static serializeNode(node, om) {
|
|
98
|
-
var _a, _b, _c, _d, _e;
|
|
103
|
+
var _a, _b, _c, _d, _e, _f;
|
|
99
104
|
switch (node.type) {
|
|
100
105
|
case "line":
|
|
101
106
|
// q/Q isolates the graphics state; "[] 0 d" resets the dash pattern to solid.
|
|
@@ -173,6 +178,16 @@ class PdfBackend {
|
|
|
173
178
|
: `${c.x} ${c.y} ${c.width} ${c.height} re `;
|
|
174
179
|
return `q\n${clipPath}\nW n \n` + draw + `Q\n`;
|
|
175
180
|
}
|
|
181
|
+
case "clip-push": {
|
|
182
|
+
// Save the graphics state and intersect the clip with this (rounded) rect. Everything
|
|
183
|
+
// drawn until the matching clip-pop is cropped to it.
|
|
184
|
+
const path = ((_f = node.radius) !== null && _f !== void 0 ? _f : 0) > 0
|
|
185
|
+
? PdfBackend.roundedRectPath(node.x, node.y, node.width, node.height, node.radius)
|
|
186
|
+
: `${node.x} ${node.y} ${node.width} ${node.height} re`;
|
|
187
|
+
return `q\n${path}\nW n\n`;
|
|
188
|
+
}
|
|
189
|
+
case "clip-pop":
|
|
190
|
+
return `Q\n`;
|
|
176
191
|
default: {
|
|
177
192
|
// Exhaustiveness guard: if a new IRNode variant is added, this fails to compile.
|
|
178
193
|
const unknown = node;
|
|
@@ -28,6 +28,8 @@ const repeating_header_element_1 = require("../elements/layout/repeating-header-
|
|
|
28
28
|
const repeating_header_renderer_1 = require("./repeating-header-renderer");
|
|
29
29
|
const deferred_element_1 = require("../elements/layout/deferred-element");
|
|
30
30
|
const deferred_renderer_1 = require("./deferred-renderer");
|
|
31
|
+
const positioned_element_1 = require("../elements/layout/positioned-element");
|
|
32
|
+
const positioned_renderer_1 = require("./positioned-renderer");
|
|
31
33
|
const box_constraints_1 = require("../layout/box-constraints");
|
|
32
34
|
class PDFRenderer {
|
|
33
35
|
static render(document, objectManager) {
|
|
@@ -43,6 +45,7 @@ class PDFRenderer {
|
|
|
43
45
|
renderer_registry_1.RendererRegistry.register(elements_1.LineElement, line_renderer_1.LineRenderer.render);
|
|
44
46
|
renderer_registry_1.RendererRegistry.register(repeating_header_element_1.RepeatingHeaderElement, repeating_header_renderer_1.RepeatingHeaderRenderer.render);
|
|
45
47
|
renderer_registry_1.RendererRegistry.register(deferred_element_1.DeferredElement, deferred_renderer_1.DeferredRenderer.render);
|
|
48
|
+
renderer_registry_1.RendererRegistry.register(positioned_element_1.PositionedElement, positioned_renderer_1.PositionedRenderer.render);
|
|
46
49
|
let pdfContent = "";
|
|
47
50
|
// Header: version line + the PDF/A binary marker (the object manager owns it so its length
|
|
48
51
|
// matches the xref offset calculation).
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { PDFObjectManager } from "../utils/pdf-object-manager";
|
|
2
|
+
import { PositionedElement } from "../elements/layout/positioned-element";
|
|
3
|
+
import { IRNode } from "../ir/display-list";
|
|
4
|
+
export declare class PositionedRenderer {
|
|
5
|
+
static render(positionedElement: PositionedElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.PositionedRenderer = void 0;
|
|
13
|
+
const renderer_registry_1 = require("../utils/renderer-registry");
|
|
14
|
+
class PositionedRenderer {
|
|
15
|
+
static render(positionedElement, objectManager) {
|
|
16
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
17
|
+
// The child was already placed (by the frame) during layout; just emit its IR. It is drawn
|
|
18
|
+
// after the frame's flow content, so it paints on top.
|
|
19
|
+
const { child } = positionedElement.getProps();
|
|
20
|
+
const renderer = renderer_registry_1.RendererRegistry.getRenderer(child);
|
|
21
|
+
return renderer ? renderer(child, objectManager) : [];
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.PositionedRenderer = PositionedRenderer;
|
|
@@ -14,7 +14,7 @@ const renderer_registry_1 = require("../utils/renderer-registry");
|
|
|
14
14
|
class RectangleRenderer {
|
|
15
15
|
static render(rectangleElement, objectManager) {
|
|
16
16
|
return __awaiter(this, void 0, void 0, function* () {
|
|
17
|
-
const { x, y, width, height, children, color, backgroundColor, borderWidth, radius, sideBorders, } = rectangleElement.getProps();
|
|
17
|
+
const { x, y, width, height, children, color, backgroundColor, borderWidth, radius, sideBorders, overflow, } = rectangleElement.getProps();
|
|
18
18
|
const h = height;
|
|
19
19
|
const nodes = [];
|
|
20
20
|
if (sideBorders) {
|
|
@@ -33,7 +33,13 @@ class RectangleRenderer {
|
|
|
33
33
|
width, height: h, stroke: color, strokeWidth: borderWidth }, (backgroundColor ? { fill: backgroundColor } : {})), (radius ? { radius } : {}));
|
|
34
34
|
nodes.push(node);
|
|
35
35
|
}
|
|
36
|
-
// Children follow the box (Rectangle is also a container).
|
|
36
|
+
// Children follow the box (Rectangle is also a container). With overflow: "hidden" they are
|
|
37
|
+
// wrapped in a clip to the (rounded) box rect, so a Positioned child is cropped at the edge
|
|
38
|
+
// instead of spilling over. Default "visible" emits no clip - byte-identical.
|
|
39
|
+
const clip = overflow === "hidden";
|
|
40
|
+
if (clip) {
|
|
41
|
+
nodes.push(Object.assign({ type: "clip-push", x, y, width, height: h }, (radius ? { radius } : {})));
|
|
42
|
+
}
|
|
37
43
|
if (children)
|
|
38
44
|
for (const child of children) {
|
|
39
45
|
const renderer = renderer_registry_1.RendererRegistry.getRenderer(child);
|
|
@@ -41,6 +47,8 @@ class RectangleRenderer {
|
|
|
41
47
|
nodes.push(...(yield renderer(child, objectManager)));
|
|
42
48
|
}
|
|
43
49
|
}
|
|
50
|
+
if (clip)
|
|
51
|
+
nodes.push({ type: "clip-pop" });
|
|
44
52
|
return nodes;
|
|
45
53
|
});
|
|
46
54
|
}
|
|
@@ -21,11 +21,10 @@ class Validator {
|
|
|
21
21
|
}
|
|
22
22
|
// Layout validation: geometry comes from the typed getSize(), not the props bag.
|
|
23
23
|
if (element instanceof pdf_element_1.SizedPDFElement) {
|
|
24
|
-
const {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// 0 is legitimate (a hairline divider, an empty spacer); only a NEGATIVE size is invalid.
|
|
24
|
+
const { width, height } = element.getSize();
|
|
25
|
+
// Negative coordinates are allowed: a `Positioned` child overflows its frame on purpose,
|
|
26
|
+
// and the page clips anything past its edge. 0 size is legitimate (a hairline divider, an
|
|
27
|
+
// empty spacer); only a NEGATIVE size is invalid.
|
|
29
28
|
if ((width !== undefined && width < 0) || (height !== undefined && height < 0)) {
|
|
30
29
|
throw new Error(`Element ${element.constructor.name} has invalid size (width: ${width}, height: ${height})`);
|
|
31
30
|
}
|
|
@@ -36,11 +35,10 @@ class Validator {
|
|
|
36
35
|
}
|
|
37
36
|
}
|
|
38
37
|
static validateSizedElement(element) {
|
|
39
|
-
const {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// A size must be set, but 0 is legitimate (a hairline divider); only NEGATIVE is invalid.
|
|
38
|
+
const { width, height } = element.getSize();
|
|
39
|
+
// Negative coordinates are legitimate: a `Positioned` child overflows its frame on purpose
|
|
40
|
+
// (a corner badge), and the page clips anything off its edge. A size must be set, but 0 is
|
|
41
|
+
// legitimate (a hairline divider); only a NEGATIVE size is invalid.
|
|
44
42
|
if (width === undefined || height === undefined || width < 0 || height < 0) {
|
|
45
43
|
throw new Error(`Element ${element.constructor.name} has invalid size (width: ${width}, height: ${height})`);
|
|
46
44
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jasy/pdf",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.2",
|
|
4
4
|
"description": "Declarative, component-based PDF generation in pure TypeScript - Flutter-style components, real AFM font metrics, a hand-rolled PDF writer. No headless browser, no Java.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"declarative",
|