@vizzly-testing/honeydiff 0.10.3 → 0.11.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/README.md CHANGED
@@ -4,780 +4,272 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dm/@vizzly-testing/honeydiff.svg)](https://www.npmjs.com/package/@vizzly-testing/honeydiff)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/vizzly-testing/honeydiff/blob/main/LICENSE)
6
6
 
7
- **Blazingly fast image comparison for visual regression testing in Node.js.** Built with Rust for native performance.
7
+ Fast image comparison for visual regression testing in Node.js.
8
+
9
+ Honeydiff is a native Rust image diff engine packaged for Node. It is built for
10
+ the messy parts of visual testing: anti-aliased text, full-page screenshots,
11
+ small rendering noise, diff artifacts, spatial clusters, perceptual metrics, and
12
+ accessibility checks.
13
+
14
+ <p align="center">
15
+ <img src="./assets/honeydiff-mascot-diff-drizzle.png" alt="Diff Drizzle, the Honeydiff Vizzly bear mascot" width="320" />
16
+ </p>
8
17
 
9
18
  ```bash
10
19
  npm install @vizzly-testing/honeydiff
11
20
  ```
12
21
 
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 analysis for catching accessibility violations
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
23
-
24
- Built for production visual regression testing at scale.
22
+ Requires Node.js 22+. Prebuilt binaries are included for macOS ARM64, Linux
23
+ x64/ARM64, and Windows x64.
25
24
 
26
25
  ## Quick Start
27
26
 
28
- ### Basic Comparison
29
-
30
- ```javascript
27
+ ```js
31
28
  import { compare, quickCompare } from '@vizzly-testing/honeydiff';
32
29
 
33
- // Quick boolean check - are these images different?
34
- if (await quickCompare('baseline.png', 'current.png')) {
35
- console.log('Visual regression detected!');
36
- }
37
-
38
- // Detailed comparison with tolerance
39
- const result = await compare('baseline.png', 'current.png', {
40
- threshold: 10, // Ignore tiny differences
41
- includeSSIM: true // Perceptual similarity score
42
- });
43
-
44
- console.log(`Diff: ${result.diffPercentage.toFixed(2)}%`);
45
- console.log(`SSIM: ${result.perceptualScore}`);
46
- // Output: Diff: 3.55%, SSIM: 0.9234
47
- ```
48
-
49
- ### TypeScript
50
-
51
- ```typescript
52
- import { compare, CompareOptions, DiffResult } from '@vizzly-testing/honeydiff';
30
+ let changed = await quickCompare('baseline.png', 'current.png');
53
31
 
54
- const options: CompareOptions = {
55
- threshold: 5,
56
- includeSSIM: true,
57
- includeClusters: true
58
- };
59
-
60
- const result: DiffResult = await compare('img1.png', 'img2.png', options);
61
-
62
- if (result.diffClusters) {
63
- console.log(`Found ${result.diffClusters.length} different regions`);
32
+ if (changed) {
33
+ console.log('Visual change detected');
64
34
  }
65
- ```
66
35
 
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);
36
+ let result = await compare('baseline.png', 'current.png', {
37
+ threshold: 2.0,
38
+ includeClusters: true,
39
+ diffPath: 'artifacts/diff.png',
40
+ maskPath: 'artifacts/mask.png',
41
+ overlayPath: 'artifacts/overlay.png',
42
+ overwrite: true,
77
43
  });
78
- ```
79
-
80
- **Playwright**
81
- ```javascript
82
- import { compare } from '@vizzly-testing/honeydiff';
83
44
 
84
- test('button hover state', async () => {
85
- await page.hover('[data-testid="submit-button"]');
86
- let screenshot = await page.screenshot();
87
-
88
- let result = await compare('baseline.png', screenshot, {
89
- threshold: 10,
90
- diffPath: './artifacts/diff.png'
91
- });
92
-
93
- expect(result.diffPercentage).toBeLessThan(1.0);
94
- });
45
+ console.log(result.isDifferent);
46
+ console.log(result.diffPercentage);
47
+ console.log(result.diffClusters);
95
48
  ```
96
49
 
97
- ## Core Features
98
-
99
- ### 1. Performance at Scale
50
+ ## Why Honeydiff?
100
51
 
101
- Process 230M pixels/second with automatic multi-core parallelization. Perfect for CI/CD pipelines.
52
+ Most image diff packages stop at basic pixel comparison. Honeydiff gives you the
53
+ pieces visual regression systems usually need once screenshots get real:
102
54
 
103
- ```javascript
104
- // 18.3M pixel screenshot compared in ~79ms
105
- const result = await compare('full-page-baseline.png', 'full-page-current.png');
106
- ```
55
+ - CIEDE2000 perceptual color thresholds, with `2.0` as the default.
56
+ - Strict exact matching when you set `threshold: 0`.
57
+ - Conservative anti-aliasing detection for font and sub-pixel rendering noise.
58
+ - Variable-height screenshot support for full-page comparisons.
59
+ - Diff, mask, and overlay image artifacts for debugging failures.
60
+ - Spatial clusters, intensity stats, SSIM, GMSD, and diff fingerprints.
61
+ - WCAG contrast screening and color vision deficiency simulation.
62
+ - Async and sync APIs with TypeScript definitions included.
107
63
 
108
- ### 2. Smart Anti-Aliasing Detection
64
+ ## Common Use
109
65
 
110
- Automatically filters out rendering artifacts (font smoothing, sub-pixel differences) while catching real visual changes.
66
+ ### Compare Two Images
111
67
 
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
- ```
68
+ ```js
69
+ import { compare } from '@vizzly-testing/honeydiff';
120
70
 
121
- Disable for pixel-perfect matching:
122
- ```javascript
123
- const result = await compare('img1.png', 'img2.png', {
124
- antialiasing: false // Strict pixel matching
71
+ let result = await compare('before.png', 'after.png', {
72
+ threshold: 2.0,
125
73
  });
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
74
 
136
- if (result.heightDiff) {
137
- console.log(`Height difference: ${result.heightDiff.height1} vs ${result.heightDiff.height2}`);
138
- console.log(`Extra pixels: ${result.heightDiff.extraPixels}`);
75
+ if (result.isDifferent) {
76
+ console.log(`${result.diffPercentage.toFixed(2)}% of pixels changed`);
139
77
  }
140
78
  ```
141
79
 
142
- ### 4. Visual Diff Artifacts
80
+ ### Use Buffers
143
81
 
144
- Generate highlighted diff images, binary masks, and overlays for debugging.
82
+ ```js
83
+ import { readFile } from 'node:fs/promises';
84
+ import { compare } from '@vizzly-testing/honeydiff';
145
85
 
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
- });
86
+ let baseline = await readFile('baseline.png');
87
+ let current = await readFile('current.png');
153
88
 
154
- console.log('Artifacts saved to ./artifacts/');
89
+ let result = await compare(baseline, current);
155
90
  ```
156
91
 
157
- ### 5. Spatial Clustering & Noise Filtering
92
+ ### Generate Review Artifacts
158
93
 
159
- Group nearby differences into regions and filter out single-pixel noise from rendering variance.
94
+ ```js
95
+ import { compare } from '@vizzly-testing/honeydiff';
160
96
 
161
- ```javascript
162
- const result = await compare('img1.png', 'img2.png', {
163
- includeClusters: true,
164
- minClusterSize: 2 // Default: filter single isolated pixels as noise
97
+ let result = await compare('baseline.png', 'current.png', {
98
+ diffPath: 'artifacts/diff.png',
99
+ maskPath: 'artifacts/mask.png',
100
+ overlayPath: 'artifacts/overlay.png',
101
+ overwrite: true,
165
102
  });
166
-
167
- if (result.diffClusters) {
168
- result.diffClusters.forEach((cluster, i) => {
169
- console.log(`Region ${i + 1}:`);
170
- console.log(` - ${cluster.pixelCount} pixels changed`);
171
- console.log(` - Center at (${cluster.centerOfMass[0]}, ${cluster.centerOfMass[1]})`);
172
- console.log(` - Bounding box: ${cluster.boundingBox.width}x${cluster.boundingBox.height}`);
173
- console.log(` - Average intensity: ${cluster.avgIntensity}`);
174
- });
175
- }
176
- // Output:
177
- // Region 1:
178
- // - 12450 pixels changed
179
- // - Center at (245, 189)
180
- // - Bounding box: 124x89
181
- // - Average intensity: 127.3
182
103
  ```
183
104
 
184
- **Noise filtering values:**
185
- | Value | Behavior |
186
- |-------|----------|
187
- | 1 | Exact matching - any different pixel counts |
188
- | 2 | Default - filters single isolated pixels as rendering noise |
189
- | 3+ | More permissive - only larger clusters detected |
105
+ ### Group Differences Into Regions
190
106
 
191
- **Cluster Merging for Text Regions:**
192
-
193
- Text changes often fragment into many small clusters (one per character). Enable cluster merging to consolidate them into logical regions:
194
-
195
- ```javascript
196
- // Simple: enable with sensible defaults
197
- const result = await compare('img1.png', 'img2.png', {
198
- includeClusters: true,
199
- clusterMerge: true // Merge nearby clusters (great for text)
200
- });
107
+ ```js
108
+ import { compare } from '@vizzly-testing/honeydiff';
201
109
 
202
- // Advanced: tune the merging behavior
203
- const result = await compare('img1.png', 'img2.png', {
110
+ let result = await compare('baseline.png', 'current.png', {
204
111
  includeClusters: true,
205
- clusterMerge: {
206
- horizontalDistance: 15, // Max gap between clusters to merge (pixels)
207
- yBandTolerance: 5, // Vertical tolerance for "same line"
208
- maxHeightRatio: 2.0, // Prevent merging very different sized clusters
209
- maxWidthRatio: 3.0
210
- }
211
- });
212
-
213
- // Before: "2024-01-01" detected as 59 clusters (one per character/gap)
214
- // After: "2024-01-01" detected as 1-2 logical regions
215
- ```
216
-
217
- Uses SWT-inspired heuristics from text detection research (Epshtein et al. 2010).
218
-
219
- ### 6. Perceptual Similarity (SSIM & GMSD)
220
-
221
- Beyond pixel counting - measure structural similarity from a human perception perspective.
222
-
223
- **SSIM (Structural Similarity Index)** - Overall perceptual similarity:
224
- ```javascript
225
- const result = await compare('img1.png', 'img2.png', {
226
- includeSSIM: true // Note: Can be slow on large images
227
- });
228
-
229
- if (result.perceptualScore !== null) {
230
- console.log(`SSIM: ${result.perceptualScore.toFixed(3)}`);
231
- // Output: SSIM: 0.923 (0.0 = different, 1.0 = identical)
232
- }
233
- ```
234
-
235
- **GMSD (Gradient Magnitude Similarity Deviation)** - Fast edge-sensitive metric:
236
- ```javascript
237
- const result = await compare('img1.png', 'img2.png', {
238
- includeGMSD: true // Very fast, great for detecting structural changes
112
+ minClusterSize: 2,
113
+ clusterMerge: true,
239
114
  });
240
115
 
241
- if (result.gmsdScore !== null) {
242
- console.log(`GMSD: ${result.gmsdScore.toFixed(4)}`);
243
- // Output: GMSD: 0.0234 (0.0 = identical, higher = more different)
244
-
245
- if (result.gmsdScore < 0.05) {
246
- console.log('Edges are very similar');
247
- }
116
+ for (let cluster of result.diffClusters ?? []) {
117
+ console.log(cluster.pixelCount, cluster.boundingBox);
248
118
  }
249
119
  ```
250
120
 
251
- GMSD is ideal for catching:
252
- - Border thickness changes
253
- - Font weight shifts
254
- - Icon updates
255
- - Any edge/outline regressions
256
-
257
- **Reference:** Xue et al. 2014 - "Gradient Magnitude Similarity Deviation: A Highly Efficient Perceptual Image Quality Index"
258
-
259
- ### 7. Thresholds
260
-
261
- Honeydiff uses CIEDE2000 Delta E thresholds for perceptual color difference:
262
-
263
- ```javascript
264
- const result = await compare('img1.png', 'img2.png', {
265
- threshold: 2.0 // Default: ignores tiny perceptual variance
266
- });
267
- ```
268
-
269
- Use exact mode when every changed pixel should count:
270
-
271
- ```javascript
272
- const result = await compare('img1.png', 'img2.png', {
273
- threshold: 0,
274
- minClusterSize: 1
275
- });
276
- ```
277
-
278
- ### 8. Buffer Support
279
-
280
- Work with in-memory images from screenshots or API responses.
281
-
282
- For large screenshots, prefer file paths when possible. Buffer inputs are copied into the native worker before async processing begins, so path inputs put less pressure on the Node.js event loop and heap.
283
-
284
- ```javascript
285
- import fs from 'node:fs';
286
-
287
- // Compare buffers directly
288
- let img1 = fs.readFileSync('image1.png');
289
- let img2 = fs.readFileSync('image2.png');
290
- let result = await compare(img1, img2, { threshold: 5 });
291
-
292
- // Mix paths and buffers
293
- let screenshot = await page.screenshot(); // Returns Buffer
294
- let result = await compare('baseline.png', screenshot);
295
- ```
296
-
297
- ### 9. Image Dimensions
298
-
299
- Fast utility to get image dimensions without loading full image data.
300
-
301
- ```javascript
302
- import { getDimensions } from '@vizzly-testing/honeydiff';
303
-
304
- let dims = await getDimensions('screenshot.png');
305
- console.log(`Image is ${dims.width}x${dims.height} pixels`);
306
- // Output: Image is 1920x1080 pixels
307
-
308
- // Works with buffers too
309
- let buffer = fs.readFileSync('image.png');
310
- let dims = await getDimensions(buffer);
311
- ```
312
-
313
- ### 10. Image Metadata
314
-
315
- Get image dimensions, file size, and format detection - useful for storage tracking and billing.
316
-
317
- ```javascript
318
- import { getImageMetadata, getImageMetadataFromFile } from '@vizzly-testing/honeydiff';
319
-
320
- // From buffer
321
- let buffer = fs.readFileSync('screenshot.png');
322
- let metadata = await getImageMetadata(buffer);
323
- console.log(`Image: ${metadata.width}x${metadata.height}`);
324
- console.log(`Size: ${metadata.fileSizeBytes} bytes`);
325
- console.log(`Format: ${metadata.format}`);
326
- // Output: Image: 1920x1080
327
- // Size: 245760 bytes
328
- // Format: png
329
-
330
- // From file (gets accurate file size from disk)
331
- let metadata = await getImageMetadataFromFile('screenshot.png');
332
- console.log(`${metadata.width}x${metadata.height}, ${metadata.fileSizeBytes} bytes`);
333
- ```
334
-
335
- ### 11. Diff Fingerprinting
121
+ ### Add Perceptual Metrics
336
122
 
337
- Group similar diffs across comparisons with fingerprinting. Perfect for batch-approving the same visual change that appears on multiple pages (e.g., header/footer updates).
338
-
339
- ```javascript
340
- import {
341
- compare,
342
- computeFingerprintSync,
343
- fingerprintSimilaritySync,
344
- fingerprintHashSync
345
- } from '@vizzly-testing/honeydiff';
123
+ ```js
124
+ import { compare } from '@vizzly-testing/honeydiff';
346
125
 
347
- // Compare images with clustering enabled
348
126
  let result = await compare('baseline.png', 'current.png', {
349
- includeClusters: true
127
+ includeSSIM: true,
128
+ includeGMSD: true,
350
129
  });
351
130
 
352
- // Compute fingerprint
353
- let fingerprint = computeFingerprintSync(result, 1920, 1080);
354
-
355
- if (fingerprint) {
356
- console.log(`Hash: ${fingerprint.hash}`);
357
- console.log(`Zones affected: ${fingerprint.zoneMask.toString(2).padStart(16, '0')}`);
358
- console.log(`Cluster count: ${fingerprint.clusterCount}`);
359
- console.log(`Magnitude: ${fingerprint.diffMagnitude}`);
360
- }
131
+ console.log(result.perceptualScore);
132
+ console.log(result.gmsdScore);
361
133
  ```
362
134
 
363
- **Grouping similar diffs:**
364
- ```javascript
365
- // Group diffs by hash for batch approval
366
- let groups = new Map();
367
-
368
- for (let comparison of comparisons) {
369
- let fp = computeFingerprintSync(comparison.result, width, height);
370
- if (fp) {
371
- let hash = fingerprintHashSync(fp);
372
- if (!groups.has(hash)) groups.set(hash, []);
373
- groups.get(hash).push(comparison);
374
- }
375
- }
135
+ ### Screenshot Contrast
376
136
 
377
- // Find groups with multiple similar diffs
378
- for (let [hash, items] of groups) {
379
- if (items.length > 1) {
380
- console.log(`Found ${items.length} similar diffs with hash ${hash}`);
381
- }
382
- }
383
- ```
137
+ ```js
138
+ import { analyzeWcagContrast } from '@vizzly-testing/honeydiff';
384
139
 
385
- **Similarity scoring:**
386
- ```javascript
387
- // Compare two fingerprints (0.0 = different, 1.0 = identical)
388
- let similarity = fingerprintSimilaritySync(fp1, fp2);
140
+ let report = await analyzeWcagContrast('screenshot.png');
389
141
 
390
- if (similarity > 0.8) {
391
- console.log('These diffs are likely the same visual change');
392
- }
142
+ console.log(report.violations.length);
143
+ console.log(report.aaNormalPassPercentage);
144
+ console.log(report.violations);
393
145
  ```
394
146
 
395
- **Fingerprint properties:**
396
- - `clusterCount` - Number of distinct change regions
397
- - `clusterPositions` - Normalized positions (0.0-1.0)
398
- - `clusterSizes` - Relative sizes (area ratio)
399
- - `zoneMask` - 4x4 grid (16 zones) showing affected regions
400
- - `diffMagnitude` - Bucketed size: `tiny`, `small`, `medium`, `large`, `massive`
401
- - `hash` - Pre-computed coarse hash for fast grouping
402
-
403
- ## Accessibility Features
404
-
405
- Built-in WCAG color contrast analysis and color blindness simulation to catch accessibility violations in screenshots and UI designs.
406
-
407
- ### WCAG Contrast Analysis
147
+ ### Simulate Color Vision Deficiency
408
148
 
409
- Analyze entire images for WCAG color contrast violations - perfect for catching accessibility issues in screenshots and UI designs.
410
-
411
- ```javascript
412
- import { analyzeWcagContrast, saveWcagOverlay } from '@vizzly-testing/honeydiff';
413
-
414
- // Analyze a screenshot for accessibility violations
415
- let analysis = await analyzeWcagContrast('screenshot.png', {
416
- edgeThreshold: 60, // Edge detection sensitivity (0-255)
417
- minRegionSize: 50, // Minimum violation region size (pixels)
418
- checkAA: true, // Check WCAG AA (4.5:1 normal, 3.0:1 large)
419
- checkAAA: true // Check WCAG AAA (7.0:1 normal, 4.5:1 large)
420
- });
421
-
422
- console.log(`Total edges analyzed: ${analysis.totalEdges}`);
423
- console.log(`AA normal text pass rate: ${analysis.aaNormalPassPercentage.toFixed(1)}%`);
424
- console.log(`Found ${analysis.violations.length} violations`);
425
-
426
- // Examine violations
427
- analysis.violations.forEach((violation, i) => {
428
- console.log(`\nViolation ${i + 1}:`);
429
- console.log(` Location: (${violation.boundingBox.x}, ${violation.boundingBox.y})`);
430
- console.log(` Contrast ratio: ${violation.contrastRatio.toFixed(2)}:1`);
431
- console.log(` Foreground: rgb(${violation.foregroundColor.slice(0,3).join(', ')})`);
432
- console.log(` Background: rgb(${violation.backgroundColor.slice(0,3).join(', ')})`);
433
- console.log(` Fails AA normal: ${violation.failsAaNormal}`);
434
- console.log(` Fails AAA normal: ${violation.failsAaaNormal}`);
435
- });
436
-
437
- // Generate visual overlay showing violations
438
- await saveWcagOverlay('screenshot.png', analysis, 'violations.png', {
439
- highlightColor: [255, 0, 0, 180] // Semi-transparent red
440
- });
441
- ```
442
-
443
- **Advanced filtering:**
444
- ```javascript
445
- let analysis = await analyzeWcagContrast('screenshot.png', {
446
- edgeThreshold: 80, // Higher = fewer false positives
447
- minRegionSize: 100, // Ignore tiny regions
448
- maxContrastThreshold: 3.5, // Only flag severe violations
449
- checkAA: true,
450
- checkAAA: false
451
- });
452
-
453
- console.log(`AA pass rate: ${analysis.aaNormalPassPercentage.toFixed(1)}%`);
454
- ```
455
-
456
- ### Color Blindness Simulation
457
-
458
- Simulate how your UI appears to users with color vision deficiencies. Uses the scientifically accurate Brettel, Viénot & Mollon 1997 algorithm.
459
-
460
- **Supported CVD types:**
461
- - `protanopia` - Red-blind (~1% of males)
462
- - `deuteranopia` - Green-blind (~1% of males, most common)
463
- - `tritanopia` - Blue-blind (~0.003% of population)
464
- - `achromatopsia` - Complete color blindness (grayscale only)
465
-
466
- ```javascript
149
+ ```js
467
150
  import {
468
- simulateColorBlindness,
469
151
  saveColorBlindnessSimulation,
470
- saveAllColorBlindnessSimulations,
471
- getColorBlindnessTypes
472
152
  } from '@vizzly-testing/honeydiff';
473
153
 
474
- // Get CVD type information
475
- let types = getColorBlindnessTypes();
476
- types.forEach(cvd => {
477
- console.log(`${cvd.name}: ${cvd.description} (${cvd.prevalence})`);
478
- });
479
-
480
- // Simulate and get buffer (useful for in-memory operations)
481
- let simulated = await simulateColorBlindness('chart.png', 'deuteranopia');
482
- fs.writeFileSync('chart-deuteranopia.png', simulated);
483
-
484
- // Or save directly to file
485
- await saveColorBlindnessSimulation('ui.png', 'protanopia', 'ui-protanopia.png');
486
-
487
- // Generate all simulations at once
488
- // Creates: dashboard_protanopia.png, dashboard_deuteranopia.png,
489
- // dashboard_tritanopia.png, dashboard_achromatopsia.png
490
- await saveAllColorBlindnessSimulations('dashboard.png', 'dashboard', 'png');
491
- ```
492
-
493
- ### CVD-Aware WCAG Analysis
494
-
495
- Analyze contrast accessibility specifically for colorblind users. This catches issues that only appear when colors are perceived differently.
496
-
497
- ```javascript
498
- import { analyzeWcagForCvd, analyzeWcagAllCvd } from '@vizzly-testing/honeydiff';
499
-
500
- // Analyze for a specific CVD type
501
- let analysis = await analyzeWcagForCvd('ui.png', 'deuteranopia', {
502
- checkAA: true,
503
- checkAAA: false
504
- });
505
-
506
- if (analysis.violations.length > 0) {
507
- console.log(`Found ${analysis.violations.length} contrast issues for red-green colorblind users`);
508
- }
509
-
510
- // Comprehensive report for all CVD types
511
- let report = await analyzeWcagAllCvd('dashboard.png', {
512
- checkAA: true,
513
- checkAAA: true
514
- });
515
-
516
- console.log('Accessibility Report:');
517
- console.log(`Normal vision: ${report.normalVision.violations.length} violations`);
518
- console.log(`Protanopia: ${report.protanopia.violations.length} violations`);
519
- console.log(`Deuteranopia: ${report.deuteranopia.violations.length} violations`);
520
- console.log(`Tritanopia: ${report.tritanopia.violations.length} violations`);
521
- console.log(`Total: ${report.totalViolations} violations`);
522
- console.log(`CVD-only issues: ~${report.cvdOnlyViolationCount}`);
523
-
524
- if (report.hasAnyViolations) {
525
- console.log('Accessibility issues detected!');
526
- }
527
- ```
528
-
529
- **Test integration:**
530
- ```javascript
531
- test('UI colors work for colorblind users', async () => {
532
- let screenshot = await page.screenshot();
533
-
534
- // Comprehensive CVD accessibility check
535
- let report = await analyzeWcagAllCvd(screenshot, { checkAA: true });
536
-
537
- expect(report.hasAnyViolations).toBe(false);
538
- expect(report.deuteranopia.aaNormalPassPercentage).toBeGreaterThan(95);
539
- });
540
- ```
541
-
542
- ## Complete API Reference
543
-
544
- ### Async Functions (Recommended)
545
-
546
- **`compare(img1, img2, options?): Promise<DiffResult>`**
547
-
548
- Full comparison with detailed results.
549
-
550
- **`quickCompare(img1, img2): Promise<boolean>`**
551
-
552
- Fast boolean check - returns true if different, false if identical.
553
-
554
- **`getDimensions(img): Promise<{ width: number, height: number }>`**
555
-
556
- Get image dimensions without loading full image data.
557
-
558
- **`getImageMetadata(buffer): Promise<ImageMetadata>`**
559
-
560
- Get image metadata (dimensions, file size, format) from a buffer.
561
-
562
- **`getImageMetadataFromFile(path): Promise<ImageMetadata>`**
563
-
564
- Get image metadata from a file path. File size is read from disk for accuracy.
565
-
566
- ### Sync Functions (Blocks Event Loop)
567
-
568
- **`compareSync(img1, img2, options?): DiffResult`**
569
-
570
- Synchronous comparison. Use only when blocking behavior is required.
571
-
572
- **`quickCompareSync(img1, img2): boolean`**
573
-
574
- Synchronous boolean check.
575
-
576
- **`getDimensionsSync(img): { width: number, height: number }`**
577
-
578
- Synchronous dimension retrieval.
579
-
580
- **`getImageMetadataSync(buffer): ImageMetadata`**
581
-
582
- Synchronous metadata retrieval from buffer.
583
-
584
- **`getImageMetadataFromFileSync(path): ImageMetadata`**
585
-
586
- Synchronous metadata retrieval from file.
587
-
588
- ### Diff Fingerprint Functions (Sync only)
589
-
590
- **`computeFingerprintSync(diffResult, width, height): DiffFingerprint | null`**
591
-
592
- Compute a fingerprint from a diff result. Requires `includeClusters: true` in compare options. Returns null if no clusters.
593
-
594
- **`fingerprintSimilaritySync(fp1, fp2): number`**
595
-
596
- Compare two fingerprints. Returns 0.0 (different) to 1.0 (identical).
597
-
598
- **`fingerprintHashSync(fingerprint): string`**
599
-
600
- Get coarse hash for fast grouping. Returns 16-char hex string.
601
-
602
- ### Input Types
603
-
604
- All functions accept:
605
- - `string` - File path to PNG image
606
- - `Buffer` - Image data in memory
607
-
608
- ### CompareOptions
609
-
610
- ```typescript
611
- interface CompareOptions {
612
- // Basic options
613
- threshold?: number; // CIEDE2000 Delta E threshold (default: 2.0)
614
- antialiasing?: boolean; // Ignore AA artifacts (default: true)
615
- maxDiffs?: number; // Stop after N diffs (default: unlimited)
616
-
617
- // Analysis options
618
- includeDiffPixels?: boolean; // List all diff pixels (memory intensive, default: false)
619
- includeClusters?: boolean; // Spatial clustering (default: false)
620
- includeSSIM?: boolean; // SSIM perceptual score (slow, default: false)
621
- includeGMSD?: boolean; // GMSD edge similarity (fast, default: false)
622
- minClusterSize?: number; // Filter clusters smaller than this (default: 2)
623
- clusterMerge?: boolean | { // Merge nearby clusters (default: false)
624
- horizontalDistance?: number; // Max horizontal gap to merge (default: 15)
625
- yBandTolerance?: number; // Vertical "same line" tolerance (default: 5)
626
- maxHeightRatio?: number; // Max height ratio to merge (default: 2.0)
627
- maxWidthRatio?: number; // Max width ratio to merge (default: 3.0)
628
- };
629
-
630
- // Output options
631
- diffPath?: string; // Save diff image path
632
- maskPath?: string; // Save mask image path
633
- overlayPath?: string; // Save overlay image path
634
- overwrite?: boolean; // Overwrite existing files (default: false)
635
- diffMaskColor?: string | number[]; // Highlight color: hex or [r,g,b,a] (default: "ff0000")
636
- }
637
- ```
638
-
639
- ### DiffResult
640
-
641
- ```typescript
154
+ await saveColorBlindnessSimulation(
155
+ 'screenshot.png',
156
+ 'deuteranopia',
157
+ 'screenshot-deuteranopia.png'
158
+ );
159
+ ```
160
+
161
+ ## Options
162
+
163
+ | Option | Default | Notes |
164
+ | --- | --- | --- |
165
+ | `threshold` | `2.0` | CIEDE2000 Delta E threshold. Use `0` for exact matching. |
166
+ | `antialiasing` | `true` | Ignore likely anti-aliased pixels. |
167
+ | `maxDiffs` | unlimited | Stop after a maximum number of differing pixels. |
168
+ | `includeDiffPixels` | `false` | Return individual differing pixel positions and intensities. |
169
+ | `includeClusters` | `false` | Return connected regions of visual change. |
170
+ | `minClusterSize` | `2` | Filter tiny isolated clusters as noise. |
171
+ | `clusterMerge` | disabled | Merge nearby clusters into logical text-like regions. |
172
+ | `includeSSIM` | `false` | Calculate structural similarity. More expensive on large images. |
173
+ | `includeGMSD` | `false` | Calculate fast edge-sensitive structural difference. |
174
+ | `diffPath` | unset | Save a highlighted diff image. |
175
+ | `maskPath` | unset | Save a binary diff mask. |
176
+ | `overlayPath` | unset | Save an overlay comparison image. |
177
+ | `overwrite` | `false` | Replace existing artifact files. |
178
+
179
+ ## Result Shape
180
+
181
+ ```ts
642
182
  interface DiffResult {
643
- // Basic metrics
644
- isDifferent: boolean; // Are images different? (respects minClusterSize filtering)
645
- diffPercentage: number; // 0.0-100.0
183
+ isDifferent: boolean;
184
+ diffPercentage: number;
646
185
  totalPixels: number;
647
- diffPixels: number; // Raw count (unaffected by minClusterSize filtering)
648
-
649
- // Anti-aliasing
186
+ diffPixels: number;
650
187
  aaPixelsIgnored: number;
651
- aaPercentage: number; // 0.0-100.0
652
-
653
- // Spatial info
654
- boundingBox: BoundingBox | null; // Rectangle containing all diffs
655
- heightDiff: HeightDiff | null; // Height difference info
656
-
657
- // Advanced analysis
658
- diffPixelsList: DiffPixel[] | null; // Null unless includeDiffPixels enabled
659
- diffClusters: DiffCluster[] | null; // Null unless includeClusters enabled
660
- intensityStats: IntensityStats | null; // Null unless includeDiffPixels enabled
661
- perceptualScore: number | null; // SSIM 0.0-1.0, null unless includeSSIM enabled
662
- gmsdScore: number | null; // GMSD 0.0+, null unless includeGMSD enabled
188
+ aaPercentage: number;
189
+ boundingBox: BoundingBox | null;
190
+ heightDiff: HeightDiff | null;
191
+ diffPixelsList: DiffPixel[] | null;
192
+ diffClusters: DiffCluster[] | null;
193
+ intensityStats: IntensityStats | null;
194
+ perceptualScore: number | null;
195
+ gmsdScore: number | null;
663
196
  }
664
197
  ```
665
198
 
666
- See [index.d.ts](./index.d.ts) for complete type definitions.
667
-
668
- ## Performance
199
+ See [index.d.ts](./index.d.ts) for the full API surface.
669
200
 
670
- Benchmarked on 18.3M pixel screenshots (750 x 24,162 px):
201
+ ## Thresholds
671
202
 
672
- | Operation | Time | Throughput |
673
- |-----------|------|------------|
674
- | Basic CIEDE2000 comparison | ~79ms | 231.8M pixels/sec |
675
- | With anti-aliasing | ~88ms | 208M pixels/sec |
676
- | With artifacts (diff/mask/overlay) | ~432ms | 42.4M pixels/sec |
203
+ Honeydiff uses CIEDE2000 Delta E for perceptual color difference.
677
204
 
678
- **vs odiff:** 6.0x faster (79ms vs 474ms)
205
+ | Threshold | Meaning |
206
+ | --- | --- |
207
+ | `0` | Exact pixel matching. |
208
+ | `1` | Barely noticeable color changes. |
209
+ | `2` | Recommended default for UI screenshots. |
210
+ | `3+` | More tolerant of rendering differences. |
679
211
 
680
- Multi-core parallelization: 215-309% CPU utilization across cores.
212
+ The default is intentionally practical for browser and app screenshots: it
213
+ filters tiny rendering variance while still catching meaningful UI changes.
681
214
 
682
- *Benchmarked on Apple M-series Mac.*
215
+ The cited algorithms and standards are listed in the repo's
216
+ [References](../../docs/REFERENCES.md), including CIEDE2000, SSIM, MS-SSIM,
217
+ GMSD, Brettel CVD simulation, sRGB, and WCAG contrast math.
683
218
 
684
- ## Common Patterns
219
+ ## Performance
685
220
 
686
- ### CI/CD Integration
221
+ Current local benchmark snapshots:
687
222
 
688
- ```javascript
689
- import { compare } from '@vizzly-testing/honeydiff';
223
+ | Scenario | Result |
224
+ | --- | ---: |
225
+ | Full HD default comparison | ~18.9ms |
226
+ | Full HD exact comparison | ~2.8ms |
227
+ | Identical image fast path | ~0.34ms |
228
+ | 4K default comparison | ~76.9ms |
229
+ | 4K exact comparison | ~14.0ms |
690
230
 
691
- async function visualRegressionTest() {
692
- let result = await compare('baseline.png', 'current.png', {
693
- threshold: 10,
694
- maxDiffs: 1000, // Early exit for fast failure
695
- diffPath: process.env.CI ? './artifacts/diff.png' : undefined
696
- });
697
-
698
- if (result.isDifferent && result.diffPercentage > 1.0) {
699
- console.error(`Visual regression: ${result.diffPercentage.toFixed(2)}% changed`);
700
- process.exit(1);
701
- }
702
- }
703
- ```
231
+ See [benchmarks/BENCHMARK_RESULTS.md](https://github.com/vizzly-testing/honeydiff/blob/main/benchmarks/BENCHMARK_RESULTS.md)
232
+ for the current benchmark notes.
704
233
 
705
- ### Progressive Enhancement
234
+ ## API Overview
706
235
 
707
- ```javascript
708
- // Start with quick check
709
- if (await quickCompare('baseline.png', 'current.png')) {
710
- // Only run expensive analysis if different
711
- let result = await compare('baseline.png', 'current.png', {
712
- includeClusters: true,
713
- includeSSIM: true,
714
- diffPath: './diff.png'
715
- });
716
-
717
- // Detailed analysis and reporting
718
- reportVisualRegression(result);
719
- }
236
+ ```js
237
+ import {
238
+ analyzeWcagAllCvd,
239
+ analyzeWcagContrast,
240
+ analyzeWcagForCvd,
241
+ compare,
242
+ compareSync,
243
+ computeFingerprintSync,
244
+ fingerprintHashSync,
245
+ fingerprintSimilaritySync,
246
+ getColorBlindnessTypes,
247
+ getDimensions,
248
+ getDimensionsSync,
249
+ getImageMetadata,
250
+ getImageMetadataFromFile,
251
+ getImageMetadataFromFileSync,
252
+ getImageMetadataSync,
253
+ quickCompare,
254
+ quickCompareSync,
255
+ saveAllColorBlindnessSimulations,
256
+ saveColorBlindnessSimulation,
257
+ saveWcagOverlay,
258
+ simulateColorBlindness,
259
+ } from '@vizzly-testing/honeydiff';
720
260
  ```
721
261
 
722
- ### Tolerance Thresholds
262
+ ## Development
723
263
 
724
- ```javascript
725
- const result = await compare('baseline.png', 'current.png', {
726
- threshold: 10
727
- });
728
-
729
- if (result.diffPercentage > 5.0) {
730
- console.error('FAIL: Major visual regression');
731
- process.exit(1);
732
- } else if (result.diffPercentage > 1.0) {
733
- console.warn('WARN: Minor visual changes detected');
734
- } else {
735
- console.log('PASS: Visual test passed');
736
- }
264
+ ```bash
265
+ pnpm install
266
+ cargo build --release
267
+ cargo test
737
268
  ```
738
269
 
739
- ## Troubleshooting
740
-
741
- **Module not found errors**
742
- - Ensure Node.js 22+ is installed
743
- - Verify your platform is supported (macOS ARM64, Linux x64/ARM64, Windows x64)
744
- - Try `npm install` again to download pre-built binaries
745
-
746
- **Large memory usage with `includeDiffPixels`**
747
- - This option stores every different pixel's position and intensity
748
- - Disable for large images with many differences
749
- - Only enable if you need detailed pixel-level data
750
-
751
- **Slow performance with `includeSSIM`**
752
- - SSIM calculation is O(width × height × window²)
753
- - Only enable when perceptual scoring is needed
754
- - Consider running SSIM only on failed comparisons
755
-
756
- **Anti-aliasing too aggressive**
757
- - Set `antialiasing: false` for stricter pixel matching
758
- - Adjust `threshold` instead for controlled flexibility
759
-
760
- ## Use Cases
761
-
762
- - **Visual Regression Testing** - Catch unintended UI changes in CI/CD
763
- - **Screenshot Validation** - Compare generated screenshots against baselines
764
- - **Accessibility Testing** - Validate WCAG contrast and color blindness visibility
765
- - **Design QA** - Ensure implementations match design specifications
766
- - **Cross-browser Testing** - Detect rendering differences across browsers
767
- - **A/B Testing** - Measure visual differences between variants
768
- - **Responsive Design** - Compare layouts across viewports
769
-
770
- ## Examples
771
-
772
- See [examples/](./examples/) directory:
773
- - [basic.js](./examples/basic.js) - Core features and comparison options
774
- - [buffers.js](./examples/buffers.js) - Working with in-memory images
775
- - [typescript.ts](./examples/typescript.ts) - TypeScript integration
776
- - [wcag.js](./examples/wcag.js) - WCAG contrast analysis
777
- - [color-blindness.js](./examples/color-blindness.js) - CVD simulation and accessibility
270
+ The package is ESM-first and release packages include native binaries under
271
+ `platforms/*.node`.
778
272
 
779
273
  ## License
780
274
 
781
- MIT License
782
-
783
- Built with native Rust for maximum performance.
275
+ MIT