@quillmark/quillify 0.0.0-stage → 0.3.0

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 +55 -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 +148 -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 +84 -0
  20. package/dist/display-list.js +339 -0
  21. package/dist/index.d.ts +6 -0
  22. package/dist/index.js +3 -0
  23. package/dist/inventory.d.ts +4 -0
  24. package/dist/inventory.js +175 -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 +159 -0
  29. package/dist/pdfjs.d.ts +17 -0
  30. package/dist/pdfjs.js +95 -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 +77 -0
  38. package/dist/strip.js +524 -0
  39. package/dist/types.d.ts +110 -0
  40. package/dist/types.js +27 -0
  41. package/dist/xfa-layer.d.ts +40 -0
  42. package/dist/xfa-layer.js +139 -0
  43. package/dist/xfa-widgets.d.ts +36 -0
  44. package/dist/xfa-widgets.js +128 -0
  45. package/dist/xfa.d.ts +15 -0
  46. package/dist/xfa.js +345 -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 +69 -5
@@ -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:` or `data:`, which is how pdf.js hands it a form's images
23
+ * and the stylesheet its field background. A face of the form is not the
24
+ * document's to load: pdf.js makes it in the host page's realm, from the
25
+ * file's bytes, a `local()` name or a face 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,139 @@
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:` or `data:`, which is how pdf.js hands it a form's images
25
+ * and the stylesheet its field background. A face of the form is not the
26
+ * document's to load: pdf.js makes it in the host page's realm, from the
27
+ * file's bytes, a `local()` name or a face 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
+ frameDocument.adoptedStyleSheets = [sheet];
65
+ // A sheet that did not apply leaves every field at its content size,
66
+ // and the rects read off it are wrong with no error.
67
+ const probe = frameDocument.createElement('div');
68
+ probe.className = 'xfaLayer';
69
+ frameDocument.body.append(probe);
70
+ const applies = view.getComputedStyle(probe).position === 'absolute';
71
+ probe.remove();
72
+ if (!applies)
73
+ throw new Error('The XFA stylesheet did not apply to the stage.');
74
+ // pdf.js names a face with its family in quotes. Chromium keeps the quotes
75
+ // as part of the name, and no rule of the form then asks for that face.
76
+ const faces = frameDocument.fonts;
77
+ const add = faces.add.bind(faces);
78
+ faces.add = (face) => {
79
+ if (/^".*"$/.test(face.family))
80
+ face.family = face.family.slice(1, -1);
81
+ return add(face);
82
+ };
83
+ return frameDocument;
84
+ });
85
+ }
86
+ /**
87
+ * Lay one page out. Each page gets its own relative wrapper: `.xfaLayer` is
88
+ * absolutely positioned, so sibling layers would stack at the initial
89
+ * containing block and every page after the first would measure against the
90
+ * wrong origin.
91
+ */
92
+ async lay(page) {
93
+ const pdfjs = await import('pdfjs-dist');
94
+ const xfa = await page.getXfa();
95
+ if (!xfa)
96
+ throw new Error(`XFA page ${page.pageNumber} laid out to nothing.`);
97
+ const viewport = pdfjs.XfaLayer.getPageViewport(xfa, { scale: 1, rotation: 0 });
98
+ // pdf.js lays out a page area whose medium has no size, and gives the page
99
+ // a side of nothing, a negative one, or no number at all.
100
+ if (!(viewport.width > 0 && viewport.height > 0))
101
+ throw new Refused({ code: 'layout', detail: 'a page has no size' });
102
+ const frameDocument = await this.ready;
103
+ const host = frameDocument.createElement('div');
104
+ // On Linux Chromium fits text to the pixel grid and rounds each advance to
105
+ // a whole pixel, unless the text asks for geometric precision.
106
+ host.style.cssText = `position:relative;overflow:hidden;background:#fff;text-rendering:geometricPrecision;width:${viewport.width}px;height:${viewport.height}px`;
107
+ const layer = frameDocument.createElement('div');
108
+ layer.className = 'xfaLayer';
109
+ host.append(layer);
110
+ frameDocument.body.append(host);
111
+ pdfjs.XfaLayer.render({
112
+ viewport: viewport.clone({ dontFlip: true }),
113
+ div: layer,
114
+ xfaHtml: xfa,
115
+ linkService: NO_LINKS,
116
+ intent: 'display'
117
+ });
118
+ await frameDocument.fonts.ready;
119
+ // A picture has a size once it has loaded, and one that fails to has none.
120
+ await Promise.all(Array.from(layer.querySelectorAll('img'), (img) => img.complete || new Promise((done) => (img.onload = img.onerror = done))));
121
+ // A frame the host page hid or took out gives the wrapper no box.
122
+ if (!host.getClientRects().length)
123
+ throw new Error('The host page hid or removed the frame the form is laid out in.');
124
+ // The frame's zoom times its ancestors' is a 32-bit float, and for some
125
+ // zooms no value brings it to 1. A rule marked `!important` beats the
126
+ // frame's own.
127
+ if ((this.#frame.currentCSSZoom ?? 1) !== 1)
128
+ throw new Error("The host page's zoom scales the frame the form is laid out in.");
129
+ return {
130
+ layer,
131
+ origin: host.getBoundingClientRect(),
132
+ width: viewport.width,
133
+ height: viewport.height
134
+ };
135
+ }
136
+ close() {
137
+ this.#frame.remove();
138
+ }
139
+ }
@@ -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,128 @@
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
+ const tooltip = tooltipOf(el);
72
+ const options = tag === 'select'
73
+ ? [...el.options]
74
+ .map((o) => o.textContent?.trim() ?? '')
75
+ .filter(Boolean)
76
+ : [];
77
+ // A template can hold a field twice at one place, the second drawn over the
78
+ // first. Controls that read the same in everything but their order are one field.
79
+ const place = JSON.stringify([base, kind, x, y, w, h, tooltip, options]);
80
+ if (places.has(place))
81
+ continue;
82
+ places.add(place);
83
+ const count = (seen.get(base) ?? 0) + 1;
84
+ seen.set(base, count);
85
+ const excl = kind === 'radio' ? exclusion(el, layer) : null;
86
+ widgets.push({
87
+ name: count === 1 ? base : `${base}#${count}`,
88
+ kind,
89
+ page: pageNumber - 1,
90
+ x,
91
+ y,
92
+ w,
93
+ h,
94
+ group: excl ? `p${pageNumber}.${somPath(excl, layer)}` : undefined,
95
+ // The leaf of the SOM path is what the form calls this option. It becomes
96
+ // the widget's on-state, which is the only thing telling one member of an
97
+ // exclusion group from another.
98
+ on: excl ? path.slice(path.lastIndexOf('.') + 1) : undefined,
99
+ tooltip,
100
+ options
101
+ });
102
+ }
103
+ return widgets;
104
+ }
105
+ /** `Page1.form1.page1.TypeRequest.type_of_request`, root-first. */
106
+ function somPath(el, layer) {
107
+ const parts = [];
108
+ for (let node = el; node && node !== layer; node = node.parentElement) {
109
+ const name = node.getAttribute('xfaname');
110
+ if (name)
111
+ parts.unshift(name);
112
+ }
113
+ return parts.length ? parts.join('.') : el.id || el.getAttribute('name') || 'field';
114
+ }
115
+ function exclusion(el, layer) {
116
+ const found = el.closest('.xfaExclgroup[xfaname]');
117
+ return found && layer.contains(found) ? found : null;
118
+ }
119
+ /** The hover text a filler reads. */
120
+ function tooltipOf(el) {
121
+ const field = el.closest('.xfaField');
122
+ const direct = el.getAttribute('aria-label') || el.getAttribute('title') || field?.getAttribute('title');
123
+ if (direct?.trim())
124
+ return direct.trim();
125
+ // pdf.js rewrites the caption's class for a check-button UI, which is every
126
+ // radio and every checkbox, so the one selector misses both.
127
+ return (field?.querySelector('.xfaCaption, .xfaCaptionForCheckButton')?.textContent?.trim() || undefined);
128
+ }
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>;