@blazediff/core-native 4.0.0 → 4.1.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/dist/index.d.ts CHANGED
@@ -9,9 +9,14 @@ interface BlazeDiffOptions {
9
9
  compression?: number;
10
10
  /** JPEG quality (1-100). Default: 90 */
11
11
  quality?: number;
12
+ /** Run structured interpretation after raw pixel diff */
13
+ interpret?: boolean;
14
+ /** Output format for diff: "png" (default) or "html" (interpret report) */
15
+ outputFormat?: "png" | "html";
12
16
  }
13
17
  type BlazeDiffResult = {
14
18
  match: true;
19
+ interpretation?: InterpretResult;
15
20
  } | {
16
21
  match: false;
17
22
  reason: "layout-diff";
@@ -20,11 +25,66 @@ type BlazeDiffResult = {
20
25
  reason: "pixel-diff";
21
26
  diffCount: number;
22
27
  diffPercentage: number;
28
+ interpretation?: InterpretResult;
23
29
  } | {
24
30
  match: false;
25
31
  reason: "file-not-exists";
26
32
  file: string;
27
33
  };
34
+ interface BoundingBox {
35
+ x: number;
36
+ y: number;
37
+ width: number;
38
+ height: number;
39
+ }
40
+ interface ShapeStats {
41
+ fillRatio: number;
42
+ borderRatio: number;
43
+ innerFillRatio: number;
44
+ centerDensity: number;
45
+ rowOccupancy: number;
46
+ colOccupancy: number;
47
+ }
48
+ interface ColorDeltaStats {
49
+ meanDelta: number;
50
+ maxDelta: number;
51
+ }
52
+ interface GradientStats {
53
+ edgeScore: number;
54
+ }
55
+ interface ClassificationSignals {
56
+ blendsWithBgInImg1: boolean;
57
+ blendsWithBgInImg2: boolean;
58
+ lowColorDelta: boolean;
59
+ lowEdgeChange: boolean;
60
+ denseFill: boolean;
61
+ sparseFill: boolean;
62
+ tinyRegion: boolean;
63
+ confidence: number;
64
+ }
65
+ interface ChangeRegion {
66
+ bbox: BoundingBox;
67
+ pixelCount: number;
68
+ percentage: number;
69
+ position: string;
70
+ shape: string;
71
+ shapeStats: ShapeStats;
72
+ changeType: string;
73
+ signals: ClassificationSignals;
74
+ confidence: number;
75
+ colorDelta: ColorDeltaStats;
76
+ gradient: GradientStats;
77
+ }
78
+ interface InterpretResult {
79
+ summary: string;
80
+ diffCount: number;
81
+ totalRegions: number;
82
+ regions: ChangeRegion[];
83
+ severity: string;
84
+ diffPercentage: number;
85
+ width: number;
86
+ height: number;
87
+ }
28
88
  /**
29
89
  * Compare two images (PNG or JPEG) and optionally generate a diff image.
30
90
  *
@@ -55,5 +115,21 @@ declare function getBinaryPath(): string;
55
115
  * Returns true if the native module loaded successfully.
56
116
  */
57
117
  declare function hasNativeBinding(): boolean;
118
+ /**
119
+ * Interpret the diff between two images, returning structured analysis results.
120
+ *
121
+ * Uses native N-API bindings when available for better performance.
122
+ * Falls back to execFile if native bindings are unavailable.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * const result = await interpret('expected.png', 'actual.png');
127
+ * console.log(result.summary);
128
+ * for (const region of result.regions) {
129
+ * console.log(`${region.position}: ${region.changeType} (${region.percentage.toFixed(2)}%)`);
130
+ * }
131
+ * ```
132
+ */
133
+ declare function interpret(image1Path: string, image2Path: string, options?: Pick<BlazeDiffOptions, "threshold" | "antialiasing">): Promise<InterpretResult>;
58
134
 
59
- export { type BlazeDiffOptions, type BlazeDiffResult, compare, getBinaryPath, hasNativeBinding };
135
+ export { type BlazeDiffOptions, type BlazeDiffResult, type BoundingBox, type ChangeRegion, type ClassificationSignals, type ColorDeltaStats, type GradientStats, type InterpretResult, type ShapeStats, compare, getBinaryPath, hasNativeBinding, interpret };
package/dist/index.js CHANGED
@@ -32,7 +32,8 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  compare: () => compare,
34
34
  getBinaryPath: () => getBinaryPath,
35
- hasNativeBinding: () => hasNativeBinding
35
+ hasNativeBinding: () => hasNativeBinding,
36
+ interpret: () => interpret
36
37
  });
37
38
  module.exports = __toCommonJS(index_exports);
38
39
 
@@ -119,25 +120,19 @@ function tryLoadNativeBinding() {
119
120
  return null;
120
121
  }
121
122
  function convertNapiResult(result) {
123
+ const interpretation = result.interpretation ?? void 0;
122
124
  if (result.matchResult) {
123
- return { match: true };
125
+ return { match: true, interpretation };
124
126
  }
125
127
  if (result.reason === "layout-diff") {
126
128
  return { match: false, reason: "layout-diff" };
127
129
  }
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
130
  return {
137
131
  match: false,
138
132
  reason: "pixel-diff",
139
133
  diffCount: result.diffCount ?? 0,
140
- diffPercentage: result.diffPercentage ?? 0
134
+ diffPercentage: result.diffPercentage ?? 0,
135
+ interpretation
141
136
  };
142
137
  }
143
138
  function convertToNapiOptions(options) {
@@ -146,7 +141,9 @@ function convertToNapiOptions(options) {
146
141
  antialiasing: options?.antialiasing,
147
142
  diffMask: options?.diffMask,
148
143
  compression: options?.compression,
149
- quality: options?.quality
144
+ quality: options?.quality,
145
+ interpret: options?.interpret,
146
+ outputFormat: options?.outputFormat
150
147
  };
151
148
  }
152
149
  function resolveBinaryPath() {
@@ -195,16 +192,21 @@ function getBinaryPathInternal() {
195
192
  }
196
193
  function buildArgs(diffOutput, options) {
197
194
  const args = [];
195
+ const useInterpret = options?.interpret || options?.outputFormat === "html";
198
196
  if (diffOutput) args.push(diffOutput);
197
+ if (useInterpret) args.push("--interpret");
199
198
  args.push("--output-format=json");
200
199
  if (!options) return args;
201
200
  if (options.threshold !== void 0)
202
201
  args.push(`--threshold=${options.threshold}`);
203
202
  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}`);
203
+ if (!useInterpret) {
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)
208
+ args.push(`--quality=${options.quality}`);
209
+ }
208
210
  return args;
209
211
  }
210
212
  function parseJsonOutput(text) {
@@ -225,6 +227,9 @@ function detectMissingFile(error, basePath, comparePath) {
225
227
  async function execFileCompare(basePath, comparePath, diffOutput, options) {
226
228
  const binaryPath = getBinaryPathInternal();
227
229
  const args = [basePath, comparePath, ...buildArgs(diffOutput, options)];
230
+ if (options?.interpret || options?.outputFormat === "html") {
231
+ return execFileInterpretCompare(binaryPath, args, basePath, comparePath);
232
+ }
228
233
  try {
229
234
  await execFileAsync(binaryPath, args);
230
235
  return { match: true };
@@ -257,6 +262,35 @@ async function execFileCompare(basePath, comparePath, diffOutput, options) {
257
262
  throw new Error(output || `blazediff exited with code ${code}`);
258
263
  }
259
264
  }
265
+ async function execFileInterpretCompare(binaryPath, args, basePath, comparePath) {
266
+ try {
267
+ const { stdout } = await execFileAsync(binaryPath, args);
268
+ const interpretation = JSON.parse(stdout);
269
+ return { match: true, interpretation };
270
+ } catch (err) {
271
+ const { code, stdout, stderr } = err;
272
+ if (code === 1 && stdout) {
273
+ const interpretation = JSON.parse(stdout);
274
+ return {
275
+ match: false,
276
+ reason: "pixel-diff",
277
+ diffCount: interpretation.diffCount,
278
+ diffPercentage: interpretation.diffPercentage,
279
+ interpretation
280
+ };
281
+ }
282
+ const errorOutput = stderr || stdout || "";
283
+ if (code === 2) {
284
+ const missingFile = detectMissingFile(errorOutput, basePath, comparePath);
285
+ if (missingFile) {
286
+ return { match: false, reason: "file-not-exists", file: missingFile };
287
+ }
288
+ }
289
+ throw new Error(
290
+ errorOutput || `blazediff --interpret exited with code ${code}`
291
+ );
292
+ }
293
+ }
260
294
  async function compare(basePath, comparePath, diffOutput, options) {
261
295
  const binding = tryLoadNativeBinding();
262
296
  if (binding) {
@@ -285,9 +319,27 @@ function getBinaryPath() {
285
319
  function hasNativeBinding() {
286
320
  return tryLoadNativeBinding() !== null;
287
321
  }
322
+ async function interpret(image1Path, image2Path, options) {
323
+ const binding = tryLoadNativeBinding();
324
+ if (binding) {
325
+ return binding.interpretImages(image1Path, image2Path, {
326
+ threshold: options?.threshold,
327
+ antialiasing: options?.antialiasing
328
+ });
329
+ }
330
+ const result = await compare(image1Path, image2Path, void 0, {
331
+ ...options,
332
+ interpret: true
333
+ });
334
+ if ("interpretation" in result && result.interpretation) {
335
+ return result.interpretation;
336
+ }
337
+ throw new Error("Interpretation result missing from compare");
338
+ }
288
339
  // Annotate the CommonJS export names for ESM import in node:
289
340
  0 && (module.exports = {
290
341
  compare,
291
342
  getBinaryPath,
292
- hasNativeBinding
343
+ hasNativeBinding,
344
+ interpret
293
345
  });
package/dist/index.mjs CHANGED
@@ -77,25 +77,19 @@ function tryLoadNativeBinding() {
77
77
  return null;
78
78
  }
79
79
  function convertNapiResult(result) {
80
+ const interpretation = result.interpretation ?? void 0;
80
81
  if (result.matchResult) {
81
- return { match: true };
82
+ return { match: true, interpretation };
82
83
  }
83
84
  if (result.reason === "layout-diff") {
84
85
  return { match: false, reason: "layout-diff" };
85
86
  }
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
87
  return {
95
88
  match: false,
96
89
  reason: "pixel-diff",
97
90
  diffCount: result.diffCount ?? 0,
98
- diffPercentage: result.diffPercentage ?? 0
91
+ diffPercentage: result.diffPercentage ?? 0,
92
+ interpretation
99
93
  };
100
94
  }
101
95
  function convertToNapiOptions(options) {
@@ -104,7 +98,9 @@ function convertToNapiOptions(options) {
104
98
  antialiasing: options?.antialiasing,
105
99
  diffMask: options?.diffMask,
106
100
  compression: options?.compression,
107
- quality: options?.quality
101
+ quality: options?.quality,
102
+ interpret: options?.interpret,
103
+ outputFormat: options?.outputFormat
108
104
  };
109
105
  }
110
106
  function resolveBinaryPath() {
@@ -153,16 +149,21 @@ function getBinaryPathInternal() {
153
149
  }
154
150
  function buildArgs(diffOutput, options) {
155
151
  const args = [];
152
+ const useInterpret = options?.interpret || options?.outputFormat === "html";
156
153
  if (diffOutput) args.push(diffOutput);
154
+ if (useInterpret) args.push("--interpret");
157
155
  args.push("--output-format=json");
158
156
  if (!options) return args;
159
157
  if (options.threshold !== void 0)
160
158
  args.push(`--threshold=${options.threshold}`);
161
159
  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}`);
160
+ if (!useInterpret) {
161
+ if (options.diffMask) args.push("--diff-mask");
162
+ if (options.compression !== void 0)
163
+ args.push(`--compression=${options.compression}`);
164
+ if (options.quality !== void 0)
165
+ args.push(`--quality=${options.quality}`);
166
+ }
166
167
  return args;
167
168
  }
168
169
  function parseJsonOutput(text) {
@@ -183,6 +184,9 @@ function detectMissingFile(error, basePath, comparePath) {
183
184
  async function execFileCompare(basePath, comparePath, diffOutput, options) {
184
185
  const binaryPath = getBinaryPathInternal();
185
186
  const args = [basePath, comparePath, ...buildArgs(diffOutput, options)];
187
+ if (options?.interpret || options?.outputFormat === "html") {
188
+ return execFileInterpretCompare(binaryPath, args, basePath, comparePath);
189
+ }
186
190
  try {
187
191
  await execFileAsync(binaryPath, args);
188
192
  return { match: true };
@@ -215,6 +219,35 @@ async function execFileCompare(basePath, comparePath, diffOutput, options) {
215
219
  throw new Error(output || `blazediff exited with code ${code}`);
216
220
  }
217
221
  }
222
+ async function execFileInterpretCompare(binaryPath, args, basePath, comparePath) {
223
+ try {
224
+ const { stdout } = await execFileAsync(binaryPath, args);
225
+ const interpretation = JSON.parse(stdout);
226
+ return { match: true, interpretation };
227
+ } catch (err) {
228
+ const { code, stdout, stderr } = err;
229
+ if (code === 1 && stdout) {
230
+ const interpretation = JSON.parse(stdout);
231
+ return {
232
+ match: false,
233
+ reason: "pixel-diff",
234
+ diffCount: interpretation.diffCount,
235
+ diffPercentage: interpretation.diffPercentage,
236
+ interpretation
237
+ };
238
+ }
239
+ const errorOutput = stderr || stdout || "";
240
+ if (code === 2) {
241
+ const missingFile = detectMissingFile(errorOutput, basePath, comparePath);
242
+ if (missingFile) {
243
+ return { match: false, reason: "file-not-exists", file: missingFile };
244
+ }
245
+ }
246
+ throw new Error(
247
+ errorOutput || `blazediff --interpret exited with code ${code}`
248
+ );
249
+ }
250
+ }
218
251
  async function compare(basePath, comparePath, diffOutput, options) {
219
252
  const binding = tryLoadNativeBinding();
220
253
  if (binding) {
@@ -243,8 +276,26 @@ function getBinaryPath() {
243
276
  function hasNativeBinding() {
244
277
  return tryLoadNativeBinding() !== null;
245
278
  }
279
+ async function interpret(image1Path, image2Path, options) {
280
+ const binding = tryLoadNativeBinding();
281
+ if (binding) {
282
+ return binding.interpretImages(image1Path, image2Path, {
283
+ threshold: options?.threshold,
284
+ antialiasing: options?.antialiasing
285
+ });
286
+ }
287
+ const result = await compare(image1Path, image2Path, void 0, {
288
+ ...options,
289
+ interpret: true
290
+ });
291
+ if ("interpretation" in result && result.interpretation) {
292
+ return result.interpretation;
293
+ }
294
+ throw new Error("Interpretation result missing from compare");
295
+ }
246
296
  export {
247
297
  compare,
248
298
  getBinaryPath,
249
- hasNativeBinding
299
+ hasNativeBinding,
300
+ interpret
250
301
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blazediff/core-native",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
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/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"
23
+ "@blazediff/core-native-darwin-arm64": "4.1.0",
24
+ "@blazediff/core-native-darwin-x64": "4.1.0",
25
+ "@blazediff/core-native-linux-arm64": "4.1.0",
26
+ "@blazediff/core-native-linux-x64": "4.1.0",
27
+ "@blazediff/core-native-win32-arm64": "4.1.0",
28
+ "@blazediff/core-native-win32-x64": "4.1.0"
29
29
  },
30
30
  "keywords": [
31
31
  "image",