@vizzly-testing/honeydiff 0.1.1 → 0.2.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 +457 -182
- package/index.d.ts +60 -0
- package/package.json +11 -1
- package/platforms/index-aarch64-apple-darwin.node +0 -0
- package/platforms/index-aarch64-unknown-linux-gnu.node +0 -0
- package/platforms/index-x86_64-pc-windows-msvc.node +0 -0
- package/platforms/index-x86_64-unknown-linux-gnu.node +0 -0
package/README.md
CHANGED
|
@@ -4,299 +4,574 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@vizzly-testing/honeydiff)
|
|
5
5
|
[](https://github.com/vizzly-testing/honeydiff/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
Blazingly fast image comparison for Node.js
|
|
8
|
-
validation, and detecting visual changes.
|
|
9
|
-
|
|
10
|
-
**Why Honeydiff?**
|
|
11
|
-
- **Fast:** Native performance processing 230M+ pixels/sec
|
|
12
|
-
- **Smart:** Intelligent anti-aliasing detection filters rendering artifacts
|
|
13
|
-
- **Flexible:** File paths or Buffers, async or sync APIs
|
|
14
|
-
- **Complete:** TypeScript types, pre-built binaries for all platforms
|
|
15
|
-
- **Advanced:** SSIM perceptual scoring, spatial clustering, variable-height screenshots
|
|
16
|
-
|
|
17
|
-
## Installation
|
|
7
|
+
**Blazingly fast image comparison for visual regression testing in Node.js.** Built with Rust for native performance.
|
|
18
8
|
|
|
19
9
|
```bash
|
|
20
10
|
npm install @vizzly-testing/honeydiff
|
|
21
11
|
```
|
|
22
12
|
|
|
23
|
-
|
|
13
|
+
Requires Node.js 22+. Pre-built binaries included for macOS (ARM64), Linux (x64/ARM64), and Windows (x64).
|
|
14
|
+
|
|
15
|
+
## Why Honeydiff?
|
|
16
|
+
|
|
17
|
+
- **6x faster than odiff** - Processes 230M+ pixels/sec with multi-core parallelization
|
|
18
|
+
- **Smart anti-aliasing** - Filters out rendering artifacts, not actual changes
|
|
19
|
+
- **Accessibility built-in** - WCAG contrast checking and color blindness simulation
|
|
20
|
+
- **Flexible inputs** - Works with file paths or Buffers
|
|
21
|
+
- **Full TypeScript support** - Complete type definitions included
|
|
22
|
+
- **Advanced analytics** - SSIM perceptual scoring, spatial clustering, intensity stats
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
required.
|
|
24
|
+
Built for production visual regression testing at scale.
|
|
27
25
|
|
|
28
26
|
## Quick Start
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
### Basic Comparison
|
|
29
|
+
|
|
31
30
|
```javascript
|
|
32
31
|
const { compare, quickCompare } = require('@vizzly-testing/honeydiff');
|
|
33
32
|
|
|
34
|
-
// Quick check
|
|
33
|
+
// Quick boolean check - are these images different?
|
|
35
34
|
if (await quickCompare('baseline.png', 'current.png')) {
|
|
36
35
|
console.log('Visual regression detected!');
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
// Detailed comparison
|
|
38
|
+
// Detailed comparison with tolerance
|
|
40
39
|
const result = await compare('baseline.png', 'current.png', {
|
|
41
|
-
pixelTolerance: 10,
|
|
42
|
-
includeSSIM: true
|
|
40
|
+
pixelTolerance: 10, // Ignore tiny differences
|
|
41
|
+
includeSSIM: true // Perceptual similarity score
|
|
43
42
|
});
|
|
44
43
|
|
|
45
|
-
console.log(`Diff: ${result.diffPercentage.toFixed(2)}
|
|
44
|
+
console.log(`Diff: ${result.diffPercentage.toFixed(2)}%`);
|
|
45
|
+
console.log(`SSIM: ${result.perceptualScore}`);
|
|
46
46
|
// Output: Diff: 3.55%, SSIM: 0.9234
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
### TypeScript
|
|
50
50
|
|
|
51
51
|
```typescript
|
|
52
52
|
import { compare, CompareOptions, DiffResult } from '@vizzly-testing/honeydiff';
|
|
53
53
|
|
|
54
|
-
const
|
|
54
|
+
const options: CompareOptions = {
|
|
55
55
|
pixelTolerance: 5,
|
|
56
56
|
includeSSIM: true,
|
|
57
57
|
includeClusters: true
|
|
58
|
-
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const result: DiffResult = await compare('img1.png', 'img2.png', options);
|
|
59
61
|
|
|
60
62
|
if (result.diffClusters) {
|
|
61
|
-
console.log(`Found ${result.diffClusters.length}
|
|
63
|
+
console.log(`Found ${result.diffClusters.length} different regions`);
|
|
62
64
|
}
|
|
63
|
-
// Output: Found 3 diff regions
|
|
64
65
|
```
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
### Test Integration
|
|
68
|
+
|
|
69
|
+
**Jest/Vitest**
|
|
70
|
+
```javascript
|
|
71
|
+
import { quickCompare } from '@vizzly-testing/honeydiff';
|
|
72
|
+
|
|
73
|
+
test('homepage should match baseline', async () => {
|
|
74
|
+
let screenshot = await page.screenshot();
|
|
75
|
+
let isDifferent = await quickCompare('baseline.png', screenshot);
|
|
76
|
+
expect(isDifferent).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Playwright**
|
|
81
|
+
```javascript
|
|
82
|
+
const { compare } = require('@vizzly-testing/honeydiff');
|
|
67
83
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
- `getDimensions(img): Promise<{ width: number, height: number }>` - Get image dimensions
|
|
84
|
+
test('button hover state', async () => {
|
|
85
|
+
await page.hover('[data-testid="submit-button"]');
|
|
86
|
+
let screenshot = await page.screenshot();
|
|
72
87
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
88
|
+
let result = await compare('baseline.png', screenshot, {
|
|
89
|
+
pixelTolerance: 10,
|
|
90
|
+
diffPath: './artifacts/diff.png'
|
|
91
|
+
});
|
|
77
92
|
|
|
78
|
-
|
|
93
|
+
expect(result.diffPercentage).toBeLessThan(1.0);
|
|
94
|
+
});
|
|
95
|
+
```
|
|
79
96
|
|
|
80
|
-
##
|
|
97
|
+
## Core Features
|
|
81
98
|
|
|
82
|
-
|
|
83
|
-
<summary><strong>CompareOptions</strong></summary>
|
|
99
|
+
### 1. Performance at Scale
|
|
84
100
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
Process 230M pixels/second with automatic multi-core parallelization. Perfect for CI/CD pipelines.
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
// 18.3M pixel screenshot compared in ~79ms
|
|
105
|
+
const result = await compare('full-page-baseline.png', 'full-page-current.png');
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 2. Smart Anti-Aliasing Detection
|
|
109
|
+
|
|
110
|
+
Automatically filters out rendering artifacts (font smoothing, sub-pixel differences) while catching real visual changes.
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
const result = await compare('img1.png', 'img2.png', {
|
|
114
|
+
antialiasing: true // Default: true
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
console.log(`Ignored ${result.aaPixelsIgnored} AA pixels`);
|
|
118
|
+
// Output: Ignored 1952904 AA pixels
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Disable for pixel-perfect matching:
|
|
122
|
+
```javascript
|
|
123
|
+
const result = await compare('img1.png', 'img2.png', {
|
|
124
|
+
antialiasing: false // Strict pixel matching
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 3. Variable Height Screenshots
|
|
129
|
+
|
|
130
|
+
Perfect for scrollable content - compare full-page screenshots with different heights.
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
// 24,162px vs 24,412px tall images
|
|
134
|
+
const result = await compare('full-page-v1.png', 'full-page-v2.png');
|
|
135
|
+
|
|
136
|
+
if (result.heightDiff) {
|
|
137
|
+
console.log(`Height difference: ${result.heightDiff.height1} vs ${result.heightDiff.height2}`);
|
|
138
|
+
console.log(`Extra pixels: ${result.heightDiff.extraPixels}`);
|
|
99
139
|
}
|
|
100
140
|
```
|
|
101
|
-
</details>
|
|
102
141
|
|
|
103
|
-
|
|
104
|
-
<summary><strong>DiffResult</strong></summary>
|
|
142
|
+
### 4. Visual Diff Artifacts
|
|
105
143
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
144
|
+
Generate highlighted diff images, binary masks, and overlays for debugging.
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
const result = await compare('baseline.png', 'current.png', {
|
|
148
|
+
diffPath: './artifacts/diff.png', // Highlighted differences
|
|
149
|
+
maskPath: './artifacts/mask.png', // Binary diff mask
|
|
150
|
+
overlayPath: './artifacts/overlay.png', // Side-by-side comparison
|
|
151
|
+
overwrite: true
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
console.log('Artifacts saved to ./artifacts/');
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 5. Spatial Clustering
|
|
158
|
+
|
|
159
|
+
Group nearby differences into regions to identify distinct changed areas.
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
const result = await compare('img1.png', 'img2.png', {
|
|
163
|
+
includeClusters: true
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (result.diffClusters) {
|
|
167
|
+
result.diffClusters.forEach((cluster, i) => {
|
|
168
|
+
console.log(`Region ${i + 1}:`);
|
|
169
|
+
console.log(` - ${cluster.pixelCount} pixels changed`);
|
|
170
|
+
console.log(` - Center at (${cluster.centerOfMass[0]}, ${cluster.centerOfMass[1]})`);
|
|
171
|
+
console.log(` - Bounding box: ${cluster.boundingBox.width}x${cluster.boundingBox.height}`);
|
|
172
|
+
console.log(` - Average intensity: ${cluster.avgIntensity}`);
|
|
173
|
+
});
|
|
120
174
|
}
|
|
175
|
+
// Output:
|
|
176
|
+
// Region 1:
|
|
177
|
+
// - 12450 pixels changed
|
|
178
|
+
// - Center at (245, 189)
|
|
179
|
+
// - Bounding box: 124x89
|
|
180
|
+
// - Average intensity: 127.3
|
|
121
181
|
```
|
|
122
|
-
</details>
|
|
123
182
|
|
|
124
|
-
|
|
183
|
+
### 6. Perceptual Similarity (SSIM)
|
|
184
|
+
|
|
185
|
+
Beyond pixel counting - measure structural similarity from a human perception perspective.
|
|
125
186
|
|
|
126
|
-
**Get Image Dimensions**
|
|
127
187
|
```javascript
|
|
128
|
-
const
|
|
188
|
+
const result = await compare('img1.png', 'img2.png', {
|
|
189
|
+
includeSSIM: true // Note: Can be slow on large images
|
|
190
|
+
});
|
|
129
191
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// Output: Image is 1920x1080 pixels
|
|
192
|
+
if (result.perceptualScore !== null) {
|
|
193
|
+
console.log(`SSIM: ${result.perceptualScore.toFixed(3)}`);
|
|
194
|
+
// Output: SSIM: 0.923 (0.0 = different, 1.0 = identical)
|
|
134
195
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
196
|
+
if (result.perceptualScore > 0.95) {
|
|
197
|
+
console.log('Images are perceptually very similar');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
138
201
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
202
|
+
### 7. Tolerance & Color Spaces
|
|
203
|
+
|
|
204
|
+
**RGB Mode (default)** - Exact matching with pixel tolerance:
|
|
205
|
+
```javascript
|
|
206
|
+
const result = await compare('img1.png', 'img2.png', {
|
|
207
|
+
pixelTolerance: 10 // Ignore differences below threshold (0-255)
|
|
208
|
+
});
|
|
142
209
|
```
|
|
143
210
|
|
|
144
|
-
**
|
|
211
|
+
**YIQ Mode** - Perceptual color matching weighted for human vision:
|
|
212
|
+
```javascript
|
|
213
|
+
const result = await compare('img1.png', 'img2.png', {
|
|
214
|
+
colorThreshold: 0.1 // YIQ mode (odiff-compatible)
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Brightness-only Mode** - Ignore color, compare structure:
|
|
219
|
+
```javascript
|
|
220
|
+
const result = await compare('img1.png', 'img2.png', {
|
|
221
|
+
ignoreColors: true // Compare brightness/luminance only
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### 8. Buffer Support
|
|
226
|
+
|
|
227
|
+
Work with in-memory images from screenshots or API responses.
|
|
228
|
+
|
|
145
229
|
```javascript
|
|
146
230
|
const fs = require('fs');
|
|
147
|
-
const { compare } = require('@vizzly-testing/honeydiff');
|
|
148
231
|
|
|
232
|
+
// Compare buffers directly
|
|
149
233
|
let img1 = fs.readFileSync('image1.png');
|
|
150
234
|
let img2 = fs.readFileSync('image2.png');
|
|
151
235
|
let result = await compare(img1, img2, { pixelTolerance: 5 });
|
|
236
|
+
|
|
237
|
+
// Mix paths and buffers
|
|
238
|
+
let screenshot = await page.screenshot(); // Returns Buffer
|
|
239
|
+
let result = await compare('baseline.png', screenshot);
|
|
152
240
|
```
|
|
153
241
|
|
|
154
|
-
|
|
242
|
+
### 9. Image Dimensions
|
|
243
|
+
|
|
244
|
+
Fast utility to get image dimensions without loading full image data.
|
|
155
245
|
|
|
156
246
|
```javascript
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
247
|
+
const { getDimensions } = require('@vizzly-testing/honeydiff');
|
|
248
|
+
|
|
249
|
+
let dims = await getDimensions('screenshot.png');
|
|
250
|
+
console.log(`Image is ${dims.width}x${dims.height} pixels`);
|
|
251
|
+
// Output: Image is 1920x1080 pixels
|
|
252
|
+
|
|
253
|
+
// Works with buffers too
|
|
254
|
+
let buffer = fs.readFileSync('image.png');
|
|
255
|
+
let dims = await getDimensions(buffer);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Accessibility Features
|
|
259
|
+
|
|
260
|
+
Built-in accessibility testing to catch WCAG compliance issues and color blindness visibility problems.
|
|
261
|
+
|
|
262
|
+
### WCAG Contrast Checking
|
|
263
|
+
|
|
264
|
+
Validate that visual changes meet WCAG AA/AAA contrast standards.
|
|
265
|
+
|
|
266
|
+
```javascript
|
|
267
|
+
const result = await compare('baseline.png', 'current.png', {
|
|
268
|
+
includeClusters: true,
|
|
269
|
+
includeAccessibilityData: true
|
|
162
270
|
});
|
|
271
|
+
|
|
272
|
+
if (result.diffClusters) {
|
|
273
|
+
result.diffClusters.forEach(cluster => {
|
|
274
|
+
if (cluster.accessibilityMetadata) {
|
|
275
|
+
let meta = cluster.accessibilityMetadata;
|
|
276
|
+
|
|
277
|
+
console.log(`Contrast ratio: ${meta.contrastRatio.toFixed(2)}:1`);
|
|
278
|
+
console.log(`Range: ${meta.minContrastRatio.toFixed(2)} - ${meta.maxContrastRatio.toFixed(2)}`);
|
|
279
|
+
|
|
280
|
+
// WCAG validation
|
|
281
|
+
if (meta.minContrastRatio < 4.5) {
|
|
282
|
+
console.log('⚠️ Below WCAG AA normal text (4.5:1)');
|
|
283
|
+
}
|
|
284
|
+
if (meta.minContrastRatio < 3.0) {
|
|
285
|
+
console.log('⚠️ Below WCAG AA large text (3.0:1)');
|
|
286
|
+
}
|
|
287
|
+
if (meta.minContrastRatio >= 7.0) {
|
|
288
|
+
console.log('✓ Meets WCAG AAA (7.0:1)');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
163
293
|
```
|
|
164
294
|
|
|
165
|
-
|
|
295
|
+
### RGB Channel Analysis
|
|
296
|
+
|
|
297
|
+
Track which color channels changed - useful for debugging design changes.
|
|
298
|
+
|
|
166
299
|
```javascript
|
|
167
|
-
|
|
300
|
+
const result = await compare('baseline.png', 'current.png', {
|
|
168
301
|
includeClusters: true,
|
|
169
|
-
|
|
170
|
-
includeDiffPixels: true
|
|
302
|
+
includeAccessibilityData: true
|
|
171
303
|
});
|
|
172
304
|
|
|
173
|
-
// Spatial clusters
|
|
174
305
|
if (result.diffClusters) {
|
|
175
|
-
result.diffClusters.forEach(
|
|
176
|
-
|
|
306
|
+
result.diffClusters.forEach(cluster => {
|
|
307
|
+
if (cluster.accessibilityMetadata) {
|
|
308
|
+
let meta = cluster.accessibilityMetadata;
|
|
309
|
+
|
|
310
|
+
console.log(`RGB diffs: R=${meta.avgRDiff}, G=${meta.avgGDiff}, B=${meta.avgBDiff}`);
|
|
311
|
+
console.log(`Dominant channel: ${meta.dominantChannel}`);
|
|
312
|
+
console.log(`Luminance: baseline=${meta.avgBaselineLuminance.toFixed(3)}, current=${meta.avgCurrentLuminance.toFixed(3)}`);
|
|
313
|
+
}
|
|
177
314
|
});
|
|
178
315
|
}
|
|
179
316
|
// Output:
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
317
|
+
// RGB diffs: R=45.2, G=12.3, B=8.7
|
|
318
|
+
// Dominant channel: R
|
|
319
|
+
// Luminance: baseline=0.234, current=0.567
|
|
320
|
+
```
|
|
183
321
|
|
|
184
|
-
|
|
185
|
-
if (result.perceptualScore !== null) {
|
|
186
|
-
console.log(`SSIM: ${result.perceptualScore.toFixed(3)}`);
|
|
187
|
-
}
|
|
188
|
-
// Output: SSIM: 0.923
|
|
322
|
+
### Color Blindness Simulation
|
|
189
323
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
324
|
+
Test if visual changes are visible to users with color vision deficiencies.
|
|
325
|
+
|
|
326
|
+
```javascript
|
|
327
|
+
const result = await compare('baseline.png', 'current.png', {
|
|
328
|
+
includeClusters: true,
|
|
329
|
+
checkColorBlindness: true,
|
|
330
|
+
colorBlindnessThreshold: 30.0 // Sensitivity threshold (0-255)
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
if (result.diffClusters) {
|
|
334
|
+
result.diffClusters.forEach(cluster => {
|
|
335
|
+
if (cluster.accessibilityMetadata?.colorBlindnessImpact) {
|
|
336
|
+
let impact = cluster.accessibilityMetadata.colorBlindnessImpact;
|
|
337
|
+
|
|
338
|
+
console.log(`Visibility score: ${(impact.visibilityScore * 100).toFixed(0)}%`);
|
|
339
|
+
console.log(`Deuteranopia (red-green, ~1% of population): ${impact.deuteranopiaVisible ? '✓' : '✗'}`);
|
|
340
|
+
console.log(`Protanopia (red-weak, ~0.5%): ${impact.protanopiaVisible ? '✓' : '✗'}`);
|
|
341
|
+
console.log(`Tritanopia (blue-yellow, rare): ${impact.tritanopiaVisible ? '✓' : '✗'}`);
|
|
342
|
+
console.log(`Achromatopsia (grayscale, very rare): ${impact.achromatopsiaVisible ? '✓' : '✗'}`);
|
|
343
|
+
|
|
344
|
+
if (impact.visibilityScore < 0.5) {
|
|
345
|
+
console.log('⚠️ May not be visible to colorblind users');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
});
|
|
193
349
|
}
|
|
194
|
-
// Output: Range: 12-255, Mean: 87.3
|
|
195
350
|
```
|
|
196
351
|
|
|
197
|
-
**
|
|
352
|
+
**Real-world example:**
|
|
353
|
+
|
|
198
354
|
```javascript
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
355
|
+
test('button color change is accessible', async () => {
|
|
356
|
+
let result = await compare('button-baseline.png', 'button-hover.png', {
|
|
357
|
+
includeClusters: true,
|
|
358
|
+
checkColorBlindness: true
|
|
359
|
+
});
|
|
203
360
|
|
|
204
|
-
|
|
361
|
+
// Ensure button hover is visible to colorblind users
|
|
362
|
+
let lowestVisibility = Math.min(
|
|
363
|
+
...result.diffClusters.map(c =>
|
|
364
|
+
c.accessibilityMetadata?.colorBlindnessImpact?.visibilityScore ?? 1
|
|
365
|
+
)
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
expect(lowestVisibility).toBeGreaterThan(0.7);
|
|
369
|
+
});
|
|
205
370
|
```
|
|
206
371
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
372
|
+
## Complete API Reference
|
|
373
|
+
|
|
374
|
+
### Async Functions (Recommended)
|
|
375
|
+
|
|
376
|
+
**`compare(img1, img2, options?): Promise<DiffResult>`**
|
|
377
|
+
|
|
378
|
+
Full comparison with detailed results.
|
|
379
|
+
|
|
380
|
+
**`quickCompare(img1, img2): Promise<boolean>`**
|
|
381
|
+
|
|
382
|
+
Fast boolean check - returns true if different, false if identical.
|
|
383
|
+
|
|
384
|
+
**`getDimensions(img): Promise<{ width: number, height: number }>`**
|
|
385
|
+
|
|
386
|
+
Get image dimensions without loading full image data.
|
|
387
|
+
|
|
388
|
+
### Sync Functions (Blocks Event Loop)
|
|
389
|
+
|
|
390
|
+
**`compareSync(img1, img2, options?): DiffResult`**
|
|
391
|
+
|
|
392
|
+
Synchronous comparison. Use only when blocking behavior is required.
|
|
393
|
+
|
|
394
|
+
**`quickCompareSync(img1, img2): boolean`**
|
|
395
|
+
|
|
396
|
+
Synchronous boolean check.
|
|
397
|
+
|
|
398
|
+
**`getDimensionsSync(img): { width: number, height: number }`**
|
|
399
|
+
|
|
400
|
+
Synchronous dimension retrieval.
|
|
401
|
+
|
|
402
|
+
### Input Types
|
|
403
|
+
|
|
404
|
+
All functions accept:
|
|
405
|
+
- `string` - File path to PNG image
|
|
406
|
+
- `Buffer` - Image data in memory
|
|
407
|
+
|
|
408
|
+
### CompareOptions
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
interface CompareOptions {
|
|
412
|
+
// Basic options
|
|
413
|
+
pixelTolerance?: number; // 0-255, ignore diffs below threshold (default: 0)
|
|
414
|
+
colorThreshold?: number; // 0.0-1.0, YIQ mode threshold (default: 0.0 = RGB)
|
|
415
|
+
antialiasing?: boolean; // Ignore AA artifacts (default: true)
|
|
416
|
+
ignoreColors?: boolean; // Brightness only (default: false)
|
|
417
|
+
maxDiffs?: number; // Stop after N diffs (default: unlimited)
|
|
418
|
+
|
|
419
|
+
// Analysis options
|
|
420
|
+
includeDiffPixels?: boolean; // List all diff pixels (memory intensive, default: false)
|
|
421
|
+
includeClusters?: boolean; // Spatial clustering (default: false)
|
|
422
|
+
includeSSIM?: boolean; // SSIM perceptual score (slow, default: false)
|
|
423
|
+
|
|
424
|
+
// Accessibility options
|
|
425
|
+
includeAccessibilityData?: boolean; // RGB, luminance, WCAG (default: false)
|
|
426
|
+
checkColorBlindness?: boolean; // Color blindness simulation (default: false)
|
|
427
|
+
colorBlindnessThreshold?: number; // Visibility threshold 0-255 (default: 30.0)
|
|
428
|
+
|
|
429
|
+
// Output options
|
|
430
|
+
diffPath?: string; // Save diff image path
|
|
431
|
+
maskPath?: string; // Save mask image path
|
|
432
|
+
overlayPath?: string; // Save overlay image path
|
|
433
|
+
overwrite?: boolean; // Overwrite existing files (default: false)
|
|
248
434
|
}
|
|
249
435
|
```
|
|
250
|
-
</details>
|
|
251
|
-
```
|
|
252
436
|
|
|
253
|
-
|
|
437
|
+
### DiffResult
|
|
254
438
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
439
|
+
```typescript
|
|
440
|
+
interface DiffResult {
|
|
441
|
+
// Basic metrics
|
|
442
|
+
isDifferent: boolean; // Are images different?
|
|
443
|
+
diffPercentage: number; // 0.0-100.0
|
|
444
|
+
totalPixels: number;
|
|
445
|
+
diffPixels: number;
|
|
446
|
+
|
|
447
|
+
// Anti-aliasing
|
|
448
|
+
aaPixelsIgnored: number;
|
|
449
|
+
aaPercentage: number; // 0.0-100.0
|
|
450
|
+
|
|
451
|
+
// Spatial info
|
|
452
|
+
boundingBox: BoundingBox | null; // Rectangle containing all diffs
|
|
453
|
+
heightDiff: HeightDiff | null; // Height difference info
|
|
454
|
+
|
|
455
|
+
// Advanced analysis
|
|
456
|
+
diffPixelsList: DiffPixel[] | null; // Null unless includeDiffPixels enabled
|
|
457
|
+
diffClusters: DiffCluster[] | null; // Null unless includeClusters enabled
|
|
458
|
+
intensityStats: IntensityStats | null; // Null unless includeDiffPixels enabled
|
|
459
|
+
perceptualScore: number | null; // 0.0-1.0, null unless includeSSIM enabled
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
See [index.d.ts](./index.d.ts) for complete type definitions.
|
|
260
464
|
|
|
261
465
|
## Performance
|
|
262
466
|
|
|
263
|
-
|
|
467
|
+
Benchmarked on 18.3M pixel screenshots (750 x 24,162 px):
|
|
264
468
|
|
|
265
469
|
| Operation | Time | Throughput |
|
|
266
470
|
|-----------|------|------------|
|
|
267
|
-
| Basic comparison | ~79ms | 231.8M pixels/sec |
|
|
471
|
+
| Basic RGB comparison | ~79ms | 231.8M pixels/sec |
|
|
472
|
+
| With anti-aliasing | ~88ms | 208M pixels/sec |
|
|
473
|
+
| YIQ color space | ~89ms | 206M pixels/sec |
|
|
268
474
|
| With artifacts (diff/mask/overlay) | ~432ms | 42.4M pixels/sec |
|
|
269
475
|
|
|
270
|
-
**
|
|
476
|
+
**vs odiff:** 6.0x faster (79ms vs 474ms)
|
|
477
|
+
|
|
478
|
+
Multi-core parallelization: 215-309% CPU utilization across cores.
|
|
271
479
|
|
|
272
480
|
*Benchmarked on Apple M-series Mac.*
|
|
273
481
|
|
|
274
|
-
##
|
|
482
|
+
## Common Patterns
|
|
483
|
+
|
|
484
|
+
### CI/CD Integration
|
|
485
|
+
|
|
486
|
+
```javascript
|
|
487
|
+
const { compare } = require('@vizzly-testing/honeydiff');
|
|
488
|
+
|
|
489
|
+
async function visualRegressionTest() {
|
|
490
|
+
let result = await compare('baseline.png', 'current.png', {
|
|
491
|
+
pixelTolerance: 10,
|
|
492
|
+
maxDiffs: 1000, // Early exit for fast failure
|
|
493
|
+
diffPath: process.env.CI ? './artifacts/diff.png' : undefined
|
|
494
|
+
});
|
|
275
495
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
496
|
+
if (result.isDifferent && result.diffPercentage > 1.0) {
|
|
497
|
+
console.error(`Visual regression: ${result.diffPercentage.toFixed(2)}% changed`);
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
```
|
|
279
502
|
|
|
280
|
-
|
|
281
|
-
- RGB mode (default): Exact matching, fastest performance
|
|
282
|
-
- YIQ mode: Perceptual matching weighted for human vision
|
|
503
|
+
### Progressive Enhancement
|
|
283
504
|
|
|
284
|
-
|
|
285
|
-
|
|
505
|
+
```javascript
|
|
506
|
+
// Start with quick check
|
|
507
|
+
if (await quickCompare('baseline.png', 'current.png')) {
|
|
508
|
+
// Only run expensive analysis if different
|
|
509
|
+
let result = await compare('baseline.png', 'current.png', {
|
|
510
|
+
includeClusters: true,
|
|
511
|
+
includeSSIM: true,
|
|
512
|
+
diffPath: './diff.png'
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// Detailed analysis and reporting
|
|
516
|
+
reportVisualRegression(result);
|
|
517
|
+
}
|
|
518
|
+
```
|
|
286
519
|
|
|
287
|
-
|
|
288
|
-
|
|
520
|
+
### Tolerance Thresholds
|
|
521
|
+
|
|
522
|
+
```javascript
|
|
523
|
+
const result = await compare('baseline.png', 'current.png', {
|
|
524
|
+
pixelTolerance: 10
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
if (result.diffPercentage > 5.0) {
|
|
528
|
+
console.error('FAIL: Major visual regression');
|
|
529
|
+
process.exit(1);
|
|
530
|
+
} else if (result.diffPercentage > 1.0) {
|
|
531
|
+
console.warn('WARN: Minor visual changes detected');
|
|
532
|
+
} else {
|
|
533
|
+
console.log('PASS: Visual test passed');
|
|
534
|
+
}
|
|
535
|
+
```
|
|
289
536
|
|
|
290
537
|
## Troubleshooting
|
|
291
538
|
|
|
292
539
|
**Module not found errors**
|
|
293
|
-
Ensure
|
|
540
|
+
- Ensure Node.js 22+ is installed
|
|
541
|
+
- Verify your platform is supported (macOS ARM64, Linux x64/ARM64, Windows x64)
|
|
542
|
+
- Try `npm install` again to download pre-built binaries
|
|
294
543
|
|
|
295
544
|
**Large memory usage with `includeDiffPixels`**
|
|
296
|
-
This option stores every different pixel's position and intensity
|
|
545
|
+
- This option stores every different pixel's position and intensity
|
|
546
|
+
- Disable for large images with many differences
|
|
547
|
+
- Only enable if you need detailed pixel-level data
|
|
297
548
|
|
|
298
549
|
**Slow performance with `includeSSIM`**
|
|
299
|
-
SSIM calculation is O(width × height × window²)
|
|
550
|
+
- SSIM calculation is O(width × height × window²)
|
|
551
|
+
- Only enable when perceptual scoring is needed
|
|
552
|
+
- Consider running SSIM only on failed comparisons
|
|
553
|
+
|
|
554
|
+
**Anti-aliasing too aggressive**
|
|
555
|
+
- Set `antialiasing: false` for stricter pixel matching
|
|
556
|
+
- Adjust `pixelTolerance` instead for controlled flexibility
|
|
557
|
+
|
|
558
|
+
## Use Cases
|
|
559
|
+
|
|
560
|
+
- **Visual Regression Testing** - Catch unintended UI changes in CI/CD
|
|
561
|
+
- **Screenshot Validation** - Compare generated screenshots against baselines
|
|
562
|
+
- **Accessibility Testing** - Validate WCAG contrast and color blindness visibility
|
|
563
|
+
- **Design QA** - Ensure implementations match design specifications
|
|
564
|
+
- **Cross-browser Testing** - Detect rendering differences across browsers
|
|
565
|
+
- **A/B Testing** - Measure visual differences between variants
|
|
566
|
+
- **Responsive Design** - Compare layouts across viewports
|
|
567
|
+
|
|
568
|
+
## Examples
|
|
569
|
+
|
|
570
|
+
See [examples/](./examples/) directory:
|
|
571
|
+
- [basic.js](./examples/basic.js) - Core features and accessibility
|
|
572
|
+
- [buffers.js](./examples/buffers.js) - Working with in-memory images
|
|
573
|
+
- [typescript.ts](./examples/typescript.ts) - TypeScript integration
|
|
574
|
+
- [accessibility.js](./examples/accessibility.js) - WCAG and color blindness
|
|
300
575
|
|
|
301
576
|
## License
|
|
302
577
|
|
package/index.d.ts
CHANGED
|
@@ -49,6 +49,42 @@ export interface IntensityStats {
|
|
|
49
49
|
stdDev: number;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export interface ColorBlindnessImpact {
|
|
53
|
+
/** Whether the difference is visible to people with deuteranopia (red-green color blindness) */
|
|
54
|
+
deuteranopiaVisible: boolean;
|
|
55
|
+
/** Whether the difference is visible to people with protanopia (red-weak color blindness) */
|
|
56
|
+
protanopiaVisible: boolean;
|
|
57
|
+
/** Whether the difference is visible to people with tritanopia (blue-yellow color blindness) */
|
|
58
|
+
tritanopiaVisible: boolean;
|
|
59
|
+
/** Whether the difference is visible to people with achromatopsia (complete color blindness) */
|
|
60
|
+
achromatopsiaVisible: boolean;
|
|
61
|
+
/** Overall visibility score (0.0-1.0, higher = more visible) */
|
|
62
|
+
visibilityScore: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface AccessibilityMetadata {
|
|
66
|
+
/** Average difference in red channel (0-255) */
|
|
67
|
+
avgRDiff: number;
|
|
68
|
+
/** Average difference in green channel (0-255) */
|
|
69
|
+
avgGDiff: number;
|
|
70
|
+
/** Average difference in blue channel (0-255) */
|
|
71
|
+
avgBDiff: number;
|
|
72
|
+
/** Dominant color channel ('R', 'G', or 'B') */
|
|
73
|
+
dominantChannel: string;
|
|
74
|
+
/** Average relative luminance of baseline pixels (0.0-1.0, sRGB) */
|
|
75
|
+
avgBaselineLuminance: number;
|
|
76
|
+
/** Average relative luminance of current pixels (0.0-1.0, sRGB) */
|
|
77
|
+
avgCurrentLuminance: number;
|
|
78
|
+
/** WCAG contrast ratio between average colors (1.0-21.0, values below 4.5 may fail WCAG AA) */
|
|
79
|
+
contrastRatio: number;
|
|
80
|
+
/** Minimum WCAG contrast ratio in this cluster (worst case) */
|
|
81
|
+
minContrastRatio: number;
|
|
82
|
+
/** Maximum WCAG contrast ratio in this cluster (best case) */
|
|
83
|
+
maxContrastRatio: number;
|
|
84
|
+
/** Color blindness visibility analysis (null unless checkColorBlindness is enabled) */
|
|
85
|
+
colorBlindnessImpact: ColorBlindnessImpact | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
52
88
|
export interface DiffCluster {
|
|
53
89
|
/** Number of pixels in this cluster */
|
|
54
90
|
pixelCount: number;
|
|
@@ -58,6 +94,8 @@ export interface DiffCluster {
|
|
|
58
94
|
avgIntensity: number;
|
|
59
95
|
/** Bounding box containing this cluster */
|
|
60
96
|
boundingBox: BoundingBox;
|
|
97
|
+
/** Accessibility metadata (null unless includeAccessibilityData is enabled) */
|
|
98
|
+
accessibilityMetadata: AccessibilityMetadata | null;
|
|
61
99
|
}
|
|
62
100
|
|
|
63
101
|
export interface DiffResult {
|
|
@@ -170,6 +208,28 @@ export interface CompareOptions {
|
|
|
170
208
|
* @default false
|
|
171
209
|
*/
|
|
172
210
|
overwrite?: boolean;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Include accessibility metadata (RGB channels, luminance, WCAG contrast)
|
|
214
|
+
* When enabled, each cluster will include detailed color and brightness analysis
|
|
215
|
+
* @default false
|
|
216
|
+
*/
|
|
217
|
+
includeAccessibilityData?: boolean;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Check if differences are visible to color blind users
|
|
221
|
+
* Automatically enables includeAccessibilityData if not already enabled
|
|
222
|
+
* Tests visibility for deuteranopia, protanopia, tritanopia, and achromatopsia
|
|
223
|
+
* @default false
|
|
224
|
+
*/
|
|
225
|
+
checkColorBlindness?: boolean;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Threshold for color blindness visibility detection (0-255)
|
|
229
|
+
* Differences below this threshold after color blindness simulation are considered invisible
|
|
230
|
+
* @default 30.0
|
|
231
|
+
*/
|
|
232
|
+
colorBlindnessThreshold?: number;
|
|
173
233
|
}
|
|
174
234
|
|
|
175
235
|
// ============================================================================
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vizzly-testing/honeydiff",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "High-performance image diffing for Node.js - Native bindings to Honeydiff Rust library",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -60,6 +60,16 @@
|
|
|
60
60
|
"index.d.ts",
|
|
61
61
|
"load-platform.js",
|
|
62
62
|
"platforms/*.node",
|
|
63
|
+
"README.md",
|
|
64
|
+
"index.js",
|
|
65
|
+
"index.d.ts",
|
|
66
|
+
"load-platform.js",
|
|
67
|
+
"platforms/*.node",
|
|
68
|
+
"README.md",
|
|
69
|
+
"index.js",
|
|
70
|
+
"index.d.ts",
|
|
71
|
+
"load-platform.js",
|
|
72
|
+
"platforms/*.node",
|
|
63
73
|
"README.md"
|
|
64
74
|
],
|
|
65
75
|
"engines": {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|