qpdf-compress 0.1.0 → 0.2.0

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,8 +17,11 @@ import { compress } from 'qpdf-compress';
17
17
  // lossless — optimize without touching image quality
18
18
  const optimized = await compress(pdfBuffer, { mode: 'lossless' });
19
19
 
20
- // lossy — recompress images as JPEG for maximum savings
21
- const smaller = await compress(pdfBuffer, { mode: 'lossy', quality: 50 });
20
+ // lossy — auto quality, downscale to 75 DPI, strip metadata
21
+ const smaller = await compress(pdfBuffer, { mode: 'lossy' });
22
+
23
+ // lossy with explicit quality
24
+ const tiny = await compress(pdfBuffer, { mode: 'lossy', quality: 50 });
22
25
  ```
23
26
 
24
27
  ## 💡 Why qpdf-compress?
@@ -28,6 +31,7 @@ const smaller = await compress(pdfBuffer, { mode: 'lossy', quality: 50 });
28
31
  - Native C++ — no WASM overhead, no shell-out to CLI tools
29
32
  - Non-blocking — all operations run off the main thread via N-API AsyncWorker
30
33
  - Multi-pass optimization — image dedup, JPEG Huffman optimization, Flate level 9
34
+ - Smart defaults — DPI downscaling, metadata stripping, adaptive JPEG quality
31
35
 
32
36
  **🛠️ Developer experience**
33
37
 
@@ -52,16 +56,20 @@ const smaller = await compress(pdfBuffer, { mode: 'lossy', quality: 50 });
52
56
 
53
57
  ### 📊 How it compares
54
58
 
55
- | | **qpdf-compress** | qpdf CLI | Ghostscript |
56
- | ------------------------- | ----------------------- | ----------------- | ----------------- |
57
- | Integration | Native Node.js addon | Shell exec | Shell exec |
58
- | Async I/O | ✅ Non-blocking | ❌ Blocks on exec | ❌ Blocks on exec |
59
- | Image deduplication | ✅ | ❌ | ❌ |
60
- | JPEG Huffman optimization | ✅ Lossless (libjpeg) | ❌ | ❌ |
61
- | Lossy image compression | ✅ Configurable quality | ❌ | ✅ |
62
- | PDF repair | ✅ Automatic | ✅ Manual flag | ⚠️ Partial |
63
- | License | Apache-2.0 | Apache-2.0 | AGPL-3.0 ⚠️ |
64
- | Dependencies | None¹ | System binary | System binary |
59
+ | | **qpdf-compress** | qpdf CLI | Ghostscript |
60
+ | ------------------------- | ------------------------ | ----------------- | ----------------- |
61
+ | Integration | Native Node.js addon | Shell exec | Shell exec |
62
+ | Async I/O | ✅ Non-blocking | ❌ Blocks on exec | ❌ Blocks on exec |
63
+ | Image deduplication | ✅ | ❌ | ❌ |
64
+ | JPEG Huffman optimization | ✅ Lossless (libjpeg) | ❌ | ❌ |
65
+ | Lossy image compression | ✅ Auto or fixed quality | ❌ | ✅ |
66
+ | CMYK → RGB conversion | ✅ Automatic | | |
67
+ | DPI downscaling | ✅ Configurable | | |
68
+ | Metadata stripping | Default on | Manual flag | ✅ |
69
+ | Unused font removal | ✅ Automatic | ❌ | ❌ |
70
+ | PDF repair | ✅ Automatic | ✅ Manual flag | ⚠️ Partial |
71
+ | License | Apache-2.0 | Apache-2.0 | AGPL-3.0 ⚠️ |
72
+ | Dependencies | None¹ | System binary | System binary |
65
73
 
66
74
  ¹ QPDF is statically linked — no runtime dependencies. Prebuilt binaries downloaded at install.
67
75
 
@@ -111,12 +119,19 @@ import { compress } from 'qpdf-compress';
111
119
  // lossless — optimize streams without touching image quality
112
120
  const optimized = await compress(pdfBuffer, { mode: 'lossless' });
113
121
 
114
- // lossy — recompress images as JPEG (default quality: 75)
122
+ // lossy — auto quality per image (skips JPEGs ≤ q90, encodes rest at q85)
115
123
  const smaller = await compress(pdfBuffer, { mode: 'lossy' });
116
124
 
117
- // lossy with custom quality (1–100)
125
+ // lossy with explicit quality (1–100)
118
126
  const tiny = await compress(pdfBuffer, { mode: 'lossy', quality: 50 });
119
127
 
128
+ // control DPI downscaling (default: 75, 0 = disabled)
129
+ const highRes = await compress(pdfBuffer, { mode: 'lossless', maxDpi: 150 });
130
+ const noDpi = await compress(pdfBuffer, { mode: 'lossless', maxDpi: 0 });
131
+
132
+ // keep metadata (stripped by default)
133
+ const withMeta = await compress(pdfBuffer, { mode: 'lossless', stripMetadata: false });
134
+
120
135
  // file path input (avoids copying into memory twice)
121
136
  const result = await compress('/path/to/file.pdf', { mode: 'lossless' });
122
137
 
@@ -135,31 +150,39 @@ const fixed = await compress(damagedBuffer, { mode: 'lossless' });
135
150
 
136
151
  Compresses a PDF document. Automatically repairs damaged PDFs.
137
152
 
138
- | Parameter | Type | Description |
139
- | ----------------- | ----------------------- | -------------------------------------------------- |
140
- | `input` | `Buffer \| string` | PDF data or file path |
141
- | `options.mode` | `'lossy' \| 'lossless'` | Compression mode |
142
- | `options.quality` | `number` | JPEG quality 1–100 (lossy only, default: 75) |
143
- | `options.output` | `string` | Write to file path instead of returning a `Buffer` |
153
+ | Parameter | Type | Description |
154
+ | ----------------------- | ----------------------- | -------------------------------------------------------------------- |
155
+ | `input` | `Buffer \| string` | PDF data or file path |
156
+ | `options.mode` | `'lossy' \| 'lossless'` | Compression mode |
157
+ | `options.quality` | `number` | JPEG quality 1–100 (lossy only). Omit for auto quality (recommended) |
158
+ | `options.maxDpi` | `number` | Downscale images exceeding this DPI. Default: `75`. `0` = disabled |
159
+ | `options.stripMetadata` | `boolean` | Remove XMP metadata, document info, and thumbnails. Default: `true` |
160
+ | `options.output` | `string` | Write to file path instead of returning a `Buffer` |
144
161
 
145
- **Lossless mode:**
162
+ **Both modes:**
146
163
 
147
164
  - Deduplicates identical images across pages
148
165
  - Optimizes embedded JPEG Huffman tables (2–15% savings, zero quality loss)
149
166
  - Recompresses all decodable streams with Flate level 9
150
167
  - Generates object streams for smaller metadata overhead
151
- - Removes unreferenced objects
168
+ - Removes unreferenced objects and unused fonts
169
+ - Downscales images exceeding `maxDpi` (default: 75 DPI)
170
+ - Strips XMP metadata, document info, and thumbnails (default: on)
171
+ - Converts CMYK and ICCBased color spaces to RGB
172
+ - Automatically repairs damaged PDFs
152
173
 
153
- **Lossy mode** (in addition to lossless optimizations):
174
+ **Lossy mode** (in addition to the above):
154
175
 
155
- - Extracts 8-bit RGB and grayscale images
156
- - Recompresses as JPEG at the specified quality
176
+ - Extracts 8-bit RGB, grayscale, and CMYK images
177
+ - **Auto quality** (default): skips existing JPEGs at q ≤ 90, encodes the rest at q85
178
+ - **Explicit quality**: recompresses all images at the specified quality (1–100)
157
179
  - Only replaces images where JPEG is actually smaller
158
- - Skips tiny images (< 50×50 px), CMYK, and indexed color
180
+ - Skips re-encoding when estimated quality is already at or below target
181
+ - Skips tiny images (< 50×50 px)
159
182
 
160
183
  ## ⚙️ How it works
161
184
 
162
- This package embeds [QPDF](https://github.com/qpdf/qpdf) (v12.3.2) as a statically linked C++ library, exposed to Node.js via N-API. Lossless JPEG optimization uses [libjpeg-turbo](https://libjpeg-turbo.org/) at the DCT coefficient level. Image recompression in lossy mode uses [stb_image_write](https://github.com/nothings/stb) for JPEG encoding.
185
+ This package embeds [QPDF](https://github.com/qpdf/qpdf) (v12.3.2) as a statically linked C++ library, exposed to Node.js via N-API. Lossless JPEG optimization uses [libjpeg-turbo](https://libjpeg-turbo.org/) at the DCT coefficient level. Image recompression in lossy mode also uses libjpeg-turbo for JPEG encoding.
163
186
 
164
187
  All operations run in a background thread via `Napi::AsyncWorker`, so the event loop is never blocked.
165
188
 
package/binding.gyp CHANGED
@@ -4,7 +4,8 @@
4
4
  "target_name": "qpdf_compress",
5
5
  "sources": [
6
6
  "src/qpdf_addon.cc",
7
- "src/stb_impl.cc"
7
+ "src/jpeg.cc",
8
+ "src/images.cc"
8
9
  ],
9
10
  "include_dirs": [
10
11
  "<!@(node -p \"require('node-addon-api').include\")",
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAe,MAAM,YAAY,CAAC;AAY/D,KAAK,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAEhC;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CACtB,KAAK,EAAE,QAAQ,EACf,OAAO,EAAE,eAAe,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAC5C,OAAO,CAAC,IAAI,CAAC,CAAC;AACjB,wBAAgB,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;AA4BrF,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAe,MAAM,YAAY,CAAC;AAY/D,KAAK,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAEhC;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CACtB,KAAK,EAAE,QAAQ,EACf,OAAO,EAAE,eAAe,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAC5C,OAAO,CAAC,IAAI,CAAC,CAAC;AACjB,wBAAgB,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;AAgCrF,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -26,13 +26,17 @@ export async function compress(input, options) {
26
26
  if (mode !== 'lossy' && mode !== 'lossless') {
27
27
  throw new TypeError("Mode must be 'lossy' or 'lossless'");
28
28
  }
29
- const quality = options.quality ?? 75;
30
- if (quality < 1 || quality > 100) {
29
+ const quality = options.quality ?? 0;
30
+ if (quality !== 0 && (quality < 1 || quality > 100)) {
31
31
  throw new RangeError('Quality must be between 1 and 100');
32
32
  }
33
+ const maxDpi = options.maxDpi ?? 75;
34
+ const stripMetadata = options.stripMetadata ?? true;
33
35
  return addon.compress(input, {
34
36
  mode,
35
37
  quality,
38
+ ...(maxDpi > 0 ? { maxDpi } : {}),
39
+ ...(stripMetadata ? { stripMetadata: true } : {}),
36
40
  ...(options.output ? { output: options.output } : {}),
37
41
  });
38
42
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC/C,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,MAAM,QAAQ,GAAG,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;AAC9D,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;IACjC,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;AAC7D,CAAC;AAED,MAAM,KAAK,GAAgB,OAAO,CAAC,qCAAqC,CAAC,CAAC;AAmB1E,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,KAAe,EAAE,OAAwB;IACtE,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,SAAS,CAAC,8BAA8B,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACrC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,SAAS,CAAC,4BAA4B,CAAC,CAAC;QACpD,CAAC;IACH,CAAC;SAAM,CAAC;QACN,MAAM,IAAI,SAAS,CAAC,4CAA4C,CAAC,CAAC;IACpE,CAAC;IACD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC1B,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;QAC5C,MAAM,IAAI,SAAS,CAAC,oCAAoC,CAAC,CAAC;IAC5D,CAAC;IACD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;IACtC,IAAI,OAAO,GAAG,CAAC,IAAI,OAAO,GAAG,GAAG,EAAE,CAAC;QACjC,MAAM,IAAI,UAAU,CAAC,mCAAmC,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE;QAC3B,IAAI;QACJ,OAAO;QACP,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACtD,CAA2B,CAAC;AAC/B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC/C,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,MAAM,QAAQ,GAAG,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;AAC9D,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;IACjC,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;AAC7D,CAAC;AAED,MAAM,KAAK,GAAgB,OAAO,CAAC,qCAAqC,CAAC,CAAC;AAmB1E,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,KAAe,EAAE,OAAwB;IACtE,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,SAAS,CAAC,8BAA8B,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACrC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,SAAS,CAAC,4BAA4B,CAAC,CAAC;QACpD,CAAC;IACH,CAAC;SAAM,CAAC;QACN,MAAM,IAAI,SAAS,CAAC,4CAA4C,CAAC,CAAC;IACpE,CAAC;IACD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC1B,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;QAC5C,MAAM,IAAI,SAAS,CAAC,oCAAoC,CAAC,CAAC;IAC5D,CAAC;IACD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,CAAC,CAAC;IACrC,IAAI,OAAO,KAAK,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,IAAI,OAAO,GAAG,GAAG,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,UAAU,CAAC,mCAAmC,CAAC,CAAC;IAC5D,CAAC;IACD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC;IACpC,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,IAAI,CAAC;IACpD,OAAO,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE;QAC3B,IAAI;QACJ,OAAO;QACP,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjD,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACtD,CAA2B,CAAC;AAC/B,CAAC"}
package/dist/types.d.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  export interface CompressOptions {
2
2
  /** Compression mode. */
3
3
  readonly mode: 'lossy' | 'lossless';
4
- /** JPEG quality for lossy mode (1–100). Default: 75. */
4
+ /** JPEG quality for lossy mode (1–100). When omitted, automatically determines optimal quality per image (capped at 85). */
5
5
  readonly quality?: number;
6
+ /** Maximum image DPI. Images exceeding this are downscaled. 0 = no limit. Default: 75. */
7
+ readonly maxDpi?: number;
8
+ /** Remove XMP metadata, document info, and thumbnails. Default: true. */
9
+ readonly stripMetadata?: boolean;
6
10
  /** Write to this file path instead of returning a Buffer. */
7
11
  readonly output?: string;
8
12
  }
@@ -10,6 +14,8 @@ export interface NativeAddon {
10
14
  compress(input: Buffer | string, options: {
11
15
  mode: 'lossy' | 'lossless';
12
16
  quality: number;
17
+ maxDpi?: number;
18
+ stripMetadata?: boolean;
13
19
  output?: string;
14
20
  }): Promise<Buffer | undefined>;
15
21
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../lib/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,wBAAwB;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,UAAU,CAAC;IACpC,wDAAwD;IACxD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CACN,KAAK,EAAE,MAAM,GAAG,MAAM,EACtB,OAAO,EAAE;QAAE,IAAI,EAAE,OAAO,GAAG,UAAU,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GACxE,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAChC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../lib/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,wBAAwB;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,UAAU,CAAC;IACpC,4HAA4H;IAC5H,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,0FAA0F;IAC1F,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CACN,KAAK,EAAE,MAAM,GAAG,MAAM,EACtB,OAAO,EAAE;QACP,IAAI,EAAE,OAAO,GAAG,UAAU,CAAC;QAC3B,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GACA,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAChC"}
package/lib/index.ts CHANGED
@@ -46,13 +46,17 @@ export async function compress(input: PdfInput, options: CompressOptions): Promi
46
46
  if (mode !== 'lossy' && mode !== 'lossless') {
47
47
  throw new TypeError("Mode must be 'lossy' or 'lossless'");
48
48
  }
49
- const quality = options.quality ?? 75;
50
- if (quality < 1 || quality > 100) {
49
+ const quality = options.quality ?? 0;
50
+ if (quality !== 0 && (quality < 1 || quality > 100)) {
51
51
  throw new RangeError('Quality must be between 1 and 100');
52
52
  }
53
+ const maxDpi = options.maxDpi ?? 75;
54
+ const stripMetadata = options.stripMetadata ?? true;
53
55
  return addon.compress(input, {
54
56
  mode,
55
57
  quality,
58
+ ...(maxDpi > 0 ? { maxDpi } : {}),
59
+ ...(stripMetadata ? { stripMetadata: true } : {}),
56
60
  ...(options.output ? { output: options.output } : {}),
57
61
  }) as Promise<Buffer | void>;
58
62
  }
package/lib/types.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  export interface CompressOptions {
2
2
  /** Compression mode. */
3
3
  readonly mode: 'lossy' | 'lossless';
4
- /** JPEG quality for lossy mode (1–100). Default: 75. */
4
+ /** JPEG quality for lossy mode (1–100). When omitted, automatically determines optimal quality per image (capped at 85). */
5
5
  readonly quality?: number;
6
+ /** Maximum image DPI. Images exceeding this are downscaled. 0 = no limit. Default: 75. */
7
+ readonly maxDpi?: number;
8
+ /** Remove XMP metadata, document info, and thumbnails. Default: true. */
9
+ readonly stripMetadata?: boolean;
6
10
  /** Write to this file path instead of returning a Buffer. */
7
11
  readonly output?: string;
8
12
  }
@@ -10,6 +14,12 @@ export interface CompressOptions {
10
14
  export interface NativeAddon {
11
15
  compress(
12
16
  input: Buffer | string,
13
- options: { mode: 'lossy' | 'lossless'; quality: number; output?: string },
17
+ options: {
18
+ mode: 'lossy' | 'lossless';
19
+ quality: number; // 0 = auto, 1–100 = fixed
20
+ maxDpi?: number;
21
+ stripMetadata?: boolean;
22
+ output?: string;
23
+ },
14
24
  ): Promise<Buffer | undefined>;
15
25
  }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "qpdf-compress",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Native PDF compression for Node.js, powered by QPDF",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "engines": {
8
- "node": ">=18.0.0"
8
+ "node": ">=20.11.0"
9
9
  },
10
10
  "keywords": [
11
11
  "pdf",
@@ -88,4 +88,4 @@
88
88
  "typescript-eslint": "^8.57.2",
89
89
  "vitest": "^4.1.2"
90
90
  }
91
- }
91
+ }
@@ -114,6 +114,11 @@ if (process.platform === 'win32') {
114
114
  `-DCMAKE_TOOLCHAIN_FILE=${join(vcpkgRoot, 'scripts', 'buildsystems', 'vcpkg.cmake')}`,
115
115
  `-DVCPKG_TARGET_TRIPLET=${triplet}`,
116
116
  );
117
+
118
+ // cross-compile for ARM64 when triplet indicates it
119
+ if (triplet.startsWith('arm64')) {
120
+ cmakeArgs.push('-A', 'ARM64');
121
+ }
117
122
  }
118
123
  // force static CRT (/MT) to match node-gyp
119
124
  cmakeArgs.push('-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded');
@@ -6,9 +6,10 @@
6
6
  * Musl (Alpine) naming: qpdf-compress-v{version}-linux-musl-{arch}.tar.gz
7
7
  * Contents: build/Release/qpdf_compress.node
8
8
  */
9
- import { existsSync, mkdirSync, createWriteStream, readFileSync, unlinkSync } from 'node:fs';
9
+ import { execFileSync, execSync } from 'node:child_process';
10
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, unlinkSync } from 'node:fs';
10
11
  import { join } from 'node:path';
11
- import { execSync, execFileSync } from 'node:child_process';
12
+ import { Readable } from 'node:stream';
12
13
  import { pipeline } from 'node:stream/promises';
13
14
 
14
15
  const root = join(import.meta.dirname, '..');
@@ -70,7 +71,7 @@ async function tryDownload() {
70
71
  const body = res.body;
71
72
  if (!body) return false;
72
73
 
73
- await pipeline(body, fileStream);
74
+ await pipeline(Readable.fromWeb(body), fileStream);
74
75
 
75
76
  // extract tar.gz into project root
76
77
  execFileSync('tar', ['xzf', tmpTar, '-C', root], { stdio: 'inherit' });