@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
package/dist/strip.js ADDED
@@ -0,0 +1,524 @@
1
+ import { PDFArray, PDFBool, PDFDict, PDFDocument, PDFHexString, PDFName, PDFNull, PDFNumber, PDFRef, PDFStream, PDFString } from 'pdf-lib';
2
+ import { Refused } from './refusal.js';
3
+ /**
4
+ * What a background's dictionaries keep. A kind of dictionary lists its keys,
5
+ * and after a colon what a key leads to:
6
+ *
7
+ * Key a number, a name, a string, a boolean, null, or an array of those
8
+ * Key:kind a dictionary or a stream of that kind
9
+ * Key:[kind] one of them or an array of them
10
+ * Key:{kind} a dictionary of them by name
11
+ * Key:^kind a reference to one the walk reaches as that kind by another key
12
+ * Key:[^kind] such a reference, or an array of them and of arrays of them
13
+ * Key:* a value no row reads yet, kept whole
14
+ *
15
+ * A reference back can be to one of two kinds, `Key:^kind|kind`. A dictionary
16
+ * by name can hold one of a kind or an array of them, `Key:{[kind]}`, or values
17
+ * of the first sort and nothing else, `Key:{}`.
18
+ *
19
+ * A value of the first sort also stays where a kind is expected: a mask can be
20
+ * an array of numbers and a color space a name. A kind `STREAMS` names is a
21
+ * stream, which keeps the keys of `STREAM` beside its own, and no other kind is
22
+ * a stream. A kind whose keys are numbers is an array read by position, as a
23
+ * color space is: it loses every part past the last its row lists, and a part
24
+ * before that which is not what its position leads to becomes null. A kind
25
+ * `PICK` names is one of several, told apart by a key of the dictionary or a
26
+ * position of the array, which holds a name where `PICK` writes one and a
27
+ * number where it writes a number. Where `PICK` names a last kind, one that is
28
+ * told as none of the others is of that kind.
29
+ */
30
+ export const KINDS = {
31
+ catalog: 'Type Pages:tree OCProperties:layers StructTreeRoot:tags MarkInfo:marks Lang',
32
+ tree: 'Type Parent:^tree Kids:[node] Count Resources:resources Rotate',
33
+ page: 'Type Parent:^tree Resources:resources MediaBox Contents:[content] Rotate Group:group ' +
34
+ 'UserUnit StructParents Tabs Annots:[link]',
35
+ link: 'Type Subtype Rect Border BS:border C H QuadPoints F OC:oc Contents StructParent A:action ' +
36
+ 'Dest:dest',
37
+ border: 'Type W S D',
38
+ uri: 'S URI',
39
+ goto: 'S D:dest',
40
+ dest: '0:^page 1 2 3 4 5',
41
+ resources: 'Font:{font} XObject:{xobject} ExtGState:{gstate} ColorSpace:{space} Pattern:{pattern} ' +
42
+ 'Shading:{shading} Properties:{oc} ProcSet',
43
+ content: '',
44
+ image: 'Type Subtype Width Height BitsPerComponent ColorSpace:space Decode ImageMask Mask:image ' +
45
+ 'SMask:image SMaskInData Matte Interpolate Intent OC:oc StructParent',
46
+ form: 'Type Subtype FormType BBox Matrix Resources:resources Group:group OC:oc StructParent StructParents',
47
+ parms: 'Predictor Colors BitsPerComponent Columns EarlyChange K EndOfLine EncodedByteAlign Rows ' +
48
+ 'EndOfBlock BlackIs1 DamagedRowsBeforeError ColorTransform JBIG2Globals:data',
49
+ data: '',
50
+ group: 'Type S CS:space I K',
51
+ gstate: 'Type LW LC LJ ML D RI OP op OPM Font:fontsize BG:function BG2:function UCR:function ' +
52
+ 'UCR2:function TR:[function] TR2:[function] HT HTO FL SM SA BM SMask:mask CA ca AIS TK ' +
53
+ 'UseBlackPtComp',
54
+ fontsize: '0:font 1',
55
+ mask: 'Type S G:form BC TR:function',
56
+ ICCBased: '0 1:icc',
57
+ Indexed: '0 1:space 2 3:data',
58
+ Separation: '0 1 2:space 3:function',
59
+ DeviceN: '0 1 2:space 3:function',
60
+ CalRGB: '0 1:cie',
61
+ CalGray: '0 1:cie',
62
+ Lab: '0 1:cie',
63
+ Pattern: '0 1:space',
64
+ icc: 'N Alternate:space Range',
65
+ cie: 'WhitePoint BlackPoint Gamma Matrix Range',
66
+ function0: 'FunctionType Domain Range Size BitsPerSample Order Encode Decode',
67
+ function2: 'FunctionType Domain Range C0 C1 N',
68
+ function3: 'FunctionType Domain Range Functions:[function] Bounds Encode',
69
+ function4: 'FunctionType Domain Range',
70
+ shaded: 'ShadingType ColorSpace:space Background BBox AntiAlias Domain Matrix Coords Extend ' +
71
+ 'Function:[function]',
72
+ mesh: 'ShadingType ColorSpace:space Background BBox AntiAlias BitsPerCoordinate BitsPerComponent ' +
73
+ 'BitsPerFlag Decode VerticesPerRow Function:[function]',
74
+ tiling: 'Type PatternType PaintType TilingType BBox XStep YStep Resources:resources Matrix',
75
+ shadingpattern: 'Type PatternType Shading:shading Matrix ExtGState:gstate',
76
+ simple: 'Type Subtype Name BaseFont FirstChar LastChar Widths FontDescriptor:descriptor ' +
77
+ 'Encoding:encoding ToUnicode:cmap',
78
+ type3: 'Type Subtype Name FontBBox FontMatrix CharProcs:{content} Encoding:encoding FirstChar ' +
79
+ 'LastChar Widths FontDescriptor:descriptor Resources:resources ToUnicode:cmap',
80
+ type0: 'Type Subtype BaseFont Encoding:cmap DescendantFonts:[font] ToUnicode:cmap',
81
+ cidfont: 'Type Subtype BaseFont CIDSystemInfo:cidsystem FontDescriptor:descriptor DW W DW2 W2 ' +
82
+ 'CIDToGIDMap:data',
83
+ cidsystem: 'Registry Ordering Supplement',
84
+ encoding: 'Type BaseEncoding Differences',
85
+ cmap: 'Type CMapName CIDSystemInfo:cidsystem WMode UseCMap:cmap',
86
+ descriptor: 'Type FontName FontFamily FontStretch FontWeight Flags FontBBox ItalicAngle Ascent Descent ' +
87
+ 'Leading CapHeight XHeight StemV StemH AvgWidth MaxWidth MissingWidth CharSet Lang ' +
88
+ 'FontFile:fontfile FontFile2:fontfile FontFile3:fontfile CIDSet:data',
89
+ fontfile: 'Subtype Length1 Length2 Length3',
90
+ layers: 'OCGs:[ocg] D:config',
91
+ config: 'Name BaseState ON:[^ocg] OFF:[^ocg] Intent AS:[auto] Order:[^ocg] ListMode RBGroups:[^ocg] ' +
92
+ 'Locked:[^ocg]',
93
+ auto: 'Event Category OCGs:[^ocg]',
94
+ ocg: 'Type Name Intent Usage:usage',
95
+ usage: 'Print:state View:state Export:state PageElement:state',
96
+ state: 'Subtype PrintState ViewState ExportState',
97
+ ocmd: 'Type OCGs:[ocg] P VE:[^ocg]',
98
+ marked: '',
99
+ marks: 'Marked Suspects',
100
+ tags: 'Type K:[kid] IDTree:ids ParentTree:parents ParentTreeNextKey RoleMap:{} ClassMap:{[attributes]}',
101
+ elem: 'Type S P:^elem|tags ID Pg:^page K:[kid] A:[attributes] C T Lang Alt E ActualText',
102
+ mcr: 'Type Pg:^page MCID',
103
+ objr: 'Type Pg:^page Obj:^link',
104
+ attributes: 'O Headers Scope ColSpan RowSpan Summary BBox Width Height Placement BackgroundColor ' +
105
+ 'BorderColor BorderStyle BorderThickness SpaceBefore SpaceAfter StartIndent EndIndent ' +
106
+ 'TextIndent TextAlign BlockAlign InlineAlign BaselineShift LineHeight TextDecorationColor ' +
107
+ 'TextDecorationThickness TextDecorationType ColumnCount ColumnGap ColumnWidths ' +
108
+ 'ListNumbering Role checked ADBE_MinWidth',
109
+ parents: 'Kids:[parents] Nums:[^elem] Limits',
110
+ ids: 'Kids:[ids] Names:[^elem] Limits'
111
+ };
112
+ export const STREAMS = [
113
+ 'content',
114
+ 'image',
115
+ 'form',
116
+ 'data',
117
+ 'icc',
118
+ 'function0',
119
+ 'function4',
120
+ 'mesh',
121
+ 'tiling',
122
+ 'cmap',
123
+ 'fontfile'
124
+ ];
125
+ export const STREAM = 'Length Filter DecodeParms:[parms]';
126
+ export const PICK = {
127
+ node: ['Type', { Pages: 'tree', Page: 'page' }],
128
+ action: ['S', { URI: 'uri', GoTo: 'goto' }],
129
+ xobject: ['Subtype', { Image: 'image', Form: 'form' }],
130
+ font: [
131
+ 'Subtype',
132
+ {
133
+ Type1: 'simple',
134
+ MMType1: 'simple',
135
+ TrueType: 'simple',
136
+ Type3: 'type3',
137
+ Type0: 'type0',
138
+ CIDFontType0: 'cidfont',
139
+ CIDFontType2: 'cidfont'
140
+ }
141
+ ],
142
+ space: [
143
+ '0',
144
+ {
145
+ ICCBased: 'ICCBased',
146
+ Indexed: 'Indexed',
147
+ Separation: 'Separation',
148
+ DeviceN: 'DeviceN',
149
+ CalRGB: 'CalRGB',
150
+ CalGray: 'CalGray',
151
+ Lab: 'Lab',
152
+ Pattern: 'Pattern'
153
+ }
154
+ ],
155
+ function: ['FunctionType', { 0: 'function0', 2: 'function2', 3: 'function3', 4: 'function4' }],
156
+ shading: [
157
+ 'ShadingType',
158
+ { 1: 'shaded', 2: 'shaded', 3: 'shaded', 4: 'mesh', 5: 'mesh', 6: 'mesh', 7: 'mesh' }
159
+ ],
160
+ pattern: ['PatternType', { 1: 'tiling', 2: 'shadingpattern' }],
161
+ oc: ['Type', { OCG: 'ocg', OCMD: 'ocmd' }, 'marked'],
162
+ kid: ['Type', { MCR: 'mcr', OBJR: 'objr' }, 'elem']
163
+ };
164
+ const SHAPES = { '[': 'list', '{': 'map', '^': 'back' };
165
+ const SLOTS = new Map();
166
+ /** The slot a spelling of the table names. One spelling is one object, so the walk compares slots as they are. */
167
+ function slot(text) {
168
+ let read = SLOTS.get(text);
169
+ if (!read) {
170
+ const many = text.startsWith('[^');
171
+ const shape = many ? 'back' : (SHAPES[text.charAt(0)] ?? 'one');
172
+ const inner = many ? text.slice(2, -1) : shape === 'back' ? text.slice(1) : text.slice(1, -1);
173
+ read = { shape, kind: shape === 'one' ? text : inner, many };
174
+ SLOTS.set(text, read);
175
+ }
176
+ return read;
177
+ }
178
+ const WHOLE = slot('*');
179
+ /** A key of a row: a name in a dictionary, or a position in an array. */
180
+ const keyed = (text) => (/^\d+$/.test(text) ? Number(text) : PDFName.of(text));
181
+ /** What a dictionary holds at a name or an array at a position, read through a reference. */
182
+ const under = (held, key) => {
183
+ if (held instanceof PDFArray)
184
+ return typeof key === 'number' ? held.lookup(key) : undefined;
185
+ return typeof key === 'number' ? undefined : held.lookup(key);
186
+ };
187
+ /** Each kind's keys with the slot of each, and what the kind is: a dictionary, a stream or an array. */
188
+ const ROWS = new Map(Object.entries(KINDS).map(([kind, keys]) => {
189
+ const stream = STREAMS.includes(kind);
190
+ const entries = (stream ? `${keys} ${STREAM}` : keys).split(' ').filter(Boolean);
191
+ const row = new Map(entries.map((entry) => {
192
+ const [key, leads = ''] = entry.split(':');
193
+ return [keyed(key), slot(leads)];
194
+ }));
195
+ return [kind, { body: stream ? 'stream' : row.has(0) ? 'array' : 'dict', keys: row }];
196
+ }));
197
+ const PICKS = new Map(Object.entries(PICK).map(([kind, [key, kinds, other]]) => [
198
+ kind,
199
+ {
200
+ key: keyed(key),
201
+ kinds: new Map(Object.entries(kinds).map(([told, kind]) => [keyed(told), kind])),
202
+ other
203
+ }
204
+ ]));
205
+ /**
206
+ * The `acroform` background: the source file's pages, and of each dictionary
207
+ * they hold the keys `KINDS` lists for its kind. With the rest of the catalog
208
+ * go the form (`/AcroForm` with its XFA packet, `/NeedsRendering`), the XMP
209
+ * metadata, the scripts, attachments, portfolio and bookmarks, and the
210
+ * usage-rights signature under `/Perms`. The signature goes here because qpdf's
211
+ * `--remove-restrictions`, which takes it too, also strips `/FT /Sig` from
212
+ * every signature field. The trailer's `/Info` and `/ID` go too.
213
+ *
214
+ * Of a page's annotations a Link stays, when it leads to an http or https
215
+ * address or to an explicit destination in the file, and leads there one way.
216
+ * A named destination resolves through the catalog's `/Dests` or `/Names`,
217
+ * which go, so a link to one goes too, and so does a link with an action
218
+ * chained after its own. The action a link keeps is a copy, so nothing the file
219
+ * hangs on the same object is cut with it.
220
+ *
221
+ * Each page's MediaBox is set to the box of its entry of `views`, the frame the
222
+ * inventory measured that page's widgets in. No box of the file is read here. A
223
+ * file whose pages are not the objects `views` names, in that order, is one
224
+ * pdf.js and pdf-lib read differently, and is refused as `damaged`.
225
+ *
226
+ * A walk from the catalog, level by level, then gives each object one kind, by
227
+ * the key that reaches it. A dictionary loses every key its kind does not list
228
+ * and every value that is not what its key leads to, and an object the walk
229
+ * does not reach is deleted: a saved file carries every object the context
230
+ * holds, reachable or not. A file in which a key reaches an object as another
231
+ * kind than it has is refused as `damaged`. A reference back to an object the
232
+ * walk did not reach as its kind is deleted, or made null in an array: a
233
+ * destination's page that the page tree does not hold, and a layer the default
234
+ * configuration names that no other key reaches.
235
+ *
236
+ * The structure tree has rows as the rest has. Of what lies outside it, it
237
+ * names a page of the page tree and a link that stays, each by a reference
238
+ * back, and nothing else: an element's `/Pg` to any other object is deleted,
239
+ * and an object reference whose `/Obj` is not such a link is taken out of its
240
+ * element's `/K`.
241
+ *
242
+ * An object that reads as an annotation or as a page, and is neither a page of
243
+ * `views` nor a link that stays, is deleted outright before the walk. No widget
244
+ * and no page outside the page tree is then in the file, whatever names one, a
245
+ * dictionary by name included, and the fields and appearance streams that hung
246
+ * off it are out of the walk's reach: a background that still carries a form
247
+ * layer is one the renderer would stamp a second one over.
248
+ */
249
+ export async function stripAcroForm(bytes, views) {
250
+ const doc = await PDFDocument.load(bytes, { updateMetadata: false });
251
+ const { context } = doc;
252
+ const at = (object, key) => object instanceof PDFDict ? object.lookup(PDFName.of(key)) : undefined;
253
+ 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
+ /** The action a link that stays keeps, `null` when it holds its destination itself, `false` when it goes. */
259
+ const leads = (link) => {
260
+ if (!link.has(PDFName.of('A')))
261
+ return at(link, 'Dest') instanceof PDFArray ? null : false;
262
+ const action = at(link, 'A');
263
+ if (link.has(PDFName.of('Dest')) || at(action, 'Next') !== undefined)
264
+ return false;
265
+ if (is(action, 'S', 'GoTo'))
266
+ return at(action, 'D') instanceof PDFArray && action.clone();
267
+ if (!is(action, 'S', 'URI'))
268
+ return false;
269
+ const uri = at(action, 'URI');
270
+ // A URI is ASCII, so its start is read off the bytes. Decoded as text, an
271
+ // address spelled in UTF-16 would pass here and be another one to a reader.
272
+ const head = uri instanceof PDFString || uri instanceof PDFHexString
273
+ ? String.fromCharCode(...uri.asBytes().subarray(0, 8))
274
+ : '';
275
+ return /^https?:\/\//i.test(head) && action.clone();
276
+ };
277
+ const pages = doc.getPages();
278
+ const differs = pages.some(({ ref }, index) => {
279
+ const viewed = views[index]?.ref;
280
+ return viewed?.num !== ref.objectNumber || viewed.gen !== ref.generationNumber;
281
+ });
282
+ if (differs || pages.length !== views.length)
283
+ throw new Refused({ code: 'damaged' });
284
+ const kept = new Set(pages.map((page) => page.ref));
285
+ const linked = new Set();
286
+ for (const [index, page] of pages.entries()) {
287
+ const listed = at(page.node, 'Annots');
288
+ const links = (listed instanceof PDFArray ? listed.asArray() : []).filter((entry) => {
289
+ const link = context.lookup(entry);
290
+ // An annotation that names its type names /Annot, so a page that lists
291
+ // itself is no link, whatever else it says.
292
+ const typed = at(link, 'Type') ?? PDFName.of('Annot');
293
+ if (!is(link, 'Subtype', 'Link') || typed !== PDFName.of('Annot'))
294
+ return false;
295
+ const action = leads(link);
296
+ if (action === false)
297
+ return false;
298
+ if (action)
299
+ link.set(PDFName.of('A'), action);
300
+ linked.add(entry);
301
+ return true;
302
+ });
303
+ page.node.set(PDFName.of('MediaBox'), context.obj(views[index].box));
304
+ page.node.delete(PDFName.of('Annots'));
305
+ if (links.length > 0)
306
+ page.node.set(PDFName.of('Annots'), context.obj(links));
307
+ }
308
+ context.trailerInfo.Info = undefined;
309
+ 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
+ const claimed = new Map();
314
+ const pending = [];
315
+ const whole = [];
316
+ const backs = [];
317
+ /** Whether what a reference names or an array holds leads nowhere: `false` while it is being read, so an array that holds itself leads somewhere. */
318
+ const plains = new Map();
319
+ /** Whether a value leads nowhere. A reference and an array are each read once, whatever reaches them, and a `[reference]` or an `[array]` among the parts marks where its own parts end. */
320
+ const plain = (value) => {
321
+ const parts = [value];
322
+ while (parts.length > 0) {
323
+ let part = parts.pop();
324
+ if (Array.isArray(part)) {
325
+ plains.set(part[0], true);
326
+ continue;
327
+ }
328
+ if (part instanceof PDFRef || part instanceof PDFArray) {
329
+ const known = plains.get(part);
330
+ if (known)
331
+ continue;
332
+ if (known === false)
333
+ return false;
334
+ plains.set(part, false);
335
+ parts.push([part]);
336
+ if (part instanceof PDFRef)
337
+ part = context.lookup(part);
338
+ }
339
+ if (part instanceof PDFArray)
340
+ for (let i = 0; i < part.size(); i++)
341
+ parts.push(part.get(i));
342
+ else if (!(part instanceof PDFNumber ||
343
+ part instanceof PDFName ||
344
+ part instanceof PDFString ||
345
+ part instanceof PDFHexString ||
346
+ part instanceof PDFBool ||
347
+ part === PDFNull))
348
+ return false;
349
+ }
350
+ return true;
351
+ };
352
+ /**
353
+ * Gives an object its kind and queues it, where this reach of it is as a kind.
354
+ * An object that has one is reached as that kind and no other, or the file is
355
+ * refused as `damaged`: the walk cuts it to the keys of the first, and a second
356
+ * would read what is left. Whether this reach of it stays.
357
+ */
358
+ const claim = (raw, value, as) => {
359
+ const had = claimed.get(raw);
360
+ if (had && had !== as)
361
+ throw new Refused({ code: 'damaged' });
362
+ if (had || !as)
363
+ return as !== undefined;
364
+ if (raw instanceof PDFRef)
365
+ claimed.set(raw, as);
366
+ pending.push([value, as]);
367
+ return true;
368
+ };
369
+ /** Whether a value stays where a key leads to `to`. */
370
+ const reach = (raw, to) => {
371
+ if (to === WHOLE || plain(raw)) {
372
+ whole.push(raw);
373
+ return true;
374
+ }
375
+ if (!to.kind && to.shape === 'one')
376
+ return false;
377
+ const value = raw instanceof PDFRef ? context.lookup(raw) : raw;
378
+ if (!(value instanceof PDFDict || value instanceof PDFStream || value instanceof PDFArray))
379
+ return false;
380
+ if (to.shape === 'map')
381
+ return claim(raw, value, value instanceof PDFDict ? to : undefined);
382
+ if (to.shape === 'list' && value instanceof PDFArray)
383
+ return claim(raw, value, to);
384
+ const pick = PICKS.get(to.kind);
385
+ const told = pick && under(value instanceof PDFStream ? value.dict : value, pick.key);
386
+ const kind = pick
387
+ ? (pick.kinds.get(told instanceof PDFNumber ? told.asNumber() : told) ?? pick.other)
388
+ : to.kind;
389
+ const body = value instanceof PDFStream ? 'stream' : value instanceof PDFArray ? 'array' : 'dict';
390
+ // A page is one the loop above framed. pdf-lib reads none in a dictionary
391
+ // written inside /Kids that names its /Type by reference. An object reference
392
+ // is one to a link that loop kept.
393
+ const fits = kind &&
394
+ ROWS.get(kind).body === body &&
395
+ (kind !== 'page' || kept.has(raw)) &&
396
+ (kind !== 'objr' || linked.has(value.get(PDFName.of('Obj'))));
397
+ return claim(raw, value, fits ? slot(kind) : undefined);
398
+ };
399
+ claim(context.trailerInfo.Root, doc.catalog, slot('catalog'));
400
+ for (let next = 0; next < pending.length; next++) {
401
+ const [value, as] = pending[next];
402
+ if (as.shape === 'list' && value instanceof PDFArray) {
403
+ let stay = 0;
404
+ for (let i = 0; i < value.size(); i++) {
405
+ const part = value.get(i);
406
+ if (reach(part, slot(as.kind)))
407
+ value.set(stay++, part);
408
+ }
409
+ while (value.size() > stay)
410
+ value.remove(value.size() - 1);
411
+ }
412
+ else if (as.shape === 'map' && value instanceof PDFDict) {
413
+ for (const [key, part] of value.entries())
414
+ if (!reach(part, slot(as.kind)))
415
+ value.delete(key);
416
+ }
417
+ else if (value instanceof PDFArray) {
418
+ const { keys } = ROWS.get(as.kind);
419
+ while (value.size() > keys.size)
420
+ value.remove(value.size() - 1);
421
+ for (let i = 0; i < value.size(); i++) {
422
+ const to = keys.get(i);
423
+ if (to.shape === 'back')
424
+ backs.push([value, i, to]);
425
+ else if (!reach(value.get(i), to))
426
+ value.set(i, PDFNull);
427
+ }
428
+ }
429
+ else {
430
+ const dict = value instanceof PDFStream ? value.dict : value;
431
+ const { keys } = ROWS.get(as.kind);
432
+ for (const [key, part] of dict.entries()) {
433
+ const to = keys.get(key);
434
+ if (to?.shape === 'back')
435
+ backs.push([dict, key, to]);
436
+ else if (!to || !reach(part, to))
437
+ dict.delete(key);
438
+ }
439
+ }
440
+ }
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
+ // A back reference stays when the walk reached what it names as its kind.
446
+ // Where the slot takes many, a value that leads nowhere stays too, and an
447
+ // array is read part by part, and so is each array in it. An array is read
448
+ // once. One the walk cut to a list of the kind stays wherever it is named.
449
+ // One that two such keys name by a reference stays under both, and inside an
450
+ // array a second reference to it does not, so no array is left holding itself.
451
+ // One that has any other kind gets the file refused.
452
+ // An array written in place is read here and never by `plain`, which would
453
+ // hold a note of each.
454
+ for (const [holder, key, to] of backs) {
455
+ const ones = to.kind.split('|').map(slot);
456
+ const list = slot(`[${to.kind}]`);
457
+ const arrays = [];
458
+ const stays = (within, part) => {
459
+ const had = claimed.get(part);
460
+ if (had && ones.includes(had))
461
+ return true;
462
+ if (!to.many)
463
+ return false;
464
+ if (had === list)
465
+ return true;
466
+ if (had === to)
467
+ return within instanceof PDFDict;
468
+ if (!(part instanceof PDFArray) && plain(part)) {
469
+ whole.push(part);
470
+ return true;
471
+ }
472
+ const value = part instanceof PDFRef ? context.lookup(part) : part;
473
+ if (!(value instanceof PDFArray))
474
+ return false;
475
+ if (had)
476
+ throw new Refused({ code: 'damaged' });
477
+ if (part instanceof PDFRef)
478
+ claimed.set(part, to);
479
+ arrays.push(value);
480
+ return true;
481
+ };
482
+ if (holder instanceof PDFArray) {
483
+ if (!stays(holder, holder.get(key)))
484
+ holder.set(key, PDFNull);
485
+ }
486
+ else if (!stays(holder, holder.get(key)))
487
+ holder.delete(key);
488
+ for (let array = arrays.pop(); array; array = arrays.pop())
489
+ for (let i = 0; i < array.size(); i++)
490
+ if (!stays(array, array.get(i)))
491
+ array.set(i, PDFNull);
492
+ }
493
+ // What no row reads and what leads nowhere are kept as they are and walked
494
+ // last, so an object a row reaches has its row whatever else names it.
495
+ while (whole.length > 0) {
496
+ let value = whole.pop();
497
+ if (value instanceof PDFRef) {
498
+ if (claimed.has(value))
499
+ continue;
500
+ claimed.set(value, WHOLE);
501
+ value = context.lookup(value);
502
+ }
503
+ if (value instanceof PDFArray)
504
+ for (let i = 0; i < value.size(); i++)
505
+ whole.push(value.get(i));
506
+ else if (value instanceof PDFDict)
507
+ for (const part of value.values())
508
+ whole.push(part);
509
+ else if (value instanceof PDFStream)
510
+ for (const part of value.dict.values())
511
+ whole.push(part);
512
+ }
513
+ // The trailer's /Size is the largest object number plus one, and deleting does
514
+ // not lower it.
515
+ let largest = 0;
516
+ for (const [ref] of context.enumerateIndirectObjects()) {
517
+ if (!claimed.has(ref))
518
+ context.delete(ref);
519
+ else if (ref.objectNumber > largest)
520
+ largest = ref.objectNumber;
521
+ }
522
+ context.largestObjectNumber = largest;
523
+ return doc.save({ useObjectStreams: false });
524
+ }
@@ -0,0 +1,110 @@
1
+ /** Geometry in PDF points, top-left origin, page-relative: the `acroform` backend's `rect`. */
2
+ export interface Rect {
3
+ x: number;
4
+ y: number;
5
+ w: number;
6
+ h: number;
7
+ }
8
+ /** AcroForm field type codes as pdf.js reports them. */
9
+ export declare const WIDGET_KINDS: readonly ["Tx", "Btn", "Ch", "Sig"];
10
+ export type WidgetKind = (typeof WIDGET_KINDS)[number];
11
+ /** One place a field is painted. */
12
+ export interface WidgetPlace {
13
+ page: number;
14
+ rect: Rect;
15
+ }
16
+ export interface InventoryWidget {
17
+ /** Unique within the inventory: the field name, suffixed when a name repeats. */
18
+ id: string;
19
+ name: string;
20
+ kind: WidgetKind;
21
+ /**
22
+ * Everywhere this field is painted. More than one when a form repeats a box:
23
+ * one name is one value in the file, so typing into any of them fills them
24
+ * all, and treating them as separate fields asks for the same answer twice.
25
+ */
26
+ places: WidgetPlace[];
27
+ multiLine: boolean;
28
+ checkBox: boolean;
29
+ radioButton: boolean;
30
+ combo: boolean;
31
+ options: string[];
32
+ readOnly: boolean;
33
+ required: boolean;
34
+ /** `/TU`, the hover text the form's author wrote. */
35
+ tooltip?: string;
36
+ /** `/MaxLen`, 0 when the field sets none. */
37
+ maxLen: number;
38
+ /** This widget's on-state name, which is how a radio group tells its options apart. */
39
+ buttonValue?: string;
40
+ }
41
+ export interface FieldInventory {
42
+ pages: {
43
+ width: number;
44
+ height: number;
45
+ }[];
46
+ widgets: InventoryWidget[];
47
+ }
48
+ /** A page as pdf.js read it. */
49
+ export interface View {
50
+ /** The object that is the page, by number and generation: null when pdf.js gives none. */
51
+ ref: {
52
+ num: number;
53
+ gen: number;
54
+ } | null;
55
+ /** The page's frame in PDF space, pdf.js's `view` of it, which every rect of the page is measured in. */
56
+ box: [x0: number, y0: number, x1: number, y1: number];
57
+ }
58
+ export type FieldType = 'string' | 'boolean' | 'enum' | 'array';
59
+ export interface QualifiedField {
60
+ /** `InventoryWidget.id` */
61
+ widget: string;
62
+ /** snake_case schema field name */
63
+ name: string;
64
+ label: string;
65
+ description: string;
66
+ type: FieldType;
67
+ multiline: boolean;
68
+ options?: string[];
69
+ example?: string;
70
+ keep: boolean;
71
+ /** `/Ff` bit 2: the form says this one has to be filled. */
72
+ required: boolean;
73
+ /** The radio group this option belongs to, when it is one. */
74
+ group?: string;
75
+ }
76
+ export interface QualifiedForm {
77
+ quill: {
78
+ name: string;
79
+ title: string;
80
+ description: string;
81
+ };
82
+ fields: QualifiedField[];
83
+ }
84
+ export declare const SNAKE_CASE: RegExp;
85
+ /** A quill as files, the shape `Quill.fromTree` and a quiver both take. */
86
+ export type QuillTree = Map<string, Uint8Array>;
87
+ /** The largest file `sanitize` opens. It bounds the compressed input. */
88
+ export declare const MAX_BYTES: number;
89
+ /**
90
+ * The most a file may come to once qpdf has it: the file qpdf writes together
91
+ * with what its Flate streams decode to, and qpdf's own memory. Of 1,423
92
+ * published files the largest comes to 15,046,490 bytes (DAF 57), and qpdf's
93
+ * memory is 17,235,968 bytes after each.
94
+ */
95
+ export declare const MAX_INFLATED_BYTES: number;
96
+ /** The most pages and fields a form has and is taken. The engine takes at most 1,000 fields in a block. */
97
+ export declare const MAX_PAGES = 25;
98
+ export declare const MAX_WIDGETS = 1000;
99
+ /**
100
+ * The most pixels the pictures of a dynamic form come to, each use counted.
101
+ * At four bytes a pixel that is 32 MB on a canvas, and as much in the
102
+ * background when the pictures are noise with an alpha channel. Of the 65 Air
103
+ * Force forms with a picture the largest comes to 6,958,512 (AF 4035).
104
+ */
105
+ export declare const MAX_PICTURE_PIXELS = 8000000;
106
+ /**
107
+ * How long `sanitize` works on a file before it refuses it. Of 59 published
108
+ * forms the slowest takes 2.7 s (SF 2809).
109
+ */
110
+ export declare const MAX_SECONDS = 60;
package/dist/types.js ADDED
@@ -0,0 +1,27 @@
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 qpdf's own memory. Of 1,423
9
+ * published files the largest comes to 15,046,490 bytes (DAF 57), and qpdf's
10
+ * memory is 17,235,968 bytes after each.
11
+ */
12
+ export const MAX_INFLATED_BYTES = 64 * 1024 * 1024;
13
+ /** The most pages and fields a form has and is taken. The engine takes at most 1,000 fields in a block. */
14
+ export const MAX_PAGES = 25;
15
+ export const MAX_WIDGETS = 1000;
16
+ /**
17
+ * The most pixels the pictures of a dynamic form come to, each use counted.
18
+ * At four bytes a pixel that is 32 MB on a canvas, and as much in the
19
+ * background when the pictures are noise with an alpha channel. Of the 65 Air
20
+ * Force forms with a picture the largest comes to 6,958,512 (AF 4035).
21
+ */
22
+ export const MAX_PICTURE_PIXELS = 8_000_000;
23
+ /**
24
+ * How long `sanitize` works on a file before it refuses it. Of 59 published
25
+ * forms the slowest takes 2.7 s (SF 2809).
26
+ */
27
+ export const MAX_SECONDS = 60;