@jasy/pdf 1.0.0-beta.4 → 1.0.0-beta.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 (111) hide show
  1. package/README.md +3 -2
  2. package/dist/api/color.d.ts +1 -24
  3. package/dist/api/color.js +2 -278
  4. package/dist/api/content.d.ts +7 -2
  5. package/dist/api/content.js +17 -7
  6. package/dist/api/descriptor.js +30 -3
  7. package/dist/api/flow.d.ts +55 -0
  8. package/dist/api/flow.js +71 -0
  9. package/dist/api/gradient.d.ts +1 -53
  10. package/dist/api/gradient.js +2 -97
  11. package/dist/api/index.d.ts +2 -0
  12. package/dist/api/index.js +2 -0
  13. package/dist/api/layout.js +4 -3
  14. package/dist/api/structure.d.ts +1 -1
  15. package/dist/api/table.js +3 -1
  16. package/dist/api/text.d.ts +8 -8
  17. package/dist/api/text.js +6 -0
  18. package/dist/canvas/painter.d.ts +2 -2
  19. package/dist/canvas/painter.js +2 -2
  20. package/dist/common/color-input.d.ts +24 -0
  21. package/dist/common/color-input.js +278 -0
  22. package/dist/common/diagnostics.d.ts +9 -1
  23. package/dist/common/gradient.d.ts +53 -0
  24. package/dist/common/gradient.js +97 -0
  25. package/dist/elements/container-element.d.ts +3 -0
  26. package/dist/elements/container-element.js +17 -5
  27. package/dist/elements/image-element.d.ts +37 -26
  28. package/dist/elements/image-element.js +88 -50
  29. package/dist/elements/index.d.ts +3 -0
  30. package/dist/elements/index.js +3 -0
  31. package/dist/elements/layout/column-break-element.d.ts +17 -0
  32. package/dist/elements/layout/column-break-element.js +26 -0
  33. package/dist/elements/layout/deferred-element.d.ts +9 -1
  34. package/dist/elements/layout/deferred-element.js +18 -3
  35. package/dist/elements/layout/expanded-element.d.ts +4 -0
  36. package/dist/elements/layout/expanded-element.js +14 -1
  37. package/dist/elements/layout/flow-element.d.ts +109 -0
  38. package/dist/elements/layout/flow-element.js +317 -0
  39. package/dist/elements/layout/keep-together-element.js +14 -2
  40. package/dist/elements/layout/padding-element.d.ts +3 -0
  41. package/dist/elements/layout/padding-element.js +9 -2
  42. package/dist/elements/layout/page-builder-element.js +2 -2
  43. package/dist/elements/layout/positioned-element.d.ts +9 -2
  44. package/dist/elements/layout/positioned-element.js +6 -2
  45. package/dist/elements/layout/proxy-element.d.ts +4 -0
  46. package/dist/elements/layout/proxy-element.js +12 -0
  47. package/dist/elements/layout/region-element.d.ts +53 -0
  48. package/dist/elements/layout/region-element.js +109 -0
  49. package/dist/elements/layout/repeating-header-element.d.ts +3 -0
  50. package/dist/elements/layout/repeating-header-element.js +16 -1
  51. package/dist/elements/layout/rotated-element.d.ts +2 -0
  52. package/dist/elements/layout/rotated-element.js +8 -0
  53. package/dist/elements/pdf-element.d.ts +32 -3
  54. package/dist/elements/pdf-element.js +50 -1
  55. package/dist/elements/rectangle-element.d.ts +12 -6
  56. package/dist/elements/rectangle-element.js +31 -12
  57. package/dist/elements/row-element.d.ts +6 -12
  58. package/dist/elements/row-element.js +34 -4
  59. package/dist/elements/svg-element.js +4 -10
  60. package/dist/elements/text-element.d.ts +17 -1
  61. package/dist/elements/text-element.js +63 -40
  62. package/dist/ir/display-list.d.ts +6 -1
  63. package/dist/layout/box-constraints.d.ts +12 -0
  64. package/dist/layout/box-constraints.js +17 -0
  65. package/dist/layout/collect-images.js +8 -4
  66. package/dist/layout/flex-item.d.ts +2 -0
  67. package/dist/layout/flex-item.js +14 -2
  68. package/dist/layout/fragmentation.d.ts +11 -5
  69. package/dist/layout/fragmentation.js +27 -7
  70. package/dist/layout/layout-cache.d.ts +8 -8
  71. package/dist/layout/layout-cache.js +10 -7
  72. package/dist/platform/browser-fs.d.ts +1 -2
  73. package/dist/platform/browser-fs.js +3 -6
  74. package/dist/platform/node-fs.d.ts +0 -1
  75. package/dist/platform/node-fs.js +0 -4
  76. package/dist/renderer/column-break-renderer.d.ts +8 -0
  77. package/dist/renderer/column-break-renderer.js +15 -0
  78. package/dist/renderer/emoji-image.js +3 -1
  79. package/dist/renderer/flow-renderer.d.ts +16 -0
  80. package/dist/renderer/flow-renderer.js +21 -0
  81. package/dist/renderer/image-renderer.js +31 -54
  82. package/dist/renderer/index.d.ts +2 -0
  83. package/dist/renderer/index.js +2 -0
  84. package/dist/renderer/page-renderer.js +3 -4
  85. package/dist/renderer/pdf-backend.js +9 -2
  86. package/dist/renderer/pdf-document-renderer.js +2 -1
  87. package/dist/renderer/pdf-renderer.js +9 -0
  88. package/dist/renderer/positioned-renderer.d.ts +14 -2
  89. package/dist/renderer/positioned-renderer.js +40 -4
  90. package/dist/renderer/rectangle-renderer.d.ts +1 -1
  91. package/dist/renderer/rectangle-renderer.js +11 -6
  92. package/dist/renderer/rotated-renderer.js +3 -2
  93. package/dist/renderer/text-renderer.js +25 -28
  94. package/dist/svg/index.d.ts +1 -1
  95. package/dist/svg/index.js +26 -5
  96. package/dist/svg/style.js +1 -1
  97. package/dist/text/line-breaker.d.ts +23 -0
  98. package/dist/text/line-breaker.js +142 -65
  99. package/dist/text/line-metrics.d.ts +2 -0
  100. package/dist/text/line-metrics.js +5 -1
  101. package/dist/text/word-splitting.js +11 -5
  102. package/dist/utils/flex-layout.d.ts +3 -1
  103. package/dist/utils/flex-layout.js +141 -33
  104. package/dist/utils/image-helper.d.ts +59 -11
  105. package/dist/utils/image-helper.js +207 -80
  106. package/dist/utils/pdf-object-manager.d.ts +6 -3
  107. package/dist/utils/pdf-object-manager.js +31 -7
  108. package/dist/utils/renderer-registry.d.ts +6 -0
  109. package/dist/utils/renderer-registry.js +1 -1
  110. package/dist/utils/ttf-parser.js +3 -2
  111. package/package.json +9 -2
@@ -0,0 +1,97 @@
1
+ import { toColor } from "./color-input.js";
2
+ export const isGradientInput = (v) => typeof v === "object" && v !== null && "kind" in v && "stops" in v;
3
+ /**
4
+ * A linear gradient. `linearGradient("#fff", "#000")` runs top to bottom; pass `{ angle }` for any
5
+ * other direction, and `{ color, at }` for a stop that is not evenly spaced.
6
+ */
7
+ export function linearGradient(...args) {
8
+ const first = args[0];
9
+ if (args.length === 1 && typeof first === "object" && first !== null && "stops" in first) {
10
+ return { kind: "linear", angle: first.angle ?? 180, stops: first.stops };
11
+ }
12
+ return { kind: "linear", angle: 180, stops: args };
13
+ }
14
+ /** A radial gradient, centred by default and reaching the edges. */
15
+ export function radialGradient(...args) {
16
+ const first = args[0];
17
+ if (args.length === 1 && typeof first === "object" && first !== null && "stops" in first) {
18
+ return {
19
+ kind: "radial",
20
+ center: first.center ?? [0.5, 0.5],
21
+ radius: first.radius ?? 0.5,
22
+ stops: first.stops,
23
+ };
24
+ }
25
+ return { kind: "radial", center: [0.5, 0.5], radius: 0.5, stops: args };
26
+ }
27
+ /**
28
+ * Spread the stops that carry no `at` evenly, first to last, the way CSS does - and check the pinned
29
+ * ones, because they end up in a PDF stitching function's `/Bounds`, which the format requires to be
30
+ * strictly increasing inside the domain. A bad offset there is not a wrong-looking gradient, it is a
31
+ * malformed file, so it is refused by name here rather than emitted.
32
+ */
33
+ function toStops(stops) {
34
+ if (stops.length < 2) {
35
+ throw new Error("A gradient needs at least two colour stops.");
36
+ }
37
+ const last = stops.length - 1;
38
+ const out = stops.map((s, i) => {
39
+ const pinned = typeof s === "object" && s !== null && "at" in s;
40
+ const at = pinned ? s.at : i / last;
41
+ if (!Number.isFinite(at) || at < 0 || at > 1) {
42
+ throw new Error(`Invalid gradient stop position ${at}: it must be a number between 0 and 1.`);
43
+ }
44
+ return {
45
+ offset: at,
46
+ color: toColor(pinned ? s.color : s),
47
+ };
48
+ });
49
+ for (let i = 1; i < out.length; i++) {
50
+ if (out[i].offset <= out[i - 1].offset) {
51
+ throw new Error(`Gradient stops must move forward: stop ${i} sits at ${out[i].offset}, which is not after ` +
52
+ `${out[i - 1].offset}. (Two stops at the SAME position would be a hard colour edge in CSS; ` +
53
+ `PDF cannot express that in one shading, so it is refused rather than drawn wrongly.)`);
54
+ }
55
+ }
56
+ // A pinned first or last stop would otherwise be IGNORED - the shading's domain is always 0..1 and
57
+ // only the INTERIOR offsets reach /Bounds. Carrying the edge colour outwards is what CSS does.
58
+ if (out[0].offset > 0)
59
+ out.unshift({ offset: 0, color: out[0].color });
60
+ if (out[out.length - 1].offset < 1)
61
+ out.push({ offset: 1, color: out[out.length - 1].color });
62
+ return out;
63
+ }
64
+ /**
65
+ * Resolve a box-relative gradient against the box it paints, into the absolute page coordinates the
66
+ * IR wants. Called by the renderer, which is the first place the box's geometry is known.
67
+ *
68
+ * The angle follows CSS: 0 points to the top and it turns clockwise, so 90 goes to the right. The
69
+ * axis runs through the box centre and is scaled so it spans the box on that diagonal - which is why
70
+ * a 45-degree gradient reaches the corners rather than stopping short of them.
71
+ */
72
+ export function resolveGradient(g, x, y, width, height) {
73
+ const stops = toStops(g.stops);
74
+ if (g.kind === "radial") {
75
+ const cx = x + g.center[0] * width;
76
+ const cy = y + g.center[1] * height;
77
+ const r = g.radius * Math.max(width, height);
78
+ return { type: "radial", x0: cx, y0: cy, r0: 0, x1: cx, y1: cy, r1: r, stops, extend: "pad" };
79
+ }
80
+ const rad = ((g.angle % 360) * Math.PI) / 180;
81
+ // CSS angles: 0 = up, clockwise. The engine lays out top-left down, so "up" is -y here.
82
+ const dx = Math.sin(rad);
83
+ const dy = -Math.cos(rad);
84
+ // Half the box's extent along the axis, so the line covers the whole box for any angle.
85
+ const half = (Math.abs(dx) * width + Math.abs(dy) * height) / 2;
86
+ const cx = x + width / 2;
87
+ const cy = y + height / 2;
88
+ return {
89
+ type: "linear",
90
+ x0: cx - dx * half,
91
+ y0: cy - dy * half,
92
+ x1: cx + dx * half,
93
+ y1: cy + dy * half,
94
+ stops,
95
+ extend: "pad",
96
+ };
97
+ }
@@ -63,6 +63,7 @@ export declare class ContainerElement extends SizedPDFElement implements Fragmen
63
63
  fragment(maxHeight: number, width: number, ctx: LayoutContext): FragmentResult;
64
64
  private cloneWithChildren;
65
65
  relativeSizeFactor(horizontal: boolean): number | undefined;
66
+ maxExtent(horizontal: boolean): number;
66
67
  /**
67
68
  * A Column stacks vertically, so a flex child of its OWN only ever needs a bounded height. But the need
68
69
  * also propagates: a `Spacer` two levels down is just as unable to resolve against an infinite axis, so
@@ -74,6 +75,8 @@ export declare class ContainerElement extends SizedPDFElement implements Fragmen
74
75
  * every existing layout untouched.
75
76
  */
76
77
  needsBoundedMain(horizontal: boolean): boolean;
78
+ /** A height of its own is a region already; without one, a region-sized child needs the page's. */
79
+ needsRegion(): boolean;
77
80
  /** A Column stacks vertically, so widths overlap and heights add up. */
78
81
  minIntrinsicMain(horizontal: boolean, ctx: LayoutContext): number;
79
82
  calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
@@ -3,8 +3,8 @@ import { flexibleOf } from "../layout/flex-item.js";
3
3
  import { FlexLayoutHelper, VERTICAL_AXIS, } from "../utils/flex-layout.js";
4
4
  import { stackMinIntrinsic, withDeclaredExtent } from "../layout/min-intrinsic.js";
5
5
  import { BoxConstraints, extentSpecs, resolveSize, } from "../layout/box-constraints.js";
6
- import { childrenForceBreak, packChildren, } from "../layout/fragmentation.js";
7
- import { SizedPDFElement, } from "./pdf-element.js";
6
+ import { childrenForceBreak, packChildren, packedUnchanged, } from "../layout/fragmentation.js";
7
+ import { SizedPDFElement, withPaddingBase, } from "./pdf-element.js";
8
8
  export class ContainerElement extends SizedPDFElement {
9
9
  constructor({ x, y, width, height, children, gap, main, cross, reverse, wrap, alignContent, breakBefore, breakAfter, ...sizing }) {
10
10
  super({ x, y, width, height });
@@ -45,13 +45,14 @@ export class ContainerElement extends SizedPDFElement {
45
45
  // Pack at the width the column is drawn at: its own rules (fixed, %, min/max) resolved against the
46
46
  // region, the same sizing path as `measure`. A narrower column has taller children.
47
47
  const ownWidth = this.measure(BoxConstraints.loose(width, Infinity), ctx).width;
48
- const { fitted, remainder, forceBreak } = packChildren(this.children, maxHeight, ownWidth, ctx, this.gap);
48
+ const { fitted, remainder, forceBreak } = packChildren(this.children, maxHeight, ownWidth, withPaddingBase(ctx, undefined), this.gap);
49
49
  // Fits as one region with no forced cut: hand the whole container back so the page renders
50
50
  // unchanged (its normal layout distributes flex / fills the page). A forced break is excluded
51
51
  // here even when nothing follows it (a trailing `PageBreak`): return the packed children, which
52
52
  // no longer hold the consumed break marker, so it does not leak into the render pass.
53
- if (remainder.length === 0 && !forceBreak)
53
+ if (remainder.length === 0 && !forceBreak && packedUnchanged(this.children, fitted)) {
54
54
  return { fitted: this, remainder: null };
55
+ }
55
56
  // Nothing fit below the content above: move on whole, not as an empty shell left behind.
56
57
  if (fitted.length === 0 && !forceBreak)
57
58
  return { fitted: null, remainder: this };
@@ -84,6 +85,9 @@ export class ContainerElement extends SizedPDFElement {
84
85
  relativeSizeFactor(horizontal) {
85
86
  return horizontal ? this.requested.widthFactor : this.requested.heightFactor;
86
87
  }
88
+ maxExtent(horizontal) {
89
+ return (horizontal ? this.requested.maxWidth : this.requested.maxHeight) ?? Infinity;
90
+ }
87
91
  /**
88
92
  * A Column stacks vertically, so a flex child of its OWN only ever needs a bounded height. But the need
89
93
  * also propagates: a `Spacer` two levels down is just as unable to resolve against an infinite axis, so
@@ -103,6 +107,12 @@ export class ContainerElement extends SizedPDFElement {
103
107
  const ownFlexChild = !horizontal && this.children.some((c) => flexibleOf(c) !== undefined);
104
108
  return ownFlexChild || this.children.some((c) => c.needsBoundedMain(horizontal));
105
109
  }
110
+ /** A height of its own is a region already; without one, a region-sized child needs the page's. */
111
+ needsRegion() {
112
+ if (this.requested.height !== undefined || this.requested.heightFactor !== undefined)
113
+ return false;
114
+ return this.children.some((c) => c.needsRegion());
115
+ }
106
116
  /** A Column stacks vertically, so widths overlap and heights add up. */
107
117
  minIntrinsicMain(horizontal, ctx) {
108
118
  const content = stackMinIntrinsic(this.children, this.gap, !horizontal, horizontal, ctx, this.wrap);
@@ -153,7 +163,9 @@ export class ContainerElement extends SizedPDFElement {
153
163
  reverse: this.reverse,
154
164
  wrap: this.wrap,
155
165
  alignContent: this.alignContent,
156
- }, ctx, mode);
166
+ },
167
+ // A Row's width is for its own children; these resolve against this column, what it offers.
168
+ withPaddingBase(ctx, undefined), mode);
157
169
  }
158
170
  // Pinned/bounded axis takes that extent; an unbounded one shrink-wraps to the children (height =
159
171
  // the stack, width = the widest child). Top-left coordinates; the container draws nothing, and
@@ -1,4 +1,5 @@
1
1
  import type { Radii } from "../ir/display-list.js";
2
+ import { type FitAlign, type ImageDimensions } from "../utils/image-helper.js";
2
3
  import { BoxConstraints, Offset, Size } from "../layout/box-constraints.js";
3
4
  import { LayoutContext, SizedPDFElement } from "./pdf-element.js";
4
5
  export declare enum BoxFit {
@@ -12,25 +13,29 @@ export declare abstract class CustomImage {
12
13
  abstract init(fresh?: boolean): void | Promise<void>;
13
14
  abstract getImageType(): string | Promise<string>;
14
15
  abstract getFileData(): string | Promise<string>;
15
- abstract getImageDimensions(): Promise<{
16
- width: number;
17
- height: number;
18
- }>;
16
+ abstract getImageDimensions(): Promise<ImageDimensions>;
17
+ /**
18
+ * Its dimensions if its bytes are already in hand (bytes, a local file), else `undefined` - then the
19
+ * async pass before layout fetches them. Reading a header from memory is all this may do: layout
20
+ * asks it, so an image built during layout (in a `PageBuilder`, a table cell) knows its size too.
21
+ */
22
+ dimensionsNow(): ImageDimensions | undefined;
19
23
  }
24
+ /**
25
+ * An image from a file path (Node). The file is read HERE, when the image is named - like an SVG or a
26
+ * font file - so a wrong path fails on the line that named it, and layout never waits on a disk.
27
+ */
20
28
  export declare class CustomLocalImage extends CustomImage {
21
29
  private imagePath;
22
30
  private fileBuffer;
23
31
  private fileRawData;
24
32
  private dims?;
25
33
  constructor(imagePath: string);
26
- init(): Promise<void>;
34
+ init(): void;
27
35
  getImageType(): Promise<string>;
28
- private loadImage;
36
+ dimensionsNow(): ImageDimensions | undefined;
29
37
  getFileData(): string;
30
- getImageDimensions(): Promise<{
31
- width: number;
32
- height: number;
33
- }>;
38
+ getImageDimensions(): Promise<ImageDimensions>;
34
39
  }
35
40
  /**
36
41
  * An image straight from raw bytes (a browser upload / fetch, no filesystem). The PDF filter is sniffed
@@ -44,10 +49,8 @@ export declare class CustomBytesImage extends CustomImage {
44
49
  init(): Promise<void>;
45
50
  getImageType(): Promise<string>;
46
51
  getFileData(): string;
47
- getImageDimensions(): Promise<{
48
- width: number;
49
- height: number;
50
- }>;
52
+ getImageDimensions(): Promise<ImageDimensions>;
53
+ dimensionsNow(): ImageDimensions | undefined;
51
54
  }
52
55
  /** Thrown when an image could not be fetched or is not one we can embed. Always says which URL. */
53
56
  export declare class ImageUrlError extends Error {
@@ -76,10 +79,8 @@ export declare class CustomUrlImage extends CustomImage {
76
79
  private image;
77
80
  getImageType(): Promise<string>;
78
81
  getFileData(): string;
79
- getImageDimensions(): Promise<{
80
- width: number;
81
- height: number;
82
- }>;
82
+ getImageDimensions(): Promise<ImageDimensions>;
83
+ dimensionsNow(): ImageDimensions | undefined;
83
84
  }
84
85
  interface ImageElementParams {
85
86
  image: CustomImage;
@@ -100,9 +101,12 @@ interface ImageElementParams {
100
101
  maxHeightFactor?: number;
101
102
  /** width / height; overrides the image's OWN ratio (CSS `aspect-ratio`), `fit` then places it. */
102
103
  aspectRatio?: number;
104
+ /** How the image fills its box (CSS `object-fit`); `fill` by default, as in CSS. */
103
105
  fit?: BoxFit;
104
106
  /** Corner radius in points - one number or per corner; rounds the image box (0 = sharp, default). */
105
107
  radius?: number | Radii;
108
+ /** Where the image sits in its box when `fit` leaves space or cuts it: 0 = start, 1 = end per axis. */
109
+ align?: FitAlign;
106
110
  /** Alternate text for accessibility (tagged PDF). With `alt` the image is a Figure; without, decoration. */
107
111
  alt?: string;
108
112
  }
@@ -112,25 +116,31 @@ export declare class ImageElement extends SizedPDFElement {
112
116
  private heightFactor?;
113
117
  private fit;
114
118
  private radius;
119
+ private align?;
115
120
  private readonly alt?;
116
121
  private requested;
117
122
  /** min/max/aspectRatio, kept as given so every layout pass re-resolves them. */
118
123
  private sizing;
119
124
  private intrinsic?;
120
- constructor({ image, width, height, widthFactor, heightFactor, fit, radius, alt, ...sizing }: ImageElementParams);
125
+ constructor({ image, width, height, widthFactor, heightFactor, fit, radius, align, alt, ...sizing }: ImageElementParams);
121
126
  /**
122
- * Loads the image and records its intrinsic pixel size. Runs in the async pre-layout pass (layout
123
- * itself is synchronous and cannot await jimp) so `calculateLayout` can turn a width-only image
124
- * into a proportional box. Best-effort: a load failure leaves the size unresolved (the renderer
125
- * surfaces the real error) and the image just keeps whatever explicit size it was given.
127
+ * Fetches what layout cannot wait for (an image from a URL) and records its own size. Runs in the
128
+ * async pass before layout; an image whose bytes are in hand has nothing to fetch. Best-effort: a
129
+ * failed fetch leaves the size unresolved (the renderer surfaces the real error) and the image keeps
130
+ * whatever explicit size it was given.
126
131
  */
127
132
  resolveIntrinsicSize(): Promise<void>;
133
+ /** Its own size in points: known now, or resolved by the pass before layout; `undefined` if neither. */
134
+ private ownSize;
128
135
  relativeSizeFactor(horizontal: boolean): number | undefined;
129
- calculateLayout(constraints: BoxConstraints, offset: Offset, _ctx: LayoutContext): Size;
136
+ calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
130
137
  protected computeSize(constraints: BoxConstraints): Size;
131
- /** Its size under these constraints - the one computation both `measure` and layout use. */
138
+ /** Its size was needed from the file and the file had not been read: say so, rather than draw it wrong. */
139
+ private reportUnsized;
140
+ /** Its size under these constraints - the one computation both `measure` and layout use. `guessed`:
141
+ * a side that would have followed the file's own size did not, because that size was unknown. */
132
142
  private sizeFor;
133
- /** The image's own pixel extent on one axis, clamped by its min/max; unknown before the pre-pass. */
143
+ /** The image's own extent on one axis, clamped by its min/max; unknown before the pre-pass. */
134
144
  private ownExtent;
135
145
  getProps(): {
136
146
  x: number;
@@ -140,6 +150,7 @@ export declare class ImageElement extends SizedPDFElement {
140
150
  image: CustomImage;
141
151
  fit: BoxFit;
142
152
  radius: number | Radii;
153
+ align: FitAlign | undefined;
143
154
  alt: string | undefined;
144
155
  };
145
156
  }
@@ -1,9 +1,10 @@
1
- import { getImageDimensions } from "../utils/image-helper.js";
1
+ import { readImageDimensions, naturalSize, } from "../utils/image-helper.js";
2
2
  import { latin1FromBytes } from "../utils/bytes.js";
3
- import { readFileBytesAsync } from "../platform/node-fs.js";
3
+ import { readFileBytes } from "../platform/node-fs.js";
4
4
  import { fetchBytes } from "../utils/fetch-bytes.js";
5
- import { extentSpecs, resolveSize, } from "../layout/box-constraints.js";
5
+ import { extentSpecs, resolveSize, sizeAtOwnRatio, } from "../layout/box-constraints.js";
6
6
  import { SizedPDFElement } from "./pdf-element.js";
7
+ import { diagnose, pageOf } from "../common/diagnostics.js";
7
8
  // path.extname without node:path (browser-safe): the substring from the last dot, if it sits after the
8
9
  // last slash (so a dot in a directory name does not count). Enough for image file extensions.
9
10
  function extname(p) {
@@ -19,22 +20,41 @@ export var BoxFit;
19
20
  BoxFit["fill"] = "FILL";
20
21
  })(BoxFit || (BoxFit = {}));
21
22
  export class CustomImage {
23
+ /**
24
+ * Its dimensions if its bytes are already in hand (bytes, a local file), else `undefined` - then the
25
+ * async pass before layout fetches them. Reading a header from memory is all this may do: layout
26
+ * asks it, so an image built during layout (in a `PageBuilder`, a table cell) knows its size too.
27
+ */
28
+ dimensionsNow() {
29
+ return undefined;
30
+ }
22
31
  }
32
+ /** Dimensions read from bytes in hand; a format we cannot read is `undefined` here - the render names it. */
33
+ function headerOf(bytes) {
34
+ try {
35
+ return readImageDimensions(bytes);
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ }
41
+ /**
42
+ * An image from a file path (Node). The file is read HERE, when the image is named - like an SVG or a
43
+ * font file - so a wrong path fails on the line that named it, and layout never waits on a disk.
44
+ */
23
45
  export class CustomLocalImage extends CustomImage {
24
46
  constructor(imagePath) {
25
47
  super();
26
48
  this.imagePath = imagePath;
27
- }
28
- async init() {
29
- if (this.fileBuffer)
30
- return; // idempotent: the pre-layout size pass and the renderer both call init()
31
49
  try {
32
- await this.loadImage(this.imagePath);
50
+ this.fileBuffer = readFileBytes(imagePath);
33
51
  }
34
52
  catch (e) {
35
- throw new Error(`@jasy/pdf: could not read the image at ${this.imagePath}: ${String(e?.message ?? e)}`);
53
+ throw new Error(`@jasy/pdf: could not read the image at ${imagePath}: ${String(e?.message ?? e)}`);
36
54
  }
55
+ this.fileRawData = latin1FromBytes(this.fileBuffer);
37
56
  }
57
+ init() { }
38
58
  async getImageType() {
39
59
  const ext = extname(this.imagePath).toLowerCase();
40
60
  switch (ext) {
@@ -51,22 +71,15 @@ export class CustomLocalImage extends CustomImage {
51
71
  throw new Error(`Unsupported image format: ${ext}`);
52
72
  }
53
73
  }
54
- async loadImage(imagePath) {
55
- const result = await readFileBytesAsync(imagePath);
56
- //const result = await convertImageToGrayscaleBuffer(imagePath);
57
- this.fileBuffer = result;
58
- this.fileRawData = latin1FromBytes(result);
59
- return result;
74
+ dimensionsNow() {
75
+ this.dims ?? (this.dims = headerOf(this.fileBuffer));
76
+ return this.dims;
60
77
  }
61
78
  getFileData() {
62
79
  return this.fileRawData;
63
80
  }
64
81
  async getImageDimensions() {
65
- if (this.dims)
66
- return this.dims; // reuse: the pre-layout size pass and the renderer both ask
67
- if (!this.fileBuffer)
68
- throw new Error("@jasy/pdf: an image was measured before it was loaded");
69
- this.dims = await getImageDimensions(this.fileBuffer);
82
+ this.dims ?? (this.dims = readImageDimensions(this.fileBuffer)); // reuse across layout + render
70
83
  return this.dims;
71
84
  }
72
85
  }
@@ -93,8 +106,11 @@ export class CustomBytesImage extends CustomImage {
93
106
  return this.fileRawData;
94
107
  }
95
108
  async getImageDimensions() {
96
- if (!this.dims)
97
- this.dims = await getImageDimensions(this.bytes); // reuse across pre-pass + render
109
+ this.dims ?? (this.dims = readImageDimensions(this.bytes)); // reuse across layout + render
110
+ return this.dims;
111
+ }
112
+ dimensionsNow() {
113
+ this.dims ?? (this.dims = headerOf(this.bytes));
98
114
  return this.dims;
99
115
  }
100
116
  }
@@ -155,9 +171,12 @@ export class CustomUrlImage extends CustomImage {
155
171
  getImageDimensions() {
156
172
  return this.image().getImageDimensions();
157
173
  }
174
+ dimensionsNow() {
175
+ return this.loaded?.dimensionsNow();
176
+ }
158
177
  }
159
178
  export class ImageElement extends SizedPDFElement {
160
- constructor({ image, width, height, widthFactor, heightFactor, fit = BoxFit.none, radius, alt, ...sizing }) {
179
+ constructor({ image, width, height, widthFactor, heightFactor, fit = BoxFit.fill, radius, align, alt, ...sizing }) {
161
180
  // Seed this.width/height from the request so getProps() reflects it before layout; calculateLayout
162
181
  // then reads `requested` (never the mutated fields) and overwrites these with the laid-out size.
163
182
  super({ x: 0, y: 0, width });
@@ -169,28 +188,42 @@ export class ImageElement extends SizedPDFElement {
169
188
  this.sizing = sizing;
170
189
  this.fit = fit;
171
190
  this.radius = radius ?? 0;
191
+ this.align = align;
172
192
  this.alt = alt;
173
193
  }
174
194
  /**
175
- * Loads the image and records its intrinsic pixel size. Runs in the async pre-layout pass (layout
176
- * itself is synchronous and cannot await jimp) so `calculateLayout` can turn a width-only image
177
- * into a proportional box. Best-effort: a load failure leaves the size unresolved (the renderer
178
- * surfaces the real error) and the image just keeps whatever explicit size it was given.
195
+ * Fetches what layout cannot wait for (an image from a URL) and records its own size. Runs in the
196
+ * async pass before layout; an image whose bytes are in hand has nothing to fetch. Best-effort: a
197
+ * failed fetch leaves the size unresolved (the renderer surfaces the real error) and the image keeps
198
+ * whatever explicit size it was given.
179
199
  */
180
200
  async resolveIntrinsicSize() {
201
+ if (this.ownSize())
202
+ return;
181
203
  try {
182
204
  await this.image.init(true); // a render begins here
183
- this.intrinsic = await this.image.getImageDimensions();
205
+ this.intrinsic = naturalSize(await this.image.getImageDimensions());
184
206
  }
185
207
  catch {
186
208
  // leave intrinsic undefined - aspect derivation simply doesn't fire
187
209
  }
188
210
  }
211
+ /** Its own size in points: known now, or resolved by the pass before layout; `undefined` if neither. */
212
+ ownSize() {
213
+ if (!this.intrinsic) {
214
+ const dims = this.image.dimensionsNow();
215
+ if (dims)
216
+ this.intrinsic = naturalSize(dims);
217
+ }
218
+ return this.intrinsic;
219
+ }
189
220
  relativeSizeFactor(horizontal) {
190
221
  return horizontal ? this.widthFactor : this.heightFactor;
191
222
  }
192
- calculateLayout(constraints, offset, _ctx) {
223
+ calculateLayout(constraints, offset, ctx) {
193
224
  const size = this.sizeFor(constraints);
225
+ if (size.guessed)
226
+ this.reportUnsized(ctx);
194
227
  this.x = offset.x;
195
228
  this.y = offset.y;
196
229
  // Kept as resolved, `undefined` included: the renderer reads an unset axis differently from 0.
@@ -202,7 +235,20 @@ export class ImageElement extends SizedPDFElement {
202
235
  const size = this.sizeFor(constraints);
203
236
  return { width: size.width ?? 0, height: size.height ?? 0 };
204
237
  }
205
- /** Its size under these constraints - the one computation both `measure` and layout use. */
238
+ /** Its size was needed from the file and the file had not been read: say so, rather than draw it wrong. */
239
+ reportUnsized(ctx) {
240
+ diagnose(ctx.diagnostics, {
241
+ code: "image-unsized",
242
+ severity: "warning",
243
+ message: "An image's own size was not known when it was laid out - an image from a URL that is first " +
244
+ "built in a PageBuilder cannot be fetched before layout. Its open side fills what it is " +
245
+ "offered. Give it a width and a height, or create it outside the PageBuilder.",
246
+ about: this,
247
+ page: pageOf(ctx),
248
+ });
249
+ }
250
+ /** Its size under these constraints - the one computation both `measure` and layout use. `guessed`:
251
+ * a side that would have followed the file's own size did not, because that size was unknown. */
206
252
  sizeFor(constraints) {
207
253
  // Relative sizing: a fixed size or a fraction of the offered box (fraction only in a bounded axis).
208
254
  // Read from `requested` (not this.width/height, which we overwrite below) so every pass re-resolves.
@@ -213,28 +259,19 @@ export class ImageElement extends SizedPDFElement {
213
259
  widthFactor: this.widthFactor,
214
260
  heightFactor: this.heightFactor,
215
261
  });
216
- // An explicit `aspectRatio` wins over the image's own, as it does in CSS - and only it may size a
217
- // box with NEITHER axis pinned. Left to the intrinsic ratio that would change what an unsized image
218
- // has always done here (fill both axes and let `fit` sort it out).
262
+ // An explicit `aspectRatio` wins over the image's own.
219
263
  const resolved = resolveSize(specs.width, specs.height, this.sizing.aspectRatio, constraints);
220
264
  let w = resolved.width;
221
265
  let h = resolved.height;
222
266
  const bounds = resolved.constraints;
223
- // Aspect auto-size: when exactly ONE axis is pinned, derive the other from the intrinsic ratio
224
- // (CSS `width: 50%; height: auto`). Only fires once the pre-pass resolved the pixel size.
225
- if (this.sizing.aspectRatio === undefined &&
226
- this.intrinsic &&
227
- this.intrinsic.width > 0 &&
228
- this.intrinsic.height > 0) {
229
- const ratio = this.intrinsic.width / this.intrinsic.height;
230
- if (w !== undefined && h === undefined)
231
- h = bounds.constrainHeight(w / ratio);
232
- else if (h !== undefined && w === undefined)
233
- w = bounds.constrainWidth(h * ratio);
267
+ // Its own ratio fills an open axis, once the pre-pass resolved its own size.
268
+ const own = this.ownSize();
269
+ if (this.sizing.aspectRatio === undefined && own && own.width > 0 && own.height > 0) {
270
+ ({ width: w, height: h } = sizeAtOwnRatio(w, h, own, bounds));
234
271
  }
235
- // An unpinned axis: a bounded one fills; an unbounded one takes the image's own extent (1px = 1pt) -
236
- // what `fit: none` (the default) draws there, so the layout reserves exactly what is drawn. Read from
237
- // `requested`, never the mutated this.width/height.
272
+ const guessed = own === undefined && (w === undefined || h === undefined);
273
+ // Still unpinned (no intrinsic size known): a bounded axis fills; an unbounded one takes the image's
274
+ // own extent (`naturalSize`). Read from `requested`, never the mutated this.width/height.
238
275
  const width = w ??
239
276
  (bounds.hasBoundedWidth
240
277
  ? bounds.maxWidth
@@ -244,11 +281,11 @@ export class ImageElement extends SizedPDFElement {
244
281
  ? bounds.maxHeight
245
282
  : (this.requested.height ?? this.ownExtent(bounds, "height")));
246
283
  // Top-left coordinates; the fit logic (renderer) and the Y-flip (seam) run later.
247
- return { width, height };
284
+ return { width, height, guessed };
248
285
  }
249
- /** The image's own pixel extent on one axis, clamped by its min/max; unknown before the pre-pass. */
286
+ /** The image's own extent on one axis, clamped by its min/max; unknown before the pre-pass. */
250
287
  ownExtent(bounds, axis) {
251
- const own = this.intrinsic?.[axis];
288
+ const own = this.ownSize()?.[axis];
252
289
  if (own === undefined)
253
290
  return undefined;
254
291
  return axis === "width" ? bounds.constrainWidth(own) : bounds.constrainHeight(own);
@@ -262,6 +299,7 @@ export class ImageElement extends SizedPDFElement {
262
299
  image: this.image,
263
300
  fit: this.fit,
264
301
  radius: this.radius,
302
+ align: this.align,
265
303
  alt: this.alt,
266
304
  };
267
305
  }
@@ -20,6 +20,9 @@ export * from "./layout/link-element.js";
20
20
  export * from "./layout/bookmark-element.js";
21
21
  export * from "./layout/anchor-element.js";
22
22
  export * from "./layout/page-builder-element.js";
23
+ export * from "./layout/flow-element.js";
24
+ export * from "./layout/region-element.js";
25
+ export * from "./layout/column-break-element.js";
23
26
  export * from "./layout/rotated-element.js";
24
27
  export * from "./layout/rotated-box-element.js";
25
28
  export * from "./line-element.js";
@@ -20,6 +20,9 @@ export * from "./layout/link-element.js";
20
20
  export * from "./layout/bookmark-element.js";
21
21
  export * from "./layout/anchor-element.js";
22
22
  export * from "./layout/page-builder-element.js";
23
+ export * from "./layout/flow-element.js";
24
+ export * from "./layout/region-element.js";
25
+ export * from "./layout/column-break-element.js";
23
26
  export * from "./layout/rotated-element.js";
24
27
  export * from "./layout/rotated-box-element.js";
25
28
  export * from "./line-element.js";
@@ -0,0 +1,17 @@
1
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
2
+ import { LayoutContext, PDFElement } from "../pdf-element.js";
3
+ /**
4
+ * A forced break to the next Region of a `Flow` - from the last one, the next page's first. Always of
5
+ * the innermost Flow; several skip several. It takes no space and draws nothing: the packer of a Region
6
+ * consumes it, so reaching the render pass means it sat outside one, which is reported there.
7
+ */
8
+ export declare class ColumnBreakElement extends PDFElement {
9
+ /** The page it was last laid out on - only so an ineffective break can name it. */
10
+ page?: number;
11
+ calculateLayout(_constraints: BoxConstraints, _offset: Offset, ctx: LayoutContext): Size;
12
+ protected computeSize(): Size;
13
+ isColumnBreak(): boolean;
14
+ /** So a container around it is packed (and the break found) instead of placed whole. */
15
+ hasForcedBreak(): boolean;
16
+ getProps(): Record<string, never>;
17
+ }
@@ -0,0 +1,26 @@
1
+ import { PDFElement } from "../pdf-element.js";
2
+ import { pageOf } from "../../common/diagnostics.js";
3
+ /**
4
+ * A forced break to the next Region of a `Flow` - from the last one, the next page's first. Always of
5
+ * the innermost Flow; several skip several. It takes no space and draws nothing: the packer of a Region
6
+ * consumes it, so reaching the render pass means it sat outside one, which is reported there.
7
+ */
8
+ export class ColumnBreakElement extends PDFElement {
9
+ calculateLayout(_constraints, _offset, ctx) {
10
+ this.page = pageOf(ctx);
11
+ return { width: 0, height: 0 };
12
+ }
13
+ computeSize() {
14
+ return { width: 0, height: 0 };
15
+ }
16
+ isColumnBreak() {
17
+ return true;
18
+ }
19
+ /** So a container around it is packed (and the break found) instead of placed whole. */
20
+ hasForcedBreak() {
21
+ return true;
22
+ }
23
+ getProps() {
24
+ return {};
25
+ }
26
+ }
@@ -8,9 +8,16 @@ import { LayoutContext, PDFElement } from "../pdf-element.js";
8
8
  */
9
9
  export declare class DeferredElement extends PDFElement implements Fragmentable {
10
10
  private resolve;
11
+ private readonly sources;
11
12
  private composed?;
12
13
  private built;
13
- constructor(resolve: (ctx: LayoutContext, width: number) => PDFElement);
14
+ /**
15
+ * `sources`: the elements the subtree will be built from (a table's cells), so the async pass before
16
+ * layout can reach what is not built yet - an image from a URL in a cell.
17
+ */
18
+ constructor(resolve: (ctx: LayoutContext, width: number) => PDFElement, sources?: readonly PDFElement[]);
19
+ /** A copy builds a subtree of its own, from copies, so even element cells it holds are its own. */
20
+ clone(): this;
14
21
  /** The subtree for this context and width: built once, and again only when one of them changes. */
15
22
  private subtree;
16
23
  private build;
@@ -23,5 +30,6 @@ export declare class DeferredElement extends PDFElement implements Fragmentable
23
30
  layoutChildren(): readonly PDFElement[];
24
31
  getProps(): {
25
32
  composed: PDFElement | undefined;
33
+ sources: readonly PDFElement[];
26
34
  };
27
35
  }