@blazediff/bin 3.0.0 → 3.1.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/README.md CHANGED
@@ -10,12 +10,17 @@
10
10
  The fastest single-threaded image diff in the world. Native Rust implementation with SIMD optimization, **3-4x faster** and **3x smaller** than [odiff](https://github.com/dmtrKovalenko/odiff).
11
11
 
12
12
  **Features:**
13
+ - **PNG & JPEG support** - auto-detected by file extension
13
14
  - SIMD-accelerated (NEON on ARM, SSE4.1 on x86)
14
15
  - Block-based two-pass optimization
15
16
  - YIQ perceptual color difference
16
17
  - Anti-aliasing detection
17
18
  - Cross-platform pre-built binaries (~700KB-900KB, no compilation required)
18
19
 
20
+ **Vendored Libraries:**
21
+ - [libspng](https://libspng.org/) - Fast PNG decoding/encoding with SIMD
22
+ - [libjpeg-turbo](https://libjpeg-turbo.org/) - High-performance JPEG codec with SIMD
23
+
19
24
  ## Installation
20
25
 
21
26
  ```bash
@@ -34,7 +39,7 @@ Pre-built [binaries](https://github.com/teimurjan/blazediff/tree/main/packages/b
34
39
 
35
40
  ### compare(basePath, comparePath, diffOutput, options?)
36
41
 
37
- Compare two PNG images and generate a diff image.
42
+ Compare two images (PNG or JPEG) and generate a diff image. Format is auto-detected from file extension.
38
43
 
39
44
  <table>
40
45
  <tr>
@@ -133,15 +138,24 @@ if (result.match) {
133
138
  ### CLI Usage
134
139
 
135
140
  ```bash
136
- # Compare two images
141
+ # Compare two PNG images
137
142
  npx blazediff expected.png actual.png diff.png
138
143
 
144
+ # Compare two JPEG images
145
+ npx blazediff expected.jpg actual.jpg diff.jpg
146
+
147
+ # Mixed formats (PNG input, JPEG output)
148
+ npx blazediff expected.png actual.png diff.jpg
149
+
139
150
  # With options
140
151
  npx blazediff expected.png actual.png diff.png --threshold 0.05 --antialiasing
141
152
 
142
- # With higher compression (smaller output file, slower)
153
+ # With higher PNG compression (smaller output file, slower)
143
154
  npx blazediff expected.png actual.png diff.png -c 6
144
155
 
156
+ # With JPEG quality setting
157
+ npx blazediff expected.jpg actual.jpg diff.jpg -q 85
158
+
145
159
  # Output as JSON
146
160
  npx blazediff expected.png actual.png diff.png --output-format json
147
161
  ```
@@ -149,24 +163,34 @@ npx blazediff expected.png actual.png diff.png --output-format json
149
163
  ### CLI Options
150
164
 
151
165
  ```
152
- Usage: blazediff [OPTIONS] <IMAGE1> <IMAGE2> <OUTPUT>
166
+ Usage: blazediff [OPTIONS] <IMAGE1> <IMAGE2> [OUTPUT]
153
167
 
154
168
  Arguments:
155
- <IMAGE1> First image path
156
- <IMAGE2> Second image path
157
- <OUTPUT> Output diff image path
169
+ <IMAGE1> First image path (PNG or JPEG)
170
+ <IMAGE2> Second image path (PNG or JPEG)
171
+ [OUTPUT] Output diff image path (optional, format detected from extension)
158
172
 
159
173
  Options:
160
174
  -t, --threshold <THRESHOLD> Color difference threshold (0.0-1.0) [default: 0.1]
161
175
  -a, --antialiasing Enable anti-aliasing detection
162
176
  --diff-mask Output only differences (transparent background)
163
177
  --fail-on-layout Fail on layout (size) difference
164
- -c, --compression <LEVEL> PNG compression level (0-9, 0=fastest/largest, 9=slowest/smallest) [default: 0]
178
+ -c, --compression <LEVEL> PNG compression level (0-9, 0=fastest, 9=smallest) [default: 0]
179
+ -q, --quality <QUALITY> JPEG quality (1-100) [default: 90]
165
180
  --output-format <FORMAT> Output format (json or text) [default: json]
166
181
  -h, --help Print help
167
182
  -V, --version Print version
168
183
  ```
169
184
 
185
+ ### Supported Formats
186
+
187
+ | Format | Extensions | Notes |
188
+ |--------|------------|-------|
189
+ | PNG | `.png` | Lossless, supports transparency |
190
+ | JPEG | `.jpg`, `.jpeg` | Lossy, smaller file sizes |
191
+
192
+ Input images can be mixed formats (e.g., compare PNG to JPEG). Output format is determined by the output file extension.
193
+
170
194
  ### Exit Codes
171
195
 
172
196
  - `0` - Images are identical
@@ -175,7 +199,7 @@ Options:
175
199
 
176
200
  ## Performance
177
201
 
178
- Benchmarked on Apple M1 Pro with 5600x3200 4K images:
202
+ Benchmarked on Apple M1 Pro with 5600x3200 4K PNG images:
179
203
 
180
204
  | Tool | Benchmark Time | vs blazediff |
181
205
  |------|------|--------------|
package/dist/index.d.ts CHANGED
@@ -9,6 +9,8 @@ interface BlazeDiffOptions {
9
9
  failOnLayoutDiff?: boolean;
10
10
  /** PNG compression level (0-9, 0=fastest/largest, 9=slowest/smallest) */
11
11
  compression?: number;
12
+ /** JPEG quality (1-100). Default: 90 */
13
+ quality?: number;
12
14
  }
13
15
  type BlazeDiffResult = {
14
16
  match: true;
@@ -26,7 +28,11 @@ type BlazeDiffResult = {
26
28
  file: string;
27
29
  };
28
30
  /**
29
- * Compare two PNG images and optionally generate a diff image.
31
+ * Compare two images (PNG or JPEG) and optionally generate a diff image.
32
+ *
33
+ * Uses native N-API bindings when available for ~10-100x better performance
34
+ * on small images (no process spawn overhead). Falls back to execFile if
35
+ * native bindings are unavailable.
30
36
  *
31
37
  * @example
32
38
  * ```ts
@@ -46,5 +52,10 @@ type BlazeDiffResult = {
46
52
  declare function compare(basePath: string, comparePath: string, diffOutput?: string, options?: BlazeDiffOptions): Promise<BlazeDiffResult>;
47
53
  /** Get the path to the blazediff binary for direct CLI usage. */
48
54
  declare function getBinaryPath(): string;
55
+ /**
56
+ * Check if native N-API bindings are available.
57
+ * Returns true if the native module loaded successfully.
58
+ */
59
+ declare function hasNativeBinding(): boolean;
49
60
 
50
- export { type BlazeDiffOptions, type BlazeDiffResult, compare, getBinaryPath };
61
+ export { type BlazeDiffOptions, type BlazeDiffResult, compare, getBinaryPath, hasNativeBinding };
package/dist/index.js CHANGED
@@ -31,7 +31,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  compare: () => compare,
34
- getBinaryPath: () => getBinaryPath
34
+ getBinaryPath: () => getBinaryPath,
35
+ hasNativeBinding: () => hasNativeBinding
35
36
  });
36
37
  module.exports = __toCommonJS(index_exports);
37
38
 
@@ -74,6 +75,81 @@ var PLATFORM_PACKAGES = {
74
75
  packageDir: "bin-win32-x64"
75
76
  }
76
77
  };
78
+ var nativeBinding = null;
79
+ var nativeBindingAttempted = false;
80
+ function tryLoadNativeBinding() {
81
+ if (nativeBindingAttempted) {
82
+ return nativeBinding;
83
+ }
84
+ nativeBindingAttempted = true;
85
+ const platform = import_node_os.default.platform();
86
+ const arch = import_node_os.default.arch();
87
+ const key = `${platform}-${arch}`;
88
+ const platformInfo = PLATFORM_PACKAGES[key];
89
+ if (!platformInfo) {
90
+ return null;
91
+ }
92
+ try {
93
+ const require2 = (0, import_node_module.createRequire)(importMetaUrl);
94
+ const binding = require2(platformInfo.packageName);
95
+ if (typeof binding?.compare === "function") {
96
+ nativeBinding = binding;
97
+ return binding;
98
+ }
99
+ } catch {
100
+ }
101
+ try {
102
+ const currentDir = import_node_path.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
103
+ const packagesDir = import_node_path.default.resolve(currentDir, "..", "..");
104
+ const nodePath = import_node_path.default.join(
105
+ packagesDir,
106
+ platformInfo.packageDir,
107
+ "blazediff.node"
108
+ );
109
+ if ((0, import_node_fs.existsSync)(nodePath)) {
110
+ const require2 = (0, import_node_module.createRequire)(importMetaUrl);
111
+ const binding = require2(nodePath);
112
+ if (typeof binding?.compare === "function") {
113
+ nativeBinding = binding;
114
+ return binding;
115
+ }
116
+ }
117
+ } catch {
118
+ }
119
+ return null;
120
+ }
121
+ function convertNapiResult(result) {
122
+ if (result.matchResult) {
123
+ return { match: true };
124
+ }
125
+ if (result.reason === "layout-diff") {
126
+ return { match: false, reason: "layout-diff" };
127
+ }
128
+ if (result.reason === "pixel-diff") {
129
+ return {
130
+ match: false,
131
+ reason: "pixel-diff",
132
+ diffCount: result.diffCount ?? 0,
133
+ diffPercentage: result.diffPercentage ?? 0
134
+ };
135
+ }
136
+ return {
137
+ match: false,
138
+ reason: "pixel-diff",
139
+ diffCount: result.diffCount ?? 0,
140
+ diffPercentage: result.diffPercentage ?? 0
141
+ };
142
+ }
143
+ function convertToNapiOptions(options) {
144
+ return {
145
+ threshold: options?.threshold,
146
+ antialiasing: options?.antialiasing,
147
+ diffMask: options?.diffMask,
148
+ failOnLayout: options?.failOnLayoutDiff,
149
+ compression: options?.compression,
150
+ quality: options?.quality
151
+ };
152
+ }
77
153
  function resolveBinaryPath() {
78
154
  const platform = import_node_os.default.platform();
79
155
  const arch = import_node_os.default.arch();
@@ -130,6 +206,7 @@ function buildArgs(diffOutput, options) {
130
206
  if (options.failOnLayoutDiff) args.push("--fail-on-layout");
131
207
  if (options.compression !== void 0)
132
208
  args.push(`--compression=${options.compression}`);
209
+ if (options.quality !== void 0) args.push(`--quality=${options.quality}`);
133
210
  return args;
134
211
  }
135
212
  function parseJsonOutput(text) {
@@ -147,7 +224,7 @@ function detectMissingFile(error, basePath, comparePath) {
147
224
  if (error.includes(comparePath)) return comparePath;
148
225
  return basePath;
149
226
  }
150
- async function compare(basePath, comparePath, diffOutput, options) {
227
+ async function execFileCompare(basePath, comparePath, diffOutput, options) {
151
228
  const binaryPath = getBinaryPathInternal();
152
229
  const args = [basePath, comparePath, ...buildArgs(diffOutput, options)];
153
230
  try {
@@ -182,11 +259,37 @@ async function compare(basePath, comparePath, diffOutput, options) {
182
259
  throw new Error(output || `blazediff exited with code ${code}`);
183
260
  }
184
261
  }
262
+ async function compare(basePath, comparePath, diffOutput, options) {
263
+ const binding = tryLoadNativeBinding();
264
+ if (binding) {
265
+ try {
266
+ const result = binding.compare(
267
+ basePath,
268
+ comparePath,
269
+ diffOutput ?? null,
270
+ convertToNapiOptions(options)
271
+ );
272
+ return convertNapiResult(result);
273
+ } catch (err) {
274
+ const message = err instanceof Error ? err.message : String(err);
275
+ const missingFile = detectMissingFile(message, basePath, comparePath);
276
+ if (missingFile) {
277
+ return { match: false, reason: "file-not-exists", file: missingFile };
278
+ }
279
+ throw err;
280
+ }
281
+ }
282
+ return execFileCompare(basePath, comparePath, diffOutput, options);
283
+ }
185
284
  function getBinaryPath() {
186
285
  return getBinaryPathInternal();
187
286
  }
287
+ function hasNativeBinding() {
288
+ return tryLoadNativeBinding() !== null;
289
+ }
188
290
  // Annotate the CommonJS export names for ESM import in node:
189
291
  0 && (module.exports = {
190
292
  compare,
191
- getBinaryPath
293
+ getBinaryPath,
294
+ hasNativeBinding
192
295
  });
package/dist/index.mjs CHANGED
@@ -33,6 +33,81 @@ var PLATFORM_PACKAGES = {
33
33
  packageDir: "bin-win32-x64"
34
34
  }
35
35
  };
36
+ var nativeBinding = null;
37
+ var nativeBindingAttempted = false;
38
+ function tryLoadNativeBinding() {
39
+ if (nativeBindingAttempted) {
40
+ return nativeBinding;
41
+ }
42
+ nativeBindingAttempted = true;
43
+ const platform = os.platform();
44
+ const arch = os.arch();
45
+ const key = `${platform}-${arch}`;
46
+ const platformInfo = PLATFORM_PACKAGES[key];
47
+ if (!platformInfo) {
48
+ return null;
49
+ }
50
+ try {
51
+ const require2 = createRequire(import.meta.url);
52
+ const binding = require2(platformInfo.packageName);
53
+ if (typeof binding?.compare === "function") {
54
+ nativeBinding = binding;
55
+ return binding;
56
+ }
57
+ } catch {
58
+ }
59
+ try {
60
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
61
+ const packagesDir = path.resolve(currentDir, "..", "..");
62
+ const nodePath = path.join(
63
+ packagesDir,
64
+ platformInfo.packageDir,
65
+ "blazediff.node"
66
+ );
67
+ if (existsSync(nodePath)) {
68
+ const require2 = createRequire(import.meta.url);
69
+ const binding = require2(nodePath);
70
+ if (typeof binding?.compare === "function") {
71
+ nativeBinding = binding;
72
+ return binding;
73
+ }
74
+ }
75
+ } catch {
76
+ }
77
+ return null;
78
+ }
79
+ function convertNapiResult(result) {
80
+ if (result.matchResult) {
81
+ return { match: true };
82
+ }
83
+ if (result.reason === "layout-diff") {
84
+ return { match: false, reason: "layout-diff" };
85
+ }
86
+ if (result.reason === "pixel-diff") {
87
+ return {
88
+ match: false,
89
+ reason: "pixel-diff",
90
+ diffCount: result.diffCount ?? 0,
91
+ diffPercentage: result.diffPercentage ?? 0
92
+ };
93
+ }
94
+ return {
95
+ match: false,
96
+ reason: "pixel-diff",
97
+ diffCount: result.diffCount ?? 0,
98
+ diffPercentage: result.diffPercentage ?? 0
99
+ };
100
+ }
101
+ function convertToNapiOptions(options) {
102
+ return {
103
+ threshold: options?.threshold,
104
+ antialiasing: options?.antialiasing,
105
+ diffMask: options?.diffMask,
106
+ failOnLayout: options?.failOnLayoutDiff,
107
+ compression: options?.compression,
108
+ quality: options?.quality
109
+ };
110
+ }
36
111
  function resolveBinaryPath() {
37
112
  const platform = os.platform();
38
113
  const arch = os.arch();
@@ -89,6 +164,7 @@ function buildArgs(diffOutput, options) {
89
164
  if (options.failOnLayoutDiff) args.push("--fail-on-layout");
90
165
  if (options.compression !== void 0)
91
166
  args.push(`--compression=${options.compression}`);
167
+ if (options.quality !== void 0) args.push(`--quality=${options.quality}`);
92
168
  return args;
93
169
  }
94
170
  function parseJsonOutput(text) {
@@ -106,7 +182,7 @@ function detectMissingFile(error, basePath, comparePath) {
106
182
  if (error.includes(comparePath)) return comparePath;
107
183
  return basePath;
108
184
  }
109
- async function compare(basePath, comparePath, diffOutput, options) {
185
+ async function execFileCompare(basePath, comparePath, diffOutput, options) {
110
186
  const binaryPath = getBinaryPathInternal();
111
187
  const args = [basePath, comparePath, ...buildArgs(diffOutput, options)];
112
188
  try {
@@ -141,10 +217,36 @@ async function compare(basePath, comparePath, diffOutput, options) {
141
217
  throw new Error(output || `blazediff exited with code ${code}`);
142
218
  }
143
219
  }
220
+ async function compare(basePath, comparePath, diffOutput, options) {
221
+ const binding = tryLoadNativeBinding();
222
+ if (binding) {
223
+ try {
224
+ const result = binding.compare(
225
+ basePath,
226
+ comparePath,
227
+ diffOutput ?? null,
228
+ convertToNapiOptions(options)
229
+ );
230
+ return convertNapiResult(result);
231
+ } catch (err) {
232
+ const message = err instanceof Error ? err.message : String(err);
233
+ const missingFile = detectMissingFile(message, basePath, comparePath);
234
+ if (missingFile) {
235
+ return { match: false, reason: "file-not-exists", file: missingFile };
236
+ }
237
+ throw err;
238
+ }
239
+ }
240
+ return execFileCompare(basePath, comparePath, diffOutput, options);
241
+ }
144
242
  function getBinaryPath() {
145
243
  return getBinaryPathInternal();
146
244
  }
245
+ function hasNativeBinding() {
246
+ return tryLoadNativeBinding() !== null;
247
+ }
147
248
  export {
148
249
  compare,
149
- getBinaryPath
250
+ getBinaryPath,
251
+ hasNativeBinding
150
252
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blazediff/bin",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "Native Rust binaries for blazediff - the fastest image diff in the world",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -20,12 +20,12 @@
20
20
  "dist"
21
21
  ],
22
22
  "optionalDependencies": {
23
- "@blazediff/bin-darwin-arm64": "3.0.0",
24
- "@blazediff/bin-darwin-x64": "3.0.0",
25
- "@blazediff/bin-linux-arm64": "3.0.0",
26
- "@blazediff/bin-linux-x64": "3.0.0",
27
- "@blazediff/bin-win32-arm64": "3.0.0",
28
- "@blazediff/bin-win32-x64": "3.0.0"
23
+ "@blazediff/bin-darwin-arm64": "3.1.1",
24
+ "@blazediff/bin-darwin-x64": "3.1.1",
25
+ "@blazediff/bin-linux-arm64": "3.1.1",
26
+ "@blazediff/bin-linux-x64": "3.1.1",
27
+ "@blazediff/bin-win32-arm64": "3.1.1",
28
+ "@blazediff/bin-win32-x64": "3.1.1"
29
29
  },
30
30
  "keywords": [
31
31
  "image",
@@ -50,6 +50,7 @@
50
50
  "typescript": "5.9.2"
51
51
  },
52
52
  "scripts": {
53
+ "typecheck": "tsc --noEmit",
53
54
  "build": "tsup",
54
55
  "clean": "rm -rf dist"
55
56
  }
package/dist/index.d.mts DELETED
@@ -1,50 +0,0 @@
1
- interface BlazeDiffOptions {
2
- /** Color difference threshold (0.0-1.0). Lower = more strict. Default: 0.1 */
3
- threshold?: number;
4
- /** Enable anti-aliasing detection to exclude AA pixels from diff count */
5
- antialiasing?: boolean;
6
- /** Output only differences with transparent background */
7
- diffMask?: boolean;
8
- /** Fail immediately if images have different dimensions */
9
- failOnLayoutDiff?: boolean;
10
- /** PNG compression level (0-9, 0=fastest/largest, 9=slowest/smallest) */
11
- compression?: number;
12
- }
13
- type BlazeDiffResult = {
14
- match: true;
15
- } | {
16
- match: false;
17
- reason: "layout-diff";
18
- } | {
19
- match: false;
20
- reason: "pixel-diff";
21
- diffCount: number;
22
- diffPercentage: number;
23
- } | {
24
- match: false;
25
- reason: "file-not-exists";
26
- file: string;
27
- };
28
- /**
29
- * Compare two PNG images and optionally generate a diff image.
30
- *
31
- * @example
32
- * ```ts
33
- * // With diff output
34
- * const result = await compare('expected.png', 'actual.png', 'diff.png');
35
- *
36
- * // Without diff output (faster, just returns comparison result)
37
- * const result = await compare('expected.png', 'actual.png');
38
- *
39
- * if (result.match) {
40
- * console.log('Images identical');
41
- * } else if (result.reason === 'pixel-diff') {
42
- * console.log(`${result.diffCount} pixels differ`);
43
- * }
44
- * ```
45
- */
46
- declare function compare(basePath: string, comparePath: string, diffOutput?: string, options?: BlazeDiffOptions): Promise<BlazeDiffResult>;
47
- /** Get the path to the blazediff binary for direct CLI usage. */
48
- declare function getBinaryPath(): string;
49
-
50
- export { type BlazeDiffOptions, type BlazeDiffResult, compare, getBinaryPath };