qpdf-compress 0.1.3 → 0.4.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
@@ -14,11 +14,11 @@ Native PDF compression for Node.js — powered by [QPDF](https://qpdf.sourceforg
14
14
  ```typescript
15
15
  import { compress } from 'qpdf-compress';
16
16
 
17
- // lossless — optimize without touching image quality
18
- const optimized = await compress(pdfBuffer, { mode: 'lossless' });
17
+ // lossless (default) pure structural optimization
18
+ const optimized = await compress(pdfBuffer);
19
19
 
20
- // lossy — recompress images as JPEG for maximum savings
21
- const smaller = await compress(pdfBuffer, { mode: 'lossy', quality: 50 });
20
+ // lossy — image recompression + downscale to 72 DPI
21
+ const smaller = await compress(pdfBuffer, { lossy: true });
22
22
  ```
23
23
 
24
24
  ## 💡 Why qpdf-compress?
@@ -28,6 +28,7 @@ const smaller = await compress(pdfBuffer, { mode: 'lossy', quality: 50 });
28
28
  - Native C++ — no WASM overhead, no shell-out to CLI tools
29
29
  - Non-blocking — all operations run off the main thread via N-API AsyncWorker
30
30
  - Multi-pass optimization — image dedup, JPEG Huffman optimization, Flate level 9
31
+ - Smart defaults — metadata stripping, image dedup, lossless JPEG Huffman optimization
31
32
 
32
33
  **🛠️ Developer experience**
33
34
 
@@ -52,16 +53,28 @@ const smaller = await compress(pdfBuffer, { mode: 'lossy', quality: 50 });
52
53
 
53
54
  ### 📊 How it compares
54
55
 
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 |
56
+ | | **qpdf-compress** | qpdf CLI | Ghostscript |
57
+ | ------------------------- | --------------------- | ----------------- | ----------------- |
58
+ | Integration | Native Node.js addon | Shell exec | Shell exec |
59
+ | Async I/O | ✅ Non-blocking | ❌ Blocks on exec | ❌ Blocks on exec |
60
+ | Image deduplication | ✅ | ❌ | ❌ |
61
+ | JPEG Huffman optimization | ✅ Lossless (libjpeg) | ❌ | ❌ |
62
+ | Lossy image compression | ✅ Auto quality | ❌ | ✅ |
63
+ | CMYK → RGB conversion | ✅ Automatic | | |
64
+ | DPI downscaling | ✅ Lossy mode | | |
65
+ | Grayscale detection | Automatic | ❌ | |
66
+ | Bitonal conversion | ✅ Automatic | ❌ | ❌ |
67
+ | Font subsetting | ✅ TrueType glyph | ❌ | ❌ |
68
+ | Unused font removal | ✅ Automatic | ❌ | ❌ |
69
+ | ICC profile stripping | ✅ Automatic | ❌ | ❌ |
70
+ | Form flattening | ✅ Automatic | ❌ | ❌ |
71
+ | Stream deduplication | ✅ Automatic | ❌ | ❌ |
72
+ | Content minification | ✅ Automatic | ❌ | ❌ |
73
+ | JS/embedded file removal | ✅ Automatic | ❌ | ❌ |
74
+ | Metadata stripping | ✅ Default on | ✅ Manual flag | ✅ |
75
+ | PDF repair | ✅ Automatic | ✅ Manual flag | ⚠️ Partial |
76
+ | License | Apache-2.0 | Apache-2.0 | AGPL-3.0 ⚠️ |
77
+ | Dependencies | None¹ | System binary | System binary |
65
78
 
66
79
  ¹ QPDF is statically linked — no runtime dependencies. Prebuilt binaries downloaded at install.
67
80
 
@@ -108,61 +121,101 @@ vcpkg install zlib libjpeg-turbo --triplet x64-windows-static
108
121
  ```typescript
109
122
  import { compress } from 'qpdf-compress';
110
123
 
111
- // lossless — optimize streams without touching image quality
112
- const optimized = await compress(pdfBuffer, { mode: 'lossless' });
124
+ // lossless (default) pure structural optimization, no image re-encoding
125
+ const optimized = await compress(pdfBuffer);
113
126
 
114
- // lossy — recompress images as JPEG (default quality: 75)
115
- const smaller = await compress(pdfBuffer, { mode: 'lossy' });
127
+ // lossy — image recompression + downscale to 72 DPI
128
+ const smaller = await compress(pdfBuffer, { lossy: true });
116
129
 
117
- // lossy with custom quality (1–100)
118
- const tiny = await compress(pdfBuffer, { mode: 'lossy', quality: 50 });
130
+ // keep metadata (stripped by default)
131
+ const withMeta = await compress(pdfBuffer, { stripMetadata: false });
119
132
 
120
133
  // file path input (avoids copying into memory twice)
121
- const result = await compress('/path/to/file.pdf', { mode: 'lossless' });
134
+ const result = await compress('/path/to/file.pdf');
122
135
 
123
136
  // write directly to file instead of returning a Buffer
124
- await compress(pdfBuffer, { mode: 'lossless', output: '/path/to/output.pdf' });
137
+ await compress(pdfBuffer, { output: '/path/to/output.pdf' });
125
138
 
126
139
  // damaged PDFs are automatically repaired during compression
127
- const fixed = await compress(damagedBuffer, { mode: 'lossless' });
140
+ const fixed = await compress(damagedBuffer);
128
141
  ```
129
142
 
130
143
  ## 📖 API
131
144
 
132
- ### `compress(input, options): Promise<Buffer>`
145
+ ### `compress(input, options?): Promise<Buffer>`
133
146
 
134
147
  ### `compress(input, options & { output: string }): Promise<void>`
135
148
 
136
149
  Compresses a PDF document. Automatically repairs damaged PDFs.
137
150
 
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` |
151
+ | Parameter | Type | Description |
152
+ | ----------------------- | ------------------ | ------------------------------------------------------------------- |
153
+ | `input` | `Buffer \| string` | PDF data or file path |
154
+ | `options.lossy` | `boolean` | Enable lossy compression. Default: `false` |
155
+ | `options.stripMetadata` | `boolean` | Remove XMP metadata, document info, and thumbnails. Default: `true` |
156
+ | `options.output` | `string` | Write to file path instead of returning a `Buffer` |
144
157
 
145
- **Lossless mode:**
158
+ **Both modes:**
146
159
 
147
- - Deduplicates identical images across pages
160
+ - Deduplicates identical images and non-image streams across pages
161
+ - Detects and converts RGB images that are actually grayscale (3× raw data reduction)
162
+ - Converts effectively black-and-white grayscale images to 1-bit (8× raw data reduction)
148
163
  - Optimizes embedded JPEG Huffman tables (2–15% savings, zero quality loss)
164
+ - Optimizes soft mask (transparency) JPEG streams
165
+ - Removes unused font resources from pages
166
+ - Subsets TrueType fonts — strips unused glyph outlines from font programs
167
+ - Strips ICC color profiles, replacing with Device equivalents
168
+ - Flattens interactive forms (AcroForm) into page content
169
+ - Flattens page tree (pushes inherited attributes to pages)
170
+ - Coalesces multiple content streams per page into one
171
+ - Minifies content streams (whitespace normalization, numeric formatting)
172
+ - Strips embedded files and JavaScript actions
149
173
  - Recompresses all decodable streams with Flate level 9
150
174
  - Generates object streams for smaller metadata overhead
151
175
  - Removes unreferenced objects
176
+ - Strips XMP metadata, document info, and thumbnails (default: on)
177
+ - Automatically repairs damaged PDFs
152
178
 
153
- **Lossy mode** (in addition to lossless optimizations):
179
+ **Lossless (default):**
154
180
 
155
- - Extracts 8-bit RGB and grayscale images
156
- - Recompresses as JPEG at the specified quality
157
- - Only replaces images where JPEG is actually smaller
158
- - Skips tiny images (< 50×50 px), CMYK, and indexed color
181
+ - Pure structural optimization no image re-encoding or downscaling
182
+ - Visually identical to the original
183
+
184
+ **Lossy** (`lossy: true`):
185
+
186
+ - Re-encodes JPEGs above q65 at q75 (skips images already at or below target)
187
+ - Downscales images exceeding 72 DPI using CTM-based rendered size detection
188
+ - Converts CMYK and ICCBased color spaces to RGB
189
+ - Only replaces images where the result is actually smaller
190
+ - Skips tiny images (< 50×50 px)
159
191
 
160
192
  ## ⚙️ How it works
161
193
 
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 also uses libjpeg-turbo for JPEG encoding.
194
+ 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. TrueType font subsetting is handled by a custom binary parser that reads cmap tables, resolves composite glyph dependencies, and rebuilds glyf/loca/hmtx tables with only the used glyphs.
163
195
 
164
196
  All operations run in a background thread via `Napi::AsyncWorker`, so the event loop is never blocked.
165
197
 
198
+ ### Compression pipeline (execution order)
199
+
200
+ 1. Deduplicate identical images
201
+ 2. Convert grayscale RGB images to DeviceGray
202
+ 3. Convert bitonal grayscale images to 1-bit
203
+ 4. Flatten page tree (push inherited attributes)
204
+ 5. _(lossy only)_ Re-encode high-quality JPEGs at q75
205
+ 6. _(lossy only)_ Downscale images above 72 DPI
206
+ 7. Optimize existing JPEG Huffman tables
207
+ 8. Optimize soft mask JPEG streams
208
+ 9. Remove unused font resources
209
+ 10. Subset TrueType fonts (strip unused glyphs)
210
+ 11. Strip ICC color profiles
211
+ 12. Flatten interactive forms into page content
212
+ 13. Coalesce multiple content streams per page
213
+ 14. Minify content streams
214
+ 15. Deduplicate identical non-image streams
215
+ 16. Strip embedded files and JavaScript
216
+ 17. _(optional)_ Strip metadata
217
+ 18. QPDFWriter: Flate 9, object streams, unreferenced object removal
218
+
166
219
  ## License
167
220
 
168
221
  Apache-2.0
package/binding.gyp CHANGED
@@ -5,7 +5,9 @@
5
5
  "sources": [
6
6
  "src/qpdf_addon.cc",
7
7
  "src/jpeg.cc",
8
- "src/images.cc"
8
+ "src/images.cc",
9
+ "src/optimize.cc",
10
+ "src/font_subset.cc"
9
11
  ],
10
12
  "include_dirs": [
11
13
  "<!@(node -p \"require('node-addon-api').include\")",
package/dist/index.d.ts CHANGED
@@ -3,16 +3,18 @@ type PdfInput = Buffer | string;
3
3
  /**
4
4
  * Compresses a PDF document. Automatically repairs damaged PDFs.
5
5
  *
6
- * In lossless mode, deduplicates images, optimizes embedded JPEG Huffman
7
- * tables, recompresses all streams with Flate level 9, generates object
8
- * streams, and removes unreferenced objects.
6
+ * By default (lossless), deduplicates images, re-encodes very high quality
7
+ * JPEGs (q91+) at q85, downscales images to 150 DPI, optimizes embedded
8
+ * JPEG Huffman tables, recompresses all streams with Flate level 9,
9
+ * generates object streams, and removes unreferenced objects.
9
10
  *
10
- * In lossy mode, additionally recompresses embedded images as JPEG at the
11
- * specified quality. Text, vectors, and fonts are preserved.
11
+ * With `lossy: true`, uses more aggressive image re-encoding (skips JPEGs
12
+ * at q65 or below, re-encodes the rest at q75) and downscales to 72 DPI.
13
+ * Text, vectors, and fonts are preserved.
12
14
  */
13
15
  export declare function compress(input: PdfInput, options: CompressOptions & {
14
16
  output: string;
15
17
  }): Promise<void>;
16
- export declare function compress(input: PdfInput, options: CompressOptions): Promise<Buffer>;
18
+ export declare function compress(input: PdfInput, options?: CompressOptions): Promise<Buffer>;
17
19
  export type { CompressOptions } from './types.js';
18
20
  //# sourceMappingURL=index.d.ts.map
@@ -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;;;;;;;;;;;GAWG;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,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;AAqBtF,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -22,18 +22,11 @@ export async function compress(input, options) {
22
22
  else {
23
23
  throw new TypeError('Input must be a Buffer or file path string');
24
24
  }
25
- const mode = options.mode;
26
- if (mode !== 'lossy' && mode !== 'lossless') {
27
- throw new TypeError("Mode must be 'lossy' or 'lossless'");
28
- }
29
- const quality = options.quality ?? 75;
30
- if (quality < 1 || quality > 100) {
31
- throw new RangeError('Quality must be between 1 and 100');
32
- }
25
+ const stripMetadata = options?.stripMetadata ?? true;
33
26
  return addon.compress(input, {
34
- mode,
35
- quality,
36
- ...(options.output ? { output: options.output } : {}),
27
+ ...(options?.lossy ? { lossy: true } : {}),
28
+ ...(stripMetadata ? { stripMetadata: true } : {}),
29
+ ...(options?.output ? { output: options.output } : {}),
37
30
  });
38
31
  }
39
32
  //# sourceMappingURL=index.js.map
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;AAqB1E,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,KAAe,EAAE,OAAyB;IACvE,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,aAAa,GAAG,OAAO,EAAE,aAAa,IAAI,IAAI,CAAC;IACrD,OAAO,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE;QAC3B,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1C,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjD,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACvD,CAA2B,CAAC;AAC/B,CAAC"}
package/dist/types.d.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  export interface CompressOptions {
2
- /** Compression mode. */
3
- readonly mode: 'lossy' | 'lossless';
4
- /** JPEG quality for lossy mode (1–100). Default: 75. */
5
- readonly quality?: number;
2
+ /** Enable lossy compression for more aggressive size reduction. Default: false. */
3
+ readonly lossy?: boolean;
4
+ /** Remove XMP metadata, document info, and thumbnails. Default: true. */
5
+ readonly stripMetadata?: boolean;
6
6
  /** Write to this file path instead of returning a Buffer. */
7
7
  readonly output?: string;
8
8
  }
9
9
  export interface NativeAddon {
10
10
  compress(input: Buffer | string, options: {
11
- mode: 'lossy' | 'lossless';
12
- quality: number;
11
+ lossy?: boolean;
12
+ stripMetadata?: boolean;
13
13
  output?: string;
14
14
  }): Promise<Buffer | undefined>;
15
15
  }
@@ -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,mFAAmF;IACnF,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,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,KAAK,CAAC,EAAE,OAAO,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
@@ -18,19 +18,21 @@ type PdfInput = Buffer | string;
18
18
  /**
19
19
  * Compresses a PDF document. Automatically repairs damaged PDFs.
20
20
  *
21
- * In lossless mode, deduplicates images, optimizes embedded JPEG Huffman
22
- * tables, recompresses all streams with Flate level 9, generates object
23
- * streams, and removes unreferenced objects.
21
+ * By default (lossless), deduplicates images, re-encodes very high quality
22
+ * JPEGs (q91+) at q85, downscales images to 150 DPI, optimizes embedded
23
+ * JPEG Huffman tables, recompresses all streams with Flate level 9,
24
+ * generates object streams, and removes unreferenced objects.
24
25
  *
25
- * In lossy mode, additionally recompresses embedded images as JPEG at the
26
- * specified quality. Text, vectors, and fonts are preserved.
26
+ * With `lossy: true`, uses more aggressive image re-encoding (skips JPEGs
27
+ * at q65 or below, re-encodes the rest at q75) and downscales to 72 DPI.
28
+ * Text, vectors, and fonts are preserved.
27
29
  */
28
30
  export function compress(
29
31
  input: PdfInput,
30
32
  options: CompressOptions & { output: string },
31
33
  ): Promise<void>;
32
- export function compress(input: PdfInput, options: CompressOptions): Promise<Buffer>;
33
- export async function compress(input: PdfInput, options: CompressOptions): Promise<Buffer | void> {
34
+ export function compress(input: PdfInput, options?: CompressOptions): Promise<Buffer>;
35
+ export async function compress(input: PdfInput, options?: CompressOptions): Promise<Buffer | void> {
34
36
  if (Buffer.isBuffer(input)) {
35
37
  if (input.length === 0) {
36
38
  throw new TypeError('Input buffer cannot be empty');
@@ -42,18 +44,11 @@ export async function compress(input: PdfInput, options: CompressOptions): Promi
42
44
  } else {
43
45
  throw new TypeError('Input must be a Buffer or file path string');
44
46
  }
45
- const mode = options.mode;
46
- if (mode !== 'lossy' && mode !== 'lossless') {
47
- throw new TypeError("Mode must be 'lossy' or 'lossless'");
48
- }
49
- const quality = options.quality ?? 75;
50
- if (quality < 1 || quality > 100) {
51
- throw new RangeError('Quality must be between 1 and 100');
52
- }
47
+ const stripMetadata = options?.stripMetadata ?? true;
53
48
  return addon.compress(input, {
54
- mode,
55
- quality,
56
- ...(options.output ? { output: options.output } : {}),
49
+ ...(options?.lossy ? { lossy: true } : {}),
50
+ ...(stripMetadata ? { stripMetadata: true } : {}),
51
+ ...(options?.output ? { output: options.output } : {}),
57
52
  }) as Promise<Buffer | void>;
58
53
  }
59
54
 
package/lib/types.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export interface CompressOptions {
2
- /** Compression mode. */
3
- readonly mode: 'lossy' | 'lossless';
4
- /** JPEG quality for lossy mode (1–100). Default: 75. */
5
- readonly quality?: number;
2
+ /** Enable lossy compression for more aggressive size reduction. Default: false. */
3
+ readonly lossy?: boolean;
4
+ /** Remove XMP metadata, document info, and thumbnails. Default: true. */
5
+ readonly stripMetadata?: boolean;
6
6
  /** Write to this file path instead of returning a Buffer. */
7
7
  readonly output?: string;
8
8
  }
@@ -10,6 +10,10 @@ export interface CompressOptions {
10
10
  export interface NativeAddon {
11
11
  compress(
12
12
  input: Buffer | string,
13
- options: { mode: 'lossy' | 'lossless'; quality: number; output?: string },
13
+ options: {
14
+ lossy?: boolean;
15
+ stripMetadata?: boolean;
16
+ output?: string;
17
+ },
14
18
  ): Promise<Buffer | undefined>;
15
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qpdf-compress",
3
- "version": "0.1.3",
3
+ "version": "0.4.0",
4
4
  "description": "Native PDF compression for Node.js, powered by QPDF",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -88,4 +88,4 @@
88
88
  "typescript-eslint": "^8.57.2",
89
89
  "vitest": "^4.1.2"
90
90
  }
91
- }
91
+ }