@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
package/dist/api/layout.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { ContainerElement } from "../elements/container-element.js";
|
|
2
|
-
import { RowElement } from "../elements/row-element.js";
|
|
3
|
-
import { RectangleElement } from "../elements/rectangle-element.js";
|
|
4
1
|
import { ExpandedElement } from "../elements/layout/expanded-element.js";
|
|
5
2
|
import { PaddingElement } from "../elements/layout/padding-element.js";
|
|
3
|
+
import { PageBreakElement } from "../elements/layout/page-break-element.js";
|
|
4
|
+
import { KeepTogetherElement } from "../elements/layout/keep-together-element.js";
|
|
6
5
|
import { PositionedElement, PositionedInsets } from "../elements/layout/positioned-element.js";
|
|
7
6
|
import { LinkElement } from "../elements/layout/link-element.js";
|
|
8
7
|
import { BookmarkElement } from "../elements/layout/bookmark-element.js";
|
|
@@ -28,13 +27,28 @@ export interface StackOptions {
|
|
|
28
27
|
* axis (`height: "50%"` only resolves where the parent bounds the height). */
|
|
29
28
|
width?: SizeInput;
|
|
30
29
|
height?: SizeInput;
|
|
30
|
+
/** Start this stack on a fresh page (CSS `break-before: page`, react-pdf's `break`). Ignored at the
|
|
31
|
+
* top of a page. */
|
|
32
|
+
breakBefore?: boolean;
|
|
33
|
+
/** Start everything after this stack on a fresh page (CSS `break-after: page`). */
|
|
34
|
+
breakAfter?: boolean;
|
|
35
|
+
/** Try to keep this stack on one page (CSS `break-inside: avoid`). If it does not fit here but would
|
|
36
|
+
* fit on a fresh page, it moves there whole; if it is taller than a whole page, it splits anyway. */
|
|
37
|
+
keepTogether?: boolean;
|
|
31
38
|
}
|
|
32
39
|
/** A vertical stack. `Column(children)` or `Column(opts, children)`. */
|
|
33
|
-
export declare function Column(children: PDFElement[]):
|
|
34
|
-
export declare function Column(opts: StackOptions, children: PDFElement[]):
|
|
40
|
+
export declare function Column(children: PDFElement[]): PDFElement;
|
|
41
|
+
export declare function Column(opts: StackOptions, children: PDFElement[]): PDFElement;
|
|
35
42
|
/** A horizontal stack. `Row(children)` or `Row(opts, children)`. */
|
|
36
|
-
export declare function Row(children: PDFElement[]):
|
|
37
|
-
export declare function Row(opts: StackOptions, children: PDFElement[]):
|
|
43
|
+
export declare function Row(children: PDFElement[]): PDFElement;
|
|
44
|
+
export declare function Row(opts: StackOptions, children: PDFElement[]): PDFElement;
|
|
45
|
+
/**
|
|
46
|
+
* Keep a group of elements on ONE page (CSS `break-inside: avoid`). `keepTogether([...])` stacks the
|
|
47
|
+
* children in a `Column` and refuses to split them across a page break - unless they do not fit on a
|
|
48
|
+
* fresh page at all, in which case they split anyway (so pagination always terminates). Equivalent to
|
|
49
|
+
* the `keepTogether` prop on `Box`/`Column`/`Row`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function keepTogether(children: PDFElement[]): KeepTogetherElement;
|
|
38
52
|
/** A bordered / filled box that wraps its children (locked §4). */
|
|
39
53
|
export interface BoxOptions {
|
|
40
54
|
/** Border (stroke) color. A box has a border only when `border` or `borderWidth` is set. */
|
|
@@ -65,22 +79,35 @@ export interface BoxOptions {
|
|
|
65
79
|
/** `"hidden"` crops children to the box (rounded corners included); `"visible"` (default) lets a
|
|
66
80
|
* `Positioned` child spill over the edge. */
|
|
67
81
|
overflow?: "hidden" | "visible";
|
|
82
|
+
/** Start this box on a fresh page (CSS `break-before: page`, react-pdf's `break`). Ignored at the top
|
|
83
|
+
* of a page. */
|
|
84
|
+
breakBefore?: boolean;
|
|
85
|
+
/** Start everything after this box on a fresh page (CSS `break-after: page`). */
|
|
86
|
+
breakAfter?: boolean;
|
|
87
|
+
/** Try to keep this box on one page (CSS `break-inside: avoid`). Moves whole to a fresh page if it
|
|
88
|
+
* would fit there; splits anyway if it is taller than a whole page. */
|
|
89
|
+
keepTogether?: boolean;
|
|
68
90
|
}
|
|
69
91
|
/**
|
|
70
92
|
* A box: maps to a `RectangleElement` (fill + border + radius) whose children are stacked
|
|
71
93
|
* inside it, optionally inset by `padding` (a `PaddingElement` wrapping a `Column` of the
|
|
72
94
|
* children). With no `border`/`borderWidth` the box has no outline (`borderWidth` 0).
|
|
73
95
|
*/
|
|
74
|
-
export declare function Box(children: PDFElement[]):
|
|
75
|
-
export declare function Box(opts: BoxOptions, children: PDFElement[]):
|
|
96
|
+
export declare function Box(children: PDFElement[]): PDFElement;
|
|
97
|
+
export declare function Box(opts: BoxOptions, children: PDFElement[]): PDFElement;
|
|
76
98
|
/** Insets a single child by `padding` (a number / `{x,y}` / `{top,…}` / 4-tuple). */
|
|
77
99
|
export declare function Padding(padding: Insets, child: PDFElement): PaddingElement;
|
|
78
100
|
/**
|
|
79
|
-
* Places a child OUT OF FLOW, relative to the nearest enclosing `relative` Box
|
|
80
|
-
* axis: pin to EDGES - `Positioned({ top, left, right,
|
|
81
|
-
* pokes into / out of the corner (a badge, a tab, a
|
|
82
|
-
* ANCHOR + nudge - `Positioned({ h: "center", v: "end",
|
|
83
|
-
* end-aligned with a pixel offset. An edge wins over an
|
|
101
|
+
* Places a child OUT OF FLOW, relative to the nearest enclosing `relative` Box - or, with none, to
|
|
102
|
+
* the page's content box. Two ways, pick per axis: pin to EDGES - `Positioned({ top, left, right,
|
|
103
|
+
* bottom }, child)`, where a negative value pokes into / out of the corner (a badge, a tab, a
|
|
104
|
+
* ribbon) and pinning both sides stretches; or ANCHOR + nudge - `Positioned({ h: "center", v: "end",
|
|
105
|
+
* x: -10, y: -8 }, child)`, i.e. centered / end-aligned with a pixel offset. An edge wins over an
|
|
106
|
+
* anchor on the same axis.
|
|
107
|
+
*
|
|
108
|
+
* A `Positioned` in a `Page`'s `header` or `footer` repeats on EVERY page and takes no space in the
|
|
109
|
+
* band, which is how you draw a watermark, a draft stamp or a corner ribbon across a document. It
|
|
110
|
+
* anchors to the page, not to the band, so `bottom: 0` is the foot of the page.
|
|
84
111
|
*/
|
|
85
112
|
export declare function Positioned(opts: PositionedInsets, child: PDFElement): PositionedElement;
|
|
86
113
|
/**
|
|
@@ -137,6 +164,12 @@ export declare function RotatedBox(opts: {
|
|
|
137
164
|
* and `b` to the edges. `flex` weights it against other flex children (default 1).
|
|
138
165
|
*/
|
|
139
166
|
export declare function Spacer(flex?: number): ExpandedElement;
|
|
167
|
+
/**
|
|
168
|
+
* A forced page break. Put it in a `Column` (or any vertical flow) and everything AFTER it starts on
|
|
169
|
+
* a fresh page, no matter how much space is left: `Column([intro, PageBreak(), chapterTwo])`. It takes
|
|
170
|
+
* no space and draws nothing. Works nested, and a break wins over `keepTogether`.
|
|
171
|
+
*/
|
|
172
|
+
export declare function PageBreak(): PageBreakElement;
|
|
140
173
|
export interface ExpandedOptions {
|
|
141
174
|
/** Share of the leftover space vs other flex children (default 1). */
|
|
142
175
|
flex?: number;
|
package/dist/api/layout.js
CHANGED
|
@@ -3,6 +3,8 @@ import { RowElement } from "../elements/row-element.js";
|
|
|
3
3
|
import { RectangleElement } from "../elements/rectangle-element.js";
|
|
4
4
|
import { ExpandedElement } from "../elements/layout/expanded-element.js";
|
|
5
5
|
import { PaddingElement } from "../elements/layout/padding-element.js";
|
|
6
|
+
import { PageBreakElement } from "../elements/layout/page-break-element.js";
|
|
7
|
+
import { KeepTogetherElement } from "../elements/layout/keep-together-element.js";
|
|
6
8
|
import { PositionedElement } from "../elements/layout/positioned-element.js";
|
|
7
9
|
import { LinkElement } from "../elements/layout/link-element.js";
|
|
8
10
|
import { BookmarkElement } from "../elements/layout/bookmark-element.js";
|
|
@@ -24,25 +26,44 @@ function stackSize(opts) {
|
|
|
24
26
|
const DEFAULT_CROSS = "start";
|
|
25
27
|
export function Column(a, b) {
|
|
26
28
|
const { opts, children } = splitArgs(a, b);
|
|
27
|
-
return new ContainerElement({
|
|
29
|
+
return maybeKeepTogether(opts, new ContainerElement({
|
|
28
30
|
x: 0,
|
|
29
31
|
y: 0,
|
|
30
32
|
children,
|
|
31
33
|
gap: opts.gap,
|
|
32
34
|
main: opts.justify, // undefined → engine default `start` (matches §5)
|
|
33
35
|
cross: opts.align ?? DEFAULT_CROSS,
|
|
36
|
+
breakBefore: opts.breakBefore,
|
|
37
|
+
breakAfter: opts.breakAfter,
|
|
34
38
|
...stackSize(opts),
|
|
35
|
-
});
|
|
39
|
+
}));
|
|
36
40
|
}
|
|
37
41
|
export function Row(a, b) {
|
|
38
42
|
const { opts, children } = splitArgs(a, b);
|
|
39
|
-
return new RowElement({
|
|
43
|
+
return maybeKeepTogether(opts, new RowElement({
|
|
40
44
|
children,
|
|
41
45
|
gap: opts.gap,
|
|
42
46
|
main: opts.justify,
|
|
43
47
|
cross: opts.align ?? DEFAULT_CROSS,
|
|
48
|
+
breakBefore: opts.breakBefore,
|
|
49
|
+
breakAfter: opts.breakAfter,
|
|
44
50
|
...stackSize(opts),
|
|
45
|
-
});
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
/** Wraps `element` in a `keepTogether` group when the `keepTogether` option is set; otherwise returns it
|
|
54
|
+
* unchanged (byte-identical). Shared by the `Box`/`Column`/`Row` prop shortcut. */
|
|
55
|
+
function maybeKeepTogether(opts, element) {
|
|
56
|
+
return opts.keepTogether ? new KeepTogetherElement({ child: element }) : element;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Keep a group of elements on ONE page (CSS `break-inside: avoid`). `keepTogether([...])` stacks the
|
|
60
|
+
* children in a `Column` and refuses to split them across a page break - unless they do not fit on a
|
|
61
|
+
* fresh page at all, in which case they split anyway (so pagination always terminates). Equivalent to
|
|
62
|
+
* the `keepTogether` prop on `Box`/`Column`/`Row`.
|
|
63
|
+
*/
|
|
64
|
+
export function keepTogether(children) {
|
|
65
|
+
const child = children.length === 1 ? children[0] : Column(children);
|
|
66
|
+
return new KeepTogetherElement({ child });
|
|
46
67
|
}
|
|
47
68
|
export function Box(a, b) {
|
|
48
69
|
const { opts, children } = splitArgs(a, b);
|
|
@@ -66,7 +87,7 @@ export function Box(a, b) {
|
|
|
66
87
|
// A point size fills `width`/`height`; a percentage fills `widthFactor`/`heightFactor`.
|
|
67
88
|
const w = opts.width !== undefined ? toDimension(opts.width) : undefined;
|
|
68
89
|
const h = opts.height !== undefined ? toDimension(opts.height) : undefined;
|
|
69
|
-
return new RectangleElement({
|
|
90
|
+
return maybeKeepTogether(opts, new RectangleElement({
|
|
70
91
|
x: 0,
|
|
71
92
|
y: 0,
|
|
72
93
|
children: content,
|
|
@@ -88,18 +109,25 @@ export function Box(a, b) {
|
|
|
88
109
|
: undefined,
|
|
89
110
|
relative: opts.relative,
|
|
90
111
|
overflow: opts.overflow,
|
|
91
|
-
|
|
112
|
+
breakBefore: opts.breakBefore,
|
|
113
|
+
breakAfter: opts.breakAfter,
|
|
114
|
+
}));
|
|
92
115
|
}
|
|
93
116
|
/** Insets a single child by `padding` (a number / `{x,y}` / `{top,…}` / 4-tuple). */
|
|
94
117
|
export function Padding(padding, child) {
|
|
95
118
|
return new PaddingElement({ margin: toEdges(padding), child });
|
|
96
119
|
}
|
|
97
120
|
/**
|
|
98
|
-
* Places a child OUT OF FLOW, relative to the nearest enclosing `relative` Box
|
|
99
|
-
* axis: pin to EDGES - `Positioned({ top, left, right,
|
|
100
|
-
* pokes into / out of the corner (a badge, a tab, a
|
|
101
|
-
* ANCHOR + nudge - `Positioned({ h: "center", v: "end",
|
|
102
|
-
* end-aligned with a pixel offset. An edge wins over an
|
|
121
|
+
* Places a child OUT OF FLOW, relative to the nearest enclosing `relative` Box - or, with none, to
|
|
122
|
+
* the page's content box. Two ways, pick per axis: pin to EDGES - `Positioned({ top, left, right,
|
|
123
|
+
* bottom }, child)`, where a negative value pokes into / out of the corner (a badge, a tab, a
|
|
124
|
+
* ribbon) and pinning both sides stretches; or ANCHOR + nudge - `Positioned({ h: "center", v: "end",
|
|
125
|
+
* x: -10, y: -8 }, child)`, i.e. centered / end-aligned with a pixel offset. An edge wins over an
|
|
126
|
+
* anchor on the same axis.
|
|
127
|
+
*
|
|
128
|
+
* A `Positioned` in a `Page`'s `header` or `footer` repeats on EVERY page and takes no space in the
|
|
129
|
+
* band, which is how you draw a watermark, a draft stamp or a corner ribbon across a document. It
|
|
130
|
+
* anchors to the page, not to the band, so `bottom: 0` is the foot of the page.
|
|
103
131
|
*/
|
|
104
132
|
export function Positioned(opts, child) {
|
|
105
133
|
return new PositionedElement({ child, ...opts });
|
|
@@ -161,6 +189,14 @@ export function RotatedBox(opts, child) {
|
|
|
161
189
|
export function Spacer(flex = 1) {
|
|
162
190
|
return new ExpandedElement({ flex, child: Column([]) });
|
|
163
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* A forced page break. Put it in a `Column` (or any vertical flow) and everything AFTER it starts on
|
|
194
|
+
* a fresh page, no matter how much space is left: `Column([intro, PageBreak(), chapterTwo])`. It takes
|
|
195
|
+
* no space and draws nothing. Works nested, and a break wins over `keepTogether`.
|
|
196
|
+
*/
|
|
197
|
+
export function PageBreak() {
|
|
198
|
+
return new PageBreakElement();
|
|
199
|
+
}
|
|
164
200
|
export function Expanded(a, b) {
|
|
165
201
|
const isOptsForm = b !== undefined;
|
|
166
202
|
const opts = (isOptsForm ? a : {});
|
package/dist/api/structure.d.ts
CHANGED
|
@@ -132,6 +132,17 @@ export interface RenderOptions {
|
|
|
132
132
|
standardFonts?: boolean;
|
|
133
133
|
/** FlateDecode-compress the streams (default true). Set false for a greppable, uncompressed PDF. */
|
|
134
134
|
compress?: boolean;
|
|
135
|
+
/** Kern the text - ON by default (still labelled experimental while we validate against react-pdf).
|
|
136
|
+
* Emits `TJ` adjustments from the font's own pairs: standard-14 (AFM), embedded `kern` table, and
|
|
137
|
+
* embedded `GPOS` Type 2, so every font path kerns. Set `false` to opt out (byte-identical to the
|
|
138
|
+
* pre-kerning output). Verified against headless Chrome and measured == drawn to the thousandth. */
|
|
139
|
+
kerning?: boolean;
|
|
140
|
+
/** Bake a form field's VALUE into its own appearance stream (`/AP`) - ON by default. Baked values
|
|
141
|
+
* render identically in every viewer and in print, and are what flattening (later) draws from. Set
|
|
142
|
+
* `false` to leave text and choice values to the viewer instead (`/NeedAppearances`, what
|
|
143
|
+
* react-pdf/pdfkit always do). Checkboxes, radios, buttons and signature boxes always carry their
|
|
144
|
+
* own appearance either way - there is no value to defer. Only meaningful with form fields present. */
|
|
145
|
+
fieldAppearances?: boolean;
|
|
135
146
|
/** What to do when content is taller than a page and cannot break: `"error"` throws (default),
|
|
136
147
|
* `"warn"` logs and clips, `"ignore"` clips silently. It is always clipped either way. */
|
|
137
148
|
onOverflow?: OverflowPolicy;
|
package/dist/api/structure.js
CHANGED
|
@@ -137,6 +137,8 @@ export async function renderPdf(doc, options) {
|
|
|
137
137
|
const om = this.objectManager;
|
|
138
138
|
om.setCompress(options?.compress !== false); // FlateDecode streams by default
|
|
139
139
|
om.setOverflowPolicy(options?.onOverflow ?? "error");
|
|
140
|
+
om.setKerning(options?.kerning !== false); // on by default (experimental); `kerning:false` opts out
|
|
141
|
+
om.acroform.setBakeAppearances(options?.fieldAppearances !== false); // baked unless opted out
|
|
140
142
|
for (const [name, value] of Object.entries(fonts)) {
|
|
141
143
|
if (isFontBytes(value)) {
|
|
142
144
|
om.registerCustomFont(name, value);
|
package/dist/api/text.d.ts
CHANGED
|
@@ -11,6 +11,17 @@ export interface TextStyle {
|
|
|
11
11
|
bold?: boolean;
|
|
12
12
|
italic?: boolean;
|
|
13
13
|
color?: ColorInput;
|
|
14
|
+
/** Draw a line under the text. Its position and thickness come from the font, not from a guess.
|
|
15
|
+
* A `Link` is NOT underlined automatically - say so if you want it. */
|
|
16
|
+
underline?: boolean;
|
|
17
|
+
/** Draw a line through the text, at half its x-height (where a browser puts it). */
|
|
18
|
+
strikethrough?: boolean;
|
|
19
|
+
/** Let the underline step around descenders, like a browser (`text-decoration-skip-ink`). Needs an
|
|
20
|
+
* EMBEDDED font - the standard-14 outlines live in the viewer, so we cannot see where their ink
|
|
21
|
+
* is. Asking for it with a standard font is an error, not a silent no-op. */
|
|
22
|
+
skipInk?: boolean;
|
|
23
|
+
/** Extra space after every glyph, in points (CSS `letter-spacing`). Negative tightens. Default 0. */
|
|
24
|
+
letterSpacing?: number;
|
|
14
25
|
/** External URL - makes this run an inline hyperlink (a /Link annotation over its glyphs). On a
|
|
15
26
|
* `span` it links just that run; on a whole `Text` (plain string) it links the whole text. */
|
|
16
27
|
href?: string;
|
|
@@ -26,8 +37,9 @@ export interface TextOptions extends TextStyle {
|
|
|
26
37
|
maxLines?: number;
|
|
27
38
|
/** What happens past `maxLines`: `"clip"` (default) cuts hard, `"ellipsis"` ends with "…". */
|
|
28
39
|
overflow?: TextOverflow;
|
|
29
|
-
/** Line-height multiplier
|
|
30
|
-
* `
|
|
40
|
+
/** Line-height multiplier: each line is `size * lineHeight` tall. Unset means the font's own
|
|
41
|
+
* natural line height (`ascent + descent + lineGap`), like CSS `line-height: normal` - which is
|
|
42
|
+
* what you want for body copy. `1` gives exactly one em, which most faces overflow slightly. */
|
|
31
43
|
lineHeight?: number;
|
|
32
44
|
/** Accessibility role for the tagged structure tree (only when rendered with `accessible`): a heading
|
|
33
45
|
* level `"h1"`..`"h6"` or `"p"` (default). Purely semantic - it does not change the visual style. */
|
|
@@ -56,6 +68,10 @@ export interface TextDefaults {
|
|
|
56
68
|
color?: ColorInput;
|
|
57
69
|
align?: "left" | "center" | "right";
|
|
58
70
|
lineHeight?: number;
|
|
71
|
+
underline?: boolean;
|
|
72
|
+
strikethrough?: boolean;
|
|
73
|
+
skipInk?: boolean;
|
|
74
|
+
letterSpacing?: number;
|
|
59
75
|
}
|
|
60
76
|
/** Maps the `Text`-style option names onto a partial engine `ResolvedTextStyle` (only the set
|
|
61
77
|
* fields), for seeding the inheritance cascade. */
|
package/dist/api/text.js
CHANGED
|
@@ -50,6 +50,9 @@ export function span(text, style = {}) {
|
|
|
50
50
|
? toFontStyle(style.bold, style.italic)
|
|
51
51
|
: undefined,
|
|
52
52
|
fontColor: style.color !== undefined ? toColor(style.color) : undefined,
|
|
53
|
+
underline: style.underline,
|
|
54
|
+
strikethrough: style.strikethrough,
|
|
55
|
+
letterSpacing: style.letterSpacing,
|
|
53
56
|
href: style.href,
|
|
54
57
|
dest: style.to,
|
|
55
58
|
};
|
|
@@ -79,6 +82,10 @@ export function Text(content, opts = {}) {
|
|
|
79
82
|
maxLines: opts.maxLines,
|
|
80
83
|
overflow: opts.overflow,
|
|
81
84
|
lineHeight: opts.lineHeight,
|
|
85
|
+
underline: opts.underline,
|
|
86
|
+
strikethrough: opts.strikethrough,
|
|
87
|
+
skipInk: opts.skipInk,
|
|
88
|
+
letterSpacing: opts.letterSpacing,
|
|
82
89
|
role: opts.role,
|
|
83
90
|
});
|
|
84
91
|
}
|
|
@@ -103,5 +110,13 @@ export function toTextStyleOverride(opts) {
|
|
|
103
110
|
style.textAlignment = ALIGN[opts.align];
|
|
104
111
|
if (opts.lineHeight !== undefined)
|
|
105
112
|
style.lineHeight = opts.lineHeight;
|
|
113
|
+
if (opts.underline !== undefined)
|
|
114
|
+
style.underline = opts.underline;
|
|
115
|
+
if (opts.strikethrough !== undefined)
|
|
116
|
+
style.strikethrough = opts.strikethrough;
|
|
117
|
+
if (opts.skipInk !== undefined)
|
|
118
|
+
style.skipInk = opts.skipInk;
|
|
119
|
+
if (opts.letterSpacing !== undefined)
|
|
120
|
+
style.letterSpacing = opts.letterSpacing;
|
|
106
121
|
return style;
|
|
107
122
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { SecurityHandler } from "./security-handler.js";
|
|
2
|
+
/** RC4. Not available in WebCrypto - for good reasons - so it is written out here, for reading only. */
|
|
3
|
+
declare function rc4(key: Uint8Array, data: Uint8Array): Uint8Array;
|
|
4
|
+
/** What the `/Encrypt` dictionary tells us, already parsed by the reader. */
|
|
5
|
+
export interface LegacyEncryptInfo {
|
|
6
|
+
revision: number;
|
|
7
|
+
/** `/Length` in BITS (default 40). */
|
|
8
|
+
keyBits: number;
|
|
9
|
+
o: Uint8Array;
|
|
10
|
+
u: Uint8Array;
|
|
11
|
+
/** `/P`, the permission flags, as the signed 32-bit value the algorithm feeds in. */
|
|
12
|
+
permissions: number;
|
|
13
|
+
/** The first string of the document `/ID`. */
|
|
14
|
+
id: Uint8Array;
|
|
15
|
+
encryptMetadata: boolean;
|
|
16
|
+
/** True when the crypt filter is AESV2 rather than RC4. */
|
|
17
|
+
aes: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare class StandardLegacy implements SecurityHandler {
|
|
20
|
+
private readonly fileKey;
|
|
21
|
+
private readonly aes;
|
|
22
|
+
private constructor();
|
|
23
|
+
readonly canEncrypt = false;
|
|
24
|
+
encryptDict(): string;
|
|
25
|
+
encrypt(): Promise<Uint8Array>;
|
|
26
|
+
decrypt(data: Uint8Array, ref?: {
|
|
27
|
+
num: number;
|
|
28
|
+
gen: number;
|
|
29
|
+
}): Promise<Uint8Array>;
|
|
30
|
+
/**
|
|
31
|
+
* Algorithm 1: every object is enciphered with its own key, derived from the file key plus the
|
|
32
|
+
* object's number and generation. Miss this and every single object decrypts to noise.
|
|
33
|
+
*/
|
|
34
|
+
private objectKey;
|
|
35
|
+
/**
|
|
36
|
+
* Open a legacy-encrypted document: derive the file key from the password (algorithm 2), then CHECK it
|
|
37
|
+
* against `/U` (algorithm 6) so a wrong password is rejected rather than silently producing noise.
|
|
38
|
+
*/
|
|
39
|
+
static open(password: string, info: LegacyEncryptInfo): SecurityHandler;
|
|
40
|
+
/** Algorithm 6: rebuild what `/U` must look like for this key and compare. */
|
|
41
|
+
private static passwordMatches;
|
|
42
|
+
}
|
|
43
|
+
export { rc4 };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { aesCbcDecrypt } from "./webcrypto.js";
|
|
2
|
+
import { md5 } from "../utils/md5.js";
|
|
3
|
+
/**
|
|
4
|
+
* The OLDER standard security handlers, for READING only: R2 and R3 (RC4, 40 and 128 bit) and R4
|
|
5
|
+
* (RC4 or AES-128).
|
|
6
|
+
*
|
|
7
|
+
* Read-only is a deliberate asymmetry, not an omission. RC4 is broken - biased keystream, and R2 uses a
|
|
8
|
+
* 40-bit key that is brute-forced in minutes - and PDF 2.0 removed it outright. But it is what PDFKit,
|
|
9
|
+
* react-pdf and Ghostscript still produce BY DEFAULT when you ask them for a password, so refusing to
|
|
10
|
+
* open such a file means refusing most of the encrypted PDFs anyone actually has. Writing one would be
|
|
11
|
+
* a different matter: offering a user "protection" we know to be broken helps nobody, so `encrypt`
|
|
12
|
+
* here throws.
|
|
13
|
+
*
|
|
14
|
+
* Two things distinguish these revisions from the modern R6:
|
|
15
|
+
* - the file key comes from MD5 over the password, `/O`, `/P` and the first `/ID` string (algorithm 2);
|
|
16
|
+
* - every object gets its OWN key, derived from the file key plus its number and generation
|
|
17
|
+
* (algorithm 1). That is why `SecurityHandler.encrypt/decrypt` take a `ref`.
|
|
18
|
+
*/
|
|
19
|
+
/** The padding every legacy revision prepends to a password (algorithm 2, step a). */
|
|
20
|
+
const PAD = new Uint8Array([
|
|
21
|
+
0x28, 0xbf, 0x4e, 0x5e, 0x4e, 0x75, 0x8a, 0x41, 0x64, 0x00, 0x4e, 0x56, 0xff, 0xfa, 0x01, 0x08,
|
|
22
|
+
0x2e, 0x2e, 0x00, 0xb6, 0xd0, 0x68, 0x3e, 0x80, 0x2f, 0x0c, 0xa9, 0xfe, 0x64, 0x53, 0x69, 0x7a,
|
|
23
|
+
]);
|
|
24
|
+
/** A password padded (or truncated) to the 32 bytes the algorithm wants. */
|
|
25
|
+
function padPassword(password) {
|
|
26
|
+
const pw = new Uint8Array(32);
|
|
27
|
+
// Latin-1: the legacy handlers predate unicode passwords entirely.
|
|
28
|
+
const raw = Uint8Array.from(password, (c) => c.charCodeAt(0) & 0xff);
|
|
29
|
+
const take = Math.min(raw.length, 32);
|
|
30
|
+
pw.set(raw.subarray(0, take), 0);
|
|
31
|
+
pw.set(PAD.subarray(0, 32 - take), take);
|
|
32
|
+
return pw;
|
|
33
|
+
}
|
|
34
|
+
/** RC4. Not available in WebCrypto - for good reasons - so it is written out here, for reading only. */
|
|
35
|
+
function rc4(key, data) {
|
|
36
|
+
const s = new Uint8Array(256);
|
|
37
|
+
for (let i = 0; i < 256; i++)
|
|
38
|
+
s[i] = i;
|
|
39
|
+
for (let i = 0, j = 0; i < 256; i++) {
|
|
40
|
+
j = (j + s[i] + key[i % key.length]) & 0xff;
|
|
41
|
+
[s[i], s[j]] = [s[j], s[i]];
|
|
42
|
+
}
|
|
43
|
+
const out = new Uint8Array(data.length);
|
|
44
|
+
for (let k = 0, i = 0, j = 0; k < data.length; k++) {
|
|
45
|
+
i = (i + 1) & 0xff;
|
|
46
|
+
j = (j + s[i]) & 0xff;
|
|
47
|
+
[s[i], s[j]] = [s[j], s[i]];
|
|
48
|
+
out[k] = data[k] ^ s[(s[i] + s[j]) & 0xff];
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
const concat = (...parts) => {
|
|
53
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
54
|
+
let at = 0;
|
|
55
|
+
for (const p of parts) {
|
|
56
|
+
out.set(p, at);
|
|
57
|
+
at += p.length;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
};
|
|
61
|
+
export class StandardLegacy {
|
|
62
|
+
constructor(fileKey, aes) {
|
|
63
|
+
this.fileKey = fileKey;
|
|
64
|
+
this.aes = aes;
|
|
65
|
+
this.canEncrypt = false;
|
|
66
|
+
}
|
|
67
|
+
encryptDict() {
|
|
68
|
+
throw new Error("@jasy/pdf: this handler only READS legacy encryption; jasy writes AES-256 (R6) exclusively");
|
|
69
|
+
}
|
|
70
|
+
async encrypt() {
|
|
71
|
+
throw new Error("@jasy/pdf: refusing to write RC4 or AES-128 - they are obsolete; jasy writes AES-256 (R6)");
|
|
72
|
+
}
|
|
73
|
+
async decrypt(data, ref) {
|
|
74
|
+
const key = this.objectKey(ref);
|
|
75
|
+
if (!this.aes)
|
|
76
|
+
return rc4(key, data);
|
|
77
|
+
if (data.length <= 16)
|
|
78
|
+
return new Uint8Array(0); // an IV and nothing else
|
|
79
|
+
return aesCbcDecrypt(key, data.subarray(0, 16), data.subarray(16));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Algorithm 1: every object is enciphered with its own key, derived from the file key plus the
|
|
83
|
+
* object's number and generation. Miss this and every single object decrypts to noise.
|
|
84
|
+
*/
|
|
85
|
+
objectKey(ref) {
|
|
86
|
+
const num = ref?.num ?? 0;
|
|
87
|
+
const gen = ref?.gen ?? 0;
|
|
88
|
+
const extra = new Uint8Array(this.aes ? 9 : 5);
|
|
89
|
+
extra[0] = num & 0xff;
|
|
90
|
+
extra[1] = (num >> 8) & 0xff;
|
|
91
|
+
extra[2] = (num >> 16) & 0xff;
|
|
92
|
+
extra[3] = gen & 0xff;
|
|
93
|
+
extra[4] = (gen >> 8) & 0xff;
|
|
94
|
+
// AES adds the literal "sAlT" - the spec really does spell it that way.
|
|
95
|
+
if (this.aes)
|
|
96
|
+
extra.set([0x73, 0x41, 0x6c, 0x54], 5);
|
|
97
|
+
const digest = md5(concat(this.fileKey, extra));
|
|
98
|
+
return digest.subarray(0, Math.min(this.fileKey.length + 5, 16));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Open a legacy-encrypted document: derive the file key from the password (algorithm 2), then CHECK it
|
|
102
|
+
* against `/U` (algorithm 6) so a wrong password is rejected rather than silently producing noise.
|
|
103
|
+
*/
|
|
104
|
+
static open(password, info) {
|
|
105
|
+
const keyLength = info.revision === 2 ? 5 : Math.max(5, Math.floor(info.keyBits / 8));
|
|
106
|
+
const p = new Uint8Array(4);
|
|
107
|
+
new DataView(p.buffer).setInt32(0, info.permissions, true); // little-endian, signed
|
|
108
|
+
let input = concat(padPassword(password), info.o.subarray(0, 32), p, info.id);
|
|
109
|
+
// R4 with /EncryptMetadata false folds in four 0xFF bytes.
|
|
110
|
+
if (info.revision >= 4 && !info.encryptMetadata) {
|
|
111
|
+
input = concat(input, new Uint8Array([0xff, 0xff, 0xff, 0xff]));
|
|
112
|
+
}
|
|
113
|
+
let key = md5(input);
|
|
114
|
+
if (info.revision >= 3) {
|
|
115
|
+
// 50 further rounds over the first keyLength bytes only.
|
|
116
|
+
for (let i = 0; i < 50; i++)
|
|
117
|
+
key = md5(key.subarray(0, keyLength));
|
|
118
|
+
}
|
|
119
|
+
key = key.subarray(0, keyLength);
|
|
120
|
+
if (!StandardLegacy.passwordMatches(key, info)) {
|
|
121
|
+
throw new Error("@jasy/pdf: wrong password.");
|
|
122
|
+
}
|
|
123
|
+
return new StandardLegacy(key, info.aes);
|
|
124
|
+
}
|
|
125
|
+
/** Algorithm 6: rebuild what `/U` must look like for this key and compare. */
|
|
126
|
+
static passwordMatches(key, info) {
|
|
127
|
+
if (info.revision === 2) {
|
|
128
|
+
const expect = rc4(key, PAD);
|
|
129
|
+
return expect.every((b, i) => b === info.u[i]);
|
|
130
|
+
}
|
|
131
|
+
// R3+ : MD5 over the padding and the /ID, then 20 rounds of RC4 with the key XORed by the round.
|
|
132
|
+
let x = md5(concat(PAD, info.id));
|
|
133
|
+
x = rc4(key, x);
|
|
134
|
+
for (let round = 1; round <= 19; round++) {
|
|
135
|
+
const rk = Uint8Array.from(key, (b) => b ^ round);
|
|
136
|
+
x = rc4(rk, x);
|
|
137
|
+
}
|
|
138
|
+
// Only the first 16 bytes are meaningful; the rest of /U is arbitrary padding.
|
|
139
|
+
for (let i = 0; i < 16; i++)
|
|
140
|
+
if (x[i] !== info.u[i])
|
|
141
|
+
return false;
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
export { rc4 };
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/** A handler encrypts/decrypts the strings + streams of a document and describes itself as an `/Encrypt` dict. */
|
|
2
2
|
export interface SecurityHandler {
|
|
3
|
+
/** Whether this scheme may be WRITTEN. False for the legacy revisions: we open them as a service to
|
|
4
|
+
* files that already exist, but handing a user protection known to be broken is not one. */
|
|
5
|
+
readonly canEncrypt: boolean;
|
|
3
6
|
/** The `/Encrypt` dictionary body (between `<<` and `>>`). Its own strings are never encrypted. */
|
|
4
7
|
encryptDict(): string;
|
|
5
8
|
/** Encrypt one string/stream. `ref` is unused for V5/R6 (one file key) but kept for future per-object schemes. */
|
|
@@ -33,6 +36,7 @@ declare class StandardAes256 implements SecurityHandler {
|
|
|
33
36
|
private readonly dict;
|
|
34
37
|
private constructor();
|
|
35
38
|
static create(opts: EncryptOptions): Promise<StandardAes256>;
|
|
39
|
+
readonly canEncrypt = true;
|
|
36
40
|
encryptDict(): string;
|
|
37
41
|
encrypt(data: Uint8Array): Promise<Uint8Array>;
|
|
38
42
|
decrypt(data: Uint8Array): Promise<Uint8Array>;
|
|
@@ -40,6 +44,18 @@ declare class StandardAes256 implements SecurityHandler {
|
|
|
40
44
|
* password is rejected, not silently turned into a garbage key), then decrypt /UE. The groundwork the
|
|
41
45
|
* future "open/edit existing PDF" path reuses. Throws on a wrong password. */
|
|
42
46
|
static recoverFileKey(userPassword: string, u: Uint8Array, ue: Uint8Array): Promise<Uint8Array>;
|
|
47
|
+
/**
|
|
48
|
+
* A handler for READING an existing file: recover its key from the password, then decrypt with it.
|
|
49
|
+
* The `/Encrypt` dictionary is the file's own, so this handler never has to describe one.
|
|
50
|
+
*
|
|
51
|
+
* `revision` 6 is the standard (ISO 32000-2). Revision **5** is Adobe's withdrawn 2008 extension and
|
|
52
|
+
* differs in ONE respect: the key is a plain SHA-256 of password + salt, without the iterated
|
|
53
|
+
* procedure of algorithm 2.B. The cipher is the same AES-256, which is why supporting it costs almost
|
|
54
|
+
* nothing - and it matters, because PDFKit and react-pdf both emit R5 when asked for AES.
|
|
55
|
+
*/
|
|
56
|
+
static forReading(userPassword: string, u: Uint8Array, ue: Uint8Array, revision?: 5 | 6): Promise<SecurityHandler>;
|
|
57
|
+
/** Revision 5: validate against /U with a single SHA-256, then unwrap /UE the same way R6 does. */
|
|
58
|
+
private static recoverFileKeyR5;
|
|
43
59
|
}
|
|
44
60
|
/** The factory = the seam. Today it always returns the AES-256 handler. */
|
|
45
61
|
export declare function createSecurityHandler(opts: EncryptOptions): Promise<SecurityHandler>;
|
|
@@ -59,6 +59,7 @@ class StandardAes256 {
|
|
|
59
59
|
constructor(fileKey, dict) {
|
|
60
60
|
this.fileKey = fileKey;
|
|
61
61
|
this.dict = dict;
|
|
62
|
+
this.canEncrypt = true;
|
|
62
63
|
}
|
|
63
64
|
static async create(opts) {
|
|
64
65
|
const user = utf8(opts.userPassword);
|
|
@@ -118,6 +119,31 @@ class StandardAes256 {
|
|
|
118
119
|
const intermediate = await hash2B(pw, u.subarray(40, 48), new Uint8Array(0));
|
|
119
120
|
return aesCbcDecryptNoPad(intermediate, new Uint8Array(16), ue);
|
|
120
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* A handler for READING an existing file: recover its key from the password, then decrypt with it.
|
|
124
|
+
* The `/Encrypt` dictionary is the file's own, so this handler never has to describe one.
|
|
125
|
+
*
|
|
126
|
+
* `revision` 6 is the standard (ISO 32000-2). Revision **5** is Adobe's withdrawn 2008 extension and
|
|
127
|
+
* differs in ONE respect: the key is a plain SHA-256 of password + salt, without the iterated
|
|
128
|
+
* procedure of algorithm 2.B. The cipher is the same AES-256, which is why supporting it costs almost
|
|
129
|
+
* nothing - and it matters, because PDFKit and react-pdf both emit R5 when asked for AES.
|
|
130
|
+
*/
|
|
131
|
+
static async forReading(userPassword, u, ue, revision = 6) {
|
|
132
|
+
const key = revision === 6
|
|
133
|
+
? await StandardAes256.recoverFileKey(userPassword, u, ue)
|
|
134
|
+
: await StandardAes256.recoverFileKeyR5(userPassword, u, ue);
|
|
135
|
+
return new StandardAes256(key, "");
|
|
136
|
+
}
|
|
137
|
+
/** Revision 5: validate against /U with a single SHA-256, then unwrap /UE the same way R6 does. */
|
|
138
|
+
static async recoverFileKeyR5(userPassword, u, ue) {
|
|
139
|
+
const pw = utf8(userPassword);
|
|
140
|
+
const check = await sha(256, concat(pw, u.subarray(32, 40)));
|
|
141
|
+
if (!bytesEqual(check, u.subarray(0, 32))) {
|
|
142
|
+
throw new Error("@jasy/pdf: wrong password.");
|
|
143
|
+
}
|
|
144
|
+
const intermediate = await sha(256, concat(pw, u.subarray(40, 48)));
|
|
145
|
+
return aesCbcDecryptNoPad(intermediate, new Uint8Array(16), ue);
|
|
146
|
+
}
|
|
121
147
|
}
|
|
122
148
|
/** The factory = the seam. Today it always returns the AES-256 handler. */
|
|
123
149
|
export async function createSecurityHandler(opts) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type PdfDocument } from "./document.js";
|
|
2
|
+
/**
|
|
3
|
+
* Reads an existing document's `/AcroForm` into a flat list of fields.
|
|
4
|
+
*
|
|
5
|
+
* The field tree is the part every producer lays out differently, and where a naive reader goes wrong:
|
|
6
|
+
*
|
|
7
|
+
* - **A field and its widget need not be the same object.** pdf-lib splits them (the field carries
|
|
8
|
+
* `/T`, `/FT` and `/V`; its `/Kids` are bare widget annotations); PDFKit and our own writer merge
|
|
9
|
+
* them into one. Both are legal, so the walk must cope with either.
|
|
10
|
+
* - **Attributes are inherited.** `/FT`, `/Ff`, `/V` and `/MaxLen` all pass down the tree, so a child may
|
|
11
|
+
* state none of them and still have a type, flags and a value (ISO 32000-1 table 220).
|
|
12
|
+
* - **Names are hierarchical.** The name you address a field by is the `/T` values along the path,
|
|
13
|
+
* joined with dots - `address.street`, not `street`.
|
|
14
|
+
* - **A field may own several widgets** (a radio group, or one field shown on several pages). It is
|
|
15
|
+
* still ONE field with one value.
|
|
16
|
+
*/
|
|
17
|
+
/** One field found in an existing document, in the terms a caller thinks in. */
|
|
18
|
+
export interface ReadField {
|
|
19
|
+
/** The fully qualified name: the `/T` values from the root down, joined with dots. */
|
|
20
|
+
name: string;
|
|
21
|
+
/** `Tx` text · `Btn` button (check box, radio, push button) · `Ch` choice · `Sig` signature. */
|
|
22
|
+
type: "Tx" | "Btn" | "Ch" | "Sig";
|
|
23
|
+
/** The current value as text, if it has one. A button's value is a name (`Yes`, `Off`). */
|
|
24
|
+
value?: string;
|
|
25
|
+
/** A multi-select choice field's values. */
|
|
26
|
+
values?: string[];
|
|
27
|
+
/** The `/Ff` field flags, already merged with anything inherited. */
|
|
28
|
+
flags: number;
|
|
29
|
+
/** `/MaxLen`: the longest text the field accepts, in characters. Inherited like the type. */
|
|
30
|
+
maxLen?: number;
|
|
31
|
+
/** The selectable options of a choice field, as `[export, label]`. */
|
|
32
|
+
options?: Array<{
|
|
33
|
+
value: string;
|
|
34
|
+
label: string;
|
|
35
|
+
}>;
|
|
36
|
+
/** For a check box or radio group: every export name its widgets can take, besides `Off`. */
|
|
37
|
+
onValues?: string[];
|
|
38
|
+
/** The object number of the FIELD dictionary - where `/V` lives, and what an update rewrites. */
|
|
39
|
+
objNum?: number;
|
|
40
|
+
/** The widgets that display this field. A radio group has one per button, and each knows only its
|
|
41
|
+
* OWN export name, which is what its `/AS` has to be set to. */
|
|
42
|
+
widgets: ReadWidget[];
|
|
43
|
+
/** True when no widget carries an appearance stream - filling it means generating one. */
|
|
44
|
+
needsAppearance: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** One widget annotation belonging to a field. */
|
|
47
|
+
export interface ReadWidget {
|
|
48
|
+
num?: number;
|
|
49
|
+
/** The export names this particular widget can show (its `/AP /N` keys, minus `Off`). */
|
|
50
|
+
onValues: string[];
|
|
51
|
+
hasAppearance: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** What a document says about its form as a whole. */
|
|
54
|
+
export interface ReadForm {
|
|
55
|
+
fields: ReadField[];
|
|
56
|
+
/** The form asks viewers to regenerate every appearance. */
|
|
57
|
+
needAppearances: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* The document carries an XFA packet beside the AcroForm (a "hybrid" form). Filling only the AcroForm
|
|
60
|
+
* side can be ignored by a viewer that prefers XFA, so this is reported rather than glossed over.
|
|
61
|
+
*/
|
|
62
|
+
hasXfa: boolean;
|
|
63
|
+
/** True when the cross-reference table had to be rebuilt to read the file at all. */
|
|
64
|
+
recovered: boolean;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Read the form of a loaded document. Returns `undefined` when there is no `/AcroForm` at all - that is
|
|
68
|
+
* a plain PDF, not an error.
|
|
69
|
+
*/
|
|
70
|
+
export declare function readAcroForm(doc: PdfDocument): ReadForm | undefined;
|