@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 CHANGED
@@ -4,299 +4,574 @@
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
- - **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
- **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?
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
- Pre-built binaries included for macOS (ARM64), Linux (x64/ARM64), and Windows (x64). No compilation
26
- required.
24
+ Built for production visual regression testing at scale.
27
25
 
28
26
  ## Quick Start
29
27
 
30
- **JavaScript**
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)}%, SSIM: ${result.perceptualScore}`);
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
- **TypeScript**
49
+ ### TypeScript
50
50
 
51
51
  ```typescript
52
52
  import { compare, CompareOptions, DiffResult } from '@vizzly-testing/honeydiff';
53
53
 
54
- const result: DiffResult = await compare('img1.png', 'img2.png', {
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} diff regions`);
63
+ console.log(`Found ${result.diffClusters.length} different regions`);
62
64
  }
63
- // Output: Found 3 diff regions
64
65
  ```
65
66
 
66
- ## API
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
- **Async (recommended)**
69
- - `compare(img1, img2, options?): Promise<DiffResult>` - Full comparison
70
- - `quickCompare(img1, img2): Promise<boolean>` - Quick check
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
- **Sync (blocks event loop)**
74
- - `compareSync(img1, img2, options?): DiffResult`
75
- - `quickCompareSync(img1, img2): boolean`
76
- - `getDimensionsSync(img): { width: number, height: number }` - Get image dimensions
88
+ let result = await compare('baseline.png', screenshot, {
89
+ pixelTolerance: 10,
90
+ diffPath: './artifacts/diff.png'
91
+ });
77
92
 
78
- Inputs: `string` (file path) or `Buffer`
93
+ expect(result.diffPercentage).toBeLessThan(1.0);
94
+ });
95
+ ```
79
96
 
80
- ## Options & Results
97
+ ## Core Features
81
98
 
82
- <details>
83
- <summary><strong>CompareOptions</strong></summary>
99
+ ### 1. Performance at Scale
84
100
 
85
- ```typescript
86
- interface CompareOptions {
87
- pixelTolerance?: number; // 0-255 (default: 0)
88
- colorThreshold?: number; // 0.0-1.0 for YIQ mode (default: 0.0 = RGB)
89
- antialiasing?: boolean; // Enable AA detection (default: true)
90
- ignoreColors?: boolean; // Brightness only (default: false)
91
- maxDiffs?: number; // Stop after N diffs (default: unlimited)
92
- includeDiffPixels?: boolean; // Detailed pixel list (default: false)
93
- includeClusters?: boolean; // Spatial clusters (default: false)
94
- includeSSIM?: boolean; // SSIM score (default: false)
95
- diffPath?: string; // Save diff image
96
- maskPath?: string; // Save mask image
97
- overlayPath?: string; // Save overlay image
98
- overwrite?: boolean; // Overwrite files (default: false)
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
- <details>
104
- <summary><strong>DiffResult</strong></summary>
142
+ ### 4. Visual Diff Artifacts
105
143
 
106
- ```typescript
107
- interface DiffResult {
108
- isDifferent: boolean;
109
- diffPercentage: number;
110
- totalPixels: number;
111
- diffPixels: number;
112
- aaPixelsIgnored: number;
113
- aaPercentage: number;
114
- boundingBox: BoundingBox | null;
115
- heightDiff: HeightDiff | null;
116
- diffPixelsList: DiffPixel[] | null;
117
- diffClusters: DiffCluster[] | null;
118
- intensityStats: IntensityStats | null;
119
- perceptualScore: number | null; // SSIM 0.0-1.0
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
- ## Examples
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 { getDimensions, getDimensionsSync } = require('@vizzly-testing/honeydiff');
188
+ const result = await compare('img1.png', 'img2.png', {
189
+ includeSSIM: true // Note: Can be slow on large images
190
+ });
129
191
 
130
- // Async
131
- let dims = await getDimensions('screenshot.png');
132
- console.log(`Image is ${dims.width}x${dims.height} pixels`);
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
- // Sync
136
- let dims = getDimensionsSync('screenshot.png');
137
- console.log(`Image is ${dims.width}x${dims.height} pixels`);
196
+ if (result.perceptualScore > 0.95) {
197
+ console.log('Images are perceptually very similar');
198
+ }
199
+ }
200
+ ```
138
201
 
139
- // Works with buffers too
140
- let buffer = fs.readFileSync('image.png');
141
- let dims = await getDimensions(buffer);
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
- **Buffers**
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
- **Diff Images**
242
+ ### 9. Image Dimensions
243
+
244
+ Fast utility to get image dimensions without loading full image data.
155
245
 
156
246
  ```javascript
157
- let result = await compare('baseline.png', 'current.png', {
158
- diffPath: './diff.png',
159
- maskPath: './mask.png',
160
- overlayPath: './overlay.png',
161
- overwrite: true
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
- **Clustering & Metrics**
295
+ ### RGB Channel Analysis
296
+
297
+ Track which color channels changed - useful for debugging design changes.
298
+
166
299
  ```javascript
167
- let result = await compare('img1.png', 'img2.png', {
300
+ const result = await compare('baseline.png', 'current.png', {
168
301
  includeClusters: true,
169
- includeSSIM: true,
170
- includeDiffPixels: true
302
+ includeAccessibilityData: true
171
303
  });
172
304
 
173
- // Spatial clusters
174
305
  if (result.diffClusters) {
175
- result.diffClusters.forEach((c, i) => {
176
- console.log(`Region ${i + 1}: ${c.pixelCount} pixels at (${c.centerOfMass[0]}, ${c.centerOfMass[1]})`);
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
- // Region 1: 12450 pixels at (245, 189)
181
- // Region 2: 3891 pixels at (672, 401)
182
- // Region 3: 1023 pixels at (1156, 823)
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
- // SSIM score
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
- // Intensity stats
191
- if (result.intensityStats) {
192
- console.log(`Range: ${result.intensityStats.min}-${result.intensityStats.max}, Mean: ${result.intensityStats.mean.toFixed(1)}`);
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
- **Complete Result Example**
352
+ **Real-world example:**
353
+
198
354
  ```javascript
199
- let result = await compare('baseline.png', 'current.png', {
200
- pixelTolerance: 10,
201
- includeClusters: true
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
- console.log(JSON.stringify(result, null, 2));
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
- <details>
208
- <summary>Example output (click to expand)</summary>
209
-
210
- ```json
211
- {
212
- "isDifferent": true,
213
- "diffPercentage": 3.55,
214
- "totalPixels": 18309000,
215
- "diffPixels": 650086,
216
- "aaPixelsIgnored": 1952904,
217
- "aaPercentage": 10.67,
218
- "boundingBox": {
219
- "x": 124,
220
- "y": 89,
221
- "width": 1456,
222
- "height": 2301
223
- },
224
- "heightDiff": {
225
- "height1": 24162,
226
- "height2": 24412,
227
- "extraPixels": 187500
228
- },
229
- "diffPixelsList": null,
230
- "diffClusters": [
231
- {
232
- "pixelCount": 12450,
233
- "centerOfMass": [245, 189]
234
- },
235
- {
236
- "pixelCount": 3891,
237
- "centerOfMass": [672, 401]
238
- }
239
- ],
240
- "intensityStats": {
241
- "min": 12,
242
- "max": 255,
243
- "mean": 87.3,
244
- "median": 76,
245
- "stdDev": 45.2
246
- },
247
- "perceptualScore": null
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
- ## Use Cases
437
+ ### DiffResult
254
438
 
255
- - **Visual Regression Testing:** Detect unintended UI changes in your test suite
256
- - **Screenshot Validation:** Compare generated screenshots against baselines
257
- - **Design QA:** Validate UI implementations match designs
258
- - **Cross-browser Testing:** Detect rendering differences across browsers
259
- - **Scrollable Content:** Compare full-page screenshots with variable heights
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
- Tested on real-world tall screenshots (750 x 24,162 pixels = 18.3M pixels):
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
- **6x faster than odiff** on large screenshots with excellent multi-core parallelization.
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
- ## Advanced Features
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
- **Variable Height Screenshots**
277
- Compare scrollable content with different heights. Overlapping regions are compared pixel-by-pixel,
278
- extra rows counted as differences.
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
- **Color Spaces**
281
- - RGB mode (default): Exact matching, fastest performance
282
- - YIQ mode: Perceptual matching weighted for human vision
503
+ ### Progressive Enhancement
283
504
 
284
- **Spatial Clustering**
285
- Group nearby differences into regions using 8-connectivity clustering. Useful for identifying distinct changed areas.
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
- **SSIM Scoring**
288
- Structural Similarity Index (0.0-1.0) provides perceptual similarity measurement beyond pixel differences.
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 you're on Node.js 22+ and your platform is supported (macOS ARM64, Linux x64/ARM64, Windows x64).
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. Disable for large images with many differences.
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²). Only enable when perceptual scoring is needed.
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.1.1",
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": {