@jasy/pdf 1.0.0-alpha.5 → 1.0.0-alpha.6

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.
Files changed (72) hide show
  1. package/dist/api/content.d.ts +6 -3
  2. package/dist/api/content.js +12 -3
  3. package/dist/api/descriptor.js +22 -1
  4. package/dist/api/dimension.d.ts +20 -0
  5. package/dist/api/dimension.js +16 -0
  6. package/dist/api/index.d.ts +2 -0
  7. package/dist/api/index.js +2 -0
  8. package/dist/api/layout.d.ts +65 -3
  9. package/dist/api/layout.js +71 -2
  10. package/dist/api/page-builder.d.ts +32 -0
  11. package/dist/api/page-builder.js +32 -0
  12. package/dist/api/text.d.ts +6 -0
  13. package/dist/api/text.js +31 -4
  14. package/dist/elements/container-element.d.ts +17 -1
  15. package/dist/elements/container-element.js +54 -22
  16. package/dist/elements/image-element.d.ts +19 -1
  17. package/dist/elements/image-element.js +52 -11
  18. package/dist/elements/index.d.ts +6 -0
  19. package/dist/elements/index.js +6 -0
  20. package/dist/elements/layout/anchor-element.d.ts +23 -0
  21. package/dist/elements/layout/anchor-element.js +27 -0
  22. package/dist/elements/layout/bookmark-element.d.ts +27 -0
  23. package/dist/elements/layout/bookmark-element.js +30 -0
  24. package/dist/elements/layout/link-element.d.ts +33 -0
  25. package/dist/elements/layout/link-element.js +39 -0
  26. package/dist/elements/layout/page-builder-element.d.ts +32 -0
  27. package/dist/elements/layout/page-builder-element.js +43 -0
  28. package/dist/elements/layout/rotated-box-element.d.ts +35 -0
  29. package/dist/elements/layout/rotated-box-element.js +54 -0
  30. package/dist/elements/layout/rotated-element.d.ts +32 -0
  31. package/dist/elements/layout/rotated-element.js +41 -0
  32. package/dist/elements/page-element.d.ts +8 -0
  33. package/dist/elements/page-element.js +13 -1
  34. package/dist/elements/pdf-element.d.ts +31 -0
  35. package/dist/elements/pdf-element.js +19 -0
  36. package/dist/elements/rectangle-element.d.ts +11 -1
  37. package/dist/elements/rectangle-element.js +49 -13
  38. package/dist/elements/row-element.d.ts +11 -1
  39. package/dist/elements/row-element.js +37 -8
  40. package/dist/elements/text-element.d.ts +4 -0
  41. package/dist/ir/display-list.d.ts +59 -1
  42. package/dist/layout/box-constraints.d.ts +8 -0
  43. package/dist/layout/box-constraints.js +14 -0
  44. package/dist/layout/collect-images.d.ts +9 -0
  45. package/dist/layout/collect-images.js +34 -0
  46. package/dist/renderer/anchor-renderer.d.ts +6 -0
  47. package/dist/renderer/anchor-renderer.js +11 -0
  48. package/dist/renderer/bookmark-renderer.d.ts +6 -0
  49. package/dist/renderer/bookmark-renderer.js +11 -0
  50. package/dist/renderer/link-renderer.d.ts +6 -0
  51. package/dist/renderer/link-renderer.js +11 -0
  52. package/dist/renderer/page-builder-renderer.d.ts +6 -0
  53. package/dist/renderer/page-builder-renderer.js +11 -0
  54. package/dist/renderer/page-renderer.js +37 -2
  55. package/dist/renderer/pdf-backend.d.ts +10 -0
  56. package/dist/renderer/pdf-backend.js +77 -1
  57. package/dist/renderer/pdf-document-renderer.d.ts +16 -7
  58. package/dist/renderer/pdf-document-renderer.js +65 -33
  59. package/dist/renderer/pdf-renderer.js +47 -2
  60. package/dist/renderer/rotated-renderer.d.ts +7 -0
  61. package/dist/renderer/rotated-renderer.js +29 -0
  62. package/dist/renderer/text-renderer.js +26 -5
  63. package/dist/utils/afm-parser.js +6 -4
  64. package/dist/utils/dest-registry.d.ts +19 -0
  65. package/dist/utils/dest-registry.js +32 -0
  66. package/dist/utils/flex-layout.d.ts +10 -3
  67. package/dist/utils/flex-layout.js +26 -6
  68. package/dist/utils/outline.d.ts +21 -0
  69. package/dist/utils/outline.js +80 -0
  70. package/dist/utils/pdf-object-manager.d.ts +6 -0
  71. package/dist/utils/pdf-object-manager.js +0 -0
  72. package/package.json +1 -1
@@ -0,0 +1,32 @@
1
+ import { PDFElement, LayoutContext } from "../pdf-element.js";
2
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
3
+ /**
4
+ * Rotates its single child at PAINT time only (like Flutter's `Transform.rotate`): the child lays out
5
+ * in its normal, axis-aligned box and the element reports that same box UP, so siblings never reflow
6
+ * around the rotated shape. Only the drawing is rotated, around the box's center, by the renderer.
7
+ *
8
+ * Layout-transparent: it delegates `calculateLayout` to the child and just records the resulting box,
9
+ * which the renderer needs as the pivot. Not `Fragmentable` on purpose - a rotated subtree is atomic
10
+ * w.r.t. pagination (it moves whole to the next page rather than splitting mid-rotation).
11
+ */
12
+ export declare class RotatedElement extends PDFElement {
13
+ private angle;
14
+ private child;
15
+ private x;
16
+ private y;
17
+ private width;
18
+ private height;
19
+ constructor({ angle, child }: {
20
+ angle: number;
21
+ child: PDFElement;
22
+ });
23
+ calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
24
+ getProps(): {
25
+ angle: number;
26
+ child: PDFElement;
27
+ x: number;
28
+ y: number;
29
+ width: number;
30
+ height: number;
31
+ };
32
+ }
@@ -0,0 +1,41 @@
1
+ import { PDFElement } from "../pdf-element.js";
2
+ /**
3
+ * Rotates its single child at PAINT time only (like Flutter's `Transform.rotate`): the child lays out
4
+ * in its normal, axis-aligned box and the element reports that same box UP, so siblings never reflow
5
+ * around the rotated shape. Only the drawing is rotated, around the box's center, by the renderer.
6
+ *
7
+ * Layout-transparent: it delegates `calculateLayout` to the child and just records the resulting box,
8
+ * which the renderer needs as the pivot. Not `Fragmentable` on purpose - a rotated subtree is atomic
9
+ * w.r.t. pagination (it moves whole to the next page rather than splitting mid-rotation).
10
+ */
11
+ export class RotatedElement extends PDFElement {
12
+ constructor({ angle, child }) {
13
+ super();
14
+ this.x = 0;
15
+ this.y = 0;
16
+ this.width = 0;
17
+ this.height = 0;
18
+ this.angle = angle;
19
+ this.child = child;
20
+ }
21
+ calculateLayout(constraints, offset, ctx) {
22
+ // Delegate entirely to the child, then record its box (position + size) as our own - the
23
+ // rotation does not change what space we take, only how the child is painted.
24
+ const size = this.child.calculateLayout(constraints, offset, ctx);
25
+ this.x = offset.x;
26
+ this.y = offset.y;
27
+ this.width = size.width;
28
+ this.height = size.height;
29
+ return size;
30
+ }
31
+ getProps() {
32
+ return {
33
+ angle: this.angle,
34
+ child: this.child,
35
+ x: this.x,
36
+ y: this.y,
37
+ width: this.width,
38
+ height: this.height,
39
+ };
40
+ }
41
+ }
@@ -20,6 +20,14 @@ interface PDFPageParams extends WithChildren {
20
20
  /** Laid out at the BOTTOM of the content box, repeated on every physical page. */
21
21
  footer?: PDFElement;
22
22
  }
23
+ /**
24
+ * The full page (media box) size in points, orientation applied. This is what a `PageBuilder` sees as
25
+ * `pageSize`; `resolvePageContentBox` below then subtracts the margins.
26
+ */
27
+ export declare function resolvePageSize(config: PDFPageConfig): {
28
+ width: number;
29
+ height: number;
30
+ };
23
31
  /**
24
32
  * The content box of a page (inside the margins, orientation applied) for a fully
25
33
  * resolved config. Single source of truth, shared by `PageElement` layout and the page
@@ -1,7 +1,18 @@
1
- import { pageFormats } from "../constants/page-sizes.js";
1
+ import { pageFormats, PageSize } from "../constants/page-sizes.js";
2
2
  import { Orientation } from "../renderer/pdf-config.js";
3
3
  import { BoxConstraints } from "../layout/box-constraints.js";
4
4
  import { PDFElement } from "./pdf-element.js";
5
+ /**
6
+ * The full page (media box) size in points, orientation applied. This is what a `PageBuilder` sees as
7
+ * `pageSize`; `resolvePageContentBox` below then subtracts the margins.
8
+ */
9
+ export function resolvePageSize(config) {
10
+ // Defaults to A4 when the config is not fully resolved, matching what PageRenderer does for the MediaBox.
11
+ const [pageW, pageH] = config.customSize ?? pageFormats[config.pageSize ?? PageSize.A4];
12
+ return config.orientation === Orientation.landscape
13
+ ? { width: pageH, height: pageW }
14
+ : { width: pageW, height: pageH };
15
+ }
5
16
  /**
6
17
  * The content box of a page (inside the margins, orientation applied) for a fully
7
18
  * resolved config. Single source of truth, shared by `PageElement` layout and the page
@@ -63,6 +74,7 @@ export class PageElement extends PDFElement {
63
74
  pageConfig: this.config,
64
75
  textStyle: ctx.textStyle,
65
76
  onOverflow: ctx.onOverflow,
77
+ pageInfo: ctx.pageInfo, // the render pass supplies it; absent during pagination
66
78
  };
67
79
  // Place the header/footer bands; the body gets the region left in between (the whole
68
80
  // content box when there is neither - byte-identical to a plain page).
@@ -23,9 +23,25 @@ export interface PositioningFrame {
23
23
  size: Size;
24
24
  }, ctx: LayoutContext) => void>;
25
25
  }
26
+ /**
27
+ * What a `PageBuilder` learns about the page it landed on. `pageNumber` is 1-based and counts PHYSICAL
28
+ * pages across the whole document (a logical `Page` that overflows contributes several); `pageCount` is
29
+ * the document's final physical page total. `pageSize` is the media box in points, orientation applied.
30
+ */
31
+ export interface PageInfo {
32
+ pageNumber: number;
33
+ pageCount: number;
34
+ pageSize: {
35
+ width: number;
36
+ height: number;
37
+ };
38
+ }
26
39
  export interface LayoutContext {
27
40
  metrics: FontMetrics;
28
41
  pageConfig: PDFPageConfig;
42
+ /** The page a `PageBuilder` is being laid out on. Only the render pass knows the real numbers, so
43
+ * during pagination this is absent and a `PageBuilder` builds against a provisional "1 of 1". */
44
+ pageInfo?: PageInfo;
29
45
  /** The nearest enclosing positioning frame, if any (set by a `relative` Box). */
30
46
  frame?: PositioningFrame;
31
47
  /** The cascaded text style descendants inherit (CSS/Flutter-style). Seeded at the document root
@@ -49,6 +65,21 @@ export declare abstract class PDFElement {
49
65
  * happens once at the IR -> backend seam.
50
66
  */
51
67
  abstract calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
68
+ /**
69
+ * The relative-size fraction (0..1) this element asked for on the given axis, if any (`horizontal` =
70
+ * width). A flex parent reads it to resolve a percentage child against its OWN main size - e.g. a
71
+ * `Box({ width: "50%" })` inside a Row, where the child otherwise gets an unbounded main axis and the
72
+ * fraction would have nothing to resolve against. Default: the element is not relatively sized.
73
+ */
74
+ relativeSizeFactor(_horizontal: boolean): number | undefined;
75
+ /**
76
+ * Whether this element needs a BOUNDED extent on the given axis to lay itself out (`true` for a
77
+ * horizontal axis = width). A flex parent measures its fixed children with an unbounded main axis, so
78
+ * they keep their natural size; but a stack holding an `Expanded`/`Spacer` has no leftover space to
79
+ * distribute unless it is told how much main axis it may occupy. Such a stack answers `true` here and
80
+ * the parent hands it the line's bounded main extent instead of `Infinity`. Default: no.
81
+ */
82
+ needsBoundedMain(_horizontal: boolean): boolean;
52
83
  }
53
84
  export declare abstract class SizedPDFElement extends PDFElement {
54
85
  protected x: number;
@@ -14,6 +14,25 @@ export class PDFElement {
14
14
  this._structId = from._structId;
15
15
  return this;
16
16
  }
17
+ /**
18
+ * The relative-size fraction (0..1) this element asked for on the given axis, if any (`horizontal` =
19
+ * width). A flex parent reads it to resolve a percentage child against its OWN main size - e.g. a
20
+ * `Box({ width: "50%" })` inside a Row, where the child otherwise gets an unbounded main axis and the
21
+ * fraction would have nothing to resolve against. Default: the element is not relatively sized.
22
+ */
23
+ relativeSizeFactor(_horizontal) {
24
+ return undefined;
25
+ }
26
+ /**
27
+ * Whether this element needs a BOUNDED extent on the given axis to lay itself out (`true` for a
28
+ * horizontal axis = width). A flex parent measures its fixed children with an unbounded main axis, so
29
+ * they keep their natural size; but a stack holding an `Expanded`/`Spacer` has no leftover space to
30
+ * distribute unless it is told how much main axis it may occupy. Such a stack answers `true` here and
31
+ * the parent hands it the line's bounded main extent instead of `Infinity`. Default: no.
32
+ */
33
+ needsBoundedMain(_horizontal) {
34
+ return false;
35
+ }
17
36
  }
18
37
  export class SizedPDFElement extends PDFElement {
19
38
  constructor(data) {
@@ -23,6 +23,11 @@ interface RectangleElementParams extends SizedElement, WithChildren {
23
23
  /** `"hidden"` crops children to the box (CSS `overflow: hidden`); `"visible"` (default) lets a
24
24
  * `Positioned` child spill over the edge. */
25
25
  overflow?: "hidden" | "visible";
26
+ /** Width as a fraction (0..1) of the offered width, instead of a fixed `width` (CSS `50%`). Only
27
+ * resolves in a bounded region; an explicit `width` wins over it. */
28
+ widthFactor?: number;
29
+ /** Height as a fraction (0..1) of the offered height; see `widthFactor`. */
30
+ heightFactor?: number;
26
31
  }
27
32
  export declare class RectangleElement extends SizedPDFElement implements Fragmentable {
28
33
  private children;
@@ -34,7 +39,7 @@ export declare class RectangleElement extends SizedPDFElement implements Fragmen
34
39
  private relative;
35
40
  private overflow;
36
41
  private sizeMemory;
37
- constructor({ children, color, backgroundColor, borderWidth, width, height, radius, sideBorders, relative, overflow, }: RectangleElementParams);
42
+ constructor({ children, color, backgroundColor, borderWidth, width, height, radius, sideBorders, relative, overflow, widthFactor, heightFactor, }: RectangleElementParams);
38
43
  /**
39
44
  * Splits the bordered box across pages (box-decoration-break: clone - every fragment
40
45
  * gets its own full border). The children stack is packed into the space left after
@@ -45,6 +50,11 @@ export declare class RectangleElement extends SizedPDFElement implements Fragmen
45
50
  */
46
51
  fragment(maxHeight: number, width: number, ctx: LayoutContext): FragmentResult;
47
52
  private cloneWithChildren;
53
+ relativeSizeFactor(horizontal: boolean): number | undefined;
54
+ /** A box has no flex children of its own, but it must not swallow the need of the stack inside it: a
55
+ * `Spacer` in a box can only resolve if the box itself was given a bounded extent to pass down. With an
56
+ * explicit size on an axis the box already knows its extent there and asks for nothing. */
57
+ needsBoundedMain(horizontal: boolean): boolean;
48
58
  calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
49
59
  getProps(): {
50
60
  x: number;
@@ -1,9 +1,9 @@
1
1
  import { Color } from "../common/color.js";
2
- import { BoxConstraints } from "../layout/box-constraints.js";
2
+ import { BoxConstraints, resolveExtent } from "../layout/box-constraints.js";
3
3
  import { packChildren } from "../layout/fragmentation.js";
4
4
  import { SizedPDFElement, } from "./pdf-element.js";
5
5
  export class RectangleElement extends SizedPDFElement {
6
- constructor({ children = [], color = new Color(0, 0, 0), backgroundColor, borderWidth, width, height, radius, sideBorders, relative, overflow, }) {
6
+ constructor({ children = [], color = new Color(0, 0, 0), backgroundColor, borderWidth, width, height, radius, sideBorders, relative, overflow, widthFactor, heightFactor, }) {
7
7
  super({ x: 0, y: 0, width, height });
8
8
  this.children = [];
9
9
  this.children = children;
@@ -15,7 +15,7 @@ export class RectangleElement extends SizedPDFElement {
15
15
  this.sideBorders = sideBorders;
16
16
  this.relative = relative ?? false;
17
17
  this.overflow = overflow ?? "visible";
18
- this.sizeMemory = { x: 0, y: 0, width, height };
18
+ this.sizeMemory = { x: 0, y: 0, width, height, widthFactor, heightFactor };
19
19
  }
20
20
  /**
21
21
  * Splits the bordered box across pages (box-decoration-break: clone - every fragment
@@ -26,7 +26,10 @@ export class RectangleElement extends SizedPDFElement {
26
26
  * box is sized by what it actually holds on each page.
27
27
  */
28
28
  fragment(maxHeight, width, ctx) {
29
- const boxWidth = this.sizeMemory.width ?? width;
29
+ // A percentage width resolves against the region width the page driver hands us, so a
30
+ // paginating %-box keeps its fraction on every page.
31
+ const boxWidth = this.sizeMemory.width ??
32
+ (this.sizeMemory.widthFactor !== undefined ? width * this.sizeMemory.widthFactor : width);
30
33
  const innerWidth = Math.max(0, boxWidth - 2 * this.borderWidth);
31
34
  // Border-box: the content is inset by the border on top and bottom, so a fragment
32
35
  // holding `c` of content is `c + 2*border` tall. (Derived, not a fudge factor.)
@@ -47,6 +50,9 @@ export class RectangleElement extends SizedPDFElement {
47
50
  x: 0,
48
51
  y: 0,
49
52
  width: this.sizeMemory.width,
53
+ widthFactor: this.sizeMemory.widthFactor,
54
+ // A split box is sized by its per-page content (height passed in), so the height factor
55
+ // is intentionally dropped here - box-decoration-break, same as an explicit height.
50
56
  height,
51
57
  children,
52
58
  color: this.color,
@@ -58,25 +64,43 @@ export class RectangleElement extends SizedPDFElement {
58
64
  overflow: this.overflow,
59
65
  });
60
66
  }
67
+ relativeSizeFactor(horizontal) {
68
+ return horizontal ? this.sizeMemory.widthFactor : this.sizeMemory.heightFactor;
69
+ }
70
+ /** A box has no flex children of its own, but it must not swallow the need of the stack inside it: a
71
+ * `Spacer` in a box can only resolve if the box itself was given a bounded extent to pass down. With an
72
+ * explicit size on an axis the box already knows its extent there and asks for nothing. */
73
+ needsBoundedMain(horizontal) {
74
+ const requested = horizontal
75
+ ? [this.sizeMemory.width, this.sizeMemory.widthFactor]
76
+ : [this.sizeMemory.height, this.sizeMemory.heightFactor];
77
+ if (requested.some((v) => v !== undefined))
78
+ return false;
79
+ return this.children.some((c) => c.needsBoundedMain(horizontal));
80
+ }
61
81
  calculateLayout(constraints, offset, ctx) {
82
+ // An explicit extent is a fixed point size or a fraction of the offered box (relative sizing);
83
+ // the fraction only resolves in a bounded region. `undefined` = fill / shrink-wrap below.
84
+ const explicitWidth = resolveExtent(this.sizeMemory.width, this.sizeMemory.widthFactor, constraints.maxWidth, constraints.hasBoundedWidth);
85
+ const explicitHeight = resolveExtent(this.sizeMemory.height, this.sizeMemory.heightFactor, constraints.maxHeight, constraints.hasBoundedHeight);
62
86
  // Width: an explicit size wins (clamped), else fill the offered box. (Without this a
63
87
  // fixed box would balloon to the parent's size.)
64
88
  this.width =
65
- this.sizeMemory.width !== undefined
66
- ? constraints.constrainWidth(this.sizeMemory.width)
89
+ explicitWidth !== undefined
90
+ ? constraints.constrainWidth(explicitWidth)
67
91
  : constraints.hasBoundedWidth
68
92
  ? constraints.maxWidth
69
93
  : this.width;
70
94
  // Width shrink-wrap: no explicit width AND an unbounded region (e.g. a `Box` badge inside a
71
95
  // `Positioned`). Resolved after the children are measured, just below.
72
- const shrinkWrapWidth = this.sizeMemory.width === undefined && !constraints.hasBoundedWidth;
96
+ const shrinkWrapWidth = explicitWidth === undefined && !constraints.hasBoundedWidth;
73
97
  // Height: explicit wins; otherwise FILL a bounded region (e.g. inside an Expanded) but
74
98
  // SHRINK-WRAP the content in an unbounded one (a note box in a stack). Shrink-wrap is
75
99
  // resolved after the children are measured, just below.
76
- const shrinkWrapHeight = this.sizeMemory.height === undefined && !constraints.hasBoundedHeight;
100
+ const shrinkWrapHeight = explicitHeight === undefined && !constraints.hasBoundedHeight;
77
101
  this.height =
78
- this.sizeMemory.height !== undefined
79
- ? constraints.constrainHeight(this.sizeMemory.height)
102
+ explicitHeight !== undefined
103
+ ? constraints.constrainHeight(explicitHeight)
80
104
  : constraints.hasBoundedHeight
81
105
  ? constraints.maxHeight
82
106
  : this.height;
@@ -89,16 +113,28 @@ export class RectangleElement extends SizedPDFElement {
89
113
  ? { origin: { x: 0, y: 0 }, size: { width: 0, height: 0 }, place: [] }
90
114
  : undefined;
91
115
  const childCtx = frame ? { ...ctx, frame } : ctx;
92
- // Lay out children stacked inside the border (inset by the border width). Width is
93
- // finalized here; height is left unbounded so each child sizes to its own content.
116
+ // Lay out children stacked inside the border (inset by the border width). Width is finalized here;
117
+ // height is left unbounded so each child sizes to its own content - EXCEPT for a child that cannot
118
+ // lay itself out without a bound (a stack holding an `Expanded`/`Spacer`). That one gets the height
119
+ // still free inside the box, so its flex child has real leftover space instead of an infinite one.
94
120
  const innerWidth = shrinkWrapWidth
95
121
  ? Infinity
96
122
  : Math.max(0, (this.width ?? 0) - 2 * this.borderWidth);
123
+ const innerHeight = shrinkWrapHeight
124
+ ? Infinity
125
+ : Math.max(0, (this.height ?? 0) - 2 * this.borderWidth);
97
126
  let contentWidth = 0;
98
127
  let contentHeight = 0;
99
128
  let yCursor = this.y + this.borderWidth;
100
129
  for (const child of this.children) {
101
- const childSize = child.calculateLayout(BoxConstraints.loose(innerWidth, Infinity), { x: this.x + this.borderWidth, y: yCursor }, childCtx);
130
+ const consumed = yCursor - (this.y + this.borderWidth);
131
+ // Same rule the flex helper applies: a child gets a finite main extent when it cannot lay itself
132
+ // out without one - a stack holding an `Expanded`/`Spacer`, or a child sized as a percentage of us.
133
+ const needsBound = child.needsBoundedMain(false) || child.relativeSizeFactor(false) !== undefined;
134
+ const childHeight = needsBound
135
+ ? Math.max(0, innerHeight - consumed) // Infinity - consumed stays Infinity, as it should
136
+ : Infinity;
137
+ const childSize = child.calculateLayout(BoxConstraints.loose(innerWidth, childHeight), { x: this.x + this.borderWidth, y: yCursor }, childCtx);
102
138
  yCursor += childSize.height;
103
139
  contentHeight += childSize.height;
104
140
  contentWidth = Math.max(contentWidth, childSize.width);
@@ -8,6 +8,11 @@ interface RowElementParams extends WithChildren {
8
8
  main?: MainAlign;
9
9
  /** Vertical alignment of each child (cross axis); defaults to `stretch`. */
10
10
  cross?: CrossAlign;
11
+ /** Width/height as points (fixed) or a fraction (0..1) of the offered box (relative sizing). */
12
+ width?: number;
13
+ height?: number;
14
+ widthFactor?: number;
15
+ heightFactor?: number;
11
16
  }
12
17
  /**
13
18
  * Horizontal stack: the mirror of `ContainerElement` (Column). Children are laid out
@@ -26,7 +31,12 @@ export declare class RowElement extends SizedPDFElement {
26
31
  private gap;
27
32
  private main;
28
33
  private cross;
29
- constructor({ children, gap, main, cross }: RowElementParams);
34
+ private requested;
35
+ constructor({ children, gap, main, cross, width, height, widthFactor, heightFactor, }: RowElementParams);
36
+ relativeSizeFactor(horizontal: boolean): number | undefined;
37
+ /** The mirror of `ContainerElement`: a Row's OWN flex child needs a bounded width, and the need of any
38
+ * descendant propagates on both axes (see the Column for the reasoning). */
39
+ needsBoundedMain(horizontal: boolean): boolean;
30
40
  calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
31
41
  getProps(): {
32
42
  x: number;
@@ -1,5 +1,6 @@
1
+ import { resolveExtent } from "../layout/box-constraints.js";
1
2
  import { FlexLayoutHelper, HORIZONTAL_AXIS } from "../utils/flex-layout.js";
2
- import { SizedPDFElement } from "./pdf-element.js";
3
+ import { FlexiblePDFElement, SizedPDFElement, } from "./pdf-element.js";
3
4
  /**
4
5
  * Horizontal stack: the mirror of `ContainerElement` (Column). Children are laid out
5
6
  * left-to-right via the shared `FlexLayoutHelper` on the horizontal axis; fixed children
@@ -13,26 +14,54 @@ import { SizedPDFElement } from "./pdf-element.js";
13
14
  * Grid/Table concern.
14
15
  */
15
16
  export class RowElement extends SizedPDFElement {
16
- constructor({ children, gap, main, cross }) {
17
+ constructor({ children, gap, main, cross, width, height, widthFactor, heightFactor, }) {
17
18
  super({ x: 0, y: 0 });
18
19
  this.children = children;
19
20
  this.gap = gap ?? 0;
20
21
  this.main = main ?? "start";
21
22
  this.cross = cross ?? "stretch";
23
+ this.requested = { width, height, widthFactor, heightFactor };
24
+ }
25
+ relativeSizeFactor(horizontal) {
26
+ return horizontal ? this.requested.widthFactor : this.requested.heightFactor;
27
+ }
28
+ /** The mirror of `ContainerElement`: a Row's OWN flex child needs a bounded width, and the need of any
29
+ * descendant propagates on both axes (see the Column for the reasoning). */
30
+ needsBoundedMain(horizontal) {
31
+ const requested = horizontal
32
+ ? [this.requested.width, this.requested.widthFactor]
33
+ : [this.requested.height, this.requested.heightFactor];
34
+ if (requested.some((v) => v !== undefined))
35
+ return false;
36
+ const ownFlexChild = horizontal && this.children.some((c) => c instanceof FlexiblePDFElement);
37
+ return ownFlexChild || this.children.some((c) => c.needsBoundedMain(horizontal));
22
38
  }
23
39
  calculateLayout(constraints, offset, ctx) {
24
40
  this.x = offset.x;
25
41
  this.y = offset.y;
26
- // Width fills the offered space (flex children split the leftover); height is the
27
- // tallest child unless the parent bounds it.
28
- const mainAvail = constraints.hasBoundedWidth ? constraints.maxWidth : Infinity;
29
- const crossAvail = constraints.hasBoundedHeight ? constraints.maxHeight : Infinity;
42
+ // Relative sizing: a pinned extent (fixed or a fraction of the offered box, clamped) wins; else
43
+ // width fills the offered space (flex children split the leftover) and height is the tallest child.
44
+ const explicitWidth = resolveExtent(this.requested.width, this.requested.widthFactor, constraints.maxWidth, constraints.hasBoundedWidth);
45
+ const explicitHeight = resolveExtent(this.requested.height, this.requested.heightFactor, constraints.maxHeight, constraints.hasBoundedHeight);
46
+ const boundedWidth = explicitWidth !== undefined
47
+ ? constraints.constrainWidth(explicitWidth)
48
+ : constraints.hasBoundedWidth
49
+ ? constraints.maxWidth
50
+ : undefined;
51
+ const boundedHeight = explicitHeight !== undefined
52
+ ? constraints.constrainHeight(explicitHeight)
53
+ : constraints.hasBoundedHeight
54
+ ? constraints.maxHeight
55
+ : undefined;
56
+ // Horizontal stack: main = width (children fill it), cross = height (children can stretch to it).
57
+ const mainAvail = boundedWidth ?? Infinity;
58
+ const crossAvail = boundedHeight ?? Infinity;
30
59
  let result = { mainUsed: 0, crossUsed: 0 };
31
60
  if (this.children.length > 0) {
32
61
  result = FlexLayoutHelper.layout(this.children, HORIZONTAL_AXIS, mainAvail, crossAvail, this.x, this.y, { gap: this.gap, main: this.main, cross: this.cross }, ctx);
33
62
  }
34
- this.width = constraints.hasBoundedWidth ? constraints.maxWidth : result.mainUsed;
35
- this.height = constraints.hasBoundedHeight ? constraints.maxHeight : result.crossUsed;
63
+ this.width = boundedWidth ?? result.mainUsed;
64
+ this.height = boundedHeight ?? result.crossUsed;
36
65
  return { width: this.width, height: this.height };
37
66
  }
38
67
  getProps() {
@@ -10,6 +10,10 @@ export interface TextSegment {
10
10
  fontColor?: Color;
11
11
  fontFamily?: string;
12
12
  fontSize?: number;
13
+ /** External URL: this segment becomes an inline hyperlink (a /Link annotation over its glyphs). */
14
+ href?: string;
15
+ /** Internal named destination (an `Anchor`): this segment links to it (a /GoTo /Link annotation). */
16
+ dest?: string;
13
17
  }
14
18
  /** Accessibility role for the tagged structure tree: a heading level or a paragraph (the default). */
15
19
  export type TextRole = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p";
@@ -103,6 +103,64 @@ export interface ClipPush {
103
103
  export interface ClipPop {
104
104
  type: "clip-pop";
105
105
  }
106
+ /**
107
+ * A hyperlink over a rectangle. Unlike every other IR node this draws NOTHING into the content stream -
108
+ * it becomes a PDF Link ANNOTATION on the page (`/Annots`), a side channel next to the drawing. Exactly
109
+ * one target is set: `href` is an external URL (a `/URI` action); `dest` is a named destination inside
110
+ * this document (a `/GoTo` action, resolved via the catalog `/Names /Dests` tree - see `Anchor`). The
111
+ * rect is the clickable region in top-left engine coords; the Y-flip converts it to PDF page space.
112
+ *
113
+ * NOTE: a Link inside a `TransformPush` is NOT yet rotated with it - the annotation rect lives in page
114
+ * space and ignores the content-stream matrix. Completing that is a planned follow-up (see todo.md).
115
+ */
116
+ export interface Link {
117
+ type: "link";
118
+ x: number;
119
+ y: number;
120
+ width: number;
121
+ height: number;
122
+ href?: string;
123
+ dest?: string;
124
+ }
125
+ /**
126
+ * A named destination anchor - a jump TARGET for an internal `Link({ to })`. Like `Link` it draws
127
+ * NOTHING; it registers `name` -> (this page, `y`) in the catalog `/Names /Dests` name tree. `y` is the
128
+ * top of the target in top-left engine coords (the Y-flip turns it into the page-space scroll target).
129
+ */
130
+ export interface Anchor {
131
+ type: "anchor";
132
+ y: number;
133
+ name: string;
134
+ }
135
+ /**
136
+ * A document-outline (bookmark) anchor. Like `Link` it draws NOTHING - it becomes an entry in the PDF
137
+ * outline tree (`/Outlines`), a side channel the viewer shows in its bookmark panel. `y` is the top of
138
+ * the target in top-left engine coords (the Y-flip turns it into the page-space scroll target); `level`
139
+ * (1-based) nests the entry under the nearest preceding entry of a smaller level.
140
+ */
141
+ export interface Outline {
142
+ type: "outline";
143
+ y: number;
144
+ title: string;
145
+ level: number;
146
+ }
147
+ /**
148
+ * Pushes an affine transform (a `q` + `cm` in the content stream): everything between this and the
149
+ * matching `TransformPop` is painted through `matrix`. This is what `Rotated` wraps its child's nodes
150
+ * in, so a subtree draws rotated (or scaled/translated) around a pivot without touching its layout.
151
+ *
152
+ * `matrix` is `[a, b, c, d, e, f]` in the engine's TOP-LEFT coordinates - the same space every other IR
153
+ * node uses. The Y-flip at the IR -> backend seam converts it to PDF's bottom-left once per page, so
154
+ * producers stay coordinate-system-blind (they never see a flipped matrix).
155
+ */
156
+ export interface TransformPush {
157
+ type: "transform-push";
158
+ matrix: [number, number, number, number, number, number];
159
+ }
160
+ /** Closes the most recent `TransformPush` (restores the graphics state). */
161
+ export interface TransformPop {
162
+ type: "transform-pop";
163
+ }
106
164
  /**
107
165
  * One drawing command of a `Path`, in absolute page coordinates. Curves are CUBIC because PDF
108
166
  * content streams have no quadratic operator - a producer converts TrueType quads to cubics before
@@ -168,4 +226,4 @@ export interface Path {
168
226
  tag?: StructTag;
169
227
  }
170
228
  /** The closed set of primitives the PDF backend knows how to draw. */
171
- export type IRNode = TextRun | Rect | Line | Image | ClipPush | ClipPop | Path;
229
+ export type IRNode = TextRun | Rect | Line | Image | ClipPush | ClipPop | Path | Link | Outline | Anchor | TransformPush | TransformPop;
@@ -44,6 +44,14 @@ export declare class BoxConstraints {
44
44
  /** Returns the constraints clamped to lie within `parent` (Flutter's enforce). */
45
45
  enforce(parent: BoxConstraints): BoxConstraints;
46
46
  }
47
+ /**
48
+ * Resolves a sized element's extent on one axis (relative sizing). An explicit point size (`fixed`)
49
+ * always wins; otherwise a `factor` (0..1) of the offered extent, but ONLY when that axis is bounded -
50
+ * a fraction of an unbounded axis has no meaning (Flutter's FractionallySizedBox under unbounded
51
+ * constraints). Returns `undefined` to mean "no explicit extent", i.e. fill the box or shrink-wrap.
52
+ * Shared by every sized element so `width: "50%"` behaves identically across Box / Column / Row / Image.
53
+ */
54
+ export declare function resolveExtent(fixed: number | undefined, factor: number | undefined, boundedMax: number, hasBounded: boolean): number | undefined;
47
55
  /** The size an element resolves to and returns UP the tree. */
48
56
  export interface Size {
49
57
  width: number;
@@ -67,3 +67,17 @@ export class BoxConstraints {
67
67
  return new BoxConstraints(Math.max(parent.minWidth, Math.min(this.minWidth, parent.maxWidth)), Math.max(parent.minWidth, Math.min(this.maxWidth, parent.maxWidth)), Math.max(parent.minHeight, Math.min(this.minHeight, parent.maxHeight)), Math.max(parent.minHeight, Math.min(this.maxHeight, parent.maxHeight)));
68
68
  }
69
69
  }
70
+ /**
71
+ * Resolves a sized element's extent on one axis (relative sizing). An explicit point size (`fixed`)
72
+ * always wins; otherwise a `factor` (0..1) of the offered extent, but ONLY when that axis is bounded -
73
+ * a fraction of an unbounded axis has no meaning (Flutter's FractionallySizedBox under unbounded
74
+ * constraints). Returns `undefined` to mean "no explicit extent", i.e. fill the box or shrink-wrap.
75
+ * Shared by every sized element so `width: "50%"` behaves identically across Box / Column / Row / Image.
76
+ */
77
+ export function resolveExtent(fixed, factor, boundedMax, hasBounded) {
78
+ if (fixed !== undefined)
79
+ return fixed;
80
+ if (factor !== undefined && hasBounded)
81
+ return boundedMax * factor;
82
+ return undefined;
83
+ }
@@ -0,0 +1,9 @@
1
+ import { PDFElement } from "../elements/pdf-element.js";
2
+ import { ImageElement } from "../elements/image-element.js";
3
+ /**
4
+ * Walks the element tree (via `getProps`, the same shallow-reflection style as the validator) and
5
+ * returns every `ImageElement`. A pre-layout pass uses this to resolve each image's intrinsic pixel
6
+ * size asynchronously: layout is synchronous and cannot await jimp, yet it needs the aspect ratio to
7
+ * give a width-only image its proportional height. Cheap - images are few and this runs once.
8
+ */
9
+ export declare function collectImageElements(root: PDFElement): ImageElement[];
@@ -0,0 +1,34 @@
1
+ import { PDFElement, hasChildrenProp, hasChildProp } from "../elements/pdf-element.js";
2
+ import { ImageElement } from "../elements/image-element.js";
3
+ /**
4
+ * Walks the element tree (via `getProps`, the same shallow-reflection style as the validator) and
5
+ * returns every `ImageElement`. A pre-layout pass uses this to resolve each image's intrinsic pixel
6
+ * size asynchronously: layout is synchronous and cannot await jimp, yet it needs the aspect ratio to
7
+ * give a width-only image its proportional height. Cheap - images are few and this runs once.
8
+ */
9
+ export function collectImageElements(root) {
10
+ const out = [];
11
+ const visit = (el) => {
12
+ if (el instanceof ImageElement)
13
+ out.push(el);
14
+ const props = el.getProps();
15
+ if (hasChildrenProp(props))
16
+ for (const c of props.children)
17
+ visit(c);
18
+ else if (hasChildProp(props))
19
+ visit(props.child);
20
+ // Wrappers that hold a subtree outside `children`/`child`: a Page's header/footer and a
21
+ // RepeatingHeaderElement's `body` (a Table with a header wraps its rows there). A
22
+ // DeferredElement's `composed` is null until layout runs, so it cannot be reached from this
23
+ // pre-layout pass; an image inside a deferred subtree just skips aspect pre-resolution.
24
+ const w = props;
25
+ if (w.header instanceof PDFElement)
26
+ visit(w.header);
27
+ if (w.footer instanceof PDFElement)
28
+ visit(w.footer);
29
+ if (w.body instanceof PDFElement)
30
+ visit(w.body);
31
+ };
32
+ visit(root);
33
+ return out;
34
+ }
@@ -0,0 +1,6 @@
1
+ import { PDFObjectManager } from "../utils/pdf-object-manager.js";
2
+ import { AnchorElement } from "../elements/layout/anchor-element.js";
3
+ import { IRNode } from "../ir/display-list.js";
4
+ export declare class AnchorRenderer {
5
+ static render(element: AnchorElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
6
+ }
@@ -0,0 +1,11 @@
1
+ import { RendererRegistry } from "../utils/renderer-registry.js";
2
+ export class AnchorRenderer {
3
+ static async render(element, objectManager) {
4
+ const { name, child, y } = element.getProps();
5
+ const renderer = RendererRegistry.getRenderer(child);
6
+ const childNodes = renderer ? await renderer(child, objectManager) : [];
7
+ // The child draws normally; an Anchor IR node carries the name + scroll target for the page renderer
8
+ // to register as a named destination (it produces no content-stream ops itself).
9
+ return [...childNodes, { type: "anchor", y, name }];
10
+ }
11
+ }
@@ -0,0 +1,6 @@
1
+ import { PDFObjectManager } from "../utils/pdf-object-manager.js";
2
+ import { BookmarkElement } from "../elements/layout/bookmark-element.js";
3
+ import { IRNode } from "../ir/display-list.js";
4
+ export declare class BookmarkRenderer {
5
+ static render(element: BookmarkElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
6
+ }