@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
@@ -22,18 +22,21 @@ export function insetContext(ctx, chrome) {
22
22
  return { ...ctx, pageBodyHeight: ctx.pageBodyHeight - chrome };
23
23
  }
24
24
  export function reportOverflow(child, childHeight, maxHeight, ctx) {
25
- const policy = ctx.onOverflow ?? "ignore";
26
25
  const name = child.constructor.name.replace(/Element$/, "");
27
- const detail = `${name} is ${Math.round(childHeight)}pt tall but the page region is only ` +
26
+ reportClipped(child, `${name} is ${Math.round(childHeight)}pt tall but the page region is only ` +
28
27
  `${Math.round(maxHeight)}pt and it cannot be broken - reduce its size, give it a bounded ` +
29
- `height, or let it split across pages.`;
28
+ `height, or let it split across pages.`, ctx);
29
+ }
30
+ /** Content cut off at the bottom, under the author's `onOverflow` policy. */
31
+ export function reportClipped(about, detail, ctx) {
32
+ const policy = ctx.onOverflow ?? "ignore";
30
33
  if (policy === "error")
31
34
  throw new Error(`Layout overflow: ${detail}`);
32
35
  diagnose(ctx.diagnostics, {
33
36
  code: "overflow-clipped",
34
37
  severity: "warning",
35
38
  message: `Layout overflow (clipped): ${detail}`,
36
- about: child,
39
+ about,
37
40
  page: pageOf(ctx),
38
41
  // `ignore` means "no console" - an `onDiagnostic` handler still hears of it.
39
42
  fallback: policy === "ignore" ? "silent" : "warn",
@@ -49,6 +52,10 @@ export function reportOverflow(child, childHeight, maxHeight, ctx) {
49
52
  export function childrenForceBreak(children) {
50
53
  return children.some((child, i) => child.hasForcedBreak() || (i > 0 && child.breaksBefore()) || child.breaksAfter());
51
54
  }
55
+ /** The packer kept every child as it was - none came back as a piece of itself (a sized Flow). */
56
+ export function packedUnchanged(children, fitted) {
57
+ return fitted.length === children.length && fitted.every((child, i) => child === children[i]);
58
+ }
52
59
  /**
53
60
  * Packs a vertical stack of children into `maxHeight`, in order. Children are measured
54
61
  * against `width` (unbounded height) and added until one would overflow; that straddling
@@ -80,6 +87,13 @@ export function packChildren(children, maxHeight, width, ctx, gap = 0) {
80
87
  // page, regardless of how much room is left.
81
88
  if (child.isPageBreak())
82
89
  return spill(i + 1, true);
90
+ // A column break cuts a Flow's Region; anywhere else it stays in place, draws nothing, and warns.
91
+ if (child.isColumnBreak()) {
92
+ if (ctx.inFlowRegion)
93
+ return spill(i + 1, "column");
94
+ fitted.push(child);
95
+ continue;
96
+ }
83
97
  // break-before: this child wants a fresh page. Honour it only if something is already on this region
84
98
  // (CSS ignores break-before at the top of a page); that guard also prevents an empty page + a loop.
85
99
  if (child.breaksBefore() && fitted.length > 0)
@@ -89,8 +103,10 @@ export function packChildren(children, maxHeight, width, ctx, gap = 0) {
89
103
  const lead = fitted.length > 0 ? gap : 0;
90
104
  // Place whole only if it fits AND holds no forced break: a child that fits by height but contains
91
105
  // a `PageBreak` (or a break-before/after) in its subtree must still be fragmented, or the break
92
- // would be swallowed.
93
- if (usedHeight + lead + childHeight <= maxHeight && !child.hasForcedBreak()) {
106
+ // would be swallowed. A region-sized child (a Flow) has no height of its own to fit by.
107
+ if (usedHeight + lead + childHeight <= maxHeight &&
108
+ !child.hasForcedBreak() &&
109
+ !child.needsRegion()) {
94
110
  fitted.push(child);
95
111
  usedHeight += lead + childHeight;
96
112
  // break-after: everything after this child starts a fresh page.
@@ -114,7 +130,11 @@ export function packChildren(children, maxHeight, width, ctx, gap = 0) {
114
130
  // break-before at the top of the region, which CSS ignores. Keep packing the siblings after
115
131
  // it instead of spilling them, so break-before does not accidentally push later content away.
116
132
  if (!split.remainder && !childBroke) {
117
- usedHeight += lead + childHeight;
133
+ // A region-sized child comes back as a different, sized piece: count what it holds.
134
+ const placed = split.fitted === child
135
+ ? childHeight
136
+ : split.fitted.measure(BoxConstraints.loose(width, Infinity), ctx).height;
137
+ usedHeight += lead + placed;
118
138
  continue;
119
139
  }
120
140
  if (split.remainder)
@@ -39,15 +39,15 @@ export declare class SizeCache {
39
39
  set(constraints: BoxConstraints, ctx: LayoutContext, onPage: boolean, size: Size): void;
40
40
  }
41
41
  /**
42
- * A subtree an element builds from the context at layout time (`Deferred`: a table whose `auto`
43
- * columns are measured from its cells and fitted to its width). It is built again only when the
44
- * context key or the width changes - the size rules and the one constraint building reads. Whether the
45
- * page belongs to the key is decided by what was built: a subtree without a `PageBuilder` is the same
46
- * on every page.
42
+ * What an element builds from the context at layout time: a subtree (`Deferred`: a table whose `auto`
43
+ * columns are measured from its cells and fitted to its width), or a Text's lines. It is built again
44
+ * only when the context key or the width changes - the size rules and the one constraint building
45
+ * reads. Whether the page belongs to the key is decided by what was built (`onPage`): a subtree
46
+ * without a `PageBuilder` is the same on every page.
47
47
  */
48
- export declare class BuiltFromContext<T extends {
49
- dependsOnPage(): boolean;
50
- }> {
48
+ export declare class BuiltFromContext<T> {
49
+ private readonly onPage;
51
50
  private last?;
51
+ constructor(onPage: (value: T) => boolean);
52
52
  get(ctx: LayoutContext, width: number, build: () => T): T;
53
53
  }
@@ -86,22 +86,25 @@ export class SizeCache {
86
86
  }
87
87
  }
88
88
  /**
89
- * A subtree an element builds from the context at layout time (`Deferred`: a table whose `auto`
90
- * columns are measured from its cells and fitted to its width). It is built again only when the
91
- * context key or the width changes - the size rules and the one constraint building reads. Whether the
92
- * page belongs to the key is decided by what was built: a subtree without a `PageBuilder` is the same
93
- * on every page.
89
+ * What an element builds from the context at layout time: a subtree (`Deferred`: a table whose `auto`
90
+ * columns are measured from its cells and fitted to its width), or a Text's lines. It is built again
91
+ * only when the context key or the width changes - the size rules and the one constraint building
92
+ * reads. Whether the page belongs to the key is decided by what was built (`onPage`): a subtree
93
+ * without a `PageBuilder` is the same on every page.
94
94
  */
95
95
  export class BuiltFromContext {
96
+ constructor(onPage) {
97
+ this.onPage = onPage;
98
+ }
96
99
  get(ctx, width, build) {
97
100
  const last = this.last;
98
101
  if (layoutCache.enabled &&
99
102
  last &&
100
103
  last.width === width &&
101
- sameContext(last.key, contextKeyOf(ctx, last.value.dependsOnPage())))
104
+ sameContext(last.key, contextKeyOf(ctx, this.onPage(last.value))))
102
105
  return last.value;
103
106
  const value = build();
104
- this.last = { key: contextKeyOf(ctx, value.dependsOnPage()), width, value };
107
+ this.last = { key: contextKeyOf(ctx, this.onPage(value)), width, value };
105
108
  return value;
106
109
  }
107
110
  }
@@ -1,2 +1 @@
1
- export declare function readFileBytes(_path: string): Uint8Array;
2
- export declare function readFileBytesAsync(_path: string): Promise<Uint8Array>;
1
+ export declare function readFileBytes(path: string): Uint8Array;
@@ -1,9 +1,6 @@
1
1
  // Browser stand-in for platform/node-fs.ts (selected by the package.json "browser" field). The browser has
2
2
  // no file paths, so loading from one throws a clear, actionable error - pass bytes instead.
3
- const hint = "needs Node. In the browser pass bytes (Uint8Array / ArrayBuffer) instead - e.g. import the file as a URL and fetch() it, or register a font from its bytes.";
4
- export function readFileBytes(_path) {
5
- throw new Error(`Loading a font from a file path ${hint}`);
6
- }
7
- export function readFileBytesAsync(_path) {
8
- throw new Error(`Loading an image from a file path ${hint}`);
3
+ export function readFileBytes(path) {
4
+ throw new Error(`Loading "${path}" from a file path needs Node. In the browser pass the bytes (a Uint8Array) ` +
5
+ "instead - e.g. import the file as a URL and fetch() it.");
9
6
  }
@@ -1,2 +1 @@
1
1
  export declare function readFileBytes(path: string): Uint8Array;
2
- export declare function readFileBytesAsync(path: string): Promise<Uint8Array>;
@@ -1,10 +1,6 @@
1
1
  // Node implementation of the platform file reads. In a browser bundle this whole module is swapped for
2
2
  // platform/browser-fs.ts via the package.json "browser" field, so `node:fs` never reaches the browser.
3
3
  import { readFileSync } from "node:fs";
4
- import { readFile } from "node:fs/promises";
5
4
  export function readFileBytes(path) {
6
5
  return readFileSync(path);
7
6
  }
8
- export function readFileBytesAsync(path) {
9
- return readFile(path);
10
- }
@@ -0,0 +1,8 @@
1
+ import type { RenderScope } from "../utils/renderer-registry.js";
2
+ import { PDFObjectManager } from "../utils/pdf-object-manager.js";
3
+ import { ColumnBreakElement } from "../elements/layout/column-break-element.js";
4
+ import { IRNode } from "../ir/display-list.js";
5
+ /** A `ColumnBreak` that reaches this pass was nowhere a Flow could break at it - it had no effect. */
6
+ export declare class ColumnBreakRenderer {
7
+ static render(element: ColumnBreakElement, objectManager: PDFObjectManager, _scope: RenderScope): Promise<IRNode[]>;
8
+ }
@@ -0,0 +1,15 @@
1
+ /** A `ColumnBreak` that reaches this pass was nowhere a Flow could break at it - it had no effect. */
2
+ export class ColumnBreakRenderer {
3
+ static async render(element, objectManager, _scope) {
4
+ objectManager.diagnostics.report({
5
+ code: "column-break-ignored",
6
+ severity: "warning",
7
+ message: "ColumnBreak had no effect: it breaks only where a Flow's content runs down a Region - not " +
8
+ "outside a Flow, and not in a Row or a table row, which never break. To start a new page, " +
9
+ "use PageBreak.",
10
+ about: element,
11
+ page: element.page,
12
+ });
13
+ return [];
14
+ }
15
+ }
@@ -1,5 +1,5 @@
1
1
  import { CustomBytesImage } from "../elements/image-element.js";
2
- import { decodePngToRgbFlate } from "../utils/image-helper.js";
2
+ import { decodePngToRgbFlate, jpegColor } from "../utils/image-helper.js";
3
3
  import { bytesFromLatin1 } from "../utils/bytes.js";
4
4
  // Per-URL fetch cache (process-wide): an emoji image is fetched once, however many times it appears.
5
5
  const cache = new Map();
@@ -48,6 +48,7 @@ export async function emojiImageNode(codePoint, source, x, baselineY, em) {
48
48
  // JPEG embeds raw; PNG is decoded to DeviceRGB + an /SMask alpha channel (transparency).
49
49
  let data = fileData;
50
50
  let smask;
51
+ const color = imageType === "DCTDecode" ? jpegColor(bytesFromLatin1(fileData)) : undefined;
51
52
  if (imageType === "FlateDecode") {
52
53
  const decoded = await decodePngToRgbFlate(bytesFromLatin1(fileData));
53
54
  data = decoded.data;
@@ -63,6 +64,7 @@ export async function emojiImageNode(codePoint, source, x, baselineY, em) {
63
64
  intrinsicHeight: dims.height,
64
65
  data,
65
66
  imageType,
67
+ ...(color ? { color } : {}),
66
68
  ...(smask ? { smask } : {}),
67
69
  };
68
70
  }
@@ -0,0 +1,16 @@
1
+ import { FlowElement, FlowPieceElement } from "../elements/layout/flow-element.js";
2
+ import { RegionElement } from "../elements/layout/region-element.js";
3
+ import { PDFObjectManager } from "../utils/pdf-object-manager.js";
4
+ import { IRNode } from "../ir/display-list.js";
5
+ import { type RenderScope } from "../utils/renderer-registry.js";
6
+ /** A Flow draws the page it made (laid out in place, in a box with a height). */
7
+ export declare class FlowRenderer {
8
+ static render(flow: FlowElement, objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
9
+ }
10
+ /** One page of a Flow: its template, whose Regions draw what they hold. */
11
+ export declare class FlowPieceRenderer {
12
+ static render(piece: FlowPieceElement, objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
13
+ }
14
+ export declare class RegionRenderer {
15
+ static render(region: RegionElement, objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
16
+ }
@@ -0,0 +1,21 @@
1
+ import { RendererRegistry, within } from "../utils/renderer-registry.js";
2
+ /** A Flow draws the page it made (laid out in place, in a box with a height). */
3
+ export class FlowRenderer {
4
+ static async render(flow, objectManager, scope) {
5
+ const { x, y, piece } = flow.getProps();
6
+ return piece ? RendererRegistry.render(piece, objectManager, within(scope, { x, y })) : [];
7
+ }
8
+ }
9
+ /** One page of a Flow: its template, whose Regions draw what they hold. */
10
+ export class FlowPieceRenderer {
11
+ static async render(piece, objectManager, scope) {
12
+ const { x, y, child } = piece.getProps();
13
+ return RendererRegistry.render(child, objectManager, within(scope, { x, y }));
14
+ }
15
+ }
16
+ export class RegionRenderer {
17
+ static async render(region, objectManager, scope) {
18
+ const { x, y, child } = region.getProps();
19
+ return child ? RendererRegistry.render(child, objectManager, within(scope, { x, y })) : [];
20
+ }
21
+ }
@@ -1,18 +1,20 @@
1
1
  import { BoxFit } from "../elements/image-element.js";
2
2
  import { bytesFromLatin1 } from "../utils/bytes.js";
3
- import { applyContainFit, applyCoverFit, applyFillFit, applyFitNone, decodePngToRgbFlate, } from "../utils/image-helper.js";
3
+ import { applyContainFit, applyCoverFit, applyFillFit, applyFitNone, decodePngToRgbFlate, jpegColor, naturalSize, } from "../utils/image-helper.js";
4
4
  import { isRounded } from "../ir/display-list.js";
5
5
  export class ImageRenderer {
6
6
  static async render(imageElement, objectManager, scope) {
7
7
  // Load the image and convert it in a binary string
8
- let { x: ownX, y: ownY, width, height, image, fit, radius, alt } = imageElement.getProps();
8
+ const { x: ownX, y: ownY, width, height, image, fit, radius, align, alt, } = imageElement.getProps();
9
9
  // Its own position is relative to `scope.origin`; the IR wants page coordinates.
10
- let x = scope.origin.x + (ownX ?? 0);
11
- let y = scope.origin.y + (ownY ?? 0);
10
+ const x = scope.origin.x + (ownX ?? 0);
11
+ const y = scope.origin.y + (ownY ?? 0);
12
12
  await image.init(); // Load and initialize the image
13
13
  const imageType = await image.getImageType(); // For the moment we can handle `png` and `jpg/jpeg` files
14
14
  const fileData = await image.getFileData();
15
15
  const dimensions = await image.getImageDimensions();
16
+ // Laid out at its own size in points; the pixels only describe the XObject.
17
+ const own = naturalSize(dimensions);
16
18
  if (!fileData) {
17
19
  throw new Error("File data cannot be `null`");
18
20
  }
@@ -20,71 +22,46 @@ export class ImageRenderer {
20
22
  // so decode it to raw DeviceRGB samples that the FlateDecode XObject path expects.
21
23
  let embedData = fileData;
22
24
  let smask;
25
+ // A JPEG is embedded as it is, so it keeps the color space it was saved in; a PNG becomes RGB.
26
+ const color = imageType === "DCTDecode" ? jpegColor(bytesFromLatin1(fileData)) : undefined;
23
27
  if (imageType === "FlateDecode") {
24
28
  const decoded = await decodePngToRgbFlate(bytesFromLatin1(fileData));
25
29
  embedData = decoded.data;
26
30
  smask = decoded.smask;
27
31
  }
28
- // Now we check the `fit` property and changing the dimensions of the image
29
- // Optionally we must add an overflow container
30
- let mustCreateOverflowContainer = false;
31
- const containerDimensions = JSON.parse(JSON.stringify({ x, y, width, height })); // Deep clone images dimensions
32
- switch (fit) {
33
- case BoxFit.cover:
34
- mustCreateOverflowContainer = true;
35
- const fitCoverResult = applyCoverFit(dimensions.width, dimensions.height, width ?? 0, height ?? 0);
36
- x += fitCoverResult.offsetX;
37
- y += fitCoverResult.offsetY;
38
- width = fitCoverResult.width;
39
- height = fitCoverResult.height;
40
- break;
41
- case BoxFit.contain:
42
- mustCreateOverflowContainer = true;
43
- const fitContainResult = applyContainFit(dimensions.width, dimensions.height, width ?? 0, height ?? 0);
44
- x += fitContainResult.offsetX;
45
- y += fitContainResult.offsetY;
46
- width = fitContainResult.width;
47
- height = fitContainResult.height;
48
- break;
49
- case BoxFit.none:
50
- const fitNoneResult = applyFitNone(dimensions.width, dimensions.height, width ?? 0, height ?? 0);
51
- x += fitNoneResult.offsetX;
52
- y += fitNoneResult.offsetY;
53
- width = fitNoneResult.width;
54
- height = fitNoneResult.height;
55
- break;
56
- case BoxFit.fill:
57
- const fitFillResult = applyFillFit(width ?? 0, height ?? 0);
58
- width = fitFillResult.width;
59
- height = fitFillResult.height;
60
- }
61
- // A radius rounds the image BOX (the element frame), so it clips to that frame too -
62
- // independent of the cover/contain overflow clip.
63
- const wantsClip = mustCreateOverflowContainer || isRounded(radius);
32
+ // The box laid out for it; `fit` decides what is drawn in it and where.
33
+ const box = {
34
+ x,
35
+ y,
36
+ width: width ?? own.width,
37
+ height: height ?? own.height,
38
+ };
39
+ const fitted = fit === BoxFit.cover
40
+ ? applyCoverFit(own.width, own.height, box.width, box.height, align)
41
+ : fit === BoxFit.contain
42
+ ? applyContainFit(own.width, own.height, box.width, box.height, align)
43
+ : fit === BoxFit.none
44
+ ? applyFitNone(own.width, own.height, box.width, box.height, align)
45
+ : applyFillFit(box.width, box.height);
46
+ // Anything but `fill` may leave the box, so it is clipped to it; a radius rounds the box too.
47
+ const wantsClip = fit !== BoxFit.fill || isRounded(radius);
64
48
  // The fitted geometry becomes a display-list primitive; the backend registers
65
49
  // the XObject and emits the placement (+ clip, rounded when a radius is set).
66
50
  const node = {
67
51
  type: "image",
68
- x,
69
- y,
70
- width: width,
71
- height: height,
52
+ x: x + fitted.offsetX,
53
+ y: y + fitted.offsetY,
54
+ width: fitted.width,
55
+ height: fitted.height,
72
56
  intrinsicWidth: dimensions.width,
73
57
  intrinsicHeight: dimensions.height,
74
58
  data: embedData,
75
59
  imageType,
60
+ ...(color ? { color } : {}),
61
+ ...(dimensions.orientation ? { orientation: dimensions.orientation } : {}),
76
62
  ...(smask ? { smask } : {}),
77
63
  ...(radius ? { radius } : {}),
78
- ...(wantsClip
79
- ? {
80
- clip: {
81
- x: containerDimensions.x,
82
- y: containerDimensions.y,
83
- width: containerDimensions.width,
84
- height: containerDimensions.height,
85
- },
86
- }
87
- : {}),
64
+ ...(wantsClip ? { clip: box } : {}),
88
65
  };
89
66
  // Accessible tagging: an image WITH alt text is a Figure; without, it stays untagged and the backend
90
67
  // treats it as decoration (an Artifact). One struct element per image.
@@ -10,3 +10,5 @@ export * from "./line-renderer.js";
10
10
  export * from "./row-renderer.js";
11
11
  export * from "./svg-renderer.js";
12
12
  export * from "./canvas-renderer.js";
13
+ export * from "./flow-renderer.js";
14
+ export * from "./column-break-renderer.js";
@@ -10,3 +10,5 @@ export * from "./line-renderer.js";
10
10
  export * from "./row-renderer.js";
11
11
  export * from "./svg-renderer.js";
12
12
  export * from "./canvas-renderer.js";
13
+ export * from "./flow-renderer.js";
14
+ export * from "./column-break-renderer.js";
@@ -3,6 +3,7 @@ import { resolvePageContentBox } from "../elements/page-element.js";
3
3
  import { RendererRegistry } from "../utils/renderer-registry.js";
4
4
  import { Orientation } from "./pdf-config.js";
5
5
  import { PdfBackend } from "./pdf-backend.js";
6
+ import { PositionedRenderer } from "./positioned-renderer.js";
6
7
  export class PageRenderer {
7
8
  static async render(page, objectManager) {
8
9
  const { children, config, header, footer } = page.getProps();
@@ -26,10 +27,8 @@ export class PageRenderer {
26
27
  // Collect the whole page as a display list (top-left coordinates), flip it to PDF
27
28
  // coordinates at this one seam, then serialize once. Serializing registers the
28
29
  // fonts/images used below, so it must run before the resource section.
29
- const nodes = [];
30
- for (const element of renderables) {
31
- nodes.push(...(await RendererRegistry.render(element, objectManager, scope)));
32
- }
30
+ // The page is the outermost stacking context: header, body and footer share one paint order.
31
+ const nodes = await PositionedRenderer.paintStacked(renderables, objectManager, scope);
33
32
  // Accessible tagging: one struct context per page (allocates its StructParents index), threaded into
34
33
  // serialize so each drawable node is wrapped in marked content. Undefined = tagging off (byte-identical).
35
34
  const structCtx = objectManager.struct.enabled ? objectManager.struct.beginPage() : undefined;
@@ -1,5 +1,6 @@
1
1
  import { isRounded, } from "../ir/display-list.js";
2
2
  import { Color } from "../common/color.js";
3
+ import { ORIENTATION_MATRIX } from "../utils/image-helper.js";
3
4
  /**
4
5
  * PDF backend - turns display-list primitives into content-stream operators.
5
6
  *
@@ -416,8 +417,14 @@ export class PdfBackend {
416
417
  case "image": {
417
418
  // The backend owns PDF resource creation: register the XObject (using the
418
419
  // source pixel dimensions) and then place it with a scaling matrix.
419
- const ref = om.registerImage(node.intrinsicWidth, node.intrinsicHeight, node.imageType, node.data, node.smask);
420
- const draw = `q\n${node.width} 0 0 ${node.height} ${node.x} ${node.y} cm\n` + `/IM${ref} Do\nQ\n`;
420
+ const ref = om.registerImage(node.intrinsicWidth, node.intrinsicHeight, node.imageType, node.data, node.smask, node.color);
421
+ // The placement scales the unit square to the box; an orientation turns the pixels inside it.
422
+ const upright = node.orientation
423
+ ? `${ORIENTATION_MATRIX[node.orientation].join(" ")} cm\n`
424
+ : "";
425
+ const draw = `q\n${node.width} 0 0 ${node.height} ${node.x} ${node.y} cm\n` +
426
+ upright +
427
+ `/IM${ref} Do\nQ\n`;
421
428
  if (!node.clip)
422
429
  return draw;
423
430
  // Clip to the frame (re … W n); rounded when a radius is set. The rectangular
@@ -102,7 +102,8 @@ export class PDFDocumentRenderer {
102
102
  // restores their real positions (layout is deterministic). A consumed trailing `PageBreak`
103
103
  // (forceBreak with no remainder) is the exception: render the packed `fitted` instead, which no
104
104
  // longer holds the break marker, so the original with the break does not leak into the render pass.
105
- if (isFirstRegion && remainder === null && !forceBreak) {
105
+ // So is a region that came back as a sized piece (a Flow): it is drawn as it was paginated.
106
+ if (isFirstRegion && remainder === null && !forceBreak && fitted === region) {
106
107
  pages.push({ kind: "whole", page });
107
108
  break;
108
109
  }
@@ -43,6 +43,11 @@ import { SignatureFieldElement } from "../elements/forms/signature-element.js";
43
43
  import { FormFieldRenderer } from "./form-field-renderer.js";
44
44
  import { PageBuilderElement } from "../elements/layout/page-builder-element.js";
45
45
  import { PageBuilderRenderer } from "./page-builder-renderer.js";
46
+ import { FlowElement, FlowPieceElement } from "../elements/layout/flow-element.js";
47
+ import { RegionElement } from "../elements/layout/region-element.js";
48
+ import { FlowPieceRenderer, FlowRenderer, RegionRenderer } from "./flow-renderer.js";
49
+ import { ColumnBreakElement } from "../elements/layout/column-break-element.js";
50
+ import { ColumnBreakRenderer } from "./column-break-renderer.js";
46
51
  import { RotatedElement } from "../elements/layout/rotated-element.js";
47
52
  import { RotatedBoxElement } from "../elements/layout/rotated-box-element.js";
48
53
  import { RotatedRenderer } from "./rotated-renderer.js";
@@ -78,6 +83,10 @@ export class PDFRenderer {
78
83
  RendererRegistry.register(BookmarkElement, BookmarkRenderer.render);
79
84
  RendererRegistry.register(AnchorElement, AnchorRenderer.render);
80
85
  RendererRegistry.register(PageBuilderElement, PageBuilderRenderer.render);
86
+ RendererRegistry.register(FlowElement, FlowRenderer.render);
87
+ RendererRegistry.register(FlowPieceElement, FlowPieceRenderer.render);
88
+ RendererRegistry.register(RegionElement, RegionRenderer.render);
89
+ RendererRegistry.register(ColumnBreakElement, ColumnBreakRenderer.render);
81
90
  RendererRegistry.register(RotatedElement, RotatedRenderer.render);
82
91
  RendererRegistry.register(RotatedBoxElement, RotatedRenderer.render);
83
92
  let pdfContent = "";
@@ -1,11 +1,23 @@
1
1
  import { PDFObjectManager } from "../utils/pdf-object-manager.js";
2
2
  import { PositionedElement } from "../elements/layout/positioned-element.js";
3
+ import type { PDFElement } from "../elements/pdf-element.js";
3
4
  import { type RenderScope } from "../utils/renderer-registry.js";
4
5
  import { IRNode } from "../ir/display-list.js";
5
6
  export declare class PositionedRenderer {
6
7
  /**
7
- * Drawn at its own place in the tree (tree order is paint order), from its frame: the child was laid
8
- * out relative to the frame's top-left, not to the flow it sits in.
8
+ * Rendered at its own place in the tree, from its frame: the child was laid out relative to the
9
+ * frame's top-left, not to the flow it sits in. Without a `zIndex` that place is also where it paints.
10
+ * With one it hands its drawing to the nearest stacking context (`paintStacked`), which sorts it - so
11
+ * tagging and everything else registered while rendering still sees the tree it stands in.
9
12
  */
10
13
  static render(positionedElement: PositionedElement, objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
14
+ /**
15
+ * Paints the children of a stacking context (a frame, a clipping Box, a rotation, the page): the
16
+ * `Positioned` below it with a negative `zIndex` first, then the children in tree order, then the
17
+ * positive ones - lowest first, ties in tree order. Without a `zIndex` below it, this is the children
18
+ * in order.
19
+ */
20
+ static paintStacked(children: readonly PDFElement[], objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
21
+ /** Children in tree order - what any element that is not a stacking context draws. */
22
+ static paintInOrder(children: readonly PDFElement[], objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
11
23
  }
@@ -1,11 +1,47 @@
1
1
  import { RendererRegistry } from "../utils/renderer-registry.js";
2
2
  export class PositionedRenderer {
3
3
  /**
4
- * Drawn at its own place in the tree (tree order is paint order), from its frame: the child was laid
5
- * out relative to the frame's top-left, not to the flow it sits in.
4
+ * Rendered at its own place in the tree, from its frame: the child was laid out relative to the
5
+ * frame's top-left, not to the flow it sits in. Without a `zIndex` that place is also where it paints.
6
+ * With one it hands its drawing to the nearest stacking context (`paintStacked`), which sorts it - so
7
+ * tagging and everything else registered while rendering still sees the tree it stands in.
6
8
  */
7
9
  static async render(positionedElement, objectManager, scope) {
8
- const { child } = positionedElement.getProps();
9
- return RendererRegistry.render(child, objectManager, { ...scope, origin: scope.frame });
10
+ const { child, zIndex } = positionedElement.getProps();
11
+ const render = () => RendererRegistry.render(child, objectManager, { ...scope, origin: scope.frame });
12
+ if (zIndex === 0 || !scope.stacked)
13
+ return render();
14
+ // Its place in the stack is taken BEFORE its child renders: a nested Positioned with the same
15
+ // zIndex hands its drawing in during that, and must come after its outer one, in tree order.
16
+ const entry = { zIndex, nodes: [] };
17
+ scope.stacked.push(entry);
18
+ entry.nodes = await render();
19
+ return [];
20
+ }
21
+ /**
22
+ * Paints the children of a stacking context (a frame, a clipping Box, a rotation, the page): the
23
+ * `Positioned` below it with a negative `zIndex` first, then the children in tree order, then the
24
+ * positive ones - lowest first, ties in tree order. Without a `zIndex` below it, this is the children
25
+ * in order.
26
+ */
27
+ static async paintStacked(children, objectManager, scope) {
28
+ const stacked = [];
29
+ const flow = await PositionedRenderer.paintInOrder(children, objectManager, {
30
+ ...scope,
31
+ stacked,
32
+ });
33
+ stacked.sort((a, b) => a.zIndex - b.zIndex); // stable: ties keep tree order
34
+ return [
35
+ ...stacked.filter((s) => s.zIndex < 0).flatMap((s) => s.nodes),
36
+ ...flow,
37
+ ...stacked.filter((s) => s.zIndex > 0).flatMap((s) => s.nodes),
38
+ ];
39
+ }
40
+ /** Children in tree order - what any element that is not a stacking context draws. */
41
+ static async paintInOrder(children, objectManager, scope) {
42
+ const nodes = [];
43
+ for (const child of children)
44
+ nodes.push(...(await RendererRegistry.render(child, objectManager, scope)));
45
+ return nodes;
10
46
  }
11
47
  }
@@ -1,6 +1,6 @@
1
1
  import { PDFObjectManager } from "../utils/pdf-object-manager.js";
2
2
  import { RectangleElement } from "../elements/rectangle-element.js";
3
- import { type RenderScope } from "../utils/renderer-registry.js";
3
+ import type { RenderScope } from "../utils/renderer-registry.js";
4
4
  import { IRNode } from "../ir/display-list.js";
5
5
  export declare class RectangleRenderer {
6
6
  static render(rectangleElement: RectangleElement, objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
@@ -1,5 +1,5 @@
1
- import { RendererRegistry } from "../utils/renderer-registry.js";
2
- import { isGradientInput, resolveGradient } from "../api/gradient.js";
1
+ import { PositionedRenderer } from "./positioned-renderer.js";
2
+ import { isGradientInput, resolveGradient } from "../common/gradient.js";
3
3
  export class RectangleRenderer {
4
4
  static async render(rectangleElement, objectManager, scope) {
5
5
  const { x: ownX, y: ownY, width, height, children, color, backgroundColor, borderWidth, radius, sideBorders, overflow, relative, } = rectangleElement.getProps();
@@ -46,11 +46,16 @@ export class RectangleRenderer {
46
46
  }
47
47
  // Its children are placed relative to its own top-left; a `relative` box is also the frame its
48
48
  // `Positioned` descendants were placed against.
49
- const childScope = { origin: { x, y }, frame: relative ? { x, y } : scope.frame };
49
+ const childScope = {
50
+ ...scope,
51
+ origin: { x, y },
52
+ frame: relative ? { x, y } : scope.frame,
53
+ };
54
+ // A frame or a clip paints its own `zIndex` order; any other box draws its children as they come.
50
55
  if (children)
51
- for (const child of children) {
52
- nodes.push(...(await RendererRegistry.render(child, objectManager, childScope)));
53
- }
56
+ nodes.push(...(rectangleElement.isStackingContext()
57
+ ? await PositionedRenderer.paintStacked(children, objectManager, childScope)
58
+ : await PositionedRenderer.paintInOrder(children, objectManager, childScope)));
54
59
  if (clip)
55
60
  nodes.push({ type: "clip-pop" });
56
61
  return nodes;
@@ -1,4 +1,5 @@
1
- import { RendererRegistry, within } from "../utils/renderer-registry.js";
1
+ import { within } from "../utils/renderer-registry.js";
2
+ import { PositionedRenderer } from "./positioned-renderer.js";
2
3
  export class RotatedRenderer {
3
4
  // Both `Rotated` (paint-only) and `RotatedBox` (layout-aware quarter-turns) expose the same
4
5
  // `{ angle, child, x, y, width, height }` and rotate the child around the box center - shared here.
@@ -25,7 +26,7 @@ export class RotatedRenderer {
25
26
  cy - sin * cx - cos * cy,
26
27
  ];
27
28
  // The child is placed relative to the box top-left.
28
- const childNodes = await RendererRegistry.render(child, objectManager, within(scope, { x: ownX, y: ownY }));
29
+ const childNodes = await PositionedRenderer.paintStacked([child], objectManager, within(scope, { x: ownX, y: ownY }));
29
30
  // Wrap the child's drawing between a push (apply the matrix) and a pop (restore).
30
31
  return [{ type: "transform-push", matrix }, ...childNodes, { type: "transform-pop" }];
31
32
  }