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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/dist/api/color.d.ts +1 -24
  2. package/dist/api/color.js +2 -278
  3. package/dist/api/content.d.ts +10 -5
  4. package/dist/api/content.js +29 -13
  5. package/dist/api/descriptor.js +30 -3
  6. package/dist/api/flow.d.ts +55 -0
  7. package/dist/api/flow.js +71 -0
  8. package/dist/api/font-url.d.ts +1 -2
  9. package/dist/api/font-url.js +8 -74
  10. package/dist/api/forms.d.ts +3 -0
  11. package/dist/api/forms.js +3 -0
  12. package/dist/api/gradient.d.ts +1 -53
  13. package/dist/api/gradient.js +2 -97
  14. package/dist/api/index.d.ts +4 -0
  15. package/dist/api/index.js +3 -0
  16. package/dist/api/layout.d.ts +6 -0
  17. package/dist/api/layout.js +9 -3
  18. package/dist/api/page-builder.d.ts +4 -0
  19. package/dist/api/page-builder.js +4 -0
  20. package/dist/api/structure.d.ts +10 -2
  21. package/dist/api/structure.js +9 -6
  22. package/dist/api/table.d.ts +5 -3
  23. package/dist/api/table.js +101 -22
  24. package/dist/api/text.d.ts +8 -8
  25. package/dist/api/text.js +6 -0
  26. package/dist/canvas/painter.d.ts +2 -2
  27. package/dist/canvas/painter.js +2 -2
  28. package/dist/common/color-input.d.ts +24 -0
  29. package/dist/common/color-input.js +278 -0
  30. package/dist/common/diagnostics.d.ts +61 -0
  31. package/dist/common/diagnostics.js +77 -0
  32. package/dist/common/gradient.d.ts +53 -0
  33. package/dist/common/gradient.js +97 -0
  34. package/dist/edit/acroform-reader.js +7 -7
  35. package/dist/edit/appearance.js +3 -3
  36. package/dist/edit/attachments.d.ts +52 -0
  37. package/dist/edit/attachments.js +178 -0
  38. package/dist/edit/document.d.ts +38 -8
  39. package/dist/edit/document.js +208 -51
  40. package/dist/edit/fill.js +8 -23
  41. package/dist/edit/filters.d.ts +15 -0
  42. package/dist/edit/filters.js +157 -0
  43. package/dist/edit/index.d.ts +4 -1
  44. package/dist/edit/index.js +3 -1
  45. package/dist/edit/objects.d.ts +3 -4
  46. package/dist/edit/objects.js +5 -18
  47. package/dist/elements/canvas-element.d.ts +3 -0
  48. package/dist/elements/canvas-element.js +14 -3
  49. package/dist/elements/container-element.d.ts +8 -1
  50. package/dist/elements/container-element.js +49 -17
  51. package/dist/elements/forms/checkbox-element.d.ts +2 -0
  52. package/dist/elements/forms/checkbox-element.js +12 -3
  53. package/dist/elements/forms/choice-element.d.ts +6 -1
  54. package/dist/elements/forms/choice-element.js +17 -6
  55. package/dist/elements/forms/field-font.d.ts +8 -0
  56. package/dist/elements/forms/field-font.js +5 -0
  57. package/dist/elements/forms/push-button-element.d.ts +6 -1
  58. package/dist/elements/forms/push-button-element.js +16 -4
  59. package/dist/elements/forms/radio-element.d.ts +2 -0
  60. package/dist/elements/forms/radio-element.js +12 -3
  61. package/dist/elements/forms/signature-element.d.ts +6 -1
  62. package/dist/elements/forms/signature-element.js +16 -4
  63. package/dist/elements/forms/text-field-element.d.ts +6 -1
  64. package/dist/elements/forms/text-field-element.js +16 -4
  65. package/dist/elements/image-element.d.ts +70 -21
  66. package/dist/elements/image-element.js +173 -52
  67. package/dist/elements/index.d.ts +3 -0
  68. package/dist/elements/index.js +3 -0
  69. package/dist/elements/layout/anchor-element.d.ts +8 -10
  70. package/dist/elements/layout/anchor-element.js +10 -18
  71. package/dist/elements/layout/bookmark-element.d.ts +9 -11
  72. package/dist/elements/layout/bookmark-element.js +11 -20
  73. package/dist/elements/layout/column-break-element.d.ts +17 -0
  74. package/dist/elements/layout/column-break-element.js +26 -0
  75. package/dist/elements/layout/default-text-style-element.d.ts +11 -13
  76. package/dist/elements/layout/default-text-style-element.js +14 -31
  77. package/dist/elements/layout/deferred-element.d.ts +20 -4
  78. package/dist/elements/layout/deferred-element.js +41 -9
  79. package/dist/elements/layout/expanded-element.d.ts +11 -0
  80. package/dist/elements/layout/expanded-element.js +53 -18
  81. package/dist/elements/layout/flow-element.d.ts +109 -0
  82. package/dist/elements/layout/flow-element.js +317 -0
  83. package/dist/elements/layout/keep-together-element.d.ts +8 -18
  84. package/dist/elements/layout/keep-together-element.js +33 -45
  85. package/dist/elements/layout/link-element.d.ts +8 -13
  86. package/dist/elements/layout/link-element.js +9 -19
  87. package/dist/elements/layout/padding-element.d.ts +18 -1
  88. package/dist/elements/layout/padding-element.js +63 -28
  89. package/dist/elements/layout/page-break-element.d.ts +4 -1
  90. package/dist/elements/layout/page-break-element.js +6 -1
  91. package/dist/elements/layout/page-builder-element.d.ts +5 -0
  92. package/dist/elements/layout/page-builder-element.js +13 -2
  93. package/dist/elements/layout/positioned-element.d.ts +23 -4
  94. package/dist/elements/layout/positioned-element.js +42 -12
  95. package/dist/elements/layout/proxy-element.d.ts +47 -0
  96. package/dist/elements/layout/proxy-element.js +106 -0
  97. package/dist/elements/layout/region-element.d.ts +53 -0
  98. package/dist/elements/layout/region-element.js +108 -0
  99. package/dist/elements/layout/repeating-header-element.d.ts +9 -0
  100. package/dist/elements/layout/repeating-header-element.js +55 -13
  101. package/dist/elements/layout/rotated-box-element.d.ts +6 -0
  102. package/dist/elements/layout/rotated-box-element.js +27 -14
  103. package/dist/elements/layout/rotated-element.d.ts +9 -0
  104. package/dist/elements/layout/rotated-element.js +33 -2
  105. package/dist/elements/layout/struct-group.d.ts +9 -17
  106. package/dist/elements/layout/struct-group.js +10 -35
  107. package/dist/elements/line-element.d.ts +1 -0
  108. package/dist/elements/line-element.js +6 -0
  109. package/dist/elements/page-element.d.ts +10 -3
  110. package/dist/elements/page-element.js +39 -31
  111. package/dist/elements/pdf-document-element.d.ts +3 -0
  112. package/dist/elements/pdf-document-element.js +7 -0
  113. package/dist/elements/pdf-element.d.ts +105 -16
  114. package/dist/elements/pdf-element.js +118 -19
  115. package/dist/elements/rectangle-element.d.ts +22 -2
  116. package/dist/elements/rectangle-element.js +119 -55
  117. package/dist/elements/row-element.d.ts +17 -12
  118. package/dist/elements/row-element.js +89 -9
  119. package/dist/elements/svg-element.d.ts +30 -6
  120. package/dist/elements/svg-element.js +91 -26
  121. package/dist/elements/text-element.d.ts +48 -13
  122. package/dist/elements/text-element.js +161 -116
  123. package/dist/forms/acroform.d.ts +9 -1
  124. package/dist/forms/acroform.js +83 -34
  125. package/dist/forms/appearance.d.ts +17 -6
  126. package/dist/forms/appearance.js +18 -16
  127. package/dist/ir/display-list.d.ts +18 -3
  128. package/dist/layout/box-constraints.d.ts +17 -0
  129. package/dist/layout/box-constraints.js +31 -0
  130. package/dist/layout/collect-images.d.ts +6 -4
  131. package/dist/layout/collect-images.js +14 -8
  132. package/dist/layout/flex-item.d.ts +11 -0
  133. package/dist/layout/flex-item.js +51 -0
  134. package/dist/layout/fragmentation.d.ts +23 -9
  135. package/dist/layout/fragmentation.js +62 -19
  136. package/dist/layout/layout-cache.d.ts +53 -0
  137. package/dist/layout/layout-cache.js +110 -0
  138. package/dist/platform/browser-fs.d.ts +1 -2
  139. package/dist/platform/browser-fs.js +3 -6
  140. package/dist/platform/node-fs.d.ts +0 -1
  141. package/dist/platform/node-fs.js +0 -4
  142. package/dist/renderer/anchor-renderer.d.ts +2 -1
  143. package/dist/renderer/anchor-renderer.js +5 -4
  144. package/dist/renderer/bookmark-renderer.d.ts +2 -1
  145. package/dist/renderer/bookmark-renderer.js +5 -4
  146. package/dist/renderer/canvas-renderer.d.ts +2 -1
  147. package/dist/renderer/canvas-renderer.js +5 -2
  148. package/dist/renderer/column-break-renderer.d.ts +8 -0
  149. package/dist/renderer/column-break-renderer.js +15 -0
  150. package/dist/renderer/container-renderer.d.ts +2 -1
  151. package/dist/renderer/container-renderer.js +6 -5
  152. package/dist/renderer/default-text-style-renderer.d.ts +2 -1
  153. package/dist/renderer/default-text-style-renderer.js +2 -3
  154. package/dist/renderer/deferred-renderer.d.ts +2 -1
  155. package/dist/renderer/deferred-renderer.js +2 -3
  156. package/dist/renderer/emoji-image.js +3 -1
  157. package/dist/renderer/expanded-renderer.d.ts +2 -1
  158. package/dist/renderer/expanded-renderer.js +2 -3
  159. package/dist/renderer/flow-renderer.d.ts +16 -0
  160. package/dist/renderer/flow-renderer.js +21 -0
  161. package/dist/renderer/form-field-renderer.d.ts +2 -1
  162. package/dist/renderer/form-field-renderer.js +16 -3
  163. package/dist/renderer/image-renderer.d.ts +2 -1
  164. package/dist/renderer/image-renderer.js +33 -53
  165. package/dist/renderer/index.d.ts +2 -0
  166. package/dist/renderer/index.js +2 -0
  167. package/dist/renderer/keep-together-renderer.d.ts +2 -1
  168. package/dist/renderer/keep-together-renderer.js +2 -3
  169. package/dist/renderer/line-renderer.d.ts +2 -1
  170. package/dist/renderer/line-renderer.js +7 -2
  171. package/dist/renderer/link-renderer.d.ts +2 -1
  172. package/dist/renderer/link-renderer.js +36 -5
  173. package/dist/renderer/padding-renderer.d.ts +2 -1
  174. package/dist/renderer/padding-renderer.js +5 -5
  175. package/dist/renderer/page-break-renderer.d.ts +6 -5
  176. package/dist/renderer/page-break-renderer.js +14 -7
  177. package/dist/renderer/page-builder-renderer.d.ts +2 -1
  178. package/dist/renderer/page-builder-renderer.js +2 -3
  179. package/dist/renderer/page-renderer.js +30 -10
  180. package/dist/renderer/pdf-backend.d.ts +4 -3
  181. package/dist/renderer/pdf-backend.js +14 -7
  182. package/dist/renderer/pdf-document-renderer.d.ts +1 -8
  183. package/dist/renderer/pdf-document-renderer.js +17 -17
  184. package/dist/renderer/pdf-renderer.js +20 -11
  185. package/dist/renderer/positioned-renderer.d.ts +18 -1
  186. package/dist/renderer/positioned-renderer.js +43 -6
  187. package/dist/renderer/rectangle-renderer.d.ts +2 -1
  188. package/dist/renderer/rectangle-renderer.js +18 -8
  189. package/dist/renderer/repeating-header-renderer.d.ts +2 -1
  190. package/dist/renderer/repeating-header-renderer.js +6 -5
  191. package/dist/renderer/rotated-renderer.d.ts +2 -1
  192. package/dist/renderer/rotated-renderer.js +9 -5
  193. package/dist/renderer/row-renderer.d.ts +2 -1
  194. package/dist/renderer/row-renderer.js +6 -5
  195. package/dist/renderer/struct-group-renderer.d.ts +2 -1
  196. package/dist/renderer/struct-group-renderer.js +2 -3
  197. package/dist/renderer/svg-renderer.d.ts +2 -1
  198. package/dist/renderer/svg-renderer.js +8 -2
  199. package/dist/renderer/text-renderer.d.ts +4 -6
  200. package/dist/renderer/text-renderer.js +114 -127
  201. package/dist/svg/gradients.js +5 -0
  202. package/dist/svg/index.d.ts +1 -1
  203. package/dist/svg/index.js +36 -6
  204. package/dist/svg/style.d.ts +2 -0
  205. package/dist/svg/style.js +12 -1
  206. package/dist/text/advance.d.ts +4 -0
  207. package/dist/text/advance.js +17 -4
  208. package/dist/text/bidi.d.ts +0 -2
  209. package/dist/text/bidi.js +0 -10
  210. package/dist/text/glyph-coverage.d.ts +1 -1
  211. package/dist/text/glyph-coverage.js +7 -3
  212. package/dist/text/hyphen-breaks.d.ts +17 -0
  213. package/dist/text/hyphen-breaks.js +54 -0
  214. package/dist/text/line-breaker.d.ts +55 -29
  215. package/dist/text/line-breaker.js +453 -188
  216. package/dist/text/line-metrics.d.ts +8 -3
  217. package/dist/text/line-metrics.js +11 -2
  218. package/dist/text/pdf-text-string.d.ts +21 -0
  219. package/dist/text/pdf-text-string.js +114 -0
  220. package/dist/text/text-decoration.d.ts +5 -5
  221. package/dist/text/text-decoration.js +5 -5
  222. package/dist/text/word-splitting.d.ts +15 -2
  223. package/dist/text/word-splitting.js +45 -13
  224. package/dist/utils/afm-parser.d.ts +2 -0
  225. package/dist/utils/afm-parser.js +10 -1
  226. package/dist/utils/dest-registry.d.ts +1 -1
  227. package/dist/utils/dest-registry.js +3 -2
  228. package/dist/utils/fetch-bytes.d.ts +19 -0
  229. package/dist/utils/fetch-bytes.js +79 -0
  230. package/dist/utils/flex-layout.d.ts +16 -14
  231. package/dist/utils/flex-layout.js +427 -217
  232. package/dist/utils/font-metrics.d.ts +4 -3
  233. package/dist/utils/image-helper.d.ts +59 -11
  234. package/dist/utils/image-helper.js +209 -81
  235. package/dist/utils/pdf-object-manager.d.ts +17 -6
  236. package/dist/utils/pdf-object-manager.js +70 -20
  237. package/dist/utils/renderer-registry.d.ts +49 -2
  238. package/dist/utils/renderer-registry.js +40 -0
  239. package/dist/utils/struct-tree.d.ts +24 -0
  240. package/dist/utils/struct-tree.js +61 -9
  241. package/dist/utils/ttf-parser.d.ts +4 -0
  242. package/dist/utils/ttf-parser.js +30 -6
  243. package/dist/validators/element-validator.js +2 -1
  244. package/package.json +1 -1
  245. package/dist/elements/layout/sized-container-element.d.ts +0 -14
  246. package/dist/elements/layout/sized-container-element.js +0 -32
@@ -1,18 +1,17 @@
1
- // Import the renderer DIRECTLY, not via the "../renderer" barrel: the barrel pulls in
2
- // pdf-renderer (and every element) while this element module is still loading, which under
3
- // ESM (Vite/vitest, and the future framework bindings) duplicates the element classes and
4
- // breaks the constructor-keyed RendererRegistry. A direct import keeps the graph acyclic.
5
- import { TextRenderer } from "../renderer/text-renderer.js";
6
1
  import { DEFAULT_TEXT_STYLE } from "../text/text-style.js";
2
+ import { BoxConstraints } from "../layout/box-constraints.js";
7
3
  import { applyTextTransform } from "../text/text-style.js";
8
4
  import { splitByFont } from "../text/font-fallback.js";
9
5
  import { runAdvance } from "../text/advance.js";
10
- import { singleLineWidth, MAX_SPACE_SHRINK, wrapStringIntoLines, breakSegmentsIntoLines, segmentLinesToSegments, } from "../text/line-breaker.js";
6
+ import { runStyleOf, singleLineWidth, MAX_SPACE_SHRINK, breakSegmentsIntoLines, } from "../text/line-breaker.js";
7
+ import { hyphenPieces } from "../text/hyphen-breaks.js";
11
8
  import { adjustForOrphansWidows, DEFAULT_ORPHANS, DEFAULT_WIDOWS, } from "../text/orphans-widows.js";
12
- import { lineBoxForSegmentLine, lineBoxForString } from "../text/line-metrics.js";
9
+ import { lineBoxForSegmentLine, linesHeight } from "../text/line-metrics.js";
13
10
  import { HorizontalAlignment, SizedPDFElement } from "./pdf-element.js";
14
11
  import { normalizeContent } from "../text/whitespace.js";
15
12
  import { coverText } from "../text/glyph-coverage.js";
13
+ import { diagnose, pageOf } from "../common/diagnostics.js";
14
+ import { BuiltFromContext } from "../layout/layout-cache.js";
16
15
  /** How far a run sits off the baseline, as a fraction of its font size (up is positive). */
17
16
  export const VERTICAL_TEXT_SHIFT = {
18
17
  baseline: 0,
@@ -31,9 +30,15 @@ function finitePoints(value, name) {
31
30
  return value;
32
31
  }
33
32
  export class TextElement extends SizedPDFElement {
34
- 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, }) {
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, }) {
35
34
  super({ x: 0, y: 0 });
35
+ this.broken = new BuiltFromContext(() => false);
36
+ /** What `covered` had to remove, reported where the text is laid out on its page (see
37
+ * `calculateLayout`). A split paragraph's first piece carries its parent's, as `lostBefore`. */
38
+ this.lostGlyphs = [];
39
+ this.lostBefore = [];
36
40
  this.role = role;
41
+ this.shownLines = shownLines;
37
42
  this.rawFontSize = fontSize;
38
43
  this.rawFontFamily = fontFamily;
39
44
  this.rawFontStyle = fontStyle;
@@ -86,8 +91,7 @@ export class TextElement extends SizedPDFElement {
86
91
  return memo.value;
87
92
  const cased = this.recased();
88
93
  // A SPAN may bring its own stack, so the element's being empty is not enough to skip the pass.
89
- const anyStack = this.fontFallback.length > 0 ||
90
- (typeof cased !== "string" && cased.some((seg) => (seg.fontFallback?.length ?? 0) > 0));
94
+ const anyStack = this.fontFallback.length > 0 || cased.some((seg) => (seg.fontFallback?.length ?? 0) > 0);
91
95
  if (!metrics)
92
96
  return cased;
93
97
  const remember = (value) => {
@@ -104,11 +108,9 @@ export class TextElement extends SizedPDFElement {
104
108
  };
105
109
  if (!anyStack)
106
110
  return remember(this.covered(cased, metrics));
107
- // Font fallback turns the content into spans - one per family - which every later pass already
108
- // knows how to handle. Nothing new downstream.
109
- const pieces = typeof cased === "string" ? [{ content: cased }] : cased;
111
+ // Font fallback splits a span into more spans - one per family. Nothing new downstream.
110
112
  const out = [];
111
- for (const seg of pieces) {
113
+ for (const seg of cased) {
112
114
  const runs = splitByFont(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontFallback ?? this.fontFallback, seg.fontStyle ?? this.fontStyle, metrics);
113
115
  if (!runs)
114
116
  out.push(seg);
@@ -125,33 +127,30 @@ export class TextElement extends SizedPDFElement {
125
127
  * back unchanged, so an ordinary document is byte-identical.
126
128
  */
127
129
  covered(content, metrics) {
128
- const report = (dropped) => metrics.reportMissingGlyph?.(dropped);
129
- if (typeof content === "string") {
130
- const { text, dropped } = coverText(content, this.fontFamily, this.fontStyle, metrics);
131
- if (dropped.length > 0)
132
- report(dropped);
133
- return text;
134
- }
135
- return content.map((seg) => {
130
+ // Collected over every span: the element is reported once, with everything it lost, on the page it
131
+ // is laid out on - not here, which may be measuring it for an earlier page.
132
+ const lost = [];
133
+ const covered = content.map((seg) => {
136
134
  const { text, dropped } = coverText(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontStyle ?? this.fontStyle, metrics);
137
- if (dropped.length > 0)
138
- report(dropped);
135
+ lost.push(...dropped);
139
136
  return text === seg.content ? seg : { ...seg, content: text };
140
137
  });
138
+ this.lostGlyphs = [...new Set(lost)];
139
+ return covered;
141
140
  }
142
- /** The content with `text-transform` applied, and nothing else. */
141
+ /** The content as spans, with `text-transform` applied, and nothing else. */
143
142
  recased() {
144
- if (this.textTransform === "none")
145
- return this.content;
146
- if (typeof this.content === "string") {
147
- return applyTextTransform(this.content, this.textTransform).text;
148
- }
143
+ // A string is one span: from here on, measuring, breaking and drawing know only spans.
144
+ const spans = typeof this.content === "string" ? [{ content: this.content }] : this.content;
145
+ const transformOf = (seg) => seg.textTransform ?? this.textTransform;
146
+ if (spans.every((seg) => transformOf(seg) === "none"))
147
+ return spans;
149
148
  // The capitalisation state runs THROUGH the spans: `span("hel") + span("lo")` is one word.
150
149
  let atWordStart = true;
151
- return this.content.map((seg) => {
152
- const done = applyTextTransform(seg.content, this.textTransform, atWordStart);
150
+ return spans.map((seg) => {
151
+ const done = applyTextTransform(seg.content, transformOf(seg), atWordStart);
153
152
  atWordStart = done.atWordStart;
154
- return { ...seg, content: done.text };
153
+ return done.text === seg.content ? seg : { ...seg, content: done.text };
155
154
  });
156
155
  }
157
156
  lineOptions() {
@@ -162,6 +161,7 @@ export class TextElement extends SizedPDFElement {
162
161
  ligatures: this.ligatures,
163
162
  // Justified lines may be squeezed to keep a word; every other alignment gets no slack.
164
163
  shrink: this.textAlignment === HorizontalAlignment.justify ? MAX_SPACE_SHRINK : 0,
164
+ shownLines: this.shownLines,
165
165
  };
166
166
  }
167
167
  applyStyle(ts) {
@@ -188,44 +188,16 @@ export class TextElement extends SizedPDFElement {
188
188
  * Splits the paragraph at line boxes (Slice 1). The lines that fit in `maxHeight` stay;
189
189
  * the rest become a remainder `TextElement` re-wrapped on the next page. If not even one
190
190
  * line fits, nothing is forced here - the caller (the container) decides whether to move
191
- * the whole element on for progress. Handles both plain strings and styled segments.
191
+ * the whole element on for progress.
192
192
  */
193
193
  fragment(maxHeight, width, ctx) {
194
194
  this.resolveStyle(ctx);
195
195
  const content = this.display(ctx.metrics);
196
- return typeof content === "string"
197
- ? this.fragmentString(content, maxHeight, width, ctx)
198
- : this.fragmentSegments(content, maxHeight, width, ctx);
199
- }
200
- // Plain string: every wrapped line gets the same box (the same one calculateTextHeight uses),
201
- // so `floor(maxHeight / lineBox)` lines fit.
202
- fragmentString(content, maxHeight, width, ctx) {
203
- const lines = wrapStringIntoLines(content, this.fontFamily, this.fontSize, this.fontStyle, width, ctx.metrics, this.maxLines, this.overflow, this.letterSpacing, this.lineOptions());
204
- const box = lineBoxForString(ctx.metrics, this.fontFamily, this.fontStyle, this.fontSize, this.lineHeight);
205
- const fittedLineCount = adjustForOrphansWidows(Math.floor(maxHeight / box.height), lines.length, this.breakRule());
206
- if (fittedLineCount <= 0)
207
- return { fitted: null, remainder: this };
208
- if (fittedLineCount >= lines.length)
209
- return { fitted: this, remainder: null };
210
- return {
211
- fitted: this.cloneWithContent(lines.slice(0, fittedLineCount).join(" ")),
212
- // A continuation is no longer the paragraph's FIRST line, so it carries no indent.
213
- remainder: this.cloneWithContent(lines.slice(fittedLineCount).join(" "), 0),
214
- };
215
- }
216
- // Styled segments: each line's height comes from the fonts on it, so pack lines by cumulative
217
- // box height. Rebuild the fitted/remainder halves back into TextSegment[].
218
- fragmentSegments(content, maxHeight, width, ctx) {
219
- const defaults = {
220
- fontFamily: this.fontFamily,
221
- fontSize: this.fontSize,
222
- fontStyle: this.fontStyle,
223
- letterSpacing: this.letterSpacing,
224
- direction: this.direction,
225
- wordSpacing: this.wordSpacing,
226
- textIndent: this.textIndent,
227
- };
228
- const lines = breakSegmentsIntoLines(content, defaults, width, ctx.metrics, this.maxLines, this.overflow);
196
+ // The SAME knobs measuring and drawing break with (`textHeight`, the renderer), or this
197
+ // would cut a paragraph at a line count it is never drawn with. Each line's height comes from
198
+ // the fonts on it, so lines are packed by their cumulative height.
199
+ const defaults = this.segmentDefaults();
200
+ const lines = this.linesAt(width, ctx);
229
201
  let used = 0;
230
202
  let packed = 0;
231
203
  for (const line of lines) {
@@ -240,9 +212,35 @@ export class TextElement extends SizedPDFElement {
240
212
  return { fitted: null, remainder: this };
241
213
  if (fittedLineCount >= lines.length)
242
214
  return { fitted: this, remainder: null };
215
+ // A piece is a window onto the SAME text, not the lines glued back together: the first page draws
216
+ // the first lines of the whole paragraph (broken exactly as here, same text, same width), and the
217
+ // next one continues from where its first line starts in the source - inside its span, keeping
218
+ // that span's style. Hard breaks, a word split over the break and the justified last line on the
219
+ // page all survive that way. A continuation is no longer the paragraph's FIRST line, so it
220
+ // carries no indent; whatever is left of a line cap goes with it.
221
+ const { segment, offset } = lines[fittedLineCount].start;
222
+ const rest = [
223
+ { ...content[segment], content: content[segment].content.slice(offset) },
224
+ ...content.slice(segment + 1),
225
+ ];
226
+ const fitted = this.cloneWithContent(content, this.textIndent, { shownLines: fittedLineCount });
227
+ // The piece is cut from already-covered text, so it would find nothing missing itself.
228
+ fitted.lostBefore = [...this.lostBefore, ...this.lostGlyphs];
229
+ return {
230
+ fitted,
231
+ remainder: this.cloneWithContent(rest, 0, {
232
+ maxLines: this.maxLines === undefined ? undefined : this.maxLines - fittedLineCount,
233
+ }),
234
+ };
235
+ }
236
+ /** How the text is broken: the knobs `textHeight` and the renderer pass too. */
237
+ segmentDefaults() {
243
238
  return {
244
- fitted: this.cloneWithContent(segmentLinesToSegments(lines.slice(0, fittedLineCount))),
245
- remainder: this.cloneWithContent(segmentLinesToSegments(lines.slice(fittedLineCount)), 0),
239
+ fontFamily: this.fontFamily,
240
+ fontSize: this.fontSize,
241
+ fontStyle: this.fontStyle,
242
+ letterSpacing: this.letterSpacing,
243
+ ...this.lineOptions(),
246
244
  };
247
245
  }
248
246
  /** The orphan/widow minimums for this paragraph; unset means the CSS default of 2 lines each. */
@@ -254,15 +252,16 @@ export class TextElement extends SizedPDFElement {
254
252
  }
255
253
  // A copy carrying the same style but different (already-wrapped) content. Re-wrapping at
256
254
  // the same width reproduces exactly those lines (greedy is deterministic).
257
- cloneWithContent(content, indent = this.textIndent) {
255
+ cloneWithContent(content, indent = this.textIndent, piece = {}) {
258
256
  return new TextElement({
259
- 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" }),
260
259
  fontSize: this.fontSize,
261
260
  fontFamily: this.fontFamily,
262
261
  fontStyle: this.fontStyle,
263
262
  color: this.color,
264
263
  textAlignment: this.textAlignment,
265
- maxLines: this.maxLines,
264
+ maxLines: piece.maxLines ?? this.maxLines,
266
265
  orphans: this.orphans,
267
266
  widows: this.widows,
268
267
  overflow: this.overflow,
@@ -275,46 +274,92 @@ export class TextElement extends SizedPDFElement {
275
274
  wordSpacing: this.wordSpacing,
276
275
  textIndent: indent,
277
276
  role: this.role,
278
- }).adoptStructId(this); // a wrapped remainder is the SAME logical paragraph (one P across pages)
277
+ fontFallback: this.fontFallback,
278
+ // Already recased: `capitalize` again would capitalise the half of a word continued on the next
279
+ // page.
280
+ textTransform: "none",
281
+ breakWord: this.breakWord,
282
+ hyphenate: this.hyphenate,
283
+ ligatures: this.ligatures,
284
+ shownLines: piece.shownLines,
285
+ }).asPieceOf(this); // a wrapped remainder is the SAME logical paragraph (one P across pages)
279
286
  }
280
287
  calculateLayout(constraints, offset, ctx) {
281
- this.resolveStyle(ctx);
288
+ const { size, lines } = this.sizeWith(constraints, ctx);
282
289
  this.x = offset.x;
283
290
  this.y = offset.y;
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. Columns always bound the width, so this leaves their layout untouched.
287
291
  this.resolved = this.display(ctx.metrics);
288
- this.width = constraints.hasBoundedWidth
289
- ? constraints.maxWidth
290
- : this.naturalWidth(ctx.metrics);
291
- const wrapWidth = this.width ?? 0;
292
- this.height = TextRenderer.calculateTextHeight(this.resolved, this.fontSize, this.fontFamily, this.fontStyle, ctx.metrics, wrapWidth, this.maxLines, this.overflow, this.lineHeight, this.letterSpacing, this.lineOptions());
292
+ const lost = [...new Set([...this.lostBefore, ...this.lostGlyphs])];
293
+ if (lost.length > 0)
294
+ ctx.metrics.reportMissingGlyph?.(lost, this, pageOf(ctx));
295
+ this.width = size.width;
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
+ }
293
311
  // Top-left coordinates (y = top of the text box). The baseline offset and the
294
312
  // Y-flip are applied downstream (the line-builder positions baselines, the seam
295
313
  // flips to PDF), so the element stays coordinate-system-blind.
296
- return { width: wrapWidth, height: this.height };
314
+ return size;
315
+ }
316
+ /**
317
+ * The size `calculateLayout` would take, without placing anything. Resolving the inherited style
318
+ * and filling the memos below are its only writes, and both are a pure function of the context:
319
+ * the same inputs leave the same state, which a later layout reads back unchanged.
320
+ */
321
+ /** A text's size reads only the width it may take: it wraps to it, and a height bound never changes
322
+ * the lines (a box that is too short cuts them, it does not reflow them). */
323
+ sizeInputs(constraints) {
324
+ return BoxConstraints.loose(constraints.maxWidth, Infinity);
325
+ }
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) {
339
+ this.resolveStyle(ctx);
340
+ const width = constraints.hasBoundedWidth
341
+ ? constraints.maxWidth
342
+ : this.naturalWidth(ctx.metrics);
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
+ };
297
349
  }
298
350
  /** The unwrapped single-line width of the content (used when width is unbounded, e.g. inside a Row).
299
- * Must match the LINE-BREAKER's one-line measure EXACTLY - not just algebraically but BIT-for-bit,
300
- * so a text laid out at this width never re-wraps inside its own natural-width box. The breaker
301
- * accumulates `currentWidth += wordWidth + spaceWidth`, grouping the word and its trailing space
302
- * into one term; we must group the same way. Adding word and space as two separate steps is
303
- * algebraically equal but, because floating-point addition is not associative, drifts by a sub-ULP
304
- * - enough to tip a borderline string (e.g. "20 Jun 2026", wider than "04 Jul 2026" only because
305
- * 'n' beats 'l') one bit over its own width, dropping the last word onto a second line. */
351
+ * Must match the breaker's one-line sum BIT for bit - it adds `width + (space + word)`, and the same
352
+ * numbers grouped differently drift by a sub-ULP, enough to wrap a text inside its own natural-width
353
+ * box. `singleLineWidth` is that sum. */
306
354
  naturalWidth(metrics) {
307
- // The breaker's own sum, to the last bit - see `singleLineWidth`.
308
- const oneLine = (text, family, size, style, letterSpacing) => singleLineWidth(text, { fontFamily: family, fontSize: size, fontStyle: style }, metrics, letterSpacing, this.wordSpacing);
355
+ const defaults = this.segmentDefaults();
309
356
  // The FIRST line starts `textIndent` in, so an unbounded box has to be that much wider.
310
357
  const indent = Math.max(0, this.textIndent);
311
- const content = this.display(metrics);
312
- if (typeof content === "string") {
313
- return (indent +
314
- oneLine(content, this.fontFamily, this.fontSize, this.fontStyle, this.letterSpacing));
315
- }
316
- return content.reduce((sum, seg) => sum +
317
- 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);
318
363
  }
319
364
  /**
320
365
  * The widest piece of this text that cannot be broken - CSS's `min-content` width. A shrinking flex
@@ -331,27 +376,22 @@ export class TextElement extends SizedPDFElement {
331
376
  return 0;
332
377
  this.resolveStyle(ctx);
333
378
  const metrics = ctx.metrics;
334
- const widest = (text, family, size, style, letterSpacing) => {
335
- // `ligatures` belongs to the FONT: it decides which glyphs are drawn and therefore how wide the
336
- // piece is. Leaving it out here would measure a floor the drawing never matches.
337
- const font = {
338
- fontFamily: family,
339
- fontSize: size,
340
- fontStyle: style,
341
- ligatures: this.ligatures,
342
- };
343
- // Split on the same boundaries the breaker uses - a hard break ends a line too.
344
- const pieces = this.breakWord || this.hyphenate ? [...text] : text.split(/[ \n]/);
345
- return pieces.reduce((max, piece) => Math.max(max, runAdvance(metrics, piece, font, letterSpacing)), 0);
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) => {
382
+ // Split on the same boundaries the breaker uses - a hard break ends a line too, and so may a
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);
346
388
  };
389
+ const defaults = this.segmentDefaults();
347
390
  const content = this.display(metrics);
348
- if (typeof content === "string") {
349
- return widest(content, this.fontFamily, this.fontSize, this.fontStyle, this.letterSpacing);
350
- }
351
391
  // Segments join without a space, so a word may straddle two of them; taking the widest piece per
352
392
  // segment can therefore UNDER-estimate. That errs towards squeezing a little too far, which lands
353
393
  // back on the old overflow behaviour rather than on something new.
354
- 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);
355
395
  }
356
396
  getProps() {
357
397
  return {
@@ -363,7 +403,11 @@ export class TextElement extends SizedPDFElement {
363
403
  fontFamily: this.fontFamily,
364
404
  fontStyle: this.fontStyle,
365
405
  color: this.color,
366
- content: this.resolved ?? this.content,
406
+ content: this.content,
407
+ /** The content as it is measured and drawn: spans, recased, split by font, drawable glyphs only. */
408
+ spans: this.resolved ?? this.display(),
409
+ /** How the text is broken - the settings it is measured with, so drawing breaks it the same. */
410
+ breaking: this.segmentDefaults(),
367
411
  textAlignment: this.textAlignment,
368
412
  maxLines: this.maxLines,
369
413
  orphans: this.orphans,
@@ -381,6 +425,7 @@ export class TextElement extends SizedPDFElement {
381
425
  ligatures: this.ligatures,
382
426
  hyphenate: this.hyphenate,
383
427
  role: this.role,
428
+ shownLines: this.shownLines,
384
429
  };
385
430
  }
386
431
  }
@@ -19,6 +19,14 @@ export declare class AcroFormCollector {
19
19
  /** The size to draw a value at. A field may ask for `0` - the PDF convention for "auto-size"; we then
20
20
  * fit the capitals comfortably inside the box, which is what a viewer's auto-size does. */
21
21
  private drawSize;
22
+ /**
23
+ * The font a field's baked appearance draws its text in: the font the field inherits when it is
24
+ * EMBEDDED, else the standard Helvetica. PDF/A and PDF/UA require every font that draws to be
25
+ * embedded, and a document that embeds its fonts means that. The /DA keeps naming Helvetica - it is
26
+ * what a viewer redraws with once someone types, and a subset font could not draw the new characters.
27
+ * Measuring and drawing both take the font from here.
28
+ */
29
+ private faceFont;
22
30
  /** Measure one line for the appearance generator. */
23
31
  private line;
24
32
  /** Bake a text field's value into an appearance stream; returns the XObject number. */
@@ -47,7 +55,7 @@ export declare class AcroFormCollector {
47
55
  private claimName;
48
56
  /** Emit the widget object for one form-field IR node, register it as a field, and return its object
49
57
  * number so the PageRenderer can add it to that page's /Annots. */
50
- addField(node: FormFieldNode, om: PDFObjectManager): number;
58
+ addField(node: FormFieldNode, om: PDFObjectManager, structParent?: number): number;
51
59
  /** A push button (/Btn, Pushbutton flag): no value, an optional action, and a baked caption. The
52
60
  * caption is measured here (the appearance module stays free of font metrics) and drawn into an
53
61
  * appearance stream that carries its own font resource. */