@jasy/pdf 1.0.0-beta.4 → 1.0.0-beta.5

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 (109) hide show
  1. package/dist/api/color.d.ts +1 -24
  2. package/dist/api/color.js +2 -278
  3. package/dist/api/content.d.ts +7 -2
  4. package/dist/api/content.js +17 -7
  5. package/dist/api/descriptor.js +30 -3
  6. package/dist/api/flow.d.ts +55 -0
  7. package/dist/api/flow.js +71 -0
  8. package/dist/api/gradient.d.ts +1 -53
  9. package/dist/api/gradient.js +2 -97
  10. package/dist/api/index.d.ts +2 -0
  11. package/dist/api/index.js +2 -0
  12. package/dist/api/layout.js +4 -3
  13. package/dist/api/table.js +3 -1
  14. package/dist/api/text.d.ts +8 -8
  15. package/dist/api/text.js +6 -0
  16. package/dist/canvas/painter.d.ts +2 -2
  17. package/dist/canvas/painter.js +2 -2
  18. package/dist/common/color-input.d.ts +24 -0
  19. package/dist/common/color-input.js +278 -0
  20. package/dist/common/diagnostics.d.ts +9 -1
  21. package/dist/common/gradient.d.ts +53 -0
  22. package/dist/common/gradient.js +97 -0
  23. package/dist/elements/container-element.d.ts +3 -0
  24. package/dist/elements/container-element.js +17 -5
  25. package/dist/elements/image-element.d.ts +37 -26
  26. package/dist/elements/image-element.js +88 -50
  27. package/dist/elements/index.d.ts +3 -0
  28. package/dist/elements/index.js +3 -0
  29. package/dist/elements/layout/column-break-element.d.ts +17 -0
  30. package/dist/elements/layout/column-break-element.js +26 -0
  31. package/dist/elements/layout/deferred-element.d.ts +9 -1
  32. package/dist/elements/layout/deferred-element.js +18 -3
  33. package/dist/elements/layout/expanded-element.d.ts +4 -0
  34. package/dist/elements/layout/expanded-element.js +14 -1
  35. package/dist/elements/layout/flow-element.d.ts +109 -0
  36. package/dist/elements/layout/flow-element.js +317 -0
  37. package/dist/elements/layout/keep-together-element.js +14 -2
  38. package/dist/elements/layout/padding-element.d.ts +3 -0
  39. package/dist/elements/layout/padding-element.js +9 -2
  40. package/dist/elements/layout/page-builder-element.js +2 -2
  41. package/dist/elements/layout/positioned-element.d.ts +9 -2
  42. package/dist/elements/layout/positioned-element.js +6 -2
  43. package/dist/elements/layout/proxy-element.d.ts +4 -0
  44. package/dist/elements/layout/proxy-element.js +12 -0
  45. package/dist/elements/layout/region-element.d.ts +53 -0
  46. package/dist/elements/layout/region-element.js +108 -0
  47. package/dist/elements/layout/repeating-header-element.d.ts +3 -0
  48. package/dist/elements/layout/repeating-header-element.js +16 -1
  49. package/dist/elements/layout/rotated-element.d.ts +2 -0
  50. package/dist/elements/layout/rotated-element.js +8 -0
  51. package/dist/elements/pdf-element.d.ts +32 -3
  52. package/dist/elements/pdf-element.js +50 -1
  53. package/dist/elements/rectangle-element.d.ts +12 -6
  54. package/dist/elements/rectangle-element.js +31 -12
  55. package/dist/elements/row-element.d.ts +6 -12
  56. package/dist/elements/row-element.js +34 -4
  57. package/dist/elements/svg-element.js +4 -10
  58. package/dist/elements/text-element.d.ts +17 -1
  59. package/dist/elements/text-element.js +63 -40
  60. package/dist/ir/display-list.d.ts +6 -1
  61. package/dist/layout/box-constraints.d.ts +12 -0
  62. package/dist/layout/box-constraints.js +17 -0
  63. package/dist/layout/collect-images.js +8 -4
  64. package/dist/layout/flex-item.d.ts +2 -0
  65. package/dist/layout/flex-item.js +14 -2
  66. package/dist/layout/fragmentation.d.ts +11 -5
  67. package/dist/layout/fragmentation.js +27 -7
  68. package/dist/layout/layout-cache.d.ts +8 -8
  69. package/dist/layout/layout-cache.js +10 -7
  70. package/dist/platform/browser-fs.d.ts +1 -2
  71. package/dist/platform/browser-fs.js +3 -6
  72. package/dist/platform/node-fs.d.ts +0 -1
  73. package/dist/platform/node-fs.js +0 -4
  74. package/dist/renderer/column-break-renderer.d.ts +8 -0
  75. package/dist/renderer/column-break-renderer.js +15 -0
  76. package/dist/renderer/emoji-image.js +3 -1
  77. package/dist/renderer/flow-renderer.d.ts +16 -0
  78. package/dist/renderer/flow-renderer.js +21 -0
  79. package/dist/renderer/image-renderer.js +31 -54
  80. package/dist/renderer/index.d.ts +2 -0
  81. package/dist/renderer/index.js +2 -0
  82. package/dist/renderer/page-renderer.js +3 -4
  83. package/dist/renderer/pdf-backend.js +9 -2
  84. package/dist/renderer/pdf-document-renderer.js +2 -1
  85. package/dist/renderer/pdf-renderer.js +9 -0
  86. package/dist/renderer/positioned-renderer.d.ts +14 -2
  87. package/dist/renderer/positioned-renderer.js +40 -4
  88. package/dist/renderer/rectangle-renderer.d.ts +1 -1
  89. package/dist/renderer/rectangle-renderer.js +11 -6
  90. package/dist/renderer/rotated-renderer.js +3 -2
  91. package/dist/renderer/text-renderer.js +25 -28
  92. package/dist/svg/index.d.ts +1 -1
  93. package/dist/svg/index.js +26 -5
  94. package/dist/svg/style.js +1 -1
  95. package/dist/text/line-breaker.d.ts +23 -0
  96. package/dist/text/line-breaker.js +142 -65
  97. package/dist/text/line-metrics.d.ts +2 -0
  98. package/dist/text/line-metrics.js +5 -1
  99. package/dist/text/word-splitting.js +11 -5
  100. package/dist/utils/flex-layout.d.ts +3 -1
  101. package/dist/utils/flex-layout.js +141 -33
  102. package/dist/utils/image-helper.d.ts +59 -11
  103. package/dist/utils/image-helper.js +207 -80
  104. package/dist/utils/pdf-object-manager.d.ts +6 -3
  105. package/dist/utils/pdf-object-manager.js +31 -7
  106. package/dist/utils/renderer-registry.d.ts +6 -0
  107. package/dist/utils/renderer-registry.js +1 -1
  108. package/dist/utils/ttf-parser.js +3 -2
  109. package/package.json +1 -1
@@ -1,16 +1,31 @@
1
1
  import { isFragmentable } from "../../layout/fragmentation.js";
2
2
  import { PDFElement } from "../pdf-element.js";
3
3
  import { BuiltFromContext } from "../../layout/layout-cache.js";
4
+ const builtOnPage = (subtree) => subtree.dependsOnPage();
4
5
  /**
5
6
  * Builds its subtree at layout time via `resolve(ctx, width)`, so the tree can depend on font
6
7
  * metrics and on the width it gets (e.g. a Table fitting its `"auto"` and `fr` columns to the cell
7
8
  * content and its own width). The engine stays table-agnostic - the closure comes from the API layer.
8
9
  */
9
10
  export class DeferredElement extends PDFElement {
10
- constructor(resolve) {
11
+ /**
12
+ * `sources`: the elements the subtree will be built from (a table's cells), so the async pass before
13
+ * layout can reach what is not built yet - an image from a URL in a cell.
14
+ */
15
+ constructor(resolve, sources = []) {
11
16
  super();
12
17
  this.resolve = resolve;
13
- this.built = new BuiltFromContext();
18
+ this.sources = sources;
19
+ this.built = new BuiltFromContext(builtOnPage);
20
+ }
21
+ /** A copy builds a subtree of its own, from copies, so even element cells it holds are its own. */
22
+ clone() {
23
+ const copy = super.clone();
24
+ const resolve = this.resolve;
25
+ copy.resolve = (ctx, width) => resolve(ctx, width).clone();
26
+ copy.built = new BuiltFromContext(builtOnPage);
27
+ copy.composed = undefined;
28
+ return copy;
14
29
  }
15
30
  /** The subtree for this context and width: built once, and again only when one of them changes. */
16
31
  subtree(ctx, width) {
@@ -41,6 +56,6 @@ export class DeferredElement extends PDFElement {
41
56
  return this.composed ? [this.composed] : [];
42
57
  }
43
58
  getProps() {
44
- return { composed: this.composed };
59
+ return { composed: this.composed, sources: this.sources };
45
60
  }
46
61
  }
@@ -12,6 +12,8 @@ export declare class ExpandedElement extends FlexiblePDFElement implements Fragm
12
12
  constructor({ flex, basis, basisFactor, child }: ExpandedElementParams);
13
13
  /** Layout-transparent: the floor is whatever the wrapped child needs. */
14
14
  minIntrinsicMain(horizontal: boolean, ctx: LayoutContext): number;
15
+ /** And the ceiling is the child's. */
16
+ maxExtent(horizontal: boolean): number;
15
17
  calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
16
18
  protected computeSize(constraints: BoxConstraints, ctx: LayoutContext): Size;
17
19
  /** The one sizing path: with an `offset` the child is laid out there, with `null` only measured. */
@@ -23,6 +25,8 @@ export declare class ExpandedElement extends FlexiblePDFElement implements Fragm
23
25
  * whole Expanded moves on.
24
26
  */
25
27
  fragment(maxHeight: number, width: number, ctx: LayoutContext): FragmentResult;
28
+ needsRegion(): boolean;
29
+ needsBoundedMain(horizontal: boolean): boolean;
26
30
  hasForcedBreak(): boolean;
27
31
  breaksBefore(): boolean;
28
32
  breaksAfter(): boolean;
@@ -15,6 +15,10 @@ export class ExpandedElement extends FlexiblePDFElement {
15
15
  minIntrinsicMain(horizontal, ctx) {
16
16
  return this.child.minIntrinsicMain(horizontal, ctx);
17
17
  }
18
+ /** And the ceiling is the child's. */
19
+ maxExtent(horizontal) {
20
+ return this.child.maxExtent(horizontal);
21
+ }
18
22
  calculateLayout(constraints, offset, ctx) {
19
23
  Validator.validateFlexElement(this);
20
24
  const size = this.sizeWith(constraints, ctx, offset);
@@ -36,7 +40,9 @@ export class ExpandedElement extends FlexiblePDFElement {
36
40
  // A bounded region: fill it - the normal flex behavior. Unbounded (measuring while paginating):
37
41
  // there's no leftover space to fill, so collapse to the child's natural height. This lets an
38
42
  // overflowing column flow instead of the flex silently hiding the overflow.
39
- const childConstraints = BoxConstraints.loose(width, constraints.hasBoundedHeight ? constraints.maxHeight : Infinity);
43
+ // `Expanded(child)` is `flex: 1` on the child: the extent its line SET (the min) is the child's too,
44
+ // so its own width or height gives way to its share - as in CSS and Flutter.
45
+ const childConstraints = new BoxConstraints(constraints.minWidth, width, constraints.minHeight, constraints.hasBoundedHeight ? constraints.maxHeight : Infinity);
40
46
  const childSize = offset
41
47
  ? this.child.calculateLayout(childConstraints, { x: offset.x, y: offset.y }, ctx)
42
48
  : this.child.measure(childConstraints, ctx);
@@ -62,6 +68,13 @@ export class ExpandedElement extends FlexiblePDFElement {
62
68
  forceBreak: split.forceBreak,
63
69
  };
64
70
  }
71
+ needsRegion() {
72
+ return this.child.needsRegion();
73
+ }
74
+ // The child IS the flex item: what it needs across the line, the line must give it.
75
+ needsBoundedMain(horizontal) {
76
+ return this.child.needsBoundedMain(horizontal);
77
+ }
65
78
  // A break inside the flex child is a break in the flow it sits in.
66
79
  hasForcedBreak() {
67
80
  return this.child.hasForcedBreak();
@@ -0,0 +1,109 @@
1
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
2
+ import { type FragmentResult } from "../../layout/fragmentation.js";
3
+ import { ContainerElement } from "../container-element.js";
4
+ import { LayoutContext, PDFElement, SizedPDFElement } from "../pdf-element.js";
5
+ /** What a template is built from: `index` counts THIS Flow's pages (0 = where it starts). */
6
+ export interface FlowPage {
7
+ index: number;
8
+ pageNumber: number;
9
+ }
10
+ export type FlowTemplate = (page: FlowPage) => PDFElement;
11
+ /** A template with no `Region` would place nothing on every page, forever. */
12
+ export declare class FlowWithoutRegionError extends Error {
13
+ constructor();
14
+ }
15
+ /** A template that leaves every Region without room, where the Flow cannot move on to find more. */
16
+ export declare class FlowRegionsEmptyError extends Error {
17
+ constructor(width: number, height: number);
18
+ }
19
+ /** Laid out with no height to fill: every line would land in the first Region. */
20
+ export declare class FlowUnboundedError extends Error {
21
+ constructor();
22
+ }
23
+ /** A PageBuilder builds again for the page it is drawn on, so Regions inside it lose their content. */
24
+ export declare class RegionInPageBuilderError extends Error {
25
+ constructor();
26
+ }
27
+ /**
28
+ * One page of a Flow: its template, with each Region holding its content. It is drawn laid out at the
29
+ * height it was filled at - not at its own size, which a template that does not fill (`50%`) differs from.
30
+ */
31
+ export declare class FlowPieceElement extends SizedPDFElement {
32
+ private readonly template;
33
+ private readonly size;
34
+ private readonly bound;
35
+ private readonly page;
36
+ constructor(template: PDFElement, size: Size, bound: number, page: object);
37
+ calculateLayout(_constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
38
+ protected computeSize(): Size;
39
+ layoutChildren(): readonly PDFElement[];
40
+ getProps(): {
41
+ child: PDFElement;
42
+ width: number;
43
+ height: number;
44
+ x: number;
45
+ y: number;
46
+ };
47
+ }
48
+ /**
49
+ * Content flowing through the Regions of a template, page after page. It has no height of its own -
50
+ * like an `Expanded`, it takes what it is given: the rest of the page when it is fragmented, the rest
51
+ * of a box with a height otherwise. On its last page it shrinks to what the content used.
52
+ */
53
+ export declare class FlowElement extends SizedPDFElement {
54
+ private readonly content;
55
+ private readonly template;
56
+ private readonly index;
57
+ private readonly balance;
58
+ private piece?;
59
+ constructor({ content, template, index, balance, }: {
60
+ content: ContainerElement;
61
+ template: FlowTemplate;
62
+ index?: number;
63
+ balance?: boolean;
64
+ });
65
+ needsRegion(): boolean;
66
+ /** Its height comes from the parent, as an `Expanded`'s does. */
67
+ needsBoundedMain(horizontal: boolean): boolean;
68
+ /** The template is built from the page it lands on. */
69
+ dependsOnPage(): boolean;
70
+ fragment(maxHeight: number, width: number, ctx: LayoutContext): FragmentResult;
71
+ calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
72
+ /** Unbounded, it has no height of its own (a parent that bounds it asks again); bounded, it fills. */
73
+ protected computeSize(constraints: BoxConstraints, ctx: LayoutContext): Size;
74
+ /**
75
+ * The one place a page of the Flow is made: the template at `width` x `maxHeight`, the content
76
+ * fragmented into its Regions in turn, and - when the content ends here - the template shrunk to
77
+ * what it used, or balanced.
78
+ */
79
+ private fill;
80
+ /**
81
+ * The content fragmented into the laid-out Regions in turn; each holds its piece. `fresh` is how tall
82
+ * each Region is on a full page - what a block that does not fit here may wait for.
83
+ */
84
+ private pour;
85
+ /**
86
+ * The last page, balanced: the smallest height at which the content, poured again, still ends here,
87
+ * so the Regions come out about equally full. Every candidate is a real pour - breaks,
88
+ * `keepTogether` and orphans hold as on any page - and the last one poured is the one drawn. A
89
+ * candidate is silent, and one that had to clip a block it could not break is too low.
90
+ */
91
+ private balanced;
92
+ /**
93
+ * The last page: the smallest height at which every filled Region still holds what it was given.
94
+ * Only a layout knows how a template's Regions answer a height (`%`, stacked, `minHeight`, fixed), so
95
+ * each candidate is laid out and checked. The first is exact for Regions side by side (the full bound
96
+ * less the smallest slack); below it the bound is searched, down to the template's own height.
97
+ * Content that overflowed its Region at full height asks for no more than it had there.
98
+ */
99
+ private shrink;
100
+ layoutChildren(): readonly PDFElement[];
101
+ getProps(): {
102
+ x: number;
103
+ y: number;
104
+ width: number | undefined;
105
+ height: number | undefined;
106
+ child: ContainerElement;
107
+ piece: FlowPieceElement | undefined;
108
+ };
109
+ }
@@ -0,0 +1,317 @@
1
+ import { BoxConstraints } from "../../layout/box-constraints.js";
2
+ import { reportClipped } from "../../layout/fragmentation.js";
3
+ import { Diagnostics, pageOf } from "../../common/diagnostics.js";
4
+ import { SizedPDFElement } from "../pdf-element.js";
5
+ import { PageBuilderElement } from "./page-builder-element.js";
6
+ import { RegionElement } from "./region-element.js";
7
+ /** A template with no `Region` would place nothing on every page, forever. */
8
+ export class FlowWithoutRegionError extends Error {
9
+ constructor() {
10
+ super("@jasy/pdf: a Flow's template holds no Region, so the content has nowhere to go. Put at least " +
11
+ "one Region in the template (in @jasy/vue: the #page slot).");
12
+ this.name = "FlowWithoutRegionError";
13
+ }
14
+ }
15
+ /** A template that leaves every Region without room, where the Flow cannot move on to find more. */
16
+ export class FlowRegionsEmptyError extends Error {
17
+ constructor(width, height) {
18
+ super(`@jasy/pdf: a Flow's template leaves its Regions no room in ${Math.round(width)} x ` +
19
+ `${Math.round(height)}pt - every Region is 0 wide or 0 tall (too many columns or gaps for the ` +
20
+ "width, or the rest of the template is taller than the space). Give the Regions room." +
21
+ (height <= 0
22
+ ? " In a wrapping Row a line is only as tall as its content - give the Row around the " +
23
+ "Flow a height."
24
+ : ""));
25
+ this.name = "FlowRegionsEmptyError";
26
+ }
27
+ }
28
+ /** Laid out with no height to fill: every line would land in the first Region. */
29
+ export class FlowUnboundedError extends Error {
30
+ constructor() {
31
+ super("@jasy/pdf: a Flow takes its height from where it sits - the page, or a box with a height. " +
32
+ "Here it has none (in a Row without a height, for instance). Give the Row or a Box around " +
33
+ "the Flow a height.");
34
+ this.name = "FlowUnboundedError";
35
+ }
36
+ }
37
+ /** A PageBuilder builds again for the page it is drawn on, so Regions inside it lose their content. */
38
+ export class RegionInPageBuilderError extends Error {
39
+ constructor() {
40
+ super("@jasy/pdf: a Region inside a PageBuilder - it builds again for the page it is drawn on, and " +
41
+ "its Regions would come back empty. A Flow's template is built for every page already and " +
42
+ "gets { index, pageNumber }: build the Regions there.");
43
+ this.name = "RegionInPageBuilderError";
44
+ }
45
+ }
46
+ /** The Regions of a laid-out template, in tree order - the order the content flows through them. */
47
+ function regionsIn(root) {
48
+ const out = [];
49
+ const visit = (el, built) => {
50
+ if (el instanceof RegionElement) {
51
+ if (built)
52
+ throw new RegionInPageBuilderError();
53
+ out.push(el);
54
+ }
55
+ else {
56
+ const inBuilder = built || el instanceof PageBuilderElement;
57
+ for (const child of el.layoutChildren())
58
+ visit(child, inBuilder);
59
+ }
60
+ };
61
+ visit(root, false);
62
+ return out;
63
+ }
64
+ const room = (r) => r.getProps().width > 0 && r.getProps().height > 0;
65
+ const SLACK_EPSILON = 1e-6;
66
+ // How close the last page's height gets to the tightest one that holds the content, in points.
67
+ const SHRINK_PRECISION = 0.01;
68
+ /**
69
+ * One page of a Flow: its template, with each Region holding its content. It is drawn laid out at the
70
+ * height it was filled at - not at its own size, which a template that does not fill (`50%`) differs from.
71
+ */
72
+ export class FlowPieceElement extends SizedPDFElement {
73
+ constructor(template, size, bound, page) {
74
+ super({ x: 0, y: 0, ...size });
75
+ this.template = template;
76
+ this.size = size;
77
+ this.bound = bound;
78
+ this.page = page;
79
+ }
80
+ calculateLayout(_constraints, offset, ctx) {
81
+ this.x = offset.x;
82
+ this.y = offset.y;
83
+ this.template.calculateLayout(BoxConstraints.loose(this.size.width, this.bound), { x: 0, y: 0 }, { ...ctx, flowPage: this.page });
84
+ return this.size;
85
+ }
86
+ computeSize() {
87
+ return this.size;
88
+ }
89
+ layoutChildren() {
90
+ return [this.template];
91
+ }
92
+ getProps() {
93
+ return { x: this.x, y: this.y, ...this.size, child: this.template };
94
+ }
95
+ }
96
+ /**
97
+ * Content flowing through the Regions of a template, page after page. It has no height of its own -
98
+ * like an `Expanded`, it takes what it is given: the rest of the page when it is fragmented, the rest
99
+ * of a box with a height otherwise. On its last page it shrinks to what the content used.
100
+ */
101
+ export class FlowElement extends SizedPDFElement {
102
+ constructor({ content, template, index = 0, balance = false, }) {
103
+ super({ x: 0, y: 0 });
104
+ this.content = content;
105
+ this.template = template;
106
+ this.index = index;
107
+ this.balance = balance;
108
+ }
109
+ needsRegion() {
110
+ return true;
111
+ }
112
+ /** Its height comes from the parent, as an `Expanded`'s does. */
113
+ needsBoundedMain(horizontal) {
114
+ return !horizontal;
115
+ }
116
+ /** The template is built from the page it lands on. */
117
+ dependsOnPage() {
118
+ return true;
119
+ }
120
+ fragment(maxHeight, width, ctx) {
121
+ // Below other content, a template with no room here may find it on the next page.
122
+ const filled = this.fill(maxHeight, width, ctx, !ctx.belowContent);
123
+ // Nothing fit: move on whole. On a fresh page the packer forces it on, so this always advances.
124
+ if (!filled.placed)
125
+ return { fitted: null, remainder: this };
126
+ return {
127
+ fitted: filled.piece,
128
+ remainder: filled.rest
129
+ ? new FlowElement({
130
+ content: filled.rest,
131
+ template: this.template,
132
+ index: this.index + 1,
133
+ balance: this.balance,
134
+ })
135
+ : null,
136
+ forceBreak: filled.forceBreak,
137
+ };
138
+ }
139
+ calculateLayout(constraints, offset, ctx) {
140
+ if (!constraints.hasBoundedHeight || !constraints.hasBoundedWidth) {
141
+ throw new FlowUnboundedError();
142
+ }
143
+ this.x = offset.x;
144
+ this.y = offset.y;
145
+ const filled = this.fill(constraints.maxHeight, constraints.maxWidth, ctx, true);
146
+ // Not paginated here (a box with a height): what does not fit is cut, as any overflow is.
147
+ if (filled.rest) {
148
+ reportClipped(this, "a Flow has more content than its Regions hold here, and it cannot continue on a next " +
149
+ "page (it sits in a box with a height) - give the box more height, or let the Flow " +
150
+ "continue on the page.", ctx);
151
+ }
152
+ this.piece = filled.piece;
153
+ const size = this.piece.calculateLayout(constraints, { x: 0, y: 0 }, ctx);
154
+ this.width = size.width;
155
+ this.height = size.height;
156
+ return size;
157
+ }
158
+ /** Unbounded, it has no height of its own (a parent that bounds it asks again); bounded, it fills. */
159
+ computeSize(constraints, ctx) {
160
+ if (!constraints.hasBoundedHeight || !constraints.hasBoundedWidth) {
161
+ return { width: constraints.hasBoundedWidth ? constraints.maxWidth : 0, height: 0 };
162
+ }
163
+ return this.fill(constraints.maxHeight, constraints.maxWidth, ctx, false).piece.measure(constraints, ctx);
164
+ }
165
+ /**
166
+ * The one place a page of the Flow is made: the template at `width` x `maxHeight`, the content
167
+ * fragmented into its Regions in turn, and - when the content ends here - the template shrunk to
168
+ * what it used, or balanced.
169
+ */
170
+ fill(maxHeight, width, ctx, final) {
171
+ const template = this.template({ index: this.index, pageNumber: pageOf(ctx) ?? 1 });
172
+ // Laid out as this page of the Flow, its Regions claim themselves - wherever the template made
173
+ // them, a PageBuilder or a Table included - and are collected from the laid-out tree.
174
+ const pageCtx = { ...ctx, flowPage: this };
175
+ const layOut = (height) => template.calculateLayout(BoxConstraints.loose(width, height), { x: 0, y: 0 }, pageCtx);
176
+ const size = layOut(maxHeight);
177
+ const regions = regionsIn(template);
178
+ if (regions.length === 0)
179
+ throw new FlowWithoutRegionError();
180
+ // No room here: every Region is empty, or the rest of the template (its height with the Regions
181
+ // empty) is taller than the space.
182
+ const own = template.measure(BoxConstraints.loose(width, Infinity), ctx).height;
183
+ if (own > maxHeight + SLACK_EPSILON || !regions.some(room)) {
184
+ if (final)
185
+ throw new FlowRegionsEmptyError(width, maxHeight);
186
+ const piece = new FlowPieceElement(template, size, maxHeight, this);
187
+ return { piece, rest: this.content, placed: false, forceBreak: false };
188
+ }
189
+ const { rest, placed, forceBreak } = this.pour(regions, ctx);
190
+ const ends = !rest && placed;
191
+ const last = !ends
192
+ ? null
193
+ : this.balance && regions.filter(room).length > 1
194
+ ? this.balanced(regions, own, maxHeight, layOut, ctx)
195
+ : this.shrink(regions, own, maxHeight, layOut, ctx);
196
+ const piece = last
197
+ ? new FlowPieceElement(template, last.size, last.bound, this)
198
+ : new FlowPieceElement(template, size, maxHeight, this);
199
+ return { piece, rest, placed, forceBreak };
200
+ }
201
+ /**
202
+ * The content fragmented into the laid-out Regions in turn; each holds its piece. `fresh` is how tall
203
+ * each Region is on a full page - what a block that does not fit here may wait for.
204
+ */
205
+ pour(regions, ctx, fresh) {
206
+ let rest = this.content;
207
+ let placed = false;
208
+ let forceBreak = false;
209
+ for (const region of regions)
210
+ region.hold(null);
211
+ for (const region of regions) {
212
+ if (!rest)
213
+ break;
214
+ if (!room(region))
215
+ continue;
216
+ const { width: w, height: h } = region.getProps();
217
+ // Each Region is a fragmentation region of its own: a page break inside it is measured by it.
218
+ const split = rest.fragment(h, w, {
219
+ ...ctx,
220
+ pageBodyHeight: fresh?.get(region) ?? h,
221
+ inFlowRegion: true,
222
+ flowPage: undefined,
223
+ });
224
+ region.hold(split.fitted);
225
+ if (split.fitted)
226
+ placed = true;
227
+ rest = split.remainder;
228
+ // A column break is this Flow's: on to the next Region (or the next page's first).
229
+ if (split.forceBreak === "column")
230
+ continue;
231
+ if (split.forceBreak) {
232
+ forceBreak = true;
233
+ break;
234
+ }
235
+ }
236
+ return { rest, placed, forceBreak };
237
+ }
238
+ /**
239
+ * The last page, balanced: the smallest height at which the content, poured again, still ends here,
240
+ * so the Regions come out about equally full. Every candidate is a real pour - breaks,
241
+ * `keepTogether` and orphans hold as on any page - and the last one poured is the one drawn. A
242
+ * candidate is silent, and one that had to clip a block it could not break is too low.
243
+ */
244
+ balanced(regions, own, maxHeight, layOut, ctx) {
245
+ const fresh = new Map(regions.map((r) => [r, r.getProps().height]));
246
+ const endsAt = (bound) => {
247
+ layOut(bound);
248
+ let clipped = false;
249
+ const trial = new Diagnostics();
250
+ trial.setHandler((d) => (clipped || (clipped = d.code === "overflow-clipped")));
251
+ const poured = this.pour(regions, { ...ctx, onOverflow: "ignore", diagnostics: trial }, fresh);
252
+ return !poured.rest && poured.placed && !clipped;
253
+ };
254
+ let low = Math.min(maxHeight, own);
255
+ let high = maxHeight;
256
+ while (high - low > SHRINK_PRECISION) {
257
+ const mid = (low + high) / 2;
258
+ if (endsAt(mid))
259
+ high = mid;
260
+ else
261
+ low = mid;
262
+ }
263
+ const size = layOut(high);
264
+ this.pour(regions, ctx, fresh);
265
+ return { size, bound: high };
266
+ }
267
+ /**
268
+ * The last page: the smallest height at which every filled Region still holds what it was given.
269
+ * Only a layout knows how a template's Regions answer a height (`%`, stacked, `minHeight`, fixed), so
270
+ * each candidate is laid out and checked. The first is exact for Regions side by side (the full bound
271
+ * less the smallest slack); below it the bound is searched, down to the template's own height.
272
+ * Content that overflowed its Region at full height asks for no more than it had there.
273
+ */
274
+ shrink(regions, own, maxHeight, layOut, ctx) {
275
+ const filled = regions.filter((r) => r.held);
276
+ const needed = new Map(filled.map((r) => {
277
+ const { width, height } = r.getProps();
278
+ const used = r.held.measure(BoxConstraints.loose(width, Infinity), ctx).height;
279
+ return [r, Math.min(used, height)];
280
+ }));
281
+ const slack = Math.min(...filled.map((r) => r.getProps().height - needed.get(r)));
282
+ const fitsAt = (bound) => {
283
+ const size = layOut(bound);
284
+ const fits = filled.every((r) => r.getProps().height + SLACK_EPSILON >= needed.get(r));
285
+ return fits ? { size, bound } : null;
286
+ };
287
+ let low = Math.min(maxHeight, own);
288
+ let high = maxHeight;
289
+ const exact = fitsAt(Math.max(low, maxHeight - slack));
290
+ if (exact) {
291
+ if (exact.bound === low)
292
+ return exact;
293
+ high = exact.bound;
294
+ }
295
+ while (high - low > SHRINK_PRECISION) {
296
+ const mid = (low + high) / 2;
297
+ if (fitsAt(mid))
298
+ high = mid;
299
+ else
300
+ low = mid;
301
+ }
302
+ return { size: layOut(high), bound: high };
303
+ }
304
+ layoutChildren() {
305
+ return [this.content];
306
+ }
307
+ getProps() {
308
+ return {
309
+ x: this.x,
310
+ y: this.y,
311
+ width: this.width,
312
+ height: this.height,
313
+ child: this.content,
314
+ piece: this.piece,
315
+ };
316
+ }
317
+ }
@@ -34,13 +34,25 @@ export class KeepTogetherElement extends ProxyElement {
34
34
  diagnose(ctx.diagnostics, {
35
35
  code: "keep-together-split",
36
36
  severity: "warning",
37
- message: "keepTogether contains a forced page break (PageBreak / breakBefore / breakAfter). The break " +
38
- "wins and the group is split. Remove the break, or the keepTogether, to resolve this.",
37
+ message: "keepTogether contains a forced break (PageBreak, ColumnBreak, breakBefore / breakAfter). " +
38
+ "The break wins and the group is split. Remove the break, or the keepTogether, to resolve this.",
39
39
  about: this,
40
40
  page: pageOf(ctx),
41
41
  });
42
42
  return this.splitChild(maxHeight, width, ctx);
43
43
  }
44
+ // A region-sized child (a Flow) has no height of its own to compare, so the same three rules ask
45
+ // whether it ENDS here, or on a fresh page.
46
+ if (this.child.needsRegion()) {
47
+ const here = this.splitChild(maxHeight, width, ctx);
48
+ if (!here.remainder)
49
+ return here;
50
+ const pageBody = ctx.pageBodyHeight ?? maxHeight;
51
+ if (maxHeight < pageBody && !this.splitChild(pageBody, width, ctx).remainder) {
52
+ return { fitted: null, remainder: this };
53
+ }
54
+ return here;
55
+ }
44
56
  const natural = this.child.measure(BoxConstraints.loose(width, Infinity), ctx).height;
45
57
  // 1. Fits in the offered space: keep whole.
46
58
  if (natural <= maxHeight)
@@ -14,6 +14,8 @@ export declare class PaddingElement extends SizedPDFElement implements Fragmenta
14
14
  private margin;
15
15
  readonly ofBox: boolean;
16
16
  constructor({ margin, child, ofBox }: PaddingElementParams);
17
+ /** Some side is a percentage. */
18
+ get hasPercentSide(): boolean;
17
19
  /** The width a percentage side resolves against, and the context the child sees: a Box's base is
18
20
  * for this padding alone, so it does not travel further down. */
19
21
  private percentBase;
@@ -27,6 +29,7 @@ export declare class PaddingElement extends SizedPDFElement implements Fragmenta
27
29
  fragment(maxHeight: number, width: number, ctx: LayoutContext): FragmentResult;
28
30
  /** A wrapper: it needs a bound exactly when its child does, and passes the bound on. */
29
31
  needsBoundedMain(horizontal: boolean): boolean;
32
+ needsRegion(): boolean;
30
33
  hasForcedBreak(): boolean;
31
34
  breaksBefore(): boolean;
32
35
  breaksAfter(): boolean;
@@ -1,5 +1,5 @@
1
1
  import { Validator } from "../../validators/element-validator.js";
2
- import { SizedPDFElement } from "../pdf-element.js";
2
+ import { SizedPDFElement, withPaddingBase, } from "../pdf-element.js";
3
3
  import { BoxConstraints } from "../../layout/box-constraints.js";
4
4
  import { insetContext, isFragmentable, } from "../../layout/fragmentation.js";
5
5
  import { resolveEdges } from "../../layout/insets.js";
@@ -10,13 +10,17 @@ export class PaddingElement extends SizedPDFElement {
10
10
  this.margin = margin;
11
11
  this.ofBox = ofBox;
12
12
  }
13
+ /** Some side is a percentage. */
14
+ get hasPercentSide() {
15
+ return this.margin.some((side) => typeof side !== "number" && side.factor !== undefined);
16
+ }
13
17
  /** The width a percentage side resolves against, and the context the child sees: a Box's base is
14
18
  * for this padding alone, so it does not travel further down. */
15
19
  percentBase(width, ctx) {
16
20
  return this.ofBox && ctx.boxPaddingBase !== undefined ? ctx.boxPaddingBase : width;
17
21
  }
18
22
  childContext(ctx) {
19
- return ctx.boxPaddingBase === undefined ? ctx : { ...ctx, boxPaddingBase: undefined };
23
+ return withPaddingBase(ctx, undefined);
20
24
  }
21
25
  /**
22
26
  * Splits the padded box across pages (box-decoration-break: clone - every fragment
@@ -42,6 +46,9 @@ export class PaddingElement extends SizedPDFElement {
42
46
  needsBoundedMain(horizontal) {
43
47
  return this.child.needsBoundedMain(horizontal);
44
48
  }
49
+ needsRegion() {
50
+ return this.child.needsRegion();
51
+ }
45
52
  hasForcedBreak() {
46
53
  return this.child.hasForcedBreak();
47
54
  }
@@ -45,8 +45,8 @@ export class PageBuilderElement extends PDFElement {
45
45
  return this.build(this.infoFrom(ctx)).measure(constraints, ctx);
46
46
  }
47
47
  // Named `composed` (not `child`) like DeferredElement: the pre-layout image walk keys on `child`, and
48
- // this subtree does not exist yet when it runs. So an Image inside a PageBuilder skips aspect-ratio
49
- // pre-resolution - give it both width and height, exactly as inside a Deferred/Table subtree.
48
+ // this subtree does not exist yet when it runs. An Image built here reads its own size when layout
49
+ // asks; only one from a URL cannot, and reports `image-unsized`.
50
50
  layoutChildren() {
51
51
  return this.composed ? [this.composed] : [];
52
52
  }
@@ -11,6 +11,9 @@ export type PositionAnchor = "start" | "center" | "end";
11
11
  * An edge wins over an anchor on the same axis. With nothing set the child sits at the top-left.
12
12
  */
13
13
  export interface PositionedInsets {
14
+ /** Paint order within its stacking context: negative under the flow content, positive over it,
15
+ * higher over lower; 0 or unset paints in tree order where it stands (CSS `z-index`). */
16
+ zIndex?: number;
14
17
  top?: number;
15
18
  right?: number;
16
19
  bottom?: number;
@@ -39,7 +42,9 @@ export declare class PositionedElement extends PDFElement {
39
42
  private child;
40
43
  private insets;
41
44
  private placeCtx?;
42
- constructor({ child, top, right, bottom, left, h, v, x, y }: PositionedElementParams);
45
+ constructor({ child, top, right, bottom, left, h, v, x, y, zIndex }: PositionedElementParams);
46
+ /** Its paint order; 0 paints where it stands in the tree, anything else is sorted by its context. */
47
+ private readonly zIndex;
43
48
  calculateLayout(_constraints: BoxConstraints, _offset: Offset, ctx: LayoutContext): Size;
44
49
  /** Out of flow: it takes no room, and measuring registers nothing with the frame. */
45
50
  protected computeSize(): Size;
@@ -49,7 +54,9 @@ export declare class PositionedElement extends PDFElement {
49
54
  * offset is relative to the frame's top-left; its owner draws the child from there.
50
55
  */
51
56
  placeIn(frameSize: Size, frameCtx: LayoutContext): void;
52
- getProps(): WithChild;
57
+ getProps(): WithChild & {
58
+ zIndex: number;
59
+ };
53
60
  }
54
61
  /**
55
62
  * The `Positioned` elements a frame owns, in tree order: every one below `roots`, including those