@jasy/pdf 1.0.0-beta.2 → 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 +40 -13
- package/dist/elements/text-element.js +144 -99
- 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 +49 -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 +3 -0
- package/dist/utils/afm-parser.js +19 -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 +41 -15
- 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;
|
|
@@ -73,19 +76,39 @@ export class TextElement extends SizedPDFElement {
|
|
|
73
76
|
* same width in most fonts).
|
|
74
77
|
*/
|
|
75
78
|
display(metrics) {
|
|
79
|
+
const memo = this.displayed;
|
|
80
|
+
if (metrics &&
|
|
81
|
+
memo &&
|
|
82
|
+
memo.metrics === metrics &&
|
|
83
|
+
// A face registered since then changes what is drawable and how the fallback stack splits.
|
|
84
|
+
memo.epoch === (metrics.fontEpoch ?? 0) &&
|
|
85
|
+
memo.family === this.fontFamily &&
|
|
86
|
+
memo.style === this.fontStyle &&
|
|
87
|
+
memo.transform === this.textTransform &&
|
|
88
|
+
memo.fallback === this.fontFallback)
|
|
89
|
+
return memo.value;
|
|
76
90
|
const cased = this.recased();
|
|
77
91
|
// A SPAN may bring its own stack, so the element's being empty is not enough to skip the pass.
|
|
78
|
-
const anyStack = this.fontFallback.length > 0 ||
|
|
79
|
-
(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);
|
|
80
93
|
if (!metrics)
|
|
81
94
|
return cased;
|
|
95
|
+
const remember = (value) => {
|
|
96
|
+
this.displayed = {
|
|
97
|
+
metrics,
|
|
98
|
+
epoch: metrics.fontEpoch ?? 0,
|
|
99
|
+
family: this.fontFamily,
|
|
100
|
+
style: this.fontStyle,
|
|
101
|
+
transform: this.textTransform,
|
|
102
|
+
fallback: this.fontFallback,
|
|
103
|
+
value,
|
|
104
|
+
};
|
|
105
|
+
return value;
|
|
106
|
+
};
|
|
82
107
|
if (!anyStack)
|
|
83
|
-
return this.covered(cased, metrics);
|
|
84
|
-
// Font fallback
|
|
85
|
-
// knows how to handle. Nothing new downstream.
|
|
86
|
-
const pieces = typeof cased === "string" ? [{ content: cased }] : cased;
|
|
108
|
+
return remember(this.covered(cased, metrics));
|
|
109
|
+
// Font fallback splits a span into more spans - one per family. Nothing new downstream.
|
|
87
110
|
const out = [];
|
|
88
|
-
for (const seg of
|
|
111
|
+
for (const seg of cased) {
|
|
89
112
|
const runs = splitByFont(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontFallback ?? this.fontFallback, seg.fontStyle ?? this.fontStyle, metrics);
|
|
90
113
|
if (!runs)
|
|
91
114
|
out.push(seg);
|
|
@@ -93,7 +116,7 @@ export class TextElement extends SizedPDFElement {
|
|
|
93
116
|
for (const run of runs)
|
|
94
117
|
out.push({ ...seg, content: run.text, fontFamily: run.fontFamily });
|
|
95
118
|
}
|
|
96
|
-
return this.covered(out, metrics);
|
|
119
|
+
return remember(this.covered(out, metrics));
|
|
97
120
|
}
|
|
98
121
|
/**
|
|
99
122
|
* Drops or substitutes whatever the RESOLVED font cannot draw. Runs last, after the fallback stack
|
|
@@ -102,30 +125,26 @@ export class TextElement extends SizedPDFElement {
|
|
|
102
125
|
* back unchanged, so an ordinary document is byte-identical.
|
|
103
126
|
*/
|
|
104
127
|
covered(content, metrics) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
report(dropped);
|
|
110
|
-
return text;
|
|
111
|
-
}
|
|
112
|
-
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) => {
|
|
113
132
|
const { text, dropped } = coverText(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontStyle ?? this.fontStyle, metrics);
|
|
114
|
-
|
|
115
|
-
report(dropped);
|
|
133
|
+
lost.push(...dropped);
|
|
116
134
|
return text === seg.content ? seg : { ...seg, content: text };
|
|
117
135
|
});
|
|
136
|
+
this.lostGlyphs = [...new Set(lost)];
|
|
137
|
+
return covered;
|
|
118
138
|
}
|
|
119
|
-
/** The content with `text-transform` applied, and nothing else. */
|
|
139
|
+
/** The content as spans, with `text-transform` applied, and nothing else. */
|
|
120
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;
|
|
121
143
|
if (this.textTransform === "none")
|
|
122
|
-
return
|
|
123
|
-
if (typeof this.content === "string") {
|
|
124
|
-
return applyTextTransform(this.content, this.textTransform).text;
|
|
125
|
-
}
|
|
144
|
+
return spans;
|
|
126
145
|
// The capitalisation state runs THROUGH the spans: `span("hel") + span("lo")` is one word.
|
|
127
146
|
let atWordStart = true;
|
|
128
|
-
return
|
|
147
|
+
return spans.map((seg) => {
|
|
129
148
|
const done = applyTextTransform(seg.content, this.textTransform, atWordStart);
|
|
130
149
|
atWordStart = done.atWordStart;
|
|
131
150
|
return { ...seg, content: done.text };
|
|
@@ -139,6 +158,7 @@ export class TextElement extends SizedPDFElement {
|
|
|
139
158
|
ligatures: this.ligatures,
|
|
140
159
|
// Justified lines may be squeezed to keep a word; every other alignment gets no slack.
|
|
141
160
|
shrink: this.textAlignment === HorizontalAlignment.justify ? MAX_SPACE_SHRINK : 0,
|
|
161
|
+
shownLines: this.shownLines,
|
|
142
162
|
};
|
|
143
163
|
}
|
|
144
164
|
applyStyle(ts) {
|
|
@@ -165,43 +185,15 @@ export class TextElement extends SizedPDFElement {
|
|
|
165
185
|
* Splits the paragraph at line boxes (Slice 1). The lines that fit in `maxHeight` stay;
|
|
166
186
|
* the rest become a remainder `TextElement` re-wrapped on the next page. If not even one
|
|
167
187
|
* line fits, nothing is forced here - the caller (the container) decides whether to move
|
|
168
|
-
* the whole element on for progress.
|
|
188
|
+
* the whole element on for progress.
|
|
169
189
|
*/
|
|
170
190
|
fragment(maxHeight, width, ctx) {
|
|
171
191
|
this.resolveStyle(ctx);
|
|
172
192
|
const content = this.display(ctx.metrics);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// Plain string: every wrapped line gets the same box (the same one calculateTextHeight uses),
|
|
178
|
-
// so `floor(maxHeight / lineBox)` lines fit.
|
|
179
|
-
fragmentString(content, maxHeight, width, ctx) {
|
|
180
|
-
const lines = wrapStringIntoLines(content, this.fontFamily, this.fontSize, this.fontStyle, width, ctx.metrics, this.maxLines, this.overflow, this.letterSpacing, this.lineOptions());
|
|
181
|
-
const box = lineBoxForString(ctx.metrics, this.fontFamily, this.fontStyle, this.fontSize, this.lineHeight);
|
|
182
|
-
const fittedLineCount = adjustForOrphansWidows(Math.floor(maxHeight / box.height), lines.length, this.breakRule());
|
|
183
|
-
if (fittedLineCount <= 0)
|
|
184
|
-
return { fitted: null, remainder: this };
|
|
185
|
-
if (fittedLineCount >= lines.length)
|
|
186
|
-
return { fitted: this, remainder: null };
|
|
187
|
-
return {
|
|
188
|
-
fitted: this.cloneWithContent(lines.slice(0, fittedLineCount).join(" ")),
|
|
189
|
-
// A continuation is no longer the paragraph's FIRST line, so it carries no indent.
|
|
190
|
-
remainder: this.cloneWithContent(lines.slice(fittedLineCount).join(" "), 0),
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
// Styled segments: each line's height comes from the fonts on it, so pack lines by cumulative
|
|
194
|
-
// box height. Rebuild the fitted/remainder halves back into TextSegment[].
|
|
195
|
-
fragmentSegments(content, maxHeight, width, ctx) {
|
|
196
|
-
const defaults = {
|
|
197
|
-
fontFamily: this.fontFamily,
|
|
198
|
-
fontSize: this.fontSize,
|
|
199
|
-
fontStyle: this.fontStyle,
|
|
200
|
-
letterSpacing: this.letterSpacing,
|
|
201
|
-
direction: this.direction,
|
|
202
|
-
wordSpacing: this.wordSpacing,
|
|
203
|
-
textIndent: this.textIndent,
|
|
204
|
-
};
|
|
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();
|
|
205
197
|
const lines = breakSegmentsIntoLines(content, defaults, width, ctx.metrics, this.maxLines, this.overflow);
|
|
206
198
|
let used = 0;
|
|
207
199
|
let packed = 0;
|
|
@@ -217,9 +209,35 @@ export class TextElement extends SizedPDFElement {
|
|
|
217
209
|
return { fitted: null, remainder: this };
|
|
218
210
|
if (fittedLineCount >= lines.length)
|
|
219
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];
|
|
220
226
|
return {
|
|
221
|
-
fitted
|
|
222
|
-
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(),
|
|
223
241
|
};
|
|
224
242
|
}
|
|
225
243
|
/** The orphan/widow minimums for this paragraph; unset means the CSS default of 2 lines each. */
|
|
@@ -231,7 +249,7 @@ export class TextElement extends SizedPDFElement {
|
|
|
231
249
|
}
|
|
232
250
|
// A copy carrying the same style but different (already-wrapped) content. Re-wrapping at
|
|
233
251
|
// the same width reproduces exactly those lines (greedy is deterministic).
|
|
234
|
-
cloneWithContent(content, indent = this.textIndent) {
|
|
252
|
+
cloneWithContent(content, indent = this.textIndent, piece = {}) {
|
|
235
253
|
return new TextElement({
|
|
236
254
|
content,
|
|
237
255
|
fontSize: this.fontSize,
|
|
@@ -239,7 +257,7 @@ export class TextElement extends SizedPDFElement {
|
|
|
239
257
|
fontStyle: this.fontStyle,
|
|
240
258
|
color: this.color,
|
|
241
259
|
textAlignment: this.textAlignment,
|
|
242
|
-
maxLines: this.maxLines,
|
|
260
|
+
maxLines: piece.maxLines ?? this.maxLines,
|
|
243
261
|
orphans: this.orphans,
|
|
244
262
|
widows: this.widows,
|
|
245
263
|
overflow: this.overflow,
|
|
@@ -252,45 +270,69 @@ export class TextElement extends SizedPDFElement {
|
|
|
252
270
|
wordSpacing: this.wordSpacing,
|
|
253
271
|
textIndent: indent,
|
|
254
272
|
role: this.role,
|
|
255
|
-
|
|
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)
|
|
256
282
|
}
|
|
257
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.
|
|
258
288
|
this.resolveStyle(ctx);
|
|
289
|
+
const size = this.measure(constraints, ctx);
|
|
259
290
|
this.x = offset.x;
|
|
260
291
|
this.y = offset.y;
|
|
261
|
-
// Bounded width (e.g. inside a Column) wraps to that width; an unbounded width
|
|
262
|
-
// (e.g. inside a Row) means the text takes its natural single-line width and does
|
|
263
|
-
// not wrap. Columns always bound the width, so this leaves their layout untouched.
|
|
264
292
|
this.resolved = this.display(ctx.metrics);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
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;
|
|
270
298
|
// Top-left coordinates (y = top of the text box). The baseline offset and the
|
|
271
299
|
// Y-flip are applied downstream (the line-builder positions baselines, the seam
|
|
272
300
|
// flips to PDF), so the element stays coordinate-system-blind.
|
|
273
|
-
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);
|
|
274
323
|
}
|
|
275
324
|
/** The unwrapped single-line width of the content (used when width is unbounded, e.g. inside a Row).
|
|
276
|
-
* Must match the
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
* into one term; we must group the same way. Adding word and space as two separate steps is
|
|
280
|
-
* algebraically equal but, because floating-point addition is not associative, drifts by a sub-ULP
|
|
281
|
-
* - enough to tip a borderline string (e.g. "20 Jun 2026", wider than "04 Jul 2026" only because
|
|
282
|
-
* '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. */
|
|
283
328
|
naturalWidth(metrics) {
|
|
284
329
|
// The breaker's own sum, to the last bit - see `singleLineWidth`.
|
|
285
|
-
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);
|
|
286
333
|
// The FIRST line starts `textIndent` in, so an unbounded box has to be that much wider.
|
|
287
334
|
const indent = Math.max(0, this.textIndent);
|
|
288
|
-
|
|
289
|
-
if (typeof content === "string") {
|
|
290
|
-
return (indent +
|
|
291
|
-
oneLine(content, this.fontFamily, this.fontSize, this.fontStyle, this.letterSpacing));
|
|
292
|
-
}
|
|
293
|
-
return content.reduce((sum, seg) => sum +
|
|
335
|
+
return this.display(metrics).reduce((sum, seg) => sum +
|
|
294
336
|
oneLine(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontSize ?? this.fontSize, seg.fontStyle ?? this.fontStyle, seg.letterSpacing ?? this.letterSpacing), indent);
|
|
295
337
|
}
|
|
296
338
|
/**
|
|
@@ -317,14 +359,12 @@ export class TextElement extends SizedPDFElement {
|
|
|
317
359
|
fontStyle: style,
|
|
318
360
|
ligatures: this.ligatures,
|
|
319
361
|
};
|
|
320
|
-
// Split on the same boundaries the breaker uses - a hard break ends a line too
|
|
321
|
-
|
|
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);
|
|
322
365
|
return pieces.reduce((max, piece) => Math.max(max, runAdvance(metrics, piece, font, letterSpacing)), 0);
|
|
323
366
|
};
|
|
324
367
|
const content = this.display(metrics);
|
|
325
|
-
if (typeof content === "string") {
|
|
326
|
-
return widest(content, this.fontFamily, this.fontSize, this.fontStyle, this.letterSpacing);
|
|
327
|
-
}
|
|
328
368
|
// Segments join without a space, so a word may straddle two of them; taking the widest piece per
|
|
329
369
|
// segment can therefore UNDER-estimate. That errs towards squeezing a little too far, which lands
|
|
330
370
|
// back on the old overflow behaviour rather than on something new.
|
|
@@ -340,7 +380,11 @@ export class TextElement extends SizedPDFElement {
|
|
|
340
380
|
fontFamily: this.fontFamily,
|
|
341
381
|
fontStyle: this.fontStyle,
|
|
342
382
|
color: this.color,
|
|
343
|
-
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(),
|
|
344
388
|
textAlignment: this.textAlignment,
|
|
345
389
|
maxLines: this.maxLines,
|
|
346
390
|
orphans: this.orphans,
|
|
@@ -358,6 +402,7 @@ export class TextElement extends SizedPDFElement {
|
|
|
358
402
|
ligatures: this.ligatures,
|
|
359
403
|
hyphenate: this.hyphenate,
|
|
360
404
|
role: this.role,
|
|
405
|
+
shownLines: this.shownLines,
|
|
361
406
|
};
|
|
362
407
|
}
|
|
363
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)}`);
|