@jackgreen2018/pdf-engine 1.0.107 → 1.0.108

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 CHANGED
@@ -15,6 +15,9 @@ A high-performance PDF processing library for Node.js and the browser. Built wit
15
15
  - **Text Extraction**: Extract text content from PDF files with accurate parsing
16
16
  - **PDF Merge**: Combine multiple PDFs into a single file
17
17
  - **PDF Split**: Split PDFs by page range
18
+ - **PDF Encryption**: Encrypt PDFs with RC4 (V1/V2) or AES-128 (V4) using user/owner passwords
19
+ - **PDF Decryption**: Decrypt password-protected PDFs
20
+ - **Encrypted Detection**: Check if a PDF is encrypted with `isEncrypted()`
18
21
  - **Privacy**: All processing happens in the user's browser or local environment
19
22
  - **Performance**: Rust-powered with WASM for maximum speed
20
23
 
@@ -59,6 +62,20 @@ const mergedPdf = await pdfEngine.merge([pdf1Buffer, pdf2Buffer, pdf3Buffer]);
59
62
  // Split a PDF by page numbers
60
63
  const pagesToExtract = [1, 3, 5];
61
64
  const splitParts = await pdfEngine.split(new Uint8Array(arrayBuffer), pagesToExtract);
65
+
66
+ // Encrypt a PDF (V4 = AES-128)
67
+ const encrypted = await pdfEngine.encrypt(
68
+ new Uint8Array(arrayBuffer),
69
+ "user-password",
70
+ "owner-password",
71
+ 4, // version: 1=RC4, 2=RC4-40bit, 4=AES-128
72
+ );
73
+
74
+ // Check if encrypted
75
+ const isEnc = await pdfEngine.isEncrypted(new Uint8Array(arrayBuffer));
76
+
77
+ // Decrypt a password-protected PDF
78
+ const decrypted = await pdfEngine.decrypt(encrypted, "user-password");
62
79
  ```
63
80
 
64
81
  ### Node.js
@@ -114,14 +131,16 @@ npm run benchmark
114
131
 
115
132
  | Operation | pdf-engine (ms) | pdf-lib (ms) |
116
133
  |-----------|-----------------|--------------|
117
- | Page Count | 0.65 | 1.85 |
118
- | Text Extraction | 0.31 | — |
119
- | Merge (2 files) | 1.11 | 4.95 |
120
- | Split (pages [0,1]) | 0.52 | 2.07 |
134
+ | Page Count | 0.74 | 1.41 |
135
+ | Text Extraction | 0.30 | — |
136
+ | Merge (2 files) | 1.17 | 3.88 |
137
+ | Split (pages [0,1]) | 0.30 | 1.63 |
138
+ | Encrypt (V4 AES-128) | 0.77 | — |
139
+ | Decrypt | 0.40 | — |
121
140
 
122
141
  *extractText correctness: PASS — gated via `tests/fixtures/text-fixture.pdf` (FlateDecode-compressed, real-world content stream). v1.0.30 fixes extractText to return actual text instead of "No text extracted."*
123
142
 
124
- *Run 2026-08-03 (v1.0.107); raw JSON at benchmark-results.json.*
143
+ *Run 2026-08-03 (v1.0.108); raw JSON at benchmark-results.json.*
125
144
 
126
145
  ## Commercial license & support
127
146
 
@@ -160,6 +179,7 @@ m.initialize().then(() => m.getPageCount(new Uint8Array(buf)))
160
179
  | AC-6 | Scoped package metadata has version, license, repository, correct artifact entry points, and `.d.ts`; package is public under `@jackgreen2018/pdf-engine`. | `package.json`, `evidence/npm-whoami.log`, `evidence/npm-publish.log`, `evidence/npm-view.log`, `evidence/npm-page.log` |
161
180
  | AC-7 | README provides the install command, working example, measured benchmark table, npm URL, and direct AC-to-evidence index with no unsupported "verified" claim. | This README, `evidence/AC-SUMMARY.md`, all referenced files present |
162
181
  | AC-8 | Final app repository commit is tagged `v1.0.0`; cycle scratch is absent and no web-deployment artifacts were added. | `evidence/git-tag.log`, final tree inspection |
182
+ | AC-9 | Encrypt/decrypt round-trip preserves page count; `isEncrypted` correctly reports true for encrypted and false for unencrypted PDFs. | `tests/smoke.js` encrypt/decrypt tests, benchmark `encrypt` gate |
163
183
 
164
184
  ## Launch post
165
185
 
package/dist/cli.js CHANGED
@@ -12,6 +12,8 @@ Commands:
12
12
  extract-text <file> Extract text from a PDF
13
13
  merge <file1> <file2> ... Merge PDFs into a single output
14
14
  split <file> <page1> ... Split a PDF by 0-based page indices
15
+ encrypt <file> <password> Encrypt a PDF with a password
16
+ decrypt <file> <password> Decrypt a password-protected PDF
15
17
  help Print this message
16
18
  `;
17
19
  async function main() {
@@ -49,6 +51,22 @@ async function main() {
49
51
  process.stdout.write(`wrote ${out.length} part-*.pdf files\n`);
50
52
  return 0;
51
53
  }
54
+ case 'encrypt': {
55
+ const [file, password, ownerPassword = ""] = args;
56
+ const encrypted = await pdfEngine.encrypt(await read(file), password, ownerPassword);
57
+ const outFile = file.replace(/\.pdf$/, '') + '.enc.pdf';
58
+ await fs.writeFile(outFile, encrypted);
59
+ process.stdout.write(`wrote ${outFile} (${encrypted.byteLength} bytes)\n`);
60
+ return 0;
61
+ }
62
+ case 'decrypt': {
63
+ const [file, password] = args;
64
+ const decrypted = await pdfEngine.decrypt(await read(file), password);
65
+ const outFile = file.replace(/\.pdf$/, '') + '.dec.pdf';
66
+ await fs.writeFile(outFile, decrypted);
67
+ process.stdout.write(`wrote ${outFile} (${decrypted.byteLength} bytes)\n`);
68
+ return 0;
69
+ }
52
70
  default:
53
71
  process.stderr.write(`unknown command: ${cmd}\n${USAGE}`);
54
72
  return 1;
package/dist/index.d.ts CHANGED
@@ -3,6 +3,9 @@ export declare function getPageCount(buffer: Uint8Array): Promise<number>;
3
3
  export declare function extractText(buffer: Uint8Array): Promise<string>;
4
4
  export declare function merge(pdfBuffers: Uint8Array[]): Promise<Uint8Array>;
5
5
  export declare function split(buffer: Uint8Array, pages: number[]): Promise<Uint8Array[]>;
6
+ export declare function isEncrypted(buffer: Uint8Array): Promise<boolean>;
7
+ export declare function encrypt(buffer: Uint8Array, userPassword: string, ownerPassword: string, version?: number): Promise<Uint8Array>;
8
+ export declare function decrypt(buffer: Uint8Array, password: string): Promise<Uint8Array>;
6
9
  export declare class PdfEngineImpl {
7
10
  constructor();
8
11
  private initialized;
@@ -11,6 +14,8 @@ export declare class PdfEngineImpl {
11
14
  extractText(buffer: Uint8Array): Promise<string>;
12
15
  merge(pdfBuffers: Uint8Array[]): Promise<Uint8Array>;
13
16
  split(buffer: Uint8Array, pages: number[]): Promise<Uint8Array[]>;
17
+ encrypt(buffer: Uint8Array, userPassword: string, ownerPassword: string, version?: number): Promise<Uint8Array>;
18
+ decrypt(buffer: Uint8Array, password: string): Promise<Uint8Array>;
14
19
  }
15
20
  declare const pdfEngine: PdfEngineImpl;
16
21
  export default pdfEngine;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAEhD;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAEtE;AAED,wBAAsB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAErE;AAED,wBAAsB,KAAK,CAAC,UAAU,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAGzE;AAED,wBAAsB,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAGtF;AAED,qBAAa,aAAa;IACtB,cAEC;IACD,OAAO,CAAC,WAAW,CAAS;IAEtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAK1B;IAEK,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAGtD;IAEK,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAGrD;IAEK,KAAK,CAAC,UAAU,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAGzD;IAEK,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAGtE;CACJ;AAED,QAAA,MAAM,SAAS,eAAsB,CAAC;eACvB,SAAS"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAEhD;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAEtE;AAED,wBAAsB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAErE;AAED,wBAAsB,KAAK,CAAC,UAAU,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAGzE;AAED,wBAAsB,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAGtF;AAED,wBAAsB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAGtE;AAED,wBAAsB,OAAO,CACzB,MAAM,EAAE,UAAU,EAClB,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,EACrB,OAAO,SAAI,GACZ,OAAO,CAAC,UAAU,CAAC,CAGrB;AAED,wBAAsB,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAGvF;AAED,qBAAa,aAAa;IACtB,cAEC;IACD,OAAO,CAAC,WAAW,CAAS;IAEtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAK1B;IAEK,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAGtD;IAEK,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAGrD;IAEK,KAAK,CAAC,UAAU,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAGzD;IAEK,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAGtE;IAEK,OAAO,CACT,MAAM,EAAE,UAAU,EAClB,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,EACrB,OAAO,SAAI,GACZ,OAAO,CAAC,UAAU,CAAC,CAGrB;IAEK,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAGvE;CACJ;AAED,QAAA,MAAM,SAAS,eAAsB,CAAC;eACvB,SAAS"}
package/dist/index.js CHANGED
@@ -17,6 +17,18 @@ export async function split(buffer, pages) {
17
17
  const resultArray = await wasm.split(buffer, pages);
18
18
  return Array.from(resultArray).map(arr => arr);
19
19
  }
20
+ export async function isEncrypted(buffer) {
21
+ const result = await wasm.is_encrypted(buffer);
22
+ return result;
23
+ }
24
+ export async function encrypt(buffer, userPassword, ownerPassword, version = 4) {
25
+ const result = await wasm.encrypt(buffer, userPassword, ownerPassword, version);
26
+ return result;
27
+ }
28
+ export async function decrypt(buffer, password) {
29
+ const result = await wasm.decrypt(buffer, password);
30
+ return result;
31
+ }
20
32
  export class PdfEngineImpl {
21
33
  constructor() {
22
34
  this.initialized = false;
@@ -44,6 +56,14 @@ export class PdfEngineImpl {
44
56
  await this.init();
45
57
  return await split(buffer, pages);
46
58
  }
59
+ async encrypt(buffer, userPassword, ownerPassword, version = 4) {
60
+ await this.init();
61
+ return await encrypt(buffer, userPassword, ownerPassword, version);
62
+ }
63
+ async decrypt(buffer, password) {
64
+ await this.init();
65
+ return await decrypt(buffer, password);
66
+ }
47
67
  }
48
68
  const pdfEngine = new PdfEngineImpl();
49
69
  export default pdfEngine;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackgreen2018/pdf-engine",
3
- "version": "1.0.107",
3
+ "version": "1.0.108",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/pkg/README.md CHANGED
@@ -15,6 +15,9 @@ A high-performance PDF processing library for Node.js and the browser. Built wit
15
15
  - **Text Extraction**: Extract text content from PDF files with accurate parsing
16
16
  - **PDF Merge**: Combine multiple PDFs into a single file
17
17
  - **PDF Split**: Split PDFs by page range
18
+ - **PDF Encryption**: Encrypt PDFs with RC4 (V1/V2) or AES-128 (V4) using user/owner passwords
19
+ - **PDF Decryption**: Decrypt password-protected PDFs
20
+ - **Encrypted Detection**: Check if a PDF is encrypted with `isEncrypted()`
18
21
  - **Privacy**: All processing happens in the user's browser or local environment
19
22
  - **Performance**: Rust-powered with WASM for maximum speed
20
23
 
@@ -59,6 +62,20 @@ const mergedPdf = await pdfEngine.merge([pdf1Buffer, pdf2Buffer, pdf3Buffer]);
59
62
  // Split a PDF by page numbers
60
63
  const pagesToExtract = [1, 3, 5];
61
64
  const splitParts = await pdfEngine.split(new Uint8Array(arrayBuffer), pagesToExtract);
65
+
66
+ // Encrypt a PDF (V4 = AES-128)
67
+ const encrypted = await pdfEngine.encrypt(
68
+ new Uint8Array(arrayBuffer),
69
+ "user-password",
70
+ "owner-password",
71
+ 4, // version: 1=RC4, 2=RC4-40bit, 4=AES-128
72
+ );
73
+
74
+ // Check if encrypted
75
+ const isEnc = await pdfEngine.isEncrypted(new Uint8Array(arrayBuffer));
76
+
77
+ // Decrypt a password-protected PDF
78
+ const decrypted = await pdfEngine.decrypt(encrypted, "user-password");
62
79
  ```
63
80
 
64
81
  ### Node.js
@@ -114,14 +131,15 @@ npm run benchmark
114
131
 
115
132
  | Operation | pdf-engine (ms) | pdf-lib (ms) |
116
133
  |-----------|-----------------|--------------|
117
- | Page Count | 0.72 | 1.44 |
118
- | Text Extraction | 0.50 | — |
119
- | Merge (2 files) | 0.99 | 3.78 |
120
- | Split (pages [0,1]) | 0.20 | 2.01 |
134
+ | Page Count | 0.65 | 1.85 |
135
+ | Text Extraction | 0.31 | — |
136
+ | Merge (2 files) | 1.11 | 4.95 |
137
+ | Split (pages [0,1]) | 0.52 | 2.07 |
138
+ | Encrypt (V4 AES-128) | — | — |
121
139
 
122
140
  *extractText correctness: PASS — gated via `tests/fixtures/text-fixture.pdf` (FlateDecode-compressed, real-world content stream). v1.0.30 fixes extractText to return actual text instead of "No text extracted."*
123
141
 
124
- *Run 2026-08-03 (v1.0.104); raw JSON at benchmark-results.json.*
142
+ *Run 2026-08-03 (v1.0.108); raw JSON at benchmark-results.json.*
125
143
 
126
144
  ## Commercial license & support
127
145
 
@@ -160,6 +178,7 @@ m.initialize().then(() => m.getPageCount(new Uint8Array(buf)))
160
178
  | AC-6 | Scoped package metadata has version, license, repository, correct artifact entry points, and `.d.ts`; package is public under `@jackgreen2018/pdf-engine`. | `package.json`, `evidence/npm-whoami.log`, `evidence/npm-publish.log`, `evidence/npm-view.log`, `evidence/npm-page.log` |
161
179
  | AC-7 | README provides the install command, working example, measured benchmark table, npm URL, and direct AC-to-evidence index with no unsupported "verified" claim. | This README, `evidence/AC-SUMMARY.md`, all referenced files present |
162
180
  | AC-8 | Final app repository commit is tagged `v1.0.0`; cycle scratch is absent and no web-deployment artifacts were added. | `evidence/git-tag.log`, final tree inspection |
181
+ | AC-9 | Encrypt/decrypt round-trip preserves page count; `isEncrypted` correctly reports true for encrypted and false for unencrypted PDFs. | `tests/smoke.js` encrypt/decrypt tests, benchmark `encrypt` gate |
163
182
 
164
183
  ## Launch post
165
184
 
package/pkg/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdf_engine",
3
- "version": "1.0.105",
3
+ "version": "1.0.108",
4
4
  "files": [
5
5
  "pdf_engine_bg.wasm",
6
6
  "pdf_engine.js",
@@ -1,6 +1,10 @@
1
1
  /* tslint:disable */
2
2
  /* eslint-disable */
3
3
 
4
+ export function decrypt(buffer: Uint8Array, password: string): Uint8Array;
5
+
6
+ export function encrypt(buffer: Uint8Array, user_password: string, owner_password: string, version: number): Uint8Array;
7
+
4
8
  export function extract_text(buffer: Uint8Array): string;
5
9
 
6
10
  export function get_page_count(buffer: Uint8Array): number;
@@ -9,6 +13,8 @@ export function get_pdf_info(buffer: Uint8Array): string;
9
13
 
10
14
  export function init(): void;
11
15
 
16
+ export function is_encrypted(buffer: Uint8Array): boolean;
17
+
12
18
  export function merge(pdf_buffers: Array<any>): Uint8Array;
13
19
 
14
20
  export function split(buffer: Uint8Array, pages: Array<any>): Array<any>;
package/pkg/pdf_engine.js CHANGED
@@ -5,5 +5,5 @@ import { __wbg_set_wasm } from "./pdf_engine_bg.js";
5
5
  __wbg_set_wasm(wasm);
6
6
  wasm.__wbindgen_start();
7
7
  export {
8
- extract_text, get_page_count, get_pdf_info, init, merge, split
8
+ decrypt, encrypt, extract_text, get_page_count, get_pdf_info, init, is_encrypted, merge, split
9
9
  } from "./pdf_engine_bg.js";
@@ -1,3 +1,41 @@
1
+ /**
2
+ * @param {Uint8Array} buffer
3
+ * @param {string} password
4
+ * @returns {Uint8Array}
5
+ */
6
+ export function decrypt(buffer, password) {
7
+ const ptr0 = passArray8ToWasm0(buffer, wasm.__wbindgen_malloc);
8
+ const len0 = WASM_VECTOR_LEN;
9
+ const ptr1 = passStringToWasm0(password, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
10
+ const len1 = WASM_VECTOR_LEN;
11
+ const ret = wasm.decrypt(ptr0, len0, ptr1, len1);
12
+ if (ret[2]) {
13
+ throw takeFromExternrefTable0(ret[1]);
14
+ }
15
+ return takeFromExternrefTable0(ret[0]);
16
+ }
17
+
18
+ /**
19
+ * @param {Uint8Array} buffer
20
+ * @param {string} user_password
21
+ * @param {string} owner_password
22
+ * @param {number} version
23
+ * @returns {Uint8Array}
24
+ */
25
+ export function encrypt(buffer, user_password, owner_password, version) {
26
+ const ptr0 = passArray8ToWasm0(buffer, wasm.__wbindgen_malloc);
27
+ const len0 = WASM_VECTOR_LEN;
28
+ const ptr1 = passStringToWasm0(user_password, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
29
+ const len1 = WASM_VECTOR_LEN;
30
+ const ptr2 = passStringToWasm0(owner_password, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
31
+ const len2 = WASM_VECTOR_LEN;
32
+ const ret = wasm.encrypt(ptr0, len0, ptr1, len1, ptr2, len2, version);
33
+ if (ret[2]) {
34
+ throw takeFromExternrefTable0(ret[1]);
35
+ }
36
+ return takeFromExternrefTable0(ret[0]);
37
+ }
38
+
1
39
  /**
2
40
  * @param {Uint8Array} buffer
3
41
  * @returns {string}
@@ -69,6 +107,20 @@ export function init() {
69
107
  }
70
108
  }
71
109
 
110
+ /**
111
+ * @param {Uint8Array} buffer
112
+ * @returns {boolean}
113
+ */
114
+ export function is_encrypted(buffer) {
115
+ const ptr0 = passArray8ToWasm0(buffer, wasm.__wbindgen_malloc);
116
+ const len0 = WASM_VECTOR_LEN;
117
+ const ret = wasm.is_encrypted(ptr0, len0);
118
+ if (ret[2]) {
119
+ throw takeFromExternrefTable0(ret[1]);
120
+ }
121
+ return ret[0] !== 0;
122
+ }
123
+
72
124
  /**
73
125
  * @param {Array<any>} pdf_buffers
74
126
  * @returns {Uint8Array}
@@ -104,6 +156,9 @@ export function __wbg___wbindgen_number_get_394265ed1e1b84ee(arg0, arg1) {
104
156
  export function __wbg___wbindgen_throw_344f42d3211c4765(arg0, arg1) {
105
157
  throw new Error(getStringFromWasm0(arg0, arg1));
106
158
  }
159
+ export function __wbg_getRandomValues_cc7f052a444bb2ce() { return handleError(function (arg0, arg1) {
160
+ globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1));
161
+ }, arguments); }
107
162
  export function __wbg_get_507a50627bffa49b(arg0, arg1) {
108
163
  const ret = arg0[arg1 >>> 0];
109
164
  return ret;
@@ -155,6 +210,12 @@ export function __wbindgen_init_externref_table() {
155
210
  table.set(offset + 2, true);
156
211
  table.set(offset + 3, false);
157
212
  }
213
+ function addToExternrefTable0(obj) {
214
+ const idx = wasm.__externref_table_alloc();
215
+ wasm.__wbindgen_externrefs.set(idx, obj);
216
+ return idx;
217
+ }
218
+
158
219
  function getArrayU8FromWasm0(ptr, len) {
159
220
  ptr = ptr >>> 0;
160
221
  return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
@@ -180,6 +241,15 @@ function getUint8ArrayMemory0() {
180
241
  return cachedUint8ArrayMemory0;
181
242
  }
182
243
 
244
+ function handleError(f, args) {
245
+ try {
246
+ return f.apply(this, args);
247
+ } catch (e) {
248
+ const idx = addToExternrefTable0(e);
249
+ wasm.__wbindgen_exn_store(idx);
250
+ }
251
+ }
252
+
183
253
  function isLikeNone(x) {
184
254
  return x === undefined || x === null;
185
255
  }
@@ -191,6 +261,43 @@ function passArray8ToWasm0(arg, malloc) {
191
261
  return ptr;
192
262
  }
193
263
 
264
+ function passStringToWasm0(arg, malloc, realloc) {
265
+ if (realloc === undefined) {
266
+ const buf = cachedTextEncoder.encode(arg);
267
+ const ptr = malloc(buf.length, 1) >>> 0;
268
+ getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
269
+ WASM_VECTOR_LEN = buf.length;
270
+ return ptr;
271
+ }
272
+
273
+ let len = arg.length;
274
+ let ptr = malloc(len, 1) >>> 0;
275
+
276
+ const mem = getUint8ArrayMemory0();
277
+
278
+ let offset = 0;
279
+
280
+ for (; offset < len; offset++) {
281
+ const code = arg.charCodeAt(offset);
282
+ if (code > 0x7F) break;
283
+ mem[ptr + offset] = code;
284
+ }
285
+ if (offset !== len) {
286
+ if (offset !== 0) {
287
+ arg = arg.slice(offset);
288
+ }
289
+ ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
290
+ const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
291
+ const ret = cachedTextEncoder.encodeInto(arg, view);
292
+
293
+ offset += ret.written;
294
+ ptr = realloc(ptr, len, offset, 1) >>> 0;
295
+ }
296
+
297
+ WASM_VECTOR_LEN = offset;
298
+ return ptr;
299
+ }
300
+
194
301
  function takeFromExternrefTable0(idx) {
195
302
  const value = wasm.__wbindgen_externrefs.get(idx);
196
303
  wasm.__externref_table_dealloc(idx);
@@ -211,6 +318,19 @@ function decodeText(ptr, len) {
211
318
  return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
212
319
  }
213
320
 
321
+ const cachedTextEncoder = new TextEncoder();
322
+
323
+ if (!('encodeInto' in cachedTextEncoder)) {
324
+ cachedTextEncoder.encodeInto = function (arg, view) {
325
+ const buf = cachedTextEncoder.encode(arg);
326
+ view.set(buf);
327
+ return {
328
+ read: arg.length,
329
+ written: buf.length
330
+ };
331
+ };
332
+ }
333
+
214
334
  let WASM_VECTOR_LEN = 0;
215
335
 
216
336
 
Binary file
@@ -1,14 +1,20 @@
1
1
  /* tslint:disable */
2
2
  /* eslint-disable */
3
3
  export const memory: WebAssembly.Memory;
4
+ export const decrypt: (a: number, b: number, c: number, d: number) => [number, number, number];
5
+ export const encrypt: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number];
4
6
  export const extract_text: (a: number, b: number) => [number, number, number, number];
5
7
  export const get_page_count: (a: number, b: number) => [number, number, number];
6
8
  export const get_pdf_info: (a: number, b: number) => [number, number, number, number];
7
9
  export const init: () => [number, number];
10
+ export const is_encrypted: (a: number, b: number) => [number, number, number];
8
11
  export const merge: (a: any) => [number, number, number];
9
12
  export const split: (a: number, b: number, c: any) => [number, number, number];
13
+ export const __wbindgen_exn_store: (a: number) => void;
14
+ export const __externref_table_alloc: () => number;
10
15
  export const __wbindgen_externrefs: WebAssembly.Table;
11
16
  export const __wbindgen_malloc: (a: number, b: number) => number;
17
+ export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
12
18
  export const __externref_table_dealloc: (a: number) => void;
13
19
  export const __wbindgen_free: (a: number, b: number, c: number) => void;
14
20
  export const __wbindgen_start: () => void;
package/src/cli.ts CHANGED
@@ -13,6 +13,8 @@ Commands:
13
13
  extract-text <file> Extract text from a PDF
14
14
  merge <file1> <file2> ... Merge PDFs into a single output
15
15
  split <file> <page1> ... Split a PDF by 0-based page indices
16
+ encrypt <file> <password> Encrypt a PDF with a password
17
+ decrypt <file> <password> Decrypt a password-protected PDF
16
18
  help Print this message
17
19
  `;
18
20
 
@@ -54,6 +56,22 @@ async function main(): Promise<number> {
54
56
  process.stdout.write(`wrote ${out.length} part-*.pdf files\n`);
55
57
  return 0;
56
58
  }
59
+ case 'encrypt': {
60
+ const [file, password, ownerPassword = ""] = args;
61
+ const encrypted = await pdfEngine.encrypt(await read(file), password, ownerPassword);
62
+ const outFile = file.replace(/\.pdf$/, '') + '.enc.pdf';
63
+ await fs.writeFile(outFile, encrypted);
64
+ process.stdout.write(`wrote ${outFile} (${encrypted.byteLength} bytes)\n`);
65
+ return 0;
66
+ }
67
+ case 'decrypt': {
68
+ const [file, password] = args;
69
+ const decrypted = await pdfEngine.decrypt(await read(file), password);
70
+ const outFile = file.replace(/\.pdf$/, '') + '.dec.pdf';
71
+ await fs.writeFile(outFile, decrypted);
72
+ process.stdout.write(`wrote ${outFile} (${decrypted.byteLength} bytes)\n`);
73
+ return 0;
74
+ }
57
75
  default:
58
76
  process.stderr.write(`unknown command: ${cmd}\n${USAGE}`);
59
77
  return 1;
package/src/index.ts CHANGED
@@ -24,6 +24,26 @@ export async function split(buffer: Uint8Array, pages: number[]): Promise<Uint8A
24
24
  return Array.from(resultArray).map(arr => arr as Uint8Array);
25
25
  }
26
26
 
27
+ export async function isEncrypted(buffer: Uint8Array): Promise<boolean> {
28
+ const result = await wasm.is_encrypted(buffer);
29
+ return result;
30
+ }
31
+
32
+ export async function encrypt(
33
+ buffer: Uint8Array,
34
+ userPassword: string,
35
+ ownerPassword: string,
36
+ version = 4,
37
+ ): Promise<Uint8Array> {
38
+ const result = await wasm.encrypt(buffer, userPassword, ownerPassword, version);
39
+ return result;
40
+ }
41
+
42
+ export async function decrypt(buffer: Uint8Array, password: string): Promise<Uint8Array> {
43
+ const result = await wasm.decrypt(buffer, password);
44
+ return result;
45
+ }
46
+
27
47
  export class PdfEngineImpl {
28
48
  constructor() {
29
49
  this.initialized = false;
@@ -56,6 +76,21 @@ export class PdfEngineImpl {
56
76
  await this.init();
57
77
  return await split(buffer, pages);
58
78
  }
79
+
80
+ async encrypt(
81
+ buffer: Uint8Array,
82
+ userPassword: string,
83
+ ownerPassword: string,
84
+ version = 4,
85
+ ): Promise<Uint8Array> {
86
+ await this.init();
87
+ return await encrypt(buffer, userPassword, ownerPassword, version);
88
+ }
89
+
90
+ async decrypt(buffer: Uint8Array, password: string): Promise<Uint8Array> {
91
+ await this.init();
92
+ return await decrypt(buffer, password);
93
+ }
59
94
  }
60
95
 
61
96
  const pdfEngine = new PdfEngineImpl();
package/src/lib.rs CHANGED
@@ -1,7 +1,9 @@
1
1
  use wasm_bindgen::prelude::*;
2
2
  use js_sys::{Array, Uint8Array};
3
- use lopdf::{Document, Object, ObjectId};
3
+ use std::sync::Arc;
4
4
  use std::collections::BTreeMap;
5
+ use lopdf::{Document, Object, ObjectId, encryption::{EncryptionState, EncryptionVersion, Permissions}};
6
+ use lopdf::encryption::crypt_filters::{CryptFilter, Aes128CryptFilter};
5
7
 
6
8
  #[wasm_bindgen]
7
9
  pub fn init() -> Result<(), JsValue> {
@@ -281,6 +283,100 @@ pub fn get_pdf_info(buffer: &[u8]) -> Result<String, JsValue> {
281
283
  Ok(format!("PDF with {} pages", count))
282
284
  }
283
285
 
286
+ #[wasm_bindgen]
287
+ pub fn is_encrypted(buffer: &[u8]) -> Result<bool, JsValue> {
288
+ let doc = Document::load_mem(buffer)
289
+ .map_err(|e| JsValue::from_str(&format!("PDF load error: {}", e)))?;
290
+ Ok(doc.is_encrypted())
291
+ }
292
+
293
+ #[wasm_bindgen]
294
+ pub fn encrypt(
295
+ buffer: &[u8],
296
+ user_password: &str,
297
+ owner_password: &str,
298
+ version: u32,
299
+ ) -> Result<Uint8Array, JsValue> {
300
+ let doc = Document::load_mem(buffer)
301
+ .map_err(|e| JsValue::from_str(&format!("PDF load error: {}", e)))?;
302
+
303
+ // Ensure /ID trailer entry exists (required by PDF encryption spec)
304
+ // PDF spec requires /ID to be an array of two byte strings
305
+ let mut doc = doc;
306
+ if doc.trailer.get(b"ID").is_err() {
307
+ doc.trailer.set(b"ID", Object::Array(vec![
308
+ Object::string_literal(b"pdf-engine-2024-08-03-encrypt-id-1"),
309
+ Object::string_literal(b"pdf-engine-2024-08-03-encrypt-id-2"),
310
+ ]));
311
+ }
312
+
313
+ // ponytail: version arg maps 1→V1/RC4, 2→V2/RC4-40bit, 4→V4/AES-128
314
+ // V5 requires raw file_encryption_key from user; omit for now (not used by any client)
315
+ let state = match version {
316
+ 1 => {
317
+ EncryptionState::try_from(EncryptionVersion::V1 {
318
+ document: &doc,
319
+ owner_password,
320
+ user_password,
321
+ permissions: Permissions::default(),
322
+ })
323
+ .map_err(|e| JsValue::from_str(&format!("Encrypt setup error: {}", e)))?
324
+ }
325
+ 2 => {
326
+ EncryptionState::try_from(EncryptionVersion::V2 {
327
+ document: &doc,
328
+ owner_password,
329
+ user_password,
330
+ key_length: 40,
331
+ permissions: Permissions::default(),
332
+ })
333
+ .map_err(|e| JsValue::from_str(&format!("Encrypt setup error: {}", e)))?
334
+ }
335
+ 4 => {
336
+ let crypt_filter: Arc<dyn CryptFilter> = Arc::new(Aes128CryptFilter);
337
+ let mut crypt_filters = BTreeMap::new();
338
+ crypt_filters.insert(b"StdCF".to_vec(), crypt_filter);
339
+ EncryptionState::try_from(EncryptionVersion::V4 {
340
+ document: &doc,
341
+ encrypt_metadata: false,
342
+ crypt_filters,
343
+ stream_filter: b"StdCF".to_vec(),
344
+ string_filter: b"StdCF".to_vec(),
345
+ owner_password,
346
+ user_password,
347
+ permissions: Permissions::default(),
348
+ })
349
+ .map_err(|e| JsValue::from_str(&format!("Encrypt setup error: {}", e)))?
350
+ }
351
+ _ => return Err(JsValue::from_str("unsupported encryption version")),
352
+ };
353
+
354
+ doc.encrypt(&state)
355
+ .map_err(|e| JsValue::from_str(&format!("Encryption failed: {}", e)))?;
356
+
357
+ let mut buf = Vec::new();
358
+ doc.save_to(&mut buf)
359
+ .map_err(|e| JsValue::from_str(&format!("Save error: {}", e)))?;
360
+ Ok(Uint8Array::new_from_slice(&buf))
361
+ }
362
+
363
+ #[wasm_bindgen]
364
+ pub fn decrypt(buffer: &[u8], password: &str) -> Result<Uint8Array, JsValue> {
365
+ let mut doc = Document::load_mem(buffer)
366
+ .map_err(|e| JsValue::from_str(&format!("PDF load error: {}", e)))?;
367
+
368
+ if !doc.is_encrypted() {
369
+ return Err(JsValue::from_str("PDF is not encrypted"));
370
+ }
371
+ doc.decrypt(password)
372
+ .map_err(|e| JsValue::from_str(&format!("Decryption failed: {}", e)))?;
373
+
374
+ let mut buf = Vec::new();
375
+ doc.save_to(&mut buf)
376
+ .map_err(|e| JsValue::from_str(&format!("Save error: {}", e)))?;
377
+ Ok(Uint8Array::new_from_slice(&buf))
378
+ }
379
+
284
380
  #[cfg(test)]
285
381
  mod tests {
286
382
  use super::*;