@jasy/pdf 1.0.0-alpha.8 → 1.0.0-alpha.9
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/README.md +4 -3
- package/dist/api/content.d.ts +5 -4
- package/dist/api/content.js +13 -7
- package/dist/api/dimension.d.ts +56 -0
- package/dist/api/dimension.js +27 -0
- package/dist/api/font-url.d.ts +28 -0
- package/dist/api/font-url.js +134 -0
- package/dist/api/gradient.d.ts +53 -0
- package/dist/api/gradient.js +97 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/insets.d.ts +19 -12
- package/dist/api/insets.js +18 -7
- package/dist/api/layout.d.ts +20 -6
- package/dist/api/layout.js +31 -6
- package/dist/api/page-builder.d.ts +8 -0
- package/dist/api/page-builder.js +12 -0
- package/dist/api/structure.d.ts +5 -0
- package/dist/api/structure.js +16 -4
- package/dist/api/text.d.ts +42 -6
- package/dist/api/text.js +29 -5
- package/dist/edit/document.d.ts +14 -4
- package/dist/edit/document.js +15 -22
- package/dist/edit/fill.d.ts +19 -2
- package/dist/edit/fill.js +63 -34
- package/dist/edit/flatten.d.ts +13 -1
- package/dist/edit/flatten.js +38 -13
- package/dist/edit/index.d.ts +2 -0
- package/dist/edit/index.js +3 -1
- package/dist/edit/objects.d.ts +5 -0
- package/dist/edit/objects.js +27 -0
- package/dist/edit/writer.d.ts +7 -0
- package/dist/edit/writer.js +23 -1
- package/dist/elements/container-element.d.ts +21 -1
- package/dist/elements/container-element.js +25 -10
- package/dist/elements/image-element.d.ts +18 -4
- package/dist/elements/image-element.js +27 -12
- package/dist/elements/layout/expanded-element.d.ts +3 -1
- package/dist/elements/layout/expanded-element.js +12 -3
- package/dist/elements/layout/padding-element.d.ts +4 -2
- package/dist/elements/layout/padding-element.js +4 -2
- package/dist/elements/layout/page-builder-element.js +2 -0
- package/dist/elements/pdf-element.d.ts +59 -1
- package/dist/elements/pdf-element.js +62 -1
- package/dist/elements/rectangle-element.d.ts +20 -6
- package/dist/elements/rectangle-element.js +25 -12
- package/dist/elements/row-element.d.ts +24 -1
- package/dist/elements/row-element.js +25 -10
- package/dist/elements/text-element.d.ts +60 -1
- package/dist/elements/text-element.js +132 -33
- package/dist/ir/display-list.d.ts +23 -6
- package/dist/ir/display-list.js +8 -1
- package/dist/layout/alignment.d.ts +10 -0
- package/dist/layout/alignment.js +6 -0
- package/dist/layout/box-constraints.d.ts +53 -0
- package/dist/layout/box-constraints.js +72 -0
- package/dist/layout/insets.d.ts +28 -0
- package/dist/layout/insets.js +20 -0
- package/dist/renderer/image-renderer.js +2 -1
- package/dist/renderer/pdf-backend.d.ts +6 -0
- package/dist/renderer/pdf-backend.js +78 -31
- package/dist/renderer/pdf-document-renderer.js +8 -1
- package/dist/renderer/rectangle-renderer.js +8 -2
- package/dist/renderer/text-renderer.d.ts +2 -2
- package/dist/renderer/text-renderer.js +150 -42
- package/dist/text/advance.js +5 -2
- package/dist/text/arabic.d.ts +20 -0
- package/dist/text/arabic.js +101 -0
- package/dist/text/bidi.d.ts +33 -0
- package/dist/text/bidi.js +94 -0
- package/dist/text/font-fallback.d.ts +25 -0
- package/dist/text/font-fallback.js +38 -0
- package/dist/text/line-breaker.d.ts +35 -1
- package/dist/text/line-breaker.js +76 -16
- package/dist/text/orphans-widows.d.ts +27 -0
- package/dist/text/orphans-widows.js +53 -0
- package/dist/text/shape.d.ts +29 -0
- package/dist/text/shape.js +67 -0
- package/dist/text/text-style.d.ts +25 -0
- package/dist/text/text-style.js +44 -1
- package/dist/utils/flex-layout.d.ts +24 -4
- package/dist/utils/flex-layout.js +169 -7
- package/dist/utils/font-metrics.d.ts +5 -0
- package/dist/utils/gsub.d.ts +36 -0
- package/dist/utils/gsub.js +178 -0
- package/dist/utils/inflate.d.ts +16 -0
- package/dist/utils/inflate.js +52 -0
- package/dist/utils/pdf-object-manager.d.ts +21 -0
- package/dist/utils/pdf-object-manager.js +107 -9
- package/dist/utils/ttf-parser.d.ts +6 -0
- package/dist/utils/ttf-parser.js +19 -0
- package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
- package/dist/utils/utf8-to-windows1252-encoder.js +10 -0
- package/dist/utils/woff.d.ts +10 -0
- package/dist/utils/woff.js +116 -0
- package/dist/validators/element-validator.js +9 -2
- package/package.json +2 -1
package/dist/edit/fill.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { PdfDocument } from "./document.js";
|
|
2
2
|
import { readAcroForm } from "./acroform-reader.js";
|
|
3
|
-
import { IncrementalWriter, serialize } from "./writer.js";
|
|
3
|
+
import { carryStrings, IncrementalWriter, serialize } from "./writer.js";
|
|
4
4
|
import { bakeAppearance, bakeButtonStates, fontMetrics, readLook } from "./appearance.js";
|
|
5
|
+
import { flattenInto } from "./flatten.js";
|
|
5
6
|
import { getArrayBuffer } from "../utils/utf8-to-windows1252-encoder.js";
|
|
6
|
-
import { get, isDict, isRef
|
|
7
|
+
import { get, isDict, isRef } from "./objects.js";
|
|
7
8
|
/** Field flags we have to honour when filling (see `forms/acroform.ts` for the writer's side). */
|
|
8
9
|
const FF_READ_ONLY = 1 << 0;
|
|
9
10
|
const FF_MULTI_SELECT = 1 << 21;
|
|
@@ -65,19 +66,6 @@ function withEntries(dict, changes, cipher) {
|
|
|
65
66
|
return `<< ${parts.join(" ")} >>`;
|
|
66
67
|
}
|
|
67
68
|
/** Every string reachable inside an object, so a dictionary can be re-enciphered in one walk. */
|
|
68
|
-
function stringsIn(o, out = [], depth = 0) {
|
|
69
|
-
if (o === undefined || o === null || depth > 64)
|
|
70
|
-
return out;
|
|
71
|
-
if (isString(o))
|
|
72
|
-
out.push(o);
|
|
73
|
-
else if (Array.isArray(o))
|
|
74
|
-
for (const e of o)
|
|
75
|
-
stringsIn(e, out, depth + 1);
|
|
76
|
-
else if (isDict(o))
|
|
77
|
-
for (const v of o.map.values())
|
|
78
|
-
stringsIn(v, out, depth + 1);
|
|
79
|
-
return out;
|
|
80
|
-
}
|
|
81
69
|
/**
|
|
82
70
|
* Draw the field again for its NEW value and append the XObject, returning its object number. Falls back
|
|
83
71
|
* to `undefined` when the widget does not say enough about itself to draw it - then the caller drops the
|
|
@@ -117,7 +105,24 @@ async function bakeStatesFor(doc, writer, field, widgetNum, state) {
|
|
|
117
105
|
data: (await doc.encryptForWrite(encoded)) ?? encoded,
|
|
118
106
|
});
|
|
119
107
|
};
|
|
120
|
-
|
|
108
|
+
// The object numbers come back too: a flatten in the same pass stamps one of these two states.
|
|
109
|
+
const onNum = await write(faces.on);
|
|
110
|
+
const offNum = await write(faces.off);
|
|
111
|
+
return {
|
|
112
|
+
ap: `<< /N << /${escName(on)} ${onNum} 0 R /Off ${offNum} 0 R >> >>`,
|
|
113
|
+
states: new Map([
|
|
114
|
+
[on, onNum],
|
|
115
|
+
["Off", offNum],
|
|
116
|
+
]),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** The appearance a widget ALREADY carries for one of its states, from `/AP /N /<state>`. */
|
|
120
|
+
function stateAppearance(doc, widgetNum, state) {
|
|
121
|
+
const states = doc.resolve(get(doc.resolve(get(doc.getObject(widgetNum), "AP")), "N"));
|
|
122
|
+
if (states === undefined || !isDict(states))
|
|
123
|
+
return undefined;
|
|
124
|
+
const chosen = states.map.get(state);
|
|
125
|
+
return isRef(chosen) ? chosen : undefined;
|
|
121
126
|
}
|
|
122
127
|
class FillError extends Error {
|
|
123
128
|
}
|
|
@@ -237,9 +242,22 @@ function plan(field, value, enciphered) {
|
|
|
237
242
|
* @param values field name -> value. Names are the fully qualified ones `readAcroForm` reports.
|
|
238
243
|
*/
|
|
239
244
|
export async function fillForm(bytes, values, options = {}) {
|
|
245
|
+
// Two option combinations describe a document nobody wants. Refused before the file is opened, since
|
|
246
|
+
// the answer does not depend on it.
|
|
247
|
+
if (options.fieldAppearances === false && options.needAppearances === false) {
|
|
248
|
+
throw new FillError("fieldAppearances and needAppearances are both false, which leaves the values invisible: nothing " +
|
|
249
|
+
"is drawn by jasy and the viewer is not asked to draw anything either. Turn one of them on");
|
|
250
|
+
}
|
|
251
|
+
if (options.flatten === true && options.fieldAppearances === false) {
|
|
252
|
+
throw new FillError("flatten needs fieldAppearances: flattening freezes the picture a field shows, and with " +
|
|
253
|
+
"fieldAppearances false there is no picture of the new value to freeze");
|
|
254
|
+
}
|
|
240
255
|
// Async because an encrypted document has to be deciphered before its field names mean anything, and
|
|
241
256
|
// because the values written back have to be enciphered again with the same file key.
|
|
242
|
-
const doc = await PdfDocument.open(bytes, {
|
|
257
|
+
const doc = await PdfDocument.open(bytes, {
|
|
258
|
+
password: options.password,
|
|
259
|
+
maxStreamSize: options.maxStreamSize,
|
|
260
|
+
});
|
|
243
261
|
if (!doc.canReEncrypt) {
|
|
244
262
|
throw new FillError("this PDF is encrypted with RC4 or AES-128; jasy can open it but writes only AES-256, so filling it " +
|
|
245
263
|
"would have to downgrade its protection - refused");
|
|
@@ -279,6 +297,8 @@ export async function fillForm(bytes, values, options = {}) {
|
|
|
279
297
|
const bake = options.fieldAppearances !== false;
|
|
280
298
|
// One metrics instance for the whole fill, not one per field and not one at module scope.
|
|
281
299
|
const metrics = bake ? fontMetrics() : undefined;
|
|
300
|
+
// Only collected when a flatten follows in this same pass; see `FreshAppearances` for why it exists.
|
|
301
|
+
const fresh = options.flatten ? new Map() : undefined;
|
|
282
302
|
// A widget we could not draw has no picture at all, so the viewer must be asked after all.
|
|
283
303
|
let someBakeFailed = false;
|
|
284
304
|
const filled = [];
|
|
@@ -323,6 +343,8 @@ export async function fillForm(bytes, values, options = {}) {
|
|
|
323
343
|
if (bake && baked === undefined)
|
|
324
344
|
someBakeFailed = true;
|
|
325
345
|
editsFor(w.num).AP = baked !== undefined ? `<< /N ${baked} 0 R >>` : undefined;
|
|
346
|
+
if (baked !== undefined)
|
|
347
|
+
fresh?.set(w.num, { kind: "ref", num: baked, gen: 0 });
|
|
326
348
|
}
|
|
327
349
|
}
|
|
328
350
|
for (const [widgetNum, state] of change.as ?? []) {
|
|
@@ -330,9 +352,20 @@ export async function fillForm(bytes, values, options = {}) {
|
|
|
330
352
|
// A button whose widget ships NO state pictures (PDFKit, react-pdf write none) stays an empty box
|
|
331
353
|
// in anything that does not redraw - and cannot be flattened at all. Draw the two states once.
|
|
332
354
|
if (bake && !field.widgets.find((w) => w.num === widgetNum)?.hasAppearance) {
|
|
333
|
-
const
|
|
334
|
-
if (
|
|
335
|
-
editsFor(widgetNum).AP = ap;
|
|
355
|
+
const drawn = await bakeStatesFor(doc, writer, field, widgetNum, state);
|
|
356
|
+
if (drawn !== undefined) {
|
|
357
|
+
editsFor(widgetNum).AP = drawn.ap;
|
|
358
|
+
const num = drawn.states.get(state);
|
|
359
|
+
if (num !== undefined)
|
|
360
|
+
fresh?.set(widgetNum, { kind: "ref", num, gen: 0 });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
// The widget keeps its own pictures and only /AS moves, so the state to stamp is the NEW one -
|
|
365
|
+
// reading /AS back out of the document would give the state before this fill.
|
|
366
|
+
const ref = stateAppearance(doc, widgetNum, state);
|
|
367
|
+
if (ref !== undefined)
|
|
368
|
+
fresh?.set(widgetNum, ref);
|
|
336
369
|
}
|
|
337
370
|
}
|
|
338
371
|
for (const [objNum, changes] of edits) {
|
|
@@ -355,18 +388,7 @@ export async function fillForm(bytes, values, options = {}) {
|
|
|
355
388
|
const alsoRewritten = bake || options.needAppearances !== false ? [acro, catalog] : [];
|
|
356
389
|
// One pass over every string that will be carried over unchanged. Without this a preserved tooltip or
|
|
357
390
|
// a /DA would be written back in the clear, into a file where nothing else is.
|
|
358
|
-
const carried =
|
|
359
|
-
if (doc.isEncrypted) {
|
|
360
|
-
for (const target of [...pending.map((p) => p.target), ...alsoRewritten]) {
|
|
361
|
-
for (const str of stringsIn(target)) {
|
|
362
|
-
if (carried.has(str))
|
|
363
|
-
continue;
|
|
364
|
-
const cipher = await doc.encryptForWrite(str.bytes);
|
|
365
|
-
if (cipher)
|
|
366
|
-
carried.set(str, hex(cipher));
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
391
|
+
const carried = await carryStrings(doc, [...pending.map((p) => p.target), ...alsoRewritten]);
|
|
370
392
|
for (const { objNum, target, changes } of pending) {
|
|
371
393
|
writer.update(objNum, withEntries(target, changes, carried));
|
|
372
394
|
}
|
|
@@ -374,8 +396,10 @@ export async function fillForm(bytes, values, options = {}) {
|
|
|
374
396
|
// When we DID draw, the flag has to go: a form that still asks for regeneration gets it, and the
|
|
375
397
|
// viewer throws our drawing away and redraws from /DA - which is how PDFKit's forms kept asking for a
|
|
376
398
|
// ZapfDingbats they do not ship.
|
|
399
|
+
// With a flatten following there is nothing left to answer it for, and that pass rewrites the same
|
|
400
|
+
// /AcroForm dictionary anyway.
|
|
377
401
|
const needAppearances = bake && !someBakeFailed ? "false" : "true";
|
|
378
|
-
if (bake || options.needAppearances !== false) {
|
|
402
|
+
if (!options.flatten && (bake || options.needAppearances !== false)) {
|
|
379
403
|
if (acro !== undefined && isDict(acro)) {
|
|
380
404
|
if (isRef(acroRef)) {
|
|
381
405
|
writer.update(acroRef.num, withEntries(acro, { NeedAppearances: needAppearances }, carried));
|
|
@@ -390,6 +414,11 @@ export async function fillForm(bytes, values, options = {}) {
|
|
|
390
414
|
}
|
|
391
415
|
}
|
|
392
416
|
}
|
|
393
|
-
|
|
417
|
+
// Same writer, so the whole operation stays one incremental update. Last, because it freezes the
|
|
418
|
+
// pictures drawn above.
|
|
419
|
+
const flattened = options.flatten
|
|
420
|
+
? await flattenInto(doc, writer, form.fields, carried, fresh)
|
|
421
|
+
: [];
|
|
422
|
+
return { bytes: writer.save(), warnings, filled, flattened };
|
|
394
423
|
}
|
|
395
424
|
export { FillError };
|
package/dist/edit/flatten.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PdfDocument } from "./document.js";
|
|
2
2
|
import { type ReadField } from "./acroform-reader.js";
|
|
3
3
|
import { IncrementalWriter, type StringCipher } from "./writer.js";
|
|
4
|
+
import { type PdfRef } from "./objects.js";
|
|
4
5
|
/**
|
|
5
6
|
* Flattening a form: the values stop being editable fields and become part of the page.
|
|
6
7
|
*
|
|
@@ -21,6 +22,8 @@ import { IncrementalWriter, type StringCipher } from "./writer.js";
|
|
|
21
22
|
export interface FlattenOptions {
|
|
22
23
|
/** The user password, for a document that is encrypted. */
|
|
23
24
|
password?: string;
|
|
25
|
+
/** Ceiling on what ONE stream may inflate to, in bytes (default 64 MB) - the zip-bomb guard. */
|
|
26
|
+
maxStreamSize?: number;
|
|
24
27
|
/** Flatten only these fields (fully qualified names). Omit for all of them. */
|
|
25
28
|
fields?: string[];
|
|
26
29
|
}
|
|
@@ -33,10 +36,19 @@ export interface FlattenResult {
|
|
|
33
36
|
export declare class FlattenError extends Error {
|
|
34
37
|
constructor(message: string);
|
|
35
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* The appearance streams a fill has just written, keyed by widget object number.
|
|
41
|
+
*
|
|
42
|
+
* Flattening reads the picture a widget shows out of the DOCUMENT, but mid-pass the new one is still in
|
|
43
|
+
* the writer - without this map a filled-and-flattened form freezes the value from before the fill.
|
|
44
|
+
* Both widget kinds go stale, for different reasons: a text field's `/AP` is replaced, a button's `/AP`
|
|
45
|
+
* keeps every state and only `/AS` moves.
|
|
46
|
+
*/
|
|
47
|
+
export type FreshAppearances = ReadonlyMap<number, PdfRef>;
|
|
36
48
|
/**
|
|
37
49
|
* Stamp the fields into their pages and take the widgets away. Shared by `flattenForm` and
|
|
38
50
|
* `fillForm(..., { flatten: true })`, so filling and flattening produce ONE incremental update.
|
|
39
51
|
*/
|
|
40
|
-
export declare function flattenInto(doc: PdfDocument, writer: IncrementalWriter, fields: ReadField[],
|
|
52
|
+
export declare function flattenInto(doc: PdfDocument, writer: IncrementalWriter, fields: ReadField[], carriedIn?: StringCipher, fresh?: FreshAppearances): Promise<string[]>;
|
|
41
53
|
/** Flatten an existing form: its values become page content and stop being editable. */
|
|
42
54
|
export declare function flattenForm(bytes: Uint8Array, options?: FlattenOptions): Promise<FlattenResult>;
|
package/dist/edit/flatten.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readAcroForm } from "./acroform-reader.js";
|
|
|
3
3
|
import { bakeAppearance, bakeButtonStates, readLook } from "./appearance.js";
|
|
4
4
|
import { getArrayBuffer } from "../utils/utf8-to-windows1252-encoder.js";
|
|
5
5
|
import { latin1FromBytes } from "../utils/bytes.js";
|
|
6
|
-
import { IncrementalWriter, serialize } from "./writer.js";
|
|
6
|
+
import { carryStrings, IncrementalWriter, serialize } from "./writer.js";
|
|
7
7
|
import { get, isDict, isRef, isStream, nameOf, } from "./objects.js";
|
|
8
8
|
export class FlattenError extends Error {
|
|
9
9
|
constructor(message) {
|
|
@@ -104,8 +104,11 @@ function appearanceOf(doc, widget) {
|
|
|
104
104
|
* Collect what has to happen. A widget that ships NO appearance gets one drawn now, from its own style
|
|
105
105
|
* and current value - the same drawing the fill path bakes. Without that, flattening a form from a
|
|
106
106
|
* producer that writes no appearances (PDFKit, react-pdf) could only ever refuse.
|
|
107
|
+
*
|
|
108
|
+
* `fresh` overrides where a widget's picture comes from. It is empty for a plain `flattenForm`, and
|
|
109
|
+
* carries the just-baked appearances when a fill flattens in the same pass - see `FreshAppearances`.
|
|
107
110
|
*/
|
|
108
|
-
async function planStamps(doc, writer, fields) {
|
|
111
|
+
async function planStamps(doc, writer, fields, fresh) {
|
|
109
112
|
const pages = widgetPages(doc);
|
|
110
113
|
const stamps = [];
|
|
111
114
|
// Widgets that leave the page without leaving a picture: a push button with no face of its own. It
|
|
@@ -119,7 +122,9 @@ async function planStamps(doc, writer, fields) {
|
|
|
119
122
|
if (w.num === undefined)
|
|
120
123
|
continue;
|
|
121
124
|
const widget = doc.getObject(w.num);
|
|
122
|
-
|
|
125
|
+
// A picture written during THIS pass wins: it lives in the writer, not in `doc`, so reading the
|
|
126
|
+
// document would freeze the value the field had BEFORE the fill.
|
|
127
|
+
let appearance = fresh?.get(w.num) ?? appearanceOf(doc, widget);
|
|
123
128
|
if (appearance === undefined) {
|
|
124
129
|
const drawnNum = await drawMissing(doc, writer, field, w.num);
|
|
125
130
|
if (drawnNum === undefined) {
|
|
@@ -277,14 +282,19 @@ function withEntries(dict, changes, cipher) {
|
|
|
277
282
|
* Stamp the fields into their pages and take the widgets away. Shared by `flattenForm` and
|
|
278
283
|
* `fillForm(..., { flatten: true })`, so filling and flattening produce ONE incremental update.
|
|
279
284
|
*/
|
|
280
|
-
export async function flattenInto(doc, writer, fields,
|
|
285
|
+
export async function flattenInto(doc, writer, fields, carriedIn, fresh) {
|
|
281
286
|
if (fields.length === 0)
|
|
282
287
|
return [];
|
|
283
|
-
const { stamps, removeOnly } = await planStamps(doc, writer, fields);
|
|
288
|
+
const { stamps, removeOnly } = await planStamps(doc, writer, fields, fresh);
|
|
284
289
|
// Group by page: one appended content stream per page, not one per widget.
|
|
285
290
|
const byPage = new Map();
|
|
286
291
|
for (const s of stamps)
|
|
287
292
|
(byPage.get(s.pageNum) ?? byPage.set(s.pageNum, []).get(s.pageNum)).push(s);
|
|
293
|
+
// Strings in the objects rewritten below have to be enciphered again; the caller's map is the seed.
|
|
294
|
+
// Untested because unreachable today: we can only flatten our own R6 files, and our page dictionaries
|
|
295
|
+
// hold no strings. It bites the moment a producer inlines an annotation (/Contents, /URI).
|
|
296
|
+
const acroDict = doc.resolve(get(doc.catalog, "AcroForm"));
|
|
297
|
+
const cipher = await carryStrings(doc, [...[...byPage.keys()].map((n) => doc.getObject(n)), acroDict, doc.catalog], new Map(carriedIn ?? []));
|
|
288
298
|
for (const [pageNum, pageStamps] of byPage) {
|
|
289
299
|
const page = doc.getObject(pageNum);
|
|
290
300
|
if (page === undefined || !isDict(page))
|
|
@@ -353,10 +363,21 @@ export async function flattenInto(doc, writer, fields, cipher) {
|
|
|
353
363
|
const gone = new Set(fields.map((f) => f.objNum).filter((n) => n !== undefined));
|
|
354
364
|
// A flattened field may hang in a PARENT's /Kids rather than in the form's /Fields. Dropping it only
|
|
355
365
|
// from the root leaves it reachable, so `readAcroForm` still reports a field with no widget left.
|
|
356
|
-
|
|
366
|
+
// Planned first, written after: a surviving parent is rewritten whole, carrying its own /T, /TU
|
|
367
|
+
// and /DA, and enciphering those needs an await the sync walk cannot do.
|
|
368
|
+
const prunes = pruneKids(doc, get(acro, "Fields"), gone);
|
|
369
|
+
await carryStrings(doc, prunes.map((p) => p.node), cipher);
|
|
370
|
+
for (const p of prunes) {
|
|
371
|
+
writer.update(p.num, withEntries(p.node, { Kids: `[${p.kept.map((k) => serialize(k, cipher)).join(" ")}]` }, cipher));
|
|
372
|
+
}
|
|
357
373
|
const roots = doc.resolve(get(acro, "Fields"));
|
|
358
374
|
const kept = Array.isArray(roots) ? roots.filter((r) => !(isRef(r) && gone.has(r.num))) : [];
|
|
359
|
-
const newAcro = withEntries(acro, {
|
|
375
|
+
const newAcro = withEntries(acro, {
|
|
376
|
+
Fields: `[${kept.map((r) => serialize(r, cipher)).join(" ")}]`,
|
|
377
|
+
// A form with nothing left in it asking a viewer to regenerate appearances is a document
|
|
378
|
+
// contradicting itself, so the flag goes with the last field.
|
|
379
|
+
...(kept.length === 0 ? { NeedAppearances: undefined } : {}),
|
|
380
|
+
}, cipher);
|
|
360
381
|
if (isRef(acroRef)) {
|
|
361
382
|
writer.update(acroRef.num, newAcro);
|
|
362
383
|
}
|
|
@@ -377,12 +398,12 @@ export async function flattenInto(doc, writer, fields, cipher) {
|
|
|
377
398
|
* Walk the field tree and rewrite every `/Kids` that still points at a flattened field. A branch left
|
|
378
399
|
* with no children at all is added to `gone` so its own owner drops it too.
|
|
379
400
|
*/
|
|
380
|
-
function pruneKids(doc,
|
|
401
|
+
function pruneKids(doc, list, gone, out = [], depth = 0) {
|
|
381
402
|
if (depth > 32)
|
|
382
|
-
return;
|
|
403
|
+
return out;
|
|
383
404
|
const entries = doc.resolve(list);
|
|
384
405
|
if (!Array.isArray(entries))
|
|
385
|
-
return;
|
|
406
|
+
return out;
|
|
386
407
|
for (const ref of entries) {
|
|
387
408
|
if (!isRef(ref) || gone.has(ref.num))
|
|
388
409
|
continue;
|
|
@@ -392,7 +413,7 @@ function pruneKids(doc, writer, list, gone, cipher, depth = 0) {
|
|
|
392
413
|
const kids = get(node, "Kids");
|
|
393
414
|
if (kids === undefined)
|
|
394
415
|
continue;
|
|
395
|
-
pruneKids(doc,
|
|
416
|
+
pruneKids(doc, kids, gone, out, depth + 1);
|
|
396
417
|
const resolved = doc.resolve(kids);
|
|
397
418
|
if (!Array.isArray(resolved))
|
|
398
419
|
continue;
|
|
@@ -403,12 +424,16 @@ function pruneKids(doc, writer, list, gone, cipher, depth = 0) {
|
|
|
403
424
|
gone.add(ref.num); // an empty branch is a field with nothing left to show
|
|
404
425
|
continue;
|
|
405
426
|
}
|
|
406
|
-
|
|
427
|
+
out.push({ num: ref.num, node, kept });
|
|
407
428
|
}
|
|
429
|
+
return out;
|
|
408
430
|
}
|
|
409
431
|
/** Flatten an existing form: its values become page content and stop being editable. */
|
|
410
432
|
export async function flattenForm(bytes, options = {}) {
|
|
411
|
-
const doc = await PdfDocument.open(bytes, {
|
|
433
|
+
const doc = await PdfDocument.open(bytes, {
|
|
434
|
+
password: options.password,
|
|
435
|
+
maxStreamSize: options.maxStreamSize,
|
|
436
|
+
});
|
|
412
437
|
if (!doc.canReEncrypt) {
|
|
413
438
|
throw new FlattenError("this PDF is encrypted with RC4 or AES-128; jasy can open it but writes only AES-256, so " +
|
|
414
439
|
"flattening it would have to downgrade its protection - refused");
|
package/dist/edit/index.d.ts
CHANGED
|
@@ -15,4 +15,6 @@ export { flattenForm, FlattenError } from "./flatten.js";
|
|
|
15
15
|
export type { FlattenOptions, FlattenResult } from "./flatten.js";
|
|
16
16
|
export type { FieldValue, FillOptions, FillResult } from "./fill.js";
|
|
17
17
|
export { PdfDocument, PdfEncryptedError } from "./document.js";
|
|
18
|
+
export type { LoadOptions, OpenOptions } from "./document.js";
|
|
19
|
+
export { DEFAULT_MAX_STREAM_SIZE, PdfStreamTooLargeError } from "../utils/inflate.js";
|
|
18
20
|
export type { PdfDict, PdfName, PdfObject, PdfRef, PdfStream, PdfString } from "./objects.js";
|
package/dist/edit/index.js
CHANGED
|
@@ -12,5 +12,7 @@ export { readAcroForm } from "./acroform-reader.js";
|
|
|
12
12
|
export { fillForm, FillError } from "./fill.js";
|
|
13
13
|
export { flattenForm, FlattenError } from "./flatten.js";
|
|
14
14
|
// The document itself, for callers who want to look further than the form.
|
|
15
|
-
// PdfEncryptedError is what a caller catches to tell "wrong password" apart from a broken file
|
|
15
|
+
// PdfEncryptedError is what a caller catches to tell "wrong password" apart from a broken file;
|
|
16
|
+
// PdfStreamTooLargeError tells "this file is attacking me" apart from both.
|
|
16
17
|
export { PdfDocument, PdfEncryptedError } from "./document.js";
|
|
18
|
+
export { DEFAULT_MAX_STREAM_SIZE, PdfStreamTooLargeError } from "../utils/inflate.js";
|
package/dist/edit/objects.d.ts
CHANGED
|
@@ -61,3 +61,8 @@ export declare const numberOf: (o: PdfObject | undefined) => number | undefined;
|
|
|
61
61
|
* which agrees with Latin-1 for everything a field name realistically holds.
|
|
62
62
|
*/
|
|
63
63
|
export declare function textOf(o: PdfObject | undefined): string | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Every string inside an object, however deep. Both sides of the crypto seam use it: strings are
|
|
66
|
+
* deciphered on open and must be enciphered again before an object is written back.
|
|
67
|
+
*/
|
|
68
|
+
export declare function stringsIn(o: PdfObject | undefined, out?: PdfString[], depth?: number): PdfString[];
|
package/dist/edit/objects.js
CHANGED
|
@@ -54,3 +54,30 @@ export function textOf(o) {
|
|
|
54
54
|
// value round-trippable, which is what a reader that may write the string back needs.
|
|
55
55
|
return latin1FromBytes(b);
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Every string inside an object, however deep. Both sides of the crypto seam use it: strings are
|
|
59
|
+
* deciphered on open and must be enciphered again before an object is written back.
|
|
60
|
+
*/
|
|
61
|
+
export function stringsIn(o, out = [], depth = 0) {
|
|
62
|
+
if (o === undefined || o === null)
|
|
63
|
+
return out;
|
|
64
|
+
// Throws rather than returning a short list: a missed string is one written back in the clear.
|
|
65
|
+
// Bounded at all only because the walk does not follow references, just inline containers.
|
|
66
|
+
if (depth > MAX_NESTING) {
|
|
67
|
+
throw new Error(`@jasy/pdf: this PDF nests objects more than ${MAX_NESTING} deep, which no legitimate producer ` +
|
|
68
|
+
`does; refusing to walk it`);
|
|
69
|
+
}
|
|
70
|
+
if (isString(o))
|
|
71
|
+
out.push(o);
|
|
72
|
+
else if (Array.isArray(o))
|
|
73
|
+
for (const e of o)
|
|
74
|
+
stringsIn(e, out, depth + 1);
|
|
75
|
+
// The dictionary only - a stream's DATA is covered by the stream's own encryption.
|
|
76
|
+
else if (isStream(o))
|
|
77
|
+
stringsIn(o.dict, out, depth + 1);
|
|
78
|
+
else if (isDict(o))
|
|
79
|
+
for (const v of o.map.values())
|
|
80
|
+
stringsIn(v, out, depth + 1);
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
const MAX_NESTING = 64;
|
package/dist/edit/writer.d.ts
CHANGED
|
@@ -51,3 +51,10 @@ export declare class IncrementalWriter {
|
|
|
51
51
|
/** The offset the ORIGINAL file's `startxref` pointed at - our `/Prev`. */
|
|
52
52
|
private previousStartXref;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Encipher every string the given objects carry, into the cipher map `serialize` consults.
|
|
56
|
+
*
|
|
57
|
+
* Not optional on an encrypted document: `serialize` writes any string missing from the map as
|
|
58
|
+
* PLAINTEXT, so an object rewritten without this pass leaks whatever it holds.
|
|
59
|
+
*/
|
|
60
|
+
export declare function carryStrings(doc: PdfDocument, objects: Array<PdfObject | undefined>, into?: Map<PdfString, string>): Promise<StringCipher>;
|
package/dist/edit/writer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isDict, isRef, isStream, isString } from "./objects.js";
|
|
1
|
+
import { isDict, isRef, isStream, isString, stringsIn, } from "./objects.js";
|
|
2
2
|
/**
|
|
3
3
|
* Writing changes back into an existing PDF, as an **incremental update**.
|
|
4
4
|
*
|
|
@@ -259,3 +259,25 @@ export class IncrementalWriter {
|
|
|
259
259
|
return m ? Number(m[1]) : undefined;
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
|
+
/** Bytes as an enciphered PDF hex string, the form every rewritten string takes in an encrypted file. */
|
|
263
|
+
const hexString = (b) => `<${Array.from(b, (x) => x.toString(16).padStart(2, "0").toUpperCase()).join("")}>`;
|
|
264
|
+
/**
|
|
265
|
+
* Encipher every string the given objects carry, into the cipher map `serialize` consults.
|
|
266
|
+
*
|
|
267
|
+
* Not optional on an encrypted document: `serialize` writes any string missing from the map as
|
|
268
|
+
* PLAINTEXT, so an object rewritten without this pass leaks whatever it holds.
|
|
269
|
+
*/
|
|
270
|
+
export async function carryStrings(doc, objects, into = new Map()) {
|
|
271
|
+
if (!doc.isEncrypted)
|
|
272
|
+
return into;
|
|
273
|
+
for (const o of objects) {
|
|
274
|
+
for (const str of stringsIn(o)) {
|
|
275
|
+
if (into.has(str))
|
|
276
|
+
continue;
|
|
277
|
+
const cipher = await doc.encryptForWrite(str.bytes);
|
|
278
|
+
if (cipher)
|
|
279
|
+
into.set(str, hexString(cipher));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return into;
|
|
283
|
+
}
|
|
@@ -9,9 +9,26 @@ interface ContainerElementParams extends SizedElement, WithChildren {
|
|
|
9
9
|
main?: MainAlign;
|
|
10
10
|
/** Horizontal alignment of each child (cross axis); defaults to `stretch`. */
|
|
11
11
|
cross?: CrossAlign;
|
|
12
|
+
/** Lay the children out backwards along the main axis (CSS `row-reverse`). */
|
|
13
|
+
reverse?: boolean;
|
|
14
|
+
/** Let the children flow onto further lines when they do not fit (CSS `flex-wrap`). */
|
|
15
|
+
wrap?: boolean;
|
|
16
|
+
/** How the block of wrapped lines sits across the axis (CSS `align-content`). */
|
|
17
|
+
alignContent?: MainAlign;
|
|
12
18
|
/** Width/height as a fraction (0..1) of the offered box instead of `width`/`height` (relative sizing). */
|
|
13
19
|
widthFactor?: number;
|
|
14
20
|
heightFactor?: number;
|
|
21
|
+
/** Lower / upper bounds per axis, in points or as a fraction; see `SizingParams`. */
|
|
22
|
+
minWidth?: number;
|
|
23
|
+
maxWidth?: number;
|
|
24
|
+
minHeight?: number;
|
|
25
|
+
maxHeight?: number;
|
|
26
|
+
minWidthFactor?: number;
|
|
27
|
+
maxWidthFactor?: number;
|
|
28
|
+
minHeightFactor?: number;
|
|
29
|
+
maxHeightFactor?: number;
|
|
30
|
+
/** width / height; derives whichever axis is left open (CSS `aspect-ratio`). */
|
|
31
|
+
aspectRatio?: number;
|
|
15
32
|
/** Start this stack on a fresh page (CSS `break-before: page`). */
|
|
16
33
|
breakBefore?: boolean;
|
|
17
34
|
/** Start everything after this stack on a fresh page (CSS `break-after: page`). */
|
|
@@ -22,10 +39,13 @@ export declare class ContainerElement extends SizedPDFElement implements Fragmen
|
|
|
22
39
|
private gap;
|
|
23
40
|
private main;
|
|
24
41
|
private cross;
|
|
42
|
+
private reverse;
|
|
43
|
+
private wrap;
|
|
44
|
+
private alignContent?;
|
|
25
45
|
private breakBefore;
|
|
26
46
|
private breakAfter;
|
|
27
47
|
private requested;
|
|
28
|
-
constructor({ x, y, width, height, children, gap, main, cross,
|
|
48
|
+
constructor({ x, y, width, height, children, gap, main, cross, reverse, wrap, alignContent, breakBefore, breakAfter, ...sizing }: ContainerElementParams);
|
|
29
49
|
breaksBefore(): boolean;
|
|
30
50
|
breaksAfter(): boolean;
|
|
31
51
|
/**
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { FlexLayoutHelper, VERTICAL_AXIS } from "../utils/flex-layout.js";
|
|
2
|
-
import {
|
|
2
|
+
import { extentSpecs, resolveSize, } from "../layout/box-constraints.js";
|
|
3
3
|
import { childrenForceBreak, packChildren, } from "../layout/fragmentation.js";
|
|
4
4
|
import { FlexiblePDFElement, SizedPDFElement, } from "./pdf-element.js";
|
|
5
5
|
export class ContainerElement extends SizedPDFElement {
|
|
6
|
-
constructor({ x, y, width, height, children, gap, main, cross,
|
|
6
|
+
constructor({ x, y, width, height, children, gap, main, cross, reverse, wrap, alignContent, breakBefore, breakAfter, ...sizing }) {
|
|
7
7
|
super({ x, y, width, height });
|
|
8
8
|
this.children = children;
|
|
9
9
|
this.gap = gap ?? 0;
|
|
10
10
|
this.main = main ?? "start";
|
|
11
11
|
this.cross = cross ?? "stretch";
|
|
12
|
-
this.
|
|
12
|
+
this.reverse = reverse ?? false;
|
|
13
|
+
this.wrap = wrap ?? false;
|
|
14
|
+
this.alignContent = alignContent;
|
|
15
|
+
this.requested = { ...sizing, width, height };
|
|
13
16
|
this.breakBefore = breakBefore ?? false;
|
|
14
17
|
this.breakAfter = breakAfter ?? false;
|
|
15
18
|
}
|
|
@@ -62,6 +65,9 @@ export class ContainerElement extends SizedPDFElement {
|
|
|
62
65
|
gap: this.gap,
|
|
63
66
|
main: this.main,
|
|
64
67
|
cross: this.cross,
|
|
68
|
+
reverse: this.reverse,
|
|
69
|
+
wrap: this.wrap,
|
|
70
|
+
alignContent: this.alignContent,
|
|
65
71
|
breakAfter,
|
|
66
72
|
});
|
|
67
73
|
}
|
|
@@ -93,17 +99,19 @@ export class ContainerElement extends SizedPDFElement {
|
|
|
93
99
|
// Relative sizing: a pinned extent (fixed points or a fraction of the offered box, clamped into
|
|
94
100
|
// the constraints) wins; else fill a bounded axis; else stay `undefined` and shrink-wrap below.
|
|
95
101
|
// A Column nested in a Row gets unbounded width - passing 0 there would collapse children to 0.
|
|
96
|
-
const
|
|
97
|
-
const
|
|
102
|
+
const specs = extentSpecs(this.requested);
|
|
103
|
+
const resolved = resolveSize(specs.width, specs.height, this.requested.aspectRatio, constraints);
|
|
104
|
+
const { width: explicitWidth, height: explicitHeight, constraints: bounds } = resolved;
|
|
105
|
+
// Fill-or-shrink-wrap is decided by the constraints we were GIVEN; a min/max only caps the result.
|
|
98
106
|
const boundedWidth = explicitWidth !== undefined
|
|
99
|
-
?
|
|
107
|
+
? bounds.constrainWidth(explicitWidth)
|
|
100
108
|
: constraints.hasBoundedWidth
|
|
101
|
-
?
|
|
109
|
+
? bounds.maxWidth
|
|
102
110
|
: undefined;
|
|
103
111
|
const boundedHeight = explicitHeight !== undefined
|
|
104
|
-
?
|
|
112
|
+
? bounds.constrainHeight(explicitHeight)
|
|
105
113
|
: constraints.hasBoundedHeight
|
|
106
|
-
?
|
|
114
|
+
? bounds.maxHeight
|
|
107
115
|
: undefined;
|
|
108
116
|
// Vertical stack: cross = width (children fill it), main = height (the stacking extent).
|
|
109
117
|
const crossAvail = boundedWidth ?? Infinity;
|
|
@@ -113,7 +121,14 @@ export class ContainerElement extends SizedPDFElement {
|
|
|
113
121
|
// Vertical flex stack (main = height, cross = width). The shared helper measures
|
|
114
122
|
// fixed children, distributes the leftover to flex children, and places everything
|
|
115
123
|
// in source order.
|
|
116
|
-
result = FlexLayoutHelper.layout(this.children, VERTICAL_AXIS, mainAvail, crossAvail, this.y, this.x, {
|
|
124
|
+
result = FlexLayoutHelper.layout(this.children, VERTICAL_AXIS, mainAvail, crossAvail, this.y, this.x, {
|
|
125
|
+
gap: this.gap,
|
|
126
|
+
main: this.main,
|
|
127
|
+
cross: this.cross,
|
|
128
|
+
reverse: this.reverse,
|
|
129
|
+
wrap: this.wrap,
|
|
130
|
+
alignContent: this.alignContent,
|
|
131
|
+
}, ctx);
|
|
117
132
|
}
|
|
118
133
|
// Pinned/bounded axis takes that extent; an unbounded one shrink-wraps to the children (height =
|
|
119
134
|
// the stack, width = the widest child). Top-left coordinates; the container draws nothing, and
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Radii } from "../ir/display-list.js";
|
|
1
2
|
import { BoxConstraints, Offset, Size } from "../layout/box-constraints.js";
|
|
2
3
|
import { LayoutContext, SizedPDFElement } from "./pdf-element.js";
|
|
3
4
|
export declare enum BoxFit {
|
|
@@ -55,9 +56,20 @@ interface ImageElementParams {
|
|
|
55
56
|
widthFactor?: number;
|
|
56
57
|
/** Height as a fraction (0..1) of the offered height; see `widthFactor`. */
|
|
57
58
|
heightFactor?: number;
|
|
59
|
+
/** Lower / upper bounds per axis, in points or as a fraction; see `SizingParams`. */
|
|
60
|
+
minWidth?: number;
|
|
61
|
+
maxWidth?: number;
|
|
62
|
+
minHeight?: number;
|
|
63
|
+
maxHeight?: number;
|
|
64
|
+
minWidthFactor?: number;
|
|
65
|
+
maxWidthFactor?: number;
|
|
66
|
+
minHeightFactor?: number;
|
|
67
|
+
maxHeightFactor?: number;
|
|
68
|
+
/** width / height; overrides the image's OWN ratio (CSS `aspect-ratio`), `fit` then places it. */
|
|
69
|
+
aspectRatio?: number;
|
|
58
70
|
fit?: BoxFit;
|
|
59
|
-
/** Corner radius in points; rounds the image box (0 = sharp, default). */
|
|
60
|
-
radius?: number;
|
|
71
|
+
/** Corner radius in points - one number or per corner; rounds the image box (0 = sharp, default). */
|
|
72
|
+
radius?: number | Radii;
|
|
61
73
|
/** Alternate text for accessibility (tagged PDF). With `alt` the image is a Figure; without, decoration. */
|
|
62
74
|
alt?: string;
|
|
63
75
|
}
|
|
@@ -69,8 +81,10 @@ export declare class ImageElement extends SizedPDFElement {
|
|
|
69
81
|
private radius;
|
|
70
82
|
private readonly alt?;
|
|
71
83
|
private requested;
|
|
84
|
+
/** min/max/aspectRatio, kept as given so every layout pass re-resolves them. */
|
|
85
|
+
private sizing;
|
|
72
86
|
private intrinsic?;
|
|
73
|
-
constructor({ image, width, height, widthFactor, heightFactor, fit, radius, alt, }: ImageElementParams);
|
|
87
|
+
constructor({ image, width, height, widthFactor, heightFactor, fit, radius, alt, ...sizing }: ImageElementParams);
|
|
74
88
|
/**
|
|
75
89
|
* Loads the image and records its intrinsic pixel size. Runs in the async pre-layout pass (layout
|
|
76
90
|
* itself is synchronous and cannot await jimp) so `calculateLayout` can turn a width-only image
|
|
@@ -87,7 +101,7 @@ export declare class ImageElement extends SizedPDFElement {
|
|
|
87
101
|
height: number | undefined;
|
|
88
102
|
image: CustomImage;
|
|
89
103
|
fit: BoxFit;
|
|
90
|
-
radius: number;
|
|
104
|
+
radius: number | Radii;
|
|
91
105
|
alt: string | undefined;
|
|
92
106
|
};
|
|
93
107
|
}
|