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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) 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 +24 -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 +112 -17
  9. package/dist/api/layout.js +118 -13
  10. package/dist/api/page-builder.d.ts +32 -0
  11. package/dist/api/page-builder.js +32 -0
  12. package/dist/api/structure.d.ts +5 -0
  13. package/dist/api/structure.js +1 -0
  14. package/dist/api/text.d.ts +24 -2
  15. package/dist/api/text.js +46 -4
  16. package/dist/elements/container-element.d.ts +26 -1
  17. package/dist/elements/container-element.js +79 -29
  18. package/dist/elements/image-element.d.ts +19 -1
  19. package/dist/elements/image-element.js +52 -11
  20. package/dist/elements/index.d.ts +8 -0
  21. package/dist/elements/index.js +8 -0
  22. package/dist/elements/layout/anchor-element.d.ts +23 -0
  23. package/dist/elements/layout/anchor-element.js +27 -0
  24. package/dist/elements/layout/bookmark-element.d.ts +27 -0
  25. package/dist/elements/layout/bookmark-element.js +30 -0
  26. package/dist/elements/layout/keep-together-element.d.ts +42 -0
  27. package/dist/elements/layout/keep-together-element.js +85 -0
  28. package/dist/elements/layout/link-element.d.ts +33 -0
  29. package/dist/elements/layout/link-element.js +39 -0
  30. package/dist/elements/layout/padding-element.d.ts +3 -0
  31. package/dist/elements/layout/padding-element.js +12 -0
  32. package/dist/elements/layout/page-break-element.d.ts +17 -0
  33. package/dist/elements/layout/page-break-element.js +24 -0
  34. package/dist/elements/layout/page-builder-element.d.ts +32 -0
  35. package/dist/elements/layout/page-builder-element.js +43 -0
  36. package/dist/elements/layout/positioned-element.d.ts +10 -7
  37. package/dist/elements/layout/positioned-element.js +21 -9
  38. package/dist/elements/layout/rotated-box-element.d.ts +35 -0
  39. package/dist/elements/layout/rotated-box-element.js +54 -0
  40. package/dist/elements/layout/rotated-element.d.ts +32 -0
  41. package/dist/elements/layout/rotated-element.js +41 -0
  42. package/dist/elements/page-element.d.ts +19 -1
  43. package/dist/elements/page-element.js +54 -17
  44. package/dist/elements/pdf-element.d.ts +59 -0
  45. package/dist/elements/pdf-element.js +51 -0
  46. package/dist/elements/rectangle-element.d.ts +20 -1
  47. package/dist/elements/rectangle-element.js +75 -18
  48. package/dist/elements/row-element.d.ts +19 -1
  49. package/dist/elements/row-element.js +45 -8
  50. package/dist/elements/text-element.d.ts +34 -4
  51. package/dist/elements/text-element.js +40 -17
  52. package/dist/ir/display-list.d.ts +60 -1
  53. package/dist/layout/box-constraints.d.ts +8 -0
  54. package/dist/layout/box-constraints.js +14 -0
  55. package/dist/layout/collect-images.d.ts +9 -0
  56. package/dist/layout/collect-images.js +34 -0
  57. package/dist/layout/fragmentation.d.ts +12 -0
  58. package/dist/layout/fragmentation.js +45 -8
  59. package/dist/renderer/anchor-renderer.d.ts +6 -0
  60. package/dist/renderer/anchor-renderer.js +11 -0
  61. package/dist/renderer/bookmark-renderer.d.ts +6 -0
  62. package/dist/renderer/bookmark-renderer.js +11 -0
  63. package/dist/renderer/keep-together-renderer.d.ts +10 -0
  64. package/dist/renderer/keep-together-renderer.js +12 -0
  65. package/dist/renderer/link-renderer.d.ts +6 -0
  66. package/dist/renderer/link-renderer.js +11 -0
  67. package/dist/renderer/page-break-renderer.d.ts +14 -0
  68. package/dist/renderer/page-break-renderer.js +15 -0
  69. package/dist/renderer/page-builder-renderer.d.ts +6 -0
  70. package/dist/renderer/page-builder-renderer.js +11 -0
  71. package/dist/renderer/page-renderer.js +37 -2
  72. package/dist/renderer/pdf-backend.d.ts +17 -0
  73. package/dist/renderer/pdf-backend.js +117 -7
  74. package/dist/renderer/pdf-document-class.js +3 -0
  75. package/dist/renderer/pdf-document-renderer.d.ts +16 -7
  76. package/dist/renderer/pdf-document-renderer.js +87 -36
  77. package/dist/renderer/pdf-renderer.js +53 -2
  78. package/dist/renderer/rotated-renderer.d.ts +7 -0
  79. package/dist/renderer/rotated-renderer.js +29 -0
  80. package/dist/renderer/text-renderer.d.ts +1 -1
  81. package/dist/renderer/text-renderer.js +123 -54
  82. package/dist/text/advance.d.ts +34 -0
  83. package/dist/text/advance.js +42 -0
  84. package/dist/text/line-breaker.d.ts +9 -8
  85. package/dist/text/line-breaker.js +28 -21
  86. package/dist/text/line-metrics.d.ts +49 -0
  87. package/dist/text/line-metrics.js +57 -0
  88. package/dist/text/text-decoration.d.ts +50 -0
  89. package/dist/text/text-decoration.js +62 -0
  90. package/dist/text/text-style.d.ts +13 -3
  91. package/dist/text/text-style.js +10 -3
  92. package/dist/utils/afm-parser.d.ts +34 -0
  93. package/dist/utils/afm-parser.js +73 -4
  94. package/dist/utils/dest-registry.d.ts +19 -0
  95. package/dist/utils/dest-registry.js +32 -0
  96. package/dist/utils/flex-layout.d.ts +10 -3
  97. package/dist/utils/flex-layout.js +26 -6
  98. package/dist/utils/font-metrics.d.ts +14 -0
  99. package/dist/utils/outline.d.ts +21 -0
  100. package/dist/utils/outline.js +80 -0
  101. package/dist/utils/pdf-object-manager.d.ts +33 -1
  102. package/dist/utils/pdf-object-manager.js +0 -0
  103. package/dist/utils/ttf-parser.d.ts +53 -0
  104. package/dist/utils/ttf-parser.js +368 -0
  105. package/package.json +1 -1
@@ -11,6 +11,23 @@ export interface TextStyle {
11
11
  bold?: boolean;
12
12
  italic?: boolean;
13
13
  color?: ColorInput;
14
+ /** Draw a line under the text. Its position and thickness come from the font, not from a guess.
15
+ * A `Link` is NOT underlined automatically - say so if you want it. */
16
+ underline?: boolean;
17
+ /** Draw a line through the text, at half its x-height (where a browser puts it). */
18
+ strikethrough?: boolean;
19
+ /** Let the underline step around descenders, like a browser (`text-decoration-skip-ink`). Needs an
20
+ * EMBEDDED font - the standard-14 outlines live in the viewer, so we cannot see where their ink
21
+ * is. Asking for it with a standard font is an error, not a silent no-op. */
22
+ skipInk?: boolean;
23
+ /** Extra space after every glyph, in points (CSS `letter-spacing`). Negative tightens. Default 0. */
24
+ letterSpacing?: number;
25
+ /** External URL - makes this run an inline hyperlink (a /Link annotation over its glyphs). On a
26
+ * `span` it links just that run; on a whole `Text` (plain string) it links the whole text. */
27
+ href?: string;
28
+ /** Internal jump target - the `name` of an `Anchor` elsewhere in the document (like `href`, but a
29
+ * same-document /GoTo instead of a URL). Use for a clickable table of contents / cross-reference. */
30
+ to?: string;
14
31
  }
15
32
  export interface TextOptions extends TextStyle {
16
33
  /** Text-internal alignment (left/center/right) - independent of a parent's `cross` (§5). */
@@ -20,8 +37,9 @@ export interface TextOptions extends TextStyle {
20
37
  maxLines?: number;
21
38
  /** What happens past `maxLines`: `"clip"` (default) cuts hard, `"ellipsis"` ends with "…". */
22
39
  overflow?: TextOverflow;
23
- /** Line-height multiplier (default `1`). `1.4` gives roomier body copy; each line is
24
- * `size * lineHeight` tall. */
40
+ /** Line-height multiplier: each line is `size * lineHeight` tall. Unset means the font's own
41
+ * natural line height (`ascent + descent + lineGap`), like CSS `line-height: normal` - which is
42
+ * what you want for body copy. `1` gives exactly one em, which most faces overflow slightly. */
25
43
  lineHeight?: number;
26
44
  /** Accessibility role for the tagged structure tree (only when rendered with `accessible`): a heading
27
45
  * level `"h1"`..`"h6"` or `"p"` (default). Purely semantic - it does not change the visual style. */
@@ -50,6 +68,10 @@ export interface TextDefaults {
50
68
  color?: ColorInput;
51
69
  align?: "left" | "center" | "right";
52
70
  lineHeight?: number;
71
+ underline?: boolean;
72
+ strikethrough?: boolean;
73
+ skipInk?: boolean;
74
+ letterSpacing?: number;
53
75
  }
54
76
  /** Maps the `Text`-style option names onto a partial engine `ResolvedTextStyle` (only the set
55
77
  * fields), for seeding the inheritance cascade. */
package/dist/api/text.js CHANGED
@@ -17,6 +17,26 @@ const ALIGN = {
17
17
  center: HorizontalAlignment.center,
18
18
  right: HorizontalAlignment.right,
19
19
  };
20
+ /**
21
+ * Validates a font `size` (a number of points). Rejects anything that would silently become a NaN
22
+ * height deep in layout - notably `Document({ size: "A4" })`, where `"A4"` is the PAGE size and belongs
23
+ * on `Page`, not a font size. `undefined` passes through (unset = inherit the cascade).
24
+ */
25
+ function toFontSize(size) {
26
+ if (size === undefined)
27
+ return undefined;
28
+ if (typeof size !== "number" || !Number.isFinite(size) || size <= 0) {
29
+ // Quote a string so "A4" reads as a string; show a bare number/NaN as-is (JSON turns NaN into null).
30
+ const shown = typeof size === "string" ? JSON.stringify(size) : String(size);
31
+ // The Page-vs-font-size mix-up only happens with a string like "A4"; for a plain bad number
32
+ // (NaN, negative, 0) the hint would just be noise, so keep the message generic there.
33
+ const hint = typeof size === "string"
34
+ ? ` Note: the page size goes on Page({ size: "A4" }), not on Document or Text.`
35
+ : "";
36
+ throw new Error(`Invalid font size ${shown} (expected a positive number of points).${hint}`);
37
+ }
38
+ return size;
39
+ }
20
40
  /**
21
41
  * An inline run for mixed-style `Text` (`Text([span("a", {bold:true}), span("b")])`). Each
22
42
  * field overrides the enclosing `Text`'s defaults for just this run; omitted fields inherit.
@@ -24,12 +44,17 @@ const ALIGN = {
24
44
  export function span(text, style = {}) {
25
45
  return {
26
46
  content: text,
27
- fontSize: style.size,
47
+ fontSize: toFontSize(style.size),
28
48
  fontFamily: style.font,
29
49
  fontStyle: style.bold !== undefined || style.italic !== undefined
30
50
  ? toFontStyle(style.bold, style.italic)
31
51
  : undefined,
32
52
  fontColor: style.color !== undefined ? toColor(style.color) : undefined,
53
+ underline: style.underline,
54
+ strikethrough: style.strikethrough,
55
+ letterSpacing: style.letterSpacing,
56
+ href: style.href,
57
+ dest: style.to,
33
58
  };
34
59
  }
35
60
  /**
@@ -38,11 +63,16 @@ export function span(text, style = {}) {
38
63
  * text-internal `align`.
39
64
  */
40
65
  export function Text(content, opts = {}) {
66
+ // A plain string given an `href`/`to` becomes a single link span, so `Text("jasy.dev", { href })` (or
67
+ // `{ to }`) is a whole-text link (inline links use `span(...)`). This routes through the styled-segment
68
+ // path, which lays a single default-font segment out identically - only the /Link annotation is added.
69
+ const isLink = opts.href !== undefined || opts.to !== undefined;
70
+ const body = typeof content === "string" && isLink ? [span(content, opts)] : content;
41
71
  // Unset properties are left undefined so they inherit the cascaded TextStyle (Document default /
42
72
  // built-in). Only bold/italic that the caller actually set become an explicit FontStyle.
43
73
  return new TextElement({
44
- content,
45
- fontSize: opts.size,
74
+ content: body,
75
+ fontSize: toFontSize(opts.size),
46
76
  fontFamily: opts.font,
47
77
  fontStyle: opts.bold !== undefined || opts.italic !== undefined
48
78
  ? toFontStyle(opts.bold, opts.italic)
@@ -52,6 +82,10 @@ export function Text(content, opts = {}) {
52
82
  maxLines: opts.maxLines,
53
83
  overflow: opts.overflow,
54
84
  lineHeight: opts.lineHeight,
85
+ underline: opts.underline,
86
+ strikethrough: opts.strikethrough,
87
+ skipInk: opts.skipInk,
88
+ letterSpacing: opts.letterSpacing,
55
89
  role: opts.role,
56
90
  });
57
91
  }
@@ -64,7 +98,7 @@ export function Paragraph(content, opts = {}) {
64
98
  export function toTextStyleOverride(opts) {
65
99
  const style = {};
66
100
  if (opts.size !== undefined)
67
- style.fontSize = opts.size;
101
+ style.fontSize = toFontSize(opts.size);
68
102
  if (opts.font !== undefined)
69
103
  style.fontFamily = opts.font;
70
104
  if (opts.bold !== undefined || opts.italic !== undefined) {
@@ -76,5 +110,13 @@ export function toTextStyleOverride(opts) {
76
110
  style.textAlignment = ALIGN[opts.align];
77
111
  if (opts.lineHeight !== undefined)
78
112
  style.lineHeight = opts.lineHeight;
113
+ if (opts.underline !== undefined)
114
+ style.underline = opts.underline;
115
+ if (opts.strikethrough !== undefined)
116
+ style.strikethrough = opts.strikethrough;
117
+ if (opts.skipInk !== undefined)
118
+ style.skipInk = opts.skipInk;
119
+ if (opts.letterSpacing !== undefined)
120
+ style.letterSpacing = opts.letterSpacing;
79
121
  return style;
80
122
  }
@@ -9,13 +9,25 @@ interface ContainerElementParams extends SizedElement, WithChildren {
9
9
  main?: MainAlign;
10
10
  /** Horizontal alignment of each child (cross axis); defaults to `stretch`. */
11
11
  cross?: CrossAlign;
12
+ /** Width/height as a fraction (0..1) of the offered box instead of `width`/`height` (relative sizing). */
13
+ widthFactor?: number;
14
+ heightFactor?: number;
15
+ /** Start this stack on a fresh page (CSS `break-before: page`). */
16
+ breakBefore?: boolean;
17
+ /** Start everything after this stack on a fresh page (CSS `break-after: page`). */
18
+ breakAfter?: boolean;
12
19
  }
13
20
  export declare class ContainerElement extends SizedPDFElement implements Fragmentable {
14
21
  private children;
15
22
  private gap;
16
23
  private main;
17
24
  private cross;
18
- constructor({ x, y, width, height, children, gap, main, cross }: ContainerElementParams);
25
+ private breakBefore;
26
+ private breakAfter;
27
+ private requested;
28
+ constructor({ x, y, width, height, children, gap, main, cross, widthFactor, heightFactor, breakBefore, breakAfter, }: ContainerElementParams);
29
+ breaksBefore(): boolean;
30
+ breaksAfter(): boolean;
19
31
  /**
20
32
  * Splits the vertical stack across pages. Children are measured against the content
21
33
  * width and packed until one would exceed `maxHeight`. That straddling child is itself
@@ -27,8 +39,21 @@ export declare class ContainerElement extends SizedPDFElement implements Fragmen
27
39
  * Flex children make the container absorb the leftover space, so it never overflows -
28
40
  * in that case we don't fragment and hand the whole container back as `fitted`.
29
41
  */
42
+ hasForcedBreak(): boolean;
30
43
  fragment(maxHeight: number, width: number, ctx: LayoutContext): FragmentResult;
31
44
  private cloneWithChildren;
45
+ relativeSizeFactor(horizontal: boolean): number | undefined;
46
+ /**
47
+ * A Column stacks vertically, so a flex child of its OWN only ever needs a bounded height. But the need
48
+ * also propagates: a `Spacer` two levels down is just as unable to resolve against an infinite axis, so
49
+ * we ask the children too - on both axes, since a nested Row's `Spacer` needs a bounded WIDTH and a
50
+ * Column nested in a Row is exactly where that width goes missing.
51
+ *
52
+ * An explicit extent on an axis short-circuits it: the element already knows how big it is there, and
53
+ * with no flex descendant nothing needs distributing, so it keeps shrink-wrapping. That is what leaves
54
+ * every existing layout untouched.
55
+ */
56
+ needsBoundedMain(horizontal: boolean): boolean;
32
57
  calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
33
58
  getProps(): ContainerElementParams;
34
59
  }
@@ -1,13 +1,23 @@
1
1
  import { FlexLayoutHelper, VERTICAL_AXIS } from "../utils/flex-layout.js";
2
- import { packChildren } from "../layout/fragmentation.js";
3
- import { SizedPDFElement, } from "./pdf-element.js";
2
+ import { resolveExtent } from "../layout/box-constraints.js";
3
+ import { childrenForceBreak, packChildren, } from "../layout/fragmentation.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, breakBefore, breakAfter, }) {
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 };
13
+ this.breakBefore = breakBefore ?? false;
14
+ this.breakAfter = breakAfter ?? false;
15
+ }
16
+ breaksBefore() {
17
+ return this.breakBefore;
18
+ }
19
+ breaksAfter() {
20
+ return this.breakAfter;
11
21
  }
12
22
  /**
13
23
  * Splits the vertical stack across pages. Children are measured against the content
@@ -20,42 +30,84 @@ export class ContainerElement extends SizedPDFElement {
20
30
  * Flex children make the container absorb the leftover space, so it never overflows -
21
31
  * in that case we don't fragment and hand the whole container back as `fitted`.
22
32
  */
33
+ hasForcedBreak() {
34
+ return childrenForceBreak(this.children);
35
+ }
23
36
  fragment(maxHeight, width, ctx) {
24
- const { fitted, remainder } = packChildren(this.children, maxHeight, width, ctx, this.gap);
25
- // Fits as one region: hand the whole container back so the page renders unchanged
26
- // (its normal layout distributes flex / fills the page).
27
- if (remainder.length === 0)
37
+ const { fitted, remainder, forceBreak } = packChildren(this.children, maxHeight, width, ctx, this.gap);
38
+ // Fits as one region with no forced cut: hand the whole container back so the page renders
39
+ // unchanged (its normal layout distributes flex / fills the page). A forced break is excluded
40
+ // here even when nothing follows it (a trailing `PageBreak`): return the packed children, which
41
+ // no longer hold the consumed break marker, so it does not leak into the render pass.
42
+ if (remainder.length === 0 && !forceBreak)
28
43
  return { fitted: this, remainder: null };
29
44
  return {
30
45
  fitted: this.cloneWithChildren(fitted),
31
- remainder: this.cloneWithChildren(remainder),
46
+ // Only the CONTINUATION carries `breakAfter`, so a stack that splits across pages still breaks
47
+ // after its LAST fragment (not after the first one). `breakBefore` is dropped on both: it was
48
+ // already honoured by the parent before this stack was ever fragmented.
49
+ remainder: remainder.length === 0 ? null : this.cloneWithChildren(remainder, this.breakAfter),
50
+ forceBreak,
32
51
  };
33
52
  }
34
- cloneWithChildren(children) {
53
+ cloneWithChildren(children, breakAfter = false) {
35
54
  return new ContainerElement({
36
55
  x: this.x,
37
56
  y: this.y,
38
- width: this.width,
39
- height: this.height,
57
+ // Carry the requested WIDTH (a %-width column keeps its width on every page); the height is
58
+ // intentionally dropped so each fragment shrink-wraps to the content it actually holds.
59
+ width: this.requested.width,
60
+ widthFactor: this.requested.widthFactor,
40
61
  children,
41
62
  gap: this.gap,
42
63
  main: this.main,
43
64
  cross: this.cross,
65
+ breakAfter,
44
66
  });
45
67
  }
68
+ relativeSizeFactor(horizontal) {
69
+ return horizontal ? this.requested.widthFactor : this.requested.heightFactor;
70
+ }
71
+ /**
72
+ * A Column stacks vertically, so a flex child of its OWN only ever needs a bounded height. But the need
73
+ * also propagates: a `Spacer` two levels down is just as unable to resolve against an infinite axis, so
74
+ * we ask the children too - on both axes, since a nested Row's `Spacer` needs a bounded WIDTH and a
75
+ * Column nested in a Row is exactly where that width goes missing.
76
+ *
77
+ * An explicit extent on an axis short-circuits it: the element already knows how big it is there, and
78
+ * with no flex descendant nothing needs distributing, so it keeps shrink-wrapping. That is what leaves
79
+ * every existing layout untouched.
80
+ */
81
+ needsBoundedMain(horizontal) {
82
+ const requested = horizontal
83
+ ? [this.requested.width, this.requested.widthFactor]
84
+ : [this.requested.height, this.requested.heightFactor];
85
+ if (requested.some((v) => v !== undefined))
86
+ return false;
87
+ const ownFlexChild = !horizontal && this.children.some((c) => c instanceof FlexiblePDFElement);
88
+ return ownFlexChild || this.children.some((c) => c.needsBoundedMain(horizontal));
89
+ }
46
90
  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
91
  this.x = offset.x;
56
92
  this.y = offset.y;
57
- const crossAvail = constraints.hasBoundedWidth ? this.width : Infinity;
58
- const mainAvail = constraints.hasBoundedHeight ? this.height : Infinity;
93
+ // Relative sizing: a pinned extent (fixed points or a fraction of the offered box, clamped into
94
+ // the constraints) wins; else fill a bounded axis; else stay `undefined` and shrink-wrap below.
95
+ // A Column nested in a Row gets unbounded width - passing 0 there would collapse children to 0.
96
+ const explicitWidth = resolveExtent(this.requested.width, this.requested.widthFactor, constraints.maxWidth, constraints.hasBoundedWidth);
97
+ const explicitHeight = resolveExtent(this.requested.height, this.requested.heightFactor, constraints.maxHeight, constraints.hasBoundedHeight);
98
+ const boundedWidth = explicitWidth !== undefined
99
+ ? constraints.constrainWidth(explicitWidth)
100
+ : constraints.hasBoundedWidth
101
+ ? constraints.maxWidth
102
+ : undefined;
103
+ const boundedHeight = explicitHeight !== undefined
104
+ ? constraints.constrainHeight(explicitHeight)
105
+ : constraints.hasBoundedHeight
106
+ ? constraints.maxHeight
107
+ : undefined;
108
+ // Vertical stack: cross = width (children fill it), main = height (the stacking extent).
109
+ const crossAvail = boundedWidth ?? Infinity;
110
+ const mainAvail = boundedHeight ?? Infinity;
59
111
  let result = { mainUsed: 0, crossUsed: 0 };
60
112
  if (this.children) {
61
113
  // Vertical flex stack (main = height, cross = width). The shared helper measures
@@ -63,14 +115,12 @@ export class ContainerElement extends SizedPDFElement {
63
115
  // in source order.
64
116
  result = FlexLayoutHelper.layout(this.children, VERTICAL_AXIS, mainAvail, crossAvail, this.y, this.x, { gap: this.gap, main: this.main, cross: this.cross }, ctx);
65
117
  }
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 };
118
+ // Pinned/bounded axis takes that extent; an unbounded one shrink-wraps to the children (height =
119
+ // the stack, width = the widest child). Top-left coordinates; the container draws nothing, and
120
+ // the Y-flip happens once at the IR -> backend seam.
121
+ this.width = boundedWidth ?? result.crossUsed;
122
+ this.height = boundedHeight ?? result.mainUsed;
123
+ return { width: this.width, height: this.height };
74
124
  }
75
125
  getProps() {
76
126
  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
  }
@@ -6,7 +6,15 @@ export * from "./rectangle-element.js";
6
6
  export * from "./image-element.js";
7
7
  export * from "./layout/expanded-element.js";
8
8
  export * from "./layout/padding-element.js";
9
+ export * from "./layout/page-break-element.js";
10
+ export * from "./layout/keep-together-element.js";
9
11
  export * from "./layout/default-text-style-element.js";
10
12
  export * from "./layout/positioned-element.js";
13
+ export * from "./layout/link-element.js";
14
+ export * from "./layout/bookmark-element.js";
15
+ export * from "./layout/anchor-element.js";
16
+ export * from "./layout/page-builder-element.js";
17
+ export * from "./layout/rotated-element.js";
18
+ export * from "./layout/rotated-box-element.js";
11
19
  export * from "./line-element.js";
12
20
  export * from "./row-element.js";
@@ -6,7 +6,15 @@ export * from "./rectangle-element.js";
6
6
  export * from "./image-element.js";
7
7
  export * from "./layout/expanded-element.js";
8
8
  export * from "./layout/padding-element.js";
9
+ export * from "./layout/page-break-element.js";
10
+ export * from "./layout/keep-together-element.js";
9
11
  export * from "./layout/default-text-style-element.js";
10
12
  export * from "./layout/positioned-element.js";
13
+ export * from "./layout/link-element.js";
14
+ export * from "./layout/bookmark-element.js";
15
+ export * from "./layout/anchor-element.js";
16
+ export * from "./layout/page-builder-element.js";
17
+ export * from "./layout/rotated-element.js";
18
+ export * from "./layout/rotated-box-element.js";
11
19
  export * from "./line-element.js";
12
20
  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
+ }