@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.
- package/dist/api/content.d.ts +5 -5
- package/dist/api/content.js +12 -6
- package/dist/api/font-url.d.ts +1 -2
- package/dist/api/font-url.js +8 -74
- package/dist/api/forms.d.ts +3 -0
- package/dist/api/forms.js +3 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +1 -0
- package/dist/api/layout.d.ts +6 -0
- package/dist/api/layout.js +7 -2
- package/dist/api/page-builder.d.ts +4 -0
- package/dist/api/page-builder.js +4 -0
- package/dist/api/structure.d.ts +10 -2
- package/dist/api/structure.js +9 -6
- package/dist/api/table.d.ts +5 -3
- package/dist/api/table.js +99 -22
- package/dist/common/diagnostics.d.ts +53 -0
- package/dist/common/diagnostics.js +77 -0
- package/dist/edit/acroform-reader.js +7 -7
- package/dist/edit/appearance.js +3 -3
- package/dist/edit/attachments.d.ts +52 -0
- package/dist/edit/attachments.js +178 -0
- package/dist/edit/document.d.ts +38 -8
- package/dist/edit/document.js +208 -51
- package/dist/edit/fill.js +8 -23
- package/dist/edit/filters.d.ts +15 -0
- package/dist/edit/filters.js +157 -0
- package/dist/edit/index.d.ts +4 -1
- package/dist/edit/index.js +3 -1
- package/dist/edit/objects.d.ts +3 -4
- package/dist/edit/objects.js +5 -18
- package/dist/elements/canvas-element.d.ts +3 -0
- package/dist/elements/canvas-element.js +14 -3
- package/dist/elements/container-element.d.ts +5 -1
- package/dist/elements/container-element.js +35 -15
- package/dist/elements/forms/checkbox-element.d.ts +2 -0
- package/dist/elements/forms/checkbox-element.js +12 -3
- package/dist/elements/forms/choice-element.d.ts +6 -1
- package/dist/elements/forms/choice-element.js +17 -6
- package/dist/elements/forms/field-font.d.ts +8 -0
- package/dist/elements/forms/field-font.js +5 -0
- package/dist/elements/forms/push-button-element.d.ts +6 -1
- package/dist/elements/forms/push-button-element.js +16 -4
- package/dist/elements/forms/radio-element.d.ts +2 -0
- package/dist/elements/forms/radio-element.js +12 -3
- package/dist/elements/forms/signature-element.d.ts +6 -1
- package/dist/elements/forms/signature-element.js +16 -4
- package/dist/elements/forms/text-field-element.d.ts +6 -1
- package/dist/elements/forms/text-field-element.js +16 -4
- package/dist/elements/image-element.d.ts +39 -1
- package/dist/elements/image-element.js +95 -12
- package/dist/elements/layout/anchor-element.d.ts +8 -10
- package/dist/elements/layout/anchor-element.js +10 -18
- package/dist/elements/layout/bookmark-element.d.ts +9 -11
- package/dist/elements/layout/bookmark-element.js +11 -20
- package/dist/elements/layout/default-text-style-element.d.ts +11 -13
- package/dist/elements/layout/default-text-style-element.js +14 -31
- package/dist/elements/layout/deferred-element.d.ts +12 -4
- package/dist/elements/layout/deferred-element.js +24 -7
- package/dist/elements/layout/expanded-element.d.ts +7 -0
- package/dist/elements/layout/expanded-element.js +40 -18
- package/dist/elements/layout/keep-together-element.d.ts +8 -18
- package/dist/elements/layout/keep-together-element.js +21 -45
- package/dist/elements/layout/link-element.d.ts +8 -13
- package/dist/elements/layout/link-element.js +9 -19
- package/dist/elements/layout/padding-element.d.ts +15 -1
- package/dist/elements/layout/padding-element.js +55 -27
- package/dist/elements/layout/page-break-element.d.ts +4 -1
- package/dist/elements/layout/page-break-element.js +6 -1
- package/dist/elements/layout/page-builder-element.d.ts +5 -0
- package/dist/elements/layout/page-builder-element.js +11 -0
- package/dist/elements/layout/positioned-element.d.ts +14 -2
- package/dist/elements/layout/positioned-element.js +36 -10
- package/dist/elements/layout/proxy-element.d.ts +43 -0
- package/dist/elements/layout/proxy-element.js +94 -0
- package/dist/elements/layout/repeating-header-element.d.ts +6 -0
- package/dist/elements/layout/repeating-header-element.js +40 -13
- package/dist/elements/layout/rotated-box-element.d.ts +6 -0
- package/dist/elements/layout/rotated-box-element.js +27 -14
- package/dist/elements/layout/rotated-element.d.ts +7 -0
- package/dist/elements/layout/rotated-element.js +25 -2
- package/dist/elements/layout/struct-group.d.ts +9 -17
- package/dist/elements/layout/struct-group.js +10 -35
- package/dist/elements/line-element.d.ts +1 -0
- package/dist/elements/line-element.js +6 -0
- package/dist/elements/page-element.d.ts +10 -3
- package/dist/elements/page-element.js +39 -31
- package/dist/elements/pdf-document-element.d.ts +3 -0
- package/dist/elements/pdf-document-element.js +7 -0
- package/dist/elements/pdf-element.d.ts +75 -15
- package/dist/elements/pdf-element.js +68 -18
- package/dist/elements/rectangle-element.d.ts +14 -0
- package/dist/elements/rectangle-element.js +97 -52
- package/dist/elements/row-element.d.ts +11 -0
- package/dist/elements/row-element.js +58 -8
- package/dist/elements/svg-element.d.ts +30 -6
- package/dist/elements/svg-element.js +88 -17
- package/dist/elements/text-element.d.ts +32 -13
- package/dist/elements/text-element.js +119 -97
- package/dist/forms/acroform.d.ts +9 -1
- package/dist/forms/acroform.js +83 -34
- package/dist/forms/appearance.d.ts +17 -6
- package/dist/forms/appearance.js +18 -16
- package/dist/ir/display-list.d.ts +12 -2
- package/dist/layout/box-constraints.d.ts +5 -0
- package/dist/layout/box-constraints.js +14 -0
- package/dist/layout/collect-images.d.ts +6 -4
- package/dist/layout/collect-images.js +6 -4
- package/dist/layout/flex-item.d.ts +9 -0
- package/dist/layout/flex-item.js +39 -0
- package/dist/layout/fragmentation.d.ts +12 -4
- package/dist/layout/fragmentation.js +37 -14
- package/dist/layout/layout-cache.d.ts +53 -0
- package/dist/layout/layout-cache.js +107 -0
- package/dist/renderer/anchor-renderer.d.ts +2 -1
- package/dist/renderer/anchor-renderer.js +5 -4
- package/dist/renderer/bookmark-renderer.d.ts +2 -1
- package/dist/renderer/bookmark-renderer.js +5 -4
- package/dist/renderer/canvas-renderer.d.ts +2 -1
- package/dist/renderer/canvas-renderer.js +5 -2
- package/dist/renderer/container-renderer.d.ts +2 -1
- package/dist/renderer/container-renderer.js +6 -5
- package/dist/renderer/default-text-style-renderer.d.ts +2 -1
- package/dist/renderer/default-text-style-renderer.js +2 -3
- package/dist/renderer/deferred-renderer.d.ts +2 -1
- package/dist/renderer/deferred-renderer.js +2 -3
- package/dist/renderer/expanded-renderer.d.ts +2 -1
- package/dist/renderer/expanded-renderer.js +2 -3
- package/dist/renderer/form-field-renderer.d.ts +2 -1
- package/dist/renderer/form-field-renderer.js +16 -3
- package/dist/renderer/image-renderer.d.ts +2 -1
- package/dist/renderer/image-renderer.js +5 -2
- package/dist/renderer/keep-together-renderer.d.ts +2 -1
- package/dist/renderer/keep-together-renderer.js +2 -3
- package/dist/renderer/line-renderer.d.ts +2 -1
- package/dist/renderer/line-renderer.js +7 -2
- package/dist/renderer/link-renderer.d.ts +2 -1
- package/dist/renderer/link-renderer.js +36 -5
- package/dist/renderer/padding-renderer.d.ts +2 -1
- package/dist/renderer/padding-renderer.js +5 -5
- package/dist/renderer/page-break-renderer.d.ts +6 -5
- package/dist/renderer/page-break-renderer.js +14 -7
- package/dist/renderer/page-builder-renderer.d.ts +2 -1
- package/dist/renderer/page-builder-renderer.js +2 -3
- package/dist/renderer/page-renderer.js +28 -7
- package/dist/renderer/pdf-backend.d.ts +4 -3
- package/dist/renderer/pdf-backend.js +5 -5
- package/dist/renderer/pdf-document-renderer.d.ts +1 -8
- package/dist/renderer/pdf-document-renderer.js +15 -16
- package/dist/renderer/pdf-renderer.js +11 -11
- package/dist/renderer/positioned-renderer.d.ts +6 -1
- package/dist/renderer/positioned-renderer.js +6 -5
- package/dist/renderer/rectangle-renderer.d.ts +2 -1
- package/dist/renderer/rectangle-renderer.js +9 -4
- package/dist/renderer/repeating-header-renderer.d.ts +2 -1
- package/dist/renderer/repeating-header-renderer.js +6 -5
- package/dist/renderer/rotated-renderer.d.ts +2 -1
- package/dist/renderer/rotated-renderer.js +8 -5
- package/dist/renderer/row-renderer.d.ts +2 -1
- package/dist/renderer/row-renderer.js +6 -5
- package/dist/renderer/struct-group-renderer.d.ts +2 -1
- package/dist/renderer/struct-group-renderer.js +2 -3
- package/dist/renderer/svg-renderer.d.ts +2 -1
- package/dist/renderer/svg-renderer.js +8 -2
- package/dist/renderer/text-renderer.d.ts +4 -6
- package/dist/renderer/text-renderer.js +101 -111
- package/dist/svg/gradients.js +5 -0
- package/dist/svg/index.js +10 -1
- package/dist/svg/style.d.ts +2 -0
- package/dist/svg/style.js +11 -0
- package/dist/text/advance.d.ts +4 -0
- package/dist/text/advance.js +17 -4
- package/dist/text/bidi.d.ts +0 -2
- package/dist/text/bidi.js +0 -10
- package/dist/text/glyph-coverage.d.ts +1 -1
- package/dist/text/glyph-coverage.js +7 -3
- package/dist/text/hyphen-breaks.d.ts +17 -0
- package/dist/text/hyphen-breaks.js +54 -0
- package/dist/text/line-breaker.d.ts +34 -31
- package/dist/text/line-breaker.js +364 -176
- package/dist/text/line-metrics.d.ts +6 -3
- package/dist/text/line-metrics.js +7 -2
- package/dist/text/pdf-text-string.d.ts +21 -0
- package/dist/text/pdf-text-string.js +114 -0
- package/dist/text/text-decoration.d.ts +5 -5
- package/dist/text/text-decoration.js +5 -5
- package/dist/text/word-splitting.d.ts +15 -2
- package/dist/text/word-splitting.js +34 -8
- package/dist/utils/afm-parser.d.ts +2 -0
- package/dist/utils/afm-parser.js +10 -1
- package/dist/utils/dest-registry.d.ts +1 -1
- package/dist/utils/dest-registry.js +3 -2
- package/dist/utils/fetch-bytes.d.ts +19 -0
- package/dist/utils/fetch-bytes.js +79 -0
- package/dist/utils/flex-layout.d.ts +13 -13
- package/dist/utils/flex-layout.js +318 -216
- package/dist/utils/font-metrics.d.ts +4 -3
- package/dist/utils/image-helper.js +2 -1
- package/dist/utils/pdf-object-manager.d.ts +11 -3
- package/dist/utils/pdf-object-manager.js +39 -13
- package/dist/utils/renderer-registry.d.ts +43 -2
- package/dist/utils/renderer-registry.js +40 -0
- package/dist/utils/struct-tree.d.ts +24 -0
- package/dist/utils/struct-tree.js +61 -9
- package/dist/utils/ttf-parser.d.ts +4 -0
- package/dist/utils/ttf-parser.js +27 -4
- package/dist/validators/element-validator.js +2 -1
- package/package.json +1 -1
- package/dist/elements/layout/sized-container-element.d.ts +0 -14
- 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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
245
|
-
remainder: this.cloneWithContent(
|
|
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
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
this.height =
|
|
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
|
|
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
|
|
300
|
-
*
|
|
301
|
-
*
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
}
|
package/dist/forms/acroform.d.ts
CHANGED
|
@@ -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. */
|
package/dist/forms/acroform.js
CHANGED
|
@@ -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
|
-
/**
|
|
32
|
-
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
219
|
-
return om.addFormXObject(`[0 0 ${num2(node.width)} ${num2(node.height)}]`, face,
|
|
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
|
|
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
|
|
235
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
const
|
|
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
|
|
390
|
-
const
|
|
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,
|
|
393
|
-
const
|
|
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)}`);
|