@jasy/pdf 1.0.0-alpha.4 → 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 (96) hide show
  1. package/README.md +5 -2
  2. package/dist/api/content.d.ts +9 -3
  3. package/dist/api/content.js +13 -3
  4. package/dist/api/descriptor.js +22 -1
  5. package/dist/api/dimension.d.ts +20 -0
  6. package/dist/api/dimension.js +16 -0
  7. package/dist/api/index.d.ts +2 -0
  8. package/dist/api/index.js +2 -0
  9. package/dist/api/layout.d.ts +65 -3
  10. package/dist/api/layout.js +71 -2
  11. package/dist/api/page-builder.d.ts +32 -0
  12. package/dist/api/page-builder.js +32 -0
  13. package/dist/api/structure.d.ts +19 -0
  14. package/dist/api/structure.js +25 -0
  15. package/dist/api/table.js +13 -5
  16. package/dist/api/text.d.ts +10 -1
  17. package/dist/api/text.js +32 -4
  18. package/dist/elements/container-element.d.ts +17 -1
  19. package/dist/elements/container-element.js +54 -22
  20. package/dist/elements/image-element.d.ts +23 -1
  21. package/dist/elements/image-element.js +54 -11
  22. package/dist/elements/index.d.ts +6 -0
  23. package/dist/elements/index.js +6 -0
  24. package/dist/elements/layout/anchor-element.d.ts +23 -0
  25. package/dist/elements/layout/anchor-element.js +27 -0
  26. package/dist/elements/layout/bookmark-element.d.ts +27 -0
  27. package/dist/elements/layout/bookmark-element.js +30 -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/page-builder-element.d.ts +32 -0
  31. package/dist/elements/layout/page-builder-element.js +43 -0
  32. package/dist/elements/layout/rotated-box-element.d.ts +35 -0
  33. package/dist/elements/layout/rotated-box-element.js +54 -0
  34. package/dist/elements/layout/rotated-element.d.ts +32 -0
  35. package/dist/elements/layout/rotated-element.js +41 -0
  36. package/dist/elements/layout/struct-group.d.ts +28 -0
  37. package/dist/elements/layout/struct-group.js +42 -0
  38. package/dist/elements/page-element.d.ts +8 -0
  39. package/dist/elements/page-element.js +13 -1
  40. package/dist/elements/pdf-element.d.ts +35 -0
  41. package/dist/elements/pdf-element.js +34 -0
  42. package/dist/elements/rectangle-element.d.ts +11 -1
  43. package/dist/elements/rectangle-element.js +49 -13
  44. package/dist/elements/row-element.d.ts +11 -1
  45. package/dist/elements/row-element.js +37 -8
  46. package/dist/elements/text-element.d.ts +11 -1
  47. package/dist/elements/text-element.js +5 -2
  48. package/dist/ir/display-list.d.ts +137 -1
  49. package/dist/layout/box-constraints.d.ts +8 -0
  50. package/dist/layout/box-constraints.js +14 -0
  51. package/dist/layout/collect-images.d.ts +9 -0
  52. package/dist/layout/collect-images.js +34 -0
  53. package/dist/layout/fragmentation.js +9 -2
  54. package/dist/renderer/anchor-renderer.d.ts +6 -0
  55. package/dist/renderer/anchor-renderer.js +11 -0
  56. package/dist/renderer/bookmark-renderer.d.ts +6 -0
  57. package/dist/renderer/bookmark-renderer.js +11 -0
  58. package/dist/renderer/emoji-image.d.ts +9 -0
  59. package/dist/renderer/emoji-image.js +68 -0
  60. package/dist/renderer/image-renderer.d.ts +1 -1
  61. package/dist/renderer/image-renderer.js +10 -2
  62. package/dist/renderer/link-renderer.d.ts +6 -0
  63. package/dist/renderer/link-renderer.js +11 -0
  64. package/dist/renderer/page-builder-renderer.d.ts +6 -0
  65. package/dist/renderer/page-builder-renderer.js +11 -0
  66. package/dist/renderer/page-renderer.js +54 -4
  67. package/dist/renderer/pdf-backend.d.ts +16 -2
  68. package/dist/renderer/pdf-backend.js +135 -3
  69. package/dist/renderer/pdf-document-renderer.d.ts +16 -7
  70. package/dist/renderer/pdf-document-renderer.js +65 -33
  71. package/dist/renderer/pdf-renderer.js +58 -2
  72. package/dist/renderer/rotated-renderer.d.ts +7 -0
  73. package/dist/renderer/rotated-renderer.js +29 -0
  74. package/dist/renderer/struct-group-renderer.d.ts +10 -0
  75. package/dist/renderer/struct-group-renderer.js +24 -0
  76. package/dist/renderer/text-renderer.d.ts +3 -0
  77. package/dist/renderer/text-renderer.js +206 -5
  78. package/dist/text/emoji-codepoints.d.ts +1 -0
  79. package/dist/text/emoji-codepoints.js +29 -0
  80. package/dist/text/line-breaker.js +6 -3
  81. package/dist/utils/afm-parser.js +6 -4
  82. package/dist/utils/dest-registry.d.ts +19 -0
  83. package/dist/utils/dest-registry.js +32 -0
  84. package/dist/utils/flex-layout.d.ts +10 -3
  85. package/dist/utils/flex-layout.js +26 -6
  86. package/dist/utils/outline.d.ts +21 -0
  87. package/dist/utils/outline.js +80 -0
  88. package/dist/utils/pdf-object-manager.d.ts +26 -1
  89. package/dist/utils/pdf-object-manager.js +0 -0
  90. package/dist/utils/struct-tree.d.ts +44 -0
  91. package/dist/utils/struct-tree.js +130 -0
  92. package/dist/utils/ttf-parser.d.ts +73 -0
  93. package/dist/utils/ttf-parser.js +456 -16
  94. package/dist/utils/ua-xmp.d.ts +9 -0
  95. package/dist/utils/ua-xmp.js +32 -0
  96. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { TextElement, TextSegment } from "../elements/text-element.js";
1
+ import { TextElement, TextRole, TextSegment } from "../elements/text-element.js";
2
2
  import { TextOverflow } from "../text/line-breaker.js";
3
3
  import { ResolvedTextStyle } from "../text/text-style.js";
4
4
  import { ColorInput } from "./color.js";
@@ -11,6 +11,12 @@ export interface TextStyle {
11
11
  bold?: boolean;
12
12
  italic?: boolean;
13
13
  color?: ColorInput;
14
+ /** External URL - makes this run an inline hyperlink (a /Link annotation over its glyphs). On a
15
+ * `span` it links just that run; on a whole `Text` (plain string) it links the whole text. */
16
+ href?: string;
17
+ /** Internal jump target - the `name` of an `Anchor` elsewhere in the document (like `href`, but a
18
+ * same-document /GoTo instead of a URL). Use for a clickable table of contents / cross-reference. */
19
+ to?: string;
14
20
  }
15
21
  export interface TextOptions extends TextStyle {
16
22
  /** Text-internal alignment (left/center/right) - independent of a parent's `cross` (§5). */
@@ -23,6 +29,9 @@ export interface TextOptions extends TextStyle {
23
29
  /** Line-height multiplier (default `1`). `1.4` gives roomier body copy; each line is
24
30
  * `size * lineHeight` tall. */
25
31
  lineHeight?: number;
32
+ /** Accessibility role for the tagged structure tree (only when rendered with `accessible`): a heading
33
+ * level `"h1"`..`"h6"` or `"p"` (default). Purely semantic - it does not change the visual style. */
34
+ role?: TextRole;
26
35
  }
27
36
  /**
28
37
  * An inline run for mixed-style `Text` (`Text([span("a", {bold:true}), span("b")])`). Each
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,14 @@ 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
+ href: style.href,
54
+ dest: style.to,
33
55
  };
34
56
  }
35
57
  /**
@@ -38,11 +60,16 @@ export function span(text, style = {}) {
38
60
  * text-internal `align`.
39
61
  */
40
62
  export function Text(content, opts = {}) {
63
+ // A plain string given an `href`/`to` becomes a single link span, so `Text("jasy.dev", { href })` (or
64
+ // `{ to }`) is a whole-text link (inline links use `span(...)`). This routes through the styled-segment
65
+ // path, which lays a single default-font segment out identically - only the /Link annotation is added.
66
+ const isLink = opts.href !== undefined || opts.to !== undefined;
67
+ const body = typeof content === "string" && isLink ? [span(content, opts)] : content;
41
68
  // Unset properties are left undefined so they inherit the cascaded TextStyle (Document default /
42
69
  // built-in). Only bold/italic that the caller actually set become an explicit FontStyle.
43
70
  return new TextElement({
44
- content,
45
- fontSize: opts.size,
71
+ content: body,
72
+ fontSize: toFontSize(opts.size),
46
73
  fontFamily: opts.font,
47
74
  fontStyle: opts.bold !== undefined || opts.italic !== undefined
48
75
  ? toFontStyle(opts.bold, opts.italic)
@@ -52,6 +79,7 @@ export function Text(content, opts = {}) {
52
79
  maxLines: opts.maxLines,
53
80
  overflow: opts.overflow,
54
81
  lineHeight: opts.lineHeight,
82
+ role: opts.role,
55
83
  });
56
84
  }
57
85
  /** `Text` with body-paragraph defaults (same options; a separate name reads as intent). */
@@ -63,7 +91,7 @@ export function Paragraph(content, opts = {}) {
63
91
  export function toTextStyleOverride(opts) {
64
92
  const style = {};
65
93
  if (opts.size !== undefined)
66
- style.fontSize = opts.size;
94
+ style.fontSize = toFontSize(opts.size);
67
95
  if (opts.font !== undefined)
68
96
  style.fontFamily = opts.font;
69
97
  if (opts.bold !== undefined || opts.italic !== undefined) {
@@ -9,13 +9,17 @@ 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;
12
15
  }
13
16
  export declare class ContainerElement extends SizedPDFElement implements Fragmentable {
14
17
  private children;
15
18
  private gap;
16
19
  private main;
17
20
  private cross;
18
- constructor({ x, y, width, height, children, gap, main, cross }: ContainerElementParams);
21
+ private requested;
22
+ constructor({ x, y, width, height, children, gap, main, cross, widthFactor, heightFactor, }: ContainerElementParams);
19
23
  /**
20
24
  * Splits the vertical stack across pages. Children are measured against the content
21
25
  * width and packed until one would exceed `maxHeight`. That straddling child is itself
@@ -29,6 +33,18 @@ export declare class ContainerElement extends SizedPDFElement implements Fragmen
29
33
  */
30
34
  fragment(maxHeight: number, width: number, ctx: LayoutContext): FragmentResult;
31
35
  private cloneWithChildren;
36
+ relativeSizeFactor(horizontal: boolean): number | undefined;
37
+ /**
38
+ * A Column stacks vertically, so a flex child of its OWN only ever needs a bounded height. But the need
39
+ * also propagates: a `Spacer` two levels down is just as unable to resolve against an infinite axis, so
40
+ * we ask the children too - on both axes, since a nested Row's `Spacer` needs a bounded WIDTH and a
41
+ * Column nested in a Row is exactly where that width goes missing.
42
+ *
43
+ * An explicit extent on an axis short-circuits it: the element already knows how big it is there, and
44
+ * with no flex descendant nothing needs distributing, so it keeps shrink-wrapping. That is what leaves
45
+ * every existing layout untouched.
46
+ */
47
+ needsBoundedMain(horizontal: boolean): boolean;
32
48
  calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
33
49
  getProps(): ContainerElementParams;
34
50
  }
@@ -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,15 +51,34 @@ 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;
61
+ /** Alternate text for accessibility (tagged PDF). With `alt` the image is a Figure; without, decoration. */
62
+ alt?: string;
55
63
  }
56
64
  export declare class ImageElement extends SizedPDFElement {
57
65
  private image;
66
+ private widthFactor?;
67
+ private heightFactor?;
58
68
  private fit;
59
69
  private radius;
60
- constructor({ image, width, height, fit, radius }: ImageElementParams);
70
+ private readonly alt?;
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;
61
82
  calculateLayout(constraints: BoxConstraints, offset: Offset, _ctx: LayoutContext): Size;
62
83
  getProps(): {
63
84
  x: number;
@@ -67,6 +88,7 @@ export declare class ImageElement extends SizedPDFElement {
67
88
  image: CustomImage;
68
89
  fit: BoxFit;
69
90
  radius: number;
91
+ alt: string | undefined;
70
92
  };
71
93
  }
72
94
  export {};
@@ -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,25 +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 }) {
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;
114
+ this.alt = alt;
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;
104
133
  }
105
134
  calculateLayout(constraints, offset, _ctx) {
106
135
  this.x = offset.x;
107
136
  this.y = offset.y;
108
- // A bounded axis overrides the intrinsic/explicit size; otherwise keep our own.
109
- if (constraints.hasBoundedWidth)
110
- this.width = constraints.maxWidth;
111
- if (constraints.hasBoundedHeight)
112
- 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);
113
155
  // Top-left coordinates; the fit logic (renderer) and the Y-flip (seam) run later.
114
156
  return { width: this.width ?? 0, height: this.height ?? 0 };
115
157
  }
@@ -122,6 +164,7 @@ export class ImageElement extends SizedPDFElement {
122
164
  image: this.image,
123
165
  fit: this.fit,
124
166
  radius: this.radius,
167
+ alt: this.alt,
125
168
  };
126
169
  }
127
170
  }
@@ -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
+ }