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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +3 -2
  2. package/dist/api/color.d.ts +1 -24
  3. package/dist/api/color.js +2 -278
  4. package/dist/api/content.d.ts +7 -2
  5. package/dist/api/content.js +17 -7
  6. package/dist/api/descriptor.js +30 -3
  7. package/dist/api/flow.d.ts +55 -0
  8. package/dist/api/flow.js +71 -0
  9. package/dist/api/gradient.d.ts +1 -53
  10. package/dist/api/gradient.js +2 -97
  11. package/dist/api/index.d.ts +2 -0
  12. package/dist/api/index.js +2 -0
  13. package/dist/api/layout.js +4 -3
  14. package/dist/api/structure.d.ts +1 -1
  15. package/dist/api/table.js +3 -1
  16. package/dist/api/text.d.ts +8 -8
  17. package/dist/api/text.js +6 -0
  18. package/dist/canvas/painter.d.ts +2 -2
  19. package/dist/canvas/painter.js +2 -2
  20. package/dist/common/color-input.d.ts +24 -0
  21. package/dist/common/color-input.js +278 -0
  22. package/dist/common/diagnostics.d.ts +9 -1
  23. package/dist/common/gradient.d.ts +53 -0
  24. package/dist/common/gradient.js +97 -0
  25. package/dist/elements/container-element.d.ts +3 -0
  26. package/dist/elements/container-element.js +17 -5
  27. package/dist/elements/image-element.d.ts +37 -26
  28. package/dist/elements/image-element.js +88 -50
  29. package/dist/elements/index.d.ts +3 -0
  30. package/dist/elements/index.js +3 -0
  31. package/dist/elements/layout/column-break-element.d.ts +17 -0
  32. package/dist/elements/layout/column-break-element.js +26 -0
  33. package/dist/elements/layout/deferred-element.d.ts +9 -1
  34. package/dist/elements/layout/deferred-element.js +18 -3
  35. package/dist/elements/layout/expanded-element.d.ts +4 -0
  36. package/dist/elements/layout/expanded-element.js +14 -1
  37. package/dist/elements/layout/flow-element.d.ts +109 -0
  38. package/dist/elements/layout/flow-element.js +317 -0
  39. package/dist/elements/layout/keep-together-element.js +14 -2
  40. package/dist/elements/layout/padding-element.d.ts +3 -0
  41. package/dist/elements/layout/padding-element.js +9 -2
  42. package/dist/elements/layout/page-builder-element.js +2 -2
  43. package/dist/elements/layout/positioned-element.d.ts +9 -2
  44. package/dist/elements/layout/positioned-element.js +6 -2
  45. package/dist/elements/layout/proxy-element.d.ts +4 -0
  46. package/dist/elements/layout/proxy-element.js +12 -0
  47. package/dist/elements/layout/region-element.d.ts +53 -0
  48. package/dist/elements/layout/region-element.js +109 -0
  49. package/dist/elements/layout/repeating-header-element.d.ts +3 -0
  50. package/dist/elements/layout/repeating-header-element.js +16 -1
  51. package/dist/elements/layout/rotated-element.d.ts +2 -0
  52. package/dist/elements/layout/rotated-element.js +8 -0
  53. package/dist/elements/pdf-element.d.ts +32 -3
  54. package/dist/elements/pdf-element.js +50 -1
  55. package/dist/elements/rectangle-element.d.ts +12 -6
  56. package/dist/elements/rectangle-element.js +31 -12
  57. package/dist/elements/row-element.d.ts +6 -12
  58. package/dist/elements/row-element.js +34 -4
  59. package/dist/elements/svg-element.js +4 -10
  60. package/dist/elements/text-element.d.ts +17 -1
  61. package/dist/elements/text-element.js +63 -40
  62. package/dist/ir/display-list.d.ts +6 -1
  63. package/dist/layout/box-constraints.d.ts +12 -0
  64. package/dist/layout/box-constraints.js +17 -0
  65. package/dist/layout/collect-images.js +8 -4
  66. package/dist/layout/flex-item.d.ts +2 -0
  67. package/dist/layout/flex-item.js +14 -2
  68. package/dist/layout/fragmentation.d.ts +11 -5
  69. package/dist/layout/fragmentation.js +27 -7
  70. package/dist/layout/layout-cache.d.ts +8 -8
  71. package/dist/layout/layout-cache.js +10 -7
  72. package/dist/platform/browser-fs.d.ts +1 -2
  73. package/dist/platform/browser-fs.js +3 -6
  74. package/dist/platform/node-fs.d.ts +0 -1
  75. package/dist/platform/node-fs.js +0 -4
  76. package/dist/renderer/column-break-renderer.d.ts +8 -0
  77. package/dist/renderer/column-break-renderer.js +15 -0
  78. package/dist/renderer/emoji-image.js +3 -1
  79. package/dist/renderer/flow-renderer.d.ts +16 -0
  80. package/dist/renderer/flow-renderer.js +21 -0
  81. package/dist/renderer/image-renderer.js +31 -54
  82. package/dist/renderer/index.d.ts +2 -0
  83. package/dist/renderer/index.js +2 -0
  84. package/dist/renderer/page-renderer.js +3 -4
  85. package/dist/renderer/pdf-backend.js +9 -2
  86. package/dist/renderer/pdf-document-renderer.js +2 -1
  87. package/dist/renderer/pdf-renderer.js +9 -0
  88. package/dist/renderer/positioned-renderer.d.ts +14 -2
  89. package/dist/renderer/positioned-renderer.js +40 -4
  90. package/dist/renderer/rectangle-renderer.d.ts +1 -1
  91. package/dist/renderer/rectangle-renderer.js +11 -6
  92. package/dist/renderer/rotated-renderer.js +3 -2
  93. package/dist/renderer/text-renderer.js +25 -28
  94. package/dist/svg/index.d.ts +1 -1
  95. package/dist/svg/index.js +26 -5
  96. package/dist/svg/style.js +1 -1
  97. package/dist/text/line-breaker.d.ts +23 -0
  98. package/dist/text/line-breaker.js +142 -65
  99. package/dist/text/line-metrics.d.ts +2 -0
  100. package/dist/text/line-metrics.js +5 -1
  101. package/dist/text/word-splitting.js +11 -5
  102. package/dist/utils/flex-layout.d.ts +3 -1
  103. package/dist/utils/flex-layout.js +141 -33
  104. package/dist/utils/image-helper.d.ts +59 -11
  105. package/dist/utils/image-helper.js +207 -80
  106. package/dist/utils/pdf-object-manager.d.ts +6 -3
  107. package/dist/utils/pdf-object-manager.js +31 -7
  108. package/dist/utils/renderer-registry.d.ts +6 -0
  109. package/dist/utils/renderer-registry.js +1 -1
  110. package/dist/utils/ttf-parser.js +3 -2
  111. package/package.json +9 -2
@@ -35,18 +35,6 @@ interface RowElementParams extends WithChildren {
35
35
  /** Start everything after this row on a fresh page (CSS `break-after: page`). */
36
36
  breakAfter?: boolean;
37
37
  }
38
- /**
39
- * Horizontal stack: the mirror of `ContainerElement` (Column). Children are laid out
40
- * left-to-right via the shared `FlexLayoutHelper` on the horizontal axis; fixed children
41
- * take their natural width, `ExpandedElement`/Spacer children split the leftover width by
42
- * `flex`, and `gap` is inserted between them. The row fills the width it is offered and
43
- * shrink-wraps its height to the tallest child (unless a height is forced on it).
44
- *
45
- * Cross/main alignment is the next foundation slice; today children sit at the top-left
46
- * (cross start, main start). The row is atomic w.r.t. pagination - it reflows whole if it
47
- * does not fit (handled by the parent's `packChildren`); synchronized cell splitting is a
48
- * Grid/Table concern.
49
- */
50
38
  export declare class RowElement extends SizedPDFElement {
51
39
  private children;
52
40
  private gap;
@@ -62,6 +50,7 @@ export declare class RowElement extends SizedPDFElement {
62
50
  breaksBefore(): boolean;
63
51
  breaksAfter(): boolean;
64
52
  relativeSizeFactor(horizontal: boolean): number | undefined;
53
+ maxExtent(horizontal: boolean): number;
65
54
  /** The mirror of `ContainerElement`: a Row's OWN flex child needs a bounded width, and the need of any
66
55
  * descendant propagates on both axes (see the Column for the reasoning). */
67
56
  needsBoundedMain(horizontal: boolean): boolean;
@@ -78,6 +67,11 @@ export declare class RowElement extends SizedPDFElement {
78
67
  private reportIgnoredBreak;
79
68
  /** The one sizing path: `layout` places the children (relative to this box), `measure` only sizes them. */
80
69
  private sizeWith;
70
+ private percentPaddingChild?;
71
+ /** A child's `%` padding resolves against this Row's width (CSS: the containing block), which the Row
72
+ * never offers - its children get their natural size - so it is told. Only when a child uses it,
73
+ * since every key below would otherwise change with the Row's width. */
74
+ private childContext;
81
75
  layoutChildren(): readonly PDFElement[];
82
76
  getProps(): {
83
77
  x: number;
@@ -1,9 +1,9 @@
1
- import { flexibleOf } from "../layout/flex-item.js";
1
+ import { flexibleOf, hasPercentPaddingItem } from "../layout/flex-item.js";
2
2
  import { extentSpecs, resolveSize, } from "../layout/box-constraints.js";
3
3
  import { FlexLayoutHelper, HORIZONTAL_AXIS, } from "../utils/flex-layout.js";
4
4
  import { stackMinIntrinsic, withDeclaredExtent } from "../layout/min-intrinsic.js";
5
5
  import { diagnose, pageOf } from "../common/diagnostics.js";
6
- import { SizedPDFElement } from "./pdf-element.js";
6
+ import { SizedPDFElement, withPaddingBase, } from "./pdf-element.js";
7
7
  /**
8
8
  * Horizontal stack: the mirror of `ContainerElement` (Column). Children are laid out
9
9
  * left-to-right via the shared `FlexLayoutHelper` on the horizontal axis; fixed children
@@ -16,6 +16,8 @@ import { SizedPDFElement } from "./pdf-element.js";
16
16
  * does not fit (handled by the parent's `packChildren`); synchronized cell splitting is a
17
17
  * Grid/Table concern.
18
18
  */
19
+ /** How far a line may run past its box before it is reported, in points (floating-point slack). */
20
+ const WIDE_EPSILON = 0.01;
19
21
  export class RowElement extends SizedPDFElement {
20
22
  constructor({ children, gap, main, cross, reverse, wrap, alignContent, width, height, breakBefore, breakAfter, ...sizing }) {
21
23
  super({ x: 0, y: 0 });
@@ -39,6 +41,9 @@ export class RowElement extends SizedPDFElement {
39
41
  relativeSizeFactor(horizontal) {
40
42
  return horizontal ? this.requested.widthFactor : this.requested.heightFactor;
41
43
  }
44
+ maxExtent(horizontal) {
45
+ return (horizontal ? this.requested.maxWidth : this.requested.maxHeight) ?? Infinity;
46
+ }
42
47
  /** The mirror of `ContainerElement`: a Row's OWN flex child needs a bounded width, and the need of any
43
48
  * descendant propagates on both axes (see the Column for the reasoning). */
44
49
  needsBoundedMain(horizontal) {
@@ -124,7 +129,10 @@ export class RowElement extends SizedPDFElement {
124
129
  // Horizontal stack: main = width (children fill it), cross = height (children can stretch to it).
125
130
  const mainAvail = boundedWidth ?? Infinity;
126
131
  const crossAvail = boundedHeight ?? Infinity;
127
- let result = { mainUsed: 0, crossUsed: 0 };
132
+ let result = {
133
+ mainUsed: 0,
134
+ crossUsed: 0,
135
+ };
128
136
  if (this.children.length > 0) {
129
137
  result = FlexLayoutHelper.layout(this.children, HORIZONTAL_AXIS, mainAvail, crossAvail, {
130
138
  gap: this.gap,
@@ -133,10 +141,32 @@ export class RowElement extends SizedPDFElement {
133
141
  reverse: this.reverse,
134
142
  wrap: this.wrap,
135
143
  alignContent: this.alignContent,
136
- }, ctx, mode);
144
+ }, this.childContext(ctx, mainAvail), mode);
145
+ }
146
+ const used = result.widestLine ?? result.mainUsed;
147
+ if (mode === "layout" && used > mainAvail + WIDE_EPSILON) {
148
+ diagnose(ctx.diagnostics, {
149
+ code: "overflow-wide",
150
+ severity: "warning",
151
+ message: `A Row's children need ${Math.round(used)}pt but it is ` +
152
+ `${Math.round(mainAvail)}pt wide; they are drawn past its edge. Let them wrap ` +
153
+ '(`breakWord`, `wrap`), make room, or clip with a Box\'s `overflow: "hidden"`.',
154
+ about: this,
155
+ page: pageOf(ctx),
156
+ fallback: "silent",
157
+ });
137
158
  }
138
159
  return { width: boundedWidth ?? result.mainUsed, height: boundedHeight ?? result.crossUsed };
139
160
  }
161
+ /** A child's `%` padding resolves against this Row's width (CSS: the containing block), which the Row
162
+ * never offers - its children get their natural size - so it is told. Only when a child uses it,
163
+ * since every key below would otherwise change with the Row's width. */
164
+ childContext(ctx, mainAvail) {
165
+ this.percentPaddingChild ?? (this.percentPaddingChild = this.children.some(hasPercentPaddingItem));
166
+ if (!this.percentPaddingChild)
167
+ return ctx;
168
+ return withPaddingBase(ctx, Number.isFinite(mainAvail) ? mainAvail : undefined);
169
+ }
140
170
  layoutChildren() {
141
171
  return this.children;
142
172
  }
@@ -1,6 +1,6 @@
1
1
  import { readFileBytes } from "../platform/node-fs.js";
2
2
  import { SvgParseError, svgSize, svgToIr } from "../svg/index.js";
3
- import { extentSpecs, resolveSize, } from "../layout/box-constraints.js";
3
+ import { extentSpecs, resolveSize, sizeAtOwnRatio, } from "../layout/box-constraints.js";
4
4
  import { SizedPDFElement } from "./pdf-element.js";
5
5
  import { IMAGE_FETCH_LIMITS, ImageUrlError, isImageUrl } from "./image-element.js";
6
6
  import { fetchBytes } from "../utils/fetch-bytes.js";
@@ -144,18 +144,12 @@ export class SvgElement extends SizedPDFElement {
144
144
  `column), where its size cannot be read before it is placed - give it both a width and a ` +
145
145
  `height, or an aspectRatio`);
146
146
  }
147
- // One pinned axis derives the other from the drawing's own ratio (CSS `width: 120; height: auto`).
148
- // A bitmap needs a decode pass before it can do this; an SVG knows it from its viewBox.
147
+ // Its own ratio fills an open axis, as for a bitmap - which needs a decode pass for it; an SVG
148
+ // knows it from its viewBox. Unsized, it is its own size scaled down to fit: a 24pt icon stays 24pt.
149
149
  const intrinsic = this.intrinsic ?? { width: 0, height: 0 };
150
150
  if (this.sizing.aspectRatio === undefined && intrinsic.width > 0 && intrinsic.height > 0) {
151
- const ratio = intrinsic.width / intrinsic.height;
152
- if (w !== undefined && h === undefined)
153
- h = bounds.constrainHeight(w / ratio);
154
- else if (h !== undefined && w === undefined)
155
- w = bounds.constrainWidth(h * ratio);
151
+ ({ width: w, height: h } = sizeAtOwnRatio(w, h, intrinsic, bounds));
156
152
  }
157
- // Nothing pinned: draw at the intrinsic size, as an unsized `<img>` does - never fill the region,
158
- // which would blow a 24pt icon up to the whole page.
159
153
  return {
160
154
  width: w ?? bounds.constrainWidth(intrinsic.width),
161
155
  height: h ?? bounds.constrainHeight(intrinsic.height),
@@ -22,8 +22,16 @@ export interface TextSegment {
22
22
  /** Unset inherits the Text's own setting; `true`/`false` overrides it for this run only. */
23
23
  underline?: boolean;
24
24
  strikethrough?: boolean;
25
+ skipInk?: boolean;
25
26
  /** Extra space after every glyph, in points; unset inherits the Text's own value. */
26
27
  letterSpacing?: number;
28
+ textTransform?: TextTransform;
29
+ wordSpacing?: number;
30
+ ligatures?: boolean;
31
+ /** How a word wider than the box splits, for the characters of THIS run (Chrome: a break after a
32
+ * character is allowed by that character's own `overflow-wrap`). */
33
+ breakWord?: boolean;
34
+ hyphenate?: Hyphenator;
27
35
  /**
28
36
  * CSS `vertical-align`, for a footnote marker or a formula index. Shifts THIS run's baseline;
29
37
  * it does not resize the text, and it does not change the line's height - set `fontSize` yourself
@@ -119,6 +127,7 @@ export declare class TextElement extends SizedPDFElement implements Fragmentable
119
127
  private direction;
120
128
  private fontFallback;
121
129
  private resolved?;
130
+ private broken;
122
131
  private textTransform;
123
132
  private wordSpacing;
124
133
  private textIndent;
@@ -187,7 +196,14 @@ export declare class TextElement extends SizedPDFElement implements Fragmentable
187
196
  * the lines (a box that is too short cuts them, it does not reflow them). */
188
197
  protected sizeInputs(constraints: BoxConstraints): BoxConstraints;
189
198
  protected computeSize(constraints: BoxConstraints, ctx: LayoutContext): Size;
190
- private measuredHeight;
199
+ /**
200
+ * The one sizing path, for measuring and layout alike: a bounded width wraps to it (in a Column), an
201
+ * unbounded one takes the natural single-line width (in a Row). The lines are what layout checks.
202
+ * The style is resolved here, as drawing reads it.
203
+ */
204
+ /** The text broken at `width`: once per context and width, for measuring, layout and splitting. */
205
+ private linesAt;
206
+ private sizeWith;
191
207
  /** The unwrapped single-line width of the content (used when width is unbounded, e.g. inside a Row).
192
208
  * Must match the breaker's one-line sum BIT for bit - it adds `width + (space + word)`, and the same
193
209
  * numbers grouped differently drift by a sub-ULP, enough to wrap a text inside its own natural-width
@@ -3,14 +3,15 @@ import { BoxConstraints } from "../layout/box-constraints.js";
3
3
  import { applyTextTransform } from "../text/text-style.js";
4
4
  import { splitByFont } from "../text/font-fallback.js";
5
5
  import { runAdvance } from "../text/advance.js";
6
- import { singleLineWidth, MAX_SPACE_SHRINK, breakSegmentsIntoLines, } from "../text/line-breaker.js";
6
+ import { runStyleOf, singleLineWidth, MAX_SPACE_SHRINK, breakSegmentsIntoLines, } from "../text/line-breaker.js";
7
7
  import { hyphenPieces } from "../text/hyphen-breaks.js";
8
8
  import { adjustForOrphansWidows, DEFAULT_ORPHANS, DEFAULT_WIDOWS, } from "../text/orphans-widows.js";
9
- import { lineBoxForSegmentLine, textHeight } from "../text/line-metrics.js";
9
+ import { lineBoxForSegmentLine, linesHeight } from "../text/line-metrics.js";
10
10
  import { HorizontalAlignment, SizedPDFElement } from "./pdf-element.js";
11
11
  import { normalizeContent } from "../text/whitespace.js";
12
12
  import { coverText } from "../text/glyph-coverage.js";
13
- import { pageOf } from "../common/diagnostics.js";
13
+ import { diagnose, pageOf } from "../common/diagnostics.js";
14
+ import { BuiltFromContext } from "../layout/layout-cache.js";
14
15
  /** How far a run sits off the baseline, as a fraction of its font size (up is positive). */
15
16
  export const VERTICAL_TEXT_SHIFT = {
16
17
  baseline: 0,
@@ -31,6 +32,7 @@ function finitePoints(value, name) {
31
32
  export class TextElement extends SizedPDFElement {
32
33
  constructor({ fontSize, content, fontFamily, fontStyle, color, textAlignment, maxLines, orphans, widows, overflow = "clip", lineHeight, underline, strikethrough, skipInk, letterSpacing, direction, fontFallback, textTransform, wordSpacing, textIndent, breakWord, hyphenate, ligatures, role, shownLines, }) {
33
34
  super({ x: 0, y: 0 });
35
+ this.broken = new BuiltFromContext(() => false);
34
36
  /** What `covered` had to remove, reported where the text is laid out on its page (see
35
37
  * `calculateLayout`). A split paragraph's first piece carries its parent's, as `lostBefore`. */
36
38
  this.lostGlyphs = [];
@@ -140,14 +142,15 @@ export class TextElement extends SizedPDFElement {
140
142
  recased() {
141
143
  // A string is one span: from here on, measuring, breaking and drawing know only spans.
142
144
  const spans = typeof this.content === "string" ? [{ content: this.content }] : this.content;
143
- if (this.textTransform === "none")
145
+ const transformOf = (seg) => seg.textTransform ?? this.textTransform;
146
+ if (spans.every((seg) => transformOf(seg) === "none"))
144
147
  return spans;
145
148
  // The capitalisation state runs THROUGH the spans: `span("hel") + span("lo")` is one word.
146
149
  let atWordStart = true;
147
150
  return spans.map((seg) => {
148
- const done = applyTextTransform(seg.content, this.textTransform, atWordStart);
151
+ const done = applyTextTransform(seg.content, transformOf(seg), atWordStart);
149
152
  atWordStart = done.atWordStart;
150
- return { ...seg, content: done.text };
153
+ return done.text === seg.content ? seg : { ...seg, content: done.text };
151
154
  });
152
155
  }
153
156
  lineOptions() {
@@ -194,7 +197,7 @@ export class TextElement extends SizedPDFElement {
194
197
  // would cut a paragraph at a line count it is never drawn with. Each line's height comes from
195
198
  // the fonts on it, so lines are packed by their cumulative height.
196
199
  const defaults = this.segmentDefaults();
197
- const lines = breakSegmentsIntoLines(content, defaults, width, ctx.metrics, this.maxLines, this.overflow);
200
+ const lines = this.linesAt(width, ctx);
198
201
  let used = 0;
199
202
  let packed = 0;
200
203
  for (const line of lines) {
@@ -251,7 +254,8 @@ export class TextElement extends SizedPDFElement {
251
254
  // the same width reproduces exactly those lines (greedy is deterministic).
252
255
  cloneWithContent(content, indent = this.textIndent, piece = {}) {
253
256
  return new TextElement({
254
- content,
257
+ // Cut from `display()`, already recased - a span's own transform too.
258
+ content: content.map((seg) => (seg.textTransform ?? "none") === "none" ? seg : { ...seg, textTransform: "none" }),
255
259
  fontSize: this.fontSize,
256
260
  fontFamily: this.fontFamily,
257
261
  fontStyle: this.fontStyle,
@@ -271,8 +275,8 @@ export class TextElement extends SizedPDFElement {
271
275
  textIndent: indent,
272
276
  role: this.role,
273
277
  fontFallback: this.fontFallback,
274
- // The content is cut from `display()`, already recased: `capitalize` again would capitalise
275
- // the half of a word continued on the next page.
278
+ // Already recased: `capitalize` again would capitalise the half of a word continued on the next
279
+ // page.
276
280
  textTransform: "none",
277
281
  breakWord: this.breakWord,
278
282
  hyphenate: this.hyphenate,
@@ -281,12 +285,7 @@ export class TextElement extends SizedPDFElement {
281
285
  }).asPieceOf(this); // a wrapped remainder is the SAME logical paragraph (one P across pages)
282
286
  }
283
287
  calculateLayout(constraints, offset, ctx) {
284
- // Bounded width (e.g. inside a Column) wraps to that width; an unbounded width
285
- // (e.g. inside a Row) means the text takes its natural single-line width and does
286
- // not wrap. Sized by `measure`, so a measured text and a laid-out one cannot differ. The style is
287
- // resolved here too: a size from the cache skips `computeSize`, and drawing reads the style.
288
- this.resolveStyle(ctx);
289
- const size = this.measure(constraints, ctx);
288
+ const { size, lines } = this.sizeWith(constraints, ctx);
290
289
  this.x = offset.x;
291
290
  this.y = offset.y;
292
291
  this.resolved = this.display(ctx.metrics);
@@ -295,6 +294,20 @@ export class TextElement extends SizedPDFElement {
295
294
  ctx.metrics.reportMissingGlyph?.(lost, this, pageOf(ctx));
296
295
  this.width = size.width;
297
296
  this.height = size.height;
297
+ // A line with a piece that could not break is drawn past the edge - say so.
298
+ const overrun = Math.max(0, ...lines.map((line) => line.overrun));
299
+ if (constraints.hasBoundedWidth && overrun > 0.01) {
300
+ diagnose(ctx.diagnostics, {
301
+ code: "overflow-wide",
302
+ severity: "warning",
303
+ message: `A line of this Text runs ${Math.round(overrun)}pt past its ${Math.round(size.width)}pt; ` +
304
+ "a word in it cannot break. Let it break (`breakWord`, `hyphenate`), make room, or clip " +
305
+ 'with a Box\'s `overflow: "hidden"`.',
306
+ about: this,
307
+ page: pageOf(ctx),
308
+ fallback: "silent",
309
+ });
310
+ }
298
311
  // Top-left coordinates (y = top of the text box). The baseline offset and the
299
312
  // Y-flip are applied downstream (the line-builder positions baselines, the seam
300
313
  // flips to PDF), so the element stays coordinate-system-blind.
@@ -311,29 +324,42 @@ export class TextElement extends SizedPDFElement {
311
324
  return BoxConstraints.loose(constraints.maxWidth, Infinity);
312
325
  }
313
326
  computeSize(constraints, ctx) {
327
+ return this.sizeWith(constraints, ctx).size;
328
+ }
329
+ /**
330
+ * The one sizing path, for measuring and layout alike: a bounded width wraps to it (in a Column), an
331
+ * unbounded one takes the natural single-line width (in a Row). The lines are what layout checks.
332
+ * The style is resolved here, as drawing reads it.
333
+ */
334
+ /** The text broken at `width`: once per context and width, for measuring, layout and splitting. */
335
+ linesAt(width, ctx) {
336
+ return this.broken.get(ctx, width, () => breakSegmentsIntoLines(this.display(ctx.metrics), this.segmentDefaults(), width, ctx.metrics, this.maxLines, this.overflow));
337
+ }
338
+ sizeWith(constraints, ctx) {
314
339
  this.resolveStyle(ctx);
315
- const content = this.display(ctx.metrics);
316
340
  const width = constraints.hasBoundedWidth
317
341
  ? constraints.maxWidth
318
342
  : this.naturalWidth(ctx.metrics);
319
- return { width, height: this.measuredHeight(ctx.metrics, width, content) };
320
- }
321
- measuredHeight(metrics, width, content) {
322
- return textHeight(content, this.segmentDefaults(), width, metrics, this.maxLines, this.overflow, this.lineHeight);
343
+ const defaults = this.segmentDefaults();
344
+ const lines = this.linesAt(width, ctx);
345
+ return {
346
+ size: { width, height: linesHeight(lines, defaults, ctx.metrics, this.lineHeight) },
347
+ lines,
348
+ };
323
349
  }
324
350
  /** The unwrapped single-line width of the content (used when width is unbounded, e.g. inside a Row).
325
351
  * Must match the breaker's one-line sum BIT for bit - it adds `width + (space + word)`, and the same
326
352
  * numbers grouped differently drift by a sub-ULP, enough to wrap a text inside its own natural-width
327
353
  * box. `singleLineWidth` is that sum. */
328
354
  naturalWidth(metrics) {
329
- // The breaker's own sum, to the last bit - see `singleLineWidth`.
330
- const oneLine = (text, family, size, style, letterSpacing) => singleLineWidth(text,
331
- // `ligatures` is part of the font: it decides which glyphs are drawn, hence the width.
332
- { fontFamily: family, fontSize: size, fontStyle: style, ligatures: this.ligatures }, metrics, letterSpacing, this.wordSpacing);
355
+ const defaults = this.segmentDefaults();
333
356
  // The FIRST line starts `textIndent` in, so an unbounded box has to be that much wider.
334
357
  const indent = Math.max(0, this.textIndent);
335
- return this.display(metrics).reduce((sum, seg) => sum +
336
- oneLine(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontSize ?? this.fontSize, seg.fontStyle ?? this.fontStyle, seg.letterSpacing ?? this.letterSpacing), indent);
358
+ return this.display(metrics).reduce((sum, seg) => {
359
+ // The breaker's own sum, to the last bit - see `singleLineWidth`.
360
+ const run = runStyleOf(seg, defaults);
361
+ return (sum + singleLineWidth(seg.content, run.font, metrics, run.letterSpacing, run.wordSpacing));
362
+ }, indent);
337
363
  }
338
364
  /**
339
365
  * The widest piece of this text that cannot be broken - CSS's `min-content` width. A shrinking flex
@@ -350,25 +376,22 @@ export class TextElement extends SizedPDFElement {
350
376
  return 0;
351
377
  this.resolveStyle(ctx);
352
378
  const metrics = ctx.metrics;
353
- const widest = (text, family, size, style, letterSpacing) => {
354
- // `ligatures` belongs to the FONT: it decides which glyphs are drawn and therefore how wide the
355
- // piece is. Leaving it out here would measure a floor the drawing never matches.
356
- const font = {
357
- fontFamily: family,
358
- fontSize: size,
359
- fontStyle: style,
360
- ligatures: this.ligatures,
361
- };
379
+ // The run style is the FONT a piece is measured in: `ligatures` decides which glyphs are drawn and
380
+ // therefore how wide it is. Leaving it out would measure a floor the drawing never matches.
381
+ const widest = (text, run) => {
362
382
  // Split on the same boundaries the breaker uses - a hard break ends a line too, and so may a
363
- // hyphen inside a word.
364
- const pieces = this.breakWord || this.hyphenate ? [...text] : text.split(/[ \n]/).flatMap(hyphenPieces);
365
- return pieces.reduce((max, piece) => Math.max(max, runAdvance(metrics, piece, font, letterSpacing)), 0);
383
+ // hyphen inside a word. A run that may split its words may split them anywhere.
384
+ const pieces = run.splitting.breakWord || run.splitting.hyphenate
385
+ ? [...text]
386
+ : text.split(/[ \n]/).flatMap(hyphenPieces);
387
+ return pieces.reduce((max, piece) => Math.max(max, runAdvance(metrics, piece, run.font, run.letterSpacing)), 0);
366
388
  };
389
+ const defaults = this.segmentDefaults();
367
390
  const content = this.display(metrics);
368
391
  // Segments join without a space, so a word may straddle two of them; taking the widest piece per
369
392
  // segment can therefore UNDER-estimate. That errs towards squeezing a little too far, which lands
370
393
  // back on the old overflow behaviour rather than on something new.
371
- return content.reduce((max, seg) => Math.max(max, widest(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontSize ?? this.fontSize, seg.fontStyle ?? this.fontStyle, seg.letterSpacing ?? this.letterSpacing)), 0);
394
+ return content.reduce((max, seg) => Math.max(max, widest(seg.content, runStyleOf(seg, defaults))), 0);
372
395
  }
373
396
  getProps() {
374
397
  return {
@@ -1,3 +1,4 @@
1
+ import type { ImageColor, Orientation } from "../utils/image-helper.js";
1
2
  import { Color } from "../common/color.js";
2
3
  import { FontStyle } from "../utils/pdf-object-manager.js";
3
4
  import type { FormFieldSpec, FieldStyle } from "../forms/field.js";
@@ -62,7 +63,7 @@ export interface Rect {
62
63
  y: number;
63
64
  width: number;
64
65
  height: number;
65
- /** Solid colour, or a gradient whose anchor points are in absolute page coordinates. */
66
+ /** Solid color, or a gradient whose anchor points are in absolute page coordinates. */
66
67
  fill?: Color | Gradient;
67
68
  stroke?: Color;
68
69
  strokeWidth: number;
@@ -94,6 +95,10 @@ export interface Image {
94
95
  imageType: string;
95
96
  /** Flate-compressed DeviceGray alpha channel for a transparent PNG, embedded as the XObject's /SMask. */
96
97
  smask?: string;
98
+ /** What the samples mean (a JPEG keeps the space it was saved in); absent = RGB. */
99
+ color?: ImageColor;
100
+ /** How the stored pixels turn upright inside the placement box (EXIF); absent = as stored. */
101
+ orientation?: Orientation;
97
102
  /** cover/contain fits clip the placement to the element's original frame. */
98
103
  clip?: {
99
104
  x: number;
@@ -110,6 +110,18 @@ export declare function resolveSize(width: ExtentSpec, height: ExtentSpec, aspec
110
110
  height?: number;
111
111
  constraints: BoxConstraints;
112
112
  };
113
+ /**
114
+ * The open axes of a box that has its own size (an image, an SVG), after `resolveSize`: one pinned axis
115
+ * derives the other from the own ratio; none pinned is the own size, scaled down to `bounds` and never
116
+ * up - the cap is what keeps it on the page.
117
+ */
118
+ export declare function sizeAtOwnRatio(width: number | undefined, height: number | undefined, own: {
119
+ width: number;
120
+ height: number;
121
+ }, bounds: BoxConstraints): {
122
+ width?: number;
123
+ height?: number;
124
+ };
113
125
  /** The size an element resolves to and returns UP the tree. */
114
126
  export interface Size {
115
127
  width: number;
@@ -167,3 +167,20 @@ export function resolveSize(width, height, aspectRatio, constraints) {
167
167
  constraints: bounds,
168
168
  };
169
169
  }
170
+ /**
171
+ * The open axes of a box that has its own size (an image, an SVG), after `resolveSize`: one pinned axis
172
+ * derives the other from the own ratio; none pinned is the own size, scaled down to `bounds` and never
173
+ * up - the cap is what keeps it on the page.
174
+ */
175
+ export function sizeAtOwnRatio(width, height, own, bounds) {
176
+ const ratio = own.width / own.height;
177
+ if (width === undefined && height === undefined) {
178
+ // The width clamp does the width; a height cap is turned into a width at the ratio.
179
+ width = bounds.constrainWidth(Math.min(own.width, bounds.maxHeight * ratio));
180
+ }
181
+ if (width !== undefined && height === undefined)
182
+ height = bounds.constrainHeight(width / ratio);
183
+ else if (height !== undefined && width === undefined)
184
+ width = bounds.constrainWidth(height * ratio);
185
+ return { width, height };
186
+ }
@@ -19,10 +19,10 @@ export function collectImageElements(root) {
19
19
  visit(c);
20
20
  else if (hasChildProp(props))
21
21
  visit(props.child);
22
- // Wrappers that hold a subtree outside `children`/`child`: a Page's header/footer and a
23
- // RepeatingHeaderElement's `body` (a Table with a header wraps its rows there). A
24
- // DeferredElement's `composed` is null until layout runs, so it cannot be reached from this
25
- // pre-layout pass; an image inside a deferred subtree just skips aspect pre-resolution.
22
+ // Wrappers that hold a subtree outside `children`/`child`: a Page's header/footer, a
23
+ // RepeatingHeaderElement's `body` (a Table with a header wraps its rows there), and the `sources` a
24
+ // DeferredElement builds from. What a PageBuilder builds does not exist yet; an image there that
25
+ // cannot tell its size without waiting is reported by its layout (`image-unsized`).
26
26
  const w = props;
27
27
  if (w.header instanceof PDFElement)
28
28
  visit(w.header);
@@ -30,6 +30,10 @@ export function collectImageElements(root) {
30
30
  visit(w.footer);
31
31
  if (w.body instanceof PDFElement)
32
32
  visit(w.body);
33
+ if (Array.isArray(w.sources))
34
+ for (const s of w.sources)
35
+ if (s instanceof PDFElement)
36
+ visit(s);
33
37
  };
34
38
  visit(root);
35
39
  return out;
@@ -5,5 +5,7 @@ export declare function flexibleOf(child: PDFElement): FlexiblePDFElement | unde
5
5
  export declare function alignSelfOf(child: PDFElement): CrossAlign | undefined;
6
6
  /** CSS `order`, 0 when unset. */
7
7
  export declare function orderOf(child: PDFElement): number;
8
+ /** The child is a `Box` with percentage padding. */
9
+ export declare function hasPercentPaddingItem(child: PDFElement): boolean;
8
10
  /** CSS `flex-shrink`, 1 when unset (the CSS default). */
9
11
  export declare function flexShrinkOf(child: PDFElement): number;
@@ -5,10 +5,15 @@ import { FlexiblePDFElement } from "../elements/pdf-element.js";
5
5
  * `display: contents` is: an `Expanded`, `alignSelf`, `order` or `flexShrink` inside it acts as if the
6
6
  * wrapper were not there. A value set on the wrapper itself wins over one set inside it.
7
7
  */
8
- /** The child, then each element it is transparent for, outermost first. */
8
+ /**
9
+ * The child, then each element it is transparent for, outermost first. An `Expanded` is one of them:
10
+ * `Expanded(child)` is `flex: 1` on the child, so the child's `alignSelf` and `order` are the item's.
11
+ */
9
12
  function* itemChain(child) {
10
- for (let el = child; el; el = el.proxied())
13
+ for (let el = child; el;) {
11
14
  yield el;
15
+ el = el.proxied() ?? (el instanceof FlexiblePDFElement ? el.layoutChildren()[0] : undefined);
16
+ }
12
17
  }
13
18
  /** The `Expanded`/`Spacer` that makes this child flexible, if any. */
14
19
  export function flexibleOf(child) {
@@ -30,6 +35,13 @@ export function orderOf(child) {
30
35
  return el.order;
31
36
  return 0;
32
37
  }
38
+ /** The child is a `Box` with percentage padding. */
39
+ export function hasPercentPaddingItem(child) {
40
+ for (const el of itemChain(child))
41
+ if (el.hasPercentPadding())
42
+ return true;
43
+ return false;
44
+ }
33
45
  /** CSS `flex-shrink`, 1 when unset (the CSS default). */
34
46
  export function flexShrinkOf(child) {
35
47
  for (const el of itemChain(child))
@@ -9,13 +9,15 @@ import { LayoutContext, PDFElement } from "../elements/pdf-element.js";
9
9
  * rest, so no information flows back UP after a break. This one-way flow is what keeps
10
10
  * fragmentation terminating (no overflow-cascade fixpoint).
11
11
  */
12
+ export type ForcedBreak = boolean | "column";
12
13
  export interface FragmentResult {
13
14
  fitted: PDFElement | null;
14
15
  remainder: PDFElement | null;
15
- /** The split ended at a FORCED page break (a `PageBreak`), not because the region filled up. The
16
- * remainder must start a fresh page, and an enclosing flow must stop packing too - so this bubbles
17
- * up through nested `fragment()` calls. Absent/false for an ordinary height-driven break. */
18
- forceBreak?: boolean;
16
+ /** The split ended at a FORCED break, not because the region filled up: `true` for a page break (the
17
+ * remainder starts a fresh page), `"column"` for a `ColumnBreak` in a Flow's Region (it starts the
18
+ * next Region; the Flow consumes it). An enclosing flow stops packing too - so this bubbles up
19
+ * through nested `fragment()` calls. Absent/false for an ordinary height-driven break. */
20
+ forceBreak?: ForcedBreak;
19
21
  }
20
22
  /**
21
23
  * An element that knows how to split itself across regions. Width is already finalized
@@ -40,6 +42,8 @@ export type OverflowPolicy = "error" | "warn" | "ignore";
40
42
  */
41
43
  export declare function insetContext(ctx: LayoutContext, chrome: number): LayoutContext;
42
44
  export declare function reportOverflow(child: PDFElement, childHeight: number, maxHeight: number, ctx: LayoutContext): void;
45
+ /** Content cut off at the bottom, under the author's `onOverflow` policy. */
46
+ export declare function reportClipped(about: PDFElement, detail: string, ctx: LayoutContext): void;
43
47
  /**
44
48
  * True if packing `children` will hit a forced cut INSIDE them: a child that IS a break, contains one
45
49
  * deeper in its subtree, or asks to break before/after itself. A flow container reports this from
@@ -48,6 +52,8 @@ export declare function reportOverflow(child: PDFElement, childHeight: number, m
48
52
  * (`breaksBefore`), so it moves whole - which is what a `keepTogether` around it relies on.
49
53
  */
50
54
  export declare function childrenForceBreak(children: PDFElement[]): boolean;
55
+ /** The packer kept every child as it was - none came back as a piece of itself (a sized Flow). */
56
+ export declare function packedUnchanged(children: PDFElement[], fitted: PDFElement[]): boolean;
51
57
  /**
52
58
  * Packs a vertical stack of children into `maxHeight`, in order. Children are measured
53
59
  * against `width` (unbounded height) and added until one would overflow; that straddling
@@ -65,5 +71,5 @@ export declare function childrenForceBreak(children: PDFElement[]): boolean;
65
71
  export declare function packChildren(children: PDFElement[], maxHeight: number, width: number, ctx: LayoutContext, gap?: number): {
66
72
  fitted: PDFElement[];
67
73
  remainder: PDFElement[];
68
- forceBreak: boolean;
74
+ forceBreak: ForcedBreak;
69
75
  };