@quillmark/quillify 0.3.0 → 0.4.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.
- package/NOTICE +1 -1
- package/README.md +2 -1
- package/assets/qpdf.worker.js +9 -2
- package/dist/api.d.ts +1 -0
- package/dist/assemble.d.ts +1 -1
- package/dist/assemble.js +2 -13
- package/dist/display-list.d.ts +17 -14
- package/dist/display-list.js +159 -120
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -0
- package/dist/inventory.js +6 -2
- package/dist/naming.js +2 -0
- package/dist/normalize.js +158 -14
- package/dist/pdfjs.js +3 -2
- package/dist/quillify.js +2 -2
- package/dist/strip.d.ts +11 -6
- package/dist/strip.js +30 -21
- package/dist/types.d.ts +20 -6
- package/dist/types.js +18 -3
- package/dist/xfa-layer.d.ts +4 -4
- package/dist/xfa-layer.js +8 -4
- package/dist/xfa-widgets.js +3 -0
- package/dist/xfa.js +87 -60
- package/package.json +3 -2
package/dist/normalize.js
CHANGED
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
* is ended at the write that passes it, and then in what qpdf wrote, on the
|
|
28
28
|
* page's thread and before any other reader opens it.
|
|
29
29
|
*/
|
|
30
|
-
import { ParseSpeeds, PDFArray, PDFDocument, PDFName, PDFRawStream } from 'pdf-lib';
|
|
31
|
-
import { openPdf } from './pdfjs.js';
|
|
30
|
+
import { ParseSpeeds, PDFArray, PDFBool, PDFDict, PDFDocument, PDFInvalidObject, PDFName, PDFNumber, PDFRawStream, PDFStream } from 'pdf-lib';
|
|
32
31
|
import { Refused } from './refusal.js';
|
|
33
32
|
import { MAX_BYTES, MAX_INFLATED_BYTES } from './types.js';
|
|
34
33
|
// Held in a constant so that Vite copies the worker byte for byte. Written
|
|
@@ -52,14 +51,141 @@ const TYPED = new RegExp(`/(?:${spelled('ObjStm')}|${spelled('XRef')})`);
|
|
|
52
51
|
* 1 MB handed over whole.
|
|
53
52
|
*/
|
|
54
53
|
const SLICE = 1024;
|
|
55
|
-
/** What qpdf leaves a stream under, alone or after one Flate layer. Each gives pixels
|
|
54
|
+
/** What qpdf leaves a stream under, alone or after one Flate layer. Each gives pixels. */
|
|
56
55
|
const CODECS = ['DCTDecode', 'JPXDecode', 'CCITTFaxDecode', 'JBIG2Decode'];
|
|
56
|
+
/** The components of a pixel in a color space that is this name or an array that begins with it. */
|
|
57
|
+
const COMPONENTS = new Map([
|
|
58
|
+
['DeviceGray', 1],
|
|
59
|
+
['CalGray', 1],
|
|
60
|
+
['Indexed', 1],
|
|
61
|
+
['Separation', 1],
|
|
62
|
+
['DeviceRGB', 3],
|
|
63
|
+
['CalRGB', 3],
|
|
64
|
+
['Lab', 3],
|
|
65
|
+
['DeviceCMYK', 4]
|
|
66
|
+
]);
|
|
57
67
|
/**
|
|
58
|
-
* The
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
68
|
+
* The bytes an image's pixels come to as its dictionary declares them: rows
|
|
69
|
+
* of whole bytes, at its bits to a component and the components of its color
|
|
70
|
+
* space, or at a bit to the pixel for a mask. A halftone's threshold array
|
|
71
|
+
* says a width and a height too, and neither bits nor a color space: a stream
|
|
72
|
+
* with a `/HalftoneType` and neither comes to two bytes to the pixel, the most
|
|
73
|
+
* such an array holds. A stream with no `/Width` and no `/Height` is no image
|
|
74
|
+
* and comes to none. One with either whose dictionary does not say what it
|
|
75
|
+
* comes to, in a component or more of a bit or more and in a number short of
|
|
76
|
+
* infinity, is refused as `damaged`.
|
|
77
|
+
*
|
|
78
|
+
* JPEG 2000 data says its own color space and bits, so an image under
|
|
79
|
+
* `JPXDecode` may leave either out, and counts at four components for the
|
|
80
|
+
* one and eight bits for the other, what a painter makes of a pixel. A
|
|
81
|
+
* CCITT decoder goes by the `/Columns` and `/Rows` of its `/DecodeParms`,
|
|
82
|
+
* so each counts where it is written and more than the image's own.
|
|
83
|
+
* `chain` is the stream's filters.
|
|
84
|
+
*/
|
|
85
|
+
function declared(dict, chain) {
|
|
86
|
+
if (!dict.has(PDFName.of('Width')) && !dict.has(PDFName.of('Height')))
|
|
87
|
+
return 0;
|
|
88
|
+
const number = (from, key) => {
|
|
89
|
+
const value = from.lookup(PDFName.of(key));
|
|
90
|
+
return value instanceof PDFNumber ? value.asNumber() : NaN;
|
|
91
|
+
};
|
|
92
|
+
const codec = chain.at(-1);
|
|
93
|
+
const jpx = codec === 'JPXDecode';
|
|
94
|
+
const parms = codec === 'CCITTFaxDecode' && dict.lookup(PDFName.of('DecodeParms'));
|
|
95
|
+
const fax = parms instanceof PDFArray ? parms.lookup(chain.length - 1) : parms;
|
|
96
|
+
const side = (key, said) => {
|
|
97
|
+
const own = number(dict, key);
|
|
98
|
+
const more = fax instanceof PDFDict ? number(fax, said) : NaN;
|
|
99
|
+
return more > own ? more : own;
|
|
100
|
+
};
|
|
101
|
+
const mask = dict.lookup(PDFName.of('ImageMask')) === PDFBool.True;
|
|
102
|
+
const space = dict.lookup(PDFName.of('ColorSpace'));
|
|
103
|
+
const threshold = dict.has(PDFName.of('HalftoneType')) && !space && !dict.has(PDFName.of('BitsPerComponent'));
|
|
104
|
+
const [family, part] = space instanceof PDFArray ? [space.lookup(0), space.lookup(1)] : [space];
|
|
105
|
+
const name = family instanceof PDFName ? family.decodeText() : '';
|
|
106
|
+
const components = mask || threshold
|
|
107
|
+
? 1
|
|
108
|
+
: name === 'ICCBased' && part instanceof PDFStream
|
|
109
|
+
? number(part.dict, 'N')
|
|
110
|
+
: name === 'DeviceN' && part instanceof PDFArray
|
|
111
|
+
? part.size()
|
|
112
|
+
: jpx && !space
|
|
113
|
+
? 4
|
|
114
|
+
: (COMPONENTS.get(name) ?? NaN);
|
|
115
|
+
const bits = mask
|
|
116
|
+
? 1
|
|
117
|
+
: threshold
|
|
118
|
+
? 16
|
|
119
|
+
: jpx && !dict.has(PDFName.of('BitsPerComponent'))
|
|
120
|
+
? 8
|
|
121
|
+
: number(dict, 'BitsPerComponent');
|
|
122
|
+
const bytes = Math.ceil((side('Width', 'Columns') * components * bits) / 8) * side('Height', 'Rows');
|
|
123
|
+
if (!(components >= 1 && bits >= 1 && bytes >= 0 && bytes < Infinity))
|
|
124
|
+
throw new Refused({ code: 'damaged' });
|
|
125
|
+
return bytes;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The bytes an image's pixels come to as its codec's own header says them,
|
|
129
|
+
* which is what a decoder that goes by the stream makes: the first frame
|
|
130
|
+
* header of a JPEG, or the SIZ segment of a JPEG 2000 codestream, bare or in
|
|
131
|
+
* the `jp2c` box of a JP2 file. None under any other codec, and none where the
|
|
132
|
+
* header is not there or is cut short, which is where the view throws.
|
|
133
|
+
*/
|
|
134
|
+
function headed(codec, data) {
|
|
135
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
136
|
+
const bytes = (width, height, components, bits) => Math.ceil((width * components * bits) / 8) * height;
|
|
137
|
+
try {
|
|
138
|
+
if (codec === 'DCTDecode') {
|
|
139
|
+
// From the first start of an image, where a reader that looks for one begins.
|
|
140
|
+
let at = 0;
|
|
141
|
+
while (view.getUint16(at) !== 0xffd8)
|
|
142
|
+
at += 1;
|
|
143
|
+
// As libjpeg finds a marker: past a byte that is none, a fill byte and a marker of no length.
|
|
144
|
+
for (at += 2;;) {
|
|
145
|
+
const marker = view.getUint8(at) === 0xff ? view.getUint8(at + 1) : 0xff;
|
|
146
|
+
if (marker === 0xff)
|
|
147
|
+
at += 1;
|
|
148
|
+
else if (marker >> 4 === 0xc && ![0xc4, 0xc8, 0xcc].includes(marker))
|
|
149
|
+
// A height of 0 is left to a DNL marker after the scan, which can say 65,535.
|
|
150
|
+
return bytes(view.getUint16(at + 7), view.getUint16(at + 5) || 0xffff, view.getUint8(at + 9), view.getUint8(at + 4));
|
|
151
|
+
else
|
|
152
|
+
at += marker <= 1 || (marker >= 0xd0 && marker <= 0xd9) ? 2 : 2 + view.getUint16(at + 2);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (codec === 'JPXDecode') {
|
|
156
|
+
let at = 0;
|
|
157
|
+
// A JP2 file is boxes: a length and a type, or a 1, a type and a length of eight bytes.
|
|
158
|
+
if (view.getUint32(4) === 0x6a502020) {
|
|
159
|
+
const wide = () => view.getUint32(at) === 1;
|
|
160
|
+
while (view.getUint32(at + 4) !== 0x6a703263)
|
|
161
|
+
at += (wide() ? Number(view.getBigUint64(at + 8)) : view.getUint32(at)) || Infinity;
|
|
162
|
+
at += wide() ? 16 : 8;
|
|
163
|
+
}
|
|
164
|
+
// The image is its grid less where it starts in it, at the most bits any component says.
|
|
165
|
+
if (view.getUint32(at) === 0xff4fff51) {
|
|
166
|
+
const components = view.getUint16(at + 40);
|
|
167
|
+
let bits = 0;
|
|
168
|
+
for (let each = 0; each < components; each++)
|
|
169
|
+
bits = Math.max(bits, (view.getUint8(at + 42 + 3 * each) & 127) + 1);
|
|
170
|
+
return bytes(view.getUint32(at + 8) - view.getUint32(at + 16), view.getUint32(at + 12) - view.getUint32(at + 20), components, bits);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// No header to go by.
|
|
176
|
+
}
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The size of the file qpdf wrote and of what its streams decode to, added up
|
|
181
|
+
* no further than the bound. pdf-lib lists the streams and the browser
|
|
182
|
+
* inflates each Flate one, keeping only what lies under a JPEG or JPEG 2000
|
|
183
|
+
* codec. An image comes to the most of what its dictionary declares, what
|
|
184
|
+
* its codec's own header says and what its Flate layer gives. An object
|
|
185
|
+
* pdf-lib cannot read, as a number of 400 digits, is one it would not count.
|
|
186
|
+
* Such an object, a stream the browser's decoder rejects, or one under a
|
|
187
|
+
* filter qpdf has not taken off, is one two readers can read two ways, and
|
|
188
|
+
* the file is refused as `damaged`.
|
|
63
189
|
*/
|
|
64
190
|
async function inflated(pdf) {
|
|
65
191
|
let total = pdf.byteLength;
|
|
@@ -72,6 +198,8 @@ async function inflated(pdf) {
|
|
|
72
198
|
parseSpeed: ParseSpeeds.Fastest
|
|
73
199
|
});
|
|
74
200
|
for (const [, object] of context.enumerateIndirectObjects()) {
|
|
201
|
+
if (object instanceof PDFInvalidObject)
|
|
202
|
+
throw new Refused({ code: 'damaged' });
|
|
75
203
|
if (!(object instanceof PDFRawStream))
|
|
76
204
|
continue;
|
|
77
205
|
const { dict, contents } = object;
|
|
@@ -85,10 +213,16 @@ async function inflated(pdf) {
|
|
|
85
213
|
const rest = chain.slice(flate ? 1 : 0);
|
|
86
214
|
if (rest.length > 1 || rest.some((name) => !CODECS.includes(name)))
|
|
87
215
|
throw new Refused({ code: 'damaged' });
|
|
216
|
+
const before = total;
|
|
217
|
+
const said = declared(dict, chain);
|
|
88
218
|
// An empty stream is one to every reader, and to the decoder a stream cut short.
|
|
89
|
-
if (!flate || !contents.length)
|
|
219
|
+
if (!flate || !contents.length) {
|
|
220
|
+
total += Math.max(said, headed(rest[0], contents));
|
|
90
221
|
continue;
|
|
222
|
+
}
|
|
91
223
|
let at = 0;
|
|
224
|
+
const keep = rest[0] === 'DCTDecode' || rest[0] === 'JPXDecode';
|
|
225
|
+
const under = [];
|
|
92
226
|
const reader = new ReadableStream({
|
|
93
227
|
pull(controller) {
|
|
94
228
|
if (at < contents.length)
|
|
@@ -104,6 +238,8 @@ async function inflated(pdf) {
|
|
|
104
238
|
total += read.value.byteLength;
|
|
105
239
|
if (total > MAX_INFLATED_BYTES)
|
|
106
240
|
return total;
|
|
241
|
+
if (keep)
|
|
242
|
+
under.push(read.value);
|
|
107
243
|
}
|
|
108
244
|
}
|
|
109
245
|
catch {
|
|
@@ -112,6 +248,8 @@ async function inflated(pdf) {
|
|
|
112
248
|
finally {
|
|
113
249
|
void reader.cancel().catch(() => { });
|
|
114
250
|
}
|
|
251
|
+
const own = keep ? headed(rest[0], new Uint8Array(await new Blob(under).arrayBuffer())) : 0;
|
|
252
|
+
total = Math.max(total, before + Math.max(said, own));
|
|
115
253
|
}
|
|
116
254
|
return total;
|
|
117
255
|
}
|
|
@@ -145,15 +283,21 @@ export async function normalizePdf(bytes, signal) {
|
|
|
145
283
|
});
|
|
146
284
|
if (!answer.loaded)
|
|
147
285
|
throw new Error(`qpdf.wasm did not load from ${WASM}`);
|
|
148
|
-
const { out, size } = answer;
|
|
286
|
+
const { out, size, said } = answer;
|
|
149
287
|
// A file that came to more than the bound in qpdf is past it, whatever qpdf then wrote.
|
|
150
288
|
const total = size > MAX_INFLATED_BYTES ? size : out?.byteLength ? await inflated(out) : 0;
|
|
151
289
|
if (total > MAX_INFLATED_BYTES)
|
|
152
290
|
throw new Refused({ code: 'too_large', bytes: total, limit: MAX_INFLATED_BYTES });
|
|
153
291
|
if (out?.byteLength)
|
|
154
292
|
return out;
|
|
155
|
-
// qpdf
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
293
|
+
// A qpdf that ends by itself ends at its first error, and its last line says
|
|
294
|
+
// when that is the password or the encryption. No other reader opens the
|
|
295
|
+
// file, which the bound does not hold: a PDF has its header in its first
|
|
296
|
+
// 1,024 bytes.
|
|
297
|
+
if (/invalid password\n$/.test(said))
|
|
298
|
+
throw new Refused({ code: 'password' });
|
|
299
|
+
if (/encryption.*\n$/.test(said))
|
|
300
|
+
throw new Refused({ code: 'encryption' });
|
|
301
|
+
const head = new TextDecoder('latin1').decode(bytes.subarray(0, 1024));
|
|
302
|
+
throw new Refused({ code: head.includes('%PDF-') ? 'damaged' : 'not_pdf' });
|
|
159
303
|
}
|
package/dist/pdfjs.js
CHANGED
|
@@ -2,8 +2,9 @@ import { Refused } from './refusal.js';
|
|
|
2
2
|
// Held in a constant so that Vite copies the worker byte for byte. Written
|
|
3
3
|
// inside `new Worker(...)`, the URL makes it bundle the file again.
|
|
4
4
|
const WORKER = new URL('../assets/pdf.worker.js', import.meta.url);
|
|
5
|
-
// The faces pdf.js lays a dynamic form's Arial, Helvetica, Myriad Pro
|
|
6
|
-
// and
|
|
5
|
+
// The faces pdf.js lays a dynamic form's Arial, Helvetica, Myriad Pro and
|
|
6
|
+
// Calibri out in, and any face the form's font list lacks, each named so that
|
|
7
|
+
// a bundler copies it.
|
|
7
8
|
const FACES = {
|
|
8
9
|
'LiberationSans-Regular.ttf': new URL('../assets/LiberationSans-Regular.ttf', import.meta.url),
|
|
9
10
|
'LiberationSans-Bold.ttf': new URL('../assets/LiberationSans-Bold.ttf', import.meta.url),
|
package/dist/quillify.js
CHANGED
|
@@ -50,12 +50,12 @@ export async function quillify(form, naming) {
|
|
|
50
50
|
return {
|
|
51
51
|
widget: entry.id,
|
|
52
52
|
name: entry.name,
|
|
53
|
+
...(entry.title === undefined ? {} : { title: entry.title }),
|
|
53
54
|
description: entry.description,
|
|
54
55
|
...typed(field, entry.text),
|
|
55
56
|
...(example === undefined ? {} : { example }),
|
|
56
57
|
keep: entry.keep && !field.readOnly,
|
|
57
|
-
required: field.required
|
|
58
|
-
...(field.kind === 'radio' ? { group: field.group } : {})
|
|
58
|
+
required: field.required
|
|
59
59
|
};
|
|
60
60
|
})
|
|
61
61
|
}, {
|
package/dist/strip.d.ts
CHANGED
|
@@ -67,11 +67,16 @@ export declare const PICK: Record<string, [key: string, kinds: Record<string, st
|
|
|
67
67
|
* and an object reference whose `/Obj` is not such a link is taken out of its
|
|
68
68
|
* element's `/K`.
|
|
69
69
|
*
|
|
70
|
-
* An object that
|
|
71
|
-
* `views` nor a link that stays,
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
70
|
+
* An object that says `/Type /Annot`, `/Subtype /Widget` or `/Type /Page`, and
|
|
71
|
+
* is neither a page of `views` nor a link that stays, has no kind. A file in
|
|
72
|
+
* which a key reaches one as a kind is refused as `damaged`, the catalog itself
|
|
73
|
+
* included, and so is a file whose catalog or page tree pdf-lib cannot read. A
|
|
74
|
+
* marked-content property list may say `/Type /Page`, and a `/Rect` says
|
|
75
|
+
* nothing: a kept object loses the keys its row does not list. So no object of
|
|
76
|
+
* a background is a widget or a page outside the page tree, and the fields and
|
|
77
|
+
* appearance streams that hung off one are out of the walk's reach: a
|
|
78
|
+
* background that still carries a form layer is one the renderer would stamp a
|
|
79
|
+
* second one over. A dictionary written in place is not an object: one by name
|
|
80
|
+
* keeps the plain values a file writes in it, a widget's among them.
|
|
76
81
|
*/
|
|
77
82
|
export declare function stripAcroForm(bytes: Uint8Array, views: View[]): Promise<Uint8Array>;
|
package/dist/strip.js
CHANGED
|
@@ -31,7 +31,7 @@ export const KINDS = {
|
|
|
31
31
|
catalog: 'Type Pages:tree OCProperties:layers StructTreeRoot:tags MarkInfo:marks Lang',
|
|
32
32
|
tree: 'Type Parent:^tree Kids:[node] Count Resources:resources Rotate',
|
|
33
33
|
page: 'Type Parent:^tree Resources:resources MediaBox Contents:[content] Rotate Group:group ' +
|
|
34
|
-
'
|
|
34
|
+
'StructParents Tabs Annots:[link]',
|
|
35
35
|
link: 'Type Subtype Rect Border BS:border C H QuadPoints F OC:oc Contents StructParent A:action ' +
|
|
36
36
|
'Dest:dest',
|
|
37
37
|
border: 'Type W S D',
|
|
@@ -239,22 +239,23 @@ const PICKS = new Map(Object.entries(PICK).map(([kind, [key, kinds, other]]) =>
|
|
|
239
239
|
* and an object reference whose `/Obj` is not such a link is taken out of its
|
|
240
240
|
* element's `/K`.
|
|
241
241
|
*
|
|
242
|
-
* An object that
|
|
243
|
-
* `views` nor a link that stays,
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
242
|
+
* An object that says `/Type /Annot`, `/Subtype /Widget` or `/Type /Page`, and
|
|
243
|
+
* is neither a page of `views` nor a link that stays, has no kind. A file in
|
|
244
|
+
* which a key reaches one as a kind is refused as `damaged`, the catalog itself
|
|
245
|
+
* included, and so is a file whose catalog or page tree pdf-lib cannot read. A
|
|
246
|
+
* marked-content property list may say `/Type /Page`, and a `/Rect` says
|
|
247
|
+
* nothing: a kept object loses the keys its row does not list. So no object of
|
|
248
|
+
* a background is a widget or a page outside the page tree, and the fields and
|
|
249
|
+
* appearance streams that hung off one are out of the walk's reach: a
|
|
250
|
+
* background that still carries a form layer is one the renderer would stamp a
|
|
251
|
+
* second one over. A dictionary written in place is not an object: one by name
|
|
252
|
+
* keeps the plain values a file writes in it, a widget's among them.
|
|
248
253
|
*/
|
|
249
254
|
export async function stripAcroForm(bytes, views) {
|
|
250
255
|
const doc = await PDFDocument.load(bytes, { updateMetadata: false });
|
|
251
256
|
const { context } = doc;
|
|
252
257
|
const at = (object, key) => object instanceof PDFDict ? object.lookup(PDFName.of(key)) : undefined;
|
|
253
258
|
const is = (object, key, name) => at(object, key) === PDFName.of(name);
|
|
254
|
-
/** An annotation need not say it is one: a `/Rect` or a `/Subtype /Widget` is enough. */
|
|
255
|
-
const annotation = (object) => is(object, 'Type', 'Annot') ||
|
|
256
|
-
at(object, 'Rect') !== undefined ||
|
|
257
|
-
is(object, 'Subtype', 'Widget');
|
|
258
259
|
/** The action a link that stays keeps, `null` when it holds its destination itself, `false` when it goes. */
|
|
259
260
|
const leads = (link) => {
|
|
260
261
|
if (!link.has(PDFName.of('A')))
|
|
@@ -274,7 +275,14 @@ export async function stripAcroForm(bytes, views) {
|
|
|
274
275
|
: '';
|
|
275
276
|
return /^https?:\/\//i.test(head) && action.clone();
|
|
276
277
|
};
|
|
277
|
-
|
|
278
|
+
// pdf-lib throws at a catalog or a page tree it does not read as one.
|
|
279
|
+
let pages;
|
|
280
|
+
try {
|
|
281
|
+
pages = doc.getPages();
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
throw new Refused({ code: 'damaged' });
|
|
285
|
+
}
|
|
278
286
|
const differs = pages.some(({ ref }, index) => {
|
|
279
287
|
const viewed = views[index]?.ref;
|
|
280
288
|
return viewed?.num !== ref.objectNumber || viewed.gen !== ref.generationNumber;
|
|
@@ -307,9 +315,6 @@ export async function stripAcroForm(bytes, views) {
|
|
|
307
315
|
}
|
|
308
316
|
context.trailerInfo.Info = undefined;
|
|
309
317
|
context.trailerInfo.ID = undefined;
|
|
310
|
-
for (const [ref, object] of context.enumerateIndirectObjects())
|
|
311
|
-
if (!kept.has(ref) && !linked.has(ref) && (is(object, 'Type', 'Page') || annotation(object)))
|
|
312
|
-
context.delete(ref);
|
|
313
318
|
const claimed = new Map();
|
|
314
319
|
const pending = [];
|
|
315
320
|
const whole = [];
|
|
@@ -353,11 +358,19 @@ export async function stripAcroForm(bytes, views) {
|
|
|
353
358
|
* Gives an object its kind and queues it, where this reach of it is as a kind.
|
|
354
359
|
* An object that has one is reached as that kind and no other, or the file is
|
|
355
360
|
* refused as `damaged`: the walk cuts it to the keys of the first, and a second
|
|
356
|
-
* would read what is left.
|
|
361
|
+
* would read what is left. So is a file that reaches as a kind an object that
|
|
362
|
+
* says it is an annotation or a page, and is neither a page of `views` nor a
|
|
363
|
+
* link that stays. Whether this reach of it stays.
|
|
357
364
|
*/
|
|
358
365
|
const claim = (raw, value, as) => {
|
|
359
366
|
const had = claimed.get(raw);
|
|
360
|
-
|
|
367
|
+
const posed = raw instanceof PDFRef &&
|
|
368
|
+
!kept.has(raw) &&
|
|
369
|
+
!linked.has(raw) &&
|
|
370
|
+
(is(value, 'Type', 'Annot') ||
|
|
371
|
+
is(value, 'Subtype', 'Widget') ||
|
|
372
|
+
(as !== slot('marked') && is(value, 'Type', 'Page')));
|
|
373
|
+
if ((had && had !== as) || (as && posed))
|
|
361
374
|
throw new Refused({ code: 'damaged' });
|
|
362
375
|
if (had || !as)
|
|
363
376
|
return as !== undefined;
|
|
@@ -438,10 +451,6 @@ export async function stripAcroForm(bytes, views) {
|
|
|
438
451
|
}
|
|
439
452
|
}
|
|
440
453
|
}
|
|
441
|
-
// A page tree node that reads as an annotation is deleted above, and the page
|
|
442
|
-
// tree holds the pages under it no more.
|
|
443
|
-
if (pages.some(({ ref }) => claimed.get(ref) !== slot('page')))
|
|
444
|
-
throw new Refused({ code: 'damaged' });
|
|
445
454
|
// A back reference stays when the walk reached what it names as its kind.
|
|
446
455
|
// Where the slot takes many, a value that leads nowhere stays too, and an
|
|
447
456
|
// array is read part by part, and so is each array in it. An array is read
|
package/dist/types.d.ts
CHANGED
|
@@ -62,6 +62,7 @@ export interface QualifiedField {
|
|
|
62
62
|
/** snake_case schema field name */
|
|
63
63
|
name: string;
|
|
64
64
|
label: string;
|
|
65
|
+
title?: string;
|
|
65
66
|
description: string;
|
|
66
67
|
type: FieldType;
|
|
67
68
|
multiline: boolean;
|
|
@@ -70,8 +71,6 @@ export interface QualifiedField {
|
|
|
70
71
|
keep: boolean;
|
|
71
72
|
/** `/Ff` bit 2: the form says this one has to be filled. */
|
|
72
73
|
required: boolean;
|
|
73
|
-
/** The radio group this option belongs to, when it is one. */
|
|
74
|
-
group?: string;
|
|
75
74
|
}
|
|
76
75
|
export interface QualifiedForm {
|
|
77
76
|
quill: {
|
|
@@ -82,20 +81,35 @@ export interface QualifiedForm {
|
|
|
82
81
|
fields: QualifiedField[];
|
|
83
82
|
}
|
|
84
83
|
export declare const SNAKE_CASE: RegExp;
|
|
85
|
-
/** A quill as files, the shape `Quill.fromTree`
|
|
84
|
+
/** A quill as files, the shape `Quill.fromTree` takes. */
|
|
86
85
|
export type QuillTree = Map<string, Uint8Array>;
|
|
87
86
|
/** The largest file `sanitize` opens. It bounds the compressed input. */
|
|
88
87
|
export declare const MAX_BYTES: number;
|
|
89
88
|
/**
|
|
90
89
|
* The most a file may come to once qpdf has it: the file qpdf writes together
|
|
91
|
-
* with what its Flate streams decode to
|
|
92
|
-
* published files the largest comes to
|
|
93
|
-
* memory is 17,235,968 bytes after
|
|
90
|
+
* with what its Flate streams decode to and what its images declare, and
|
|
91
|
+
* qpdf's own memory. Of 1,423 published files the largest comes to
|
|
92
|
+
* 48,477,587 bytes (AF 4101), and qpdf's memory is 17,235,968 bytes after
|
|
93
|
+
* each. Their largest image is 6,026 by 4,523 pixels at a bit each, 3,410,342
|
|
94
|
+
* bytes (AF 1520), and the one that declares the most bytes is 2,322 by 2,916
|
|
95
|
+
* at three bytes each, 20,312,856 (AF 4101).
|
|
94
96
|
*/
|
|
95
97
|
export declare const MAX_INFLATED_BYTES: number;
|
|
96
98
|
/** The most pages and fields a form has and is taken. The engine takes at most 1,000 fields in a block. */
|
|
97
99
|
export declare const MAX_PAGES = 25;
|
|
98
100
|
export declare const MAX_WIDGETS = 1000;
|
|
101
|
+
/** 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. */
|
|
102
|
+
export declare const limits: Readonly<{
|
|
103
|
+
bytes: number;
|
|
104
|
+
pages: number;
|
|
105
|
+
fields: number;
|
|
106
|
+
}>;
|
|
107
|
+
/**
|
|
108
|
+
* The longest side a page has and is taken, in points, and the farthest its
|
|
109
|
+
* box reaches from the origin: the PDF specification's limit for a side (ISO
|
|
110
|
+
* 32000-1, Annex C). Of 2,127 published pages the longest is 3,456 (AFMC 1656).
|
|
111
|
+
*/
|
|
112
|
+
export declare const MAX_SIDE = 14400;
|
|
99
113
|
/**
|
|
100
114
|
* The most pixels the pictures of a dynamic form come to, each use counted.
|
|
101
115
|
* At four bytes a pixel that is 32 MB on a canvas, and as much in the
|
package/dist/types.js
CHANGED
|
@@ -5,14 +5,29 @@ export const SNAKE_CASE = /^[a-z][a-z0-9_]*$/;
|
|
|
5
5
|
export const MAX_BYTES = 8 * 1024 * 1024;
|
|
6
6
|
/**
|
|
7
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
|
|
9
|
-
* published files the largest comes to
|
|
10
|
-
* memory is 17,235,968 bytes after
|
|
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).
|
|
11
14
|
*/
|
|
12
15
|
export const MAX_INFLATED_BYTES = 64 * 1024 * 1024;
|
|
13
16
|
/** The most pages and fields a form has and is taken. The engine takes at most 1,000 fields in a block. */
|
|
14
17
|
export const MAX_PAGES = 25;
|
|
15
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;
|
|
16
31
|
/**
|
|
17
32
|
* The most pixels the pictures of a dynamic form come to, each use counted.
|
|
18
33
|
* At four bytes a pixel that is 32 MB on a canvas, and as much in the
|
package/dist/xfa-layer.d.ts
CHANGED
|
@@ -19,10 +19,10 @@ export interface XfaPage {
|
|
|
19
19
|
* can zoom the frame, which the frame undoes, or `lay` throws.
|
|
20
20
|
*
|
|
21
21
|
* The frame runs no script, and its policy lets its document load only an
|
|
22
|
-
* image from `blob
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
26
|
*/
|
|
27
27
|
export declare class XfaStage {
|
|
28
28
|
#private;
|
package/dist/xfa-layer.js
CHANGED
|
@@ -21,10 +21,10 @@ const SHELL = `<meta http-equiv="Content-Security-Policy" content="default-src '
|
|
|
21
21
|
* can zoom the frame, which the frame undoes, or `lay` throws.
|
|
22
22
|
*
|
|
23
23
|
* The frame runs no script, and its policy lets its document load only an
|
|
24
|
-
* image from `blob
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
28
|
*/
|
|
29
29
|
export class XfaStage {
|
|
30
30
|
#frame = document.createElement('iframe');
|
|
@@ -61,6 +61,10 @@ export class XfaStage {
|
|
|
61
61
|
const view = frameDocument.defaultView;
|
|
62
62
|
const sheet = new view.CSSStyleSheet();
|
|
63
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);
|
|
64
68
|
frameDocument.adoptedStyleSheets = [sheet];
|
|
65
69
|
// A sheet that did not apply leaves every field at its content size,
|
|
66
70
|
// and the rects read off it are wrong with no error.
|
package/dist/xfa-widgets.js
CHANGED
|
@@ -68,6 +68,9 @@ export function harvestWidgets(page, pageNumber) {
|
|
|
68
68
|
const y = +(rect.top - origin.top).toFixed(2);
|
|
69
69
|
const w = +rect.width.toFixed(2);
|
|
70
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' });
|
|
71
74
|
const tooltip = tooltipOf(el);
|
|
72
75
|
const options = tag === 'select'
|
|
73
76
|
? [...el.options]
|