@blazediff/core-native 4.0.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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Teimur Gasanov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,238 @@
1
+ # @blazediff/core-native
2
+
3
+ <div align="center">
4
+
5
+ [![npm bundle size](https://img.shields.io/bundlephobia/min/%40blazediff%2Fbin)](https://www.npmjs.com/package/@blazediff/core-native)
6
+ [![NPM Downloads](https://img.shields.io/npm/dy/%40blazediff%2Fbin)](https://www.npmjs.com/package/@blazediff/core-native)
7
+ [![Crates.io](https://img.shields.io/crates/v/blazediff.svg)](https://crates.io/crates/blazediff)
8
+
9
+ </div>
10
+
11
+ 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).
12
+
13
+ **Features:**
14
+ - **PNG, JPEG & QOI support** - auto-detected by file extension
15
+ - SIMD-accelerated (NEON on ARM, SSE4.1 on x86)
16
+ - Block-based two-pass optimization
17
+ - YIQ perceptual color difference
18
+ - Anti-aliasing detection
19
+ - Cross-platform pre-built binaries (~700KB-900KB, no compilation required)
20
+
21
+ **Vendored Libraries:**
22
+ - [libspng](https://libspng.org/) - Fast PNG decoding/encoding with SIMD
23
+ - [libjpeg-turbo](https://libjpeg-turbo.org/) - High-performance JPEG codec with SIMD
24
+ - [qoi](https://github.com/aldanor/qoi-rust) - QOI (Quite OK Image) format for fast lossless compression
25
+
26
+ > **Note:** This package was previously published as [`@blazediff/bin`](https://www.npmjs.com/package/@blazediff/bin), which is now deprecated. Please use `@blazediff/core-native` instead.
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ npm install @blazediff/core-native
32
+ ```
33
+
34
+ Also available as a Rust crate: [`cargo install blazediff`](https://crates.io/crates/blazediff)
35
+
36
+ Pre-built binaries are included via platform-specific packages:
37
+ - [`@blazediff/core-native-darwin-arm64`](https://github.com/teimurjan/blazediff/tree/main/packages/core-native-darwin-arm64) - macOS ARM64 (Apple Silicon)
38
+ - [`@blazediff/core-native-darwin-x64`](https://github.com/teimurjan/blazediff/tree/main/packages/core-native-darwin-x64) - macOS x64 (Intel)
39
+ - [`@blazediff/core-native-linux-arm64`](https://github.com/teimurjan/blazediff/tree/main/packages/core-native-linux-arm64) - Linux ARM64
40
+ - [`@blazediff/core-native-linux-x64`](https://github.com/teimurjan/blazediff/tree/main/packages/core-native-linux-x64) - Linux x64
41
+ - [`@blazediff/core-native-win32-arm64`](https://github.com/teimurjan/blazediff/tree/main/packages/core-native-win32-arm64) - Windows ARM64
42
+ - [`@blazediff/core-native-win32-x64`](https://github.com/teimurjan/blazediff/tree/main/packages/core-native-win32-x64) - Windows x64
43
+
44
+ ## API
45
+
46
+ ### compare(basePath, comparePath, diffOutput, options?)
47
+
48
+ Compare two images (PNG or JPEG) and generate a diff image. Format is auto-detected from file extension.
49
+
50
+ <table>
51
+ <tr>
52
+ <th width="500">Parameter</th>
53
+ <th width="500">Type</th>
54
+ <th width="500">Description</th>
55
+ </tr>
56
+ <tr>
57
+ <td><code>basePath</code></td>
58
+ <td>string</td>
59
+ <td>Path to the base/expected image</td>
60
+ </tr>
61
+ <tr>
62
+ <td><code>comparePath</code></td>
63
+ <td>string</td>
64
+ <td>Path to the comparison/actual image</td>
65
+ </tr>
66
+ <tr>
67
+ <td><code>diffOutput</code></td>
68
+ <td>string</td>
69
+ <td>Path where the diff image will be saved</td>
70
+ </tr>
71
+ <tr>
72
+ <td><code>options</code></td>
73
+ <td>BlazeDiffOptions</td>
74
+ <td>Comparison options (optional)</td>
75
+ </tr>
76
+ </table>
77
+
78
+ <strong>Returns:</strong> `Promise<BlazeDiffResult>`
79
+
80
+ <table>
81
+ <tr>
82
+ <th width="500">Option</th>
83
+ <th width="500">Type</th>
84
+ <th width="500">Default</th>
85
+ <th width="500">Description</th>
86
+ </tr>
87
+ <tr>
88
+ <td><code>threshold</code></td>
89
+ <td>number</td>
90
+ <td>0.1</td>
91
+ <td>Color difference threshold (0.0-1.0). Lower = more strict</td>
92
+ </tr>
93
+ <tr>
94
+ <td><code>antialiasing</code></td>
95
+ <td>boolean</td>
96
+ <td>false</td>
97
+ <td>Enable anti-aliasing detection to exclude AA pixels from diff count</td>
98
+ </tr>
99
+ <tr>
100
+ <td><code>diffMask</code></td>
101
+ <td>boolean</td>
102
+ <td>false</td>
103
+ <td>Output only differences with transparent background</td>
104
+ </tr>
105
+ </table>
106
+
107
+ ### Result Types
108
+
109
+ ```typescript
110
+ type BlazeDiffResult =
111
+ | { match: true }
112
+ | { match: false; reason: "layout-diff" }
113
+ | { match: false; reason: "pixel-diff"; diffCount: number; diffPercentage: number }
114
+ | { match: false; reason: "file-not-exists"; file: string };
115
+ ```
116
+
117
+ ## Usage
118
+
119
+ ### Programmatic API
120
+
121
+ ```typescript
122
+ import { compare } from '@blazediff/core-native';
123
+
124
+ const result = await compare('expected.png', 'actual.png', 'diff.png', {
125
+ threshold: 0.1,
126
+ antialiasing: true,
127
+ });
128
+
129
+ if (result.match) {
130
+ console.log('Images are identical!');
131
+ } else if (result.reason === 'pixel-diff') {
132
+ console.log(`${result.diffCount} pixels differ (${result.diffPercentage.toFixed(2)}%)`);
133
+ } else if (result.reason === 'layout-diff') {
134
+ console.log('Images have different dimensions');
135
+ }
136
+ ```
137
+
138
+ ### CLI Usage
139
+
140
+ ```bash
141
+ # Compare two PNG images
142
+ npx blazediff expected.png actual.png diff.png
143
+
144
+ # Compare two JPEG images
145
+ npx blazediff expected.jpg actual.jpg diff.jpg
146
+
147
+ # Compare two QOI images
148
+ npx blazediff expected.qoi actual.qoi diff.qoi
149
+
150
+ # Mixed formats (PNG input, QOI output - recommended for smallest diff files)
151
+ npx blazediff expected.png actual.png diff.qoi
152
+
153
+ # With options
154
+ npx blazediff expected.png actual.png diff.png --threshold 0.05 --antialiasing
155
+
156
+ # With higher PNG compression (smaller output file, slower)
157
+ npx blazediff expected.png actual.png diff.png -c 6
158
+
159
+ # With JPEG quality setting
160
+ npx blazediff expected.jpg actual.jpg diff.jpg -q 85
161
+
162
+ # Output as JSON
163
+ npx blazediff expected.png actual.png diff.png --output-format json
164
+ ```
165
+
166
+ ### CLI Options
167
+
168
+ ```
169
+ Usage: blazediff [OPTIONS] <IMAGE1> <IMAGE2> [OUTPUT]
170
+
171
+ Arguments:
172
+ <IMAGE1> First image path (PNG, JPEG, or QOI)
173
+ <IMAGE2> Second image path (PNG, JPEG, or QOI)
174
+ [OUTPUT] Output diff image path (optional, format detected from extension)
175
+
176
+ Options:
177
+ -t, --threshold <THRESHOLD> Color difference threshold (0.0-1.0) [default: 0.1]
178
+ -a, --antialiasing Enable anti-aliasing detection
179
+ --diff-mask Output only differences (transparent background)
180
+ -c, --compression <LEVEL> PNG compression level (0-9, 0=fastest, 9=smallest) [default: 0]
181
+ -q, --quality <QUALITY> JPEG quality (1-100) [default: 90]
182
+ --output-format <FORMAT> Output format (json or text) [default: json]
183
+ -h, --help Print help
184
+ -V, --version Print version
185
+ ```
186
+
187
+ ### Supported Formats
188
+
189
+ | Format | Extensions | Notes |
190
+ |--------|------------|-------|
191
+ | PNG | `.png` | Lossless, supports transparency |
192
+ | JPEG | `.jpg`, `.jpeg` | Lossy, smaller file sizes |
193
+ | QOI | `.qoi` | Fast lossless, ideal for diff outputs (12x smaller than uncompressed PNG) |
194
+
195
+ Input images can be mixed formats (e.g., compare PNG to JPEG). Output format is determined by the output file extension.
196
+
197
+ **QOI for diff outputs:** QOI excels at encoding diff images with large uniform areas, producing files 12x smaller than PNG (level 0) while being faster to encode.
198
+
199
+ ### Exit Codes
200
+
201
+ - `0` - Images are identical
202
+ - `1` - Images differ (includes layout/size mismatch)
203
+ - `2` - Error (file not found, invalid format, etc.)
204
+
205
+ ## Performance
206
+
207
+ Benchmarked on Apple M1 Pro with 5600x3200 4K PNG images:
208
+
209
+ | Tool | Benchmark Time | vs blazediff |
210
+ |------|------|--------------|
211
+ | **blazediff** | ~327ms | - |
212
+ | odiff | ~1215ms | 3.4x slower |
213
+
214
+ Binary sizes (stripped, LTO optimized) - **~3x smaller than odiff**:
215
+
216
+ | Platform | blazediff | odiff |
217
+ |----------|-----------|-------|
218
+ | macOS ARM64 | 702 KB | 2.2 MB |
219
+ | macOS x64 | 773 KB | 2.6 MB |
220
+ | Linux ARM64 | 753 KB | 2.3 MB |
221
+ | Linux x64 | 869 KB | 2.9 MB |
222
+ | Windows ARM64 | 580 KB | 2.4 MB |
223
+ | Windows x64 | 915 KB | 3.0 MB |
224
+
225
+ ## Algorithm
226
+
227
+ BlazeDiff uses a two-pass block-based approach with SIMD acceleration:
228
+
229
+ 1. **Cold Pass**: Scans image in 8x8 blocks using 32-bit integer comparison to identify changed regions
230
+ 2. **Hot Pass**: Only processes blocks marked as changed, applying YIQ perceptual color difference
231
+ 3. **SIMD**: Uses NEON (ARM) or SSE4.1 (x86) for parallel pixel processing
232
+ 4. **Anti-aliasing**: Implements Vysniauskas (2009) algorithm to detect AA artifacts
233
+
234
+ ## References
235
+
236
+ - **YIQ Color Space**: [Kotsarenko & Ramos (2009)](https://doaj.org/article/b2e3b5088ba943eebd9af2927fef08ad) - "Measuring perceived color difference using YIQ NTSC transmission color space"
237
+ - **Anti-Aliasing Detection**: [Vysniauskas (2009)](https://www.researchgate.net/publication/234073157_Anti-aliased_Pixel_and_Intensity_Slope_Detector) - "Anti-aliased Pixel and Intensity Slope Detector"
238
+ - **Inspiration**: [odiff](https://github.com/dmtrKovalenko/odiff) - Fast image comparison tool written in Zig
@@ -0,0 +1,59 @@
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
+ /** PNG compression level (0-9, 0=fastest/largest, 9=slowest/smallest) */
9
+ compression?: number;
10
+ /** JPEG quality (1-100). Default: 90 */
11
+ quality?: 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 images (PNG or JPEG) and optionally generate a diff image.
30
+ *
31
+ * Uses native N-API bindings when available for ~10-100x better performance
32
+ * on small images (no process spawn overhead). Falls back to execFile if
33
+ * native bindings are unavailable.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * // With diff output
38
+ * const result = await compare('expected.png', 'actual.png', 'diff.png');
39
+ *
40
+ * // Without diff output (faster, just returns comparison result)
41
+ * const result = await compare('expected.png', 'actual.png');
42
+ *
43
+ * if (result.match) {
44
+ * console.log('Images identical');
45
+ * } else if (result.reason === 'pixel-diff') {
46
+ * console.log(`${result.diffCount} pixels differ`);
47
+ * }
48
+ * ```
49
+ */
50
+ declare function compare(basePath: string, comparePath: string, diffOutput?: string, options?: BlazeDiffOptions): Promise<BlazeDiffResult>;
51
+ /** Get the path to the blazediff binary for direct CLI usage. */
52
+ declare function getBinaryPath(): string;
53
+ /**
54
+ * Check if native N-API bindings are available.
55
+ * Returns true if the native module loaded successfully.
56
+ */
57
+ declare function hasNativeBinding(): boolean;
58
+
59
+ export { type BlazeDiffOptions, type BlazeDiffResult, compare, getBinaryPath, hasNativeBinding };
package/dist/index.js ADDED
@@ -0,0 +1,293 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ compare: () => compare,
34
+ getBinaryPath: () => getBinaryPath,
35
+ hasNativeBinding: () => hasNativeBinding
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // ../../node_modules/.pnpm/tsup@8.5.0_jiti@2.6.0_postcss@8.5.6_tsx@4.20.6_typescript@5.9.2_yaml@2.8.1/node_modules/tsup/assets/cjs_shims.js
40
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.src || new URL("main.js", document.baseURI).href;
41
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
42
+
43
+ // src/index.ts
44
+ var import_node_child_process = require("child_process");
45
+ var import_node_fs = require("fs");
46
+ var import_node_module = require("module");
47
+ var import_node_os = __toESM(require("os"));
48
+ var import_node_path = __toESM(require("path"));
49
+ var import_node_url = require("url");
50
+ var import_node_util = require("util");
51
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
52
+ var PLATFORM_PACKAGES = {
53
+ "darwin-arm64": {
54
+ packageName: "@blazediff/core-native-darwin-arm64",
55
+ packageDir: "core-native-darwin-arm64"
56
+ },
57
+ "darwin-x64": {
58
+ packageName: "@blazediff/core-native-darwin-x64",
59
+ packageDir: "core-native-darwin-x64"
60
+ },
61
+ "linux-arm64": {
62
+ packageName: "@blazediff/core-native-linux-arm64",
63
+ packageDir: "core-native-linux-arm64"
64
+ },
65
+ "linux-x64": {
66
+ packageName: "@blazediff/core-native-linux-x64",
67
+ packageDir: "core-native-linux-x64"
68
+ },
69
+ "win32-arm64": {
70
+ packageName: "@blazediff/core-native-win32-arm64",
71
+ packageDir: "core-native-win32-arm64"
72
+ },
73
+ "win32-x64": {
74
+ packageName: "@blazediff/core-native-win32-x64",
75
+ packageDir: "core-native-win32-x64"
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
+ compression: options?.compression,
149
+ quality: options?.quality
150
+ };
151
+ }
152
+ function resolveBinaryPath() {
153
+ const platform = import_node_os.default.platform();
154
+ const arch = import_node_os.default.arch();
155
+ const key = `${platform}-${arch}`;
156
+ const platformInfo = PLATFORM_PACKAGES[key];
157
+ if (!platformInfo) {
158
+ throw new Error(
159
+ `Unsupported platform: ${key}. Supported: ${Object.keys(PLATFORM_PACKAGES).join(", ")}`
160
+ );
161
+ }
162
+ const binaryName = platform === "win32" ? "blazediff.exe" : "blazediff";
163
+ try {
164
+ const require2 = (0, import_node_module.createRequire)(importMetaUrl);
165
+ const packagePath = require2.resolve(
166
+ `${platformInfo.packageName}/package.json`
167
+ );
168
+ const packageDir = import_node_path.default.dirname(packagePath);
169
+ const binaryPath = import_node_path.default.join(packageDir, binaryName);
170
+ if ((0, import_node_fs.existsSync)(binaryPath)) {
171
+ return binaryPath;
172
+ }
173
+ } catch {
174
+ }
175
+ const currentDir = import_node_path.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
176
+ const packagesDir = import_node_path.default.resolve(currentDir, "..", "..");
177
+ const siblingPath = import_node_path.default.join(
178
+ packagesDir,
179
+ platformInfo.packageDir,
180
+ binaryName
181
+ );
182
+ if ((0, import_node_fs.existsSync)(siblingPath)) {
183
+ return siblingPath;
184
+ }
185
+ throw new Error(
186
+ `Platform package ${platformInfo.packageName} is not installed. This usually means the optional dependency wasn't installed for your platform. Try reinstalling with: npm install @blazediff/core-native`
187
+ );
188
+ }
189
+ var cachedBinaryPath = null;
190
+ function getBinaryPathInternal() {
191
+ if (!cachedBinaryPath) {
192
+ cachedBinaryPath = resolveBinaryPath();
193
+ }
194
+ return cachedBinaryPath;
195
+ }
196
+ function buildArgs(diffOutput, options) {
197
+ const args = [];
198
+ if (diffOutput) args.push(diffOutput);
199
+ args.push("--output-format=json");
200
+ if (!options) return args;
201
+ if (options.threshold !== void 0)
202
+ args.push(`--threshold=${options.threshold}`);
203
+ if (options.antialiasing) args.push("--antialiasing");
204
+ if (options.diffMask) args.push("--diff-mask");
205
+ if (options.compression !== void 0)
206
+ args.push(`--compression=${options.compression}`);
207
+ if (options.quality !== void 0) args.push(`--quality=${options.quality}`);
208
+ return args;
209
+ }
210
+ function parseJsonOutput(text) {
211
+ try {
212
+ return JSON.parse(text);
213
+ } catch {
214
+ return null;
215
+ }
216
+ }
217
+ function detectMissingFile(error, basePath, comparePath) {
218
+ if (!/Failed to load images:.*(?:No such file|not found)/i.test(error)) {
219
+ return null;
220
+ }
221
+ if (error.includes(basePath)) return basePath;
222
+ if (error.includes(comparePath)) return comparePath;
223
+ return basePath;
224
+ }
225
+ async function execFileCompare(basePath, comparePath, diffOutput, options) {
226
+ const binaryPath = getBinaryPathInternal();
227
+ const args = [basePath, comparePath, ...buildArgs(diffOutput, options)];
228
+ try {
229
+ await execFileAsync(binaryPath, args);
230
+ return { match: true };
231
+ } catch (err) {
232
+ const { code, stdout, stderr } = err;
233
+ const output = stdout || stderr || "";
234
+ if (code === 1) {
235
+ const json = parseJsonOutput(output);
236
+ if (json?.error?.includes("Layout differs")) {
237
+ return { match: false, reason: "layout-diff" };
238
+ }
239
+ if (json) {
240
+ return {
241
+ match: false,
242
+ reason: "pixel-diff",
243
+ diffCount: json.diffCount,
244
+ diffPercentage: json.diffPercentage
245
+ };
246
+ }
247
+ if (output.includes("Layout differs")) {
248
+ return { match: false, reason: "layout-diff" };
249
+ }
250
+ }
251
+ if (code === 2) {
252
+ const missingFile = detectMissingFile(output, basePath, comparePath);
253
+ if (missingFile) {
254
+ return { match: false, reason: "file-not-exists", file: missingFile };
255
+ }
256
+ }
257
+ throw new Error(output || `blazediff exited with code ${code}`);
258
+ }
259
+ }
260
+ async function compare(basePath, comparePath, diffOutput, options) {
261
+ const binding = tryLoadNativeBinding();
262
+ if (binding) {
263
+ try {
264
+ const result = binding.compare(
265
+ basePath,
266
+ comparePath,
267
+ diffOutput ?? null,
268
+ convertToNapiOptions(options)
269
+ );
270
+ return convertNapiResult(result);
271
+ } catch (err) {
272
+ const message = err instanceof Error ? err.message : String(err);
273
+ const missingFile = detectMissingFile(message, basePath, comparePath);
274
+ if (missingFile) {
275
+ return { match: false, reason: "file-not-exists", file: missingFile };
276
+ }
277
+ throw err;
278
+ }
279
+ }
280
+ return execFileCompare(basePath, comparePath, diffOutput, options);
281
+ }
282
+ function getBinaryPath() {
283
+ return getBinaryPathInternal();
284
+ }
285
+ function hasNativeBinding() {
286
+ return tryLoadNativeBinding() !== null;
287
+ }
288
+ // Annotate the CommonJS export names for ESM import in node:
289
+ 0 && (module.exports = {
290
+ compare,
291
+ getBinaryPath,
292
+ hasNativeBinding
293
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,250 @@
1
+ // src/index.ts
2
+ import { execFile } from "child_process";
3
+ import { existsSync } from "fs";
4
+ import { createRequire } from "module";
5
+ import os from "os";
6
+ import path from "path";
7
+ import { fileURLToPath } from "url";
8
+ import { promisify } from "util";
9
+ var execFileAsync = promisify(execFile);
10
+ var PLATFORM_PACKAGES = {
11
+ "darwin-arm64": {
12
+ packageName: "@blazediff/core-native-darwin-arm64",
13
+ packageDir: "core-native-darwin-arm64"
14
+ },
15
+ "darwin-x64": {
16
+ packageName: "@blazediff/core-native-darwin-x64",
17
+ packageDir: "core-native-darwin-x64"
18
+ },
19
+ "linux-arm64": {
20
+ packageName: "@blazediff/core-native-linux-arm64",
21
+ packageDir: "core-native-linux-arm64"
22
+ },
23
+ "linux-x64": {
24
+ packageName: "@blazediff/core-native-linux-x64",
25
+ packageDir: "core-native-linux-x64"
26
+ },
27
+ "win32-arm64": {
28
+ packageName: "@blazediff/core-native-win32-arm64",
29
+ packageDir: "core-native-win32-arm64"
30
+ },
31
+ "win32-x64": {
32
+ packageName: "@blazediff/core-native-win32-x64",
33
+ packageDir: "core-native-win32-x64"
34
+ }
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
+ compression: options?.compression,
107
+ quality: options?.quality
108
+ };
109
+ }
110
+ function resolveBinaryPath() {
111
+ const platform = os.platform();
112
+ const arch = os.arch();
113
+ const key = `${platform}-${arch}`;
114
+ const platformInfo = PLATFORM_PACKAGES[key];
115
+ if (!platformInfo) {
116
+ throw new Error(
117
+ `Unsupported platform: ${key}. Supported: ${Object.keys(PLATFORM_PACKAGES).join(", ")}`
118
+ );
119
+ }
120
+ const binaryName = platform === "win32" ? "blazediff.exe" : "blazediff";
121
+ try {
122
+ const require2 = createRequire(import.meta.url);
123
+ const packagePath = require2.resolve(
124
+ `${platformInfo.packageName}/package.json`
125
+ );
126
+ const packageDir = path.dirname(packagePath);
127
+ const binaryPath = path.join(packageDir, binaryName);
128
+ if (existsSync(binaryPath)) {
129
+ return binaryPath;
130
+ }
131
+ } catch {
132
+ }
133
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
134
+ const packagesDir = path.resolve(currentDir, "..", "..");
135
+ const siblingPath = path.join(
136
+ packagesDir,
137
+ platformInfo.packageDir,
138
+ binaryName
139
+ );
140
+ if (existsSync(siblingPath)) {
141
+ return siblingPath;
142
+ }
143
+ throw new Error(
144
+ `Platform package ${platformInfo.packageName} is not installed. This usually means the optional dependency wasn't installed for your platform. Try reinstalling with: npm install @blazediff/core-native`
145
+ );
146
+ }
147
+ var cachedBinaryPath = null;
148
+ function getBinaryPathInternal() {
149
+ if (!cachedBinaryPath) {
150
+ cachedBinaryPath = resolveBinaryPath();
151
+ }
152
+ return cachedBinaryPath;
153
+ }
154
+ function buildArgs(diffOutput, options) {
155
+ const args = [];
156
+ if (diffOutput) args.push(diffOutput);
157
+ args.push("--output-format=json");
158
+ if (!options) return args;
159
+ if (options.threshold !== void 0)
160
+ args.push(`--threshold=${options.threshold}`);
161
+ if (options.antialiasing) args.push("--antialiasing");
162
+ if (options.diffMask) args.push("--diff-mask");
163
+ if (options.compression !== void 0)
164
+ args.push(`--compression=${options.compression}`);
165
+ if (options.quality !== void 0) args.push(`--quality=${options.quality}`);
166
+ return args;
167
+ }
168
+ function parseJsonOutput(text) {
169
+ try {
170
+ return JSON.parse(text);
171
+ } catch {
172
+ return null;
173
+ }
174
+ }
175
+ function detectMissingFile(error, basePath, comparePath) {
176
+ if (!/Failed to load images:.*(?:No such file|not found)/i.test(error)) {
177
+ return null;
178
+ }
179
+ if (error.includes(basePath)) return basePath;
180
+ if (error.includes(comparePath)) return comparePath;
181
+ return basePath;
182
+ }
183
+ async function execFileCompare(basePath, comparePath, diffOutput, options) {
184
+ const binaryPath = getBinaryPathInternal();
185
+ const args = [basePath, comparePath, ...buildArgs(diffOutput, options)];
186
+ try {
187
+ await execFileAsync(binaryPath, args);
188
+ return { match: true };
189
+ } catch (err) {
190
+ const { code, stdout, stderr } = err;
191
+ const output = stdout || stderr || "";
192
+ if (code === 1) {
193
+ const json = parseJsonOutput(output);
194
+ if (json?.error?.includes("Layout differs")) {
195
+ return { match: false, reason: "layout-diff" };
196
+ }
197
+ if (json) {
198
+ return {
199
+ match: false,
200
+ reason: "pixel-diff",
201
+ diffCount: json.diffCount,
202
+ diffPercentage: json.diffPercentage
203
+ };
204
+ }
205
+ if (output.includes("Layout differs")) {
206
+ return { match: false, reason: "layout-diff" };
207
+ }
208
+ }
209
+ if (code === 2) {
210
+ const missingFile = detectMissingFile(output, basePath, comparePath);
211
+ if (missingFile) {
212
+ return { match: false, reason: "file-not-exists", file: missingFile };
213
+ }
214
+ }
215
+ throw new Error(output || `blazediff exited with code ${code}`);
216
+ }
217
+ }
218
+ async function compare(basePath, comparePath, diffOutput, options) {
219
+ const binding = tryLoadNativeBinding();
220
+ if (binding) {
221
+ try {
222
+ const result = binding.compare(
223
+ basePath,
224
+ comparePath,
225
+ diffOutput ?? null,
226
+ convertToNapiOptions(options)
227
+ );
228
+ return convertNapiResult(result);
229
+ } catch (err) {
230
+ const message = err instanceof Error ? err.message : String(err);
231
+ const missingFile = detectMissingFile(message, basePath, comparePath);
232
+ if (missingFile) {
233
+ return { match: false, reason: "file-not-exists", file: missingFile };
234
+ }
235
+ throw err;
236
+ }
237
+ }
238
+ return execFileCompare(basePath, comparePath, diffOutput, options);
239
+ }
240
+ function getBinaryPath() {
241
+ return getBinaryPathInternal();
242
+ }
243
+ function hasNativeBinding() {
244
+ return tryLoadNativeBinding() !== null;
245
+ }
246
+ export {
247
+ compare,
248
+ getBinaryPath,
249
+ hasNativeBinding
250
+ };
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@blazediff/core-native",
3
+ "version": "4.0.0",
4
+ "description": "Native Rust binaries for blazediff - the fastest image diff in the world",
5
+ "private": false,
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "main": "dist/index.js",
10
+ "module": "dist/index.mjs",
11
+ "types": "dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.mjs",
16
+ "require": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "optionalDependencies": {
23
+ "@blazediff/core-native-darwin-arm64": "4.0.0",
24
+ "@blazediff/core-native-darwin-x64": "4.0.0",
25
+ "@blazediff/core-native-linux-arm64": "4.0.0",
26
+ "@blazediff/core-native-linux-x64": "4.0.0",
27
+ "@blazediff/core-native-win32-arm64": "4.0.0",
28
+ "@blazediff/core-native-win32-x64": "4.0.0"
29
+ },
30
+ "keywords": [
31
+ "image",
32
+ "comparison",
33
+ "diff",
34
+ "pixel",
35
+ "visual-testing",
36
+ "visual-regression",
37
+ "screenshot",
38
+ "native",
39
+ "rust",
40
+ "simd",
41
+ "fast"
42
+ ],
43
+ "author": "Teimur Gasanov <me@teimurjan.dev> (https://github.com/teimurjan)",
44
+ "repository": "https://github.com/teimurjan/blazediff",
45
+ "homepage": "https://blazediff.dev",
46
+ "license": "MIT",
47
+ "devDependencies": {
48
+ "@types/node": "^24.3.0",
49
+ "tsup": "8.5.0",
50
+ "typescript": "5.9.2",
51
+ "vitest": "^3.2.4"
52
+ },
53
+ "scripts": {
54
+ "typecheck": "tsc --noEmit",
55
+ "build": "tsup",
56
+ "clean": "rm -rf dist",
57
+ "test": "vitest run",
58
+ "test:watch": "vitest"
59
+ }
60
+ }