@vizzly-testing/honeydiff 0.2.1 → 0.3.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
@@ -16,7 +16,7 @@ Requires Node.js 22+. Pre-built binaries included for macOS (ARM64), Linux (x64/
16
16
 
17
17
  - **6x faster than odiff** - Processes 230M+ pixels/sec with multi-core parallelization
18
18
  - **Smart anti-aliasing** - Filters out rendering artifacts, not actual changes
19
- - **Accessibility built-in** - WCAG contrast checking and color blindness simulation
19
+ - **Accessibility built-in** - WCAG contrast analysis for catching accessibility violations
20
20
  - **Flexible inputs** - Works with file paths or Buffers
21
21
  - **Full TypeScript support** - Complete type definitions included
22
22
  - **Advanced analytics** - SSIM perceptual scoring, spatial clustering, intensity stats
@@ -257,116 +257,55 @@ let dims = await getDimensions(buffer);
257
257
 
258
258
  ## Accessibility Features
259
259
 
260
- Built-in accessibility testing to catch WCAG compliance issues and color blindness visibility problems.
260
+ Built-in WCAG color contrast analysis to catch accessibility violations in screenshots and UI designs.
261
261
 
262
- ### WCAG Contrast Checking
262
+ ### WCAG Contrast Analysis
263
263
 
264
- Validate that visual changes meet WCAG AA/AAA contrast standards.
264
+ Analyze entire images for WCAG color contrast violations - perfect for catching accessibility issues in screenshots and UI designs.
265
265
 
266
266
  ```javascript
267
- const result = await compare('baseline.png', 'current.png', {
268
- includeClusters: true,
269
- includeAccessibilityData: 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)
270
275
  });
271
276
 
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
- }
293
- ```
294
-
295
- ### RGB Channel Analysis
296
-
297
- Track which color channels changed - useful for debugging design changes.
298
-
299
- ```javascript
300
- const result = await compare('baseline.png', 'current.png', {
301
- includeClusters: true,
302
- includeAccessibilityData: true
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}`);
303
290
  });
304
291
 
305
- if (result.diffClusters) {
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
- }
314
- });
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.
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)
292
+ // Generate visual overlay showing violations
293
+ await saveWcagOverlay('screenshot.png', analysis, 'violations.png', {
294
+ highlightColor: [255, 0, 0, 180] // Semi-transparent red
331
295
  });
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
- });
349
- }
350
296
  ```
351
297
 
352
- **Real-world example:**
353
-
298
+ **Advanced filtering:**
354
299
  ```javascript
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
- );
367
-
368
- expect(lowestVisibility).toBeGreaterThan(0.7);
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
369
306
  });
307
+
308
+ console.log(`AA pass rate: ${analysis.aaNormalPassPercentage.toFixed(1)}%`);
370
309
  ```
371
310
 
372
311
  ## Complete API Reference
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/index.js CHANGED
@@ -21,4 +21,12 @@ module.exports = {
21
21
  quickCompare: addon.quickCompare,
22
22
  compareSync: addon.compareSync,
23
23
  quickCompareSync: addon.quickCompareSync,
24
+ getDimensions: addon.getDimensions,
25
+ getDimensionsSync: addon.getDimensionsSync,
26
+
27
+ // WCAG Accessibility API
28
+ analyzeWcagContrast: addon.analyzeWcagContrast,
29
+ analyzeWcagContrastSync: addon.analyzeWcagContrastSync,
30
+ saveWcagOverlay: addon.saveWcagOverlay,
31
+ saveWcagOverlaySync: addon.saveWcagOverlaySync,
24
32
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizzly-testing/honeydiff",
3
- "version": "0.2.1",
3
+ "version": "0.3.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",
@@ -70,6 +70,16 @@
70
70
  "index.d.ts",
71
71
  "load-platform.js",
72
72
  "platforms/*.node",
73
+ "README.md",
74
+ "index.js",
75
+ "index.d.ts",
76
+ "load-platform.js",
77
+ "platforms/*.node",
78
+ "README.md",
79
+ "index.js",
80
+ "index.d.ts",
81
+ "load-platform.js",
82
+ "platforms/*.node",
73
83
  "README.md"
74
84
  ],
75
85
  "engines": {