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

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 (210) hide show
  1. package/dist/api/content.d.ts +5 -5
  2. package/dist/api/content.js +12 -6
  3. package/dist/api/font-url.d.ts +1 -2
  4. package/dist/api/font-url.js +8 -74
  5. package/dist/api/forms.d.ts +3 -0
  6. package/dist/api/forms.js +3 -0
  7. package/dist/api/index.d.ts +2 -0
  8. package/dist/api/index.js +1 -0
  9. package/dist/api/layout.d.ts +6 -0
  10. package/dist/api/layout.js +7 -2
  11. package/dist/api/page-builder.d.ts +4 -0
  12. package/dist/api/page-builder.js +4 -0
  13. package/dist/api/structure.d.ts +10 -2
  14. package/dist/api/structure.js +9 -6
  15. package/dist/api/table.d.ts +5 -3
  16. package/dist/api/table.js +99 -22
  17. package/dist/common/diagnostics.d.ts +53 -0
  18. package/dist/common/diagnostics.js +77 -0
  19. package/dist/edit/acroform-reader.js +7 -7
  20. package/dist/edit/appearance.js +3 -3
  21. package/dist/edit/attachments.d.ts +52 -0
  22. package/dist/edit/attachments.js +178 -0
  23. package/dist/edit/document.d.ts +38 -8
  24. package/dist/edit/document.js +208 -51
  25. package/dist/edit/fill.js +8 -23
  26. package/dist/edit/filters.d.ts +15 -0
  27. package/dist/edit/filters.js +157 -0
  28. package/dist/edit/index.d.ts +4 -1
  29. package/dist/edit/index.js +3 -1
  30. package/dist/edit/objects.d.ts +3 -4
  31. package/dist/edit/objects.js +5 -18
  32. package/dist/elements/canvas-element.d.ts +3 -0
  33. package/dist/elements/canvas-element.js +14 -3
  34. package/dist/elements/container-element.d.ts +5 -1
  35. package/dist/elements/container-element.js +35 -15
  36. package/dist/elements/forms/checkbox-element.d.ts +2 -0
  37. package/dist/elements/forms/checkbox-element.js +12 -3
  38. package/dist/elements/forms/choice-element.d.ts +6 -1
  39. package/dist/elements/forms/choice-element.js +17 -6
  40. package/dist/elements/forms/field-font.d.ts +8 -0
  41. package/dist/elements/forms/field-font.js +5 -0
  42. package/dist/elements/forms/push-button-element.d.ts +6 -1
  43. package/dist/elements/forms/push-button-element.js +16 -4
  44. package/dist/elements/forms/radio-element.d.ts +2 -0
  45. package/dist/elements/forms/radio-element.js +12 -3
  46. package/dist/elements/forms/signature-element.d.ts +6 -1
  47. package/dist/elements/forms/signature-element.js +16 -4
  48. package/dist/elements/forms/text-field-element.d.ts +6 -1
  49. package/dist/elements/forms/text-field-element.js +16 -4
  50. package/dist/elements/image-element.d.ts +39 -1
  51. package/dist/elements/image-element.js +95 -12
  52. package/dist/elements/layout/anchor-element.d.ts +8 -10
  53. package/dist/elements/layout/anchor-element.js +10 -18
  54. package/dist/elements/layout/bookmark-element.d.ts +9 -11
  55. package/dist/elements/layout/bookmark-element.js +11 -20
  56. package/dist/elements/layout/default-text-style-element.d.ts +11 -13
  57. package/dist/elements/layout/default-text-style-element.js +14 -31
  58. package/dist/elements/layout/deferred-element.d.ts +12 -4
  59. package/dist/elements/layout/deferred-element.js +24 -7
  60. package/dist/elements/layout/expanded-element.d.ts +7 -0
  61. package/dist/elements/layout/expanded-element.js +40 -18
  62. package/dist/elements/layout/keep-together-element.d.ts +8 -18
  63. package/dist/elements/layout/keep-together-element.js +21 -45
  64. package/dist/elements/layout/link-element.d.ts +8 -13
  65. package/dist/elements/layout/link-element.js +9 -19
  66. package/dist/elements/layout/padding-element.d.ts +15 -1
  67. package/dist/elements/layout/padding-element.js +55 -27
  68. package/dist/elements/layout/page-break-element.d.ts +4 -1
  69. package/dist/elements/layout/page-break-element.js +6 -1
  70. package/dist/elements/layout/page-builder-element.d.ts +5 -0
  71. package/dist/elements/layout/page-builder-element.js +11 -0
  72. package/dist/elements/layout/positioned-element.d.ts +14 -2
  73. package/dist/elements/layout/positioned-element.js +36 -10
  74. package/dist/elements/layout/proxy-element.d.ts +43 -0
  75. package/dist/elements/layout/proxy-element.js +94 -0
  76. package/dist/elements/layout/repeating-header-element.d.ts +6 -0
  77. package/dist/elements/layout/repeating-header-element.js +40 -13
  78. package/dist/elements/layout/rotated-box-element.d.ts +6 -0
  79. package/dist/elements/layout/rotated-box-element.js +27 -14
  80. package/dist/elements/layout/rotated-element.d.ts +7 -0
  81. package/dist/elements/layout/rotated-element.js +25 -2
  82. package/dist/elements/layout/struct-group.d.ts +9 -17
  83. package/dist/elements/layout/struct-group.js +10 -35
  84. package/dist/elements/line-element.d.ts +1 -0
  85. package/dist/elements/line-element.js +6 -0
  86. package/dist/elements/page-element.d.ts +10 -3
  87. package/dist/elements/page-element.js +39 -31
  88. package/dist/elements/pdf-document-element.d.ts +3 -0
  89. package/dist/elements/pdf-document-element.js +7 -0
  90. package/dist/elements/pdf-element.d.ts +75 -15
  91. package/dist/elements/pdf-element.js +68 -18
  92. package/dist/elements/rectangle-element.d.ts +14 -0
  93. package/dist/elements/rectangle-element.js +97 -52
  94. package/dist/elements/row-element.d.ts +11 -0
  95. package/dist/elements/row-element.js +58 -8
  96. package/dist/elements/svg-element.d.ts +30 -6
  97. package/dist/elements/svg-element.js +88 -17
  98. package/dist/elements/text-element.d.ts +40 -13
  99. package/dist/elements/text-element.js +144 -99
  100. package/dist/forms/acroform.d.ts +9 -1
  101. package/dist/forms/acroform.js +83 -34
  102. package/dist/forms/appearance.d.ts +17 -6
  103. package/dist/forms/appearance.js +18 -16
  104. package/dist/ir/display-list.d.ts +12 -2
  105. package/dist/layout/box-constraints.d.ts +5 -0
  106. package/dist/layout/box-constraints.js +14 -0
  107. package/dist/layout/collect-images.d.ts +6 -4
  108. package/dist/layout/collect-images.js +6 -4
  109. package/dist/layout/flex-item.d.ts +9 -0
  110. package/dist/layout/flex-item.js +39 -0
  111. package/dist/layout/fragmentation.d.ts +12 -4
  112. package/dist/layout/fragmentation.js +37 -14
  113. package/dist/layout/layout-cache.d.ts +53 -0
  114. package/dist/layout/layout-cache.js +107 -0
  115. package/dist/renderer/anchor-renderer.d.ts +2 -1
  116. package/dist/renderer/anchor-renderer.js +5 -4
  117. package/dist/renderer/bookmark-renderer.d.ts +2 -1
  118. package/dist/renderer/bookmark-renderer.js +5 -4
  119. package/dist/renderer/canvas-renderer.d.ts +2 -1
  120. package/dist/renderer/canvas-renderer.js +5 -2
  121. package/dist/renderer/container-renderer.d.ts +2 -1
  122. package/dist/renderer/container-renderer.js +6 -5
  123. package/dist/renderer/default-text-style-renderer.d.ts +2 -1
  124. package/dist/renderer/default-text-style-renderer.js +2 -3
  125. package/dist/renderer/deferred-renderer.d.ts +2 -1
  126. package/dist/renderer/deferred-renderer.js +2 -3
  127. package/dist/renderer/expanded-renderer.d.ts +2 -1
  128. package/dist/renderer/expanded-renderer.js +2 -3
  129. package/dist/renderer/form-field-renderer.d.ts +2 -1
  130. package/dist/renderer/form-field-renderer.js +16 -3
  131. package/dist/renderer/image-renderer.d.ts +2 -1
  132. package/dist/renderer/image-renderer.js +5 -2
  133. package/dist/renderer/keep-together-renderer.d.ts +2 -1
  134. package/dist/renderer/keep-together-renderer.js +2 -3
  135. package/dist/renderer/line-renderer.d.ts +2 -1
  136. package/dist/renderer/line-renderer.js +7 -2
  137. package/dist/renderer/link-renderer.d.ts +2 -1
  138. package/dist/renderer/link-renderer.js +36 -5
  139. package/dist/renderer/padding-renderer.d.ts +2 -1
  140. package/dist/renderer/padding-renderer.js +5 -5
  141. package/dist/renderer/page-break-renderer.d.ts +6 -5
  142. package/dist/renderer/page-break-renderer.js +14 -7
  143. package/dist/renderer/page-builder-renderer.d.ts +2 -1
  144. package/dist/renderer/page-builder-renderer.js +2 -3
  145. package/dist/renderer/page-renderer.js +28 -7
  146. package/dist/renderer/pdf-backend.d.ts +4 -3
  147. package/dist/renderer/pdf-backend.js +5 -5
  148. package/dist/renderer/pdf-document-renderer.d.ts +1 -8
  149. package/dist/renderer/pdf-document-renderer.js +15 -16
  150. package/dist/renderer/pdf-renderer.js +11 -11
  151. package/dist/renderer/positioned-renderer.d.ts +6 -1
  152. package/dist/renderer/positioned-renderer.js +6 -5
  153. package/dist/renderer/rectangle-renderer.d.ts +2 -1
  154. package/dist/renderer/rectangle-renderer.js +9 -4
  155. package/dist/renderer/repeating-header-renderer.d.ts +2 -1
  156. package/dist/renderer/repeating-header-renderer.js +6 -5
  157. package/dist/renderer/rotated-renderer.d.ts +2 -1
  158. package/dist/renderer/rotated-renderer.js +8 -5
  159. package/dist/renderer/row-renderer.d.ts +2 -1
  160. package/dist/renderer/row-renderer.js +6 -5
  161. package/dist/renderer/struct-group-renderer.d.ts +2 -1
  162. package/dist/renderer/struct-group-renderer.js +2 -3
  163. package/dist/renderer/svg-renderer.d.ts +2 -1
  164. package/dist/renderer/svg-renderer.js +8 -2
  165. package/dist/renderer/text-renderer.d.ts +4 -6
  166. package/dist/renderer/text-renderer.js +101 -111
  167. package/dist/svg/gradients.js +5 -0
  168. package/dist/svg/index.js +10 -1
  169. package/dist/svg/style.d.ts +2 -0
  170. package/dist/svg/style.js +11 -0
  171. package/dist/text/advance.d.ts +4 -0
  172. package/dist/text/advance.js +49 -4
  173. package/dist/text/bidi.d.ts +0 -2
  174. package/dist/text/bidi.js +0 -10
  175. package/dist/text/glyph-coverage.d.ts +1 -1
  176. package/dist/text/glyph-coverage.js +7 -3
  177. package/dist/text/hyphen-breaks.d.ts +17 -0
  178. package/dist/text/hyphen-breaks.js +54 -0
  179. package/dist/text/line-breaker.d.ts +34 -31
  180. package/dist/text/line-breaker.js +364 -176
  181. package/dist/text/line-metrics.d.ts +6 -3
  182. package/dist/text/line-metrics.js +7 -2
  183. package/dist/text/pdf-text-string.d.ts +21 -0
  184. package/dist/text/pdf-text-string.js +114 -0
  185. package/dist/text/text-decoration.d.ts +5 -5
  186. package/dist/text/text-decoration.js +5 -5
  187. package/dist/text/word-splitting.d.ts +15 -2
  188. package/dist/text/word-splitting.js +34 -8
  189. package/dist/utils/afm-parser.d.ts +3 -0
  190. package/dist/utils/afm-parser.js +19 -1
  191. package/dist/utils/dest-registry.d.ts +1 -1
  192. package/dist/utils/dest-registry.js +3 -2
  193. package/dist/utils/fetch-bytes.d.ts +19 -0
  194. package/dist/utils/fetch-bytes.js +79 -0
  195. package/dist/utils/flex-layout.d.ts +13 -13
  196. package/dist/utils/flex-layout.js +318 -216
  197. package/dist/utils/font-metrics.d.ts +4 -3
  198. package/dist/utils/image-helper.js +2 -1
  199. package/dist/utils/pdf-object-manager.d.ts +11 -3
  200. package/dist/utils/pdf-object-manager.js +41 -15
  201. package/dist/utils/renderer-registry.d.ts +43 -2
  202. package/dist/utils/renderer-registry.js +40 -0
  203. package/dist/utils/struct-tree.d.ts +24 -0
  204. package/dist/utils/struct-tree.js +61 -9
  205. package/dist/utils/ttf-parser.d.ts +4 -0
  206. package/dist/utils/ttf-parser.js +27 -4
  207. package/dist/validators/element-validator.js +2 -1
  208. package/package.json +1 -1
  209. package/dist/elements/layout/sized-container-element.d.ts +0 -14
  210. package/dist/elements/layout/sized-container-element.js +0 -32
@@ -17,6 +17,13 @@ export class PDFDocumentElement extends PDFElement {
17
17
  this.children.forEach((child) => child.calculateLayout(new BoxConstraints(), origin, ctx));
18
18
  return { width: 0, height: 0 };
19
19
  }
20
+ /** The root has no size of its own. */
21
+ computeSize() {
22
+ return { width: 0, height: 0 };
23
+ }
24
+ layoutChildren() {
25
+ return this.children;
26
+ }
20
27
  getProps() {
21
28
  return { children: this.children, defaultTextStyle: this.defaultTextStyle };
22
29
  }
@@ -2,6 +2,7 @@ import type { FontMetrics } from "../utils/font-metrics.js";
2
2
  import type { PDFPageConfig } from "./page-element.js";
3
3
  import type { ResolvedTextStyle } from "../text/text-style.js";
4
4
  import type { OverflowPolicy } from "../layout/fragmentation.js";
5
+ import type { Diagnostics } from "../common/diagnostics.js";
5
6
  import type { BoxConstraints, Offset, Size } from "../layout/box-constraints.js";
6
7
  import type { CrossAlign } from "../layout/alignment.js";
7
8
  /**
@@ -11,18 +12,14 @@ import type { CrossAlign } from "../layout/alignment.js";
11
12
  * height. The PDF byte writer is deliberately absent - layout must not touch it.
12
13
  */
13
14
  /**
14
- * A positioning frame - the CSS "containing block" that `Positioned` children resolve their
15
- * offsets against. A `relative` Box (later the page) creates one and threads it to its subtree;
16
- * `Positioned` descendants register a placement closure in `place`, which the frame drains once it
17
- * has finished sizing itself (so `right`/`bottom` can resolve against the final box).
15
+ * A positioning frame - the CSS "containing block" that `Positioned` children resolve their offsets
16
+ * against: the page's content box, or a `relative` Box. Threaded to the subtree so a `Positioned`
17
+ * can tell it has one. The frame's owner finds its `Positioned` descendants by walking the tree
18
+ * after sizing itself, and lays them out against its own box - layout itself registers nothing.
18
19
  */
19
20
  export interface PositioningFrame {
20
21
  origin: Offset;
21
22
  size: Size;
22
- place: Array<(frame: {
23
- origin: Offset;
24
- size: Size;
25
- }, ctx: LayoutContext) => void>;
26
23
  }
27
24
  /**
28
25
  * What a `PageBuilder` learns about the page it landed on. `pageNumber` is 1-based and counts PHYSICAL
@@ -62,24 +59,86 @@ export interface LayoutContext {
62
59
  /** What to do when an element overflows a page region and cannot break (set from the render option;
63
60
  * absent = clip silently). Evaluated in `packChildren` where the forced placement happens. */
64
61
  onOverflow?: OverflowPolicy;
65
- /** The FULL body height of the current page (between header and footer), set by the page driver. A
66
- * `keepTogether` group reads it to decide the degrade rule: veto splitting only when the group would
67
- * fit on a fresh page; a group taller than a whole page drops the veto and splits. */
62
+ /** The render's diagnostics channel. Never part of a size, so never part of the layout-cache key;
63
+ * absent when an element is laid out on its own, which then warns as before. */
64
+ diagnostics?: Diagnostics;
65
+ /** The physical page (1-based) pagination is filling - only so a diagnostic can name it; the render
66
+ * pass knows it as `pageInfo`. Never part of a size. */
67
+ paginatingPage?: number;
68
+ /** The height a FRESH page gives this region: the page body (between header and footer, set by the
69
+ * page driver) minus what encloses the region on every page - a border, padding, a repeated table
70
+ * header (`insetContext`). A `keepTogether` group reads it to decide the degrade rule: veto splitting
71
+ * only when the group would fit on a fresh page; a group taller than that drops the veto and splits. */
68
72
  pageBodyHeight?: number;
73
+ /** Something already sits above this element in the current page region. Then "does not fit"
74
+ * means "move to the next page", never an overflow - only content at the top of a fresh page is
75
+ * forced on and reported. Set by `packChildren`; absent = top of the page. */
76
+ belowContent?: boolean;
77
+ /** What a `Box`'s percentage padding resolves against: the width the Box itself was offered (CSS:
78
+ * the containing block), not its inner width. Set by the Box for its own padding only. */
79
+ boxPaddingBase?: number;
69
80
  }
70
81
  export declare abstract class PDFElement {
71
82
  private _structId;
72
83
  get structId(): number;
73
84
  /** Make this element share `from`'s tagging identity - used by fragmentation clones. Returns this. */
74
85
  adoptStructId(from: PDFElement): this;
86
+ /**
87
+ * Marks this element as a piece of `original`, split across pages: it is the same element to a
88
+ * reader of the tagged PDF (one structure element) and to the Row or Column it sits in (its flex
89
+ * properties). Every fragmentation clone ends in this call, so a property added to the base is
90
+ * carried in ONE place. Returns this.
91
+ */
92
+ asPieceOf(original: PDFElement): this;
93
+ /** Takes the flex properties `original` set (CSS `align-self`, `order`, `flex-shrink`). Returns this. */
94
+ takeFlexItemFrom(original: PDFElement): this;
75
95
  abstract getProps(): unknown;
76
96
  /**
77
97
  * Lays the element out: `constraints` bound its size (flows DOWN), `offset` is the
78
- * absolute top-left position the parent assigns it, and the returned `Size` is the
98
+ * top-left position the parent assigns it, relative to the parent's own top-left (absolute
99
+ * coordinates exist only in the render pass), and the returned `Size` is the
79
100
  * space it actually took (flows UP). Layout works in a top-left origin; the Y-flip
80
101
  * happens once at the IR -> backend seam.
81
102
  */
82
103
  abstract calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
104
+ /**
105
+ * The size `calculateLayout` would return under these constraints - with NO side effects: no
106
+ * position, no stored size, nothing registered with a frame. A parent asks this to decide, and lays
107
+ * a child out once afterwards; asking with a full layout doubled the work at every nesting level.
108
+ * Do not override it - implement `computeSize` (as Flutter's `getDryLayout` / `computeDryLayout`).
109
+ * The same question twice is answered from the layout cache (`layout/layout-cache.ts`).
110
+ */
111
+ measure(constraints: BoxConstraints, ctx: LayoutContext): Size;
112
+ /**
113
+ * The part of the constraints this element's size actually reads - what the layout cache keys on.
114
+ * All of it by default. An element whose size ignores a bound returns it opened up, so two questions
115
+ * that differ only there share one answer. It must be TRUE for `computeSize`: reading a bound that
116
+ * is dropped here makes the cache hand back a wrong size.
117
+ */
118
+ protected sizeInputs(constraints: BoxConstraints): BoxConstraints;
119
+ private sizeCache?;
120
+ /**
121
+ * Whether this element's size can depend on the page it lands on (`pageConfig`, `pageInfo`): a
122
+ * `PageBuilder`, a `Page`, and everything holding one. Only those key a cached size on the page, so
123
+ * every other size is reused across pages and across the pagination and render passes. Remembered:
124
+ * the tree does not change, and an answer that was `false` can never become `true`.
125
+ */
126
+ dependsOnPage(): boolean;
127
+ private pageDependent?;
128
+ /**
129
+ * How this element sizes itself for `measure`. It must come from the SAME sizing computation as
130
+ * `calculateLayout`, or a measured size and a laid-out one can differ.
131
+ */
132
+ protected abstract computeSize(constraints: BoxConstraints, ctx: LayoutContext): Size;
133
+ /** The elements this one lays out, for walks that must see the whole tree. Leaves have none. */
134
+ layoutChildren(): readonly PDFElement[];
135
+ /**
136
+ * The element this one is transparent for, if any: a `ProxyElement` returns its child. A surrounding
137
+ * Row or Column looks through it for the flex item (`layout/flex-item.ts`).
138
+ */
139
+ proxied(): PDFElement | undefined;
140
+ /** Lays out its own `Positioned` descendants (a page, a `relative` Box); a walk for them stops here. */
141
+ isPositioningFrame(): boolean;
83
142
  /**
84
143
  * The relative-size fraction (0..1) this element asked for on the given axis, if any (`horizontal` =
85
144
  * width). A flex parent reads it to resolve a percentage child against its OWN main size - e.g. a
@@ -141,9 +200,9 @@ export declare abstract class PDFElement {
141
200
  /**
142
201
  * Where this child sits among its siblings (CSS `order`), lowest first, source order deciding ties.
143
202
  * It moves the child in the LAYOUT only - the element tree, and therefore the reading order a tagged
144
- * PDF exposes, is untouched. Default 0.
203
+ * PDF exposes, is untouched. Unset means 0; a stack reads it through `layout/flex-item.ts`.
145
204
  */
146
- order: number;
205
+ order?: number;
147
206
  /**
148
207
  * How willingly this child gives up main-axis space when the line overflows (CSS `flex-shrink`),
149
208
  * weighted by its own size as CSS does. Default 1, as in CSS: an overflowing line squeezes its
@@ -154,8 +213,9 @@ export declare abstract class PDFElement {
154
213
  * caution outlived its use: four of the gallery cases turned out to be overflowing their page
155
214
  * margin without anyone noticing, because nothing ever pulled them back. Shrinking is bounded from
156
215
  * below by `minIntrinsicMain`, so a child is never squeezed past what it can actually hold.
216
+ * Unset means 1; a stack reads it through `layout/flex-item.ts`.
157
217
  */
158
- flexShrink: number;
218
+ flexShrink?: number;
159
219
  /** Sets `flexShrink` and returns the element. */
160
220
  withFlexShrink(shrink: number | undefined): this;
161
221
  /** Sets `order` and returns the element, so a factory can thread it through in one expression. */
@@ -1,3 +1,4 @@
1
+ import { SizeCache } from "../layout/layout-cache.js";
1
2
  let _nextStructId = 0;
2
3
  export class PDFElement {
3
4
  constructor() {
@@ -5,24 +6,6 @@ export class PDFElement {
5
6
  // fragmentation copies (adoptStructId), so a paragraph or table split across pages stays ONE structure
6
7
  // element. Only the elements that actually tag (Text, Image, StructGroup) read it.
7
8
  this._structId = _nextStructId++;
8
- /**
9
- * Where this child sits among its siblings (CSS `order`), lowest first, source order deciding ties.
10
- * It moves the child in the LAYOUT only - the element tree, and therefore the reading order a tagged
11
- * PDF exposes, is untouched. Default 0.
12
- */
13
- this.order = 0;
14
- /**
15
- * How willingly this child gives up main-axis space when the line overflows (CSS `flex-shrink`),
16
- * weighted by its own size as CSS does. Default 1, as in CSS: an overflowing line squeezes its
17
- * children back inside instead of letting them run past the edge of the box. `0` opts out and keeps
18
- * a child at its natural size.
19
- *
20
- * This was 0 until the alpha, so that adding flexShrink could not move an existing layout. That
21
- * caution outlived its use: four of the gallery cases turned out to be overflowing their page
22
- * margin without anyone noticing, because nothing ever pulled them back. Shrinking is bounded from
23
- * below by `minIntrinsicMain`, so a child is never squeezed past what it can actually hold.
24
- */
25
- this.flexShrink = 1;
26
9
  }
27
10
  get structId() {
28
11
  return this._structId;
@@ -32,6 +15,73 @@ export class PDFElement {
32
15
  this._structId = from._structId;
33
16
  return this;
34
17
  }
18
+ /**
19
+ * Marks this element as a piece of `original`, split across pages: it is the same element to a
20
+ * reader of the tagged PDF (one structure element) and to the Row or Column it sits in (its flex
21
+ * properties). Every fragmentation clone ends in this call, so a property added to the base is
22
+ * carried in ONE place. Returns this.
23
+ */
24
+ asPieceOf(original) {
25
+ return this.adoptStructId(original).takeFlexItemFrom(original);
26
+ }
27
+ /** Takes the flex properties `original` set (CSS `align-self`, `order`, `flex-shrink`). Returns this. */
28
+ takeFlexItemFrom(original) {
29
+ this.alignSelf = original.alignSelf ?? this.alignSelf;
30
+ this.order = original.order ?? this.order;
31
+ this.flexShrink = original.flexShrink ?? this.flexShrink;
32
+ return this;
33
+ }
34
+ /**
35
+ * The size `calculateLayout` would return under these constraints - with NO side effects: no
36
+ * position, no stored size, nothing registered with a frame. A parent asks this to decide, and lays
37
+ * a child out once afterwards; asking with a full layout doubled the work at every nesting level.
38
+ * Do not override it - implement `computeSize` (as Flutter's `getDryLayout` / `computeDryLayout`).
39
+ * The same question twice is answered from the layout cache (`layout/layout-cache.ts`).
40
+ */
41
+ measure(constraints, ctx) {
42
+ const cache = (this.sizeCache ?? (this.sizeCache = new SizeCache()));
43
+ const asked = this.sizeInputs(constraints);
44
+ const onPage = this.dependsOnPage();
45
+ const known = cache.get(asked, ctx, onPage);
46
+ if (known)
47
+ return known;
48
+ const size = this.computeSize(constraints, ctx);
49
+ cache.set(asked, ctx, onPage, size);
50
+ return size;
51
+ }
52
+ /**
53
+ * The part of the constraints this element's size actually reads - what the layout cache keys on.
54
+ * All of it by default. An element whose size ignores a bound returns it opened up, so two questions
55
+ * that differ only there share one answer. It must be TRUE for `computeSize`: reading a bound that
56
+ * is dropped here makes the cache hand back a wrong size.
57
+ */
58
+ sizeInputs(constraints) {
59
+ return constraints;
60
+ }
61
+ /**
62
+ * Whether this element's size can depend on the page it lands on (`pageConfig`, `pageInfo`): a
63
+ * `PageBuilder`, a `Page`, and everything holding one. Only those key a cached size on the page, so
64
+ * every other size is reused across pages and across the pagination and render passes. Remembered:
65
+ * the tree does not change, and an answer that was `false` can never become `true`.
66
+ */
67
+ dependsOnPage() {
68
+ return (this.pageDependent ?? (this.pageDependent = this.layoutChildren().some((child) => child.dependsOnPage())));
69
+ }
70
+ /** The elements this one lays out, for walks that must see the whole tree. Leaves have none. */
71
+ layoutChildren() {
72
+ return [];
73
+ }
74
+ /**
75
+ * The element this one is transparent for, if any: a `ProxyElement` returns its child. A surrounding
76
+ * Row or Column looks through it for the flex item (`layout/flex-item.ts`).
77
+ */
78
+ proxied() {
79
+ return undefined;
80
+ }
81
+ /** Lays out its own `Positioned` descendants (a page, a `relative` Box); a walk for them stops here. */
82
+ isPositioningFrame() {
83
+ return false;
84
+ }
35
85
  /**
36
86
  * The relative-size fraction (0..1) this element asked for on the given axis, if any (`horizontal` =
37
87
  * width). A flex parent reads it to resolve a percentage child against its OWN main size - e.g. a
@@ -78,9 +78,22 @@ export declare class RectangleElement extends SizedPDFElement implements Fragmen
78
78
  * `Spacer` in a box can only resolve if the box itself was given a bounded extent to pass down. With an
79
79
  * explicit size on an axis the box already knows its extent there and asks for nothing. */
80
80
  needsBoundedMain(horizontal: boolean): boolean;
81
+ /** A Box's own padding resolves a percentage against what the Box was OFFERED (CSS: the containing
82
+ * block), not against the inner width it hands its children - so it is told. Only a bounded offer:
83
+ * in a Row a child is offered no width at all, and there the padding keeps resolving against the
84
+ * Box's own width, as it always did. */
85
+ private paddingContext;
81
86
  /** A Box stacks its children vertically inside its border, which is on both sides of the axis. */
82
87
  minIntrinsicMain(horizontal: boolean, ctx: LayoutContext): number;
83
88
  calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
89
+ protected computeSize(constraints: BoxConstraints, ctx: LayoutContext): Size;
90
+ /**
91
+ * The one sizing path. With an `offset` it lays the children out and keeps its geometry; with
92
+ * `null` it only measures - no field written, no child placed, no frame drained.
93
+ */
94
+ private sizeWith;
95
+ isPositioningFrame(): boolean;
96
+ layoutChildren(): readonly PDFElement[];
84
97
  getProps(): {
85
98
  x: number;
86
99
  y: number;
@@ -93,6 +106,7 @@ export declare class RectangleElement extends SizedPDFElement implements Fragmen
93
106
  radius: number | Radii;
94
107
  sideBorders: SideBorders | undefined;
95
108
  overflow: "hidden" | "visible";
109
+ relative: boolean;
96
110
  };
97
111
  }
98
112
  export {};
@@ -1,7 +1,10 @@
1
+ import { widthRules } from "../layout/box-constraints.js";
1
2
  import { Color } from "../common/color.js";
2
3
  import { BoxConstraints, extentSpecs, resolveSize, } from "../layout/box-constraints.js";
3
4
  import { stackMinIntrinsic, withDeclaredExtent } from "../layout/min-intrinsic.js";
4
- import { childrenForceBreak, packChildren, } from "../layout/fragmentation.js";
5
+ import { positionedIn } from "./layout/positioned-element.js";
6
+ import { PaddingElement } from "./layout/padding-element.js";
7
+ import { childrenForceBreak, insetContext, packChildren, } from "../layout/fragmentation.js";
5
8
  import { SizedPDFElement, } from "./pdf-element.js";
6
9
  export class RectangleElement extends SizedPDFElement {
7
10
  constructor({ children = [], color = new Color(0, 0, 0), backgroundColor, borderWidth, width, height, radius, sideBorders, relative, overflow, breakBefore, breakAfter, ...sizing }) {
@@ -20,8 +23,10 @@ export class RectangleElement extends SizedPDFElement {
20
23
  this.breakAfter = breakAfter ?? false;
21
24
  this.sizeMemory = { ...sizing, x: 0, y: 0, width, height };
22
25
  }
26
+ // A break before the FIRST child is a break before this stack (CSS propagates it): the whole stack
27
+ // moves to the next page, instead of leaving an empty piece - or an empty border - behind.
23
28
  breaksBefore() {
24
- return this.breakBefore;
29
+ return this.breakBefore || (this.children[0]?.breaksBefore() ?? false);
25
30
  }
26
31
  breaksAfter() {
27
32
  return this.breakAfter;
@@ -35,23 +40,29 @@ export class RectangleElement extends SizedPDFElement {
35
40
  * box is sized by what it actually holds on each page.
36
41
  */
37
42
  fragment(maxHeight, width, ctx) {
38
- // A percentage width resolves against the region width the page driver hands us, so a
39
- // paginating %-box keeps its fraction on every page.
40
- const boxWidth = this.sizeMemory.width ??
41
- (this.sizeMemory.widthFactor !== undefined ? width * this.sizeMemory.widthFactor : width);
43
+ // The box's own width rules (fixed, %, min/max) resolved against the region - the same sizing
44
+ // path as `measure`, so the content is packed at the width it will be drawn at.
45
+ const boxWidth = this.measure(BoxConstraints.loose(width, Infinity), ctx).width;
42
46
  const innerWidth = Math.max(0, boxWidth - 2 * this.borderWidth);
43
47
  // Border-box: the content is inset by the border on top and bottom, so a fragment
44
48
  // holding `c` of content is `c + 2*border` tall. (Derived, not a fudge factor.)
45
49
  const innerMaxHeight = maxHeight - 2 * this.borderWidth;
46
- const { fitted, remainder, forceBreak } = packChildren(this.children, innerMaxHeight, innerWidth, ctx);
50
+ const { fitted, remainder, forceBreak } = packChildren(this.children, innerMaxHeight, innerWidth, insetContext(this.paddingContext(ctx, width), 2 * this.borderWidth));
47
51
  // Fits with no forced cut: return the whole box unchanged. A trailing forced break is the
48
52
  // exception - fall through so the box is rebuilt from the packed children (without the consumed
49
53
  // break marker), rather than handing back the original that still holds it.
50
- if (remainder.length === 0 && !forceBreak)
51
- return { fitted: this, remainder: null };
52
- const contentHeight = (kids) => kids.reduce((sum, child) => sum +
53
- child.calculateLayout(BoxConstraints.loose(innerWidth, Infinity), { x: 0, y: 0 }, ctx)
54
- .height, 0);
54
+ if (remainder.length === 0 && !forceBreak) {
55
+ // The content fits, but a fixed height may not - such a box cannot shrink, so it moves on whole.
56
+ const own = this.measure(BoxConstraints.loose(width, Infinity), ctx).height;
57
+ return own <= maxHeight
58
+ ? { fitted: this, remainder: null }
59
+ : { fitted: null, remainder: this };
60
+ }
61
+ // Nothing fit below the content above: move on whole, not as an empty bordered box.
62
+ if (fitted.length === 0 && !forceBreak)
63
+ return { fitted: null, remainder: this };
64
+ const childCtx = this.paddingContext(ctx, width);
65
+ const contentHeight = (kids) => kids.reduce((sum, child) => sum + child.measure(BoxConstraints.loose(innerWidth, Infinity), childCtx).height, 0);
55
66
  return {
56
67
  fitted: this.cloneWithChildren(fitted, contentHeight(fitted) + 2 * this.borderWidth),
57
68
  // A trailing forced break leaves nothing to carry over: no remainder box, no extra page.
@@ -70,10 +81,9 @@ export class RectangleElement extends SizedPDFElement {
70
81
  return new RectangleElement({
71
82
  x: 0,
72
83
  y: 0,
73
- width: this.sizeMemory.width,
74
- widthFactor: this.sizeMemory.widthFactor,
75
- // A split box is sized by its per-page content (height passed in), so the height factor
76
- // is intentionally dropped here - box-decoration-break, same as an explicit height.
84
+ ...widthRules(this.sizeMemory),
85
+ // A split box is sized by its per-page content (height passed in), so the height rules are
86
+ // intentionally dropped here - box-decoration-break, same as an explicit height.
77
87
  height,
78
88
  children,
79
89
  color: this.color,
@@ -84,7 +94,7 @@ export class RectangleElement extends SizedPDFElement {
84
94
  relative: this.relative,
85
95
  overflow: this.overflow,
86
96
  breakAfter,
87
- });
97
+ }).asPieceOf(this);
88
98
  }
89
99
  relativeSizeFactor(horizontal) {
90
100
  return horizontal ? this.sizeMemory.widthFactor : this.sizeMemory.heightFactor;
@@ -100,12 +110,32 @@ export class RectangleElement extends SizedPDFElement {
100
110
  return false;
101
111
  return this.children.some((c) => c.needsBoundedMain(horizontal));
102
112
  }
113
+ /** A Box's own padding resolves a percentage against what the Box was OFFERED (CSS: the containing
114
+ * block), not against the inner width it hands its children - so it is told. Only a bounded offer:
115
+ * in a Row a child is offered no width at all, and there the padding keeps resolving against the
116
+ * Box's own width, as it always did. */
117
+ paddingContext(ctx, offered) {
118
+ const own = this.children[0];
119
+ return own instanceof PaddingElement && own.ofBox && Number.isFinite(offered)
120
+ ? { ...ctx, boxPaddingBase: offered }
121
+ : ctx;
122
+ }
103
123
  /** A Box stacks its children vertically inside its border, which is on both sides of the axis. */
104
124
  minIntrinsicMain(horizontal, ctx) {
105
125
  const content = stackMinIntrinsic(this.children, 0, !horizontal, horizontal, ctx) + 2 * this.borderWidth;
106
126
  return withDeclaredExtent(content, horizontal ? this.sizeMemory.width : this.sizeMemory.height, horizontal ? this.sizeMemory.minWidth : this.sizeMemory.minHeight);
107
127
  }
108
128
  calculateLayout(constraints, offset, ctx) {
129
+ return this.sizeWith(constraints, ctx, offset);
130
+ }
131
+ computeSize(constraints, ctx) {
132
+ return this.sizeWith(constraints, ctx, null);
133
+ }
134
+ /**
135
+ * The one sizing path. With an `offset` it lays the children out and keeps its geometry; with
136
+ * `null` it only measures - no field written, no child placed, no frame drained.
137
+ */
138
+ sizeWith(constraints, ctx, offset) {
109
139
  // An explicit extent is a fixed point size or a fraction of the offered box (relative sizing);
110
140
  // the fraction only resolves in a bounded region. `undefined` = fill / shrink-wrap below.
111
141
  const specs = extentSpecs(this.sizeMemory);
@@ -118,12 +148,11 @@ export class RectangleElement extends SizedPDFElement {
118
148
  // fixed box would balloon to the parent's size.)
119
149
  // Whether there is a region to fill is decided by the constraints we were GIVEN: a `maxWidth` caps
120
150
  // a box, it does not turn a shrink-wrapping one into a filling one (CSS `max-width` does not either).
121
- this.width =
122
- explicitWidth !== undefined
123
- ? bounds.constrainWidth(explicitWidth)
124
- : constraints.hasBoundedWidth
125
- ? bounds.maxWidth
126
- : this.width;
151
+ let width = explicitWidth !== undefined
152
+ ? bounds.constrainWidth(explicitWidth)
153
+ : constraints.hasBoundedWidth
154
+ ? bounds.maxWidth
155
+ : undefined;
127
156
  // Width shrink-wrap: no explicit width AND an unbounded region (e.g. a `Box` badge inside a
128
157
  // `Positioned`). Resolved after the children are measured, just below.
129
158
  const shrinkWrapWidth = explicitWidth === undefined && !constraints.hasBoundedWidth;
@@ -131,43 +160,46 @@ export class RectangleElement extends SizedPDFElement {
131
160
  // SHRINK-WRAP the content in an unbounded one (a note box in a stack). Shrink-wrap is
132
161
  // resolved after the children are measured, just below.
133
162
  const shrinkWrapHeight = explicitHeight === undefined && !constraints.hasBoundedHeight;
134
- this.height =
135
- explicitHeight !== undefined
136
- ? bounds.constrainHeight(explicitHeight)
137
- : constraints.hasBoundedHeight
138
- ? bounds.maxHeight
139
- : this.height;
140
- this.x = this.sizeMemory.x + offset.x;
141
- this.y = this.sizeMemory.y + offset.y;
142
- // A `relative` box is a positioning frame: thread a fresh frame to the subtree so any
143
- // `Positioned` descendant registers against it (out of flow), then drain it below once the
144
- // box is sized. A plain box leaves `ctx` untouched -> byte-identical.
145
- const frame = this.relative
146
- ? { origin: { x: 0, y: 0 }, size: { width: 0, height: 0 }, place: [] }
163
+ let height = explicitHeight !== undefined
164
+ ? bounds.constrainHeight(explicitHeight)
165
+ : constraints.hasBoundedHeight
166
+ ? bounds.maxHeight
167
+ : undefined;
168
+ const x = this.sizeMemory.x + (offset?.x ?? 0);
169
+ const y = this.sizeMemory.y + (offset?.y ?? 0);
170
+ // A `relative` box is a positioning frame: its subtree is told it has one, and once the box is
171
+ // sized it lays out the `Positioned` descendants against itself (below). Measuring places
172
+ // nothing, so it needs no frame. A plain box leaves `ctx` untouched.
173
+ const frame = this.relative && offset
174
+ ? { origin: { x: 0, y: 0 }, size: { width: 0, height: 0 } }
147
175
  : undefined;
148
- const childCtx = frame ? { ...ctx, frame } : ctx;
176
+ const childCtx = this.paddingContext(frame ? { ...ctx, frame } : ctx, constraints.maxWidth);
149
177
  // Lay out children stacked inside the border (inset by the border width). Width is finalized here;
150
178
  // height is left unbounded so each child sizes to its own content - EXCEPT for a child that cannot
151
179
  // lay itself out without a bound (a stack holding an `Expanded`/`Spacer`). That one gets the height
152
180
  // still free inside the box, so its flex child has real leftover space instead of an infinite one.
153
181
  const innerWidth = shrinkWrapWidth
154
182
  ? Infinity
155
- : Math.max(0, (this.width ?? 0) - 2 * this.borderWidth);
183
+ : Math.max(0, (width ?? 0) - 2 * this.borderWidth);
156
184
  const innerHeight = shrinkWrapHeight
157
185
  ? Infinity
158
- : Math.max(0, (this.height ?? 0) - 2 * this.borderWidth);
186
+ : Math.max(0, (height ?? 0) - 2 * this.borderWidth);
159
187
  let contentWidth = 0;
160
188
  let contentHeight = 0;
161
- let yCursor = this.y + this.borderWidth;
189
+ // Children are placed relative to this box's top-left, inside the border.
190
+ let yCursor = this.borderWidth;
162
191
  for (const child of this.children) {
163
- const consumed = yCursor - (this.y + this.borderWidth);
192
+ const consumed = yCursor - this.borderWidth;
164
193
  // Same rule the flex helper applies: a child gets a finite main extent when it cannot lay itself
165
194
  // out without one - a stack holding an `Expanded`/`Spacer`, or a child sized as a percentage of us.
166
195
  const needsBound = child.needsBoundedMain(false) || child.relativeSizeFactor(false) !== undefined;
167
196
  const childHeight = needsBound
168
197
  ? Math.max(0, innerHeight - consumed) // Infinity - consumed stays Infinity, as it should
169
198
  : Infinity;
170
- const childSize = child.calculateLayout(BoxConstraints.loose(innerWidth, childHeight), { x: this.x + this.borderWidth, y: yCursor }, childCtx);
199
+ const childConstraints = BoxConstraints.loose(innerWidth, childHeight);
200
+ const childSize = offset
201
+ ? child.calculateLayout(childConstraints, { x: this.borderWidth, y: yCursor }, childCtx)
202
+ : child.measure(childConstraints, childCtx);
171
203
  yCursor += childSize.height;
172
204
  contentHeight += childSize.height;
173
205
  contentWidth = Math.max(contentWidth, childSize.width);
@@ -175,32 +207,44 @@ export class RectangleElement extends SizedPDFElement {
175
207
  // No explicit width and no bounded region: shrink-wrap to the widest child.
176
208
  // Clamped, so a min/max still applies to a box that sized itself to its content.
177
209
  if (shrinkWrapWidth)
178
- this.width = bounds.constrainWidth(contentWidth + 2 * this.borderWidth);
210
+ width = bounds.constrainWidth(contentWidth + 2 * this.borderWidth);
179
211
  // A ratio on a shrink-wrapping box can only be applied HERE: before the children were measured
180
212
  // there was no width to derive the height from.
181
213
  const ratio = this.sizeMemory.aspectRatio;
182
214
  const ratioSizesHeight = ratio !== undefined && ratio > 0 && shrinkWrapWidth && shrinkWrapHeight;
183
215
  // No explicit height and no bounded region: the border hugs its content.
184
216
  if (ratioSizesHeight)
185
- this.height = bounds.constrainHeight((this.width ?? 0) / ratio);
217
+ height = bounds.constrainHeight((width ?? 0) / ratio);
186
218
  else if (shrinkWrapHeight)
187
- this.height = bounds.constrainHeight(contentHeight + 2 * this.borderWidth);
188
- // The box is sized now: place any out-of-flow `Positioned` descendants against its box.
219
+ height = bounds.constrainHeight(contentHeight + 2 * this.borderWidth);
220
+ if (offset) {
221
+ this.x = x;
222
+ this.y = y;
223
+ this.width = width;
224
+ this.height = height;
225
+ }
226
+ // The box is sized now: place its out-of-flow `Positioned` descendants, relative to its own
227
+ // top-left. Each is drawn at its own place in the tree (tree order).
189
228
  if (frame) {
190
- frame.origin = { x: this.x, y: this.y };
191
- frame.size = { width: this.width ?? 0, height: this.height ?? 0 };
192
- for (const place of frame.place)
193
- place(frame, ctx);
229
+ const box = { width: width ?? 0, height: height ?? 0 };
230
+ for (const p of positionedIn(this.children))
231
+ p.placeIn(box, childCtx);
194
232
  }
195
233
  // Border-box model: width/height ARE the outer box (the rect we draw); the content
196
234
  // is inset by the border on every side. Report the honest box size - no phantom
197
235
  // border added on (that asymmetric "+border" was the source of the magic-3 fudge in
198
236
  // fragment). Top-left coordinates; the Y-flip happens at the IR -> backend seam.
199
237
  return {
200
- width: this.width ?? 0,
201
- height: this.height ?? 0,
238
+ width: width ?? 0,
239
+ height: height ?? 0,
202
240
  };
203
241
  }
242
+ isPositioningFrame() {
243
+ return this.relative;
244
+ }
245
+ layoutChildren() {
246
+ return this.children ?? [];
247
+ }
204
248
  getProps() {
205
249
  return {
206
250
  x: this.x,
@@ -214,6 +258,7 @@ export class RectangleElement extends SizedPDFElement {
214
258
  radius: this.radius,
215
259
  sideBorders: this.sideBorders,
216
260
  overflow: this.overflow,
261
+ relative: this.relative,
217
262
  };
218
263
  }
219
264
  }
@@ -68,6 +68,17 @@ export declare class RowElement extends SizedPDFElement {
68
68
  /** A Row lays out side by side, so widths add up (gaps included) and heights overlap. */
69
69
  minIntrinsicMain(horizontal: boolean, ctx: LayoutContext): number;
70
70
  calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
71
+ protected computeSize(constraints: BoxConstraints, ctx: LayoutContext): Size;
72
+ /** The first element inside this row that asks for a page break by flag, once found (or `null`). */
73
+ private declaredBreak?;
74
+ /**
75
+ * A row is one line: nothing inside it starts a new page. A `breakBefore` / `breakAfter` in a cell
76
+ * (a table row is a Row too) is therefore ignored - say so. A `PageBreak` reports itself.
77
+ */
78
+ private reportIgnoredBreak;
79
+ /** The one sizing path: `layout` places the children (relative to this box), `measure` only sizes them. */
80
+ private sizeWith;
81
+ layoutChildren(): readonly PDFElement[];
71
82
  getProps(): {
72
83
  x: number;
73
84
  y: number;