qpdf-compress 0.2.0 → 0.4.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,124 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [0.4.0] - 2026-03-31
8
+
9
+ ### Added
10
+
11
+ - **Grayscale detection**: automatically converts RGB images where R==G==B to DeviceGray (3× raw data reduction)
12
+ - **Bitonal conversion**: converts 8-bit grayscale images that are effectively B&W to 1-bit (8× raw data reduction)
13
+ - **TrueType font subsetting**: strips unused glyph outlines from `/FontFile2` font programs via custom binary parser (cmap format 0/4, composite glyph dependency resolution, glyf/loca/hmtx table rebuilding)
14
+ - **Soft mask optimization**: losslessly optimizes JPEG `/SMask` transparency streams
15
+ - **ICC profile stripping**: replaces ICCBased color spaces with Device equivalents on images and page resources
16
+ - **Form flattening**: stamps widget annotation appearances into page content and removes `/AcroForm`
17
+ - **Page tree flattening**: pushes inherited attributes to individual pages for optimal QPDFWriter output
18
+ - **Content stream coalescing**: merges multiple content streams per page into one
19
+ - **Content stream minification**: normalizes whitespace and trims numeric formatting (trailing/leading zeros)
20
+ - **Non-image stream deduplication**: deduplicates identical font, ICC, and other non-image streams via FNV-1a hash + full byte comparison
21
+ - **Embedded file stripping**: removes `/EmbeddedFiles` from the document name tree
22
+ - **JavaScript removal**: strips `/OpenAction`, `/AA`, and `/JavaScript` from catalog, pages, and annotations
23
+ - **Accurate DPI calculation**: content stream CTM matrix parser (~170 lines) tracks `q`/`Q`/`cm`/`Do` operators to find actual rendered image dimensions, with MediaBox fallback
24
+
25
+ ### Fixed
26
+
27
+ - **Grayscale JPEG inflation**: `convertGrayscaleImages` now skips DCTDecode images — previously replacing JPEG with raw gray + Flate inflated photographic images 2–4× in lossless mode
28
+ - **Font subset size comparison**: compares uncompressed sizes instead of uncompressed subset vs Flate-compressed original
29
+ - **Stream dedup safety**: `deduplicateStreams` now checks `/DecodeParms` equality — identical raw bytes with different decode parameters produce different content
30
+ - **Content stream decoding**: decoded once per page in `subsetFonts` instead of redundantly per font
31
+
32
+ ### Changed
33
+
34
+ - Split `images.cc` into `images.cc` (image operations) and `optimize.cc` (structural optimizations)
35
+ - Added `font_subset.cc`/`font_subset.h` for TrueType binary parsing
36
+ - Source: 5 `.cc` files, 4 headers, ~3000 lines total
37
+
38
+ ## [0.3.0] - 2026-03-30
39
+
40
+ ### Changed
41
+
42
+ - **BREAKING**: Replaced `mode: 'lossy' | 'lossless'` with `lossy?: boolean` (default `false`)
43
+ - **BREAKING**: Removed `quality` parameter — quality is now automatically determined per mode
44
+ - **BREAKING**: Removed `maxDpi` parameter — DPI is now automatically determined per mode
45
+ - **BREAKING**: Options parameter is now optional — `compress(input)` defaults to lossless
46
+ - Lossless: pure structural optimization — no image re-encoding or downscaling
47
+ - Lossy: skip images at q ≤ 65, re-encode at q75, downscale to 72 DPI
48
+
49
+ ## [0.2.0] - 2026-03-30
50
+
51
+ ### Added
52
+
53
+ - CMYK → RGB conversion for JPEG images
54
+ - ICCBased color space support (extracts and converts embedded ICC profiles)
55
+ - PNG optimization (re-encodes as JPEG when beneficial)
56
+ - DPI downscaling (configurable max DPI, default 75)
57
+ - Metadata stripping (XMP, document info, thumbnails) — enabled by default
58
+ - Unused font removal
59
+ - Auto quality mode — estimates existing JPEG quality and skips re-encoding when already below target
60
+
61
+ ## [0.1.3] - 2026-03-30
62
+
63
+ ### Fixed
64
+
65
+ - Included `<cstdio>` before `<jpeglib.h>` for Linux compatibility
66
+ - Restored Node 20 to CI matrix (matching engines `>=20.11.0`)
67
+ - Used static import for `Readable` instead of dynamic `import()`
68
+
69
+ ### Added
70
+
71
+ - Build provenance attestation for prebuilt binaries via `actions/attest-build-provenance`
72
+
73
+ ## [0.1.2] - 2026-03-30
74
+
75
+ ### Fixed
76
+
77
+ - Fixed Linux cross-build tarball structure (prebuilds now install correctly)
78
+ - Fixed Debian Bookworm cross-builds (use `libjpeg62-turbo-dev` package)
79
+ - Moved `setjmp` before `jpeg_create_*` calls to prevent UB on allocation failure
80
+ - Used `uint64_t` for FNV-1a image dedup hash (correct on 32-bit ARM)
81
+ - Added component count validation in `encodeJpeg`
82
+ - Wrapped web `ReadableStream` with `Readable.fromWeb()` in install script
83
+ - Made `strerror` calls thread-safe by copying to `std::string`
84
+
85
+ ### Changed
86
+
87
+ - Bumped minimum Node.js version to 20.11.0 (`import.meta.dirname` requirement)
88
+ - Updated CI matrix to Node 22 and 24
89
+
90
+ ## [0.1.1] - 2026-03-30
91
+
92
+ ### Changed
93
+
94
+ - Replaced stb_image_write with libjpeg-turbo for lossy JPEG encoding (better quality per byte)
95
+ - Split monolithic `qpdf_addon.cc` into focused modules: `jpeg.cc`, `images.cc`, `qpdf_addon.cc`
96
+ - Extracted shared `forEachImage()` template helper
97
+ - Bumped minimum Node.js version to 20.11.0
98
+
99
+ ### Fixed
100
+
101
+ - Removed global mutex that serialized concurrent compress() calls
102
+ - Isolated setjmp/longjmp in C-style functions to avoid UB with C++ destructors
103
+ - Fixed prebuild tarball structure so install script finds the .node file
104
+ - Added mode validation (reject invalid mode strings)
105
+
106
+ ## [0.1.0] - 2026-03-30
107
+
108
+ ### Added
109
+
110
+ - Native PDF compression via QPDF 12.3.2 with single `compress()` API
111
+ - Flate compression (level 9) for all streams
112
+ - Lossless JPEG optimization via libjpeg-turbo
113
+ - Image deduplication (content-hash based)
114
+ - PDF repair merged into compress pipeline
115
+ - Prebuilt binaries for macOS (arm64, x64), Linux glibc (x64, arm64, arm), Linux musl (x64, arm64), Windows (x64, arm64)
116
+ - Automatic prebuild download with source compilation fallback
117
+ - N-API addon for ABI stability across Node.js versions
118
+
119
+ ### Infrastructure
120
+
121
+ - CI matrix: 3 OS × 3 Node versions (20, 22, 24)
122
+ - Release pipeline with prebuild generation and npm provenance
123
+ - QEMU-based cross-compilation for Linux arm64/arm and musl variants
124
+ - vcpkg integration for Windows static linking
package/README.md CHANGED
@@ -14,14 +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 — 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 });
20
+ // lossy — image recompression + downscale to 72 DPI
21
+ const smaller = await compress(pdfBuffer, { lossy: true });
25
22
  ```
26
23
 
27
24
  ## 💡 Why qpdf-compress?
@@ -31,7 +28,7 @@ const tiny = await compress(pdfBuffer, { mode: 'lossy', quality: 50 });
31
28
  - Native C++ — no WASM overhead, no shell-out to CLI tools
32
29
  - Non-blocking — all operations run off the main thread via N-API AsyncWorker
33
30
  - Multi-pass optimization — image dedup, JPEG Huffman optimization, Flate level 9
34
- - Smart defaults — DPI downscaling, metadata stripping, adaptive JPEG quality
31
+ - Smart defaults — metadata stripping, image dedup, lossless JPEG Huffman optimization
35
32
 
36
33
  **🛠️ Developer experience**
37
34
 
@@ -56,20 +53,28 @@ const tiny = await compress(pdfBuffer, { mode: 'lossy', quality: 50 });
56
53
 
57
54
  ### 📊 How it compares
58
55
 
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 |
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 |
73
78
 
74
79
  ¹ QPDF is statically linked — no runtime dependencies. Prebuilt binaries downloaded at install.
75
80
 
@@ -116,76 +121,101 @@ vcpkg install zlib libjpeg-turbo --triplet x64-windows-static
116
121
  ```typescript
117
122
  import { compress } from 'qpdf-compress';
118
123
 
119
- // lossless — optimize streams without touching image quality
120
- const optimized = await compress(pdfBuffer, { mode: 'lossless' });
121
-
122
- // lossy — auto quality per image (skips JPEGs ≤ q90, encodes rest at q85)
123
- const smaller = await compress(pdfBuffer, { mode: 'lossy' });
124
+ // lossless (default) pure structural optimization, no image re-encoding
125
+ const optimized = await compress(pdfBuffer);
124
126
 
125
- // lossy with explicit quality (1–100)
126
- const tiny = await compress(pdfBuffer, { mode: 'lossy', quality: 50 });
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 });
127
+ // lossy image recompression + downscale to 72 DPI
128
+ const smaller = await compress(pdfBuffer, { lossy: true });
131
129
 
132
130
  // keep metadata (stripped by default)
133
- const withMeta = await compress(pdfBuffer, { mode: 'lossless', stripMetadata: false });
131
+ const withMeta = await compress(pdfBuffer, { stripMetadata: false });
134
132
 
135
133
  // file path input (avoids copying into memory twice)
136
- const result = await compress('/path/to/file.pdf', { mode: 'lossless' });
134
+ const result = await compress('/path/to/file.pdf');
137
135
 
138
136
  // write directly to file instead of returning a Buffer
139
- await compress(pdfBuffer, { mode: 'lossless', output: '/path/to/output.pdf' });
137
+ await compress(pdfBuffer, { output: '/path/to/output.pdf' });
140
138
 
141
139
  // damaged PDFs are automatically repaired during compression
142
- const fixed = await compress(damagedBuffer, { mode: 'lossless' });
140
+ const fixed = await compress(damagedBuffer);
143
141
  ```
144
142
 
145
143
  ## 📖 API
146
144
 
147
- ### `compress(input, options): Promise<Buffer>`
145
+ ### `compress(input, options?): Promise<Buffer>`
148
146
 
149
147
  ### `compress(input, options & { output: string }): Promise<void>`
150
148
 
151
149
  Compresses a PDF document. Automatically repairs damaged PDFs.
152
150
 
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` |
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` |
161
157
 
162
158
  **Both modes:**
163
159
 
164
- - 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)
165
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
166
173
  - Recompresses all decodable streams with Flate level 9
167
174
  - Generates object streams for smaller metadata overhead
168
- - Removes unreferenced objects and unused fonts
169
- - Downscales images exceeding `maxDpi` (default: 75 DPI)
175
+ - Removes unreferenced objects
170
176
  - Strips XMP metadata, document info, and thumbnails (default: on)
171
- - Converts CMYK and ICCBased color spaces to RGB
172
177
  - Automatically repairs damaged PDFs
173
178
 
174
- **Lossy mode** (in addition to the above):
179
+ **Lossless (default):**
175
180
 
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)
179
- - Only replaces images where JPEG is actually smaller
180
- - Skips re-encoding when estimated quality is already at or below target
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
181
190
  - Skips tiny images (< 50×50 px)
182
191
 
183
192
  ## ⚙️ How it works
184
193
 
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.
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.
186
195
 
187
196
  All operations run in a background thread via `Napi::AsyncWorker`, so the event loop is never blocked.
188
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
+
189
219
  ## License
190
220
 
191
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;AAgCrF,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,22 +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 ?? 0;
30
- if (quality !== 0 && (quality < 1 || quality > 100)) {
31
- throw new RangeError('Quality must be between 1 and 100');
32
- }
33
- const maxDpi = options.maxDpi ?? 75;
34
- const stripMetadata = options.stripMetadata ?? true;
25
+ const stripMetadata = options?.stripMetadata ?? true;
35
26
  return addon.compress(input, {
36
- mode,
37
- quality,
38
- ...(maxDpi > 0 ? { maxDpi } : {}),
27
+ ...(options?.lossy ? { lossy: true } : {}),
39
28
  ...(stripMetadata ? { stripMetadata: true } : {}),
40
- ...(options.output ? { output: options.output } : {}),
29
+ ...(options?.output ? { output: options.output } : {}),
41
30
  });
42
31
  }
43
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,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"}
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,10 +1,6 @@
1
1
  export interface CompressOptions {
2
- /** Compression mode. */
3
- readonly mode: 'lossy' | 'lossless';
4
- /** JPEG quality for lossy mode (1–100). When omitted, automatically determines optimal quality per image (capped at 85). */
5
- readonly quality?: number;
6
- /** Maximum image DPI. Images exceeding this are downscaled. 0 = no limit. Default: 75. */
7
- readonly maxDpi?: number;
2
+ /** Enable lossy compression for more aggressive size reduction. Default: false. */
3
+ readonly lossy?: boolean;
8
4
  /** Remove XMP metadata, document info, and thumbnails. Default: true. */
9
5
  readonly stripMetadata?: boolean;
10
6
  /** Write to this file path instead of returning a Buffer. */
@@ -12,9 +8,7 @@ export interface CompressOptions {
12
8
  }
13
9
  export interface NativeAddon {
14
10
  compress(input: Buffer | string, options: {
15
- mode: 'lossy' | 'lossless';
16
- quality: number;
17
- maxDpi?: number;
11
+ lossy?: boolean;
18
12
  stripMetadata?: boolean;
19
13
  output?: string;
20
14
  }): Promise<Buffer | undefined>;
@@ -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,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"}
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,22 +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 ?? 0;
50
- if (quality !== 0 && (quality < 1 || quality > 100)) {
51
- throw new RangeError('Quality must be between 1 and 100');
52
- }
53
- const maxDpi = options.maxDpi ?? 75;
54
- const stripMetadata = options.stripMetadata ?? true;
47
+ const stripMetadata = options?.stripMetadata ?? true;
55
48
  return addon.compress(input, {
56
- mode,
57
- quality,
58
- ...(maxDpi > 0 ? { maxDpi } : {}),
49
+ ...(options?.lossy ? { lossy: true } : {}),
59
50
  ...(stripMetadata ? { stripMetadata: true } : {}),
60
- ...(options.output ? { output: options.output } : {}),
51
+ ...(options?.output ? { output: options.output } : {}),
61
52
  }) as Promise<Buffer | void>;
62
53
  }
63
54
 
package/lib/types.ts CHANGED
@@ -1,10 +1,6 @@
1
1
  export interface CompressOptions {
2
- /** Compression mode. */
3
- readonly mode: 'lossy' | 'lossless';
4
- /** JPEG quality for lossy mode (1–100). When omitted, automatically determines optimal quality per image (capped at 85). */
5
- readonly quality?: number;
6
- /** Maximum image DPI. Images exceeding this are downscaled. 0 = no limit. Default: 75. */
7
- readonly maxDpi?: number;
2
+ /** Enable lossy compression for more aggressive size reduction. Default: false. */
3
+ readonly lossy?: boolean;
8
4
  /** Remove XMP metadata, document info, and thumbnails. Default: true. */
9
5
  readonly stripMetadata?: boolean;
10
6
  /** Write to this file path instead of returning a Buffer. */
@@ -15,9 +11,7 @@ export interface NativeAddon {
15
11
  compress(
16
12
  input: Buffer | string,
17
13
  options: {
18
- mode: 'lossy' | 'lossless';
19
- quality: number; // 0 = auto, 1–100 = fixed
20
- maxDpi?: number;
14
+ lossy?: boolean;
21
15
  stripMetadata?: boolean;
22
16
  output?: string;
23
17
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qpdf-compress",
3
- "version": "0.2.0",
3
+ "version": "0.4.1",
4
4
  "description": "Native PDF compression for Node.js, powered by QPDF",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -38,7 +38,10 @@
38
38
  "src",
39
39
  "lib",
40
40
  "scripts",
41
- "binding.gyp"
41
+ "binding.gyp",
42
+ "README.md",
43
+ "CHANGELOG.md",
44
+ "LICENSE"
42
45
  ],
43
46
  "repository": {
44
47
  "type": "git",