@jasy/pdf 1.0.0-beta.2 → 1.0.0-beta.4
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/dist/api/content.d.ts +5 -5
- package/dist/api/content.js +12 -6
- package/dist/api/font-url.d.ts +1 -2
- package/dist/api/font-url.js +8 -74
- package/dist/api/forms.d.ts +3 -0
- package/dist/api/forms.js +3 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +1 -0
- package/dist/api/layout.d.ts +6 -0
- package/dist/api/layout.js +7 -2
- package/dist/api/page-builder.d.ts +4 -0
- package/dist/api/page-builder.js +4 -0
- package/dist/api/structure.d.ts +10 -2
- package/dist/api/structure.js +9 -6
- package/dist/api/table.d.ts +5 -3
- package/dist/api/table.js +99 -22
- package/dist/common/diagnostics.d.ts +53 -0
- package/dist/common/diagnostics.js +77 -0
- package/dist/edit/acroform-reader.js +7 -7
- package/dist/edit/appearance.js +3 -3
- package/dist/edit/attachments.d.ts +52 -0
- package/dist/edit/attachments.js +178 -0
- package/dist/edit/document.d.ts +38 -8
- package/dist/edit/document.js +208 -51
- package/dist/edit/fill.js +8 -23
- package/dist/edit/filters.d.ts +15 -0
- package/dist/edit/filters.js +157 -0
- package/dist/edit/index.d.ts +4 -1
- package/dist/edit/index.js +3 -1
- package/dist/edit/objects.d.ts +3 -4
- package/dist/edit/objects.js +5 -18
- package/dist/elements/canvas-element.d.ts +3 -0
- package/dist/elements/canvas-element.js +14 -3
- package/dist/elements/container-element.d.ts +5 -1
- package/dist/elements/container-element.js +35 -15
- package/dist/elements/forms/checkbox-element.d.ts +2 -0
- package/dist/elements/forms/checkbox-element.js +12 -3
- package/dist/elements/forms/choice-element.d.ts +6 -1
- package/dist/elements/forms/choice-element.js +17 -6
- package/dist/elements/forms/field-font.d.ts +8 -0
- package/dist/elements/forms/field-font.js +5 -0
- package/dist/elements/forms/push-button-element.d.ts +6 -1
- package/dist/elements/forms/push-button-element.js +16 -4
- package/dist/elements/forms/radio-element.d.ts +2 -0
- package/dist/elements/forms/radio-element.js +12 -3
- package/dist/elements/forms/signature-element.d.ts +6 -1
- package/dist/elements/forms/signature-element.js +16 -4
- package/dist/elements/forms/text-field-element.d.ts +6 -1
- package/dist/elements/forms/text-field-element.js +16 -4
- package/dist/elements/image-element.d.ts +39 -1
- package/dist/elements/image-element.js +95 -12
- package/dist/elements/layout/anchor-element.d.ts +8 -10
- package/dist/elements/layout/anchor-element.js +10 -18
- package/dist/elements/layout/bookmark-element.d.ts +9 -11
- package/dist/elements/layout/bookmark-element.js +11 -20
- package/dist/elements/layout/default-text-style-element.d.ts +11 -13
- package/dist/elements/layout/default-text-style-element.js +14 -31
- package/dist/elements/layout/deferred-element.d.ts +12 -4
- package/dist/elements/layout/deferred-element.js +24 -7
- package/dist/elements/layout/expanded-element.d.ts +7 -0
- package/dist/elements/layout/expanded-element.js +40 -18
- package/dist/elements/layout/keep-together-element.d.ts +8 -18
- package/dist/elements/layout/keep-together-element.js +21 -45
- package/dist/elements/layout/link-element.d.ts +8 -13
- package/dist/elements/layout/link-element.js +9 -19
- package/dist/elements/layout/padding-element.d.ts +15 -1
- package/dist/elements/layout/padding-element.js +55 -27
- package/dist/elements/layout/page-break-element.d.ts +4 -1
- package/dist/elements/layout/page-break-element.js +6 -1
- package/dist/elements/layout/page-builder-element.d.ts +5 -0
- package/dist/elements/layout/page-builder-element.js +11 -0
- package/dist/elements/layout/positioned-element.d.ts +14 -2
- package/dist/elements/layout/positioned-element.js +36 -10
- package/dist/elements/layout/proxy-element.d.ts +43 -0
- package/dist/elements/layout/proxy-element.js +94 -0
- package/dist/elements/layout/repeating-header-element.d.ts +6 -0
- package/dist/elements/layout/repeating-header-element.js +40 -13
- package/dist/elements/layout/rotated-box-element.d.ts +6 -0
- package/dist/elements/layout/rotated-box-element.js +27 -14
- package/dist/elements/layout/rotated-element.d.ts +7 -0
- package/dist/elements/layout/rotated-element.js +25 -2
- package/dist/elements/layout/struct-group.d.ts +9 -17
- package/dist/elements/layout/struct-group.js +10 -35
- package/dist/elements/line-element.d.ts +1 -0
- package/dist/elements/line-element.js +6 -0
- package/dist/elements/page-element.d.ts +10 -3
- package/dist/elements/page-element.js +39 -31
- package/dist/elements/pdf-document-element.d.ts +3 -0
- package/dist/elements/pdf-document-element.js +7 -0
- package/dist/elements/pdf-element.d.ts +75 -15
- package/dist/elements/pdf-element.js +68 -18
- package/dist/elements/rectangle-element.d.ts +14 -0
- package/dist/elements/rectangle-element.js +97 -52
- package/dist/elements/row-element.d.ts +11 -0
- package/dist/elements/row-element.js +58 -8
- package/dist/elements/svg-element.d.ts +30 -6
- package/dist/elements/svg-element.js +88 -17
- package/dist/elements/text-element.d.ts +40 -13
- package/dist/elements/text-element.js +144 -99
- package/dist/forms/acroform.d.ts +9 -1
- package/dist/forms/acroform.js +83 -34
- package/dist/forms/appearance.d.ts +17 -6
- package/dist/forms/appearance.js +18 -16
- package/dist/ir/display-list.d.ts +12 -2
- package/dist/layout/box-constraints.d.ts +5 -0
- package/dist/layout/box-constraints.js +14 -0
- package/dist/layout/collect-images.d.ts +6 -4
- package/dist/layout/collect-images.js +6 -4
- package/dist/layout/flex-item.d.ts +9 -0
- package/dist/layout/flex-item.js +39 -0
- package/dist/layout/fragmentation.d.ts +12 -4
- package/dist/layout/fragmentation.js +37 -14
- package/dist/layout/layout-cache.d.ts +53 -0
- package/dist/layout/layout-cache.js +107 -0
- package/dist/renderer/anchor-renderer.d.ts +2 -1
- package/dist/renderer/anchor-renderer.js +5 -4
- package/dist/renderer/bookmark-renderer.d.ts +2 -1
- package/dist/renderer/bookmark-renderer.js +5 -4
- package/dist/renderer/canvas-renderer.d.ts +2 -1
- package/dist/renderer/canvas-renderer.js +5 -2
- package/dist/renderer/container-renderer.d.ts +2 -1
- package/dist/renderer/container-renderer.js +6 -5
- package/dist/renderer/default-text-style-renderer.d.ts +2 -1
- package/dist/renderer/default-text-style-renderer.js +2 -3
- package/dist/renderer/deferred-renderer.d.ts +2 -1
- package/dist/renderer/deferred-renderer.js +2 -3
- package/dist/renderer/expanded-renderer.d.ts +2 -1
- package/dist/renderer/expanded-renderer.js +2 -3
- package/dist/renderer/form-field-renderer.d.ts +2 -1
- package/dist/renderer/form-field-renderer.js +16 -3
- package/dist/renderer/image-renderer.d.ts +2 -1
- package/dist/renderer/image-renderer.js +5 -2
- package/dist/renderer/keep-together-renderer.d.ts +2 -1
- package/dist/renderer/keep-together-renderer.js +2 -3
- package/dist/renderer/line-renderer.d.ts +2 -1
- package/dist/renderer/line-renderer.js +7 -2
- package/dist/renderer/link-renderer.d.ts +2 -1
- package/dist/renderer/link-renderer.js +36 -5
- package/dist/renderer/padding-renderer.d.ts +2 -1
- package/dist/renderer/padding-renderer.js +5 -5
- package/dist/renderer/page-break-renderer.d.ts +6 -5
- package/dist/renderer/page-break-renderer.js +14 -7
- package/dist/renderer/page-builder-renderer.d.ts +2 -1
- package/dist/renderer/page-builder-renderer.js +2 -3
- package/dist/renderer/page-renderer.js +28 -7
- package/dist/renderer/pdf-backend.d.ts +4 -3
- package/dist/renderer/pdf-backend.js +5 -5
- package/dist/renderer/pdf-document-renderer.d.ts +1 -8
- package/dist/renderer/pdf-document-renderer.js +15 -16
- package/dist/renderer/pdf-renderer.js +11 -11
- package/dist/renderer/positioned-renderer.d.ts +6 -1
- package/dist/renderer/positioned-renderer.js +6 -5
- package/dist/renderer/rectangle-renderer.d.ts +2 -1
- package/dist/renderer/rectangle-renderer.js +9 -4
- package/dist/renderer/repeating-header-renderer.d.ts +2 -1
- package/dist/renderer/repeating-header-renderer.js +6 -5
- package/dist/renderer/rotated-renderer.d.ts +2 -1
- package/dist/renderer/rotated-renderer.js +8 -5
- package/dist/renderer/row-renderer.d.ts +2 -1
- package/dist/renderer/row-renderer.js +6 -5
- package/dist/renderer/struct-group-renderer.d.ts +2 -1
- package/dist/renderer/struct-group-renderer.js +2 -3
- package/dist/renderer/svg-renderer.d.ts +2 -1
- package/dist/renderer/svg-renderer.js +8 -2
- package/dist/renderer/text-renderer.d.ts +4 -6
- package/dist/renderer/text-renderer.js +101 -111
- package/dist/svg/gradients.js +5 -0
- package/dist/svg/index.js +10 -1
- package/dist/svg/style.d.ts +2 -0
- package/dist/svg/style.js +11 -0
- package/dist/text/advance.d.ts +4 -0
- package/dist/text/advance.js +49 -4
- package/dist/text/bidi.d.ts +0 -2
- package/dist/text/bidi.js +0 -10
- package/dist/text/glyph-coverage.d.ts +1 -1
- package/dist/text/glyph-coverage.js +7 -3
- package/dist/text/hyphen-breaks.d.ts +17 -0
- package/dist/text/hyphen-breaks.js +54 -0
- package/dist/text/line-breaker.d.ts +34 -31
- package/dist/text/line-breaker.js +364 -176
- package/dist/text/line-metrics.d.ts +6 -3
- package/dist/text/line-metrics.js +7 -2
- package/dist/text/pdf-text-string.d.ts +21 -0
- package/dist/text/pdf-text-string.js +114 -0
- package/dist/text/text-decoration.d.ts +5 -5
- package/dist/text/text-decoration.js +5 -5
- package/dist/text/word-splitting.d.ts +15 -2
- package/dist/text/word-splitting.js +34 -8
- package/dist/utils/afm-parser.d.ts +3 -0
- package/dist/utils/afm-parser.js +19 -1
- package/dist/utils/dest-registry.d.ts +1 -1
- package/dist/utils/dest-registry.js +3 -2
- package/dist/utils/fetch-bytes.d.ts +19 -0
- package/dist/utils/fetch-bytes.js +79 -0
- package/dist/utils/flex-layout.d.ts +13 -13
- package/dist/utils/flex-layout.js +318 -216
- package/dist/utils/font-metrics.d.ts +4 -3
- package/dist/utils/image-helper.js +2 -1
- package/dist/utils/pdf-object-manager.d.ts +11 -3
- package/dist/utils/pdf-object-manager.js +41 -15
- package/dist/utils/renderer-registry.d.ts +43 -2
- package/dist/utils/renderer-registry.js +40 -0
- package/dist/utils/struct-tree.d.ts +24 -0
- package/dist/utils/struct-tree.js +61 -9
- package/dist/utils/ttf-parser.d.ts +4 -0
- package/dist/utils/ttf-parser.js +27 -4
- package/dist/validators/element-validator.js +2 -1
- package/package.json +1 -1
- package/dist/elements/layout/sized-container-element.d.ts +0 -14
- package/dist/elements/layout/sized-container-element.js +0 -32
package/dist/edit/document.js
CHANGED
|
@@ -3,12 +3,15 @@ import { DEFAULT_MAX_STREAM_SIZE, inflateBounded, PdfStreamTooLargeError, } from
|
|
|
3
3
|
import { get, isDict, isRef, isStream, isString, nameOf, numberOf, stringsIn, } from "./objects.js";
|
|
4
4
|
import { StandardAes256 } from "../crypto/security-handler.js";
|
|
5
5
|
import { StandardLegacy } from "../crypto/legacy-handler.js";
|
|
6
|
+
import { ascii85Decode, asciiHexDecode, lzwDecode, PdfFilterError, runLengthDecode, } from "./filters.js";
|
|
6
7
|
const EMPTY = new Uint8Array(0);
|
|
7
8
|
/** Thrown when a file cannot be opened because of its encryption - always saying which of the reasons it
|
|
8
9
|
* is: no password, wrong password, or a revision we do not implement. */
|
|
9
10
|
export class PdfEncryptedError extends Error {
|
|
10
|
-
|
|
11
|
+
/** `needsPassword`: the password is missing or wrong - as opposed to a file we cannot decrypt at all. */
|
|
12
|
+
constructor(message, needsPassword = false) {
|
|
11
13
|
super(`@jasy/pdf: ${message}`);
|
|
14
|
+
this.needsPassword = needsPassword;
|
|
12
15
|
}
|
|
13
16
|
}
|
|
14
17
|
/** The raw bytes of a `/Encrypt` entry that must be a string (`/U`, `/UE`). */
|
|
@@ -20,6 +23,8 @@ export class PdfDocument {
|
|
|
20
23
|
this.cache = new Map();
|
|
21
24
|
/** Object streams already unpacked, by their object number. */
|
|
22
25
|
this.objStmCache = new Map();
|
|
26
|
+
/** Attached files encrypted on their own (`/EFF`) and opened without the password: their streams. */
|
|
27
|
+
this.lockedFiles = new Set();
|
|
23
28
|
/** Decrypted stream payloads, keyed by the stream's data offset (unique per stream). */
|
|
24
29
|
this.plainStreams = new Map();
|
|
25
30
|
/** True when the cross-reference index was unusable and had to be rebuilt by scanning. Surfaced so
|
|
@@ -61,10 +66,37 @@ export class PdfDocument {
|
|
|
61
66
|
const doc = PdfDocument.load(bytes, opts);
|
|
62
67
|
if (!doc.isEncrypted)
|
|
63
68
|
return doc;
|
|
64
|
-
doc.
|
|
69
|
+
doc.enciphered = doc.encipheredParts();
|
|
70
|
+
try {
|
|
71
|
+
doc.security = await doc.securityHandlerFor(opts.password);
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
// Only the attached files are encrypted (Acrobat's "encrypt only file attachments"): without a
|
|
75
|
+
// password the document opens as it is, and an attachment asks for it when it is read. A WRONG
|
|
76
|
+
// password is still reported as one.
|
|
77
|
+
const filesOnly = !doc.enciphered.streams && !doc.enciphered.strings;
|
|
78
|
+
const noPassword = opts.password === undefined;
|
|
79
|
+
if (!(filesOnly && noPassword && e instanceof PdfEncryptedError && e.needsPassword))
|
|
80
|
+
throw e;
|
|
81
|
+
}
|
|
65
82
|
await doc.decryptEverything();
|
|
66
83
|
return doc;
|
|
67
84
|
}
|
|
85
|
+
/** Which parts the `/Encrypt` dictionary enciphers. Before V4 there are no crypt filters: everything. */
|
|
86
|
+
encipheredParts() {
|
|
87
|
+
const enc = this.resolve(this.trailer.map.get("Encrypt"));
|
|
88
|
+
if ((numberOf(this.lookup(enc, "V")) ?? 0) < 4)
|
|
89
|
+
return { streams: true, strings: true, files: true };
|
|
90
|
+
const real = (name) => name !== undefined &&
|
|
91
|
+
name !== "Identity" &&
|
|
92
|
+
nameOf(this.lookup(this.lookup(this.lookup(enc, "CF"), name), "CFM")) !== "None";
|
|
93
|
+
const stmf = nameOf(this.lookup(enc, "StmF")) ?? "Identity";
|
|
94
|
+
return {
|
|
95
|
+
streams: real(stmf),
|
|
96
|
+
strings: real(nameOf(this.lookup(enc, "StrF")) ?? "Identity"),
|
|
97
|
+
files: real(nameOf(this.lookup(enc, "EFF")) ?? stmf),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
68
100
|
/**
|
|
69
101
|
* Build the handler for this file's `/Encrypt` dictionary, or say precisely why we cannot.
|
|
70
102
|
*
|
|
@@ -85,8 +117,8 @@ export class PdfDocument {
|
|
|
85
117
|
const pw = password ?? "";
|
|
86
118
|
const rejected = () => {
|
|
87
119
|
throw password === undefined
|
|
88
|
-
? new PdfEncryptedError("this PDF is encrypted; pass its password to open it")
|
|
89
|
-
: new PdfEncryptedError("wrong password for this PDF");
|
|
120
|
+
? new PdfEncryptedError("this PDF is encrypted; pass its password to open it", true)
|
|
121
|
+
: new PdfEncryptedError("wrong password for this PDF", true);
|
|
90
122
|
};
|
|
91
123
|
// `recoverFileKey` / `StandardLegacy.open` say "wrong password" by name; anything else that comes out
|
|
92
124
|
// of them is structural (a malformed /U, /UE, /O) and must not be reported as a bad password.
|
|
@@ -118,10 +150,7 @@ export class PdfDocument {
|
|
|
118
150
|
const u = stringBytes(this.lookup(enc, "U"));
|
|
119
151
|
if (o === undefined || u === undefined)
|
|
120
152
|
incomplete("incomplete");
|
|
121
|
-
const cipher = this.
|
|
122
|
-
if (cipher.kind === "identity") {
|
|
123
|
-
throw new PdfEncryptedError("this PDF declares encryption but selects the Identity crypt filter, which jasy does not model");
|
|
124
|
-
}
|
|
153
|
+
const cipher = this.legacyCipher(enc, v);
|
|
125
154
|
const ids = this.trailer.map.get("ID");
|
|
126
155
|
const firstId = Array.isArray(ids) ? stringBytes(this.resolve(ids[0])) : undefined;
|
|
127
156
|
try {
|
|
@@ -145,28 +174,42 @@ export class PdfDocument {
|
|
|
145
174
|
throw new PdfEncryptedError(`this PDF uses standard security V${v}/R${r}, which jasy does not implement`);
|
|
146
175
|
}
|
|
147
176
|
/**
|
|
148
|
-
* Which cipher and key length
|
|
177
|
+
* Which cipher and key length a legacy (V2-V4) file uses.
|
|
149
178
|
*
|
|
150
|
-
* For V4 the answer is NOT the top-level `/Length`: the document names
|
|
151
|
-
*
|
|
152
|
-
* `/
|
|
179
|
+
* For V4 the answer is NOT the top-level `/Length`: the document names crypt filters - `/StmF` for
|
|
180
|
+
* streams, `/StrF` for strings, `/EFF` for attached files - and each carries its own `/CFM` and
|
|
181
|
+
* `/Length` (in BYTES, where the top-level one is in bits). `/Identity` means "not enciphered". One
|
|
182
|
+
* handler serves them all, so the filters that DO encipher must agree; two different ciphers in one
|
|
183
|
+
* file are refused by name.
|
|
153
184
|
*/
|
|
154
|
-
|
|
185
|
+
legacyCipher(enc, v) {
|
|
155
186
|
const topBits = numberOf(this.lookup(enc, "Length")) ?? 40;
|
|
156
187
|
if (v < 4)
|
|
157
188
|
return { kind: "rc4", keyBits: topBits };
|
|
158
189
|
const stmf = nameOf(this.lookup(enc, "StmF")) ?? "Identity";
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
190
|
+
const used = [
|
|
191
|
+
stmf,
|
|
192
|
+
nameOf(this.lookup(enc, "StrF")) ?? "Identity",
|
|
193
|
+
nameOf(this.lookup(enc, "EFF")) ?? stmf,
|
|
194
|
+
]
|
|
195
|
+
.filter((name) => name !== "Identity")
|
|
196
|
+
.map((name) => {
|
|
197
|
+
const cf = this.lookup(this.lookup(enc, "CF"), name);
|
|
198
|
+
const cfm = nameOf(this.lookup(cf, "CFM"));
|
|
199
|
+
const cfBytes = numberOf(this.lookup(cf, "Length"));
|
|
200
|
+
return cfm === undefined || cfm === "None"
|
|
201
|
+
? undefined
|
|
202
|
+
: {
|
|
203
|
+
kind: cfm === "AESV2" ? "aes" : "rc4",
|
|
204
|
+
keyBits: cfBytes !== undefined ? cfBytes * 8 : topBits,
|
|
205
|
+
};
|
|
206
|
+
})
|
|
207
|
+
.filter((c) => c !== undefined);
|
|
208
|
+
const first = used[0] ?? { kind: "rc4", keyBits: topBits };
|
|
209
|
+
if (used.some((c) => c.kind !== first.kind || c.keyBits !== first.keyBits)) {
|
|
210
|
+
throw new PdfEncryptedError("this PDF enciphers its streams, strings and attached files with different crypt filters, which jasy does not model");
|
|
211
|
+
}
|
|
212
|
+
return first;
|
|
170
213
|
}
|
|
171
214
|
/**
|
|
172
215
|
* Decrypt every string and stream, once, in place.
|
|
@@ -178,8 +221,7 @@ export class PdfDocument {
|
|
|
178
221
|
*/
|
|
179
222
|
async decryptEverything() {
|
|
180
223
|
const security = this.security;
|
|
181
|
-
|
|
182
|
-
return;
|
|
224
|
+
const enciphered = this.enciphered ?? { streams: true, strings: true, files: true };
|
|
183
225
|
const encryptNum = isRef(this.trailer.map.get("Encrypt"))
|
|
184
226
|
? this.trailer.map.get("Encrypt").num
|
|
185
227
|
: undefined;
|
|
@@ -199,6 +241,10 @@ export class PdfDocument {
|
|
|
199
241
|
continue; // never encrypted - it holds the index itself
|
|
200
242
|
if (type === "Metadata" && metadataInClear)
|
|
201
243
|
continue;
|
|
244
|
+
if (type === "EmbeddedFile")
|
|
245
|
+
continue; // attached files follow /EFF, below
|
|
246
|
+
if (!security || !enciphered.streams || this.identityCrypt(obj))
|
|
247
|
+
continue;
|
|
202
248
|
// Keyed by the stream's DATA OFFSET, not its object number: `streamData` receives the stream
|
|
203
249
|
// object and has no number in hand.
|
|
204
250
|
this.plainStreams.set(obj.start, await security.decrypt(this.rawStream(obj), { num, gen: entry.gen }));
|
|
@@ -209,7 +255,26 @@ export class PdfDocument {
|
|
|
209
255
|
// itself stays - it was never encrypted.
|
|
210
256
|
this.cache.clear();
|
|
211
257
|
this.objStmCache.clear();
|
|
258
|
+
// Attached files follow their own crypt filter (/EFF). They are found by what points at them - an
|
|
259
|
+
// /EF entry - because a file stream need not say /Type /EmbeddedFile.
|
|
260
|
+
for (const num of this.embeddedFileStreams()) {
|
|
261
|
+
const entry = this.xref.get(num);
|
|
262
|
+
const obj = this.getObject(num);
|
|
263
|
+
if (entry?.kind !== "offset" || obj === undefined || !isStream(obj))
|
|
264
|
+
continue;
|
|
265
|
+
this.plainStreams.delete(obj.start); // a file stream without /Type was taken for a plain stream
|
|
266
|
+
if (!enciphered.files || this.identityCrypt(obj))
|
|
267
|
+
continue;
|
|
268
|
+
if (!security) {
|
|
269
|
+
this.lockedFiles.add(obj.start);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
this.plainStreams.set(obj.start, await security.decrypt(this.rawStream(obj), { num, gen: entry.gen }));
|
|
273
|
+
}
|
|
274
|
+
this.cache.clear();
|
|
212
275
|
// Then the strings, in objects that sit directly in the file.
|
|
276
|
+
if (!security || !enciphered.strings)
|
|
277
|
+
return;
|
|
213
278
|
for (const [num, entry] of this.xref) {
|
|
214
279
|
if (entry.kind !== "offset" || num === encryptNum)
|
|
215
280
|
continue;
|
|
@@ -221,14 +286,40 @@ export class PdfDocument {
|
|
|
221
286
|
s.bytes = await security.decrypt(s.bytes, { num, gen: entry.gen });
|
|
222
287
|
}
|
|
223
288
|
catch {
|
|
224
|
-
// The password already validated against /U, so
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
289
|
+
// Only AES can fail here (RC4 has no padding). The password already validated against /U, so
|
|
290
|
+
// the key is right: a string that does not decipher was left in the clear by its producer -
|
|
291
|
+
// PDFKit does so with name-tree keys, of any length - and is read as it stands (and enciphered
|
|
292
|
+
// when written back).
|
|
228
293
|
}
|
|
229
294
|
}
|
|
230
295
|
}
|
|
231
296
|
}
|
|
297
|
+
/** A stream that names the `/Identity` crypt filter in its own `/Filter` is not enciphered. */
|
|
298
|
+
identityCrypt(stream) {
|
|
299
|
+
const filter = this.resolve(get(stream, "Filter"));
|
|
300
|
+
const filters = Array.isArray(filter) ? filter : [filter];
|
|
301
|
+
const i = filters.findIndex((f) => nameOf(this.resolve(f)) === "Crypt");
|
|
302
|
+
if (i < 0)
|
|
303
|
+
return false;
|
|
304
|
+
const parms = this.resolve(get(stream, "DecodeParms"));
|
|
305
|
+
const own = Array.isArray(parms) ? this.resolve(parms[i]) : parms;
|
|
306
|
+
return (nameOf(this.lookup(own, "Name")) ?? "Identity") === "Identity";
|
|
307
|
+
}
|
|
308
|
+
/** The object numbers of every attached file's stream: whatever an /EF dictionary points at. */
|
|
309
|
+
embeddedFileStreams() {
|
|
310
|
+
const out = new Set();
|
|
311
|
+
for (const num of this.xref.keys()) {
|
|
312
|
+
const obj = this.getObject(num);
|
|
313
|
+
if (obj !== undefined && isStream(obj) && nameOf(get(obj, "Type")) === "EmbeddedFile")
|
|
314
|
+
out.add(num);
|
|
315
|
+
const ef = isDict(obj) ? this.resolve(get(obj, "EF")) : undefined;
|
|
316
|
+
if (ef !== undefined && isDict(ef))
|
|
317
|
+
for (const ref of ef.map.values())
|
|
318
|
+
if (isRef(ref))
|
|
319
|
+
out.add(ref.num);
|
|
320
|
+
}
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
232
323
|
// -------------------------------------------------------------------------------------------
|
|
233
324
|
// Cross-reference index
|
|
234
325
|
// -------------------------------------------------------------------------------------------
|
|
@@ -504,7 +595,11 @@ export class PdfDocument {
|
|
|
504
595
|
/** True once the password has been accepted and the contents decrypted, so what is read is plaintext.
|
|
505
596
|
* Always true for a document that was never encrypted. */
|
|
506
597
|
get isReadable() {
|
|
507
|
-
return !this.isEncrypted || this.security !== undefined;
|
|
598
|
+
return !this.isEncrypted || this.security !== undefined || !this.encryptsDocument;
|
|
599
|
+
}
|
|
600
|
+
/** Whether streams or strings are enciphered - not the case when only attached files are. */
|
|
601
|
+
get encryptsDocument() {
|
|
602
|
+
return this.enciphered === undefined || this.enciphered.streams || this.enciphered.strings;
|
|
508
603
|
}
|
|
509
604
|
/**
|
|
510
605
|
* Encipher a value that is about to be written back INTO this document, or `undefined` when the
|
|
@@ -525,7 +620,13 @@ export class PdfDocument {
|
|
|
525
620
|
* a string happening to be present in whatever we rewrite, which is a guarantee by accident.
|
|
526
621
|
*/
|
|
527
622
|
get canReEncrypt() {
|
|
528
|
-
|
|
623
|
+
if (!this.isEncrypted)
|
|
624
|
+
return true;
|
|
625
|
+
// A part left as /Identity must stay in the clear when written back - one handler cannot do both.
|
|
626
|
+
const e = this.enciphered;
|
|
627
|
+
if (e && !(e.streams && e.strings && e.files))
|
|
628
|
+
return false;
|
|
629
|
+
return this.security?.canEncrypt ?? false;
|
|
529
630
|
}
|
|
530
631
|
/** The generation of an object as the file records it. Members of an object stream are always 0. */
|
|
531
632
|
generationOf(num) {
|
|
@@ -550,8 +651,17 @@ export class PdfDocument {
|
|
|
550
651
|
const end = text.indexOf("endstream", stream.start);
|
|
551
652
|
return this.bytes.subarray(stream.start, end < 0 ? this.bytes.length : end);
|
|
552
653
|
}
|
|
553
|
-
/**
|
|
554
|
-
|
|
654
|
+
/**
|
|
655
|
+
* A stream's DECODED bytes: deciphered if the document is encrypted, then run through its filters.
|
|
656
|
+
*
|
|
657
|
+
* `strict` is for data handed to a caller as it is (an attachment): every filter must decode, or this
|
|
658
|
+
* throws a `PdfFilterError` - never bytes still encoded or half decoded. The default suits the
|
|
659
|
+
* structural reads, where a filter that fails is skipped and the rest is read as it stands.
|
|
660
|
+
*/
|
|
661
|
+
streamData(stream, strict = false) {
|
|
662
|
+
if (this.lockedFiles.has(stream.start)) {
|
|
663
|
+
throw new PdfEncryptedError("this attached file is encrypted on its own; open the PDF with its password to read it", true);
|
|
664
|
+
}
|
|
555
665
|
// A decrypted payload replaces the bytes from the file; filters then run on the plaintext, which is
|
|
556
666
|
// the order the format prescribes - encryption wraps the already-filtered data.
|
|
557
667
|
const raw = this.plainStreams.get(stream.start) ?? this.rawStream(stream);
|
|
@@ -563,36 +673,68 @@ export class PdfDocument {
|
|
|
563
673
|
const parms = Array.isArray(parmsRaw) ? parmsRaw : [parmsRaw];
|
|
564
674
|
let data = raw;
|
|
565
675
|
filters.forEach((filter, i) => {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
676
|
+
try {
|
|
677
|
+
data = this.decodeFilter(filter, data, this.resolve(parms[i]), strict);
|
|
678
|
+
}
|
|
679
|
+
catch (e) {
|
|
680
|
+
// A stream that blew the ceiling is an attack, not a stream we failed to decode - swallowing
|
|
681
|
+
// it here would hand back silent garbage.
|
|
682
|
+
if (e instanceof PdfStreamTooLargeError)
|
|
683
|
+
throw e;
|
|
684
|
+
if (strict) {
|
|
685
|
+
throw e instanceof PdfFilterError
|
|
686
|
+
? e
|
|
687
|
+
: new PdfFilterError(`${filter}: ${String(e?.message ?? e)}`);
|
|
577
688
|
}
|
|
578
|
-
|
|
689
|
+
// not our data to repair; the rest is read as it stands
|
|
579
690
|
}
|
|
580
|
-
// Other filters (LZW, DCT, …) are left encoded on purpose: this reader only needs the structural
|
|
581
|
-
// streams - the index, object streams and appearances - and every one of those is Flate.
|
|
582
691
|
});
|
|
583
692
|
return data;
|
|
584
693
|
}
|
|
694
|
+
/**
|
|
695
|
+
* One filter. Each pass is bounded on what it produces, because /Filter is a list and
|
|
696
|
+
* [/FlateDecode /FlateDecode] is a legal nested bomb. The image codecs (DCT, JPX, CCITT, JBIG2) decode
|
|
697
|
+
* to pixels, not to a file: strict refuses them by name, the default leaves them encoded.
|
|
698
|
+
*/
|
|
699
|
+
decodeFilter(filter, data, parms, strict) {
|
|
700
|
+
switch (filter) {
|
|
701
|
+
case "FlateDecode":
|
|
702
|
+
case "Fl":
|
|
703
|
+
return this.undoPredictor(inflateBounded(data, this.maxStreamSize), parms, strict);
|
|
704
|
+
case "LZWDecode":
|
|
705
|
+
case "LZW":
|
|
706
|
+
return this.undoPredictor(lzwDecode(data, this.maxStreamSize, numberOf(get(parms, "EarlyChange")) ?? 1), parms, strict);
|
|
707
|
+
case "ASCIIHexDecode":
|
|
708
|
+
case "AHx":
|
|
709
|
+
return asciiHexDecode(data);
|
|
710
|
+
case "ASCII85Decode":
|
|
711
|
+
case "A85":
|
|
712
|
+
return ascii85Decode(data, this.maxStreamSize);
|
|
713
|
+
case "RunLengthDecode":
|
|
714
|
+
case "RL":
|
|
715
|
+
return runLengthDecode(data, this.maxStreamSize);
|
|
716
|
+
case "Crypt":
|
|
717
|
+
return data; // deciphering happens once, on open
|
|
718
|
+
default:
|
|
719
|
+
if (strict) {
|
|
720
|
+
throw new PdfFilterError(`/${filter} decodes to image pixels, not to a file - jasy returns no data it cannot decode`);
|
|
721
|
+
}
|
|
722
|
+
return data;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
585
725
|
/**
|
|
586
726
|
* Undo a PNG predictor (`/Predictor` 10-15), which xref streams almost always use - the IRS form in
|
|
587
727
|
* our fixtures does. Each row is prefixed with its filter type and is decoded against the row above.
|
|
588
728
|
*/
|
|
589
|
-
undoPredictor(data, parms) {
|
|
729
|
+
undoPredictor(data, parms, strict = false) {
|
|
590
730
|
const predictor = numberOf(get(parms, "Predictor")) ?? 1;
|
|
591
|
-
if (predictor < 10)
|
|
592
|
-
return data; // 1 = none; 2 (TIFF) does not occur in the streams we read
|
|
593
731
|
const colors = numberOf(get(parms, "Colors")) ?? 1;
|
|
594
732
|
const bpc = numberOf(get(parms, "BitsPerComponent")) ?? 8;
|
|
595
733
|
const columns = numberOf(get(parms, "Columns")) ?? 1;
|
|
734
|
+
if (predictor === 2)
|
|
735
|
+
return this.undoTiffPredictor(data, colors, bpc, columns, strict);
|
|
736
|
+
if (predictor < 10)
|
|
737
|
+
return data; // 1 = none
|
|
596
738
|
const bpp = Math.max(1, Math.ceil((colors * bpc) / 8));
|
|
597
739
|
const rowLen = Math.ceil((colors * bpc * columns) / 8);
|
|
598
740
|
const rows = Math.floor(data.length / (rowLen + 1));
|
|
@@ -634,6 +776,21 @@ export class PdfDocument {
|
|
|
634
776
|
}
|
|
635
777
|
return out;
|
|
636
778
|
}
|
|
779
|
+
/** TIFF predictor 2: each component is the difference from the one to its left, per row. */
|
|
780
|
+
undoTiffPredictor(data, colors, bpc, columns, strict) {
|
|
781
|
+
if (bpc !== 8) {
|
|
782
|
+
if (strict)
|
|
783
|
+
throw new PdfFilterError(`TIFF predictor with ${bpc} bits per component`);
|
|
784
|
+
return data;
|
|
785
|
+
}
|
|
786
|
+
const out = Uint8Array.from(data);
|
|
787
|
+
const rowLen = colors * columns;
|
|
788
|
+
for (let row = 0; row + rowLen <= out.length; row += rowLen) {
|
|
789
|
+
for (let i = colors; i < rowLen; i++)
|
|
790
|
+
out[row + i] = (out[row + i] + out[row + i - colors]) & 0xff;
|
|
791
|
+
}
|
|
792
|
+
return out;
|
|
793
|
+
}
|
|
637
794
|
/** Every object number the index knows, ascending - for walking a whole document. */
|
|
638
795
|
objectNumbers() {
|
|
639
796
|
return [...this.xref.keys()].sort((a, b) => a - b);
|
package/dist/edit/fill.js
CHANGED
|
@@ -5,6 +5,7 @@ import { bakeAppearance, bakeButtonStates, fontMetrics, readLook } from "./appea
|
|
|
5
5
|
import { flattenInto } from "./flatten.js";
|
|
6
6
|
import { getArrayBuffer } from "../utils/utf8-to-windows1252-encoder.js";
|
|
7
7
|
import { get, isDict, isRef } from "./objects.js";
|
|
8
|
+
import { encodePdfText } from "../text/pdf-text-string.js";
|
|
8
9
|
/** Field flags we have to honour when filling (see `forms/acroform.ts` for the writer's side). */
|
|
9
10
|
const FF_READ_ONLY = 1 << 0;
|
|
10
11
|
const FF_MULTI_SELECT = 1 << 21;
|
|
@@ -12,23 +13,11 @@ const FF_EDIT = 1 << 18;
|
|
|
12
13
|
const FF_PUSHBUTTON = 1 << 16;
|
|
13
14
|
const FF_RADIO = 1 << 15;
|
|
14
15
|
/**
|
|
15
|
-
* A text value in PDF form. Plain ASCII becomes a `(literal)`; anything else becomes
|
|
16
|
-
* string
|
|
17
|
-
*
|
|
18
|
-
* back as mojibake, because a literal is PDFDocEncoded.
|
|
16
|
+
* A text value in PDF form. Plain ASCII becomes a `(literal)`; anything else becomes `<hex>` of the ONE
|
|
17
|
+
* text-string encoding the writer uses (`encodePdfText`: PDFDocEncoding, else UTF-16BE with its BOM).
|
|
18
|
+
* Writing UTF-8 into a literal (the obvious mistake) reads back as mojibake.
|
|
19
19
|
*/
|
|
20
|
-
const
|
|
21
|
-
if (/^[\x20-\x7e]*$/.test(s))
|
|
22
|
-
return Uint8Array.from(s, (c) => c.charCodeAt(0));
|
|
23
|
-
const out = new Uint8Array(2 + s.length * 2);
|
|
24
|
-
out[0] = 0xfe;
|
|
25
|
-
out[1] = 0xff;
|
|
26
|
-
for (let i = 0; i < s.length; i++) {
|
|
27
|
-
out[2 + i * 2] = s.charCodeAt(i) >> 8;
|
|
28
|
-
out[3 + i * 2] = s.charCodeAt(i) & 0xff;
|
|
29
|
-
}
|
|
30
|
-
return out;
|
|
31
|
-
};
|
|
20
|
+
const isAscii = (s) => /^[\x20-\x7e]*$/.test(s);
|
|
32
21
|
const hex = (b) => `<${Array.from(b, (x) => x.toString(16).padStart(2, "0").toUpperCase()).join("")}>`;
|
|
33
22
|
/** A value in PDF form. `enciphered` holds ciphertext for the strings of an encrypted document; without
|
|
34
23
|
* it the value is written in the clear, exactly as before. */
|
|
@@ -36,14 +25,10 @@ const pdfText = (s, enciphered) => {
|
|
|
36
25
|
const ready = enciphered?.get(s);
|
|
37
26
|
if (ready !== undefined)
|
|
38
27
|
return ready;
|
|
39
|
-
|
|
40
|
-
if (ascii) {
|
|
28
|
+
if (isAscii(s)) {
|
|
41
29
|
return `(${s.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)")})`;
|
|
42
30
|
}
|
|
43
|
-
|
|
44
|
-
for (let i = 0; i < s.length; i++)
|
|
45
|
-
out += s.charCodeAt(i).toString(16).padStart(4, "0").toUpperCase();
|
|
46
|
-
return `<${out}>`;
|
|
31
|
+
return hex(encodePdfText(s));
|
|
47
32
|
};
|
|
48
33
|
const escName = (s) => Array.from(new TextEncoder().encode(s))
|
|
49
34
|
.map((b) => b >= 0x21 && b <= 0x7e && !"()<>[]{}/%#".includes(String.fromCharCode(b))
|
|
@@ -287,7 +272,7 @@ export async function fillForm(bytes, values, options = {}) {
|
|
|
287
272
|
for (const text of typeof v === "string" ? [v] : Array.isArray(v) ? v : []) {
|
|
288
273
|
if (enciphered.has(text))
|
|
289
274
|
continue;
|
|
290
|
-
const cipher = await doc.encryptForWrite(
|
|
275
|
+
const cipher = await doc.encryptForWrite(encodePdfText(text));
|
|
291
276
|
if (cipher)
|
|
292
277
|
enciphered.set(text, hex(cipher));
|
|
293
278
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Thrown when a stream's data is not valid for the filter it declares. */
|
|
2
|
+
export declare class PdfFilterError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
/** `/ASCIIHexDecode`: hex digits, whitespace ignored, `>` ends it; an odd last digit is followed by 0. */
|
|
6
|
+
export declare function asciiHexDecode(data: Uint8Array): Uint8Array;
|
|
7
|
+
/** `/ASCII85Decode`: groups of five in `!`..`u`, `z` for four zero bytes, `~>` ends it. */
|
|
8
|
+
export declare function ascii85Decode(data: Uint8Array, max: number): Uint8Array;
|
|
9
|
+
/** `/RunLengthDecode`: a length byte, then that many literals (0-127) or one byte repeated (129-255). */
|
|
10
|
+
export declare function runLengthDecode(data: Uint8Array, max: number): Uint8Array;
|
|
11
|
+
/**
|
|
12
|
+
* `/LZWDecode`: 9 to 12 bit codes, 256 clears the table, 257 ends. `earlyChange` 1 (the default) widens
|
|
13
|
+
* the code one entry early, as PDF's LZW does; 0 is the textbook variant.
|
|
14
|
+
*/
|
|
15
|
+
export declare function lzwDecode(data: Uint8Array, max: number, earlyChange?: number): Uint8Array;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { PdfStreamTooLargeError } from "../utils/inflate.js";
|
|
2
|
+
// The standard stream filters besides Flate (which `utils/inflate.ts` decodes). Each one is bounded on
|
|
3
|
+
// what it PRODUCES: LZW and RunLength expand, so a small stream can be a bomb just like a Flate one.
|
|
4
|
+
/** Thrown when a stream's data is not valid for the filter it declares. */
|
|
5
|
+
export class PdfFilterError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(`@jasy/pdf: ${message}`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const tooLarge = (max) => new PdfStreamTooLargeError(max, `a stream in this PDF expands past the ${max} byte limit for a single stream; if the file is ` +
|
|
11
|
+
`genuinely that large, raise it with { maxStreamSize }`);
|
|
12
|
+
/** `/ASCIIHexDecode`: hex digits, whitespace ignored, `>` ends it; an odd last digit is followed by 0. */
|
|
13
|
+
export function asciiHexDecode(data) {
|
|
14
|
+
const out = [];
|
|
15
|
+
let high = -1;
|
|
16
|
+
for (const c of data) {
|
|
17
|
+
if (c === 0x3e)
|
|
18
|
+
break; // ">"
|
|
19
|
+
if (c === 0x20 || (c >= 0x09 && c <= 0x0d) || c === 0x00)
|
|
20
|
+
continue;
|
|
21
|
+
const v = c >= 0x30 && c <= 0x39
|
|
22
|
+
? c - 0x30
|
|
23
|
+
: c >= 0x41 && c <= 0x46
|
|
24
|
+
? c - 55
|
|
25
|
+
: c >= 0x61 && c <= 0x66
|
|
26
|
+
? c - 87
|
|
27
|
+
: -1;
|
|
28
|
+
if (v < 0)
|
|
29
|
+
throw new PdfFilterError(`ASCIIHexDecode: "${String.fromCharCode(c)}" is not a hex digit`);
|
|
30
|
+
if (high < 0)
|
|
31
|
+
high = v;
|
|
32
|
+
else {
|
|
33
|
+
out.push((high << 4) | v);
|
|
34
|
+
high = -1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (high >= 0)
|
|
38
|
+
out.push(high << 4);
|
|
39
|
+
return Uint8Array.from(out);
|
|
40
|
+
}
|
|
41
|
+
/** `/ASCII85Decode`: groups of five in `!`..`u`, `z` for four zero bytes, `~>` ends it. */
|
|
42
|
+
export function ascii85Decode(data, max) {
|
|
43
|
+
const out = [];
|
|
44
|
+
const group = [];
|
|
45
|
+
const flush = (count) => {
|
|
46
|
+
let n = 0;
|
|
47
|
+
for (let i = 0; i < 5; i++)
|
|
48
|
+
n = n * 85 + (group[i] ?? 84);
|
|
49
|
+
if (n > 0xffffffff)
|
|
50
|
+
throw new PdfFilterError("ASCII85Decode: a group decodes past 32 bits");
|
|
51
|
+
for (let i = 0; i < count - 1; i++)
|
|
52
|
+
out.push((n >>> (24 - 8 * i)) & 0xff);
|
|
53
|
+
group.length = 0;
|
|
54
|
+
if (out.length > max)
|
|
55
|
+
throw tooLarge(max);
|
|
56
|
+
};
|
|
57
|
+
for (let i = 0; i < data.length; i++) {
|
|
58
|
+
const c = data[i];
|
|
59
|
+
if (c === 0x7e)
|
|
60
|
+
break; // "~", the start of "~>"
|
|
61
|
+
if (c === 0x20 || (c >= 0x09 && c <= 0x0d) || c === 0x00)
|
|
62
|
+
continue;
|
|
63
|
+
if (c === 0x7a && group.length === 0) {
|
|
64
|
+
out.push(0, 0, 0, 0); // "z"
|
|
65
|
+
if (out.length > max)
|
|
66
|
+
throw tooLarge(max);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (c < 0x21 || c > 0x75) {
|
|
70
|
+
throw new PdfFilterError(`ASCII85Decode: "${String.fromCharCode(c)}" is outside the alphabet`);
|
|
71
|
+
}
|
|
72
|
+
group.push(c - 33);
|
|
73
|
+
if (group.length === 5)
|
|
74
|
+
flush(5);
|
|
75
|
+
}
|
|
76
|
+
if (group.length === 1)
|
|
77
|
+
throw new PdfFilterError("ASCII85Decode: a final group of one character");
|
|
78
|
+
if (group.length > 0)
|
|
79
|
+
flush(group.length);
|
|
80
|
+
return Uint8Array.from(out);
|
|
81
|
+
}
|
|
82
|
+
/** `/RunLengthDecode`: a length byte, then that many literals (0-127) or one byte repeated (129-255). */
|
|
83
|
+
export function runLengthDecode(data, max) {
|
|
84
|
+
const out = [];
|
|
85
|
+
let i = 0;
|
|
86
|
+
while (i < data.length) {
|
|
87
|
+
const len = data[i++];
|
|
88
|
+
if (len === 128)
|
|
89
|
+
break;
|
|
90
|
+
if (len < 128) {
|
|
91
|
+
if (i + len + 1 > data.length)
|
|
92
|
+
throw new PdfFilterError("RunLengthDecode: data ends inside a run");
|
|
93
|
+
for (let k = 0; k <= len; k++)
|
|
94
|
+
out.push(data[i + k]);
|
|
95
|
+
i += len + 1;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
if (i >= data.length)
|
|
99
|
+
throw new PdfFilterError("RunLengthDecode: data ends inside a run");
|
|
100
|
+
for (let k = 0; k < 257 - len; k++)
|
|
101
|
+
out.push(data[i]);
|
|
102
|
+
i += 1;
|
|
103
|
+
}
|
|
104
|
+
if (out.length > max)
|
|
105
|
+
throw tooLarge(max);
|
|
106
|
+
}
|
|
107
|
+
return Uint8Array.from(out);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* `/LZWDecode`: 9 to 12 bit codes, 256 clears the table, 257 ends. `earlyChange` 1 (the default) widens
|
|
111
|
+
* the code one entry early, as PDF's LZW does; 0 is the textbook variant.
|
|
112
|
+
*/
|
|
113
|
+
export function lzwDecode(data, max, earlyChange = 1) {
|
|
114
|
+
const out = [];
|
|
115
|
+
let table = [];
|
|
116
|
+
let width = 9;
|
|
117
|
+
let prev;
|
|
118
|
+
const reset = () => {
|
|
119
|
+
table = Array.from({ length: 258 }, (_, i) => Uint8Array.of(i));
|
|
120
|
+
width = 9;
|
|
121
|
+
prev = undefined;
|
|
122
|
+
};
|
|
123
|
+
reset();
|
|
124
|
+
let bitPos = 0;
|
|
125
|
+
const totalBits = data.length * 8;
|
|
126
|
+
while (bitPos + width <= totalBits) {
|
|
127
|
+
let code = 0;
|
|
128
|
+
for (let b = 0; b < width; b++) {
|
|
129
|
+
const bit = (data[(bitPos + b) >> 3] >> (7 - ((bitPos + b) & 7))) & 1;
|
|
130
|
+
code = (code << 1) | bit;
|
|
131
|
+
}
|
|
132
|
+
bitPos += width;
|
|
133
|
+
if (code === 256) {
|
|
134
|
+
reset();
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (code === 257)
|
|
138
|
+
break;
|
|
139
|
+
let entry;
|
|
140
|
+
if (code < table.length)
|
|
141
|
+
entry = table[code];
|
|
142
|
+
else if (code === table.length && prev)
|
|
143
|
+
entry = Uint8Array.from([...prev, prev[0]]);
|
|
144
|
+
else
|
|
145
|
+
throw new PdfFilterError(`LZWDecode: code ${code} is not in the table`);
|
|
146
|
+
for (const b of entry)
|
|
147
|
+
out.push(b);
|
|
148
|
+
if (out.length > max)
|
|
149
|
+
throw tooLarge(max);
|
|
150
|
+
if (prev)
|
|
151
|
+
table.push(Uint8Array.from([...prev, entry[0]]));
|
|
152
|
+
prev = entry;
|
|
153
|
+
if (table.length + earlyChange >= 1 << width && width < 12)
|
|
154
|
+
width++;
|
|
155
|
+
}
|
|
156
|
+
return Uint8Array.from(out);
|
|
157
|
+
}
|
package/dist/edit/index.d.ts
CHANGED
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
* that only generates should not carry it - the same reason jimp is kept out of the text path. Importing
|
|
6
6
|
* `@jasy/pdf` alone pulls in none of this.
|
|
7
7
|
*
|
|
8
|
-
* The scope is deliberately narrow: AcroForm fields
|
|
8
|
+
* The scope is deliberately narrow: AcroForm fields and attachments, nothing else. This is not a PDF editor. What we
|
|
9
9
|
* do not understand, we leave untouched, and what we cannot do safely we refuse by name.
|
|
10
10
|
*/
|
|
11
11
|
export { readAcroForm } from "./acroform-reader.js";
|
|
12
12
|
export type { ReadField, ReadForm, ReadWidget } from "./acroform-reader.js";
|
|
13
|
+
export { readAttachments, AttachmentError } from "./attachments.js";
|
|
14
|
+
export type { Attachment, AttachmentLocation } from "./attachments.js";
|
|
15
|
+
export { PdfFilterError } from "./filters.js";
|
|
13
16
|
export { fillForm, FillError } from "./fill.js";
|
|
14
17
|
export { flattenForm, FlattenError } from "./flatten.js";
|
|
15
18
|
export type { FlattenOptions, FlattenResult } from "./flatten.js";
|
package/dist/edit/index.js
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
* that only generates should not carry it - the same reason jimp is kept out of the text path. Importing
|
|
6
6
|
* `@jasy/pdf` alone pulls in none of this.
|
|
7
7
|
*
|
|
8
|
-
* The scope is deliberately narrow: AcroForm fields
|
|
8
|
+
* The scope is deliberately narrow: AcroForm fields and attachments, nothing else. This is not a PDF editor. What we
|
|
9
9
|
* do not understand, we leave untouched, and what we cannot do safely we refuse by name.
|
|
10
10
|
*/
|
|
11
11
|
export { readAcroForm } from "./acroform-reader.js";
|
|
12
|
+
export { readAttachments, AttachmentError } from "./attachments.js";
|
|
13
|
+
export { PdfFilterError } from "./filters.js";
|
|
12
14
|
export { fillForm, FillError } from "./fill.js";
|
|
13
15
|
export { flattenForm, FlattenError } from "./flatten.js";
|
|
14
16
|
// The document itself, for callers who want to look further than the form.
|