@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 +390 -235
- package/index.d.ts +261 -60
- package/package.json +11 -1
- package/platforms/index-aarch64-apple-darwin.node +0 -0
- package/platforms/index-aarch64-unknown-linux-gnu.node +0 -0
- package/platforms/index-x86_64-pc-windows-msvc.node +0 -0
- package/platforms/index-x86_64-unknown-linux-gnu.node +0 -0
package/README.md
CHANGED
|
@@ -4,358 +4,513 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@vizzly-testing/honeydiff)
|
|
5
5
|
[](https://github.com/vizzly-testing/honeydiff/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
Blazingly fast image comparison for Node.js
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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)}
|
|
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
|
-
|
|
49
|
+
### TypeScript
|
|
51
50
|
|
|
52
51
|
```typescript
|
|
53
52
|
import { compare, CompareOptions, DiffResult } from '@vizzly-testing/honeydiff';
|
|
54
53
|
|
|
55
|
-
const
|
|
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}
|
|
63
|
+
console.log(`Found ${result.diffClusters.length} different regions`);
|
|
63
64
|
}
|
|
64
|
-
// Output: Found 3 diff regions
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
### Test Integration
|
|
68
68
|
|
|
69
|
-
**
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
+
**Playwright**
|
|
81
|
+
```javascript
|
|
82
|
+
const { compare } = require('@vizzly-testing/honeydiff');
|
|
80
83
|
|
|
81
|
-
|
|
84
|
+
test('button hover state', async () => {
|
|
85
|
+
await page.hover('[data-testid="submit-button"]');
|
|
86
|
+
let screenshot = await page.screenshot();
|
|
82
87
|
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
let result = await compare('baseline.png', screenshot, {
|
|
89
|
+
pixelTolerance: 10,
|
|
90
|
+
diffPath: './artifacts/diff.png'
|
|
91
|
+
});
|
|
85
92
|
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
108
|
-
<summary><strong>DiffResult</strong></summary>
|
|
97
|
+
## Core Features
|
|
109
98
|
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
121
|
+
Disable for pixel-perfect matching:
|
|
149
122
|
```javascript
|
|
150
|
-
const
|
|
151
|
-
|
|
123
|
+
const result = await compare('img1.png', 'img2.png', {
|
|
124
|
+
antialiasing: false // Strict pixel matching
|
|
125
|
+
});
|
|
126
|
+
```
|
|
152
127
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
142
|
+
### 4. Visual Diff Artifacts
|
|
143
|
+
|
|
144
|
+
Generate highlighted diff images, binary masks, and overlays for debugging.
|
|
159
145
|
|
|
160
146
|
```javascript
|
|
161
|
-
|
|
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
|
-
|
|
157
|
+
### 5. Spatial Clustering
|
|
158
|
+
|
|
159
|
+
Group nearby differences into regions to identify distinct changed areas.
|
|
160
|
+
|
|
170
161
|
```javascript
|
|
171
|
-
|
|
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((
|
|
180
|
-
console.log(`Region ${i + 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:
|
|
185
|
-
//
|
|
186
|
-
//
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
202
|
+
### 7. Tolerance & Color Spaces
|
|
202
203
|
|
|
204
|
+
**RGB Mode (default)** - Exact matching with pixel tolerance:
|
|
203
205
|
```javascript
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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(
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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
|
-
|
|
376
|
+
### DiffResult
|
|
305
377
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
454
|
+
// Detailed analysis and reporting
|
|
455
|
+
reportVisualRegression(result);
|
|
456
|
+
}
|
|
457
|
+
```
|
|
342
458
|
|
|
343
|
-
|
|
344
|
-
Group nearby differences into regions using 8-connectivity clustering. Useful for identifying distinct changed areas.
|
|
459
|
+
### Tolerance Thresholds
|
|
345
460
|
|
|
346
|
-
|
|
347
|
-
|
|
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
|
|
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
|
|
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²)
|
|
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.
|
|
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": {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|