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 +50 -27
- package/binding.gyp +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +7 -1
- package/dist/types.d.ts.map +1 -1
- package/lib/index.ts +6 -2
- package/lib/types.ts +12 -2
- package/package.json +3 -3
- package/scripts/download-qpdf.mjs +5 -0
- package/scripts/install.mjs +4 -3
- package/src/images.cc +596 -0
- package/src/images.h +42 -0
- package/src/jpeg.cc +227 -0
- package/src/jpeg.h +30 -0
- package/src/qpdf_addon.cc +64 -426
- package/src/stb_image_write.h +0 -1724
- package/src/stb_impl.cc +0 -2
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 —
|
|
21
|
-
const smaller = await compress(pdfBuffer, { mode: 'lossy'
|
|
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**
|
|
56
|
-
| ------------------------- |
|
|
57
|
-
| Integration | Native Node.js addon
|
|
58
|
-
| Async I/O | ✅ Non-blocking
|
|
59
|
-
| Image deduplication | ✅
|
|
60
|
-
| JPEG Huffman optimization | ✅ Lossless (libjpeg)
|
|
61
|
-
| Lossy image compression | ✅
|
|
62
|
-
|
|
|
63
|
-
|
|
|
64
|
-
|
|
|
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 —
|
|
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
|
|
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
|
|
139
|
-
|
|
|
140
|
-
| `input`
|
|
141
|
-
| `options.mode`
|
|
142
|
-
| `options.quality`
|
|
143
|
-
| `options.
|
|
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
|
-
**
|
|
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
|
|
174
|
+
**Lossy mode** (in addition to the above):
|
|
154
175
|
|
|
155
|
-
- Extracts 8-bit RGB and
|
|
156
|
-
-
|
|
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
|
|
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
|
|
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
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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 ??
|
|
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,
|
|
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).
|
|
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
|
}
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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,
|
|
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 ??
|
|
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).
|
|
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: {
|
|
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.
|
|
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": ">=
|
|
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');
|
package/scripts/install.mjs
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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' });
|