@vizzly-testing/honeydiff 0.0.2
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 +327 -0
- package/index.d.ts +276 -0
- package/index.js +24 -0
- package/load-platform.js +25 -0
- package/package.json +53 -0
- package/platforms/index-x86_64-unknown-linux-gnu.node +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# @vizzly-testing/honeydiff
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@vizzly-testing/honeydiff)
|
|
4
|
+
[](https://www.npmjs.com/package/@vizzly-testing/honeydiff)
|
|
5
|
+
[](https://github.com/vizzly-testing/honeydiff/actions)
|
|
6
|
+
[](../../LICENSE)
|
|
7
|
+
|
|
8
|
+
High-performance image diffing for Node.js - Native bindings to the Honeydiff Rust library.
|
|
9
|
+
|
|
10
|
+
Blazing fast image comparison with support for anti-aliasing detection, perceptual similarity (SSIM), spatial clustering, and variable-height screenshots. Perfect for visual regression testing.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- **🚀 Native Performance** - Rust-powered via Neon bindings (1.1B pixels/sec for Full HD)
|
|
15
|
+
- **📦 Dual Input** - Works with file paths or Buffers
|
|
16
|
+
- **⚡ Async + Sync APIs** - Choose between non-blocking promises or synchronous operations
|
|
17
|
+
- **🎯 Full TypeScript Support** - Complete type definitions included
|
|
18
|
+
- **🔍 Advanced Analysis**:
|
|
19
|
+
- Anti-aliasing detection
|
|
20
|
+
- SSIM perceptual similarity scoring
|
|
21
|
+
- Spatial clustering of diff regions
|
|
22
|
+
- Intensity statistics (min, max, mean, median, std dev)
|
|
23
|
+
- Bounding box detection
|
|
24
|
+
- **📸 Variable Height Support** - Compare scrollable screenshots with different heights
|
|
25
|
+
- **🎨 Visual Artifacts** - Generate diff, mask, and overlay images
|
|
26
|
+
- **⚙️ Flexible Options** - Pixel tolerance, color thresholds, custom diff colors
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install @vizzly-testing/honeydiff
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Requirements:**
|
|
35
|
+
- Node.js 22+
|
|
36
|
+
|
|
37
|
+
**Pre-built Binaries:**
|
|
38
|
+
This package ships with pre-built native binaries for:
|
|
39
|
+
- macOS (Apple Silicon arm64)
|
|
40
|
+
- Linux (x64 and arm64)
|
|
41
|
+
- Windows (x64)
|
|
42
|
+
|
|
43
|
+
No Rust toolchain required! The correct binary is automatically loaded for your platform.
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
### JavaScript (Async - Recommended)
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
const { compare, quickCompare } = require('@vizzly-testing/honeydiff');
|
|
51
|
+
|
|
52
|
+
// Quick boolean check
|
|
53
|
+
if (await quickCompare('baseline.png', 'current.png')) {
|
|
54
|
+
console.log('Visual regression detected!');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Detailed comparison
|
|
58
|
+
const result = await compare('baseline.png', 'current.png', {
|
|
59
|
+
pixelTolerance: 10,
|
|
60
|
+
includeSSIM: true
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
console.log(`Diff: ${result.diffPercentage.toFixed(2)}%`);
|
|
64
|
+
console.log(`SSIM: ${result.perceptualScore}`);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### TypeScript
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { compare, CompareOptions, DiffResult } from '@vizzly-testing/honeydiff';
|
|
71
|
+
|
|
72
|
+
const options: CompareOptions = {
|
|
73
|
+
pixelTolerance: 5,
|
|
74
|
+
includeSSIM: true,
|
|
75
|
+
includeClusters: true
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const result: DiffResult = await compare('img1.png', 'img2.png', options);
|
|
79
|
+
|
|
80
|
+
if (result.diffClusters) {
|
|
81
|
+
console.log(`Found ${result.diffClusters.length} diff regions`);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## API Reference
|
|
86
|
+
|
|
87
|
+
### Async API (Recommended)
|
|
88
|
+
|
|
89
|
+
#### `compare(img1, img2, options?): Promise<DiffResult>`
|
|
90
|
+
|
|
91
|
+
Compare two images asynchronously. Non-blocking operation.
|
|
92
|
+
|
|
93
|
+
**Parameters:**
|
|
94
|
+
- `img1`: `string | Buffer` - First image (file path or Buffer)
|
|
95
|
+
- `img2`: `string | Buffer` - Second image (file path or Buffer)
|
|
96
|
+
- `options`: `CompareOptions` (optional) - Comparison configuration
|
|
97
|
+
|
|
98
|
+
**Returns:** `Promise<DiffResult>` - Detailed diff results
|
|
99
|
+
|
|
100
|
+
**Example:**
|
|
101
|
+
```javascript
|
|
102
|
+
const result = await compare('baseline.png', 'current.png', {
|
|
103
|
+
pixelTolerance: 10,
|
|
104
|
+
antialiasing: true,
|
|
105
|
+
includeSSIM: true,
|
|
106
|
+
diffPath: '/tmp/diff.png'
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### `quickCompare(img1, img2): Promise<boolean>`
|
|
111
|
+
|
|
112
|
+
Quick async comparison returning only a boolean.
|
|
113
|
+
|
|
114
|
+
**Returns:** `Promise<boolean>` - `true` if different, `false` if identical
|
|
115
|
+
|
|
116
|
+
### Sync API
|
|
117
|
+
|
|
118
|
+
#### `compareSync(img1, img2, options?): DiffResult`
|
|
119
|
+
|
|
120
|
+
Synchronous comparison (blocks event loop). Use only when necessary.
|
|
121
|
+
|
|
122
|
+
#### `quickCompareSync(img1, img2): boolean`
|
|
123
|
+
|
|
124
|
+
Synchronous quick comparison returning a boolean.
|
|
125
|
+
|
|
126
|
+
## Options
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
interface CompareOptions {
|
|
130
|
+
/** Pixel tolerance 0-255 (default: 0) */
|
|
131
|
+
pixelTolerance?: number;
|
|
132
|
+
|
|
133
|
+
/** YIQ color threshold 0.0-1.0 (default: 0.0 = RGB mode) */
|
|
134
|
+
colorThreshold?: number;
|
|
135
|
+
|
|
136
|
+
/** Enable anti-aliasing detection (default: true) */
|
|
137
|
+
antialiasing?: boolean;
|
|
138
|
+
|
|
139
|
+
/** Ignore colors, compare brightness only (default: false) */
|
|
140
|
+
ignoreColors?: boolean;
|
|
141
|
+
|
|
142
|
+
/** Max differences to find before stopping (default: unlimited) */
|
|
143
|
+
maxDiffs?: number;
|
|
144
|
+
|
|
145
|
+
/** Include detailed pixel list (default: false) */
|
|
146
|
+
includeDiffPixels?: boolean;
|
|
147
|
+
|
|
148
|
+
/** Include spatial clusters (default: false) */
|
|
149
|
+
includeClusters?: boolean;
|
|
150
|
+
|
|
151
|
+
/** Calculate SSIM score (default: false) */
|
|
152
|
+
includeSSIM?: boolean;
|
|
153
|
+
|
|
154
|
+
/** Path to save diff image */
|
|
155
|
+
diffPath?: string;
|
|
156
|
+
|
|
157
|
+
/** Path to save binary mask */
|
|
158
|
+
maskPath?: string;
|
|
159
|
+
|
|
160
|
+
/** Path to save side-by-side overlay */
|
|
161
|
+
overlayPath?: string;
|
|
162
|
+
|
|
163
|
+
/** Overwrite existing files (default: false) */
|
|
164
|
+
overwrite?: boolean;
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Result Object
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
interface DiffResult {
|
|
172
|
+
isDifferent: boolean;
|
|
173
|
+
diffPercentage: number;
|
|
174
|
+
totalPixels: number;
|
|
175
|
+
diffPixels: number;
|
|
176
|
+
aaPixelsIgnored: number;
|
|
177
|
+
aaPercentage: number;
|
|
178
|
+
boundingBox: BoundingBox | null;
|
|
179
|
+
heightDiff: HeightDiff | null;
|
|
180
|
+
diffPixelsList: DiffPixel[] | null;
|
|
181
|
+
diffClusters: DiffCluster[] | null;
|
|
182
|
+
intensityStats: IntensityStats | null;
|
|
183
|
+
perceptualScore: number | null; // SSIM 0.0-1.0
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Usage Examples
|
|
188
|
+
|
|
189
|
+
### With Buffers
|
|
190
|
+
|
|
191
|
+
```javascript
|
|
192
|
+
const fs = require('fs');
|
|
193
|
+
const { compare } = require('@vizzly-testing/honeydiff');
|
|
194
|
+
|
|
195
|
+
const img1 = fs.readFileSync('image1.png');
|
|
196
|
+
const img2 = fs.readFileSync('image2.png');
|
|
197
|
+
|
|
198
|
+
const result = await compare(img1, img2, {
|
|
199
|
+
pixelTolerance: 5
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Generate Diff Images
|
|
204
|
+
|
|
205
|
+
```javascript
|
|
206
|
+
const result = await compare('baseline.png', 'current.png', {
|
|
207
|
+
diffPath: './output/diff.png',
|
|
208
|
+
maskPath: './output/mask.png',
|
|
209
|
+
overlayPath: './output/overlay.png',
|
|
210
|
+
overwrite: true
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
console.log(`Diff images saved with ${result.diffPercentage}% difference`);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Spatial Clustering
|
|
217
|
+
|
|
218
|
+
```javascript
|
|
219
|
+
const result = await compare('img1.png', 'img2.png', {
|
|
220
|
+
includeClusters: true
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (result.diffClusters) {
|
|
224
|
+
result.diffClusters.forEach((cluster, i) => {
|
|
225
|
+
console.log(`Cluster ${i + 1}:`);
|
|
226
|
+
console.log(` Pixels: ${cluster.pixelCount}`);
|
|
227
|
+
console.log(` Center: (${cluster.centerOfMass[0]}, ${cluster.centerOfMass[1]})`);
|
|
228
|
+
console.log(` Bounds: ${cluster.boundingBox.width}x${cluster.boundingBox.height}`);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Perceptual Similarity (SSIM)
|
|
234
|
+
|
|
235
|
+
```javascript
|
|
236
|
+
const result = await compare('img1.png', 'img2.png', {
|
|
237
|
+
includeSSIM: true
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (result.perceptualScore !== null) {
|
|
241
|
+
if (result.perceptualScore > 0.95) {
|
|
242
|
+
console.log('Perceptually very similar');
|
|
243
|
+
} else if (result.perceptualScore > 0.80) {
|
|
244
|
+
console.log('Somewhat similar');
|
|
245
|
+
} else {
|
|
246
|
+
console.log('Perceptually different');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Intensity Statistics
|
|
252
|
+
|
|
253
|
+
```javascript
|
|
254
|
+
const result = await compare('img1.png', 'img2.png', {
|
|
255
|
+
includeDiffPixels: true // Enables intensity stats
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if (result.intensityStats) {
|
|
259
|
+
console.log(`Intensity range: ${result.intensityStats.min}-${result.intensityStats.max}`);
|
|
260
|
+
console.log(`Mean: ${result.intensityStats.mean.toFixed(1)}`);
|
|
261
|
+
console.log(`Median: ${result.intensityStats.median}`);
|
|
262
|
+
console.log(`Std Dev: ${result.intensityStats.stdDev.toFixed(1)}`);
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Performance
|
|
267
|
+
|
|
268
|
+
Benchmarks on M-series Mac:
|
|
269
|
+
|
|
270
|
+
| Image Size | Time | Throughput |
|
|
271
|
+
|------------|------|------------|
|
|
272
|
+
| 1920x1080 (Full HD) | ~2ms | 1.1B pixels/sec |
|
|
273
|
+
| 3840x2160 (4K) | ~3ms | 3.0B pixels/sec |
|
|
274
|
+
|
|
275
|
+
With diff artifact generation: ~30-35ms for Full HD images
|
|
276
|
+
|
|
277
|
+
## Variable Height Support
|
|
278
|
+
|
|
279
|
+
Honeydiff can compare images with different heights (same width) - perfect for scrollable screenshots:
|
|
280
|
+
|
|
281
|
+
```javascript
|
|
282
|
+
const result = await compare('short.png', 'tall.png');
|
|
283
|
+
|
|
284
|
+
if (result.heightDiff) {
|
|
285
|
+
console.log(`Height diff: ${result.heightDiff.height1}px vs ${result.heightDiff.height2}px`);
|
|
286
|
+
console.log(`Extra pixels: ${result.heightDiff.extraPixels}`);
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Building from Source
|
|
291
|
+
|
|
292
|
+
Only needed if you want to contribute or modify the native code.
|
|
293
|
+
|
|
294
|
+
**Prerequisites:**
|
|
295
|
+
- Rust toolchain (install from https://rustup.rs)
|
|
296
|
+
- Node.js 22+
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
# Clone the repository
|
|
300
|
+
git clone https://github.com/vizzly-testing/honeydiff
|
|
301
|
+
cd honeydiff/crates/honeydiff-node
|
|
302
|
+
|
|
303
|
+
# Install dependencies
|
|
304
|
+
npm install
|
|
305
|
+
|
|
306
|
+
# Build for your platform
|
|
307
|
+
npm run build
|
|
308
|
+
|
|
309
|
+
# Run examples
|
|
310
|
+
node examples/basic.js
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Cross-Platform Builds
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
# Build for all supported platforms (requires cross-compilation setup)
|
|
317
|
+
npm run cross-build
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Related Projects
|
|
321
|
+
|
|
322
|
+
- [honeydiff](https://github.com/vizzly-testing/honeydiff) - The core Rust library
|
|
323
|
+
- [Vizzly](https://github.com/vizzly-testing/vizzly) - Visual regression testing platform
|
|
324
|
+
|
|
325
|
+
## License
|
|
326
|
+
|
|
327
|
+
MIT - See [LICENSE](../../LICENSE) for details
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript definitions for @vizzly-testing/honeydiff
|
|
3
|
+
* High-performance image diffing for Node.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Core Types
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
export interface BoundingBox {
|
|
11
|
+
/** X coordinate of top-left corner */
|
|
12
|
+
x: number;
|
|
13
|
+
/** Y coordinate of top-left corner */
|
|
14
|
+
y: number;
|
|
15
|
+
/** Width of the bounding box */
|
|
16
|
+
width: number;
|
|
17
|
+
/** Height of the bounding box */
|
|
18
|
+
height: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface HeightDiff {
|
|
22
|
+
/** Height of the first image in pixels */
|
|
23
|
+
height1: number;
|
|
24
|
+
/** Height of the second image in pixels */
|
|
25
|
+
height2: number;
|
|
26
|
+
/** Number of extra pixels due to height difference */
|
|
27
|
+
extraPixels: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DiffPixel {
|
|
31
|
+
/** X coordinate of the differing pixel */
|
|
32
|
+
x: number;
|
|
33
|
+
/** Y coordinate of the differing pixel */
|
|
34
|
+
y: number;
|
|
35
|
+
/** Intensity of the difference (0-255, higher = more different) */
|
|
36
|
+
intensity: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface IntensityStats {
|
|
40
|
+
/** Maximum difference intensity observed (0-255) */
|
|
41
|
+
max: number;
|
|
42
|
+
/** Minimum difference intensity observed (0-255) */
|
|
43
|
+
min: number;
|
|
44
|
+
/** Mean (average) difference intensity */
|
|
45
|
+
mean: number;
|
|
46
|
+
/** Median difference intensity */
|
|
47
|
+
median: number;
|
|
48
|
+
/** Standard deviation of difference intensities */
|
|
49
|
+
stdDev: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface DiffCluster {
|
|
53
|
+
/** Number of pixels in this cluster */
|
|
54
|
+
pixelCount: number;
|
|
55
|
+
/** Center of mass [x, y] coordinates */
|
|
56
|
+
centerOfMass: [number, number];
|
|
57
|
+
/** Average intensity of differences in this cluster */
|
|
58
|
+
avgIntensity: number;
|
|
59
|
+
/** Bounding box containing this cluster */
|
|
60
|
+
boundingBox: BoundingBox;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface DiffResult {
|
|
64
|
+
/** Whether the images are different based on the comparison threshold */
|
|
65
|
+
isDifferent: boolean;
|
|
66
|
+
/** Percentage of pixels that differ (0.0 - 100.0) */
|
|
67
|
+
diffPercentage: number;
|
|
68
|
+
/** Total number of pixels compared */
|
|
69
|
+
totalPixels: number;
|
|
70
|
+
/** Number of pixels that differ */
|
|
71
|
+
diffPixels: number;
|
|
72
|
+
/** Number of pixels ignored due to anti-aliasing detection */
|
|
73
|
+
aaPixelsIgnored: number;
|
|
74
|
+
/** Percentage of pixels ignored as anti-aliasing (0.0 - 100.0) */
|
|
75
|
+
aaPercentage: number;
|
|
76
|
+
/** Bounding box containing all differences (null if identical) */
|
|
77
|
+
boundingBox: BoundingBox | null;
|
|
78
|
+
/** Height difference information (null if same height) */
|
|
79
|
+
heightDiff: HeightDiff | null;
|
|
80
|
+
/** List of differing pixels with positions and intensities (null unless includeDiffPixels is enabled) */
|
|
81
|
+
diffPixelsList: DiffPixel[] | null;
|
|
82
|
+
/** Clustered groups of connected differing pixels (null unless includeClusters is enabled) */
|
|
83
|
+
diffClusters: DiffCluster[] | null;
|
|
84
|
+
/** Statistical metrics about difference intensities (null unless includeDiffPixels is enabled) */
|
|
85
|
+
intensityStats: IntensityStats | null;
|
|
86
|
+
/** SSIM (Structural Similarity Index) perceptual score 0.0-1.0 (null unless includeSSIM is enabled) */
|
|
87
|
+
perceptualScore: number | null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Options
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
export interface CompareOptions {
|
|
95
|
+
/**
|
|
96
|
+
* Pixel difference threshold (0-255). Differences below this are ignored.
|
|
97
|
+
* NOTE: When using YIQ comparison, use colorThreshold instead (0.0-1.0)
|
|
98
|
+
* @default 0
|
|
99
|
+
*/
|
|
100
|
+
pixelTolerance?: number;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Color difference threshold for YIQ comparison (0.0-1.0)
|
|
104
|
+
* Set to 0.0 to use RGB comparison (default)
|
|
105
|
+
* Set to 0.1 for odiff-style YIQ comparison
|
|
106
|
+
* @default 0.0
|
|
107
|
+
*/
|
|
108
|
+
colorThreshold?: number;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Enable anti-aliasing detection to ignore AA differences
|
|
112
|
+
* @default true
|
|
113
|
+
*/
|
|
114
|
+
antialiasing?: boolean;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Ignore color differences (compare only structure/brightness)
|
|
118
|
+
* @default false
|
|
119
|
+
*/
|
|
120
|
+
ignoreColors?: boolean;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Maximum number of differences to detect before stopping
|
|
124
|
+
* @default undefined (unlimited)
|
|
125
|
+
*/
|
|
126
|
+
maxDiffs?: number;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Include list of differing pixels with positions and intensities
|
|
130
|
+
* WARNING: This can use significant memory for images with many differences
|
|
131
|
+
* @default false
|
|
132
|
+
*/
|
|
133
|
+
includeDiffPixels?: boolean;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Include clustered groups of connected differing pixels
|
|
137
|
+
* Automatically enables includeDiffPixels if not already enabled
|
|
138
|
+
* Clusters are sorted by size (largest first)
|
|
139
|
+
* @default false
|
|
140
|
+
*/
|
|
141
|
+
includeClusters?: boolean;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Calculate SSIM (Structural Similarity Index) perceptual score
|
|
145
|
+
* WARNING: SSIM can be computationally expensive on large images
|
|
146
|
+
* @default false
|
|
147
|
+
*/
|
|
148
|
+
includeSSIM?: boolean;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Path to save the diff image (highlighted differences)
|
|
152
|
+
* @default undefined
|
|
153
|
+
*/
|
|
154
|
+
diffPath?: string;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Path to save the mask image (binary diff mask)
|
|
158
|
+
* @default undefined
|
|
159
|
+
*/
|
|
160
|
+
maskPath?: string;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Path to save the overlay image (side-by-side comparison)
|
|
164
|
+
* @default undefined
|
|
165
|
+
*/
|
|
166
|
+
overlayPath?: string;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Whether to overwrite existing output files
|
|
170
|
+
* @default false
|
|
171
|
+
*/
|
|
172
|
+
overwrite?: boolean;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// Input Types
|
|
177
|
+
// ============================================================================
|
|
178
|
+
|
|
179
|
+
/** Image input can be a file path or a Buffer containing image data */
|
|
180
|
+
export type ImageInput = string | Buffer;
|
|
181
|
+
|
|
182
|
+
// ============================================================================
|
|
183
|
+
// API Functions
|
|
184
|
+
// ============================================================================
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Compare two images asynchronously (recommended)
|
|
188
|
+
*
|
|
189
|
+
* @param img1 - First image (file path or Buffer)
|
|
190
|
+
* @param img2 - Second image (file path or Buffer)
|
|
191
|
+
* @param options - Comparison options
|
|
192
|
+
* @returns Promise resolving to detailed diff results
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* const result = await compare('baseline.png', 'current.png', {
|
|
197
|
+
* pixelTolerance: 10,
|
|
198
|
+
* includeSSIM: true
|
|
199
|
+
* });
|
|
200
|
+
*
|
|
201
|
+
* console.log(`Diff: ${result.diffPercentage.toFixed(2)}%`);
|
|
202
|
+
* console.log(`SSIM: ${result.perceptualScore}`);
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
export function compare(
|
|
206
|
+
img1: ImageInput,
|
|
207
|
+
img2: ImageInput,
|
|
208
|
+
options?: CompareOptions
|
|
209
|
+
): Promise<DiffResult>;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Quick asynchronous comparison (returns only boolean)
|
|
213
|
+
*
|
|
214
|
+
* @param img1 - First image (file path or Buffer)
|
|
215
|
+
* @param img2 - Second image (file path or Buffer)
|
|
216
|
+
* @returns Promise resolving to true if different, false if identical
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```typescript
|
|
220
|
+
* if (await quickCompare('baseline.png', 'current.png')) {
|
|
221
|
+
* console.log('Visual regression detected!');
|
|
222
|
+
* }
|
|
223
|
+
* ```
|
|
224
|
+
*/
|
|
225
|
+
export function quickCompare(
|
|
226
|
+
img1: ImageInput,
|
|
227
|
+
img2: ImageInput
|
|
228
|
+
): Promise<boolean>;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Compare two images synchronously (blocks event loop)
|
|
232
|
+
*
|
|
233
|
+
* Use this only when you need blocking behavior.
|
|
234
|
+
* For most cases, prefer the async compare() function.
|
|
235
|
+
*
|
|
236
|
+
* @param img1 - First image (file path or Buffer)
|
|
237
|
+
* @param img2 - Second image (file path or Buffer)
|
|
238
|
+
* @param options - Comparison options
|
|
239
|
+
* @returns Detailed diff results
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```typescript
|
|
243
|
+
* const result = compareSync('baseline.png', 'current.png', {
|
|
244
|
+
* pixelTolerance: 5,
|
|
245
|
+
* includeClusters: true
|
|
246
|
+
* });
|
|
247
|
+
*
|
|
248
|
+
* if (result.diffClusters) {
|
|
249
|
+
* console.log(`Found ${result.diffClusters.length} diff regions`);
|
|
250
|
+
* }
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
export function compareSync(
|
|
254
|
+
img1: ImageInput,
|
|
255
|
+
img2: ImageInput,
|
|
256
|
+
options?: CompareOptions
|
|
257
|
+
): DiffResult;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Quick synchronous comparison (returns only boolean, blocks event loop)
|
|
261
|
+
*
|
|
262
|
+
* @param img1 - First image (file path or Buffer)
|
|
263
|
+
* @param img2 - Second image (file path or Buffer)
|
|
264
|
+
* @returns true if different, false if identical
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* ```typescript
|
|
268
|
+
* if (quickCompareSync('baseline.png', 'current.png')) {
|
|
269
|
+
* console.log('Images differ');
|
|
270
|
+
* }
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
export function quickCompareSync(
|
|
274
|
+
img1: ImageInput,
|
|
275
|
+
img2: ImageInput
|
|
276
|
+
): boolean;
|
package/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vizzly-testing/honeydiff - Native Node.js bindings for Honeydiff
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
let addon;
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
// Load the platform-specific native addon
|
|
9
|
+
addon = require('./load-platform.js');
|
|
10
|
+
} catch (e) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
'@vizzly-testing/honeydiff failed to load native binary.\n' +
|
|
13
|
+
'Platform: ' + process.platform + ' ' + process.arch + '\n' +
|
|
14
|
+
'Original error: ' + e.message
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Export all functions from the native addon
|
|
19
|
+
module.exports = {
|
|
20
|
+
compare: addon.compare,
|
|
21
|
+
quickCompare: addon.quickCompare,
|
|
22
|
+
compareSync: addon.compareSync,
|
|
23
|
+
quickCompareSync: addon.quickCompareSync,
|
|
24
|
+
};
|
package/load-platform.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function getPlatformBinary() {
|
|
5
|
+
const platform = os.platform();
|
|
6
|
+
const arch = os.arch();
|
|
7
|
+
|
|
8
|
+
let target;
|
|
9
|
+
if (platform === 'darwin' && arch === 'arm64') {
|
|
10
|
+
target = 'aarch64-apple-darwin';
|
|
11
|
+
} else if (platform === 'linux' && arch === 'x64') {
|
|
12
|
+
target = 'x86_64-unknown-linux-gnu';
|
|
13
|
+
} else if (platform === 'linux' && arch === 'arm64') {
|
|
14
|
+
target = 'aarch64-unknown-linux-gnu';
|
|
15
|
+
} else if (platform === 'win32' && arch === 'x64') {
|
|
16
|
+
target = 'x86_64-pc-windows-msvc';
|
|
17
|
+
} else {
|
|
18
|
+
throw new Error(`Unsupported platform: ${platform} ${arch}. Supported: macOS ARM64, Linux x64/ARM64, Windows x64`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const binaryPath = path.join(__dirname, 'platforms', `index-${target}.node`);
|
|
22
|
+
return require(binaryPath);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = getPlatformBinary();
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vizzly-testing/honeydiff",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "High-performance image diffing for Node.js - Native bindings to Honeydiff Rust library",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"neon": {
|
|
8
|
+
"targets": [
|
|
9
|
+
"x86_64-pc-windows-msvc",
|
|
10
|
+
"aarch64-apple-darwin",
|
|
11
|
+
"x86_64-unknown-linux-gnu",
|
|
12
|
+
"aarch64-unknown-linux-gnu"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "cargo build --release",
|
|
17
|
+
"build-debug": "cargo build",
|
|
18
|
+
"test": "node examples/basic.js",
|
|
19
|
+
"clean": "cargo clean && rm -f index.node"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/vizzly-testing/honeydiff.git",
|
|
24
|
+
"directory": "crates/honeydiff-node"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"image",
|
|
28
|
+
"diff",
|
|
29
|
+
"comparison",
|
|
30
|
+
"visual-regression",
|
|
31
|
+
"testing",
|
|
32
|
+
"screenshot",
|
|
33
|
+
"rust",
|
|
34
|
+
"neon",
|
|
35
|
+
"native"
|
|
36
|
+
],
|
|
37
|
+
"author": "Robert DeLuca",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"files": [
|
|
40
|
+
"index.js",
|
|
41
|
+
"index.d.ts",
|
|
42
|
+
"index.node",
|
|
43
|
+
"README.md",
|
|
44
|
+
"index.js",
|
|
45
|
+
"index.d.ts",
|
|
46
|
+
"load-platform.js",
|
|
47
|
+
"platforms/*.node",
|
|
48
|
+
"README.md"
|
|
49
|
+
],
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=22.0.0"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
Binary file
|