@jasy/pdf 1.0.0-alpha.8 → 1.0.0-alpha.9

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 (97) hide show
  1. package/README.md +4 -3
  2. package/dist/api/content.d.ts +5 -4
  3. package/dist/api/content.js +13 -7
  4. package/dist/api/dimension.d.ts +56 -0
  5. package/dist/api/dimension.js +27 -0
  6. package/dist/api/font-url.d.ts +28 -0
  7. package/dist/api/font-url.js +134 -0
  8. package/dist/api/gradient.d.ts +53 -0
  9. package/dist/api/gradient.js +97 -0
  10. package/dist/api/index.d.ts +2 -0
  11. package/dist/api/index.js +2 -0
  12. package/dist/api/insets.d.ts +19 -12
  13. package/dist/api/insets.js +18 -7
  14. package/dist/api/layout.d.ts +20 -6
  15. package/dist/api/layout.js +31 -6
  16. package/dist/api/page-builder.d.ts +8 -0
  17. package/dist/api/page-builder.js +12 -0
  18. package/dist/api/structure.d.ts +5 -0
  19. package/dist/api/structure.js +16 -4
  20. package/dist/api/text.d.ts +42 -6
  21. package/dist/api/text.js +29 -5
  22. package/dist/edit/document.d.ts +14 -4
  23. package/dist/edit/document.js +15 -22
  24. package/dist/edit/fill.d.ts +19 -2
  25. package/dist/edit/fill.js +63 -34
  26. package/dist/edit/flatten.d.ts +13 -1
  27. package/dist/edit/flatten.js +38 -13
  28. package/dist/edit/index.d.ts +2 -0
  29. package/dist/edit/index.js +3 -1
  30. package/dist/edit/objects.d.ts +5 -0
  31. package/dist/edit/objects.js +27 -0
  32. package/dist/edit/writer.d.ts +7 -0
  33. package/dist/edit/writer.js +23 -1
  34. package/dist/elements/container-element.d.ts +21 -1
  35. package/dist/elements/container-element.js +25 -10
  36. package/dist/elements/image-element.d.ts +18 -4
  37. package/dist/elements/image-element.js +27 -12
  38. package/dist/elements/layout/expanded-element.d.ts +3 -1
  39. package/dist/elements/layout/expanded-element.js +12 -3
  40. package/dist/elements/layout/padding-element.d.ts +4 -2
  41. package/dist/elements/layout/padding-element.js +4 -2
  42. package/dist/elements/layout/page-builder-element.js +2 -0
  43. package/dist/elements/pdf-element.d.ts +59 -1
  44. package/dist/elements/pdf-element.js +62 -1
  45. package/dist/elements/rectangle-element.d.ts +20 -6
  46. package/dist/elements/rectangle-element.js +25 -12
  47. package/dist/elements/row-element.d.ts +24 -1
  48. package/dist/elements/row-element.js +25 -10
  49. package/dist/elements/text-element.d.ts +60 -1
  50. package/dist/elements/text-element.js +132 -33
  51. package/dist/ir/display-list.d.ts +23 -6
  52. package/dist/ir/display-list.js +8 -1
  53. package/dist/layout/alignment.d.ts +10 -0
  54. package/dist/layout/alignment.js +6 -0
  55. package/dist/layout/box-constraints.d.ts +53 -0
  56. package/dist/layout/box-constraints.js +72 -0
  57. package/dist/layout/insets.d.ts +28 -0
  58. package/dist/layout/insets.js +20 -0
  59. package/dist/renderer/image-renderer.js +2 -1
  60. package/dist/renderer/pdf-backend.d.ts +6 -0
  61. package/dist/renderer/pdf-backend.js +78 -31
  62. package/dist/renderer/pdf-document-renderer.js +8 -1
  63. package/dist/renderer/rectangle-renderer.js +8 -2
  64. package/dist/renderer/text-renderer.d.ts +2 -2
  65. package/dist/renderer/text-renderer.js +150 -42
  66. package/dist/text/advance.js +5 -2
  67. package/dist/text/arabic.d.ts +20 -0
  68. package/dist/text/arabic.js +101 -0
  69. package/dist/text/bidi.d.ts +33 -0
  70. package/dist/text/bidi.js +94 -0
  71. package/dist/text/font-fallback.d.ts +25 -0
  72. package/dist/text/font-fallback.js +38 -0
  73. package/dist/text/line-breaker.d.ts +35 -1
  74. package/dist/text/line-breaker.js +76 -16
  75. package/dist/text/orphans-widows.d.ts +27 -0
  76. package/dist/text/orphans-widows.js +53 -0
  77. package/dist/text/shape.d.ts +29 -0
  78. package/dist/text/shape.js +67 -0
  79. package/dist/text/text-style.d.ts +25 -0
  80. package/dist/text/text-style.js +44 -1
  81. package/dist/utils/flex-layout.d.ts +24 -4
  82. package/dist/utils/flex-layout.js +169 -7
  83. package/dist/utils/font-metrics.d.ts +5 -0
  84. package/dist/utils/gsub.d.ts +36 -0
  85. package/dist/utils/gsub.js +178 -0
  86. package/dist/utils/inflate.d.ts +16 -0
  87. package/dist/utils/inflate.js +52 -0
  88. package/dist/utils/pdf-object-manager.d.ts +21 -0
  89. package/dist/utils/pdf-object-manager.js +107 -9
  90. package/dist/utils/ttf-parser.d.ts +6 -0
  91. package/dist/utils/ttf-parser.js +19 -0
  92. package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
  93. package/dist/utils/utf8-to-windows1252-encoder.js +10 -0
  94. package/dist/utils/woff.d.ts +10 -0
  95. package/dist/utils/woff.js +116 -0
  96. package/dist/validators/element-validator.js +9 -2
  97. package/package.json +2 -1
@@ -1,7 +1,7 @@
1
1
  import { getImageDimensions } from "../utils/image-helper.js";
2
2
  import { latin1FromBytes } from "../utils/bytes.js";
3
3
  import { readFileBytesAsync } from "../platform/node-fs.js";
4
- import { resolveExtent } from "../layout/box-constraints.js";
4
+ import { extentSpecs, resolveSize, } from "../layout/box-constraints.js";
5
5
  import { SizedPDFElement } from "./pdf-element.js";
6
6
  // path.extname without node:path (browser-safe): the substring from the last dot, if it sits after the
7
7
  // last slash (so a dot in a directory name does not count). Enough for image file extensions.
@@ -100,7 +100,7 @@ export class CustomBytesImage extends CustomImage {
100
100
  }
101
101
  }
102
102
  export class ImageElement extends SizedPDFElement {
103
- constructor({ image, width, height, widthFactor, heightFactor, fit = BoxFit.none, radius, alt, }) {
103
+ constructor({ image, width, height, widthFactor, heightFactor, fit = BoxFit.none, radius, alt, ...sizing }) {
104
104
  // Seed this.width/height from the request so getProps() reflects it before layout; calculateLayout
105
105
  // then reads `requested` (never the mutated fields) and overwrites these with the laid-out size.
106
106
  super({ x: 0, y: 0, width });
@@ -109,6 +109,7 @@ export class ImageElement extends SizedPDFElement {
109
109
  this.requested = { width, height };
110
110
  this.widthFactor = widthFactor;
111
111
  this.heightFactor = heightFactor;
112
+ this.sizing = sizing;
112
113
  this.fit = fit;
113
114
  this.radius = radius ?? 0;
114
115
  this.alt = alt;
@@ -136,22 +137,36 @@ export class ImageElement extends SizedPDFElement {
136
137
  this.y = offset.y;
137
138
  // Relative sizing: a fixed size or a fraction of the offered box (fraction only in a bounded axis).
138
139
  // Read from `requested` (not this.width/height, which we overwrite below) so every pass re-resolves.
139
- let w = resolveExtent(this.requested.width, this.widthFactor, constraints.maxWidth, constraints.hasBoundedWidth);
140
- let h = resolveExtent(this.requested.height, this.heightFactor, constraints.maxHeight, constraints.hasBoundedHeight);
141
- // Aspect auto-size: when the user pinned exactly ONE axis, derive the other from the intrinsic
142
- // ratio (CSS `width: 50%; height: auto`). Only fires once the pre-pass resolved the pixel size.
143
- if (this.intrinsic && this.intrinsic.width > 0 && this.intrinsic.height > 0) {
140
+ const specs = extentSpecs({
141
+ ...this.sizing,
142
+ width: this.requested.width,
143
+ height: this.requested.height,
144
+ widthFactor: this.widthFactor,
145
+ heightFactor: this.heightFactor,
146
+ });
147
+ // An explicit `aspectRatio` wins over the image's own, as it does in CSS - and only it may size a
148
+ // box with NEITHER axis pinned. Left to the intrinsic ratio that would change what an unsized image
149
+ // has always done here (fill both axes and let `fit` sort it out).
150
+ const resolved = resolveSize(specs.width, specs.height, this.sizing.aspectRatio, constraints);
151
+ let w = resolved.width;
152
+ let h = resolved.height;
153
+ const bounds = resolved.constraints;
154
+ // Aspect auto-size: when exactly ONE axis is pinned, derive the other from the intrinsic ratio
155
+ // (CSS `width: 50%; height: auto`). Only fires once the pre-pass resolved the pixel size.
156
+ if (this.sizing.aspectRatio === undefined &&
157
+ this.intrinsic &&
158
+ this.intrinsic.width > 0 &&
159
+ this.intrinsic.height > 0) {
144
160
  const ratio = this.intrinsic.width / this.intrinsic.height;
145
161
  if (w !== undefined && h === undefined)
146
- h = w / ratio;
162
+ h = bounds.constrainHeight(w / ratio);
147
163
  else if (h !== undefined && w === undefined)
148
- w = h * ratio;
164
+ w = bounds.constrainWidth(h * ratio);
149
165
  }
150
166
  // Fall back to the prior behavior for an unpinned axis: a bounded axis fills, otherwise keep the
151
167
  // requested fixed size (or undefined). Read from `requested`, never the mutated this.width/height.
152
- this.width = w ?? (constraints.hasBoundedWidth ? constraints.maxWidth : this.requested.width);
153
- this.height =
154
- h ?? (constraints.hasBoundedHeight ? constraints.maxHeight : this.requested.height);
168
+ this.width = w ?? (bounds.hasBoundedWidth ? bounds.maxWidth : this.requested.width);
169
+ this.height = h ?? (bounds.hasBoundedHeight ? bounds.maxHeight : this.requested.height);
155
170
  // Top-left coordinates; the fit logic (renderer) and the Y-flip (seam) run later.
156
171
  return { width: this.width ?? 0, height: this.height ?? 0 };
157
172
  }
@@ -9,7 +9,7 @@ export declare class ExpandedElement extends FlexiblePDFElement implements Fragm
9
9
  private y;
10
10
  private width;
11
11
  private height;
12
- constructor({ flex, child }: ExpandedElementParams);
12
+ constructor({ flex, basis, basisFactor, child }: ExpandedElementParams);
13
13
  calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
14
14
  /**
15
15
  * When the column paginates, a flex region can't "fill" across pages, so it delegates to
@@ -24,6 +24,8 @@ export declare class ExpandedElement extends FlexiblePDFElement implements Fragm
24
24
  y: number;
25
25
  width: number;
26
26
  height: number;
27
+ basis: number;
28
+ basisFactor: number | undefined;
27
29
  child: PDFElement;
28
30
  };
29
31
  }
@@ -3,8 +3,8 @@ import { FlexiblePDFElement, } from "../pdf-element.js";
3
3
  import { BoxConstraints } from "../../layout/box-constraints.js";
4
4
  import { isFragmentable } from "../../layout/fragmentation.js";
5
5
  export class ExpandedElement extends FlexiblePDFElement {
6
- constructor({ flex, child }) {
7
- super({ flex });
6
+ constructor({ flex, basis, basisFactor, child }) {
7
+ super({ flex, basis, basisFactor });
8
8
  this.x = 0;
9
9
  this.y = 0;
10
10
  this.width = 0;
@@ -50,7 +50,14 @@ export class ExpandedElement extends FlexiblePDFElement {
50
50
  };
51
51
  }
52
52
  cloneWithChild(child) {
53
- return new ExpandedElement({ flex: this.flex, child });
53
+ // The basis has to come along: a fragment that lost it would claim only its share of the leftover,
54
+ // so a `flexBasis` slot would silently change width on the page it continues onto.
55
+ return new ExpandedElement({
56
+ flex: this.flex,
57
+ basis: this.basis,
58
+ basisFactor: this.basisFactor,
59
+ child,
60
+ });
54
61
  }
55
62
  getProps() {
56
63
  return {
@@ -58,6 +65,8 @@ export class ExpandedElement extends FlexiblePDFElement {
58
65
  y: this.y,
59
66
  width: this.width,
60
67
  height: this.height,
68
+ basis: this.basis,
69
+ basisFactor: this.basisFactor,
61
70
  child: this.child,
62
71
  };
63
72
  }
@@ -1,8 +1,10 @@
1
1
  import { PDFElement, LayoutContext, WithChild, SizedPDFElement } from "../pdf-element.js";
2
2
  import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
3
3
  import { Fragmentable, FragmentResult } from "../../layout/fragmentation.js";
4
+ import { type EdgeSpecs } from "../../layout/insets.js";
4
5
  interface PaddingElementParams extends WithChild {
5
- margin: [number, number, number, number];
6
+ /** Still unresolved: a side may be a percentage of the offered WIDTH (see `Insets`). */
7
+ margin: EdgeSpecs;
6
8
  }
7
9
  export declare class PaddingElement extends SizedPDFElement implements Fragmentable {
8
10
  private child;
@@ -25,7 +27,7 @@ export declare class PaddingElement extends SizedPDFElement implements Fragmenta
25
27
  y: number;
26
28
  width: number | undefined;
27
29
  height: number | undefined;
28
- margin: [number, number, number, number];
30
+ margin: EdgeSpecs;
29
31
  child: PDFElement;
30
32
  };
31
33
  }
@@ -2,6 +2,7 @@ import { Validator } from "../../validators/element-validator.js";
2
2
  import { SizedPDFElement } from "../pdf-element.js";
3
3
  import { BoxConstraints } from "../../layout/box-constraints.js";
4
4
  import { isFragmentable } from "../../layout/fragmentation.js";
5
+ import { resolveEdges } from "../../layout/insets.js";
5
6
  export class PaddingElement extends SizedPDFElement {
6
7
  constructor({ margin, child }) {
7
8
  super({ x: 0, y: 0 });
@@ -18,7 +19,7 @@ export class PaddingElement extends SizedPDFElement {
18
19
  if (!isFragmentable(this.child)) {
19
20
  return { fitted: null, remainder: this };
20
21
  }
21
- const [marginTop, marginRight, marginBottom, marginLeft] = this.margin;
22
+ const [marginTop, marginRight, marginBottom, marginLeft] = resolveEdges(this.margin, width);
22
23
  const childWidth = width - marginLeft - marginRight;
23
24
  const childMaxHeight = maxHeight - marginTop - marginBottom;
24
25
  const split = this.child.fragment(childMaxHeight, childWidth, ctx);
@@ -48,7 +49,8 @@ export class PaddingElement extends SizedPDFElement {
48
49
  this.width = constraints.maxWidth;
49
50
  this.x = offset.x;
50
51
  this.y = offset.y;
51
- const [marginTop, marginRight, marginBottom, marginLeft] = this.margin;
52
+ // Percentages resolve against the width we were OFFERED, so every pass gets the same answer.
53
+ const [marginTop, marginRight, marginBottom, marginLeft] = resolveEdges(this.margin, constraints.maxWidth);
52
54
  // The child is inset by the margins: shifted down/right, narrowed by the
53
55
  // horizontal margins, and left height-unbounded so it sizes to its own content.
54
56
  const childOffset = {
@@ -27,6 +27,8 @@ export class PageBuilderElement extends PDFElement {
27
27
  return (ctx.pageInfo ?? {
28
28
  pageNumber: 1,
29
29
  pageCount: 1,
30
+ subPageNumber: 1,
31
+ subPageTotalPages: 1,
30
32
  pageSize: resolvePageSize(ctx.pageConfig),
31
33
  });
32
34
  }
@@ -3,6 +3,7 @@ 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
5
  import type { BoxConstraints, Offset, Size } from "../layout/box-constraints.js";
6
+ import type { CrossAlign } from "../layout/alignment.js";
6
7
  /**
7
8
  * Everything the layout pass needs, threaded explicitly (no global singleton):
8
9
  * font metrics for measuring, and the geometry of the page currently being laid out.
@@ -29,8 +30,18 @@ export interface PositioningFrame {
29
30
  * the document's final physical page total. `pageSize` is the media box in points, orientation applied.
30
31
  */
31
32
  export interface PageInfo {
33
+ /** 1-based, counting PHYSICAL pages across the whole document. */
32
34
  pageNumber: number;
35
+ /** Physical pages in the whole document. */
33
36
  pageCount: number;
37
+ /**
38
+ * 1-based within the LOGICAL page this sheet came from - the `Page(...)` element the author wrote.
39
+ * A document of one `Page` has `subPageNumber === pageNumber`; a document of several (an invoice
40
+ * plus its attachment, say) restarts the count at each one.
41
+ */
42
+ subPageNumber: number;
43
+ /** Physical pages this logical page produced. */
44
+ subPageTotalPages: number;
34
45
  pageSize: {
35
46
  width: number;
36
47
  height: number;
@@ -108,6 +119,31 @@ export declare abstract class PDFElement {
108
119
  * parent packer once this element has been placed whole. Default: no.
109
120
  */
110
121
  breaksAfter(): boolean;
122
+ /**
123
+ * This child's own cross-axis alignment inside its Column / Row (CSS `align-self`), overriding the
124
+ * container's `align`. Lives on the base element because any child of a stack may set it, not just
125
+ * the sized ones. `undefined` means "follow the container".
126
+ */
127
+ alignSelf?: CrossAlign;
128
+ /** Sets `alignSelf` and returns the element, so a factory can thread it through in one expression. */
129
+ withAlignSelf(align: CrossAlign | undefined): this;
130
+ /**
131
+ * Where this child sits among its siblings (CSS `order`), lowest first, source order deciding ties.
132
+ * It moves the child in the LAYOUT only - the element tree, and therefore the reading order a tagged
133
+ * PDF exposes, is untouched. Default 0.
134
+ */
135
+ order: number;
136
+ /**
137
+ * How willingly this child gives up main-axis space when the line overflows (CSS `flex-shrink`),
138
+ * weighted by its own size as CSS does. **Default 0, deliberately unlike CSS's 1**: shrinking is a
139
+ * change of what a document LOOKS like, and every document written before this existed must keep
140
+ * laying out exactly as it did. Opt in per child.
141
+ */
142
+ flexShrink: number;
143
+ /** Sets `flexShrink` and returns the element. */
144
+ withFlexShrink(shrink: number | undefined): this;
145
+ /** Sets `order` and returns the element, so a factory can thread it through in one expression. */
146
+ withOrder(order: number | undefined): this;
111
147
  }
112
148
  export declare abstract class SizedPDFElement extends PDFElement {
113
149
  protected x: number;
@@ -120,14 +156,32 @@ export declare abstract class SizedPDFElement extends PDFElement {
120
156
  export declare abstract class FlexiblePDFElement extends PDFElement {
121
157
  protected flex: number;
122
158
  protected verticalChildAlignment: VerticalAlignment;
159
+ /** Points reserved before any leftover is shared out (CSS `flex-basis`); default 0. */
160
+ protected basis: number;
161
+ /** The same as a fraction of the line, for `flexBasis: "25%"`. */
162
+ protected basisFactor?: number;
123
163
  constructor(data: FlexibleElement);
124
164
  getFlex(): number;
165
+ /**
166
+ * The main extent this child starts from, before the leftover is shared out. `lineMain` is what the
167
+ * line offers its items (its extent minus the gaps), so a percentage basis resolves against the same
168
+ * base a percentage WIDTH already does - and falls back to 0 on an unbounded axis, where a fraction
169
+ * has no meaning.
170
+ */
171
+ /** Whether a PERCENTAGE basis was given - it resolves to 0 on an unbounded axis, so asking for the
172
+ * number alone cannot tell "no basis" from "a basis that does not apply here". */
173
+ hasBasisFactor(): boolean;
174
+ getBasis(lineMain: number): number;
125
175
  }
126
176
  export declare enum HorizontalAlignment {
177
+ /** CSS `text-align: start` - the side the writing STARTS on: left in `ltr`, right in `rtl`. The
178
+ * default, so a right-to-left paragraph begins on the right without anyone asking for it. */
179
+ start = "START",
127
180
  left = "LEFT",
128
181
  right = "RIGHT",
129
182
  center = "CENTER",
130
- block = "BLOCK"
183
+ /** CSS `text-align: justify` - both edges line up, the last line of a paragraph excepted. */
184
+ justify = "JUSTIFY"
131
185
  }
132
186
  export declare enum VerticalAlignment {
133
187
  top = "TOP",
@@ -142,6 +196,10 @@ export interface WithChild {
142
196
  }
143
197
  export interface FlexibleElement {
144
198
  flex: number;
199
+ /** Points reserved before the leftover is shared out (CSS `flex-basis`). */
200
+ basis?: number;
201
+ /** The same as a fraction of the line, for `flexBasis: "25%"`. */
202
+ basisFactor?: number;
145
203
  verticalChildAlignment?: VerticalAlignment;
146
204
  }
147
205
  export interface SizedElement {
@@ -5,6 +5,19 @@ export class PDFElement {
5
5
  // fragmentation copies (adoptStructId), so a paragraph or table split across pages stays ONE structure
6
6
  // element. Only the elements that actually tag (Text, Image, StructGroup) read it.
7
7
  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 0, deliberately unlike CSS's 1**: shrinking is a
17
+ * change of what a document LOOKS like, and every document written before this existed must keep
18
+ * laying out exactly as it did. Opt in per child.
19
+ */
20
+ this.flexShrink = 0;
8
21
  }
9
22
  get structId() {
10
23
  return this._structId;
@@ -65,6 +78,31 @@ export class PDFElement {
65
78
  breaksAfter() {
66
79
  return false;
67
80
  }
81
+ /** Sets `alignSelf` and returns the element, so a factory can thread it through in one expression. */
82
+ withAlignSelf(align) {
83
+ if (align !== undefined)
84
+ this.alignSelf = align;
85
+ return this;
86
+ }
87
+ /** Sets `flexShrink` and returns the element. */
88
+ withFlexShrink(shrink) {
89
+ if (shrink !== undefined) {
90
+ if (!Number.isFinite(shrink) || shrink < 0) {
91
+ throw new Error(`Invalid flexShrink ${shrink}: it must be a number of at least 0.`);
92
+ }
93
+ this.flexShrink = shrink;
94
+ }
95
+ return this;
96
+ }
97
+ /** Sets `order` and returns the element, so a factory can thread it through in one expression. */
98
+ withOrder(order) {
99
+ if (order !== undefined) {
100
+ if (!Number.isFinite(order))
101
+ throw new Error(`Invalid order ${order}: it must be a number.`);
102
+ this.order = order;
103
+ }
104
+ return this;
105
+ }
68
106
  }
69
107
  export class SizedPDFElement extends PDFElement {
70
108
  constructor(data) {
@@ -83,17 +121,40 @@ export class FlexiblePDFElement extends PDFElement {
83
121
  super();
84
122
  this.flex = data.flex;
85
123
  this.verticalChildAlignment = data.verticalChildAlignment || VerticalAlignment.middle;
124
+ this.basis = data.basis ?? 0;
125
+ this.basisFactor = data.basisFactor;
86
126
  }
87
127
  getFlex() {
88
128
  return this.flex;
89
129
  }
130
+ /**
131
+ * The main extent this child starts from, before the leftover is shared out. `lineMain` is what the
132
+ * line offers its items (its extent minus the gaps), so a percentage basis resolves against the same
133
+ * base a percentage WIDTH already does - and falls back to 0 on an unbounded axis, where a fraction
134
+ * has no meaning.
135
+ */
136
+ /** Whether a PERCENTAGE basis was given - it resolves to 0 on an unbounded axis, so asking for the
137
+ * number alone cannot tell "no basis" from "a basis that does not apply here". */
138
+ hasBasisFactor() {
139
+ return this.basisFactor !== undefined;
140
+ }
141
+ getBasis(lineMain) {
142
+ if (this.basisFactor !== undefined) {
143
+ return Number.isFinite(lineMain) ? lineMain * this.basisFactor : 0;
144
+ }
145
+ return this.basis;
146
+ }
90
147
  }
91
148
  export var HorizontalAlignment;
92
149
  (function (HorizontalAlignment) {
150
+ /** CSS `text-align: start` - the side the writing STARTS on: left in `ltr`, right in `rtl`. The
151
+ * default, so a right-to-left paragraph begins on the right without anyone asking for it. */
152
+ HorizontalAlignment["start"] = "START";
93
153
  HorizontalAlignment["left"] = "LEFT";
94
154
  HorizontalAlignment["right"] = "RIGHT";
95
155
  HorizontalAlignment["center"] = "CENTER";
96
- HorizontalAlignment["block"] = "BLOCK";
156
+ /** CSS `text-align: justify` - both edges line up, the last line of a paragraph excepted. */
157
+ HorizontalAlignment["justify"] = "JUSTIFY";
97
158
  })(HorizontalAlignment || (HorizontalAlignment = {}));
98
159
  export var VerticalAlignment;
99
160
  (function (VerticalAlignment) {
@@ -1,3 +1,5 @@
1
+ import type { Radii } from "../ir/display-list.js";
2
+ import type { GradientInput } from "../api/gradient.js";
1
3
  import { Color } from "../common/color.js";
2
4
  import { BoxConstraints, Offset, Size } from "../layout/box-constraints.js";
3
5
  import { Fragmentable, FragmentResult } from "../layout/fragmentation.js";
@@ -12,10 +14,11 @@ export interface SideBorders {
12
14
  }
13
15
  interface RectangleElementParams extends SizedElement, WithChildren {
14
16
  color?: Color;
15
- backgroundColor?: Color;
17
+ /** Solid colour, or a box-relative gradient the renderer resolves against this box. */
18
+ backgroundColor?: Color | GradientInput;
16
19
  borderWidth?: number;
17
- /** Corner radius in points; 0 = sharp corners (default). */
18
- radius?: number;
20
+ /** Corner radius in points - one number for all four corners, or per corner. 0 = sharp (default). */
21
+ radius?: number | Radii;
19
22
  /** Individual side borders; overrides the uniform `color` border when present. */
20
23
  sideBorders?: SideBorders;
21
24
  /** When true, this box is a positioning frame for `Positioned` descendants (CSS `relative`). */
@@ -28,6 +31,17 @@ interface RectangleElementParams extends SizedElement, WithChildren {
28
31
  widthFactor?: number;
29
32
  /** Height as a fraction (0..1) of the offered height; see `widthFactor`. */
30
33
  heightFactor?: number;
34
+ /** Lower / upper bounds per axis, in points or as a fraction; see `SizingParams`. */
35
+ minWidth?: number;
36
+ maxWidth?: number;
37
+ minHeight?: number;
38
+ maxHeight?: number;
39
+ minWidthFactor?: number;
40
+ maxWidthFactor?: number;
41
+ minHeightFactor?: number;
42
+ maxHeightFactor?: number;
43
+ /** width / height; derives whichever axis is left open (CSS `aspect-ratio`). */
44
+ aspectRatio?: number;
31
45
  /** Start this box on a fresh page (CSS `break-before: page`). */
32
46
  breakBefore?: boolean;
33
47
  /** Start everything after this box on a fresh page (CSS `break-after: page`). */
@@ -45,7 +59,7 @@ export declare class RectangleElement extends SizedPDFElement implements Fragmen
45
59
  private breakBefore;
46
60
  private breakAfter;
47
61
  private sizeMemory;
48
- constructor({ children, color, backgroundColor, borderWidth, width, height, radius, sideBorders, relative, overflow, widthFactor, heightFactor, breakBefore, breakAfter, }: RectangleElementParams);
62
+ constructor({ children, color, backgroundColor, borderWidth, width, height, radius, sideBorders, relative, overflow, breakBefore, breakAfter, ...sizing }: RectangleElementParams);
49
63
  breaksBefore(): boolean;
50
64
  breaksAfter(): boolean;
51
65
  /**
@@ -72,9 +86,9 @@ export declare class RectangleElement extends SizedPDFElement implements Fragmen
72
86
  height: number | undefined;
73
87
  children: PDFElement[];
74
88
  color: Color;
75
- backgroundColor: Color | undefined;
89
+ backgroundColor: Color | GradientInput | undefined;
76
90
  borderWidth: number;
77
- radius: number;
91
+ radius: number | Radii;
78
92
  sideBorders: SideBorders | undefined;
79
93
  overflow: "hidden" | "visible";
80
94
  };
@@ -1,9 +1,9 @@
1
1
  import { Color } from "../common/color.js";
2
- import { BoxConstraints, resolveExtent } from "../layout/box-constraints.js";
2
+ import { BoxConstraints, extentSpecs, resolveSize, } from "../layout/box-constraints.js";
3
3
  import { childrenForceBreak, packChildren, } from "../layout/fragmentation.js";
4
4
  import { SizedPDFElement, } from "./pdf-element.js";
5
5
  export class RectangleElement extends SizedPDFElement {
6
- constructor({ children = [], color = new Color(0, 0, 0), backgroundColor, borderWidth, width, height, radius, sideBorders, relative, overflow, widthFactor, heightFactor, breakBefore, breakAfter, }) {
6
+ constructor({ children = [], color = new Color(0, 0, 0), backgroundColor, borderWidth, width, height, radius, sideBorders, relative, overflow, breakBefore, breakAfter, ...sizing }) {
7
7
  super({ x: 0, y: 0, width, height });
8
8
  this.children = [];
9
9
  this.children = children;
@@ -17,7 +17,7 @@ export class RectangleElement extends SizedPDFElement {
17
17
  this.overflow = overflow ?? "visible";
18
18
  this.breakBefore = breakBefore ?? false;
19
19
  this.breakAfter = breakAfter ?? false;
20
- this.sizeMemory = { x: 0, y: 0, width, height, widthFactor, heightFactor };
20
+ this.sizeMemory = { ...sizing, x: 0, y: 0, width, height };
21
21
  }
22
22
  breaksBefore() {
23
23
  return this.breakBefore;
@@ -102,15 +102,21 @@ export class RectangleElement extends SizedPDFElement {
102
102
  calculateLayout(constraints, offset, ctx) {
103
103
  // An explicit extent is a fixed point size or a fraction of the offered box (relative sizing);
104
104
  // the fraction only resolves in a bounded region. `undefined` = fill / shrink-wrap below.
105
- const explicitWidth = resolveExtent(this.sizeMemory.width, this.sizeMemory.widthFactor, constraints.maxWidth, constraints.hasBoundedWidth);
106
- const explicitHeight = resolveExtent(this.sizeMemory.height, this.sizeMemory.heightFactor, constraints.maxHeight, constraints.hasBoundedHeight);
105
+ const specs = extentSpecs(this.sizeMemory);
106
+ const resolved = resolveSize(specs.width, specs.height, this.sizeMemory.aspectRatio, constraints);
107
+ const explicitWidth = resolved.width;
108
+ const explicitHeight = resolved.height;
109
+ // min/max come back as narrowed constraints, so the fill and shrink-wrap paths below obey them too.
110
+ const bounds = resolved.constraints;
107
111
  // Width: an explicit size wins (clamped), else fill the offered box. (Without this a
108
112
  // fixed box would balloon to the parent's size.)
113
+ // Whether there is a region to fill is decided by the constraints we were GIVEN: a `maxWidth` caps
114
+ // a box, it does not turn a shrink-wrapping one into a filling one (CSS `max-width` does not either).
109
115
  this.width =
110
116
  explicitWidth !== undefined
111
- ? constraints.constrainWidth(explicitWidth)
117
+ ? bounds.constrainWidth(explicitWidth)
112
118
  : constraints.hasBoundedWidth
113
- ? constraints.maxWidth
119
+ ? bounds.maxWidth
114
120
  : this.width;
115
121
  // Width shrink-wrap: no explicit width AND an unbounded region (e.g. a `Box` badge inside a
116
122
  // `Positioned`). Resolved after the children are measured, just below.
@@ -121,9 +127,9 @@ export class RectangleElement extends SizedPDFElement {
121
127
  const shrinkWrapHeight = explicitHeight === undefined && !constraints.hasBoundedHeight;
122
128
  this.height =
123
129
  explicitHeight !== undefined
124
- ? constraints.constrainHeight(explicitHeight)
130
+ ? bounds.constrainHeight(explicitHeight)
125
131
  : constraints.hasBoundedHeight
126
- ? constraints.maxHeight
132
+ ? bounds.maxHeight
127
133
  : this.height;
128
134
  this.x = this.sizeMemory.x + offset.x;
129
135
  this.y = this.sizeMemory.y + offset.y;
@@ -161,11 +167,18 @@ export class RectangleElement extends SizedPDFElement {
161
167
  contentWidth = Math.max(contentWidth, childSize.width);
162
168
  }
163
169
  // No explicit width and no bounded region: shrink-wrap to the widest child.
170
+ // Clamped, so a min/max still applies to a box that sized itself to its content.
164
171
  if (shrinkWrapWidth)
165
- this.width = contentWidth + 2 * this.borderWidth;
172
+ this.width = bounds.constrainWidth(contentWidth + 2 * this.borderWidth);
173
+ // A ratio on a shrink-wrapping box can only be applied HERE: before the children were measured
174
+ // there was no width to derive the height from.
175
+ const ratio = this.sizeMemory.aspectRatio;
176
+ const ratioSizesHeight = ratio !== undefined && ratio > 0 && shrinkWrapWidth && shrinkWrapHeight;
166
177
  // No explicit height and no bounded region: the border hugs its content.
167
- if (shrinkWrapHeight)
168
- this.height = contentHeight + 2 * this.borderWidth;
178
+ if (ratioSizesHeight)
179
+ this.height = bounds.constrainHeight((this.width ?? 0) / ratio);
180
+ else if (shrinkWrapHeight)
181
+ this.height = bounds.constrainHeight(contentHeight + 2 * this.borderWidth);
169
182
  // The box is sized now: place any out-of-flow `Positioned` descendants against its box.
170
183
  if (frame) {
171
184
  frame.origin = { x: this.x, y: this.y };
@@ -8,11 +8,28 @@ interface RowElementParams extends WithChildren {
8
8
  main?: MainAlign;
9
9
  /** Vertical alignment of each child (cross axis); defaults to `stretch`. */
10
10
  cross?: CrossAlign;
11
+ /** Lay the children out backwards along the main axis (CSS `row-reverse`). */
12
+ reverse?: boolean;
13
+ /** Let the children flow onto further lines when they do not fit (CSS `flex-wrap`). */
14
+ wrap?: boolean;
15
+ /** How the block of wrapped lines sits across the axis (CSS `align-content`). */
16
+ alignContent?: MainAlign;
11
17
  /** Width/height as points (fixed) or a fraction (0..1) of the offered box (relative sizing). */
12
18
  width?: number;
13
19
  height?: number;
14
20
  widthFactor?: number;
15
21
  heightFactor?: number;
22
+ /** Lower / upper bounds per axis, in points or as a fraction; see `SizingParams`. */
23
+ minWidth?: number;
24
+ maxWidth?: number;
25
+ minHeight?: number;
26
+ maxHeight?: number;
27
+ minWidthFactor?: number;
28
+ maxWidthFactor?: number;
29
+ minHeightFactor?: number;
30
+ maxHeightFactor?: number;
31
+ /** width / height; derives whichever axis is left open (CSS `aspect-ratio`). */
32
+ aspectRatio?: number;
16
33
  /** Start this row on a fresh page (CSS `break-before: page`). */
17
34
  breakBefore?: boolean;
18
35
  /** Start everything after this row on a fresh page (CSS `break-after: page`). */
@@ -35,10 +52,13 @@ export declare class RowElement extends SizedPDFElement {
35
52
  private gap;
36
53
  private main;
37
54
  private cross;
55
+ private reverse;
56
+ private wrap;
57
+ private alignContent?;
38
58
  private breakBefore;
39
59
  private breakAfter;
40
60
  private requested;
41
- constructor({ children, gap, main, cross, width, height, widthFactor, heightFactor, breakBefore, breakAfter, }: RowElementParams);
61
+ constructor({ children, gap, main, cross, reverse, wrap, alignContent, width, height, breakBefore, breakAfter, ...sizing }: RowElementParams);
42
62
  breaksBefore(): boolean;
43
63
  breaksAfter(): boolean;
44
64
  relativeSizeFactor(horizontal: boolean): number | undefined;
@@ -55,6 +75,9 @@ export declare class RowElement extends SizedPDFElement {
55
75
  gap: number;
56
76
  main: MainAlign;
57
77
  cross: CrossAlign;
78
+ reverse: boolean;
79
+ wrap: boolean;
80
+ alignContent: MainAlign | undefined;
58
81
  };
59
82
  }
60
83
  export {};
@@ -1,4 +1,4 @@
1
- import { resolveExtent } from "../layout/box-constraints.js";
1
+ import { extentSpecs, resolveSize, } from "../layout/box-constraints.js";
2
2
  import { FlexLayoutHelper, HORIZONTAL_AXIS } from "../utils/flex-layout.js";
3
3
  import { FlexiblePDFElement, SizedPDFElement, } from "./pdf-element.js";
4
4
  /**
@@ -14,13 +14,16 @@ import { FlexiblePDFElement, SizedPDFElement, } from "./pdf-element.js";
14
14
  * Grid/Table concern.
15
15
  */
16
16
  export class RowElement extends SizedPDFElement {
17
- constructor({ children, gap, main, cross, width, height, widthFactor, heightFactor, breakBefore, breakAfter, }) {
17
+ constructor({ children, gap, main, cross, reverse, wrap, alignContent, width, height, breakBefore, breakAfter, ...sizing }) {
18
18
  super({ x: 0, y: 0 });
19
19
  this.children = children;
20
20
  this.gap = gap ?? 0;
21
21
  this.main = main ?? "start";
22
22
  this.cross = cross ?? "stretch";
23
- this.requested = { width, height, widthFactor, heightFactor };
23
+ this.reverse = reverse ?? false;
24
+ this.wrap = wrap ?? false;
25
+ this.alignContent = alignContent;
26
+ this.requested = { ...sizing, width, height };
24
27
  this.breakBefore = breakBefore ?? false;
25
28
  this.breakAfter = breakAfter ?? false;
26
29
  }
@@ -49,24 +52,33 @@ export class RowElement extends SizedPDFElement {
49
52
  this.y = offset.y;
50
53
  // Relative sizing: a pinned extent (fixed or a fraction of the offered box, clamped) wins; else
51
54
  // width fills the offered space (flex children split the leftover) and height is the tallest child.
52
- const explicitWidth = resolveExtent(this.requested.width, this.requested.widthFactor, constraints.maxWidth, constraints.hasBoundedWidth);
53
- const explicitHeight = resolveExtent(this.requested.height, this.requested.heightFactor, constraints.maxHeight, constraints.hasBoundedHeight);
55
+ const specs = extentSpecs(this.requested);
56
+ const resolved = resolveSize(specs.width, specs.height, this.requested.aspectRatio, constraints);
57
+ const { width: explicitWidth, height: explicitHeight, constraints: bounds } = resolved;
58
+ // Fill-or-shrink-wrap is decided by the constraints we were GIVEN; a min/max only caps the result.
54
59
  const boundedWidth = explicitWidth !== undefined
55
- ? constraints.constrainWidth(explicitWidth)
60
+ ? bounds.constrainWidth(explicitWidth)
56
61
  : constraints.hasBoundedWidth
57
- ? constraints.maxWidth
62
+ ? bounds.maxWidth
58
63
  : undefined;
59
64
  const boundedHeight = explicitHeight !== undefined
60
- ? constraints.constrainHeight(explicitHeight)
65
+ ? bounds.constrainHeight(explicitHeight)
61
66
  : constraints.hasBoundedHeight
62
- ? constraints.maxHeight
67
+ ? bounds.maxHeight
63
68
  : undefined;
64
69
  // Horizontal stack: main = width (children fill it), cross = height (children can stretch to it).
65
70
  const mainAvail = boundedWidth ?? Infinity;
66
71
  const crossAvail = boundedHeight ?? Infinity;
67
72
  let result = { mainUsed: 0, crossUsed: 0 };
68
73
  if (this.children.length > 0) {
69
- result = FlexLayoutHelper.layout(this.children, HORIZONTAL_AXIS, mainAvail, crossAvail, this.x, this.y, { gap: this.gap, main: this.main, cross: this.cross }, ctx);
74
+ result = FlexLayoutHelper.layout(this.children, HORIZONTAL_AXIS, mainAvail, crossAvail, this.x, this.y, {
75
+ gap: this.gap,
76
+ main: this.main,
77
+ cross: this.cross,
78
+ reverse: this.reverse,
79
+ wrap: this.wrap,
80
+ alignContent: this.alignContent,
81
+ }, ctx);
70
82
  }
71
83
  this.width = boundedWidth ?? result.mainUsed;
72
84
  this.height = boundedHeight ?? result.crossUsed;
@@ -82,6 +94,9 @@ export class RowElement extends SizedPDFElement {
82
94
  gap: this.gap,
83
95
  main: this.main,
84
96
  cross: this.cross,
97
+ reverse: this.reverse,
98
+ wrap: this.wrap,
99
+ alignContent: this.alignContent,
85
100
  };
86
101
  }
87
102
  }