@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.
Files changed (59) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +85 -0
  3. package/README.md +56 -2
  4. package/assets/LiberationSans-Bold.ttf +0 -0
  5. package/assets/LiberationSans-BoldItalic.ttf +0 -0
  6. package/assets/LiberationSans-Italic.ttf +0 -0
  7. package/assets/LiberationSans-Regular.ttf +0 -0
  8. package/assets/pdf.worker.js +64908 -0
  9. package/assets/qpdf.wasm +0 -0
  10. package/assets/qpdf.worker.js +155 -0
  11. package/assets/xfa-layer.d.ts +2 -0
  12. package/assets/xfa-layer.js +1 -0
  13. package/dist/api.d.ts +83 -0
  14. package/dist/api.js +1 -0
  15. package/dist/assemble.d.ts +21 -0
  16. package/dist/assemble.js +155 -0
  17. package/dist/coords.d.ts +7 -0
  18. package/dist/coords.js +28 -0
  19. package/dist/display-list.d.ts +87 -0
  20. package/dist/display-list.js +372 -0
  21. package/dist/index.d.ts +6 -0
  22. package/dist/index.js +4 -0
  23. package/dist/inventory.d.ts +4 -0
  24. package/dist/inventory.js +179 -0
  25. package/dist/naming.d.ts +3 -0
  26. package/dist/naming.js +116 -0
  27. package/dist/normalize.d.ts +39 -0
  28. package/dist/normalize.js +303 -0
  29. package/dist/pdfjs.d.ts +17 -0
  30. package/dist/pdfjs.js +96 -0
  31. package/dist/quillify.d.ts +13 -0
  32. package/dist/quillify.js +86 -0
  33. package/dist/refusal.d.ts +47 -0
  34. package/dist/refusal.js +8 -0
  35. package/dist/sanitize.d.ts +20 -0
  36. package/dist/sanitize.js +72 -0
  37. package/dist/strip.d.ts +82 -0
  38. package/dist/strip.js +533 -0
  39. package/dist/types.d.ts +125 -0
  40. package/dist/types.js +42 -0
  41. package/dist/xfa-layer.d.ts +40 -0
  42. package/dist/xfa-layer.js +143 -0
  43. package/dist/xfa-widgets.d.ts +36 -0
  44. package/dist/xfa-widgets.js +131 -0
  45. package/dist/xfa.d.ts +15 -0
  46. package/dist/xfa.js +371 -0
  47. package/licenses/LiberationSans-LICENSE +102 -0
  48. package/licenses/compiler-rt-LICENSE +311 -0
  49. package/licenses/emscripten-LICENSE +102 -0
  50. package/licenses/libcxx-LICENSE +311 -0
  51. package/licenses/libcxxabi-LICENSE +311 -0
  52. package/licenses/libjpeg-turbo-LICENSE +132 -0
  53. package/licenses/libjpeg-turbo-README.ijg +258 -0
  54. package/licenses/musl-COPYRIGHT +193 -0
  55. package/licenses/pdf.js-LICENSE +177 -0
  56. package/licenses/qpdf-LICENSE +202 -0
  57. package/licenses/qpdf-NOTICE +42 -0
  58. package/licenses/zlib-README +118 -0
  59. 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>;