@jasy/pdf 1.0.0-beta.3 → 1.0.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/color.d.ts +1 -24
- package/dist/api/color.js +2 -278
- package/dist/api/content.d.ts +10 -5
- package/dist/api/content.js +29 -13
- package/dist/api/descriptor.js +30 -3
- package/dist/api/flow.d.ts +55 -0
- package/dist/api/flow.js +71 -0
- 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/gradient.d.ts +1 -53
- package/dist/api/gradient.js +2 -97
- package/dist/api/index.d.ts +4 -0
- package/dist/api/index.js +3 -0
- package/dist/api/layout.d.ts +6 -0
- package/dist/api/layout.js +9 -3
- 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 +101 -22
- package/dist/api/text.d.ts +8 -8
- package/dist/api/text.js +6 -0
- package/dist/canvas/painter.d.ts +2 -2
- package/dist/canvas/painter.js +2 -2
- package/dist/common/color-input.d.ts +24 -0
- package/dist/common/color-input.js +278 -0
- package/dist/common/diagnostics.d.ts +61 -0
- package/dist/common/diagnostics.js +77 -0
- package/dist/common/gradient.d.ts +53 -0
- package/dist/common/gradient.js +97 -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 +8 -1
- package/dist/elements/container-element.js +49 -17
- 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 +70 -21
- package/dist/elements/image-element.js +173 -52
- package/dist/elements/index.d.ts +3 -0
- package/dist/elements/index.js +3 -0
- 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/column-break-element.d.ts +17 -0
- package/dist/elements/layout/column-break-element.js +26 -0
- 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 +20 -4
- package/dist/elements/layout/deferred-element.js +41 -9
- package/dist/elements/layout/expanded-element.d.ts +11 -0
- package/dist/elements/layout/expanded-element.js +53 -18
- package/dist/elements/layout/flow-element.d.ts +109 -0
- package/dist/elements/layout/flow-element.js +317 -0
- package/dist/elements/layout/keep-together-element.d.ts +8 -18
- package/dist/elements/layout/keep-together-element.js +33 -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 +18 -1
- package/dist/elements/layout/padding-element.js +63 -28
- 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 +13 -2
- package/dist/elements/layout/positioned-element.d.ts +23 -4
- package/dist/elements/layout/positioned-element.js +42 -12
- package/dist/elements/layout/proxy-element.d.ts +47 -0
- package/dist/elements/layout/proxy-element.js +106 -0
- package/dist/elements/layout/region-element.d.ts +53 -0
- package/dist/elements/layout/region-element.js +108 -0
- package/dist/elements/layout/repeating-header-element.d.ts +9 -0
- package/dist/elements/layout/repeating-header-element.js +55 -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 +9 -0
- package/dist/elements/layout/rotated-element.js +33 -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 +105 -16
- package/dist/elements/pdf-element.js +118 -19
- package/dist/elements/rectangle-element.d.ts +22 -2
- package/dist/elements/rectangle-element.js +119 -55
- package/dist/elements/row-element.d.ts +17 -12
- package/dist/elements/row-element.js +89 -9
- package/dist/elements/svg-element.d.ts +30 -6
- package/dist/elements/svg-element.js +91 -26
- package/dist/elements/text-element.d.ts +48 -13
- package/dist/elements/text-element.js +161 -116
- 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 +18 -3
- package/dist/layout/box-constraints.d.ts +17 -0
- package/dist/layout/box-constraints.js +31 -0
- package/dist/layout/collect-images.d.ts +6 -4
- package/dist/layout/collect-images.js +14 -8
- package/dist/layout/flex-item.d.ts +11 -0
- package/dist/layout/flex-item.js +51 -0
- package/dist/layout/fragmentation.d.ts +23 -9
- package/dist/layout/fragmentation.js +62 -19
- package/dist/layout/layout-cache.d.ts +53 -0
- package/dist/layout/layout-cache.js +110 -0
- package/dist/platform/browser-fs.d.ts +1 -2
- package/dist/platform/browser-fs.js +3 -6
- package/dist/platform/node-fs.d.ts +0 -1
- package/dist/platform/node-fs.js +0 -4
- 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/column-break-renderer.d.ts +8 -0
- package/dist/renderer/column-break-renderer.js +15 -0
- 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/emoji-image.js +3 -1
- package/dist/renderer/expanded-renderer.d.ts +2 -1
- package/dist/renderer/expanded-renderer.js +2 -3
- package/dist/renderer/flow-renderer.d.ts +16 -0
- package/dist/renderer/flow-renderer.js +21 -0
- 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 +33 -53
- package/dist/renderer/index.d.ts +2 -0
- package/dist/renderer/index.js +2 -0
- 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 +30 -10
- package/dist/renderer/pdf-backend.d.ts +4 -3
- package/dist/renderer/pdf-backend.js +14 -7
- package/dist/renderer/pdf-document-renderer.d.ts +1 -8
- package/dist/renderer/pdf-document-renderer.js +17 -17
- package/dist/renderer/pdf-renderer.js +20 -11
- package/dist/renderer/positioned-renderer.d.ts +18 -1
- package/dist/renderer/positioned-renderer.js +43 -6
- package/dist/renderer/rectangle-renderer.d.ts +2 -1
- package/dist/renderer/rectangle-renderer.js +18 -8
- 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 +9 -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 +114 -127
- package/dist/svg/gradients.js +5 -0
- package/dist/svg/index.d.ts +1 -1
- package/dist/svg/index.js +36 -6
- package/dist/svg/style.d.ts +2 -0
- package/dist/svg/style.js +12 -1
- 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 +55 -29
- package/dist/text/line-breaker.js +453 -188
- package/dist/text/line-metrics.d.ts +8 -3
- package/dist/text/line-metrics.js +11 -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 +45 -13
- 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 +16 -14
- package/dist/utils/flex-layout.js +427 -217
- package/dist/utils/font-metrics.d.ts +4 -3
- package/dist/utils/image-helper.d.ts +59 -11
- package/dist/utils/image-helper.js +209 -81
- package/dist/utils/pdf-object-manager.d.ts +17 -6
- package/dist/utils/pdf-object-manager.js +70 -20
- package/dist/utils/renderer-registry.d.ts +49 -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 +30 -6
- 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
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)}`);
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type { FieldAlign, FieldStyle } from "./field.js";
|
|
2
2
|
import type { FontMetrics } from "../utils/font-metrics.js";
|
|
3
3
|
import type { FontStyle } from "../utils/pdf-object-manager.js";
|
|
4
|
+
/**
|
|
5
|
+
* The font an appearance draws its text in: its resource name inside the appearance stream's own
|
|
6
|
+
* /Resources, and how a string becomes a `Tj` operand in it. One object, so the caller that picks the
|
|
7
|
+
* font also decides its encoding - a WinAnsi literal for Helvetica, glyph ids for an embedded font.
|
|
8
|
+
*/
|
|
9
|
+
export interface FaceFont {
|
|
10
|
+
res: string;
|
|
11
|
+
encode(text: string): string;
|
|
12
|
+
}
|
|
13
|
+
/** The standard Helvetica (`/Helv`) a field's /DA names: WinAnsi literal strings. */
|
|
14
|
+
export declare const HELV: FaceFont;
|
|
4
15
|
/** The unchecked appearance: just the box. */
|
|
5
16
|
export declare function checkboxOff(w: number, h: number, style: FieldStyle): string;
|
|
6
17
|
/** The checked appearance: the box plus a vector checkmark (font-free, so it renders everywhere). */
|
|
@@ -12,16 +23,16 @@ export declare function radioOn(w: number, h: number, style: FieldStyle): string
|
|
|
12
23
|
/**
|
|
13
24
|
* A push button's face: the box plus its caption, centred. The caller measures the caption (this module
|
|
14
25
|
* stays free of font metrics and the writer) and passes `captionWidth` in points and `capHeight` as an em
|
|
15
|
-
* fraction; the baseline is placed so the CAPITALS sit optically centred, not the em box. `
|
|
16
|
-
*
|
|
26
|
+
* fraction; the baseline is placed so the CAPITALS sit optically centred, not the em box. `font` is
|
|
27
|
+
* what the caption is drawn in.
|
|
17
28
|
*/
|
|
18
|
-
export declare function pushButtonFace(w: number, h: number, style: FieldStyle, caption: string, captionWidth: number, capHeight: number,
|
|
29
|
+
export declare function pushButtonFace(w: number, h: number, style: FieldStyle, caption: string, captionWidth: number, capHeight: number, font: FaceFont, size: number): string;
|
|
19
30
|
/**
|
|
20
31
|
* An EMPTY signature field: the box, a signing rule across the lower third, and an optional hint centred
|
|
21
32
|
* just above it. This is the placeholder look - once someone actually signs, the signing tool replaces
|
|
22
33
|
* this appearance with its own.
|
|
23
34
|
*/
|
|
24
|
-
export declare function signatureFace(w: number, h: number, style: FieldStyle, label: string, labelWidth: number,
|
|
35
|
+
export declare function signatureFace(w: number, h: number, style: FieldStyle, label: string, labelWidth: number, font: FaceFont, size: number): string;
|
|
25
36
|
/** One pre-measured line of field text. The caller owns wrapping + measuring, so this module stays free
|
|
26
37
|
* of font metrics (and can be reused verbatim when filling an existing PDF). */
|
|
27
38
|
/**
|
|
@@ -50,9 +61,9 @@ export interface FieldLine {
|
|
|
50
61
|
* single line sits optically centred; a multi-line field stacks from the top, both measured off the
|
|
51
62
|
* font's cap height rather than a guessed ratio.
|
|
52
63
|
*/
|
|
53
|
-
export declare function textFieldFace(w: number, h: number, style: FieldStyle, lines: FieldLine[], capHeight: number, size: number,
|
|
64
|
+
export declare function textFieldFace(w: number, h: number, style: FieldStyle, lines: FieldLine[], capHeight: number, size: number, font: FaceFont, multiline: boolean, align?: FieldAlign): string;
|
|
54
65
|
/**
|
|
55
66
|
* A list box, baked: the visible options stacked from the top, with a highlight behind the selected
|
|
56
67
|
* ones. A combo box (dropdown) uses `textFieldFace` instead - it shows only its current value.
|
|
57
68
|
*/
|
|
58
|
-
export declare function listBoxFace(w: number, h: number, style: FieldStyle, lines: FieldLine[], capHeight: number, size: number,
|
|
69
|
+
export declare function listBoxFace(w: number, h: number, style: FieldStyle, lines: FieldLine[], capHeight: number, size: number, font: FaceFont, align?: FieldAlign): string;
|
package/dist/forms/appearance.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { escPdf, num2, pdfColor } from "./pdf.js";
|
|
2
2
|
import { wrapStringIntoLines } from "../text/line-breaker.js";
|
|
3
|
+
/** The standard Helvetica (`/Helv`) a field's /DA names: WinAnsi literal strings. */
|
|
4
|
+
export const HELV = { res: "Helv", encode: (text) => `(${escPdf(text)})` };
|
|
3
5
|
// Appearance streams (/AP): the actual drawn content of a field's box, in the field's OWN coordinate
|
|
4
6
|
// space (bottom-left origin, [0 0 w h]). Shared between CREATE (bake the field's look now) and, later,
|
|
5
7
|
// FILL-existing (regenerate a field's look after setting its value). This is why it is its own module.
|
|
@@ -63,34 +65,34 @@ export function radioOn(w, h, style) {
|
|
|
63
65
|
/**
|
|
64
66
|
* A push button's face: the box plus its caption, centred. The caller measures the caption (this module
|
|
65
67
|
* 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. `
|
|
67
|
-
*
|
|
68
|
+
* fraction; the baseline is placed so the CAPITALS sit optically centred, not the em box. `font` is
|
|
69
|
+
* what the caption is drawn in.
|
|
68
70
|
*/
|
|
69
|
-
export function pushButtonFace(w, h, style, caption, captionWidth, capHeight,
|
|
71
|
+
export function pushButtonFace(w, h, style, caption, captionWidth, capHeight, font, size) {
|
|
70
72
|
// Baseline placed so the CAPITALS sit optically centred in the box, not the em square.
|
|
71
73
|
const y = (h - capHeight * size) / 2;
|
|
72
|
-
const caption_ = caption ? centredText(w, h, style, caption, captionWidth, y,
|
|
74
|
+
const caption_ = caption ? centredText(w, h, style, caption, captionWidth, y, font, size) : "";
|
|
73
75
|
return `${box(w, h, style)}\n${caption_}`;
|
|
74
76
|
}
|
|
75
77
|
/** One centred, clipped text run inside a field box. Clipping means an over-long caption is cut at the
|
|
76
78
|
* 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,
|
|
79
|
+
function centredText(w, h, style, text, textWidth, baselineY, font, size) {
|
|
78
80
|
return (`q 0 0 ${num2(w)} ${num2(h)} re W n ` +
|
|
79
|
-
`BT /${
|
|
80
|
-
`${num2((w - textWidth) / 2)} ${num2(baselineY)} Td
|
|
81
|
+
`BT /${font.res} ${num2(size)} Tf ${pdfColor(style.color)} rg ` +
|
|
82
|
+
`${num2((w - textWidth) / 2)} ${num2(baselineY)} Td ${font.encode(text)} Tj ET Q`);
|
|
81
83
|
}
|
|
82
84
|
/**
|
|
83
85
|
* An EMPTY signature field: the box, a signing rule across the lower third, and an optional hint centred
|
|
84
86
|
* just above it. This is the placeholder look - once someone actually signs, the signing tool replaces
|
|
85
87
|
* this appearance with its own.
|
|
86
88
|
*/
|
|
87
|
-
export function signatureFace(w, h, style, label, labelWidth,
|
|
89
|
+
export function signatureFace(w, h, style, label, labelWidth, font, size) {
|
|
88
90
|
const ruleY = h * 0.3;
|
|
89
91
|
const inset = w * 0.08;
|
|
90
92
|
const stroke = style.border ?? style.color;
|
|
91
93
|
const rule = `${pdfColor(stroke)} RG ${num2(Math.max(0.5, style.borderWidth * 0.75))} w ` +
|
|
92
94
|
`${num2(inset)} ${num2(ruleY)} m ${num2(w - inset)} ${num2(ruleY)} l S`;
|
|
93
|
-
const hint = label ? centredText(w, h, style, label, labelWidth, ruleY + 6,
|
|
95
|
+
const hint = label ? centredText(w, h, style, label, labelWidth, ruleY + 6, font, size) : "";
|
|
94
96
|
return `${box(w, h, style)}\n${rule}\n${hint}`;
|
|
95
97
|
}
|
|
96
98
|
// Horizontal padding inside a field box, on top of the border - the same small inset Acrobat leaves so
|
|
@@ -147,11 +149,11 @@ function alignX(boxWidth, pad, width, align = "left") {
|
|
|
147
149
|
* visibly shift on first click. The measurement matches: `getStringWidth` is the plain glyph sum, so
|
|
148
150
|
* measured still equals drawn.
|
|
149
151
|
*/
|
|
150
|
-
function textBlock(lines, style, size,
|
|
152
|
+
function textBlock(lines, style, size, font) {
|
|
151
153
|
const runs = lines
|
|
152
|
-
.map((l) => `1 0 0 1 ${num2(l.x)} ${num2(l.y)} Tm
|
|
154
|
+
.map((l) => `1 0 0 1 ${num2(l.x)} ${num2(l.y)} Tm ${font.encode(l.text)} Tj`)
|
|
153
155
|
.join("\n");
|
|
154
|
-
return `BT /${
|
|
156
|
+
return `BT /${font.res} ${num2(size)} Tf ${pdfColor(style.color)} rg\n${runs}\nET`;
|
|
155
157
|
}
|
|
156
158
|
/** Everything a field draws is clipped to its own box, so a long value is cut at the frame rather than
|
|
157
159
|
* bleeding across the page. */
|
|
@@ -163,7 +165,7 @@ function clipped(w, h, ops) {
|
|
|
163
165
|
* single line sits optically centred; a multi-line field stacks from the top, both measured off the
|
|
164
166
|
* font's cap height rather than a guessed ratio.
|
|
165
167
|
*/
|
|
166
|
-
export function textFieldFace(w, h, style, lines, capHeight, size,
|
|
168
|
+
export function textFieldFace(w, h, style, lines, capHeight, size, font, multiline, align = "left") {
|
|
167
169
|
const face = box(w, h, style);
|
|
168
170
|
if (lines.length === 0)
|
|
169
171
|
return face;
|
|
@@ -176,13 +178,13 @@ export function textFieldFace(w, h, style, lines, capHeight, size, fontRes, mult
|
|
|
176
178
|
// Single line: centre the capitals in the box.
|
|
177
179
|
y: multiline ? h - pad - capHeight * size - i * lineHeight : (h - capHeight * size) / 2,
|
|
178
180
|
}));
|
|
179
|
-
return `${face}\n${clipped(w, h, textBlock(placed, style, size,
|
|
181
|
+
return `${face}\n${clipped(w, h, textBlock(placed, style, size, font))}`;
|
|
180
182
|
}
|
|
181
183
|
/**
|
|
182
184
|
* A list box, baked: the visible options stacked from the top, with a highlight behind the selected
|
|
183
185
|
* ones. A combo box (dropdown) uses `textFieldFace` instead - it shows only its current value.
|
|
184
186
|
*/
|
|
185
|
-
export function listBoxFace(w, h, style, lines, capHeight, size,
|
|
187
|
+
export function listBoxFace(w, h, style, lines, capHeight, size, font, align = "left") {
|
|
186
188
|
const pad = FIELD_PAD + style.borderWidth;
|
|
187
189
|
const lineHeight = size * 1.15;
|
|
188
190
|
const rowTop = (i) => h - pad - (i + 1) * lineHeight;
|
|
@@ -199,6 +201,6 @@ export function listBoxFace(w, h, style, lines, capHeight, size, fontRes, align
|
|
|
199
201
|
// Baseline inside the row: lift it off the row bottom by the slack under the capitals.
|
|
200
202
|
y: rowTop(i) + (lineHeight - capHeight * size) / 2,
|
|
201
203
|
}));
|
|
202
|
-
const inner = [highlights, textBlock(placed, style, size,
|
|
204
|
+
const inner = [highlights, textBlock(placed, style, size, font)].filter(Boolean).join("\n");
|
|
203
205
|
return `${box(w, h, style)}\n${clipped(w, h, inner)}`;
|
|
204
206
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ImageColor, Orientation } from "../utils/image-helper.js";
|
|
1
2
|
import { Color } from "../common/color.js";
|
|
2
3
|
import { FontStyle } from "../utils/pdf-object-manager.js";
|
|
3
4
|
import type { FormFieldSpec, FieldStyle } from "../forms/field.js";
|
|
@@ -9,8 +10,8 @@ import type { FormFieldSpec, FieldStyle } from "../forms/field.js";
|
|
|
9
10
|
* are intentionally "dumb": absolute geometry + semantic style, with no PDF
|
|
10
11
|
* operators, no font indices, and no object numbers - those are the backend's job.
|
|
11
12
|
*
|
|
12
|
-
* Coordinates are in PDF points (1/72")
|
|
13
|
-
*
|
|
13
|
+
* Coordinates are in PDF points (1/72") with the engine's top-left origin. The Y-flip to PDF's
|
|
14
|
+
* bottom-left happens once per page, at this seam (`PdfBackend.flipY`).
|
|
14
15
|
*/
|
|
15
16
|
/**
|
|
16
17
|
* Structure tag for accessible (PDF/UA) tagging. `role` is the PDF structure type the content maps to
|
|
@@ -62,7 +63,7 @@ export interface Rect {
|
|
|
62
63
|
y: number;
|
|
63
64
|
width: number;
|
|
64
65
|
height: number;
|
|
65
|
-
/** Solid
|
|
66
|
+
/** Solid color, or a gradient whose anchor points are in absolute page coordinates. */
|
|
66
67
|
fill?: Color | Gradient;
|
|
67
68
|
stroke?: Color;
|
|
68
69
|
strokeWidth: number;
|
|
@@ -94,6 +95,10 @@ export interface Image {
|
|
|
94
95
|
imageType: string;
|
|
95
96
|
/** Flate-compressed DeviceGray alpha channel for a transparent PNG, embedded as the XObject's /SMask. */
|
|
96
97
|
smask?: string;
|
|
98
|
+
/** What the samples mean (a JPEG keeps the space it was saved in); absent = RGB. */
|
|
99
|
+
color?: ImageColor;
|
|
100
|
+
/** How the stored pixels turn upright inside the placement box (EXIF); absent = as stored. */
|
|
101
|
+
orientation?: Orientation;
|
|
97
102
|
/** cover/contain fits clip the placement to the element's original frame. */
|
|
98
103
|
clip?: {
|
|
99
104
|
x: number;
|
|
@@ -140,6 +145,9 @@ export interface Link {
|
|
|
140
145
|
height: number;
|
|
141
146
|
href?: string;
|
|
142
147
|
dest?: string;
|
|
148
|
+
/** Accessible tagging only: the `/Link` structure element the annotation belongs to, and its text. */
|
|
149
|
+
structKey?: number;
|
|
150
|
+
alt?: string;
|
|
143
151
|
}
|
|
144
152
|
/**
|
|
145
153
|
* A named destination anchor - a jump TARGET for an internal `Link({ to })`. Like `Link` it draws
|
|
@@ -177,6 +185,13 @@ export interface FormFieldNode {
|
|
|
177
185
|
height: number;
|
|
178
186
|
field: FormFieldSpec;
|
|
179
187
|
style: FieldStyle;
|
|
188
|
+
/** Accessible tagging only: the `/Form` structure element the widget belongs to. */
|
|
189
|
+
structKey?: number;
|
|
190
|
+
/** The font the field's text inherits; its baked appearance draws in it when it is embedded. */
|
|
191
|
+
font?: {
|
|
192
|
+
family: string;
|
|
193
|
+
style: FontStyle;
|
|
194
|
+
};
|
|
180
195
|
}
|
|
181
196
|
/**
|
|
182
197
|
* Pushes an affine transform (a `q` + `cm` in the content stream): everything between this and the
|
|
@@ -84,6 +84,11 @@ export interface SizingParams {
|
|
|
84
84
|
/** width / height. Derives whichever axis is left open; see `resolveSize`. */
|
|
85
85
|
aspectRatio?: number;
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* The width rules of a box, for a piece of it split across pages: it keeps how wide it may be, and
|
|
89
|
+
* its height comes from what it holds on that page (CSS `box-decoration-break: clone`).
|
|
90
|
+
*/
|
|
91
|
+
export declare function widthRules(s: SizingParams): SizingParams;
|
|
87
92
|
/** Split `SizingParams` into the per-axis form `resolveSize` takes. */
|
|
88
93
|
export declare function extentSpecs(p: SizingParams): {
|
|
89
94
|
width: ExtentSpec;
|
|
@@ -105,6 +110,18 @@ export declare function resolveSize(width: ExtentSpec, height: ExtentSpec, aspec
|
|
|
105
110
|
height?: number;
|
|
106
111
|
constraints: BoxConstraints;
|
|
107
112
|
};
|
|
113
|
+
/**
|
|
114
|
+
* The open axes of a box that has its own size (an image, an SVG), after `resolveSize`: one pinned axis
|
|
115
|
+
* derives the other from the own ratio; none pinned is the own size, scaled down to `bounds` and never
|
|
116
|
+
* up - the cap is what keeps it on the page.
|
|
117
|
+
*/
|
|
118
|
+
export declare function sizeAtOwnRatio(width: number | undefined, height: number | undefined, own: {
|
|
119
|
+
width: number;
|
|
120
|
+
height: number;
|
|
121
|
+
}, bounds: BoxConstraints): {
|
|
122
|
+
width?: number;
|
|
123
|
+
height?: number;
|
|
124
|
+
};
|
|
108
125
|
/** The size an element resolves to and returns UP the tree. */
|
|
109
126
|
export interface Size {
|
|
110
127
|
width: number;
|
|
@@ -91,6 +91,20 @@ export function resolveExtent(fixed, factor, boundedMax, hasBounded) {
|
|
|
91
91
|
return boundedMax * factor;
|
|
92
92
|
return undefined;
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* The width rules of a box, for a piece of it split across pages: it keeps how wide it may be, and
|
|
96
|
+
* its height comes from what it holds on that page (CSS `box-decoration-break: clone`).
|
|
97
|
+
*/
|
|
98
|
+
export function widthRules(s) {
|
|
99
|
+
return {
|
|
100
|
+
width: s.width,
|
|
101
|
+
widthFactor: s.widthFactor,
|
|
102
|
+
minWidth: s.minWidth,
|
|
103
|
+
maxWidth: s.maxWidth,
|
|
104
|
+
minWidthFactor: s.minWidthFactor,
|
|
105
|
+
maxWidthFactor: s.maxWidthFactor,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
94
108
|
/** Split `SizingParams` into the per-axis form `resolveSize` takes. */
|
|
95
109
|
export function extentSpecs(p) {
|
|
96
110
|
return {
|
|
@@ -153,3 +167,20 @@ export function resolveSize(width, height, aspectRatio, constraints) {
|
|
|
153
167
|
constraints: bounds,
|
|
154
168
|
};
|
|
155
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* The open axes of a box that has its own size (an image, an SVG), after `resolveSize`: one pinned axis
|
|
172
|
+
* derives the other from the own ratio; none pinned is the own size, scaled down to `bounds` and never
|
|
173
|
+
* up - the cap is what keeps it on the page.
|
|
174
|
+
*/
|
|
175
|
+
export function sizeAtOwnRatio(width, height, own, bounds) {
|
|
176
|
+
const ratio = own.width / own.height;
|
|
177
|
+
if (width === undefined && height === undefined) {
|
|
178
|
+
// The width clamp does the width; a height cap is turned into a width at the ratio.
|
|
179
|
+
width = bounds.constrainWidth(Math.min(own.width, bounds.maxHeight * ratio));
|
|
180
|
+
}
|
|
181
|
+
if (width !== undefined && height === undefined)
|
|
182
|
+
height = bounds.constrainHeight(width / ratio);
|
|
183
|
+
else if (height !== undefined && width === undefined)
|
|
184
|
+
width = bounds.constrainWidth(height * ratio);
|
|
185
|
+
return { width, height };
|
|
186
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { PDFElement } from "../elements/pdf-element.js";
|
|
2
2
|
import { ImageElement } from "../elements/image-element.js";
|
|
3
|
+
import { SvgElement } from "../elements/svg-element.js";
|
|
3
4
|
/**
|
|
4
5
|
* Walks the element tree (via `getProps`, the same shallow-reflection style as the validator) and
|
|
5
|
-
* returns every
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* returns every image - a bitmap, or an SVG (which needs this only when it comes from a URL). A
|
|
7
|
+
* pre-layout pass uses this to resolve each one's intrinsic size asynchronously: layout is synchronous
|
|
8
|
+
* and cannot await a decoder or a fetch, yet it needs the aspect ratio to give a width-only image its
|
|
9
|
+
* proportional height. Cheap - images are few and this runs once.
|
|
8
10
|
*/
|
|
9
|
-
export declare function collectImageElements(root: PDFElement): ImageElement[];
|
|
11
|
+
export declare function collectImageElements(root: PDFElement): (ImageElement | SvgElement)[];
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { PDFElement, hasChildrenProp, hasChildProp } from "../elements/pdf-element.js";
|
|
2
2
|
import { ImageElement } from "../elements/image-element.js";
|
|
3
|
+
import { SvgElement } from "../elements/svg-element.js";
|
|
3
4
|
/**
|
|
4
5
|
* Walks the element tree (via `getProps`, the same shallow-reflection style as the validator) and
|
|
5
|
-
* returns every
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* returns every image - a bitmap, or an SVG (which needs this only when it comes from a URL). A
|
|
7
|
+
* pre-layout pass uses this to resolve each one's intrinsic size asynchronously: layout is synchronous
|
|
8
|
+
* and cannot await a decoder or a fetch, yet it needs the aspect ratio to give a width-only image its
|
|
9
|
+
* proportional height. Cheap - images are few and this runs once.
|
|
8
10
|
*/
|
|
9
11
|
export function collectImageElements(root) {
|
|
10
12
|
const out = [];
|
|
11
13
|
const visit = (el) => {
|
|
12
|
-
if (el instanceof ImageElement)
|
|
14
|
+
if (el instanceof ImageElement || el instanceof SvgElement)
|
|
13
15
|
out.push(el);
|
|
14
16
|
const props = el.getProps();
|
|
15
17
|
if (hasChildrenProp(props))
|
|
@@ -17,10 +19,10 @@ export function collectImageElements(root) {
|
|
|
17
19
|
visit(c);
|
|
18
20
|
else if (hasChildProp(props))
|
|
19
21
|
visit(props.child);
|
|
20
|
-
// Wrappers that hold a subtree outside `children`/`child`: a Page's header/footer
|
|
21
|
-
// RepeatingHeaderElement's `body` (a Table with a header wraps its rows there)
|
|
22
|
-
// DeferredElement
|
|
23
|
-
//
|
|
22
|
+
// Wrappers that hold a subtree outside `children`/`child`: a Page's header/footer, a
|
|
23
|
+
// RepeatingHeaderElement's `body` (a Table with a header wraps its rows there), and the `sources` a
|
|
24
|
+
// DeferredElement builds from. What a PageBuilder builds does not exist yet; an image there that
|
|
25
|
+
// cannot tell its size without waiting is reported by its layout (`image-unsized`).
|
|
24
26
|
const w = props;
|
|
25
27
|
if (w.header instanceof PDFElement)
|
|
26
28
|
visit(w.header);
|
|
@@ -28,6 +30,10 @@ export function collectImageElements(root) {
|
|
|
28
30
|
visit(w.footer);
|
|
29
31
|
if (w.body instanceof PDFElement)
|
|
30
32
|
visit(w.body);
|
|
33
|
+
if (Array.isArray(w.sources))
|
|
34
|
+
for (const s of w.sources)
|
|
35
|
+
if (s instanceof PDFElement)
|
|
36
|
+
visit(s);
|
|
31
37
|
};
|
|
32
38
|
visit(root);
|
|
33
39
|
return out;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FlexiblePDFElement, PDFElement } from "../elements/pdf-element.js";
|
|
2
|
+
import type { CrossAlign } from "./alignment.js";
|
|
3
|
+
/** The `Expanded`/`Spacer` that makes this child flexible, if any. */
|
|
4
|
+
export declare function flexibleOf(child: PDFElement): FlexiblePDFElement | undefined;
|
|
5
|
+
export declare function alignSelfOf(child: PDFElement): CrossAlign | undefined;
|
|
6
|
+
/** CSS `order`, 0 when unset. */
|
|
7
|
+
export declare function orderOf(child: PDFElement): number;
|
|
8
|
+
/** The child is a `Box` with percentage padding. */
|
|
9
|
+
export declare function hasPercentPaddingItem(child: PDFElement): boolean;
|
|
10
|
+
/** CSS `flex-shrink`, 1 when unset (the CSS default). */
|
|
11
|
+
export declare function flexShrinkOf(child: PDFElement): number;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { FlexiblePDFElement } from "../elements/pdf-element.js";
|
|
2
|
+
/**
|
|
3
|
+
* How a Row or Column sees a child as a flex item. A wrapper that only marks its child (a `Link`, an
|
|
4
|
+
* `Anchor`, a `DefaultTextStyle` - see `PDFElement.proxied`) is looked through, as CSS
|
|
5
|
+
* `display: contents` is: an `Expanded`, `alignSelf`, `order` or `flexShrink` inside it acts as if the
|
|
6
|
+
* wrapper were not there. A value set on the wrapper itself wins over one set inside it.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The child, then each element it is transparent for, outermost first. An `Expanded` is one of them:
|
|
10
|
+
* `Expanded(child)` is `flex: 1` on the child, so the child's `alignSelf` and `order` are the item's.
|
|
11
|
+
*/
|
|
12
|
+
function* itemChain(child) {
|
|
13
|
+
for (let el = child; el;) {
|
|
14
|
+
yield el;
|
|
15
|
+
el = el.proxied() ?? (el instanceof FlexiblePDFElement ? el.layoutChildren()[0] : undefined);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** The `Expanded`/`Spacer` that makes this child flexible, if any. */
|
|
19
|
+
export function flexibleOf(child) {
|
|
20
|
+
for (const el of itemChain(child))
|
|
21
|
+
if (el instanceof FlexiblePDFElement)
|
|
22
|
+
return el;
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
export function alignSelfOf(child) {
|
|
26
|
+
for (const el of itemChain(child))
|
|
27
|
+
if (el.alignSelf !== undefined)
|
|
28
|
+
return el.alignSelf;
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
/** CSS `order`, 0 when unset. */
|
|
32
|
+
export function orderOf(child) {
|
|
33
|
+
for (const el of itemChain(child))
|
|
34
|
+
if (el.order !== undefined)
|
|
35
|
+
return el.order;
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
/** The child is a `Box` with percentage padding. */
|
|
39
|
+
export function hasPercentPaddingItem(child) {
|
|
40
|
+
for (const el of itemChain(child))
|
|
41
|
+
if (el.hasPercentPadding())
|
|
42
|
+
return true;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
/** CSS `flex-shrink`, 1 when unset (the CSS default). */
|
|
46
|
+
export function flexShrinkOf(child) {
|
|
47
|
+
for (const el of itemChain(child))
|
|
48
|
+
if (el.flexShrink !== undefined)
|
|
49
|
+
return el.flexShrink;
|
|
50
|
+
return 1;
|
|
51
|
+
}
|