@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 +25 -5
- package/dist/cli.js +18 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -0
- package/package.json +1 -1
- package/pkg/README.md +24 -5
- package/pkg/package.json +1 -1
- package/pkg/pdf_engine.d.ts +6 -0
- package/pkg/pdf_engine.js +1 -1
- package/pkg/pdf_engine_bg.js +120 -0
- package/pkg/pdf_engine_bg.wasm +0 -0
- package/pkg/pdf_engine_bg.wasm.d.ts +6 -0
- package/src/cli.ts +18 -0
- package/src/index.ts +35 -0
- package/src/lib.rs +97 -1
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.
|
|
118
|
-
| Text Extraction | 0.
|
|
119
|
-
| Merge (2 files) | 1.
|
|
120
|
-
| Split (pages [0,1]) | 0.
|
|
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.
|
|
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;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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
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.
|
|
118
|
-
| Text Extraction | 0.
|
|
119
|
-
| Merge (2 files) |
|
|
120
|
-
| Split (pages [0,1]) | 0.
|
|
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.
|
|
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
package/pkg/pdf_engine.d.ts
CHANGED
|
@@ -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";
|
package/pkg/pdf_engine_bg.js
CHANGED
|
@@ -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
|
|
package/pkg/pdf_engine_bg.wasm
CHANGED
|
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
|
|
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::*;
|