@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
@@ -1,13 +1,15 @@
1
1
  import { FlexLayoutHelper, VERTICAL_AXIS } from "../utils/flex-layout.js";
2
+ import { resolveExtent } from "../layout/box-constraints.js";
2
3
  import { packChildren } from "../layout/fragmentation.js";
3
- import { SizedPDFElement, } from "./pdf-element.js";
4
+ import { FlexiblePDFElement, SizedPDFElement, } from "./pdf-element.js";
4
5
  export class ContainerElement extends SizedPDFElement {
5
- constructor({ x, y, width, height, children, gap, main, cross }) {
6
+ constructor({ x, y, width, height, children, gap, main, cross, widthFactor, heightFactor, }) {
6
7
  super({ x, y, width, height });
7
8
  this.children = children;
8
9
  this.gap = gap ?? 0;
9
10
  this.main = main ?? "start";
10
11
  this.cross = cross ?? "stretch";
12
+ this.requested = { width, height, widthFactor, heightFactor };
11
13
  }
12
14
  /**
13
15
  * Splits the vertical stack across pages. Children are measured against the content
@@ -35,27 +37,59 @@ export class ContainerElement extends SizedPDFElement {
35
37
  return new ContainerElement({
36
38
  x: this.x,
37
39
  y: this.y,
38
- width: this.width,
39
- height: this.height,
40
+ // Carry the requested WIDTH (a %-width column keeps its width on every page); the height is
41
+ // intentionally dropped so each fragment shrink-wraps to the content it actually holds.
42
+ width: this.requested.width,
43
+ widthFactor: this.requested.widthFactor,
40
44
  children,
41
45
  gap: this.gap,
42
46
  main: this.main,
43
47
  cross: this.cross,
44
48
  });
45
49
  }
50
+ relativeSizeFactor(horizontal) {
51
+ return horizontal ? this.requested.widthFactor : this.requested.heightFactor;
52
+ }
53
+ /**
54
+ * A Column stacks vertically, so a flex child of its OWN only ever needs a bounded height. But the need
55
+ * also propagates: a `Spacer` two levels down is just as unable to resolve against an infinite axis, so
56
+ * we ask the children too - on both axes, since a nested Row's `Spacer` needs a bounded WIDTH and a
57
+ * Column nested in a Row is exactly where that width goes missing.
58
+ *
59
+ * An explicit extent on an axis short-circuits it: the element already knows how big it is there, and
60
+ * with no flex descendant nothing needs distributing, so it keeps shrink-wrapping. That is what leaves
61
+ * every existing layout untouched.
62
+ */
63
+ needsBoundedMain(horizontal) {
64
+ const requested = horizontal
65
+ ? [this.requested.width, this.requested.widthFactor]
66
+ : [this.requested.height, this.requested.heightFactor];
67
+ if (requested.some((v) => v !== undefined))
68
+ return false;
69
+ const ownFlexChild = !horizontal && this.children.some((c) => c instanceof FlexiblePDFElement);
70
+ return ownFlexChild || this.children.some((c) => c.needsBoundedMain(horizontal));
71
+ }
46
72
  calculateLayout(constraints, offset, ctx) {
47
- // The container fills the width/height it is offered; when an axis is unbounded it
48
- // shrink-wraps to its children instead (mirrors how Row shrink-wraps). Width unbounded
49
- // happens for a Column nested in a Row (the Row offers its fixed children unbounded
50
- // width); passing 0 there would collapse every child to width 0.
51
- if (constraints.hasBoundedWidth)
52
- this.width = constraints.maxWidth;
53
- if (constraints.hasBoundedHeight)
54
- this.height = constraints.maxHeight;
55
73
  this.x = offset.x;
56
74
  this.y = offset.y;
57
- const crossAvail = constraints.hasBoundedWidth ? this.width : Infinity;
58
- const mainAvail = constraints.hasBoundedHeight ? this.height : Infinity;
75
+ // Relative sizing: a pinned extent (fixed points or a fraction of the offered box, clamped into
76
+ // the constraints) wins; else fill a bounded axis; else stay `undefined` and shrink-wrap below.
77
+ // A Column nested in a Row gets unbounded width - passing 0 there would collapse children to 0.
78
+ const explicitWidth = resolveExtent(this.requested.width, this.requested.widthFactor, constraints.maxWidth, constraints.hasBoundedWidth);
79
+ const explicitHeight = resolveExtent(this.requested.height, this.requested.heightFactor, constraints.maxHeight, constraints.hasBoundedHeight);
80
+ const boundedWidth = explicitWidth !== undefined
81
+ ? constraints.constrainWidth(explicitWidth)
82
+ : constraints.hasBoundedWidth
83
+ ? constraints.maxWidth
84
+ : undefined;
85
+ const boundedHeight = explicitHeight !== undefined
86
+ ? constraints.constrainHeight(explicitHeight)
87
+ : constraints.hasBoundedHeight
88
+ ? constraints.maxHeight
89
+ : undefined;
90
+ // Vertical stack: cross = width (children fill it), main = height (the stacking extent).
91
+ const crossAvail = boundedWidth ?? Infinity;
92
+ const mainAvail = boundedHeight ?? Infinity;
59
93
  let result = { mainUsed: 0, crossUsed: 0 };
60
94
  if (this.children) {
61
95
  // Vertical flex stack (main = height, cross = width). The shared helper measures
@@ -63,14 +97,12 @@ export class ContainerElement extends SizedPDFElement {
63
97
  // in source order.
64
98
  result = FlexLayoutHelper.layout(this.children, VERTICAL_AXIS, mainAvail, crossAvail, this.y, this.x, { gap: this.gap, main: this.main, cross: this.cross }, ctx);
65
99
  }
66
- // Bounded: fill the offered extent. Unbounded: shrink to the children (height = the
67
- // stack, width = the widest child). Top-left coordinates; the container draws nothing,
68
- // and the Y-flip happens once at the IR -> backend seam.
69
- const width = constraints.hasBoundedWidth ? this.width : result.crossUsed;
70
- const height = constraints.hasBoundedHeight ? this.height : result.mainUsed;
71
- this.width = width;
72
- this.height = height;
73
- return { width, height };
100
+ // Pinned/bounded axis takes that extent; an unbounded one shrink-wraps to the children (height =
101
+ // the stack, width = the widest child). Top-left coordinates; the container draws nothing, and
102
+ // the Y-flip happens once at the IR -> backend seam.
103
+ this.width = boundedWidth ?? result.crossUsed;
104
+ this.height = boundedHeight ?? result.mainUsed;
105
+ return { width: this.width, height: this.height };
74
106
  }
75
107
  getProps() {
76
108
  return {
@@ -19,6 +19,7 @@ export declare class CustomLocalImage extends CustomImage {
19
19
  private imagePath;
20
20
  private fileBuffer;
21
21
  private fileRawData;
22
+ private dims?;
22
23
  constructor(imagePath: string);
23
24
  init(): Promise<void>;
24
25
  getImageType(): Promise<string>;
@@ -36,6 +37,7 @@ export declare class CustomLocalImage extends CustomImage {
36
37
  export declare class CustomBytesImage extends CustomImage {
37
38
  private bytes;
38
39
  private fileRawData;
40
+ private dims?;
39
41
  constructor(bytes: Uint8Array);
40
42
  init(): Promise<void>;
41
43
  getImageType(): Promise<string>;
@@ -49,6 +51,10 @@ interface ImageElementParams {
49
51
  image: CustomImage;
50
52
  width?: number;
51
53
  height?: number;
54
+ /** Width as a fraction (0..1) of the offered width instead of a fixed `width` (relative sizing). */
55
+ widthFactor?: number;
56
+ /** Height as a fraction (0..1) of the offered height; see `widthFactor`. */
57
+ heightFactor?: number;
52
58
  fit?: BoxFit;
53
59
  /** Corner radius in points; rounds the image box (0 = sharp, default). */
54
60
  radius?: number;
@@ -57,10 +63,22 @@ interface ImageElementParams {
57
63
  }
58
64
  export declare class ImageElement extends SizedPDFElement {
59
65
  private image;
66
+ private widthFactor?;
67
+ private heightFactor?;
60
68
  private fit;
61
69
  private radius;
62
70
  private readonly alt?;
63
- constructor({ image, width, height, fit, radius, alt }: ImageElementParams);
71
+ private requested;
72
+ private intrinsic?;
73
+ constructor({ image, width, height, widthFactor, heightFactor, fit, radius, alt, }: ImageElementParams);
74
+ /**
75
+ * Loads the image and records its intrinsic pixel size. Runs in the async pre-layout pass (layout
76
+ * itself is synchronous and cannot await jimp) so `calculateLayout` can turn a width-only image
77
+ * into a proportional box. Best-effort: a load failure leaves the size unresolved (the renderer
78
+ * surfaces the real error) and the image just keeps whatever explicit size it was given.
79
+ */
80
+ resolveIntrinsicSize(): Promise<void>;
81
+ relativeSizeFactor(horizontal: boolean): number | undefined;
64
82
  calculateLayout(constraints: BoxConstraints, offset: Offset, _ctx: LayoutContext): Size;
65
83
  getProps(): {
66
84
  x: number;
@@ -1,6 +1,7 @@
1
1
  import { getImageDimensions } from "../utils/image-helper.js";
2
2
  import { latin1FromBytes } from "../utils/bytes.js";
3
3
  import { readFileBytesAsync } from "../platform/node-fs.js";
4
+ import { resolveExtent } from "../layout/box-constraints.js";
4
5
  import { SizedPDFElement } from "./pdf-element.js";
5
6
  // path.extname without node:path (browser-safe): the substring from the last dot, if it sits after the
6
7
  // last slash (so a dot in a directory name does not count). Enough for image file extensions.
@@ -24,6 +25,8 @@ export class CustomLocalImage extends CustomImage {
24
25
  this.imagePath = imagePath;
25
26
  }
26
27
  async init() {
28
+ if (this.fileBuffer)
29
+ return; // idempotent: the pre-layout size pass and the renderer both call init()
27
30
  try {
28
31
  // Loading image and convert it to base64
29
32
  await this.loadImage(this.imagePath);
@@ -59,13 +62,13 @@ export class CustomLocalImage extends CustomImage {
59
62
  return this.fileRawData;
60
63
  }
61
64
  async getImageDimensions() {
65
+ if (this.dims)
66
+ return this.dims; // reuse: the pre-layout size pass and the renderer both ask
62
67
  if (!this.fileBuffer) {
63
68
  throw new Error("You must first call the `loadAndConvertImage` method");
64
69
  }
65
- // Since now (30.09.2024) we using "Jimp" - So we don't need our custom method to get the image dimension.
66
- // But at the moment I let it still here...
67
- const dimensions = await getImageDimensions(this.fileBuffer);
68
- return dimensions;
70
+ this.dims = await getImageDimensions(this.fileBuffer);
71
+ return this.dims;
69
72
  }
70
73
  }
71
74
  /**
@@ -91,26 +94,64 @@ export class CustomBytesImage extends CustomImage {
91
94
  return this.fileRawData;
92
95
  }
93
96
  async getImageDimensions() {
94
- return getImageDimensions(this.bytes);
97
+ if (!this.dims)
98
+ this.dims = await getImageDimensions(this.bytes); // reuse across pre-pass + render
99
+ return this.dims;
95
100
  }
96
101
  }
97
102
  export class ImageElement extends SizedPDFElement {
98
- constructor({ image, width, height, fit = BoxFit.none, radius, alt }) {
103
+ constructor({ image, width, height, widthFactor, heightFactor, fit = BoxFit.none, radius, alt, }) {
104
+ // Seed this.width/height from the request so getProps() reflects it before layout; calculateLayout
105
+ // then reads `requested` (never the mutated fields) and overwrites these with the laid-out size.
99
106
  super({ x: 0, y: 0, width });
100
107
  this.image = image;
101
108
  this.height = height;
109
+ this.requested = { width, height };
110
+ this.widthFactor = widthFactor;
111
+ this.heightFactor = heightFactor;
102
112
  this.fit = fit;
103
113
  this.radius = radius ?? 0;
104
114
  this.alt = alt;
105
115
  }
116
+ /**
117
+ * Loads the image and records its intrinsic pixel size. Runs in the async pre-layout pass (layout
118
+ * itself is synchronous and cannot await jimp) so `calculateLayout` can turn a width-only image
119
+ * into a proportional box. Best-effort: a load failure leaves the size unresolved (the renderer
120
+ * surfaces the real error) and the image just keeps whatever explicit size it was given.
121
+ */
122
+ async resolveIntrinsicSize() {
123
+ try {
124
+ await this.image.init();
125
+ this.intrinsic = await this.image.getImageDimensions();
126
+ }
127
+ catch {
128
+ // leave intrinsic undefined - aspect derivation simply doesn't fire
129
+ }
130
+ }
131
+ relativeSizeFactor(horizontal) {
132
+ return horizontal ? this.widthFactor : this.heightFactor;
133
+ }
106
134
  calculateLayout(constraints, offset, _ctx) {
107
135
  this.x = offset.x;
108
136
  this.y = offset.y;
109
- // A bounded axis overrides the intrinsic/explicit size; otherwise keep our own.
110
- if (constraints.hasBoundedWidth)
111
- this.width = constraints.maxWidth;
112
- if (constraints.hasBoundedHeight)
113
- this.height = constraints.maxHeight;
137
+ // Relative sizing: a fixed size or a fraction of the offered box (fraction only in a bounded axis).
138
+ // Read from `requested` (not this.width/height, which we overwrite below) so every pass re-resolves.
139
+ let w = resolveExtent(this.requested.width, this.widthFactor, constraints.maxWidth, constraints.hasBoundedWidth);
140
+ let h = resolveExtent(this.requested.height, this.heightFactor, constraints.maxHeight, constraints.hasBoundedHeight);
141
+ // Aspect auto-size: when the user pinned exactly ONE axis, derive the other from the intrinsic
142
+ // ratio (CSS `width: 50%; height: auto`). Only fires once the pre-pass resolved the pixel size.
143
+ if (this.intrinsic && this.intrinsic.width > 0 && this.intrinsic.height > 0) {
144
+ const ratio = this.intrinsic.width / this.intrinsic.height;
145
+ if (w !== undefined && h === undefined)
146
+ h = w / ratio;
147
+ else if (h !== undefined && w === undefined)
148
+ w = h * ratio;
149
+ }
150
+ // Fall back to the prior behavior for an unpinned axis: a bounded axis fills, otherwise keep the
151
+ // requested fixed size (or undefined). Read from `requested`, never the mutated this.width/height.
152
+ this.width = w ?? (constraints.hasBoundedWidth ? constraints.maxWidth : this.requested.width);
153
+ this.height =
154
+ h ?? (constraints.hasBoundedHeight ? constraints.maxHeight : this.requested.height);
114
155
  // Top-left coordinates; the fit logic (renderer) and the Y-flip (seam) run later.
115
156
  return { width: this.width ?? 0, height: this.height ?? 0 };
116
157
  }
@@ -8,5 +8,11 @@ export * from "./layout/expanded-element.js";
8
8
  export * from "./layout/padding-element.js";
9
9
  export * from "./layout/default-text-style-element.js";
10
10
  export * from "./layout/positioned-element.js";
11
+ export * from "./layout/link-element.js";
12
+ export * from "./layout/bookmark-element.js";
13
+ export * from "./layout/anchor-element.js";
14
+ export * from "./layout/page-builder-element.js";
15
+ export * from "./layout/rotated-element.js";
16
+ export * from "./layout/rotated-box-element.js";
11
17
  export * from "./line-element.js";
12
18
  export * from "./row-element.js";
@@ -8,5 +8,11 @@ export * from "./layout/expanded-element.js";
8
8
  export * from "./layout/padding-element.js";
9
9
  export * from "./layout/default-text-style-element.js";
10
10
  export * from "./layout/positioned-element.js";
11
+ export * from "./layout/link-element.js";
12
+ export * from "./layout/bookmark-element.js";
13
+ export * from "./layout/anchor-element.js";
14
+ export * from "./layout/page-builder-element.js";
15
+ export * from "./layout/rotated-element.js";
16
+ export * from "./layout/rotated-box-element.js";
11
17
  export * from "./line-element.js";
12
18
  export * from "./row-element.js";
@@ -0,0 +1,23 @@
1
+ import { PDFElement, LayoutContext } from "../pdf-element.js";
2
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
3
+ /**
4
+ * Marks its child as a named jump target for an internal `Link({ to: name })`. Layout-transparent: it
5
+ * delegates `calculateLayout` to the child and records the child's top, which the renderer turns into an
6
+ * `Anchor` IR node (and PDFRenderer into a /Names /Dests entry). It draws nothing - the child renders
7
+ * exactly as it would on its own.
8
+ */
9
+ export declare class AnchorElement extends PDFElement {
10
+ private name;
11
+ private child;
12
+ private y;
13
+ constructor({ name, child }: {
14
+ name: string;
15
+ child: PDFElement;
16
+ });
17
+ calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
18
+ getProps(): {
19
+ name: string;
20
+ child: PDFElement;
21
+ y: number;
22
+ };
23
+ }
@@ -0,0 +1,27 @@
1
+ import { PDFElement } from "../pdf-element.js";
2
+ /**
3
+ * Marks its child as a named jump target for an internal `Link({ to: name })`. Layout-transparent: it
4
+ * delegates `calculateLayout` to the child and records the child's top, which the renderer turns into an
5
+ * `Anchor` IR node (and PDFRenderer into a /Names /Dests entry). It draws nothing - the child renders
6
+ * exactly as it would on its own.
7
+ */
8
+ export class AnchorElement extends PDFElement {
9
+ constructor({ name, child }) {
10
+ super();
11
+ this.y = 0;
12
+ this.name = name;
13
+ this.child = child;
14
+ }
15
+ calculateLayout(constraints, offset, ctx) {
16
+ const size = this.child.calculateLayout(constraints, offset, ctx);
17
+ this.y = offset.y; // the child's top - the scroll target for a link that jumps here
18
+ return size;
19
+ }
20
+ getProps() {
21
+ return {
22
+ name: this.name,
23
+ child: this.child,
24
+ y: this.y,
25
+ };
26
+ }
27
+ }
@@ -0,0 +1,27 @@
1
+ import { PDFElement, LayoutContext } from "../pdf-element.js";
2
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
3
+ /**
4
+ * Adds a document-outline entry (a bookmark) that jumps to its child. Layout-transparent: it delegates
5
+ * `calculateLayout` to the child and records the child's top, which the renderer turns into an `Outline`
6
+ * IR node (and PDFRenderer into a /Outlines tree entry). `title` is the label shown in the viewer's
7
+ * bookmark panel; `level` (1-based, default 1) nests it under the nearest preceding smaller level. It
8
+ * draws nothing - the child renders exactly as it would without the bookmark.
9
+ */
10
+ export declare class BookmarkElement extends PDFElement {
11
+ private title;
12
+ private level;
13
+ private child;
14
+ private y;
15
+ constructor({ title, level, child }: {
16
+ title: string;
17
+ level?: number;
18
+ child: PDFElement;
19
+ });
20
+ calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
21
+ getProps(): {
22
+ title: string;
23
+ level: number;
24
+ child: PDFElement;
25
+ y: number;
26
+ };
27
+ }
@@ -0,0 +1,30 @@
1
+ import { PDFElement } from "../pdf-element.js";
2
+ /**
3
+ * Adds a document-outline entry (a bookmark) that jumps to its child. Layout-transparent: it delegates
4
+ * `calculateLayout` to the child and records the child's top, which the renderer turns into an `Outline`
5
+ * IR node (and PDFRenderer into a /Outlines tree entry). `title` is the label shown in the viewer's
6
+ * bookmark panel; `level` (1-based, default 1) nests it under the nearest preceding smaller level. It
7
+ * draws nothing - the child renders exactly as it would without the bookmark.
8
+ */
9
+ export class BookmarkElement extends PDFElement {
10
+ constructor({ title, level = 1, child }) {
11
+ super();
12
+ this.y = 0;
13
+ this.title = title;
14
+ this.level = level;
15
+ this.child = child;
16
+ }
17
+ calculateLayout(constraints, offset, ctx) {
18
+ const size = this.child.calculateLayout(constraints, offset, ctx);
19
+ this.y = offset.y; // the child's top - the scroll target for the bookmark
20
+ return size;
21
+ }
22
+ getProps() {
23
+ return {
24
+ title: this.title,
25
+ level: this.level,
26
+ child: this.child,
27
+ y: this.y,
28
+ };
29
+ }
30
+ }
@@ -0,0 +1,33 @@
1
+ import { PDFElement, LayoutContext } from "../pdf-element.js";
2
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
3
+ /**
4
+ * Makes its child a clickable hyperlink. `href` is an external URL; `dest` is an internal named
5
+ * destination (an `Anchor` elsewhere in the document) - exactly one is set. Layout-transparent: it
6
+ * delegates `calculateLayout` to the child and records the resulting box, which the renderer turns into
7
+ * a `Link` IR node (and the page renderer into a /Link annotation). The link draws nothing itself -
8
+ * style the child (e.g. a blue Text) if you want it to look like a link.
9
+ */
10
+ export declare class LinkElement extends PDFElement {
11
+ private href?;
12
+ private dest?;
13
+ private child;
14
+ private x;
15
+ private y;
16
+ private width;
17
+ private height;
18
+ constructor({ href, dest, child }: {
19
+ href?: string;
20
+ dest?: string;
21
+ child: PDFElement;
22
+ });
23
+ calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
24
+ getProps(): {
25
+ href: string | undefined;
26
+ dest: string | undefined;
27
+ child: PDFElement;
28
+ x: number;
29
+ y: number;
30
+ width: number;
31
+ height: number;
32
+ };
33
+ }
@@ -0,0 +1,39 @@
1
+ import { PDFElement } from "../pdf-element.js";
2
+ /**
3
+ * Makes its child a clickable hyperlink. `href` is an external URL; `dest` is an internal named
4
+ * destination (an `Anchor` elsewhere in the document) - exactly one is set. Layout-transparent: it
5
+ * delegates `calculateLayout` to the child and records the resulting box, which the renderer turns into
6
+ * a `Link` IR node (and the page renderer into a /Link annotation). The link draws nothing itself -
7
+ * style the child (e.g. a blue Text) if you want it to look like a link.
8
+ */
9
+ export class LinkElement extends PDFElement {
10
+ constructor({ href, dest, child }) {
11
+ super();
12
+ this.x = 0;
13
+ this.y = 0;
14
+ this.width = 0;
15
+ this.height = 0;
16
+ this.href = href;
17
+ this.dest = dest;
18
+ this.child = child;
19
+ }
20
+ calculateLayout(constraints, offset, ctx) {
21
+ const size = this.child.calculateLayout(constraints, offset, ctx);
22
+ this.x = offset.x;
23
+ this.y = offset.y;
24
+ this.width = size.width;
25
+ this.height = size.height;
26
+ return size;
27
+ }
28
+ getProps() {
29
+ return {
30
+ href: this.href,
31
+ dest: this.dest,
32
+ child: this.child,
33
+ x: this.x,
34
+ y: this.y,
35
+ width: this.width,
36
+ height: this.height,
37
+ };
38
+ }
39
+ }
@@ -0,0 +1,32 @@
1
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
2
+ import { LayoutContext, PageInfo, PDFElement } from "../pdf-element.js";
3
+ /**
4
+ * Builds its subtree from the page it lands on, so a header, a footer or plain body content can show
5
+ * `pageNumber` / `pageCount`. The closure runs on EVERY layout, because the numbers differ between the
6
+ * two driver passes: pagination has to size the element before the page total exists, so it builds
7
+ * against a provisional "1 of 1"; the render pass then rebuilds it with the real `PageInfo`.
8
+ *
9
+ * Two consequences, and neither is magic - both come from the same chicken-and-egg: the content decides
10
+ * the page count that the content displays.
11
+ *
12
+ * 1. In the flowing BODY the box is reserved by the provisional build, so a much wider final string
13
+ * (`"9 of 10"` vs `"1 of 1"`) can paint slightly past it. Keep dynamic body content roughly constant
14
+ * in width.
15
+ * 2. A header/footer is re-laid out per page, so its real height is honoured - but the BODY band was
16
+ * already sized against the provisional build. A later page whose header is SHORTER simply gains
17
+ * room (harmless). A later page whose header is TALLER shrinks the band its body was measured for,
18
+ * and that body can overflow. So: a conditional header may shrink on later pages, never grow.
19
+ */
20
+ export declare class PageBuilderElement extends PDFElement {
21
+ private composed?;
22
+ private readonly build;
23
+ constructor({ build }: {
24
+ build: (info: PageInfo) => PDFElement;
25
+ });
26
+ /** The real page info during the render pass; a provisional "1 of 1" while paginating. */
27
+ private infoFrom;
28
+ calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
29
+ getProps(): {
30
+ composed: PDFElement | undefined;
31
+ };
32
+ }
@@ -0,0 +1,43 @@
1
+ import { PDFElement } from "../pdf-element.js";
2
+ import { resolvePageSize } from "../page-element.js";
3
+ /**
4
+ * Builds its subtree from the page it lands on, so a header, a footer or plain body content can show
5
+ * `pageNumber` / `pageCount`. The closure runs on EVERY layout, because the numbers differ between the
6
+ * two driver passes: pagination has to size the element before the page total exists, so it builds
7
+ * against a provisional "1 of 1"; the render pass then rebuilds it with the real `PageInfo`.
8
+ *
9
+ * Two consequences, and neither is magic - both come from the same chicken-and-egg: the content decides
10
+ * the page count that the content displays.
11
+ *
12
+ * 1. In the flowing BODY the box is reserved by the provisional build, so a much wider final string
13
+ * (`"9 of 10"` vs `"1 of 1"`) can paint slightly past it. Keep dynamic body content roughly constant
14
+ * in width.
15
+ * 2. A header/footer is re-laid out per page, so its real height is honoured - but the BODY band was
16
+ * already sized against the provisional build. A later page whose header is SHORTER simply gains
17
+ * room (harmless). A later page whose header is TALLER shrinks the band its body was measured for,
18
+ * and that body can overflow. So: a conditional header may shrink on later pages, never grow.
19
+ */
20
+ export class PageBuilderElement extends PDFElement {
21
+ constructor({ build }) {
22
+ super();
23
+ this.build = build;
24
+ }
25
+ /** The real page info during the render pass; a provisional "1 of 1" while paginating. */
26
+ infoFrom(ctx) {
27
+ return (ctx.pageInfo ?? {
28
+ pageNumber: 1,
29
+ pageCount: 1,
30
+ pageSize: resolvePageSize(ctx.pageConfig),
31
+ });
32
+ }
33
+ calculateLayout(constraints, offset, ctx) {
34
+ this.composed = this.build(this.infoFrom(ctx));
35
+ return this.composed.calculateLayout(constraints, offset, ctx);
36
+ }
37
+ // Named `composed` (not `child`) like DeferredElement: the pre-layout image walk keys on `child`, and
38
+ // this subtree does not exist yet when it runs. So an Image inside a PageBuilder skips aspect-ratio
39
+ // pre-resolution - give it both width and height, exactly as inside a Deferred/Table subtree.
40
+ getProps() {
41
+ return { composed: this.composed };
42
+ }
43
+ }
@@ -0,0 +1,35 @@
1
+ import { PDFElement, LayoutContext } from "../pdf-element.js";
2
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
3
+ /**
4
+ * Rotates its child by whole quarter-turns and, unlike `RotatedElement` (paint-only), makes the
5
+ * rotation LAYOUT-AWARE: for a 90 / 270 turn it SWAPS width and height, so the box reports the rotated
6
+ * footprint UP and siblings reflow around it. A tall text becomes a narrow vertical strip that actually
7
+ * reserves its narrow width and its long height - the "vertical label beside a table" case.
8
+ *
9
+ * `turns` is the number of 90-degree CLOCKWISE quarter-turns (like Flutter's `RotatedBox.quarterTurns`):
10
+ * 0 = none, 1 = 90, 2 = 180, 3 = 270. The child lays out with the box's constraints swapped, then it is
11
+ * placed centered on the box's center so a plain center-rotation (the shared renderer) maps it exactly
12
+ * onto the swapped box.
13
+ */
14
+ export declare class RotatedBoxElement extends PDFElement {
15
+ private turns;
16
+ private child;
17
+ private x;
18
+ private y;
19
+ private width;
20
+ private height;
21
+ constructor({ turns, child }: {
22
+ turns: number;
23
+ child: PDFElement;
24
+ });
25
+ calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
26
+ /** Same shape as `RotatedElement` (angle + box), so both share `RotatedRenderer`. */
27
+ getProps(): {
28
+ angle: number;
29
+ child: PDFElement;
30
+ x: number;
31
+ y: number;
32
+ width: number;
33
+ height: number;
34
+ };
35
+ }
@@ -0,0 +1,54 @@
1
+ import { PDFElement } from "../pdf-element.js";
2
+ import { BoxConstraints } from "../../layout/box-constraints.js";
3
+ /**
4
+ * Rotates its child by whole quarter-turns and, unlike `RotatedElement` (paint-only), makes the
5
+ * rotation LAYOUT-AWARE: for a 90 / 270 turn it SWAPS width and height, so the box reports the rotated
6
+ * footprint UP and siblings reflow around it. A tall text becomes a narrow vertical strip that actually
7
+ * reserves its narrow width and its long height - the "vertical label beside a table" case.
8
+ *
9
+ * `turns` is the number of 90-degree CLOCKWISE quarter-turns (like Flutter's `RotatedBox.quarterTurns`):
10
+ * 0 = none, 1 = 90, 2 = 180, 3 = 270. The child lays out with the box's constraints swapped, then it is
11
+ * placed centered on the box's center so a plain center-rotation (the shared renderer) maps it exactly
12
+ * onto the swapped box.
13
+ */
14
+ export class RotatedBoxElement extends PDFElement {
15
+ constructor({ turns, child }) {
16
+ super();
17
+ this.x = 0;
18
+ this.y = 0;
19
+ this.width = 0;
20
+ this.height = 0;
21
+ this.turns = ((Math.trunc(turns) % 4) + 4) % 4;
22
+ this.child = child;
23
+ }
24
+ calculateLayout(constraints, offset, ctx) {
25
+ const quarter = this.turns % 2 === 1; // 90 or 270: axes swap
26
+ // A quarter-turned child experiences the box's constraints with the axes swapped.
27
+ const childConstraints = quarter
28
+ ? new BoxConstraints(constraints.minHeight, constraints.maxHeight, constraints.minWidth, constraints.maxWidth)
29
+ : constraints;
30
+ // Measure the child to learn its natural size, then take that size swapped as our own box.
31
+ const child = this.child.calculateLayout(childConstraints, offset, ctx);
32
+ this.width = quarter ? child.height : child.width;
33
+ this.height = quarter ? child.width : child.height;
34
+ this.x = offset.x;
35
+ this.y = offset.y;
36
+ // Re-place the child CENTERED on the box center: a rotation around that center then maps the
37
+ // child's (w x h) box exactly onto our (swapped) box - so the shared renderer needs no special case.
38
+ const cx = this.x + this.width / 2;
39
+ const cy = this.y + this.height / 2;
40
+ this.child.calculateLayout(childConstraints, { x: cx - child.width / 2, y: cy - child.height / 2 }, ctx);
41
+ return { width: this.width, height: this.height };
42
+ }
43
+ /** Same shape as `RotatedElement` (angle + box), so both share `RotatedRenderer`. */
44
+ getProps() {
45
+ return {
46
+ angle: this.turns * 90,
47
+ child: this.child,
48
+ x: this.x,
49
+ y: this.y,
50
+ width: this.width,
51
+ height: this.height,
52
+ };
53
+ }
54
+ }