@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.
@@ -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).
@@ -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).
@@ -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";
@@ -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
- this.children.forEach((child) => child.calculateLayout(childConstraints, bands.bodyOrigin, pageCtx));
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 = Math.max(0, ((_a = this.width) !== null && _a !== void 0 ? _a : 0) - 2 * this.borderWidth);
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 }, ctx);
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: (_b = this.width) !== null && _b !== void 0 ? _b : 0,
102
- height: (_c = this.height) !== null && _c !== void 0 ? _c : 0,
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 { x, y, width, height } = element.getSize();
25
- if (x < 0 || y < 0) {
26
- throw new Error(`Element ${element.constructor.name} has invalid coordinates (x: ${x}, y: ${y})`);
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 { x, y, width, height } = element.getSize();
40
- if (x < 0 || y < 0) {
41
- throw new Error(`Element ${element.constructor.name} has invalid coordinates (x: ${x}, y: ${y})`);
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.1",
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",