qpdf-compress 0.4.1 → 0.5.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/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.5.0] - 2026-04-04
8
+
9
+ ### Added
10
+
11
+ - `concurrency()` — get/set max concurrent compress operations dispatched to the libuv thread pool (default: CPU cores, powered by p-limit)
12
+ - Husky + lint-staged pre-commit hook (Prettier + ESLint on staged files)
13
+
7
14
  ## [0.4.0] - 2026-03-31
8
15
 
9
16
  ### Added
package/README.md CHANGED
@@ -189,6 +189,24 @@ Compresses a PDF document. Automatically repairs damaged PDFs.
189
189
  - Only replaces images where the result is actually smaller
190
190
  - Skips tiny images (< 50×50 px)
191
191
 
192
+ ### `concurrency(value?): number`
193
+
194
+ Gets or sets the maximum number of concurrent compress operations dispatched to the thread pool.
195
+
196
+ The default is the number of CPU cores (`os.availableParallelism()`). A value of `0` resets to the default.
197
+
198
+ Excess calls are queued in JavaScript, preventing libuv thread pool starvation.
199
+
200
+ ```typescript
201
+ import { compress, concurrency } from 'qpdf-compress';
202
+
203
+ concurrency(); // 8 (CPU cores)
204
+ concurrency(2); // limit to 2 concurrent operations
205
+ concurrency(0); // reset to default
206
+ ```
207
+
208
+ ---
209
+
192
210
  ## ⚙️ How it works
193
211
 
194
212
  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.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Gets or sets the maximum number of concurrent operations.
3
+ *
4
+ * The default value is the number of CPU cores (`os.availableParallelism()`).
5
+ * A value of `0` resets to the default.
6
+ */
7
+ export declare function concurrency(value?: number): number;
8
+ export declare function withConcurrency<T>(fn: () => Promise<T>): Promise<T>;
9
+ //# sourceMappingURL=concurrency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"concurrency.d.ts","sourceRoot":"","sources":["../lib/concurrency.ts"],"names":[],"mappings":"AAKA;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAQlD;AAED,wBAAgB,eAAe,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAEnE"}
@@ -0,0 +1,22 @@
1
+ import { availableParallelism } from 'node:os';
2
+ import pLimit from 'p-limit';
3
+ let limit = pLimit(availableParallelism());
4
+ /**
5
+ * Gets or sets the maximum number of concurrent operations.
6
+ *
7
+ * The default value is the number of CPU cores (`os.availableParallelism()`).
8
+ * A value of `0` resets to the default.
9
+ */
10
+ export function concurrency(value) {
11
+ if (value !== undefined) {
12
+ if (!Number.isInteger(value) || value < 0) {
13
+ throw new TypeError('Concurrency must be a non-negative integer');
14
+ }
15
+ limit = pLimit(value === 0 ? availableParallelism() : value);
16
+ }
17
+ return limit.concurrency;
18
+ }
19
+ export function withConcurrency(fn) {
20
+ return limit(fn);
21
+ }
22
+ //# sourceMappingURL=concurrency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"concurrency.js","sourceRoot":"","sources":["../lib/concurrency.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,MAA8B,MAAM,SAAS,CAAC;AAErD,IAAI,KAAK,GAAkB,MAAM,CAAC,oBAAoB,EAAE,CAAC,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YAC1C,MAAM,IAAI,SAAS,CAAC,4CAA4C,CAAC,CAAC;QACpE,CAAC;QACD,KAAK,GAAG,MAAM,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,oBAAoB,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO,KAAK,CAAC,WAAW,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,eAAe,CAAI,EAAoB;IACrD,OAAO,KAAK,CAAC,EAAE,CAAC,CAAC;AACnB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -16,5 +16,6 @@ export declare function compress(input: PdfInput, options: CompressOptions & {
16
16
  output: string;
17
17
  }): Promise<void>;
18
18
  export declare function compress(input: PdfInput, options?: CompressOptions): Promise<Buffer>;
19
+ export { concurrency } from './concurrency.js';
19
20
  export type { CompressOptions } from './types.js';
20
21
  //# 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;;;;;;;;;;;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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAIA,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;AAuBtF,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { dirname, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import { withConcurrency } from './concurrency.js';
4
5
  const require = createRequire(import.meta.url);
5
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
6
7
  const addonDir = resolve(__dirname, '..', 'build', 'Release');
@@ -23,10 +24,11 @@ export async function compress(input, options) {
23
24
  throw new TypeError('Input must be a Buffer or file path string');
24
25
  }
25
26
  const stripMetadata = options?.stripMetadata ?? true;
26
- return addon.compress(input, {
27
+ return withConcurrency(() => addon.compress(input, {
27
28
  ...(options?.lossy ? { lossy: true } : {}),
28
29
  ...(stripMetadata ? { stripMetadata: true } : {}),
29
30
  ...(options?.output ? { output: options.output } : {}),
30
- });
31
+ }));
31
32
  }
33
+ export { concurrency } from './concurrency.js';
32
34
  //# 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;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"}
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;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGnD,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,eAAe,CAAC,GAAG,EAAE,CAC1B,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE;QACpB,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,CAAC,CACuB,CAAC;AAC9B,CAAC;AAED,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { availableParallelism } from 'node:os';
2
+ import pLimit, { type LimitFunction } from 'p-limit';
3
+
4
+ let limit: LimitFunction = pLimit(availableParallelism());
5
+
6
+ /**
7
+ * Gets or sets the maximum number of concurrent operations.
8
+ *
9
+ * The default value is the number of CPU cores (`os.availableParallelism()`).
10
+ * A value of `0` resets to the default.
11
+ */
12
+ export function concurrency(value?: number): number {
13
+ if (value !== undefined) {
14
+ if (!Number.isInteger(value) || value < 0) {
15
+ throw new TypeError('Concurrency must be a non-negative integer');
16
+ }
17
+ limit = pLimit(value === 0 ? availableParallelism() : value);
18
+ }
19
+ return limit.concurrency;
20
+ }
21
+
22
+ export function withConcurrency<T>(fn: () => Promise<T>): Promise<T> {
23
+ return limit(fn);
24
+ }
package/lib/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { dirname, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import { withConcurrency } from './concurrency.js';
4
5
  import type { CompressOptions, NativeAddon } from './types.js';
5
6
 
6
7
  const require = createRequire(import.meta.url);
@@ -45,11 +46,14 @@ export async function compress(input: PdfInput, options?: CompressOptions): Prom
45
46
  throw new TypeError('Input must be a Buffer or file path string');
46
47
  }
47
48
  const stripMetadata = options?.stripMetadata ?? true;
48
- return addon.compress(input, {
49
- ...(options?.lossy ? { lossy: true } : {}),
50
- ...(stripMetadata ? { stripMetadata: true } : {}),
51
- ...(options?.output ? { output: options.output } : {}),
52
- }) as Promise<Buffer | void>;
49
+ return withConcurrency(() =>
50
+ addon.compress(input, {
51
+ ...(options?.lossy ? { lossy: true } : {}),
52
+ ...(stripMetadata ? { stripMetadata: true } : {}),
53
+ ...(options?.output ? { output: options.output } : {}),
54
+ }),
55
+ ) as Promise<Buffer | void>;
53
56
  }
54
57
 
58
+ export { concurrency } from './concurrency.js';
55
59
  export type { CompressOptions } from './types.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qpdf-compress",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Native PDF compression for Node.js, powered by QPDF",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -74,10 +74,16 @@
74
74
  "lint": "eslint --fix .",
75
75
  "lint:check": "eslint .",
76
76
  "format": "prettier --write .",
77
- "format:check": "prettier --check ."
77
+ "format:check": "prettier --check .",
78
+ "prepare": "husky"
79
+ },
80
+ "lint-staged": {
81
+ "*.{ts,js,json,md,yaml,yml}": "prettier --write",
82
+ "*.{ts,js}": "eslint --max-warnings 0"
78
83
  },
79
84
  "dependencies": {
80
- "node-addon-api": "^8.0.0"
85
+ "node-addon-api": "^8.0.0",
86
+ "p-limit": "^7.3.0"
81
87
  },
82
88
  "devDependencies": {
83
89
  "@eslint/js": "^10.0.1",
@@ -85,6 +91,8 @@
85
91
  "eslint": "^10.1.0",
86
92
  "eslint-config-prettier": "^10.1.8",
87
93
  "eslint-plugin-prettier": "^5.5.5",
94
+ "husky": "^9.1.7",
95
+ "lint-staged": "^16.4.0",
88
96
  "node-gyp": "^11.0.0",
89
97
  "prettier": "^3.8.1",
90
98
  "typescript": "^5.9.3",