@jasy/pdf 1.0.0-alpha.6 → 1.0.0-alpha.8
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/README.md +7 -7
- package/dist/api/descriptor.js +26 -1
- package/dist/api/forms.d.ts +347 -0
- package/dist/api/forms.js +189 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/layout.d.ts +47 -14
- package/dist/api/layout.js +47 -11
- package/dist/api/structure.d.ts +11 -0
- package/dist/api/structure.js +2 -0
- package/dist/api/text.d.ts +18 -2
- package/dist/api/text.js +15 -0
- package/dist/crypto/legacy-handler.d.ts +43 -0
- package/dist/crypto/legacy-handler.js +145 -0
- package/dist/crypto/security-handler.d.ts +16 -0
- package/dist/crypto/security-handler.js +26 -0
- package/dist/edit/acroform-reader.d.ts +70 -0
- package/dist/edit/acroform-reader.js +126 -0
- package/dist/edit/appearance.d.ts +47 -0
- package/dist/edit/appearance.js +171 -0
- package/dist/edit/document.d.ts +135 -0
- package/dist/edit/document.js +649 -0
- package/dist/edit/fill.d.ts +48 -0
- package/dist/edit/fill.js +395 -0
- package/dist/edit/flatten.d.ts +42 -0
- package/dist/edit/flatten.js +437 -0
- package/dist/edit/index.d.ts +18 -0
- package/dist/edit/index.js +16 -0
- package/dist/edit/lexer.d.ts +30 -0
- package/dist/edit/lexer.js +285 -0
- package/dist/edit/objects.d.ts +63 -0
- package/dist/edit/objects.js +56 -0
- package/dist/edit/writer.d.ts +53 -0
- package/dist/edit/writer.js +261 -0
- package/dist/elements/container-element.d.ts +10 -1
- package/dist/elements/container-element.js +26 -8
- package/dist/elements/forms/checkbox-element.d.ts +51 -0
- package/dist/elements/forms/checkbox-element.js +48 -0
- package/dist/elements/forms/choice-element.d.ts +53 -0
- package/dist/elements/forms/choice-element.js +59 -0
- package/dist/elements/forms/push-button-element.d.ts +56 -0
- package/dist/elements/forms/push-button-element.js +49 -0
- package/dist/elements/forms/radio-element.d.ts +52 -0
- package/dist/elements/forms/radio-element.js +49 -0
- package/dist/elements/forms/signature-element.d.ts +54 -0
- package/dist/elements/forms/signature-element.js +47 -0
- package/dist/elements/forms/text-field-element.d.ts +61 -0
- package/dist/elements/forms/text-field-element.js +54 -0
- package/dist/elements/index.d.ts +8 -0
- package/dist/elements/index.js +8 -0
- package/dist/elements/layout/keep-together-element.d.ts +42 -0
- package/dist/elements/layout/keep-together-element.js +85 -0
- package/dist/elements/layout/padding-element.d.ts +3 -0
- package/dist/elements/layout/padding-element.js +12 -0
- package/dist/elements/layout/page-break-element.d.ts +17 -0
- package/dist/elements/layout/page-break-element.js +24 -0
- package/dist/elements/layout/positioned-element.d.ts +10 -7
- package/dist/elements/layout/positioned-element.js +21 -9
- package/dist/elements/page-element.d.ts +11 -1
- package/dist/elements/page-element.js +41 -16
- package/dist/elements/pdf-element.d.ts +28 -0
- package/dist/elements/pdf-element.js +32 -0
- package/dist/elements/rectangle-element.d.ts +10 -1
- package/dist/elements/rectangle-element.js +27 -6
- package/dist/elements/row-element.d.ts +9 -1
- package/dist/elements/row-element.js +9 -1
- package/dist/elements/text-element.d.ts +30 -4
- package/dist/elements/text-element.js +40 -17
- package/dist/forms/acroform.d.ts +63 -0
- package/dist/forms/acroform.js +427 -0
- package/dist/forms/appearance.d.ts +58 -0
- package/dist/forms/appearance.js +204 -0
- package/dist/forms/field.d.ts +164 -0
- package/dist/forms/field.js +1 -0
- package/dist/forms/pdf.d.ts +19 -0
- package/dist/forms/pdf.js +28 -0
- package/dist/ir/display-list.d.ts +18 -1
- package/dist/layout/fragmentation.d.ts +12 -0
- package/dist/layout/fragmentation.js +45 -8
- package/dist/renderer/form-field-renderer.d.ts +20 -0
- package/dist/renderer/form-field-renderer.js +12 -0
- package/dist/renderer/keep-together-renderer.d.ts +10 -0
- package/dist/renderer/keep-together-renderer.js +12 -0
- package/dist/renderer/page-break-renderer.d.ts +14 -0
- package/dist/renderer/page-break-renderer.js +15 -0
- package/dist/renderer/page-renderer.js +8 -2
- package/dist/renderer/pdf-backend.d.ts +7 -0
- package/dist/renderer/pdf-backend.js +48 -6
- package/dist/renderer/pdf-document-class.js +3 -0
- package/dist/renderer/pdf-document-renderer.js +26 -7
- package/dist/renderer/pdf-renderer.js +25 -1
- package/dist/renderer/text-renderer.d.ts +1 -1
- package/dist/renderer/text-renderer.js +107 -59
- package/dist/text/advance.d.ts +34 -0
- package/dist/text/advance.js +42 -0
- package/dist/text/line-breaker.d.ts +9 -8
- package/dist/text/line-breaker.js +28 -21
- package/dist/text/line-metrics.d.ts +49 -0
- package/dist/text/line-metrics.js +57 -0
- package/dist/text/text-decoration.d.ts +50 -0
- package/dist/text/text-decoration.js +62 -0
- package/dist/text/text-style.d.ts +13 -3
- package/dist/text/text-style.js +10 -3
- package/dist/utils/afm-parser.d.ts +34 -0
- package/dist/utils/afm-parser.js +67 -0
- package/dist/utils/dest-registry.d.ts +5 -2
- package/dist/utils/dest-registry.js +5 -5
- package/dist/utils/font-metrics.d.ts +14 -0
- package/dist/utils/outline.js +1 -2
- package/dist/utils/pdf-object-manager.d.ts +45 -1
- package/dist/utils/pdf-object-manager.js +193 -26
- package/dist/utils/struct-tree.js +2 -3
- package/dist/utils/ttf-parser.d.ts +53 -0
- package/dist/utils/ttf-parser.js +368 -0
- package/dist/validators/element-validator.js +7 -3
- package/package.json +9 -2
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { escPdf, num2, pdfColor } from "./pdf.js";
|
|
2
|
+
import { wrapStringIntoLines } from "../text/line-breaker.js";
|
|
3
|
+
// Appearance streams (/AP): the actual drawn content of a field's box, in the field's OWN coordinate
|
|
4
|
+
// space (bottom-left origin, [0 0 w h]). Shared between CREATE (bake the field's look now) and, later,
|
|
5
|
+
// FILL-existing (regenerate a field's look after setting its value). This is why it is its own module.
|
|
6
|
+
/** Draw the field box: background fill + border stroke, inset so the stroke stays inside the BBox. */
|
|
7
|
+
function box(w, h, style) {
|
|
8
|
+
const ops = [];
|
|
9
|
+
if (style.background)
|
|
10
|
+
ops.push(`${pdfColor(style.background)} rg 0 0 ${num2(w)} ${num2(h)} re f`);
|
|
11
|
+
if (style.border && style.borderWidth > 0) {
|
|
12
|
+
const bw = style.borderWidth;
|
|
13
|
+
const i = bw / 2;
|
|
14
|
+
ops.push(`${pdfColor(style.border)} RG ${num2(bw)} w ${num2(i)} ${num2(i)} ${num2(w - bw)} ${num2(h - bw)} re S`);
|
|
15
|
+
}
|
|
16
|
+
return ops.join("\n");
|
|
17
|
+
}
|
|
18
|
+
/** The unchecked appearance: just the box. */
|
|
19
|
+
export function checkboxOff(w, h, style) {
|
|
20
|
+
return box(w, h, style);
|
|
21
|
+
}
|
|
22
|
+
/** The checked appearance: the box plus a vector checkmark (font-free, so it renders everywhere). */
|
|
23
|
+
export function checkboxOn(w, h, style) {
|
|
24
|
+
const lw = Math.max(1, Math.min(w, h) * 0.12);
|
|
25
|
+
// A tick: down-stroke to the low point, then up-stroke to the high point. Round caps/joins (2 J / 1 j).
|
|
26
|
+
const p = (fx, fy) => `${num2(fx * w)} ${num2(fy * h)}`;
|
|
27
|
+
const tick = `${pdfColor(style.color)} RG ${num2(lw)} w 2 J 1 j ` +
|
|
28
|
+
`${p(0.22, 0.52)} m ${p(0.42, 0.3)} l ${p(0.78, 0.74)} l S`;
|
|
29
|
+
return `${box(w, h, style)}\n${tick}`;
|
|
30
|
+
}
|
|
31
|
+
// A circle path (4 cubic-bezier arcs, kappa = 0.5523) around (cx, cy) with radius r. Not stroked/filled -
|
|
32
|
+
// the caller appends `S` (stroke) or `f` (fill).
|
|
33
|
+
function circlePath(cx, cy, r) {
|
|
34
|
+
const k = 0.5523 * r;
|
|
35
|
+
const n = num2;
|
|
36
|
+
return (`${n(cx + r)} ${n(cy)} m ` +
|
|
37
|
+
`${n(cx + r)} ${n(cy + k)} ${n(cx + k)} ${n(cy + r)} ${n(cx)} ${n(cy + r)} c ` +
|
|
38
|
+
`${n(cx - k)} ${n(cy + r)} ${n(cx - r)} ${n(cy + k)} ${n(cx - r)} ${n(cy)} c ` +
|
|
39
|
+
`${n(cx - r)} ${n(cy - k)} ${n(cx - k)} ${n(cy - r)} ${n(cx)} ${n(cy - r)} c ` +
|
|
40
|
+
`${n(cx + k)} ${n(cy - r)} ${n(cx + r)} ${n(cy - k)} ${n(cx + r)} ${n(cy)} c`);
|
|
41
|
+
}
|
|
42
|
+
/** The ring of a radio button (the outline), in [0 0 w h]. */
|
|
43
|
+
function radioRing(w, h, style) {
|
|
44
|
+
const r = Math.min(w, h) / 2;
|
|
45
|
+
const bw = style.borderWidth > 0 ? style.borderWidth : 1;
|
|
46
|
+
const ops = [];
|
|
47
|
+
if (style.background)
|
|
48
|
+
ops.push(`${pdfColor(style.background)} rg ${circlePath(w / 2, h / 2, r)} f`);
|
|
49
|
+
const stroke = style.border ?? style.color;
|
|
50
|
+
ops.push(`${pdfColor(stroke)} RG ${num2(bw)} w ${circlePath(w / 2, h / 2, r - bw / 2)} S`);
|
|
51
|
+
return ops.join("\n");
|
|
52
|
+
}
|
|
53
|
+
/** An unselected radio button: just the ring. */
|
|
54
|
+
export function radioOff(w, h, style) {
|
|
55
|
+
return radioRing(w, h, style);
|
|
56
|
+
}
|
|
57
|
+
/** A selected radio button: the ring plus a filled inner dot. */
|
|
58
|
+
export function radioOn(w, h, style) {
|
|
59
|
+
const r = Math.min(w, h) / 2;
|
|
60
|
+
const dot = `${pdfColor(style.color)} rg ${circlePath(w / 2, h / 2, r * 0.45)} f`;
|
|
61
|
+
return `${radioRing(w, h, style)}\n${dot}`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A push button's face: the box plus its caption, centred. The caller measures the caption (this module
|
|
65
|
+
* stays free of font metrics and the writer) and passes `captionWidth` in points and `capHeight` as an em
|
|
66
|
+
* fraction; the baseline is placed so the CAPITALS sit optically centred, not the em box. `fontRes` is
|
|
67
|
+
* the font's resource name inside the appearance stream's own /Resources.
|
|
68
|
+
*/
|
|
69
|
+
export function pushButtonFace(w, h, style, caption, captionWidth, capHeight, fontRes, size) {
|
|
70
|
+
// Baseline placed so the CAPITALS sit optically centred in the box, not the em square.
|
|
71
|
+
const y = (h - capHeight * size) / 2;
|
|
72
|
+
const caption_ = caption ? centredText(w, h, style, caption, captionWidth, y, fontRes, size) : "";
|
|
73
|
+
return `${box(w, h, style)}\n${caption_}`;
|
|
74
|
+
}
|
|
75
|
+
/** One centred, clipped text run inside a field box. Clipping means an over-long caption is cut at the
|
|
76
|
+
* border instead of bleeding across the page. Shared by the push button and the signature placeholder. */
|
|
77
|
+
function centredText(w, h, style, text, textWidth, baselineY, fontRes, size) {
|
|
78
|
+
return (`q 0 0 ${num2(w)} ${num2(h)} re W n ` +
|
|
79
|
+
`BT /${fontRes} ${num2(size)} Tf ${pdfColor(style.color)} rg ` +
|
|
80
|
+
`${num2((w - textWidth) / 2)} ${num2(baselineY)} Td (${escPdf(text)}) Tj ET Q`);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* An EMPTY signature field: the box, a signing rule across the lower third, and an optional hint centred
|
|
84
|
+
* just above it. This is the placeholder look - once someone actually signs, the signing tool replaces
|
|
85
|
+
* this appearance with its own.
|
|
86
|
+
*/
|
|
87
|
+
export function signatureFace(w, h, style, label, labelWidth, fontRes, size) {
|
|
88
|
+
const ruleY = h * 0.3;
|
|
89
|
+
const inset = w * 0.08;
|
|
90
|
+
const stroke = style.border ?? style.color;
|
|
91
|
+
const rule = `${pdfColor(stroke)} RG ${num2(Math.max(0.5, style.borderWidth * 0.75))} w ` +
|
|
92
|
+
`${num2(inset)} ${num2(ruleY)} m ${num2(w - inset)} ${num2(ruleY)} l S`;
|
|
93
|
+
const hint = label ? centredText(w, h, style, label, labelWidth, ruleY + 6, fontRes, size) : "";
|
|
94
|
+
return `${box(w, h, style)}\n${rule}\n${hint}`;
|
|
95
|
+
}
|
|
96
|
+
// Horizontal padding inside a field box, on top of the border - the same small inset Acrobat leaves so
|
|
97
|
+
// the value does not touch the frame.
|
|
98
|
+
const FIELD_PAD = 2;
|
|
99
|
+
// A list box highlights its selected rows. Viewers each pick their own colour; once we bake the
|
|
100
|
+
// appearance WE decide, and this light blue is the familiar selection tint.
|
|
101
|
+
const SELECTION_TINT = "0.60 0.75 0.95";
|
|
102
|
+
/** One pre-measured line of field text. The caller owns wrapping + measuring, so this module stays free
|
|
103
|
+
* of font metrics (and can be reused verbatim when filling an existing PDF). */
|
|
104
|
+
/**
|
|
105
|
+
* A field value split into the lines it is DRAWN as.
|
|
106
|
+
*
|
|
107
|
+
* An explicit line break in the value is a line break, and only then does width wrapping apply within
|
|
108
|
+
* each paragraph. Wrapping on width alone ran the paragraphs together - a multi-line field showed
|
|
109
|
+
* "first lineSecond line" - and it did so on BOTH paths, creating a field and filling one, which is why
|
|
110
|
+
* this lives here rather than in either of them.
|
|
111
|
+
*/
|
|
112
|
+
export function wrapFieldValue(value, fontFamily, fontSize, fontStyle, maxWidth, metrics, multiline) {
|
|
113
|
+
if (!multiline)
|
|
114
|
+
return [value.replace(/\r\n?|\n/g, " ")];
|
|
115
|
+
// CR, LF and CRLF all mean one break (PDF 7.3.4.2 says the same about a literal string).
|
|
116
|
+
return value
|
|
117
|
+
.split(/\r\n?|\n/)
|
|
118
|
+
.flatMap((para) => para === ""
|
|
119
|
+
? [""]
|
|
120
|
+
: wrapStringIntoLines(para, fontFamily, fontSize, fontStyle, maxWidth, metrics));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The size a field draws its value at. `0` in `/DA` is the PDF convention for "auto, fit the box", and
|
|
124
|
+
* both paths - creating a field and filling one - have to resolve it the SAME way, or the same field
|
|
125
|
+
* comes out at two different sizes depending on which one touched it last.
|
|
126
|
+
*/
|
|
127
|
+
export function fieldDrawSize(fontSize, height, borderWidth, capHeight) {
|
|
128
|
+
if (fontSize > 0)
|
|
129
|
+
return fontSize;
|
|
130
|
+
const available = height - 2 * (2 + borderWidth);
|
|
131
|
+
return Math.max(4, Math.min(12, (available * 0.7) / (capHeight || 0.7)));
|
|
132
|
+
}
|
|
133
|
+
/** Where a line of `width` starts, given the box, its padding and the requested alignment. */
|
|
134
|
+
function alignX(boxWidth, pad, width, align = "left") {
|
|
135
|
+
if (align === "center")
|
|
136
|
+
return (boxWidth - width) / 2;
|
|
137
|
+
if (align === "right")
|
|
138
|
+
return boxWidth - pad - width;
|
|
139
|
+
return pad;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* `BT … ET` for absolutely-placed lines. `Tm` (not `Td`) so each line's position is independent.
|
|
143
|
+
*
|
|
144
|
+
* Field text is drawn UNKERNED (a plain `Tj`, never a `TJ` array), even though ordinary document text
|
|
145
|
+
* kerns by default. That is deliberate: the moment someone clicks into a field, the viewer regenerates
|
|
146
|
+
* this appearance from `/DA` - and viewers do not kern field text. Kerning here would make the value
|
|
147
|
+
* visibly shift on first click. The measurement matches: `getStringWidth` is the plain glyph sum, so
|
|
148
|
+
* measured still equals drawn.
|
|
149
|
+
*/
|
|
150
|
+
function textBlock(lines, style, size, fontRes) {
|
|
151
|
+
const runs = lines
|
|
152
|
+
.map((l) => `1 0 0 1 ${num2(l.x)} ${num2(l.y)} Tm (${escPdf(l.text)}) Tj`)
|
|
153
|
+
.join("\n");
|
|
154
|
+
return `BT /${fontRes} ${num2(size)} Tf ${pdfColor(style.color)} rg\n${runs}\nET`;
|
|
155
|
+
}
|
|
156
|
+
/** Everything a field draws is clipped to its own box, so a long value is cut at the frame rather than
|
|
157
|
+
* bleeding across the page. */
|
|
158
|
+
function clipped(w, h, ops) {
|
|
159
|
+
return `q 0 0 ${num2(w)} ${num2(h)} re W n\n${ops}\nQ`;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* A text field's value, baked. `lines` is already wrapped (one entry for a single-line field). A
|
|
163
|
+
* single line sits optically centred; a multi-line field stacks from the top, both measured off the
|
|
164
|
+
* font's cap height rather than a guessed ratio.
|
|
165
|
+
*/
|
|
166
|
+
export function textFieldFace(w, h, style, lines, capHeight, size, fontRes, multiline, align = "left") {
|
|
167
|
+
const face = box(w, h, style);
|
|
168
|
+
if (lines.length === 0)
|
|
169
|
+
return face;
|
|
170
|
+
const pad = FIELD_PAD + style.borderWidth;
|
|
171
|
+
const lineHeight = size * 1.15;
|
|
172
|
+
const placed = lines.map((l, i) => ({
|
|
173
|
+
text: l.text,
|
|
174
|
+
x: alignX(w, pad, l.width, align),
|
|
175
|
+
// Multi-line: the first line's capitals start one cap-height below the top inset, then step down.
|
|
176
|
+
// Single line: centre the capitals in the box.
|
|
177
|
+
y: multiline ? h - pad - capHeight * size - i * lineHeight : (h - capHeight * size) / 2,
|
|
178
|
+
}));
|
|
179
|
+
return `${face}\n${clipped(w, h, textBlock(placed, style, size, fontRes))}`;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* A list box, baked: the visible options stacked from the top, with a highlight behind the selected
|
|
183
|
+
* ones. A combo box (dropdown) uses `textFieldFace` instead - it shows only its current value.
|
|
184
|
+
*/
|
|
185
|
+
export function listBoxFace(w, h, style, lines, capHeight, size, fontRes, align = "left") {
|
|
186
|
+
const pad = FIELD_PAD + style.borderWidth;
|
|
187
|
+
const lineHeight = size * 1.15;
|
|
188
|
+
const rowTop = (i) => h - pad - (i + 1) * lineHeight;
|
|
189
|
+
// Highlights first, so the text sits on top of them.
|
|
190
|
+
const highlights = lines
|
|
191
|
+
.map((l, i) => l.selected
|
|
192
|
+
? `${SELECTION_TINT} rg ${num2(pad / 2)} ${num2(rowTop(i))} ${num2(w - pad)} ${num2(lineHeight)} re f`
|
|
193
|
+
: "")
|
|
194
|
+
.filter(Boolean)
|
|
195
|
+
.join("\n");
|
|
196
|
+
const placed = lines.map((l, i) => ({
|
|
197
|
+
text: l.text,
|
|
198
|
+
x: alignX(w, pad, l.width, align),
|
|
199
|
+
// Baseline inside the row: lift it off the row bottom by the slack under the capitals.
|
|
200
|
+
y: rowTop(i) + (lineHeight - capHeight * size) / 2,
|
|
201
|
+
}));
|
|
202
|
+
const inner = [highlights, textBlock(placed, style, size, fontRes)].filter(Boolean).join("\n");
|
|
203
|
+
return `${box(w, h, style)}\n${clipped(w, h, inner)}`;
|
|
204
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { Color } from "../common/color.js";
|
|
2
|
+
/**
|
|
3
|
+
* The semantic description of ONE AcroForm field, independent of whether it is being CREATED (the field
|
|
4
|
+
* elements produce it) or FILLED in an existing PDF (the reader parses existing fields into it, later).
|
|
5
|
+
* This shared model is the spine that makes "create" and "fill existing" two parts of one feature.
|
|
6
|
+
*
|
|
7
|
+
* A discriminated union, one `kind` per PDF field family (/Tx, /Btn, /Ch, /Sig) - all of them
|
|
8
|
+
* implemented, so a `switch` over `kind` is exhaustive.
|
|
9
|
+
*/
|
|
10
|
+
export type FormFieldSpec = TextFieldSpec | CheckboxSpec | RadioSpec | ChoiceSpec | PushButtonSpec | SignatureSpec;
|
|
11
|
+
/**
|
|
12
|
+
* What every field kind shares, whatever its family. Kept in one place so a new field type cannot
|
|
13
|
+
* silently miss them.
|
|
14
|
+
*/
|
|
15
|
+
export interface CommonFieldProps {
|
|
16
|
+
/** A tooltip / accessible name (/TU). */
|
|
17
|
+
tooltip?: string;
|
|
18
|
+
/** Show the field but do not let it be changed. */
|
|
19
|
+
readOnly?: boolean;
|
|
20
|
+
/** Mark it as a field that must be filled in before the form is submitted (/Ff Required). */
|
|
21
|
+
required?: boolean;
|
|
22
|
+
/** Hide the widget entirely - neither on screen nor in print. */
|
|
23
|
+
hidden?: boolean;
|
|
24
|
+
/** Include the widget when printing (default true). `false` keeps it on screen only. */
|
|
25
|
+
print?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** How a value is aligned inside its field box (the PDF calls this "quadding", /Q). */
|
|
28
|
+
export type FieldAlign = "left" | "center" | "right";
|
|
29
|
+
/**
|
|
30
|
+
* A variable text field (/Tx). The PDF variants live in field flags; here they are typed props, so you
|
|
31
|
+
* never hand-assemble a `/Ff` bitmask.
|
|
32
|
+
*/
|
|
33
|
+
export interface TextFieldSpec extends CommonFieldProps {
|
|
34
|
+
kind: "text";
|
|
35
|
+
/** The field's name (/T) - what you read the value back by. Must be unique in the document. */
|
|
36
|
+
name: string;
|
|
37
|
+
/** The current text value (/V). */
|
|
38
|
+
value?: string;
|
|
39
|
+
/** A tooltip / accessible name for the field (/TU). */
|
|
40
|
+
tooltip?: string;
|
|
41
|
+
/** Wrap across multiple lines instead of a single line (the Multiline flag). */
|
|
42
|
+
multiline?: boolean;
|
|
43
|
+
/** Mask the typed characters (the Password flag). */
|
|
44
|
+
password?: boolean;
|
|
45
|
+
/** How the value sits in the box (default left). Applies to the baked appearance AND /Q. */
|
|
46
|
+
align?: FieldAlign;
|
|
47
|
+
/** Maximum number of characters the field accepts (/MaxLen). */
|
|
48
|
+
maxLength?: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A checkbox (/Btn, neither the Radio nor the Pushbutton flag). Its value is the "on" export name when
|
|
52
|
+
* checked and `Off` when not - PDF checkboxes use NAMES, not strings; we hide that behind `checked`.
|
|
53
|
+
*/
|
|
54
|
+
export interface CheckboxSpec extends CommonFieldProps {
|
|
55
|
+
kind: "checkbox";
|
|
56
|
+
/** The field name (/T). */
|
|
57
|
+
name: string;
|
|
58
|
+
/** Whether it starts checked. */
|
|
59
|
+
checked?: boolean;
|
|
60
|
+
/** The "on" export value (the /AP state name + /V when checked). Default `"Yes"`. */
|
|
61
|
+
onValue?: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* One radio button (/Btn with the Radio flag). Every radio sharing a `group` is ONE AcroForm field whose
|
|
65
|
+
* `/Kids` are the individual buttons - mutually exclusive, so picking one clears the rest. `value` is this
|
|
66
|
+
* button's export name (unique within the group); it becomes the field's value when this one is picked.
|
|
67
|
+
*/
|
|
68
|
+
export interface RadioSpec extends CommonFieldProps {
|
|
69
|
+
kind: "radio";
|
|
70
|
+
/** The shared group name (/T of the parent field). All radios with the same `group` are one field. */
|
|
71
|
+
group: string;
|
|
72
|
+
/** This button's export value - unique in the group; the field's /V when this one is selected. */
|
|
73
|
+
value: string;
|
|
74
|
+
/** Whether this button starts selected (at most one per group should be). */
|
|
75
|
+
selected?: boolean;
|
|
76
|
+
}
|
|
77
|
+
/** One entry of a choice field. `label` is what the reader sees; `value` is what gets stored. */
|
|
78
|
+
export interface ChoiceOption {
|
|
79
|
+
value: string;
|
|
80
|
+
label?: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A choice field (/Ch): a dropdown (combo box) or a list box. `combo` picks which. The value(s) are
|
|
84
|
+
* export strings from `options`. A combo may be `editable` (the reader can type a value not in the list);
|
|
85
|
+
* a list box may allow `multiSelect`.
|
|
86
|
+
*/
|
|
87
|
+
export interface ChoiceSpec extends CommonFieldProps {
|
|
88
|
+
kind: "choice";
|
|
89
|
+
/** The field name (/T). */
|
|
90
|
+
name: string;
|
|
91
|
+
/** The selectable options. */
|
|
92
|
+
options: ChoiceOption[];
|
|
93
|
+
/** `true` = dropdown (combo box), `false` = list box. */
|
|
94
|
+
combo: boolean;
|
|
95
|
+
/** Combo only: let the reader type a value that is not in the list. */
|
|
96
|
+
editable?: boolean;
|
|
97
|
+
/** List box only: allow more than one selection. */
|
|
98
|
+
multiSelect?: boolean;
|
|
99
|
+
/** The selected value (single-select). */
|
|
100
|
+
value?: string;
|
|
101
|
+
/** How the value sits in the box (default left). */
|
|
102
|
+
align?: FieldAlign;
|
|
103
|
+
/** The selected values (a `multiSelect` list box). */
|
|
104
|
+
values?: string[];
|
|
105
|
+
/** Tooltip / accessible name. */
|
|
106
|
+
tooltip?: string;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* What a push button does when clicked. A typed union, so an action can never be half-specified (a
|
|
110
|
+
* submit without a target is not expressible). Scripted (JavaScript) actions are deliberately out of
|
|
111
|
+
* scope - a jasy form is data, not a program.
|
|
112
|
+
*/
|
|
113
|
+
export type ButtonAction = {
|
|
114
|
+
kind: "reset";
|
|
115
|
+
} | {
|
|
116
|
+
kind: "submit";
|
|
117
|
+
url: string;
|
|
118
|
+
} | {
|
|
119
|
+
kind: "url";
|
|
120
|
+
url: string;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* A push button (/Btn with the Pushbutton flag). Unlike every other field it holds NO value - it is a
|
|
124
|
+
* click target that fires an action. Its caption is baked into the appearance stream, so the button
|
|
125
|
+
* looks the same in every viewer and in print.
|
|
126
|
+
*/
|
|
127
|
+
export interface PushButtonSpec extends CommonFieldProps {
|
|
128
|
+
kind: "pushbutton";
|
|
129
|
+
/** The field name (/T). */
|
|
130
|
+
name: string;
|
|
131
|
+
/** The caption drawn on the button. */
|
|
132
|
+
label: string;
|
|
133
|
+
/** What clicking it does; omit for a button that does nothing (a plain label). */
|
|
134
|
+
action?: ButtonAction;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* A signature field (/Sig) - a PLACEHOLDER where a signature can later be applied. jasy creates the
|
|
138
|
+
* field and its empty appearance; it does not sign (real signing needs a certificate and a byte-range
|
|
139
|
+
* digest, a separate feature). An unsigned field has no /V; a signer fills that in later.
|
|
140
|
+
*/
|
|
141
|
+
export interface SignatureSpec extends CommonFieldProps {
|
|
142
|
+
kind: "signature";
|
|
143
|
+
/** The field name (/T). */
|
|
144
|
+
name: string;
|
|
145
|
+
/** A hint drawn inside the empty box, e.g. "Signature" or the signer's role. */
|
|
146
|
+
label?: string;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* How a field's WIDGET is painted: the box (border + fill) and how its value looks (font size + colour).
|
|
150
|
+
* Kept separate from the semantic spec because it maps to the widget `/MK` + `/DA`, not the field value -
|
|
151
|
+
* and the fill-existing path reuses it to restyle a field it did not create.
|
|
152
|
+
*/
|
|
153
|
+
export interface FieldStyle {
|
|
154
|
+
/** Border (stroke) colour of the field box. Omit for no border. */
|
|
155
|
+
border?: Color;
|
|
156
|
+
/** Background fill of the field box. Omit for transparent. */
|
|
157
|
+
background?: Color;
|
|
158
|
+
/** The value's text colour (drawn into `/DA`). */
|
|
159
|
+
color: Color;
|
|
160
|
+
/** Font size in points; `0` lets the viewer auto-size to the box (the PDF convention). */
|
|
161
|
+
fontSize: number;
|
|
162
|
+
/** Border thickness in points (only drawn when a `border` is set). */
|
|
163
|
+
borderWidth: number;
|
|
164
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Color } from "../common/color.js";
|
|
2
|
+
import type { FontStyle } from "../utils/pdf-object-manager.js";
|
|
3
|
+
/** The regular face, for measuring a field's built-in Helvetica. `FontStyle` is a string enum, so this
|
|
4
|
+
* is its exact value - asserted here once instead of importing the enum (which would be a runtime
|
|
5
|
+
* import of the writer, see above). */
|
|
6
|
+
export declare const NORMAL_STYLE: FontStyle;
|
|
7
|
+
/** Escape a PDF literal string: backslash first, then the string delimiters. */
|
|
8
|
+
export declare const escPdf: (s: string) => string;
|
|
9
|
+
/** Trim a number to 2 decimals (PDF operands). */
|
|
10
|
+
export declare const num2: (n: number) => number;
|
|
11
|
+
/** A colour as PDF operands "r g b" in 0..1 (Color stores 0..255). */
|
|
12
|
+
export declare const pdfColor: (c: Color) => string;
|
|
13
|
+
/**
|
|
14
|
+
* Escape a PDF **Name** token (`/Yes`, `/Off`, a checkbox's export value). A Name may only hold the
|
|
15
|
+
* regular characters; whitespace, the delimiters `()<>[]{}/%` and `#` itself must be written `#XX` by
|
|
16
|
+
* BYTE. Without this, an export value like `"Ja / Nein"` would break the dictionary it sits in.
|
|
17
|
+
* Literal strings - `(text)` - use `escPdf` instead; the two escapes are not interchangeable.
|
|
18
|
+
*/
|
|
19
|
+
export declare const escName: (s: string) => string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Small PDF-writer helpers shared across the forms module (widget dicts + appearance streams).
|
|
2
|
+
//
|
|
3
|
+
// Note: everything under `forms/` imports the object manager TYPE-ONLY. Keeping the runtime dependency
|
|
4
|
+
// one-way (the writer owns a collector, never the reverse) avoids an ESM import cycle.
|
|
5
|
+
/** The regular face, for measuring a field's built-in Helvetica. `FontStyle` is a string enum, so this
|
|
6
|
+
* is its exact value - asserted here once instead of importing the enum (which would be a runtime
|
|
7
|
+
* import of the writer, see above). */
|
|
8
|
+
export const NORMAL_STYLE = "normal";
|
|
9
|
+
/** Escape a PDF literal string: backslash first, then the string delimiters. */
|
|
10
|
+
export const escPdf = (s) => s.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
|
|
11
|
+
/** Trim a number to 2 decimals (PDF operands). */
|
|
12
|
+
export const num2 = (n) => Number(n.toFixed(2));
|
|
13
|
+
/** A colour as PDF operands "r g b" in 0..1 (Color stores 0..255). */
|
|
14
|
+
export const pdfColor = (c) => c
|
|
15
|
+
.toArray()
|
|
16
|
+
.map((v) => (v / 255).toFixed(3))
|
|
17
|
+
.join(" ");
|
|
18
|
+
/**
|
|
19
|
+
* Escape a PDF **Name** token (`/Yes`, `/Off`, a checkbox's export value). A Name may only hold the
|
|
20
|
+
* regular characters; whitespace, the delimiters `()<>[]{}/%` and `#` itself must be written `#XX` by
|
|
21
|
+
* BYTE. Without this, an export value like `"Ja / Nein"` would break the dictionary it sits in.
|
|
22
|
+
* Literal strings - `(text)` - use `escPdf` instead; the two escapes are not interchangeable.
|
|
23
|
+
*/
|
|
24
|
+
export const escName = (s) => Array.from(new TextEncoder().encode(s))
|
|
25
|
+
.map((b) => b >= 0x21 && b <= 0x7e && !"()<>[]{}/%#".includes(String.fromCharCode(b))
|
|
26
|
+
? String.fromCharCode(b)
|
|
27
|
+
: "#" + b.toString(16).padStart(2, "0").toUpperCase())
|
|
28
|
+
.join("");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Color } from "../common/color.js";
|
|
2
2
|
import { FontStyle } from "../utils/pdf-object-manager.js";
|
|
3
|
+
import type { FormFieldSpec, FieldStyle } from "../forms/field.js";
|
|
3
4
|
/**
|
|
4
5
|
* Display list - the seam between layout and the PDF backend.
|
|
5
6
|
*
|
|
@@ -35,6 +36,7 @@ export interface TextRun {
|
|
|
35
36
|
fontStyle: FontStyle;
|
|
36
37
|
fontSize: number;
|
|
37
38
|
color: Color;
|
|
39
|
+
letterSpacing?: number;
|
|
38
40
|
tag?: StructTag;
|
|
39
41
|
}
|
|
40
42
|
/** An axis-aligned rectangle. An absent `fill` or `stroke` means that part is not drawn. */
|
|
@@ -144,6 +146,21 @@ export interface Outline {
|
|
|
144
146
|
title: string;
|
|
145
147
|
level: number;
|
|
146
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* An interactive form field (AcroForm). Like `Link` it draws NOTHING into the content stream - it becomes
|
|
151
|
+
* a Widget ANNOTATION on the page (`/Annots`) and a field in the catalog `/AcroForm`, both built from the
|
|
152
|
+
* shared `FormFieldSpec`. The rect is the field box in top-left engine coords; the Y-flip converts it to
|
|
153
|
+
* page space. See `forms/acroform.ts` for the widget/field assembly.
|
|
154
|
+
*/
|
|
155
|
+
export interface FormFieldNode {
|
|
156
|
+
type: "formfield";
|
|
157
|
+
x: number;
|
|
158
|
+
y: number;
|
|
159
|
+
width: number;
|
|
160
|
+
height: number;
|
|
161
|
+
field: FormFieldSpec;
|
|
162
|
+
style: FieldStyle;
|
|
163
|
+
}
|
|
147
164
|
/**
|
|
148
165
|
* Pushes an affine transform (a `q` + `cm` in the content stream): everything between this and the
|
|
149
166
|
* matching `TransformPop` is painted through `matrix`. This is what `Rotated` wraps its child's nodes
|
|
@@ -226,4 +243,4 @@ export interface Path {
|
|
|
226
243
|
tag?: StructTag;
|
|
227
244
|
}
|
|
228
245
|
/** The closed set of primitives the PDF backend knows how to draw. */
|
|
229
|
-
export type IRNode = TextRun | Rect | Line | Image | ClipPush | ClipPop | Path | Link | Outline | Anchor | TransformPush | TransformPop;
|
|
246
|
+
export type IRNode = TextRun | Rect | Line | Image | ClipPush | ClipPop | Path | Link | Outline | Anchor | FormFieldNode | TransformPush | TransformPop;
|
|
@@ -12,6 +12,10 @@ import { LayoutContext, PDFElement } from "../elements/pdf-element.js";
|
|
|
12
12
|
export interface FragmentResult {
|
|
13
13
|
fitted: PDFElement | null;
|
|
14
14
|
remainder: PDFElement | null;
|
|
15
|
+
/** The split ended at a FORCED page break (a `PageBreak`), not because the region filled up. The
|
|
16
|
+
* remainder must start a fresh page, and an enclosing flow must stop packing too - so this bubbles
|
|
17
|
+
* up through nested `fragment()` calls. Absent/false for an ordinary height-driven break. */
|
|
18
|
+
forceBreak?: boolean;
|
|
15
19
|
}
|
|
16
20
|
/**
|
|
17
21
|
* An element that knows how to split itself across regions. Width is already finalized
|
|
@@ -29,6 +33,13 @@ export declare function isFragmentable(element: PDFElement): element is PDFEleme
|
|
|
29
33
|
* logs and clips, `"ignore"` clips silently.
|
|
30
34
|
*/
|
|
31
35
|
export type OverflowPolicy = "error" | "warn" | "ignore";
|
|
36
|
+
export declare function reportOverflow(child: PDFElement, childHeight: number, maxHeight: number, policy: OverflowPolicy): void;
|
|
37
|
+
/**
|
|
38
|
+
* True if packing `children` will hit a forced cut: a child that IS a break, contains one deeper in its
|
|
39
|
+
* subtree, or asks to break before/after itself. A flow container reports this from `hasForcedBreak` so
|
|
40
|
+
* its parent fragments it (runs `packChildren`) instead of placing it whole and swallowing the cut.
|
|
41
|
+
*/
|
|
42
|
+
export declare function childrenForceBreak(children: PDFElement[]): boolean;
|
|
32
43
|
/**
|
|
33
44
|
* Packs a vertical stack of children into `maxHeight`, in order. Children are measured
|
|
34
45
|
* against `width` (unbounded height) and added until one would overflow; that straddling
|
|
@@ -46,4 +57,5 @@ export type OverflowPolicy = "error" | "warn" | "ignore";
|
|
|
46
57
|
export declare function packChildren(children: PDFElement[], maxHeight: number, width: number, ctx: LayoutContext, gap?: number): {
|
|
47
58
|
fitted: PDFElement[];
|
|
48
59
|
remainder: PDFElement[];
|
|
60
|
+
forceBreak: boolean;
|
|
49
61
|
};
|
|
@@ -10,7 +10,7 @@ export function isFragmentable(element) {
|
|
|
10
10
|
const canFragment = element.canFragment;
|
|
11
11
|
return typeof canFragment !== "function" || canFragment.call(element);
|
|
12
12
|
}
|
|
13
|
-
function reportOverflow(child, childHeight, maxHeight, policy) {
|
|
13
|
+
export function reportOverflow(child, childHeight, maxHeight, policy) {
|
|
14
14
|
if (policy === "ignore")
|
|
15
15
|
return;
|
|
16
16
|
const name = child.constructor.name.replace(/Element$/, "");
|
|
@@ -21,6 +21,14 @@ function reportOverflow(child, childHeight, maxHeight, policy) {
|
|
|
21
21
|
throw new Error(`Layout overflow: ${detail}`);
|
|
22
22
|
console.warn(`Layout overflow (clipped): ${detail}`);
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* True if packing `children` will hit a forced cut: a child that IS a break, contains one deeper in its
|
|
26
|
+
* subtree, or asks to break before/after itself. A flow container reports this from `hasForcedBreak` so
|
|
27
|
+
* its parent fragments it (runs `packChildren`) instead of placing it whole and swallowing the cut.
|
|
28
|
+
*/
|
|
29
|
+
export function childrenForceBreak(children) {
|
|
30
|
+
return children.some((child) => child.hasForcedBreak() || child.breaksBefore() || child.breaksAfter());
|
|
31
|
+
}
|
|
24
32
|
/**
|
|
25
33
|
* Packs a vertical stack of children into `maxHeight`, in order. Children are measured
|
|
26
34
|
* against `width` (unbounded height) and added until one would overflow; that straddling
|
|
@@ -39,30 +47,61 @@ export function packChildren(children, maxHeight, width, ctx, gap = 0) {
|
|
|
39
47
|
const fitted = [];
|
|
40
48
|
const remainder = [];
|
|
41
49
|
let usedHeight = 0;
|
|
50
|
+
// Everything from index `from` onward spills to the next region; `broke` says whether the cut was
|
|
51
|
+
// a FORCED page break (so an enclosing flow stops too) rather than the region filling up.
|
|
52
|
+
const spill = (from, broke) => {
|
|
53
|
+
for (let j = from; j < children.length; j++)
|
|
54
|
+
remainder.push(children[j]);
|
|
55
|
+
return { fitted, remainder, forceBreak: broke };
|
|
56
|
+
};
|
|
42
57
|
for (let i = 0; i < children.length; i++) {
|
|
43
58
|
const child = children[i];
|
|
59
|
+
// A forced break: consume the marker (it draws nothing) and send everything after it to a fresh
|
|
60
|
+
// page, regardless of how much room is left.
|
|
61
|
+
if (child.isPageBreak())
|
|
62
|
+
return spill(i + 1, true);
|
|
63
|
+
// break-before: this child wants a fresh page. Honour it only if something is already on this region
|
|
64
|
+
// (CSS ignores break-before at the top of a page); that guard also prevents an empty page + a loop.
|
|
65
|
+
if (child.breaksBefore() && fitted.length > 0)
|
|
66
|
+
return spill(i, true);
|
|
44
67
|
const childHeight = child.calculateLayout(BoxConstraints.loose(width, Infinity), { x: 0, y: 0 }, ctx).height;
|
|
45
68
|
// A gap precedes every child except the first one placed in this region.
|
|
46
69
|
const lead = fitted.length > 0 ? gap : 0;
|
|
47
|
-
if
|
|
70
|
+
// Place whole only if it fits AND holds no forced break: a child that fits by height but contains
|
|
71
|
+
// a `PageBreak` (or a break-before/after) in its subtree must still be fragmented, or the break
|
|
72
|
+
// would be swallowed.
|
|
73
|
+
if (usedHeight + lead + childHeight <= maxHeight && !child.hasForcedBreak()) {
|
|
48
74
|
fitted.push(child);
|
|
49
75
|
usedHeight += lead + childHeight;
|
|
76
|
+
// break-after: everything after this child starts a fresh page.
|
|
77
|
+
if (child.breaksAfter())
|
|
78
|
+
return spill(i + 1, true);
|
|
50
79
|
continue;
|
|
51
80
|
}
|
|
52
|
-
// `child` straddles the boundary
|
|
81
|
+
// `child` straddles the boundary, or carries a forced break. Split it; otherwise place/defer whole.
|
|
53
82
|
const remaining = maxHeight - usedHeight - lead;
|
|
54
83
|
let placedPart = false;
|
|
84
|
+
let childBroke = false;
|
|
55
85
|
if (isFragmentable(child)) {
|
|
56
86
|
const split = child.fragment(Math.max(0, remaining), width, ctx);
|
|
87
|
+
childBroke = split.forceBreak ?? false;
|
|
57
88
|
if (split.fitted) {
|
|
58
89
|
fitted.push(split.fitted);
|
|
90
|
+
// The child fully fit and triggered no cut: it only reached this path because its subtree
|
|
91
|
+
// FLAGGED a possible break (hasForcedBreak) that turned out to be a no-op here - e.g. a
|
|
92
|
+
// break-before at the top of the region, which CSS ignores. Keep packing the siblings after
|
|
93
|
+
// it instead of spilling them, so break-before does not accidentally push later content away.
|
|
94
|
+
if (!split.remainder && !childBroke) {
|
|
95
|
+
usedHeight += lead + childHeight;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
59
98
|
if (split.remainder)
|
|
60
99
|
remainder.push(split.remainder);
|
|
61
100
|
placedPart = true;
|
|
62
101
|
}
|
|
63
102
|
}
|
|
64
103
|
if (!placedPart) {
|
|
65
|
-
if (fitted.length === 0) {
|
|
104
|
+
if (fitted.length === 0 && !childBroke) {
|
|
66
105
|
// Taller than the whole region and unsplittable: force it on (it overflows and is clipped)
|
|
67
106
|
// so the next region still advances - and surface it per the overflow policy.
|
|
68
107
|
reportOverflow(child, childHeight, maxHeight, ctx.onOverflow ?? "ignore");
|
|
@@ -72,9 +111,7 @@ export function packChildren(children, maxHeight, width, ctx, gap = 0) {
|
|
|
72
111
|
remainder.push(child);
|
|
73
112
|
}
|
|
74
113
|
}
|
|
75
|
-
|
|
76
|
-
remainder.push(children[j]);
|
|
77
|
-
break;
|
|
114
|
+
return spill(i + 1, childBroke);
|
|
78
115
|
}
|
|
79
|
-
return { fitted, remainder };
|
|
116
|
+
return { fitted, remainder, forceBreak: false };
|
|
80
117
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
2
|
+
import { TextFieldElement } from "../elements/forms/text-field-element.js";
|
|
3
|
+
import { CheckboxElement } from "../elements/forms/checkbox-element.js";
|
|
4
|
+
import { RadioElement } from "../elements/forms/radio-element.js";
|
|
5
|
+
import { ChoiceElement } from "../elements/forms/choice-element.js";
|
|
6
|
+
import { PushButtonElement } from "../elements/forms/push-button-element.js";
|
|
7
|
+
import { SignatureFieldElement } from "../elements/forms/signature-element.js";
|
|
8
|
+
import { IRNode } from "../ir/display-list.js";
|
|
9
|
+
/** Any form-field element exposes its box + shared spec + style through `getProps()`. */
|
|
10
|
+
type FormFieldElement = TextFieldElement | CheckboxElement | RadioElement | ChoiceElement | PushButtonElement | SignatureFieldElement;
|
|
11
|
+
/**
|
|
12
|
+
* Renders a form-field element to a single `formfield` IR node - the field's box + its shared
|
|
13
|
+
* `FormFieldSpec` + style. It draws nothing; the PageRenderer peels it into a Widget /Annot and the
|
|
14
|
+
* /AcroForm (via forms/acroform.ts). One renderer serves every field element (the spec's `kind`
|
|
15
|
+
* discriminates downstream).
|
|
16
|
+
*/
|
|
17
|
+
export declare class FormFieldRenderer {
|
|
18
|
+
static render(element: FormFieldElement, _objectManager: PDFObjectManager): Promise<IRNode[]>;
|
|
19
|
+
}
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a form-field element to a single `formfield` IR node - the field's box + its shared
|
|
3
|
+
* `FormFieldSpec` + style. It draws nothing; the PageRenderer peels it into a Widget /Annot and the
|
|
4
|
+
* /AcroForm (via forms/acroform.ts). One renderer serves every field element (the spec's `kind`
|
|
5
|
+
* discriminates downstream).
|
|
6
|
+
*/
|
|
7
|
+
export class FormFieldRenderer {
|
|
8
|
+
static async render(element, _objectManager) {
|
|
9
|
+
const { x, y, width, height, spec, style } = element.getProps();
|
|
10
|
+
return [{ type: "formfield", x, y, width, height, field: spec, style }];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { KeepTogetherElement } from "../elements/layout/keep-together-element.js";
|
|
2
|
+
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
3
|
+
import { IRNode } from "../ir/display-list.js";
|
|
4
|
+
/**
|
|
5
|
+
* A `keepTogether` is layout-transparent: it just renders its child. All of its work happens in the
|
|
6
|
+
* layout pass (`fragment`), never here.
|
|
7
|
+
*/
|
|
8
|
+
export declare class KeepTogetherRenderer {
|
|
9
|
+
static render(group: KeepTogetherElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { RendererRegistry } from "../utils/renderer-registry.js";
|
|
2
|
+
/**
|
|
3
|
+
* A `keepTogether` is layout-transparent: it just renders its child. All of its work happens in the
|
|
4
|
+
* layout pass (`fragment`), never here.
|
|
5
|
+
*/
|
|
6
|
+
export class KeepTogetherRenderer {
|
|
7
|
+
static async render(group, objectManager) {
|
|
8
|
+
const { child } = group.getProps();
|
|
9
|
+
const renderer = RendererRegistry.getRenderer(child);
|
|
10
|
+
return renderer ? await renderer(child, objectManager) : [];
|
|
11
|
+
}
|
|
12
|
+
}
|