@jackgreen2018/pdf-engine 1.0.113 → 1.0.114

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
@@ -17,6 +17,7 @@ A high-performance PDF processing library for Node.js and the browser. Built wit
17
17
  - **PDF Encryption**: Encrypt PDFs with RC4 (V1/V2) or AES-128 (V4) using user/owner passwords
18
18
  - **PDF Decryption**: Decrypt password-protected PDFs
19
19
  - **Encrypted Detection**: Check if a PDF is encrypted with `isEncrypted()`
20
+ - **Stamp Image**: Overlay PNG/JPEG images onto PDF pages with `stampImage()`
20
21
  - **Privacy**: All processing happens in the user's browser or local environment
21
22
  - **Performance**: Rust-powered with WASM for maximum speed
22
23
 
@@ -73,6 +74,16 @@ const encrypted = await pdfEngine.encrypt(
73
74
  // Check if encrypted
74
75
  const isEnc = await pdfEngine.isEncrypted(new Uint8Array(arrayBuffer));
75
76
 
77
+ // Stamp an image onto a PDF page
78
+ const stampedPdf = await pdfEngine.stampImage(
79
+ new Uint8Array(arrayBuffer),
80
+ imageBuffer,
81
+ 100, // x position
82
+ 100, // y position
83
+ 50, // width
84
+ 50, // height
85
+ );
86
+
76
87
  // Decrypt a password-protected PDF
77
88
  const decrypted = await pdfEngine.decrypt(encrypted, "user-password");
78
89
  ```
@@ -130,16 +141,18 @@ npm run benchmark
130
141
 
131
142
  | Operation | pdf-engine (ms) | pdf-lib (ms) |
132
143
  |-----------|-----------------|--------------|
133
- | Page Count | 0.73 | 1.48 |
134
- | Text Extraction | 0.30 | — |
135
- | Merge (2 files) | 0.79 | 4.49 |
136
- | Split (pages [0,1]) | 0.24 | 2.39 |
144
+ | Page Count | 0.72 | 1.59 |
145
+ | Text Extraction | 0.23 | — |
146
+ | Merge (2 files) | 0.80 | 4.06 |
147
+ | Split (pages [0,1]) | 0.24 | 1.77 |
137
148
  | Encrypt (V4 AES-128) | 0.79 | — |
138
- | Decrypt | 0.38 | — |
149
+ | Decrypt | 0.32 | — |
150
+ | Stamp Image (1x1 PNG) | 1.24 | — |
139
151
 
140
152
  *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."*
153
+ *stampImage: PASS — v1.0.114 fixes empty-page stamping (pages without /Contents now initialize empty content stream before image insertion).*
141
154
 
142
- *Run 2026-08-03 (v1.0.113); raw JSON at benchmark-results.json.*
155
+ *Run 2026-08-04 (v1.0.114); raw JSON at benchmark-results.json.*
143
156
 
144
157
  ## License
145
158
 
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export declare function split(buffer: Uint8Array, pages: number[]): Promise<Uint
6
6
  export declare function isEncrypted(buffer: Uint8Array): Promise<boolean>;
7
7
  export declare function encrypt(buffer: Uint8Array, userPassword: string, ownerPassword: string, version?: number): Promise<Uint8Array>;
8
8
  export declare function decrypt(buffer: Uint8Array, password: string): Promise<Uint8Array>;
9
+ export declare function stampImage(buffer: Uint8Array, imageBuffer: Uint8Array, x: number, y: number, width: number, height: number): Promise<Uint8Array>;
9
10
  export declare class PdfEngineImpl {
10
11
  constructor();
11
12
  private initialized;
@@ -17,6 +18,7 @@ export declare class PdfEngineImpl {
17
18
  isEncrypted(buffer: Uint8Array): Promise<boolean>;
18
19
  encrypt(buffer: Uint8Array, userPassword: string, ownerPassword: string, version?: number): Promise<Uint8Array>;
19
20
  decrypt(buffer: Uint8Array, password: string): Promise<Uint8Array>;
21
+ stampImage(buffer: Uint8Array, imageBuffer: Uint8Array, x: number, y: number, width: number, height: number): Promise<Uint8Array>;
20
22
  }
21
23
  declare const pdfEngine: PdfEngineImpl;
22
24
  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,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,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAGtD;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"}
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,wBAAsB,UAAU,CAC5B,MAAM,EAAE,UAAU,EAClB,WAAW,EAAE,UAAU,EACvB,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,CAAC,CAGrB;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,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAGtD;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;IAEK,UAAU,CACZ,MAAM,EAAE,UAAU,EAClB,WAAW,EAAE,UAAU,EACvB,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,CAAC,CAGrB;CACJ;AAED,QAAA,MAAM,SAAS,eAAsB,CAAC;eACvB,SAAS"}
package/dist/index.js CHANGED
@@ -29,6 +29,10 @@ export async function decrypt(buffer, password) {
29
29
  const result = await wasm.decrypt(buffer, password);
30
30
  return result;
31
31
  }
32
+ export async function stampImage(buffer, imageBuffer, x, y, width, height) {
33
+ const result = await wasm.stamp_image(buffer, imageBuffer, x, y, width, height);
34
+ return result;
35
+ }
32
36
  export class PdfEngineImpl {
33
37
  constructor() {
34
38
  this.initialized = false;
@@ -68,6 +72,10 @@ export class PdfEngineImpl {
68
72
  await this.init();
69
73
  return await decrypt(buffer, password);
70
74
  }
75
+ async stampImage(buffer, imageBuffer, x, y, width, height) {
76
+ await this.init();
77
+ return await stampImage(buffer, imageBuffer, x, y, width, height);
78
+ }
71
79
  }
72
80
  const pdfEngine = new PdfEngineImpl();
73
81
  export default pdfEngine;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackgreen2018/pdf-engine",
3
- "version": "1.0.113",
3
+ "version": "1.0.114",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/pkg/README.md CHANGED
@@ -130,12 +130,12 @@ npm run benchmark
130
130
 
131
131
  | Operation | pdf-engine (ms) | pdf-lib (ms) |
132
132
  |-----------|-----------------|--------------|
133
- | Page Count | 0.73 | 1.48 |
134
- | Text Extraction | 0.30 | — |
135
- | Merge (2 files) | 0.79 | 4.49 |
136
- | Split (pages [0,1]) | 0.24 | 2.39 |
137
- | Encrypt (V4 AES-128) | 0.79 | — |
138
- | Decrypt | 0.38 | — |
133
+ | Page Count | 0.80 | 1.60 |
134
+ | Text Extraction | 0.47 | — |
135
+ | Merge (2 files) | 1.06 | 3.92 |
136
+ | Split (pages [0,1]) | 0.20 | 1.69 |
137
+ | Encrypt (V4 AES-128) | 0.81 | — |
138
+ | Decrypt | 0.35 | — |
139
139
 
140
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."*
141
141
 
@@ -1,6 +1,28 @@
1
1
  /* tslint:disable */
2
2
  /* eslint-disable */
3
3
 
4
+ /**
5
+ * Chroma subsampling format
6
+ */
7
+ export enum ChromaSampling {
8
+ /**
9
+ * Both vertically and horizontally subsampled.
10
+ */
11
+ Cs420 = 0,
12
+ /**
13
+ * Horizontally subsampled.
14
+ */
15
+ Cs422 = 1,
16
+ /**
17
+ * Not subsampled.
18
+ */
19
+ Cs444 = 2,
20
+ /**
21
+ * Monochrome.
22
+ */
23
+ Cs400 = 3,
24
+ }
25
+
4
26
  export function decrypt(buffer: Uint8Array, password: string): Uint8Array;
5
27
 
6
28
  export function encrypt(buffer: Uint8Array, user_password: string, owner_password: string, version: number): Uint8Array;
@@ -18,3 +40,16 @@ export function is_encrypted(buffer: Uint8Array): boolean;
18
40
  export function merge(pdf_buffers: Array<any>): Uint8Array;
19
41
 
20
42
  export function split(buffer: Uint8Array, pages: Array<any>): Array<any>;
43
+
44
+ /**
45
+ * Stamp (composite) an image onto a single-page PDF.
46
+ *
47
+ * `x`, `y` are the bottom-left position in PDF user-space coordinates
48
+ * (points, origin at bottom-left by default). `width` and `height` are in
49
+ * the same units. The image is stretched to fill the given rectangle.
50
+ *
51
+ * ponytail: uses lopdf::xobject::image_from (embed_image feature) which
52
+ * supports PNG and JPEG via the image crate; the image crate is
53
+ * wasm-compatible with the png/jpeg features enabled.
54
+ */
55
+ export function stamp_image(buffer: Uint8Array, image_buffer: Uint8Array, x: number, y: number, width: number, height: number): Uint8Array;
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
- decrypt, encrypt, extract_text, get_page_count, get_pdf_info, init, is_encrypted, merge, split
8
+ ChromaSampling, decrypt, encrypt, extract_text, get_page_count, get_pdf_info, init, is_encrypted, merge, split, stamp_image
9
9
  } from "./pdf_engine_bg.js";
@@ -1,3 +1,26 @@
1
+ /**
2
+ * Chroma subsampling format
3
+ * @enum {0 | 1 | 2 | 3}
4
+ */
5
+ export const ChromaSampling = Object.freeze({
6
+ /**
7
+ * Both vertically and horizontally subsampled.
8
+ */
9
+ Cs420: 0, "0": "Cs420",
10
+ /**
11
+ * Horizontally subsampled.
12
+ */
13
+ Cs422: 1, "1": "Cs422",
14
+ /**
15
+ * Not subsampled.
16
+ */
17
+ Cs444: 2, "2": "Cs444",
18
+ /**
19
+ * Monochrome.
20
+ */
21
+ Cs400: 3, "3": "Cs400",
22
+ });
23
+
1
24
  /**
2
25
  * @param {Uint8Array} buffer
3
26
  * @param {string} password
@@ -147,6 +170,36 @@ export function split(buffer, pages) {
147
170
  }
148
171
  return takeFromExternrefTable0(ret[0]);
149
172
  }
173
+
174
+ /**
175
+ * Stamp (composite) an image onto a single-page PDF.
176
+ *
177
+ * `x`, `y` are the bottom-left position in PDF user-space coordinates
178
+ * (points, origin at bottom-left by default). `width` and `height` are in
179
+ * the same units. The image is stretched to fill the given rectangle.
180
+ *
181
+ * ponytail: uses lopdf::xobject::image_from (embed_image feature) which
182
+ * supports PNG and JPEG via the image crate; the image crate is
183
+ * wasm-compatible with the png/jpeg features enabled.
184
+ * @param {Uint8Array} buffer
185
+ * @param {Uint8Array} image_buffer
186
+ * @param {number} x
187
+ * @param {number} y
188
+ * @param {number} width
189
+ * @param {number} height
190
+ * @returns {Uint8Array}
191
+ */
192
+ export function stamp_image(buffer, image_buffer, x, y, width, height) {
193
+ const ptr0 = passArray8ToWasm0(buffer, wasm.__wbindgen_malloc);
194
+ const len0 = WASM_VECTOR_LEN;
195
+ const ptr1 = passArray8ToWasm0(image_buffer, wasm.__wbindgen_malloc);
196
+ const len1 = WASM_VECTOR_LEN;
197
+ const ret = wasm.stamp_image(ptr0, len0, ptr1, len1, x, y, width, height);
198
+ if (ret[2]) {
199
+ throw takeFromExternrefTable0(ret[1]);
200
+ }
201
+ return takeFromExternrefTable0(ret[0]);
202
+ }
150
203
  export function __wbg___wbindgen_number_get_394265ed1e1b84ee(arg0, arg1) {
151
204
  const obj = arg1;
152
205
  const ret = typeof(obj) === 'number' ? obj : undefined;
Binary file
@@ -10,6 +10,7 @@ export const init: () => [number, number];
10
10
  export const is_encrypted: (a: number, b: number) => [number, number, number];
11
11
  export const merge: (a: any) => [number, number, number];
12
12
  export const split: (a: number, b: number, c: any) => [number, number, number];
13
+ export const stamp_image: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number];
13
14
  export const __wbindgen_exn_store: (a: number) => void;
14
15
  export const __externref_table_alloc: () => number;
15
16
  export const __wbindgen_externrefs: WebAssembly.Table;
package/src/index.ts CHANGED
@@ -44,6 +44,18 @@ export async function decrypt(buffer: Uint8Array, password: string): Promise<Uin
44
44
  return result;
45
45
  }
46
46
 
47
+ export async function stampImage(
48
+ buffer: Uint8Array,
49
+ imageBuffer: Uint8Array,
50
+ x: number,
51
+ y: number,
52
+ width: number,
53
+ height: number,
54
+ ): Promise<Uint8Array> {
55
+ const result = await wasm.stamp_image(buffer, imageBuffer, x, y, width, height);
56
+ return result;
57
+ }
58
+
47
59
  export class PdfEngineImpl {
48
60
  constructor() {
49
61
  this.initialized = false;
@@ -96,6 +108,18 @@ export class PdfEngineImpl {
96
108
  await this.init();
97
109
  return await decrypt(buffer, password);
98
110
  }
111
+
112
+ async stampImage(
113
+ buffer: Uint8Array,
114
+ imageBuffer: Uint8Array,
115
+ x: number,
116
+ y: number,
117
+ width: number,
118
+ height: number,
119
+ ): Promise<Uint8Array> {
120
+ await this.init();
121
+ return await stampImage(buffer, imageBuffer, x, y, width, height);
122
+ }
99
123
  }
100
124
 
101
125
  const pdfEngine = new PdfEngineImpl();
package/src/lib.rs CHANGED
@@ -376,6 +376,53 @@ pub fn decrypt(buffer: &[u8], password: &str) -> Result<Uint8Array, JsValue> {
376
376
  Ok(Uint8Array::new_from_slice(&buf))
377
377
  }
378
378
 
379
+ /// Stamp (composite) an image onto a single-page PDF.
380
+ ///
381
+ /// `x`, `y` are the bottom-left position in PDF user-space coordinates
382
+ /// (points, origin at bottom-left by default). `width` and `height` are in
383
+ /// the same units. The image is stretched to fill the given rectangle.
384
+ ///
385
+ /// ponytail: uses lopdf::xobject::image_from (embed_image feature) which
386
+ /// supports PNG and JPEG via the image crate; the image crate is
387
+ /// wasm-compatible with the png/jpeg features enabled.
388
+ #[wasm_bindgen]
389
+ pub fn stamp_image(
390
+ buffer: &[u8],
391
+ image_buffer: &[u8],
392
+ x: f64,
393
+ y: f64,
394
+ width: f64,
395
+ height: f64,
396
+ ) -> Result<Uint8Array, JsValue> {
397
+ let mut doc = Document::load_mem(buffer)
398
+ .map_err(|e| JsValue::from_str(&format!("PDF load error: {}", e)))?;
399
+
400
+ let pages = doc.get_pages();
401
+ if pages.is_empty() {
402
+ return Err(JsValue::from_str("PDF has no pages"));
403
+ }
404
+ let page_id = *pages.values().next().unwrap();
405
+
406
+ // ponytail: pdf-lib creates pages without a /Contents key; insert_image
407
+ // requires one. Add an empty stream if missing so the page can accept
408
+ // the image overlay.
409
+ if doc.get_page_content(page_id).is_empty() {
410
+ doc.add_page_contents(page_id, Vec::new())
411
+ .map_err(|e| JsValue::from_str(&format!("Page content init error: {}", e)))?;
412
+ }
413
+
414
+ let img_stream = lopdf::xobject::image_from(image_buffer.to_vec())
415
+ .map_err(|e| JsValue::from_str(&format!("Image decode error: {}", e)))?;
416
+
417
+ doc.insert_image(page_id, img_stream, (x as f32, y as f32), (width as f32, height as f32))
418
+ .map_err(|e| JsValue::from_str(&format!("Insert image error: {}", e)))?;
419
+
420
+ let mut buf = Vec::new();
421
+ doc.save_to(&mut buf)
422
+ .map_err(|e| JsValue::from_str(&format!("Save error: {}", e)))?;
423
+ Ok(Uint8Array::new_from_slice(&buf))
424
+ }
425
+
379
426
  #[cfg(test)]
380
427
  mod tests {
381
428
  use super::*;