@quillmark/quillify 0.0.0-stage → 0.3.1
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/LICENSE +201 -0
- package/NOTICE +85 -0
- package/README.md +56 -2
- package/assets/LiberationSans-Bold.ttf +0 -0
- package/assets/LiberationSans-BoldItalic.ttf +0 -0
- package/assets/LiberationSans-Italic.ttf +0 -0
- package/assets/LiberationSans-Regular.ttf +0 -0
- package/assets/pdf.worker.js +64908 -0
- package/assets/qpdf.wasm +0 -0
- package/assets/qpdf.worker.js +155 -0
- package/assets/xfa-layer.d.ts +2 -0
- package/assets/xfa-layer.js +1 -0
- package/dist/api.d.ts +83 -0
- package/dist/api.js +1 -0
- package/dist/assemble.d.ts +21 -0
- package/dist/assemble.js +155 -0
- package/dist/coords.d.ts +7 -0
- package/dist/coords.js +28 -0
- package/dist/display-list.d.ts +87 -0
- package/dist/display-list.js +372 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/inventory.d.ts +4 -0
- package/dist/inventory.js +179 -0
- package/dist/naming.d.ts +3 -0
- package/dist/naming.js +116 -0
- package/dist/normalize.d.ts +39 -0
- package/dist/normalize.js +303 -0
- package/dist/pdfjs.d.ts +17 -0
- package/dist/pdfjs.js +96 -0
- package/dist/quillify.d.ts +13 -0
- package/dist/quillify.js +86 -0
- package/dist/refusal.d.ts +47 -0
- package/dist/refusal.js +8 -0
- package/dist/sanitize.d.ts +20 -0
- package/dist/sanitize.js +72 -0
- package/dist/strip.d.ts +82 -0
- package/dist/strip.js +533 -0
- package/dist/types.d.ts +125 -0
- package/dist/types.js +42 -0
- package/dist/xfa-layer.d.ts +40 -0
- package/dist/xfa-layer.js +143 -0
- package/dist/xfa-widgets.d.ts +36 -0
- package/dist/xfa-widgets.js +131 -0
- package/dist/xfa.d.ts +15 -0
- package/dist/xfa.js +371 -0
- package/licenses/LiberationSans-LICENSE +102 -0
- package/licenses/compiler-rt-LICENSE +311 -0
- package/licenses/emscripten-LICENSE +102 -0
- package/licenses/libcxx-LICENSE +311 -0
- package/licenses/libcxxabi-LICENSE +311 -0
- package/licenses/libjpeg-turbo-LICENSE +132 -0
- package/licenses/libjpeg-turbo-README.ijg +258 -0
- package/licenses/musl-COPYRIGHT +193 -0
- package/licenses/pdf.js-LICENSE +177 -0
- package/licenses/qpdf-LICENSE +202 -0
- package/licenses/qpdf-NOTICE +42 -0
- package/licenses/zlib-README +118 -0
- package/package.json +70 -5
package/dist/types.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** AcroForm field type codes as pdf.js reports them. */
|
|
2
|
+
export const WIDGET_KINDS = ['Tx', 'Btn', 'Ch', 'Sig'];
|
|
3
|
+
export const SNAKE_CASE = /^[a-z][a-z0-9_]*$/;
|
|
4
|
+
/** The largest file `sanitize` opens. It bounds the compressed input. */
|
|
5
|
+
export const MAX_BYTES = 8 * 1024 * 1024;
|
|
6
|
+
/**
|
|
7
|
+
* The most a file may come to once qpdf has it: the file qpdf writes together
|
|
8
|
+
* with what its Flate streams decode to and what its images declare, and
|
|
9
|
+
* qpdf's own memory. Of 1,423 published files the largest comes to
|
|
10
|
+
* 48,477,587 bytes (AF 4101), and qpdf's memory is 17,235,968 bytes after
|
|
11
|
+
* each. Their largest image is 6,026 by 4,523 pixels at a bit each, 3,410,342
|
|
12
|
+
* bytes (AF 1520), and the one that declares the most bytes is 2,322 by 2,916
|
|
13
|
+
* at three bytes each, 20,312,856 (AF 4101).
|
|
14
|
+
*/
|
|
15
|
+
export const MAX_INFLATED_BYTES = 64 * 1024 * 1024;
|
|
16
|
+
/** The most pages and fields a form has and is taken. The engine takes at most 1,000 fields in a block. */
|
|
17
|
+
export const MAX_PAGES = 25;
|
|
18
|
+
export const MAX_WIDGETS = 1000;
|
|
19
|
+
/** The limits a consumer prints before a read. A refusal's own `limit` is the one to print after it: `too_large` carries `MAX_INFLATED_BYTES` for the decoded bound. */
|
|
20
|
+
export const limits = Object.freeze({
|
|
21
|
+
bytes: MAX_BYTES,
|
|
22
|
+
pages: MAX_PAGES,
|
|
23
|
+
fields: MAX_WIDGETS
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* The longest side a page has and is taken, in points, and the farthest its
|
|
27
|
+
* box reaches from the origin: the PDF specification's limit for a side (ISO
|
|
28
|
+
* 32000-1, Annex C). Of 2,127 published pages the longest is 3,456 (AFMC 1656).
|
|
29
|
+
*/
|
|
30
|
+
export const MAX_SIDE = 14_400;
|
|
31
|
+
/**
|
|
32
|
+
* The most pixels the pictures of a dynamic form come to, each use counted.
|
|
33
|
+
* At four bytes a pixel that is 32 MB on a canvas, and as much in the
|
|
34
|
+
* background when the pictures are noise with an alpha channel. Of the 65 Air
|
|
35
|
+
* Force forms with a picture the largest comes to 6,958,512 (AF 4035).
|
|
36
|
+
*/
|
|
37
|
+
export const MAX_PICTURE_PIXELS = 8_000_000;
|
|
38
|
+
/**
|
|
39
|
+
* How long `sanitize` works on a file before it refuses it. Of 59 published
|
|
40
|
+
* forms the slowest takes 2.7 s (SF 2809).
|
|
41
|
+
*/
|
|
42
|
+
export const MAX_SECONDS = 60;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { PDFPageProxy } from 'pdfjs-dist';
|
|
2
|
+
/** One page after the browser has solved its layout. */
|
|
3
|
+
export interface XfaPage {
|
|
4
|
+
layer: HTMLElement;
|
|
5
|
+
/** The page box in client coordinates. Every harvested rect is read against it. */
|
|
6
|
+
origin: DOMRect;
|
|
7
|
+
/** Page size in points. pdf.js normalizes XFA to points and emits CSS px, so 1px is 1pt. */
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Somewhere out of sight to lay XFA pages out: a frame, whose document takes no
|
|
13
|
+
* rule from the host page and gives it none. `display: none` is not an option:
|
|
14
|
+
* it zeroes every rect, and it does that silently. The frame is hidden, out of
|
|
15
|
+
* the flow and of no size instead, so the host page shows, lays out and
|
|
16
|
+
* scrolls as it did, and the form inside is laid out all the same. A host page
|
|
17
|
+
* can still take the frame's boxes away, and `lay` throws when one has. It can
|
|
18
|
+
* take the frame out before it has loaded, and `ready` then rejects. And it
|
|
19
|
+
* can zoom the frame, which the frame undoes, or `lay` throws.
|
|
20
|
+
*
|
|
21
|
+
* The frame runs no script, and its policy lets its document load only an
|
|
22
|
+
* image from `blob:`, which is how pdf.js hands it a form's images, or from
|
|
23
|
+
* `data:`. A face of the form is not the document's to load: pdf.js makes it
|
|
24
|
+
* in the host page's realm, from the file's bytes, a `local()` name or a face
|
|
25
|
+
* the package ships.
|
|
26
|
+
*/
|
|
27
|
+
export declare class XfaStage {
|
|
28
|
+
#private;
|
|
29
|
+
/** The frame's document, once the stylesheet `XfaLayer.render` counts on applies in it. */
|
|
30
|
+
readonly ready: Promise<Document>;
|
|
31
|
+
constructor();
|
|
32
|
+
/**
|
|
33
|
+
* Lay one page out. Each page gets its own relative wrapper: `.xfaLayer` is
|
|
34
|
+
* absolutely positioned, so sibling layers would stack at the initial
|
|
35
|
+
* containing block and every page after the first would measure against the
|
|
36
|
+
* wrong origin.
|
|
37
|
+
*/
|
|
38
|
+
lay(page: PDFPageProxy): Promise<XfaPage>;
|
|
39
|
+
close(): void;
|
|
40
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import css from '../assets/xfa-layer.js';
|
|
2
|
+
import { Refused } from './refusal.js';
|
|
3
|
+
/** `XfaLayer.render` reads these off a link service; XFA forms here carry no links. */
|
|
4
|
+
const NO_LINKS = {
|
|
5
|
+
addLinkAttributes() { },
|
|
6
|
+
externalLinkTarget: 0,
|
|
7
|
+
externalLinkRel: '',
|
|
8
|
+
externalLinkEnabled: true,
|
|
9
|
+
getDestinationHash: () => '',
|
|
10
|
+
getAnchorUrl: () => ''
|
|
11
|
+
};
|
|
12
|
+
const SHELL = `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src blob: data:">`;
|
|
13
|
+
/**
|
|
14
|
+
* Somewhere out of sight to lay XFA pages out: a frame, whose document takes no
|
|
15
|
+
* rule from the host page and gives it none. `display: none` is not an option:
|
|
16
|
+
* it zeroes every rect, and it does that silently. The frame is hidden, out of
|
|
17
|
+
* the flow and of no size instead, so the host page shows, lays out and
|
|
18
|
+
* scrolls as it did, and the form inside is laid out all the same. A host page
|
|
19
|
+
* can still take the frame's boxes away, and `lay` throws when one has. It can
|
|
20
|
+
* take the frame out before it has loaded, and `ready` then rejects. And it
|
|
21
|
+
* can zoom the frame, which the frame undoes, or `lay` throws.
|
|
22
|
+
*
|
|
23
|
+
* The frame runs no script, and its policy lets its document load only an
|
|
24
|
+
* image from `blob:`, which is how pdf.js hands it a form's images, or from
|
|
25
|
+
* `data:`. A face of the form is not the document's to load: pdf.js makes it
|
|
26
|
+
* in the host page's realm, from the file's bytes, a `local()` name or a face
|
|
27
|
+
* the package ships.
|
|
28
|
+
*/
|
|
29
|
+
export class XfaStage {
|
|
30
|
+
#frame = document.createElement('iframe');
|
|
31
|
+
/** The frame's document, once the stylesheet `XfaLayer.render` counts on applies in it. */
|
|
32
|
+
ready;
|
|
33
|
+
constructor() {
|
|
34
|
+
const frame = this.#frame;
|
|
35
|
+
frame.sandbox.add('allow-same-origin');
|
|
36
|
+
frame.style.cssText = 'position:fixed;width:0;height:0;border:0;visibility:hidden';
|
|
37
|
+
frame.srcdoc = SHELL;
|
|
38
|
+
this.ready = new Promise((loaded, removed) => {
|
|
39
|
+
// A frame that leaves the document before it has loaded never loads.
|
|
40
|
+
const left = () => {
|
|
41
|
+
if (document.contains(frame))
|
|
42
|
+
return;
|
|
43
|
+
watch.disconnect();
|
|
44
|
+
removed(new Error('The host page removed the frame the form is laid out in before it had loaded.'));
|
|
45
|
+
};
|
|
46
|
+
const watch = new MutationObserver(left);
|
|
47
|
+
frame.onload = () => {
|
|
48
|
+
watch.disconnect();
|
|
49
|
+
loaded();
|
|
50
|
+
};
|
|
51
|
+
document.body.append(frame);
|
|
52
|
+
// A zoom on the frame or an ancestor scales the layout inside it. The
|
|
53
|
+
// frame's own zoom undoes its ancestors', where the browser tells it.
|
|
54
|
+
const zoom = document.body.currentCSSZoom;
|
|
55
|
+
if (zoom)
|
|
56
|
+
frame.style.zoom = String(1 / zoom);
|
|
57
|
+
watch.observe(document, { childList: true, subtree: true });
|
|
58
|
+
left();
|
|
59
|
+
}).then(() => {
|
|
60
|
+
const frameDocument = frame.contentDocument;
|
|
61
|
+
const view = frameDocument.defaultView;
|
|
62
|
+
const sheet = new view.CSSStyleSheet();
|
|
63
|
+
sheet.replaceSync(css);
|
|
64
|
+
// pdf.js's sheet gives a field out of focus a background from a data: URL,
|
|
65
|
+
// which nothing here reads and a host's policy may refuse, a logged error
|
|
66
|
+
// for each field.
|
|
67
|
+
sheet.insertRule(':root { --xfa-unfocused-field-background: none; }', sheet.cssRules.length);
|
|
68
|
+
frameDocument.adoptedStyleSheets = [sheet];
|
|
69
|
+
// A sheet that did not apply leaves every field at its content size,
|
|
70
|
+
// and the rects read off it are wrong with no error.
|
|
71
|
+
const probe = frameDocument.createElement('div');
|
|
72
|
+
probe.className = 'xfaLayer';
|
|
73
|
+
frameDocument.body.append(probe);
|
|
74
|
+
const applies = view.getComputedStyle(probe).position === 'absolute';
|
|
75
|
+
probe.remove();
|
|
76
|
+
if (!applies)
|
|
77
|
+
throw new Error('The XFA stylesheet did not apply to the stage.');
|
|
78
|
+
// pdf.js names a face with its family in quotes. Chromium keeps the quotes
|
|
79
|
+
// as part of the name, and no rule of the form then asks for that face.
|
|
80
|
+
const faces = frameDocument.fonts;
|
|
81
|
+
const add = faces.add.bind(faces);
|
|
82
|
+
faces.add = (face) => {
|
|
83
|
+
if (/^".*"$/.test(face.family))
|
|
84
|
+
face.family = face.family.slice(1, -1);
|
|
85
|
+
return add(face);
|
|
86
|
+
};
|
|
87
|
+
return frameDocument;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Lay one page out. Each page gets its own relative wrapper: `.xfaLayer` is
|
|
92
|
+
* absolutely positioned, so sibling layers would stack at the initial
|
|
93
|
+
* containing block and every page after the first would measure against the
|
|
94
|
+
* wrong origin.
|
|
95
|
+
*/
|
|
96
|
+
async lay(page) {
|
|
97
|
+
const pdfjs = await import('pdfjs-dist');
|
|
98
|
+
const xfa = await page.getXfa();
|
|
99
|
+
if (!xfa)
|
|
100
|
+
throw new Error(`XFA page ${page.pageNumber} laid out to nothing.`);
|
|
101
|
+
const viewport = pdfjs.XfaLayer.getPageViewport(xfa, { scale: 1, rotation: 0 });
|
|
102
|
+
// pdf.js lays out a page area whose medium has no size, and gives the page
|
|
103
|
+
// a side of nothing, a negative one, or no number at all.
|
|
104
|
+
if (!(viewport.width > 0 && viewport.height > 0))
|
|
105
|
+
throw new Refused({ code: 'layout', detail: 'a page has no size' });
|
|
106
|
+
const frameDocument = await this.ready;
|
|
107
|
+
const host = frameDocument.createElement('div');
|
|
108
|
+
// On Linux Chromium fits text to the pixel grid and rounds each advance to
|
|
109
|
+
// a whole pixel, unless the text asks for geometric precision.
|
|
110
|
+
host.style.cssText = `position:relative;overflow:hidden;background:#fff;text-rendering:geometricPrecision;width:${viewport.width}px;height:${viewport.height}px`;
|
|
111
|
+
const layer = frameDocument.createElement('div');
|
|
112
|
+
layer.className = 'xfaLayer';
|
|
113
|
+
host.append(layer);
|
|
114
|
+
frameDocument.body.append(host);
|
|
115
|
+
pdfjs.XfaLayer.render({
|
|
116
|
+
viewport: viewport.clone({ dontFlip: true }),
|
|
117
|
+
div: layer,
|
|
118
|
+
xfaHtml: xfa,
|
|
119
|
+
linkService: NO_LINKS,
|
|
120
|
+
intent: 'display'
|
|
121
|
+
});
|
|
122
|
+
await frameDocument.fonts.ready;
|
|
123
|
+
// A picture has a size once it has loaded, and one that fails to has none.
|
|
124
|
+
await Promise.all(Array.from(layer.querySelectorAll('img'), (img) => img.complete || new Promise((done) => (img.onload = img.onerror = done))));
|
|
125
|
+
// A frame the host page hid or took out gives the wrapper no box.
|
|
126
|
+
if (!host.getClientRects().length)
|
|
127
|
+
throw new Error('The host page hid or removed the frame the form is laid out in.');
|
|
128
|
+
// The frame's zoom times its ancestors' is a 32-bit float, and for some
|
|
129
|
+
// zooms no value brings it to 1. A rule marked `!important` beats the
|
|
130
|
+
// frame's own.
|
|
131
|
+
if ((this.#frame.currentCSSZoom ?? 1) !== 1)
|
|
132
|
+
throw new Error("The host page's zoom scales the frame the form is laid out in.");
|
|
133
|
+
return {
|
|
134
|
+
layer,
|
|
135
|
+
origin: host.getBoundingClientRect(),
|
|
136
|
+
width: viewport.width,
|
|
137
|
+
height: viewport.height
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
close() {
|
|
141
|
+
this.#frame.remove();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { XfaPage } from './xfa-layer.js';
|
|
2
|
+
export type XfaKind = 'text' | 'multiline' | 'checkbox' | 'radio' | 'choice' | 'signature';
|
|
3
|
+
export interface XfaWidget {
|
|
4
|
+
/**
|
|
5
|
+
* `p<page>.<SOM path>`, with `#n` where that repeats. It is the AcroForm field
|
|
6
|
+
* name of every control but a radio, whose field is its group's.
|
|
7
|
+
*/
|
|
8
|
+
name: string;
|
|
9
|
+
kind: XfaKind;
|
|
10
|
+
page: number;
|
|
11
|
+
/** Points, top-left origin, page-relative: the rect `form.json` wants. */
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
w: number;
|
|
15
|
+
h: number;
|
|
16
|
+
/** The exclusion group this radio belongs to, as a SOM path. */
|
|
17
|
+
group?: string;
|
|
18
|
+
/** This radio's own on-state, which is how the group tells its options apart. */
|
|
19
|
+
on?: string;
|
|
20
|
+
tooltip?: string;
|
|
21
|
+
options: string[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Where the browser paints a check box or a radio button. pdf.js gives the
|
|
25
|
+
* control the width its template sets and the height of what is left of its
|
|
26
|
+
* row, and the browser paints the largest square centered in that box.
|
|
27
|
+
*/
|
|
28
|
+
export declare function square(box: DOMRect): Pick<DOMRect, 'left' | 'top' | 'width' | 'height'>;
|
|
29
|
+
/**
|
|
30
|
+
* Every control on a laid-out page, named after what the form called it.
|
|
31
|
+
*
|
|
32
|
+
* What the form called it is its SOM path: the `xfaname` attributes from the
|
|
33
|
+
* layer root down to the control. Its `id` or `name` is read only when there
|
|
34
|
+
* is none.
|
|
35
|
+
*/
|
|
36
|
+
export declare function harvestWidgets(page: XfaPage, pageNumber: number): XfaWidget[];
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Refused } from './refusal.js';
|
|
2
|
+
/**
|
|
3
|
+
* Where the browser paints a check box or a radio button. pdf.js gives the
|
|
4
|
+
* control the width its template sets and the height of what is left of its
|
|
5
|
+
* row, and the browser paints the largest square centered in that box.
|
|
6
|
+
*/
|
|
7
|
+
export function square(box) {
|
|
8
|
+
const side = Math.min(box.width, box.height);
|
|
9
|
+
return {
|
|
10
|
+
left: box.left + (box.width - side) / 2,
|
|
11
|
+
top: box.top + (box.height - side) / 2,
|
|
12
|
+
width: side,
|
|
13
|
+
height: side
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Every control on a laid-out page, named after what the form called it.
|
|
18
|
+
*
|
|
19
|
+
* What the form called it is its SOM path: the `xfaname` attributes from the
|
|
20
|
+
* layer root down to the control. Its `id` or `name` is read only when there
|
|
21
|
+
* is none.
|
|
22
|
+
*/
|
|
23
|
+
export function harvestWidgets(page, pageNumber) {
|
|
24
|
+
const { layer, origin } = page;
|
|
25
|
+
const view = layer.ownerDocument.defaultView;
|
|
26
|
+
const widgets = [];
|
|
27
|
+
const seen = new Map();
|
|
28
|
+
const places = new Set();
|
|
29
|
+
// No field of a quill prints a barcode of what was entered, or hides it.
|
|
30
|
+
for (const [selector, construct] of [
|
|
31
|
+
['.xfaBarcode', 'barcode'],
|
|
32
|
+
['.xfaPasswordEdit', 'password']
|
|
33
|
+
])
|
|
34
|
+
if (layer.querySelector(selector))
|
|
35
|
+
throw new Refused({ code: 'unpaintable', page: pageNumber - 1, construct });
|
|
36
|
+
// A signature field is read after every control, so a control that shares its
|
|
37
|
+
// SOM path keeps the occurrence count it has without it.
|
|
38
|
+
for (const el of [
|
|
39
|
+
...layer.querySelectorAll('input, select, textarea'),
|
|
40
|
+
...layer.querySelectorAll('.xfaSignature')
|
|
41
|
+
]) {
|
|
42
|
+
const style = view.getComputedStyle(el);
|
|
43
|
+
if (style.visibility === 'hidden' || style.display === 'none')
|
|
44
|
+
continue;
|
|
45
|
+
const box = el.getBoundingClientRect();
|
|
46
|
+
if (box.width <= 0 || box.height <= 0)
|
|
47
|
+
continue;
|
|
48
|
+
const type = el.getAttribute('type')?.toLowerCase();
|
|
49
|
+
const tag = el.tagName.toLowerCase();
|
|
50
|
+
const kind = tag === 'div'
|
|
51
|
+
? 'signature'
|
|
52
|
+
: tag === 'select'
|
|
53
|
+
? 'choice'
|
|
54
|
+
: tag === 'textarea'
|
|
55
|
+
? 'multiline'
|
|
56
|
+
: type === 'checkbox'
|
|
57
|
+
? 'checkbox'
|
|
58
|
+
: type === 'radio'
|
|
59
|
+
? 'radio'
|
|
60
|
+
: 'text';
|
|
61
|
+
const rect = kind === 'checkbox' || kind === 'radio' ? square(box) : box;
|
|
62
|
+
const path = somPath(el, layer);
|
|
63
|
+
// A SOM path can recur: nine do on DAF 1288, six of them within a page, and
|
|
64
|
+
// every field on its second page starts `Page1.`. The page and an occurrence
|
|
65
|
+
// count make the AcroForm name unique without losing what the form called it.
|
|
66
|
+
const base = `p${pageNumber}.${path}`;
|
|
67
|
+
const x = +(rect.left - origin.left).toFixed(2);
|
|
68
|
+
const y = +(rect.top - origin.top).toFixed(2);
|
|
69
|
+
const w = +rect.width.toFixed(2);
|
|
70
|
+
const h = +rect.height.toFixed(2);
|
|
71
|
+
// pdf.js gives a control the place its layout comes to, on the page or not.
|
|
72
|
+
if (x >= page.width || y >= page.height || x + w <= 0 || y + h <= 0)
|
|
73
|
+
throw new Refused({ code: 'layout', detail: 'a field is off its page' });
|
|
74
|
+
const tooltip = tooltipOf(el);
|
|
75
|
+
const options = tag === 'select'
|
|
76
|
+
? [...el.options]
|
|
77
|
+
.map((o) => o.textContent?.trim() ?? '')
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
: [];
|
|
80
|
+
// A template can hold a field twice at one place, the second drawn over the
|
|
81
|
+
// first. Controls that read the same in everything but their order are one field.
|
|
82
|
+
const place = JSON.stringify([base, kind, x, y, w, h, tooltip, options]);
|
|
83
|
+
if (places.has(place))
|
|
84
|
+
continue;
|
|
85
|
+
places.add(place);
|
|
86
|
+
const count = (seen.get(base) ?? 0) + 1;
|
|
87
|
+
seen.set(base, count);
|
|
88
|
+
const excl = kind === 'radio' ? exclusion(el, layer) : null;
|
|
89
|
+
widgets.push({
|
|
90
|
+
name: count === 1 ? base : `${base}#${count}`,
|
|
91
|
+
kind,
|
|
92
|
+
page: pageNumber - 1,
|
|
93
|
+
x,
|
|
94
|
+
y,
|
|
95
|
+
w,
|
|
96
|
+
h,
|
|
97
|
+
group: excl ? `p${pageNumber}.${somPath(excl, layer)}` : undefined,
|
|
98
|
+
// The leaf of the SOM path is what the form calls this option. It becomes
|
|
99
|
+
// the widget's on-state, which is the only thing telling one member of an
|
|
100
|
+
// exclusion group from another.
|
|
101
|
+
on: excl ? path.slice(path.lastIndexOf('.') + 1) : undefined,
|
|
102
|
+
tooltip,
|
|
103
|
+
options
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return widgets;
|
|
107
|
+
}
|
|
108
|
+
/** `Page1.form1.page1.TypeRequest.type_of_request`, root-first. */
|
|
109
|
+
function somPath(el, layer) {
|
|
110
|
+
const parts = [];
|
|
111
|
+
for (let node = el; node && node !== layer; node = node.parentElement) {
|
|
112
|
+
const name = node.getAttribute('xfaname');
|
|
113
|
+
if (name)
|
|
114
|
+
parts.unshift(name);
|
|
115
|
+
}
|
|
116
|
+
return parts.length ? parts.join('.') : el.id || el.getAttribute('name') || 'field';
|
|
117
|
+
}
|
|
118
|
+
function exclusion(el, layer) {
|
|
119
|
+
const found = el.closest('.xfaExclgroup[xfaname]');
|
|
120
|
+
return found && layer.contains(found) ? found : null;
|
|
121
|
+
}
|
|
122
|
+
/** The hover text a filler reads. */
|
|
123
|
+
function tooltipOf(el) {
|
|
124
|
+
const field = el.closest('.xfaField');
|
|
125
|
+
const direct = el.getAttribute('aria-label') || el.getAttribute('title') || field?.getAttribute('title');
|
|
126
|
+
if (direct?.trim())
|
|
127
|
+
return direct.trim();
|
|
128
|
+
// pdf.js rewrites the caption's class for a check-button UI, which is every
|
|
129
|
+
// radio and every checkbox, so the one selector misses both.
|
|
130
|
+
return (field?.querySelector('.xfaCaption, .xfaCaptionForCheckButton')?.textContent?.trim() || undefined);
|
|
131
|
+
}
|
package/dist/xfa.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A dynamic Adobe LiveCycle form as an ordinary static AcroForm, or the input
|
|
3
|
+
* unchanged when it is not one.
|
|
4
|
+
*
|
|
5
|
+
* In a dynamic form the fields exist only as XML inside `/AcroForm /XFA`:
|
|
6
|
+
* `/Fields` is empty, and the one page in the file is a placeholder, most often
|
|
7
|
+
* a "Please wait" notice. Converting one takes an XFA layout engine, and pdf.js
|
|
8
|
+
* has one. This runs it, reads the geometry the browser solved off the DOM, and
|
|
9
|
+
* writes both the printed page and the fields back out as a PDF the later
|
|
10
|
+
* stages already take.
|
|
11
|
+
*
|
|
12
|
+
* It is a normalization: bytes in, bytes out, and every later stage reads the
|
|
13
|
+
* result as it reads a static form.
|
|
14
|
+
*/
|
|
15
|
+
export declare function xfaToAcroForm(bytes: Uint8Array, signal?: AbortSignal): Promise<Uint8Array>;
|