@vizzly-testing/honeydiff 0.2.0 → 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,205 +4,300 @@
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?
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
25
23
 
26
- Pre-built binaries included for macOS (ARM64), Linux (x64/ARM64), and Windows (x64). No compilation
27
- required.
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)}`);
194
+ // Output: SSIM: 0.923 (0.0 = different, 1.0 = identical)
195
+
196
+ if (result.perceptualScore > 0.95) {
197
+ console.log('Images are perceptually very similar');
198
+ }
191
199
  }
192
- // Output: SSIM: 0.923
200
+ ```
201
+
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
+ });
209
+ ```
210
+
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
+
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);
256
+ ```
193
257
 
194
- // Intensity stats
195
- if (result.intensityStats) {
196
- console.log(`Range: ${result.intensityStats.min}-${result.intensityStats.max}, Mean: ${result.intensityStats.mean.toFixed(1)}`);
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
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
+ });
197
292
  }
198
- // Output: Range: 12-255, Mean: 87.3
199
293
  ```
200
294
 
201
- **Accessibility Features**
295
+ ### RGB Channel Analysis
296
+
297
+ Track which color channels changed - useful for debugging design changes.
202
298
 
203
299
  ```javascript
204
- // WCAG contrast checking and RGB analysis
205
- let result = await compare('baseline.png', 'current.png', {
300
+ const result = await compare('baseline.png', 'current.png', {
206
301
  includeClusters: true,
207
302
  includeAccessibilityData: true
208
303
  });
@@ -211,31 +306,40 @@ if (result.diffClusters) {
211
306
  result.diffClusters.forEach(cluster => {
212
307
  if (cluster.accessibilityMetadata) {
213
308
  let meta = cluster.accessibilityMetadata;
309
+
214
310
  console.log(`RGB diffs: R=${meta.avgRDiff}, G=${meta.avgGDiff}, B=${meta.avgBDiff}`);
215
311
  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
- }
312
+ console.log(`Luminance: baseline=${meta.avgBaselineLuminance.toFixed(3)}, current=${meta.avgCurrentLuminance.toFixed(3)}`);
221
313
  }
222
314
  });
223
315
  }
316
+ // Output:
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
+ ```
321
+
322
+ ### Color Blindness Simulation
323
+
324
+ Test if visual changes are visible to users with color vision deficiencies.
224
325
 
225
- // Color blindness simulation
226
- let result = await compare('baseline.png', 'current.png', {
326
+ ```javascript
327
+ const result = await compare('baseline.png', 'current.png', {
227
328
  includeClusters: true,
228
- checkColorBlindness: true
329
+ checkColorBlindness: true,
330
+ colorBlindnessThreshold: 30.0 // Sensitivity threshold (0-255)
229
331
  });
230
332
 
231
333
  if (result.diffClusters) {
232
334
  result.diffClusters.forEach(cluster => {
233
335
  if (cluster.accessibilityMetadata?.colorBlindnessImpact) {
234
336
  let impact = cluster.accessibilityMetadata.colorBlindnessImpact;
337
+
235
338
  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 ? '✓' : '✗'}`);
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 ? '✓' : '✗'}`);
239
343
 
240
344
  if (impact.visibilityScore < 0.5) {
241
345
  console.log('⚠️ May not be visible to colorblind users');
@@ -245,117 +349,229 @@ if (result.diffClusters) {
245
349
  }
246
350
  ```
247
351
 
248
- **Complete Result Example**
352
+ **Real-world example:**
353
+
249
354
  ```javascript
250
- let result = await compare('baseline.png', 'current.png', {
251
- pixelTolerance: 10,
252
- includeClusters: true
253
- });
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
+ });
360
+
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
+ );
254
367
 
255
- console.log(JSON.stringify(result, null, 2));
368
+ expect(lowestVisibility).toBeGreaterThan(0.7);
369
+ });
256
370
  ```
257
371
 
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
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)
299
434
  }
300
435
  ```
301
- </details>
302
- ```
303
436
 
304
- ## Use Cases
437
+ ### DiffResult
305
438
 
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
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.
312
464
 
313
465
  ## Performance
314
466
 
315
- 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):
316
468
 
317
469
  | Operation | Time | Throughput |
318
470
  |-----------|------|------------|
319
- | 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 |
320
474
  | With artifacts (diff/mask/overlay) | ~432ms | 42.4M pixels/sec |
321
475
 
322
- **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.
323
479
 
324
480
  *Benchmarked on Apple M-series Mac.*
325
481
 
326
- ## Advanced Features
482
+ ## Common Patterns
483
+
484
+ ### CI/CD Integration
485
+
486
+ ```javascript
487
+ const { compare } = require('@vizzly-testing/honeydiff');
327
488
 
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
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
+ });
334
495
 
335
- **Variable Height Screenshots**
336
- Compare scrollable content with different heights. Overlapping regions are compared pixel-by-pixel,
337
- 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
+ ```
338
502
 
339
- **Color Spaces**
340
- - RGB mode (default): Exact matching, fastest performance
341
- - YIQ mode: Perceptual matching weighted for human vision
503
+ ### Progressive Enhancement
342
504
 
343
- **Spatial Clustering**
344
- 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
+ ```
345
519
 
346
- **SSIM Scoring**
347
- 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
+ ```
348
536
 
349
537
  ## Troubleshooting
350
538
 
351
539
  **Module not found errors**
352
- 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
353
543
 
354
544
  **Large memory usage with `includeDiffPixels`**
355
- 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
356
548
 
357
549
  **Slow performance with `includeSSIM`**
358
- 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
359
575
 
360
576
  ## License
361
577
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizzly-testing/honeydiff",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "High-performance image diffing for Node.js - Native bindings to Honeydiff Rust library",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -65,6 +65,11 @@
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",
68
73
  "README.md"
69
74
  ],
70
75
  "engines": {