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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/dist/api/content.d.ts +5 -5
  2. package/dist/api/content.js +12 -6
  3. package/dist/api/font-url.d.ts +1 -2
  4. package/dist/api/font-url.js +8 -74
  5. package/dist/api/forms.d.ts +3 -0
  6. package/dist/api/forms.js +3 -0
  7. package/dist/api/index.d.ts +2 -0
  8. package/dist/api/index.js +1 -0
  9. package/dist/api/layout.d.ts +6 -0
  10. package/dist/api/layout.js +7 -2
  11. package/dist/api/page-builder.d.ts +4 -0
  12. package/dist/api/page-builder.js +4 -0
  13. package/dist/api/structure.d.ts +10 -2
  14. package/dist/api/structure.js +9 -6
  15. package/dist/api/table.d.ts +5 -3
  16. package/dist/api/table.js +99 -22
  17. package/dist/common/diagnostics.d.ts +53 -0
  18. package/dist/common/diagnostics.js +77 -0
  19. package/dist/edit/acroform-reader.js +7 -7
  20. package/dist/edit/appearance.js +3 -3
  21. package/dist/edit/attachments.d.ts +52 -0
  22. package/dist/edit/attachments.js +178 -0
  23. package/dist/edit/document.d.ts +38 -8
  24. package/dist/edit/document.js +208 -51
  25. package/dist/edit/fill.js +8 -23
  26. package/dist/edit/filters.d.ts +15 -0
  27. package/dist/edit/filters.js +157 -0
  28. package/dist/edit/index.d.ts +4 -1
  29. package/dist/edit/index.js +3 -1
  30. package/dist/edit/objects.d.ts +3 -4
  31. package/dist/edit/objects.js +5 -18
  32. package/dist/elements/canvas-element.d.ts +3 -0
  33. package/dist/elements/canvas-element.js +14 -3
  34. package/dist/elements/container-element.d.ts +5 -1
  35. package/dist/elements/container-element.js +35 -15
  36. package/dist/elements/forms/checkbox-element.d.ts +2 -0
  37. package/dist/elements/forms/checkbox-element.js +12 -3
  38. package/dist/elements/forms/choice-element.d.ts +6 -1
  39. package/dist/elements/forms/choice-element.js +17 -6
  40. package/dist/elements/forms/field-font.d.ts +8 -0
  41. package/dist/elements/forms/field-font.js +5 -0
  42. package/dist/elements/forms/push-button-element.d.ts +6 -1
  43. package/dist/elements/forms/push-button-element.js +16 -4
  44. package/dist/elements/forms/radio-element.d.ts +2 -0
  45. package/dist/elements/forms/radio-element.js +12 -3
  46. package/dist/elements/forms/signature-element.d.ts +6 -1
  47. package/dist/elements/forms/signature-element.js +16 -4
  48. package/dist/elements/forms/text-field-element.d.ts +6 -1
  49. package/dist/elements/forms/text-field-element.js +16 -4
  50. package/dist/elements/image-element.d.ts +39 -1
  51. package/dist/elements/image-element.js +95 -12
  52. package/dist/elements/layout/anchor-element.d.ts +8 -10
  53. package/dist/elements/layout/anchor-element.js +10 -18
  54. package/dist/elements/layout/bookmark-element.d.ts +9 -11
  55. package/dist/elements/layout/bookmark-element.js +11 -20
  56. package/dist/elements/layout/default-text-style-element.d.ts +11 -13
  57. package/dist/elements/layout/default-text-style-element.js +14 -31
  58. package/dist/elements/layout/deferred-element.d.ts +12 -4
  59. package/dist/elements/layout/deferred-element.js +24 -7
  60. package/dist/elements/layout/expanded-element.d.ts +7 -0
  61. package/dist/elements/layout/expanded-element.js +40 -18
  62. package/dist/elements/layout/keep-together-element.d.ts +8 -18
  63. package/dist/elements/layout/keep-together-element.js +21 -45
  64. package/dist/elements/layout/link-element.d.ts +8 -13
  65. package/dist/elements/layout/link-element.js +9 -19
  66. package/dist/elements/layout/padding-element.d.ts +15 -1
  67. package/dist/elements/layout/padding-element.js +55 -27
  68. package/dist/elements/layout/page-break-element.d.ts +4 -1
  69. package/dist/elements/layout/page-break-element.js +6 -1
  70. package/dist/elements/layout/page-builder-element.d.ts +5 -0
  71. package/dist/elements/layout/page-builder-element.js +11 -0
  72. package/dist/elements/layout/positioned-element.d.ts +14 -2
  73. package/dist/elements/layout/positioned-element.js +36 -10
  74. package/dist/elements/layout/proxy-element.d.ts +43 -0
  75. package/dist/elements/layout/proxy-element.js +94 -0
  76. package/dist/elements/layout/repeating-header-element.d.ts +6 -0
  77. package/dist/elements/layout/repeating-header-element.js +40 -13
  78. package/dist/elements/layout/rotated-box-element.d.ts +6 -0
  79. package/dist/elements/layout/rotated-box-element.js +27 -14
  80. package/dist/elements/layout/rotated-element.d.ts +7 -0
  81. package/dist/elements/layout/rotated-element.js +25 -2
  82. package/dist/elements/layout/struct-group.d.ts +9 -17
  83. package/dist/elements/layout/struct-group.js +10 -35
  84. package/dist/elements/line-element.d.ts +1 -0
  85. package/dist/elements/line-element.js +6 -0
  86. package/dist/elements/page-element.d.ts +10 -3
  87. package/dist/elements/page-element.js +39 -31
  88. package/dist/elements/pdf-document-element.d.ts +3 -0
  89. package/dist/elements/pdf-document-element.js +7 -0
  90. package/dist/elements/pdf-element.d.ts +75 -15
  91. package/dist/elements/pdf-element.js +68 -18
  92. package/dist/elements/rectangle-element.d.ts +14 -0
  93. package/dist/elements/rectangle-element.js +97 -52
  94. package/dist/elements/row-element.d.ts +11 -0
  95. package/dist/elements/row-element.js +58 -8
  96. package/dist/elements/svg-element.d.ts +30 -6
  97. package/dist/elements/svg-element.js +88 -17
  98. package/dist/elements/text-element.d.ts +32 -13
  99. package/dist/elements/text-element.js +119 -97
  100. package/dist/forms/acroform.d.ts +9 -1
  101. package/dist/forms/acroform.js +83 -34
  102. package/dist/forms/appearance.d.ts +17 -6
  103. package/dist/forms/appearance.js +18 -16
  104. package/dist/ir/display-list.d.ts +12 -2
  105. package/dist/layout/box-constraints.d.ts +5 -0
  106. package/dist/layout/box-constraints.js +14 -0
  107. package/dist/layout/collect-images.d.ts +6 -4
  108. package/dist/layout/collect-images.js +6 -4
  109. package/dist/layout/flex-item.d.ts +9 -0
  110. package/dist/layout/flex-item.js +39 -0
  111. package/dist/layout/fragmentation.d.ts +12 -4
  112. package/dist/layout/fragmentation.js +37 -14
  113. package/dist/layout/layout-cache.d.ts +53 -0
  114. package/dist/layout/layout-cache.js +107 -0
  115. package/dist/renderer/anchor-renderer.d.ts +2 -1
  116. package/dist/renderer/anchor-renderer.js +5 -4
  117. package/dist/renderer/bookmark-renderer.d.ts +2 -1
  118. package/dist/renderer/bookmark-renderer.js +5 -4
  119. package/dist/renderer/canvas-renderer.d.ts +2 -1
  120. package/dist/renderer/canvas-renderer.js +5 -2
  121. package/dist/renderer/container-renderer.d.ts +2 -1
  122. package/dist/renderer/container-renderer.js +6 -5
  123. package/dist/renderer/default-text-style-renderer.d.ts +2 -1
  124. package/dist/renderer/default-text-style-renderer.js +2 -3
  125. package/dist/renderer/deferred-renderer.d.ts +2 -1
  126. package/dist/renderer/deferred-renderer.js +2 -3
  127. package/dist/renderer/expanded-renderer.d.ts +2 -1
  128. package/dist/renderer/expanded-renderer.js +2 -3
  129. package/dist/renderer/form-field-renderer.d.ts +2 -1
  130. package/dist/renderer/form-field-renderer.js +16 -3
  131. package/dist/renderer/image-renderer.d.ts +2 -1
  132. package/dist/renderer/image-renderer.js +5 -2
  133. package/dist/renderer/keep-together-renderer.d.ts +2 -1
  134. package/dist/renderer/keep-together-renderer.js +2 -3
  135. package/dist/renderer/line-renderer.d.ts +2 -1
  136. package/dist/renderer/line-renderer.js +7 -2
  137. package/dist/renderer/link-renderer.d.ts +2 -1
  138. package/dist/renderer/link-renderer.js +36 -5
  139. package/dist/renderer/padding-renderer.d.ts +2 -1
  140. package/dist/renderer/padding-renderer.js +5 -5
  141. package/dist/renderer/page-break-renderer.d.ts +6 -5
  142. package/dist/renderer/page-break-renderer.js +14 -7
  143. package/dist/renderer/page-builder-renderer.d.ts +2 -1
  144. package/dist/renderer/page-builder-renderer.js +2 -3
  145. package/dist/renderer/page-renderer.js +28 -7
  146. package/dist/renderer/pdf-backend.d.ts +4 -3
  147. package/dist/renderer/pdf-backend.js +5 -5
  148. package/dist/renderer/pdf-document-renderer.d.ts +1 -8
  149. package/dist/renderer/pdf-document-renderer.js +15 -16
  150. package/dist/renderer/pdf-renderer.js +11 -11
  151. package/dist/renderer/positioned-renderer.d.ts +6 -1
  152. package/dist/renderer/positioned-renderer.js +6 -5
  153. package/dist/renderer/rectangle-renderer.d.ts +2 -1
  154. package/dist/renderer/rectangle-renderer.js +9 -4
  155. package/dist/renderer/repeating-header-renderer.d.ts +2 -1
  156. package/dist/renderer/repeating-header-renderer.js +6 -5
  157. package/dist/renderer/rotated-renderer.d.ts +2 -1
  158. package/dist/renderer/rotated-renderer.js +8 -5
  159. package/dist/renderer/row-renderer.d.ts +2 -1
  160. package/dist/renderer/row-renderer.js +6 -5
  161. package/dist/renderer/struct-group-renderer.d.ts +2 -1
  162. package/dist/renderer/struct-group-renderer.js +2 -3
  163. package/dist/renderer/svg-renderer.d.ts +2 -1
  164. package/dist/renderer/svg-renderer.js +8 -2
  165. package/dist/renderer/text-renderer.d.ts +4 -6
  166. package/dist/renderer/text-renderer.js +101 -111
  167. package/dist/svg/gradients.js +5 -0
  168. package/dist/svg/index.js +10 -1
  169. package/dist/svg/style.d.ts +2 -0
  170. package/dist/svg/style.js +11 -0
  171. package/dist/text/advance.d.ts +4 -0
  172. package/dist/text/advance.js +17 -4
  173. package/dist/text/bidi.d.ts +0 -2
  174. package/dist/text/bidi.js +0 -10
  175. package/dist/text/glyph-coverage.d.ts +1 -1
  176. package/dist/text/glyph-coverage.js +7 -3
  177. package/dist/text/hyphen-breaks.d.ts +17 -0
  178. package/dist/text/hyphen-breaks.js +54 -0
  179. package/dist/text/line-breaker.d.ts +34 -31
  180. package/dist/text/line-breaker.js +364 -176
  181. package/dist/text/line-metrics.d.ts +6 -3
  182. package/dist/text/line-metrics.js +7 -2
  183. package/dist/text/pdf-text-string.d.ts +21 -0
  184. package/dist/text/pdf-text-string.js +114 -0
  185. package/dist/text/text-decoration.d.ts +5 -5
  186. package/dist/text/text-decoration.js +5 -5
  187. package/dist/text/word-splitting.d.ts +15 -2
  188. package/dist/text/word-splitting.js +34 -8
  189. package/dist/utils/afm-parser.d.ts +2 -0
  190. package/dist/utils/afm-parser.js +10 -1
  191. package/dist/utils/dest-registry.d.ts +1 -1
  192. package/dist/utils/dest-registry.js +3 -2
  193. package/dist/utils/fetch-bytes.d.ts +19 -0
  194. package/dist/utils/fetch-bytes.js +79 -0
  195. package/dist/utils/flex-layout.d.ts +13 -13
  196. package/dist/utils/flex-layout.js +318 -216
  197. package/dist/utils/font-metrics.d.ts +4 -3
  198. package/dist/utils/image-helper.js +2 -1
  199. package/dist/utils/pdf-object-manager.d.ts +11 -3
  200. package/dist/utils/pdf-object-manager.js +39 -13
  201. package/dist/utils/renderer-registry.d.ts +43 -2
  202. package/dist/utils/renderer-registry.js +40 -0
  203. package/dist/utils/struct-tree.d.ts +24 -0
  204. package/dist/utils/struct-tree.js +61 -9
  205. package/dist/utils/ttf-parser.d.ts +4 -0
  206. package/dist/utils/ttf-parser.js +27 -4
  207. package/dist/validators/element-validator.js +2 -1
  208. package/package.json +1 -1
  209. package/dist/elements/layout/sized-container-element.d.ts +0 -14
  210. package/dist/elements/layout/sized-container-element.js +0 -32
@@ -1,18 +1,16 @@
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 { 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, textHeight } 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 { pageOf } from "../common/diagnostics.js";
16
14
  /** How far a run sits off the baseline, as a fraction of its font size (up is positive). */
17
15
  export const VERTICAL_TEXT_SHIFT = {
18
16
  baseline: 0,
@@ -31,9 +29,14 @@ function finitePoints(value, name) {
31
29
  return value;
32
30
  }
33
31
  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, }) {
32
+ 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
33
  super({ x: 0, y: 0 });
34
+ /** What `covered` had to remove, reported where the text is laid out on its page (see
35
+ * `calculateLayout`). A split paragraph's first piece carries its parent's, as `lostBefore`. */
36
+ this.lostGlyphs = [];
37
+ this.lostBefore = [];
36
38
  this.role = role;
39
+ this.shownLines = shownLines;
37
40
  this.rawFontSize = fontSize;
38
41
  this.rawFontFamily = fontFamily;
39
42
  this.rawFontStyle = fontStyle;
@@ -86,8 +89,7 @@ export class TextElement extends SizedPDFElement {
86
89
  return memo.value;
87
90
  const cased = this.recased();
88
91
  // 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));
92
+ const anyStack = this.fontFallback.length > 0 || cased.some((seg) => (seg.fontFallback?.length ?? 0) > 0);
91
93
  if (!metrics)
92
94
  return cased;
93
95
  const remember = (value) => {
@@ -104,11 +106,9 @@ export class TextElement extends SizedPDFElement {
104
106
  };
105
107
  if (!anyStack)
106
108
  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;
109
+ // Font fallback splits a span into more spans - one per family. Nothing new downstream.
110
110
  const out = [];
111
- for (const seg of pieces) {
111
+ for (const seg of cased) {
112
112
  const runs = splitByFont(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontFallback ?? this.fontFallback, seg.fontStyle ?? this.fontStyle, metrics);
113
113
  if (!runs)
114
114
  out.push(seg);
@@ -125,30 +125,26 @@ export class TextElement extends SizedPDFElement {
125
125
  * back unchanged, so an ordinary document is byte-identical.
126
126
  */
127
127
  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) => {
128
+ // Collected over every span: the element is reported once, with everything it lost, on the page it
129
+ // is laid out on - not here, which may be measuring it for an earlier page.
130
+ const lost = [];
131
+ const covered = content.map((seg) => {
136
132
  const { text, dropped } = coverText(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontStyle ?? this.fontStyle, metrics);
137
- if (dropped.length > 0)
138
- report(dropped);
133
+ lost.push(...dropped);
139
134
  return text === seg.content ? seg : { ...seg, content: text };
140
135
  });
136
+ this.lostGlyphs = [...new Set(lost)];
137
+ return covered;
141
138
  }
142
- /** The content with `text-transform` applied, and nothing else. */
139
+ /** The content as spans, with `text-transform` applied, and nothing else. */
143
140
  recased() {
141
+ // A string is one span: from here on, measuring, breaking and drawing know only spans.
142
+ const spans = typeof this.content === "string" ? [{ content: this.content }] : this.content;
144
143
  if (this.textTransform === "none")
145
- return this.content;
146
- if (typeof this.content === "string") {
147
- return applyTextTransform(this.content, this.textTransform).text;
148
- }
144
+ return spans;
149
145
  // The capitalisation state runs THROUGH the spans: `span("hel") + span("lo")` is one word.
150
146
  let atWordStart = true;
151
- return this.content.map((seg) => {
147
+ return spans.map((seg) => {
152
148
  const done = applyTextTransform(seg.content, this.textTransform, atWordStart);
153
149
  atWordStart = done.atWordStart;
154
150
  return { ...seg, content: done.text };
@@ -162,6 +158,7 @@ export class TextElement extends SizedPDFElement {
162
158
  ligatures: this.ligatures,
163
159
  // Justified lines may be squeezed to keep a word; every other alignment gets no slack.
164
160
  shrink: this.textAlignment === HorizontalAlignment.justify ? MAX_SPACE_SHRINK : 0,
161
+ shownLines: this.shownLines,
165
162
  };
166
163
  }
167
164
  applyStyle(ts) {
@@ -188,43 +185,15 @@ export class TextElement extends SizedPDFElement {
188
185
  * Splits the paragraph at line boxes (Slice 1). The lines that fit in `maxHeight` stay;
189
186
  * the rest become a remainder `TextElement` re-wrapped on the next page. If not even one
190
187
  * 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.
188
+ * the whole element on for progress.
192
189
  */
193
190
  fragment(maxHeight, width, ctx) {
194
191
  this.resolveStyle(ctx);
195
192
  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
- };
193
+ // The SAME knobs measuring and drawing break with (`textHeight`, the renderer), or this
194
+ // would cut a paragraph at a line count it is never drawn with. Each line's height comes from
195
+ // the fonts on it, so lines are packed by their cumulative height.
196
+ const defaults = this.segmentDefaults();
228
197
  const lines = breakSegmentsIntoLines(content, defaults, width, ctx.metrics, this.maxLines, this.overflow);
229
198
  let used = 0;
230
199
  let packed = 0;
@@ -240,9 +209,35 @@ export class TextElement extends SizedPDFElement {
240
209
  return { fitted: null, remainder: this };
241
210
  if (fittedLineCount >= lines.length)
242
211
  return { fitted: this, remainder: null };
212
+ // A piece is a window onto the SAME text, not the lines glued back together: the first page draws
213
+ // the first lines of the whole paragraph (broken exactly as here, same text, same width), and the
214
+ // next one continues from where its first line starts in the source - inside its span, keeping
215
+ // that span's style. Hard breaks, a word split over the break and the justified last line on the
216
+ // page all survive that way. A continuation is no longer the paragraph's FIRST line, so it
217
+ // carries no indent; whatever is left of a line cap goes with it.
218
+ const { segment, offset } = lines[fittedLineCount].start;
219
+ const rest = [
220
+ { ...content[segment], content: content[segment].content.slice(offset) },
221
+ ...content.slice(segment + 1),
222
+ ];
223
+ const fitted = this.cloneWithContent(content, this.textIndent, { shownLines: fittedLineCount });
224
+ // The piece is cut from already-covered text, so it would find nothing missing itself.
225
+ fitted.lostBefore = [...this.lostBefore, ...this.lostGlyphs];
243
226
  return {
244
- fitted: this.cloneWithContent(segmentLinesToSegments(lines.slice(0, fittedLineCount))),
245
- remainder: this.cloneWithContent(segmentLinesToSegments(lines.slice(fittedLineCount)), 0),
227
+ fitted,
228
+ remainder: this.cloneWithContent(rest, 0, {
229
+ maxLines: this.maxLines === undefined ? undefined : this.maxLines - fittedLineCount,
230
+ }),
231
+ };
232
+ }
233
+ /** How the text is broken: the knobs `textHeight` and the renderer pass too. */
234
+ segmentDefaults() {
235
+ return {
236
+ fontFamily: this.fontFamily,
237
+ fontSize: this.fontSize,
238
+ fontStyle: this.fontStyle,
239
+ letterSpacing: this.letterSpacing,
240
+ ...this.lineOptions(),
246
241
  };
247
242
  }
248
243
  /** The orphan/widow minimums for this paragraph; unset means the CSS default of 2 lines each. */
@@ -254,7 +249,7 @@ export class TextElement extends SizedPDFElement {
254
249
  }
255
250
  // A copy carrying the same style but different (already-wrapped) content. Re-wrapping at
256
251
  // the same width reproduces exactly those lines (greedy is deterministic).
257
- cloneWithContent(content, indent = this.textIndent) {
252
+ cloneWithContent(content, indent = this.textIndent, piece = {}) {
258
253
  return new TextElement({
259
254
  content,
260
255
  fontSize: this.fontSize,
@@ -262,7 +257,7 @@ export class TextElement extends SizedPDFElement {
262
257
  fontStyle: this.fontStyle,
263
258
  color: this.color,
264
259
  textAlignment: this.textAlignment,
265
- maxLines: this.maxLines,
260
+ maxLines: piece.maxLines ?? this.maxLines,
266
261
  orphans: this.orphans,
267
262
  widows: this.widows,
268
263
  overflow: this.overflow,
@@ -275,45 +270,69 @@ export class TextElement extends SizedPDFElement {
275
270
  wordSpacing: this.wordSpacing,
276
271
  textIndent: indent,
277
272
  role: this.role,
278
- }).adoptStructId(this); // a wrapped remainder is the SAME logical paragraph (one P across pages)
273
+ 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.
276
+ textTransform: "none",
277
+ breakWord: this.breakWord,
278
+ hyphenate: this.hyphenate,
279
+ ligatures: this.ligatures,
280
+ shownLines: piece.shownLines,
281
+ }).asPieceOf(this); // a wrapped remainder is the SAME logical paragraph (one P across pages)
279
282
  }
280
283
  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.
281
288
  this.resolveStyle(ctx);
289
+ const size = this.measure(constraints, ctx);
282
290
  this.x = offset.x;
283
291
  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
292
  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());
293
+ const lost = [...new Set([...this.lostBefore, ...this.lostGlyphs])];
294
+ if (lost.length > 0)
295
+ ctx.metrics.reportMissingGlyph?.(lost, this, pageOf(ctx));
296
+ this.width = size.width;
297
+ this.height = size.height;
293
298
  // Top-left coordinates (y = top of the text box). The baseline offset and the
294
299
  // Y-flip are applied downstream (the line-builder positions baselines, the seam
295
300
  // flips to PDF), so the element stays coordinate-system-blind.
296
- return { width: wrapWidth, height: this.height };
301
+ return size;
302
+ }
303
+ /**
304
+ * The size `calculateLayout` would take, without placing anything. Resolving the inherited style
305
+ * and filling the memos below are its only writes, and both are a pure function of the context:
306
+ * the same inputs leave the same state, which a later layout reads back unchanged.
307
+ */
308
+ /** A text's size reads only the width it may take: it wraps to it, and a height bound never changes
309
+ * the lines (a box that is too short cuts them, it does not reflow them). */
310
+ sizeInputs(constraints) {
311
+ return BoxConstraints.loose(constraints.maxWidth, Infinity);
312
+ }
313
+ computeSize(constraints, ctx) {
314
+ this.resolveStyle(ctx);
315
+ const content = this.display(ctx.metrics);
316
+ const width = constraints.hasBoundedWidth
317
+ ? constraints.maxWidth
318
+ : 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);
297
323
  }
298
324
  /** 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. */
325
+ * Must match the breaker's one-line sum BIT for bit - it adds `width + (space + word)`, and the same
326
+ * numbers grouped differently drift by a sub-ULP, enough to wrap a text inside its own natural-width
327
+ * box. `singleLineWidth` is that sum. */
306
328
  naturalWidth(metrics) {
307
329
  // 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);
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);
309
333
  // The FIRST line starts `textIndent` in, so an unbounded box has to be that much wider.
310
334
  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 +
335
+ return this.display(metrics).reduce((sum, seg) => sum +
317
336
  oneLine(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontSize ?? this.fontSize, seg.fontStyle ?? this.fontStyle, seg.letterSpacing ?? this.letterSpacing), indent);
318
337
  }
319
338
  /**
@@ -340,14 +359,12 @@ export class TextElement extends SizedPDFElement {
340
359
  fontStyle: style,
341
360
  ligatures: this.ligatures,
342
361
  };
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]/);
362
+ // 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);
345
365
  return pieces.reduce((max, piece) => Math.max(max, runAdvance(metrics, piece, font, letterSpacing)), 0);
346
366
  };
347
367
  const content = this.display(metrics);
348
- if (typeof content === "string") {
349
- return widest(content, this.fontFamily, this.fontSize, this.fontStyle, this.letterSpacing);
350
- }
351
368
  // Segments join without a space, so a word may straddle two of them; taking the widest piece per
352
369
  // segment can therefore UNDER-estimate. That errs towards squeezing a little too far, which lands
353
370
  // back on the old overflow behaviour rather than on something new.
@@ -363,7 +380,11 @@ export class TextElement extends SizedPDFElement {
363
380
  fontFamily: this.fontFamily,
364
381
  fontStyle: this.fontStyle,
365
382
  color: this.color,
366
- content: this.resolved ?? this.content,
383
+ content: this.content,
384
+ /** The content as it is measured and drawn: spans, recased, split by font, drawable glyphs only. */
385
+ spans: this.resolved ?? this.display(),
386
+ /** How the text is broken - the settings it is measured with, so drawing breaks it the same. */
387
+ breaking: this.segmentDefaults(),
367
388
  textAlignment: this.textAlignment,
368
389
  maxLines: this.maxLines,
369
390
  orphans: this.orphans,
@@ -381,6 +402,7 @@ export class TextElement extends SizedPDFElement {
381
402
  ligatures: this.ligatures,
382
403
  hyphenate: this.hyphenate,
383
404
  role: this.role,
405
+ shownLines: this.shownLines,
384
406
  };
385
407
  }
386
408
  }
@@ -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. */
@@ -1,5 +1,6 @@
1
+ import { coverText } from "../text/glyph-coverage.js";
1
2
  import { NORMAL_STYLE, escName, num2, pdfColor } from "./pdf.js";
2
- import { checkboxOff, checkboxOn, listBoxFace, pushButtonFace, radioOff, radioOn, signatureFace, fieldDrawSize, textFieldFace, wrapFieldValue, } from "./appearance.js";
3
+ import { HELV, checkboxOff, checkboxOn, listBoxFace, pushButtonFace, radioOff, radioOn, signatureFace, fieldDrawSize, textFieldFace, wrapFieldValue, } from "./appearance.js";
3
4
  // AcroForm field flags (/Ff), by 1-based bit position per the PDF spec.
4
5
  const FF_READ_ONLY = 1 << 0; // bit 1
5
6
  const FF_REQUIRED = 1 << 1; // bit 2: must be filled in before the form is submitted
@@ -28,8 +29,12 @@ function boxChrome(node, extraMK = "") {
28
29
  out += ` /BS << /W ${num2(style.borderWidth)} /S /S >>`;
29
30
  return out;
30
31
  }
31
- /** The annotation flags (/F). Bit 2 = Hidden, bit 3 = Print. A widget prints by default; `hidden`
32
- * removes it from screen AND print, `print: false` keeps it on screen only. */
32
+ /** A widget in the structure tree (accessible tagging) names its slot in the ParentTree. */
33
+ function withStructParent(dict, structParent) {
34
+ return structParent === undefined
35
+ ? dict
36
+ : dict.replace(/^<< /, `<< /StructParent ${structParent} `);
37
+ }
33
38
  function annotFlags(f) {
34
39
  if (f.hidden)
35
40
  return 2;
@@ -162,9 +167,9 @@ function actionDict(a, om) {
162
167
  case "submit":
163
168
  // /Flags 4 = ExportFormat: post the field values as HTML form data rather than FDF, which is what
164
169
  // an ordinary web endpoint expects.
165
- return `/A << /S /SubmitForm /F << /FS /URL /F ${om.pdfString(a.url)} >> /Flags 4 >>`;
170
+ return `/A << /S /SubmitForm /F << /FS /URL /F ${om.pdfUri(a.url)} >> /Flags 4 >>`;
166
171
  case "url":
167
- return `/A << /S /URI /URI ${om.pdfString(a.url)} >>`;
172
+ return `/A << /S /URI /URI ${om.pdfUri(a.url)} >>`;
168
173
  }
169
174
  }
170
175
  /**
@@ -196,27 +201,66 @@ export class AcroFormCollector {
196
201
  drawSize(style, height, capHeight) {
197
202
  return fieldDrawSize(style.fontSize, height, style.borderWidth, capHeight);
198
203
  }
204
+ /**
205
+ * The font a field's baked appearance draws its text in: the font the field inherits when it is
206
+ * EMBEDDED, else the standard Helvetica. PDF/A and PDF/UA require every font that draws to be
207
+ * embedded, and a document that embeds its fonts means that. The /DA keeps naming Helvetica - it is
208
+ * what a viewer redraws with once someone types, and a subset font could not draw the new characters.
209
+ * Measuring and drawing both take the font from here.
210
+ */
211
+ faceFont(node, om) {
212
+ const inherited = node.font;
213
+ if (inherited && om.isCustomFont(inherited.family, inherited.style)) {
214
+ const { family, style } = inherited;
215
+ const resource = om.getCustomFontResource(family, style);
216
+ return {
217
+ family,
218
+ style,
219
+ // Drawn SHAPED (ligatures, joining forms) - the shaping `getStringWidth` measures with.
220
+ face: {
221
+ res: `F${resource.fontIndex}`,
222
+ encode: (text) => `<${om.encodeCustomText(family, text, style)}>`,
223
+ },
224
+ resources: `/Font << /F${resource.fontIndex} ${resource.resourceIndex} 0 R >>`,
225
+ // A glyph the font lacks would draw as .notdef, which PDF/A forbids.
226
+ cover: (text) => {
227
+ const { text: drawable, dropped } = coverText(text, family, style, om, false);
228
+ if (dropped.length > 0)
229
+ om.reportMissingGlyph?.(dropped);
230
+ return drawable;
231
+ },
232
+ };
233
+ }
234
+ return {
235
+ family: "Helvetica",
236
+ style: NORMAL_STYLE,
237
+ face: HELV,
238
+ resources: `/Font << /Helv ${this.helv(om)} 0 R >>`,
239
+ cover: (text) => text,
240
+ };
241
+ }
199
242
  /** Measure one line for the appearance generator. */
200
- line(om, text, size) {
201
- return { text, width: om.getStringWidth(text, "Helvetica", size, NORMAL_STYLE) };
243
+ line(om, font, text, size) {
244
+ return { text, width: om.getStringWidth(text, font.family, size, font.style) };
202
245
  }
203
246
  /** Bake a text field's value into an appearance stream; returns the XObject number. */
204
247
  bakeText(node, om) {
205
248
  const f = node.field;
206
249
  if (f.kind !== "text")
207
250
  throw new Error("bakeText: not a text field");
208
- const { capHeight } = om.getFontDecoration("Helvetica", NORMAL_STYLE);
251
+ const font = this.faceFont(node, om);
252
+ const { capHeight } = om.getFontDecoration(font.family, font.style);
209
253
  const size = this.drawSize(node.style, node.height, capHeight);
210
254
  // A password field shows the mask, never the characters - the value still lives in /V.
211
- const value = f.password ? "•".repeat([...(f.value ?? "")].length) : (f.value ?? "");
255
+ const value = font.cover(f.password ? "•".repeat([...(f.value ?? "")].length) : (f.value ?? ""));
212
256
  const innerWidth = Math.max(1, node.width - 2 * (2 + node.style.borderWidth));
213
257
  let lines = [];
214
258
  if (value) {
215
- const texts = wrapFieldValue(value, "Helvetica", size, NORMAL_STYLE, innerWidth, om, f.multiline ?? false);
216
- lines = texts.map((t) => this.line(om, t, size));
259
+ const texts = wrapFieldValue(value, font.family, size, font.style, innerWidth, om, f.multiline ?? false);
260
+ lines = texts.map((t) => this.line(om, font, t, size));
217
261
  }
218
- const face = textFieldFace(node.width, node.height, node.style, lines, capHeight, size, "Helv", f.multiline ?? false, f.align);
219
- return om.addFormXObject(`[0 0 ${num2(node.width)} ${num2(node.height)}]`, face, `/Font << /Helv ${this.helv(om)} 0 R >>`);
262
+ const face = textFieldFace(node.width, node.height, node.style, lines, capHeight, size, font.face, f.multiline ?? false, f.align);
263
+ return om.addFormXObject(`[0 0 ${num2(node.width)} ${num2(node.height)}]`, face, font.resources);
220
264
  }
221
265
  /** Bake a choice field: a combo shows its current value on one line, a list box shows its options
222
266
  * with the selected rows highlighted. */
@@ -224,24 +268,27 @@ export class AcroFormCollector {
224
268
  const f = node.field;
225
269
  if (f.kind !== "choice")
226
270
  throw new Error("bakeChoice: not a choice");
227
- const { capHeight } = om.getFontDecoration("Helvetica", NORMAL_STYLE);
271
+ const font = this.faceFont(node, om);
272
+ const { capHeight } = om.getFontDecoration(font.family, font.style);
228
273
  const size = this.drawSize(node.style, node.height, capHeight);
229
274
  const shown = (v) => f.options.find((o) => o.value === v)?.label ?? v;
230
275
  const chosen = choiceSelection(f);
231
276
  const selected = new Set(chosen);
232
277
  let face;
233
278
  if (f.combo) {
234
- const current = chosen.length ? [this.line(om, shown(chosen[0]), size)] : [];
235
- face = textFieldFace(node.width, node.height, node.style, current, capHeight, size, "Helv", false, f.align);
279
+ const current = chosen.length
280
+ ? [this.line(om, font, font.cover(shown(chosen[0])), size)]
281
+ : [];
282
+ face = textFieldFace(node.width, node.height, node.style, current, capHeight, size, font.face, false, f.align);
236
283
  }
237
284
  else {
238
285
  const rows = f.options.map((o) => ({
239
- ...this.line(om, o.label ?? o.value, size),
286
+ ...this.line(om, font, font.cover(o.label ?? o.value), size),
240
287
  selected: selected.has(o.value),
241
288
  }));
242
- face = listBoxFace(node.width, node.height, node.style, rows, capHeight, size, "Helv", f.align);
289
+ face = listBoxFace(node.width, node.height, node.style, rows, capHeight, size, font.face, f.align);
243
290
  }
244
- return om.addFormXObject(`[0 0 ${num2(node.width)} ${num2(node.height)}]`, face, `/Font << /Helv ${this.helv(om)} 0 R >>`);
291
+ return om.addFormXObject(`[0 0 ${num2(node.width)} ${num2(node.height)}]`, face, font.resources);
245
292
  }
246
293
  get isEmpty() {
247
294
  return this.fieldRefs.length === 0;
@@ -278,9 +325,9 @@ export class AcroFormCollector {
278
325
  }
279
326
  /** Emit the widget object for one form-field IR node, register it as a field, and return its object
280
327
  * number so the PageRenderer can add it to that page's /Annots. */
281
- addField(node, om) {
328
+ addField(node, om, structParent) {
282
329
  if (node.field.kind === "radio")
283
- return this.addRadio(node, om);
330
+ return this.addRadio(node, om, structParent);
284
331
  this.claimName(node.field.name, node.field.kind);
285
332
  let dict;
286
333
  if (node.field.kind === "checkbox") {
@@ -307,7 +354,7 @@ export class AcroFormCollector {
307
354
  if (!this.bake)
308
355
  this.needAppearances = true;
309
356
  }
310
- const objNum = om.addObject(dict);
357
+ const objNum = om.addObject(withStructParent(dict, structParent));
311
358
  this.fieldRefs.push(objNum);
312
359
  return objNum;
313
360
  }
@@ -318,13 +365,15 @@ export class AcroFormCollector {
318
365
  const f = node.field;
319
366
  if (f.kind !== "pushbutton")
320
367
  throw new Error("buildPushButtonWidget: not a push button");
321
- const { capHeight } = om.getFontDecoration("Helvetica", NORMAL_STYLE);
368
+ const font = this.faceFont(node, om);
369
+ const { capHeight } = om.getFontDecoration(font.family, font.style);
322
370
  // `fontSize: 0` means auto-size, here as everywhere: resolve it before measuring or drawing.
323
371
  const size = this.drawSize(node.style, node.height, capHeight);
324
- const width = om.getStringWidth(f.label, "Helvetica", size, NORMAL_STYLE);
325
- const fontNum = this.helv(om);
326
- const face = pushButtonFace(node.width, node.height, node.style, f.label, width, capHeight, "Helv", size);
327
- const apRef = om.addFormXObject(`[0 0 ${num2(node.width)} ${num2(node.height)}]`, face, `/Font << /Helv ${fontNum} 0 R >>`);
372
+ const caption = font.cover(f.label);
373
+ const width = om.getStringWidth(caption, font.family, size, font.style);
374
+ this.helv(om); // the /DA below names it
375
+ const face = pushButtonFace(node.width, node.height, node.style, caption, width, capHeight, font.face, size);
376
+ const apRef = om.addFormXObject(`[0 0 ${num2(node.width)} ${num2(node.height)}]`, face, font.resources);
328
377
  const parts = [`/Type /Annot /Subtype /Widget /FT /Btn`, `/T ${om.pdfString(f.name)}`];
329
378
  if (f.tooltip !== undefined)
330
379
  parts.push(`/TU ${om.pdfString(f.tooltip)}`);
@@ -343,7 +392,7 @@ export class AcroFormCollector {
343
392
  /** A radio button: one KID widget of its group's shared field. The group field is reserved on first
344
393
  * sight (and added to /Fields); each button adds itself to that field's /Kids. Returns the kid widget
345
394
  * number, which goes into the PAGE /Annots (the parent field does not). */
346
- addRadio(node, om) {
395
+ addRadio(node, om, structParent) {
347
396
  const f = node.field;
348
397
  if (f.kind !== "radio")
349
398
  throw new Error("addRadio: not a radio");
@@ -375,7 +424,7 @@ export class AcroFormCollector {
375
424
  `/AS /${escName(as)}`,
376
425
  `/DA ${om.pdfString(`/ZaDb 0 Tf ${pdfColor(node.style.color)} rg`)}`,
377
426
  ];
378
- const kidNum = om.addObject(`<< ${parts.join(" ")}${boxChrome(node)} >>`);
427
+ const kidNum = om.addObject(withStructParent(`<< ${parts.join(" ")}${boxChrome(node)} >>`, structParent));
379
428
  g.kids.push(kidNum);
380
429
  return kidNum;
381
430
  }
@@ -386,12 +435,12 @@ export class AcroFormCollector {
386
435
  if (f.kind !== "signature")
387
436
  throw new Error("buildSignatureWidget: not a signature");
388
437
  this.hasSignature = true;
389
- const label = f.label ?? "";
390
- const { capHeight } = om.getFontDecoration("Helvetica", NORMAL_STYLE);
438
+ const font = this.faceFont(node, om);
439
+ const label = font.cover(f.label ?? "");
440
+ const { capHeight } = om.getFontDecoration(font.family, font.style);
391
441
  const size = this.drawSize(node.style, node.height, capHeight);
392
- const labelWidth = label ? om.getStringWidth(label, "Helvetica", size, NORMAL_STYLE) : 0;
393
- const fontNum = this.helv(om);
394
- const apRef = om.addFormXObject(`[0 0 ${num2(node.width)} ${num2(node.height)}]`, signatureFace(node.width, node.height, node.style, label, labelWidth, "Helv", size), `/Font << /Helv ${fontNum} 0 R >>`);
442
+ const labelWidth = label ? om.getStringWidth(label, font.family, size, font.style) : 0;
443
+ const apRef = om.addFormXObject(`[0 0 ${num2(node.width)} ${num2(node.height)}]`, signatureFace(node.width, node.height, node.style, label, labelWidth, font.face, size), font.resources);
395
444
  const parts = [`/Type /Annot /Subtype /Widget /FT /Sig`, `/T ${om.pdfString(f.name)}`];
396
445
  if (f.tooltip !== undefined)
397
446
  parts.push(`/TU ${om.pdfString(f.tooltip)}`);