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