@vizzly-testing/honeydiff 0.8.1 → 0.10.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/CHANGELOG.md +49 -0
- package/README.md +139 -12
- package/examples/basic.js +316 -0
- package/examples/buffers.js +40 -0
- package/examples/color-blindness.js +211 -0
- package/examples/typescript.ts +78 -0
- package/examples/wcag.js +159 -0
- package/index.d.ts +222 -0
- package/index.js +5 -0
- package/package.json +2 -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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,55 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.10.0] - 2026-02-03
|
|
9
|
+
|
|
10
|
+
## What's Changed
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **GMSD (Gradient Magnitude Similarity Deviation)** - New `includeGMSD` option for fast, edge-sensitive perceptual comparison
|
|
14
|
+
- Returns `gmsdScore` in results (0.0 = identical, higher = more different)
|
|
15
|
+
- Very fast compared to SSIM, ideal for detecting border/outline changes
|
|
16
|
+
- Based on Xue et al. 2014 research paper
|
|
17
|
+
- **Note:** GMSD requires images with identical dimensions. For variable-height comparisons, `gmsdScore` will be `null`
|
|
18
|
+
- **Cluster Merging** - New `clusterMerge` option to consolidate fragmented text regions
|
|
19
|
+
- Simple API: `clusterMerge: true` enables with sensible defaults
|
|
20
|
+
- Advanced API: Pass an object with `horizontalDistance`, `yBandTolerance`, `maxHeightRatio`, `maxWidthRatio`
|
|
21
|
+
- Solves the "59 clusters for one date string" problem by intelligently merging nearby character-level changes into logical text regions
|
|
22
|
+
- Uses SWT-inspired heuristics (Epshtein et al. 2010) for horizontal-biased text detection
|
|
23
|
+
|
|
24
|
+
**Full Changelog**: https://github.com/vizzly-testing/honeydiff/compare/v0.9.0...v0.10.0
|
|
25
|
+
|
|
26
|
+
## [Unreleased]
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- **GMSD (Gradient Magnitude Similarity Deviation)** - New `includeGMSD` option for fast, edge-sensitive perceptual comparison
|
|
30
|
+
- Returns `gmsdScore` in results (0.0 = identical, higher = more different)
|
|
31
|
+
- Very fast compared to SSIM, ideal for detecting border/outline changes
|
|
32
|
+
- Based on Xue et al. 2014 research paper
|
|
33
|
+
- **Cluster Merging** - New `clusterMerge` option to consolidate fragmented text regions
|
|
34
|
+
- Simple API: `clusterMerge: true` enables with sensible defaults
|
|
35
|
+
- Advanced API: Pass an object with `horizontalDistance`, `yBandTolerance`, `maxHeightRatio`, `maxWidthRatio`
|
|
36
|
+
- Solves the "59 clusters for one date string" problem
|
|
37
|
+
- Uses SWT-inspired heuristics (Epshtein et al. 2010)
|
|
38
|
+
|
|
39
|
+
## [0.9.0] - 2026-01-26
|
|
40
|
+
|
|
41
|
+
## What's Changed
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
- **Diff Fingerprinting API** - New functionality for grouping similar diffs across different image comparisons. This enables batch-approving the same visual change that appears on multiple pages (e.g., header/footer updates).
|
|
45
|
+
- `computeFingerprintSync()` - Generate a compact fingerprint from a diff result
|
|
46
|
+
- `fingerprintSimilaritySync()` - Compare two fingerprints with a similarity score (0.0-1.0)
|
|
47
|
+
- `fingerprintHashSync()` - Get a coarse hash for fast O(1) grouping
|
|
48
|
+
- `DiffFingerprint` interface with rich metadata:
|
|
49
|
+
- `zoneMask` - 4x4 spatial grid showing which regions are affected
|
|
50
|
+
- `clusterCount`, `clusterPositions`, `clusterSizes` - Spatial distribution data
|
|
51
|
+
- `diffMagnitude` - Bucketed size categories (tiny/small/medium/large/massive)
|
|
52
|
+
- `avgIntensity`, `avgDensity` - Change characteristics
|
|
53
|
+
- `hash` - Pre-computed coarse hash for grouping
|
|
54
|
+
|
|
55
|
+
**Full Changelog**: https://github.com/vizzly-testing/honeydiff/compare/v0.8.1...v0.9.0
|
|
56
|
+
|
|
8
57
|
## [0.8.1] - 2026-01-09
|
|
9
58
|
|
|
10
59
|
## What's Changed
|
package/README.md
CHANGED
|
@@ -188,10 +188,39 @@ if (result.diffClusters) {
|
|
|
188
188
|
| 2 | Default - filters single isolated pixels as rendering noise |
|
|
189
189
|
| 3+ | More permissive - only larger clusters detected |
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
**Cluster Merging for Text Regions:**
|
|
192
|
+
|
|
193
|
+
Text changes often fragment into many small clusters (one per character). Enable cluster merging to consolidate them into logical regions:
|
|
194
|
+
|
|
195
|
+
```javascript
|
|
196
|
+
// Simple: enable with sensible defaults
|
|
197
|
+
const result = await compare('img1.png', 'img2.png', {
|
|
198
|
+
includeClusters: true,
|
|
199
|
+
clusterMerge: true // Merge nearby clusters (great for text)
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Advanced: tune the merging behavior
|
|
203
|
+
const result = await compare('img1.png', 'img2.png', {
|
|
204
|
+
includeClusters: true,
|
|
205
|
+
clusterMerge: {
|
|
206
|
+
horizontalDistance: 15, // Max gap between clusters to merge (pixels)
|
|
207
|
+
yBandTolerance: 5, // Vertical tolerance for "same line"
|
|
208
|
+
maxHeightRatio: 2.0, // Prevent merging very different sized clusters
|
|
209
|
+
maxWidthRatio: 3.0
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Before: "2024-01-01" detected as 59 clusters (one per character/gap)
|
|
214
|
+
// After: "2024-01-01" detected as 1-2 logical regions
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Uses SWT-inspired heuristics from text detection research (Epshtein et al. 2010).
|
|
218
|
+
|
|
219
|
+
### 6. Perceptual Similarity (SSIM & GMSD)
|
|
192
220
|
|
|
193
221
|
Beyond pixel counting - measure structural similarity from a human perception perspective.
|
|
194
222
|
|
|
223
|
+
**SSIM (Structural Similarity Index)** - Overall perceptual similarity:
|
|
195
224
|
```javascript
|
|
196
225
|
const result = await compare('img1.png', 'img2.png', {
|
|
197
226
|
includeSSIM: true // Note: Can be slow on large images
|
|
@@ -200,13 +229,33 @@ const result = await compare('img1.png', 'img2.png', {
|
|
|
200
229
|
if (result.perceptualScore !== null) {
|
|
201
230
|
console.log(`SSIM: ${result.perceptualScore.toFixed(3)}`);
|
|
202
231
|
// Output: SSIM: 0.923 (0.0 = different, 1.0 = identical)
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**GMSD (Gradient Magnitude Similarity Deviation)** - Fast edge-sensitive metric:
|
|
236
|
+
```javascript
|
|
237
|
+
const result = await compare('img1.png', 'img2.png', {
|
|
238
|
+
includeGMSD: true // Very fast, great for detecting structural changes
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (result.gmsdScore !== null) {
|
|
242
|
+
console.log(`GMSD: ${result.gmsdScore.toFixed(4)}`);
|
|
243
|
+
// Output: GMSD: 0.0234 (0.0 = identical, higher = more different)
|
|
203
244
|
|
|
204
|
-
if (result.
|
|
205
|
-
console.log('
|
|
245
|
+
if (result.gmsdScore < 0.05) {
|
|
246
|
+
console.log('Edges are very similar');
|
|
206
247
|
}
|
|
207
248
|
}
|
|
208
249
|
```
|
|
209
250
|
|
|
251
|
+
GMSD is ideal for catching:
|
|
252
|
+
- Border thickness changes
|
|
253
|
+
- Font weight shifts
|
|
254
|
+
- Icon updates
|
|
255
|
+
- Any edge/outline regressions
|
|
256
|
+
|
|
257
|
+
**Reference:** Xue et al. 2014 - "Gradient Magnitude Similarity Deviation: A Highly Efficient Perceptual Image Quality Index"
|
|
258
|
+
|
|
210
259
|
### 7. Tolerance & Color Spaces
|
|
211
260
|
|
|
212
261
|
**RGB Mode (default)** - Exact matching with pixel tolerance:
|
|
@@ -285,6 +334,69 @@ let metadata = await getImageMetadataFromFile('screenshot.png');
|
|
|
285
334
|
console.log(`${metadata.width}x${metadata.height}, ${metadata.fileSizeBytes} bytes`);
|
|
286
335
|
```
|
|
287
336
|
|
|
337
|
+
### 11. Diff Fingerprinting
|
|
338
|
+
|
|
339
|
+
Group similar diffs across comparisons with fingerprinting. Perfect for batch-approving the same visual change that appears on multiple pages (e.g., header/footer updates).
|
|
340
|
+
|
|
341
|
+
```javascript
|
|
342
|
+
const { compare, computeFingerprintSync, fingerprintSimilaritySync, fingerprintHashSync } = require('@vizzly-testing/honeydiff');
|
|
343
|
+
|
|
344
|
+
// Compare images with clustering enabled
|
|
345
|
+
let result = await compare('baseline.png', 'current.png', {
|
|
346
|
+
includeClusters: true
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Compute fingerprint
|
|
350
|
+
let fingerprint = computeFingerprintSync(result, 1920, 1080);
|
|
351
|
+
|
|
352
|
+
if (fingerprint) {
|
|
353
|
+
console.log(`Hash: ${fingerprint.hash}`);
|
|
354
|
+
console.log(`Zones affected: ${fingerprint.zoneMask.toString(2).padStart(16, '0')}`);
|
|
355
|
+
console.log(`Cluster count: ${fingerprint.clusterCount}`);
|
|
356
|
+
console.log(`Magnitude: ${fingerprint.diffMagnitude}`);
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**Grouping similar diffs:**
|
|
361
|
+
```javascript
|
|
362
|
+
// Group diffs by hash for batch approval
|
|
363
|
+
let groups = new Map();
|
|
364
|
+
|
|
365
|
+
for (let comparison of comparisons) {
|
|
366
|
+
let fp = computeFingerprintSync(comparison.result, width, height);
|
|
367
|
+
if (fp) {
|
|
368
|
+
let hash = fingerprintHashSync(fp);
|
|
369
|
+
if (!groups.has(hash)) groups.set(hash, []);
|
|
370
|
+
groups.get(hash).push(comparison);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Find groups with multiple similar diffs
|
|
375
|
+
for (let [hash, items] of groups) {
|
|
376
|
+
if (items.length > 1) {
|
|
377
|
+
console.log(`Found ${items.length} similar diffs with hash ${hash}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**Similarity scoring:**
|
|
383
|
+
```javascript
|
|
384
|
+
// Compare two fingerprints (0.0 = different, 1.0 = identical)
|
|
385
|
+
let similarity = fingerprintSimilaritySync(fp1, fp2);
|
|
386
|
+
|
|
387
|
+
if (similarity > 0.8) {
|
|
388
|
+
console.log('These diffs are likely the same visual change');
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
**Fingerprint properties:**
|
|
393
|
+
- `clusterCount` - Number of distinct change regions
|
|
394
|
+
- `clusterPositions` - Normalized positions (0.0-1.0)
|
|
395
|
+
- `clusterSizes` - Relative sizes (area ratio)
|
|
396
|
+
- `zoneMask` - 4x4 grid (16 zones) showing affected regions
|
|
397
|
+
- `diffMagnitude` - Bucketed size: `tiny`, `small`, `medium`, `large`, `massive`
|
|
398
|
+
- `hash` - Pre-computed coarse hash for fast grouping
|
|
399
|
+
|
|
288
400
|
## Accessibility Features
|
|
289
401
|
|
|
290
402
|
Built-in WCAG color contrast analysis and color blindness simulation to catch accessibility violations in screenshots and UI designs.
|
|
@@ -470,6 +582,20 @@ Synchronous metadata retrieval from buffer.
|
|
|
470
582
|
|
|
471
583
|
Synchronous metadata retrieval from file.
|
|
472
584
|
|
|
585
|
+
### Diff Fingerprint Functions (Sync only)
|
|
586
|
+
|
|
587
|
+
**`computeFingerprintSync(diffResult, width, height): DiffFingerprint | null`**
|
|
588
|
+
|
|
589
|
+
Compute a fingerprint from a diff result. Requires `includeClusters: true` in compare options. Returns null if no clusters.
|
|
590
|
+
|
|
591
|
+
**`fingerprintSimilaritySync(fp1, fp2): number`**
|
|
592
|
+
|
|
593
|
+
Compare two fingerprints. Returns 0.0 (different) to 1.0 (identical).
|
|
594
|
+
|
|
595
|
+
**`fingerprintHashSync(fingerprint): string`**
|
|
596
|
+
|
|
597
|
+
Get coarse hash for fast grouping. Returns 16-char hex string.
|
|
598
|
+
|
|
473
599
|
### Input Types
|
|
474
600
|
|
|
475
601
|
All functions accept:
|
|
@@ -481,22 +607,22 @@ All functions accept:
|
|
|
481
607
|
```typescript
|
|
482
608
|
interface CompareOptions {
|
|
483
609
|
// Basic options
|
|
484
|
-
|
|
485
|
-
colorThreshold?: number; // 0.0-1.0, YIQ mode threshold (default: 0.0 = RGB)
|
|
610
|
+
threshold?: number; // CIEDE2000 Delta E threshold (default: 2.0)
|
|
486
611
|
antialiasing?: boolean; // Ignore AA artifacts (default: true)
|
|
487
|
-
ignoreColors?: boolean; // Brightness only (default: false)
|
|
488
612
|
maxDiffs?: number; // Stop after N diffs (default: unlimited)
|
|
489
613
|
|
|
490
614
|
// Analysis options
|
|
491
615
|
includeDiffPixels?: boolean; // List all diff pixels (memory intensive, default: false)
|
|
492
616
|
includeClusters?: boolean; // Spatial clustering (default: false)
|
|
493
617
|
includeSSIM?: boolean; // SSIM perceptual score (slow, default: false)
|
|
618
|
+
includeGMSD?: boolean; // GMSD edge similarity (fast, default: false)
|
|
494
619
|
minClusterSize?: number; // Filter clusters smaller than this (default: 2)
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
620
|
+
clusterMerge?: boolean | { // Merge nearby clusters (default: false)
|
|
621
|
+
horizontalDistance?: number; // Max horizontal gap to merge (default: 15)
|
|
622
|
+
yBandTolerance?: number; // Vertical "same line" tolerance (default: 5)
|
|
623
|
+
maxHeightRatio?: number; // Max height ratio to merge (default: 2.0)
|
|
624
|
+
maxWidthRatio?: number; // Max width ratio to merge (default: 3.0)
|
|
625
|
+
};
|
|
500
626
|
|
|
501
627
|
// Output options
|
|
502
628
|
diffPath?: string; // Save diff image path
|
|
@@ -529,7 +655,8 @@ interface DiffResult {
|
|
|
529
655
|
diffPixelsList: DiffPixel[] | null; // Null unless includeDiffPixels enabled
|
|
530
656
|
diffClusters: DiffCluster[] | null; // Null unless includeClusters enabled
|
|
531
657
|
intensityStats: IntensityStats | null; // Null unless includeDiffPixels enabled
|
|
532
|
-
perceptualScore: number | null; // 0.0-1.0, null unless includeSSIM enabled
|
|
658
|
+
perceptualScore: number | null; // SSIM 0.0-1.0, null unless includeSSIM enabled
|
|
659
|
+
gmsdScore: number | null; // GMSD 0.0+, null unless includeGMSD enabled
|
|
533
660
|
}
|
|
534
661
|
```
|
|
535
662
|
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Basic usage example for @honeydiff/node
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as honeydiff from '../index.js';
|
|
6
|
+
|
|
7
|
+
async function main() {
|
|
8
|
+
console.log('Honeydiff Node.js Examples\n');
|
|
9
|
+
|
|
10
|
+
// Example 1: Quick async comparison
|
|
11
|
+
console.log('1. Quick comparison:');
|
|
12
|
+
let isDifferent = await honeydiff.quickCompare(
|
|
13
|
+
'../../tests/fixtures/screenshots/vizzly-baseline.png',
|
|
14
|
+
'../../tests/fixtures/screenshots/vizzly-with-diff.png'
|
|
15
|
+
);
|
|
16
|
+
console.log(` Result: ${isDifferent ? 'DIFFERENT' : 'IDENTICAL'}\n`);
|
|
17
|
+
|
|
18
|
+
// Example 2: Detailed async comparison
|
|
19
|
+
console.log('2. Detailed comparison with options:');
|
|
20
|
+
let result = await honeydiff.compare(
|
|
21
|
+
'../../tests/fixtures/screenshots/vizzly-baseline.png',
|
|
22
|
+
'../../tests/fixtures/screenshots/vizzly-with-diff.png',
|
|
23
|
+
{
|
|
24
|
+
pixelTolerance: 0,
|
|
25
|
+
antialiasing: true,
|
|
26
|
+
includeSSIM: true,
|
|
27
|
+
includeClusters: true,
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
console.log(` Diff percentage: ${result.diffPercentage.toFixed(2)}%`);
|
|
32
|
+
console.log(` Different pixels: ${result.diffPixels} / ${result.totalPixels}`);
|
|
33
|
+
console.log(` AA pixels ignored: ${result.aaPixelsIgnored}`);
|
|
34
|
+
|
|
35
|
+
if (result.perceptualScore !== null) {
|
|
36
|
+
console.log(` SSIM score: ${result.perceptualScore.toFixed(4)}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (result.boundingBox) {
|
|
40
|
+
console.log(
|
|
41
|
+
` Bounding box: (${result.boundingBox.x}, ${result.boundingBox.y}) ${result.boundingBox.width}x${result.boundingBox.height}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (result.diffClusters && result.diffClusters.length > 0) {
|
|
46
|
+
console.log(` Found ${result.diffClusters.length} diff clusters`);
|
|
47
|
+
result.diffClusters.slice(0, 3).forEach((cluster, i) => {
|
|
48
|
+
console.log(
|
|
49
|
+
` Cluster ${i + 1}: ${cluster.pixelCount} pixels at (${cluster.centerOfMass[0].toFixed(0)}, ${cluster.centerOfMass[1].toFixed(0)})`
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (result.intensityStats) {
|
|
55
|
+
console.log(` Intensity stats:`);
|
|
56
|
+
console.log(` Range: ${result.intensityStats.min}-${result.intensityStats.max}`);
|
|
57
|
+
console.log(
|
|
58
|
+
` Mean: ${result.intensityStats.mean.toFixed(1)}, Median: ${result.intensityStats.median}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log();
|
|
63
|
+
|
|
64
|
+
// Example 3: Synchronous comparison (for when you need blocking behavior)
|
|
65
|
+
console.log('3. Synchronous comparison:');
|
|
66
|
+
let syncResult = honeydiff.compareSync(
|
|
67
|
+
'../../tests/fixtures/screenshots/vizzly-baseline.png',
|
|
68
|
+
'../../tests/fixtures/screenshots/vizzly-with-diff.png',
|
|
69
|
+
{ pixelTolerance: 10 }
|
|
70
|
+
);
|
|
71
|
+
console.log(` Diff percentage: ${syncResult.diffPercentage.toFixed(2)}%`);
|
|
72
|
+
console.log();
|
|
73
|
+
|
|
74
|
+
// Example 4: Get image dimensions
|
|
75
|
+
console.log('4. Get image dimensions:');
|
|
76
|
+
let dimensions = await honeydiff.getDimensions(
|
|
77
|
+
'../../tests/fixtures/screenshots/vizzly-baseline.png'
|
|
78
|
+
);
|
|
79
|
+
console.log(` Dimensions: ${dimensions.width}x${dimensions.height} pixels`);
|
|
80
|
+
console.log();
|
|
81
|
+
|
|
82
|
+
// Example 5: Get dimensions synchronously
|
|
83
|
+
console.log('5. Get dimensions (sync):');
|
|
84
|
+
let syncDimensions = honeydiff.getDimensionsSync(
|
|
85
|
+
'../../tests/fixtures/screenshots/vizzly-with-diff.png'
|
|
86
|
+
);
|
|
87
|
+
console.log(` Dimensions: ${syncDimensions.width}x${syncDimensions.height} pixels`);
|
|
88
|
+
console.log();
|
|
89
|
+
|
|
90
|
+
// Example 6: Generate diff artifacts
|
|
91
|
+
console.log('6. Generate diff artifacts:');
|
|
92
|
+
let artifactResult = await honeydiff.compare(
|
|
93
|
+
'../../tests/fixtures/screenshots/vizzly-baseline.png',
|
|
94
|
+
'../../tests/fixtures/screenshots/vizzly-with-diff.png',
|
|
95
|
+
{
|
|
96
|
+
diffPath: '/tmp/honeydiff-diff.png',
|
|
97
|
+
maskPath: '/tmp/honeydiff-mask.png',
|
|
98
|
+
overlayPath: '/tmp/honeydiff-overlay.png',
|
|
99
|
+
overwrite: true,
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
console.log(` Diff percentage: ${artifactResult.diffPercentage.toFixed(2)}%`);
|
|
103
|
+
console.log(` Artifacts saved to /tmp/honeydiff-*.png`);
|
|
104
|
+
console.log();
|
|
105
|
+
|
|
106
|
+
// Example 7: Accessibility metadata tests
|
|
107
|
+
console.log('7. Accessibility metadata (RGB + luminance + WCAG):');
|
|
108
|
+
let accessibilityResult = await honeydiff.compare(
|
|
109
|
+
'../../tests/fixtures/screenshots/vizzly-baseline.png',
|
|
110
|
+
'../../tests/fixtures/screenshots/vizzly-with-diff.png',
|
|
111
|
+
{
|
|
112
|
+
includeClusters: true,
|
|
113
|
+
includeAccessibilityData: true,
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Validate accessibility data is present
|
|
118
|
+
if (!accessibilityResult.diffClusters || accessibilityResult.diffClusters.length === 0) {
|
|
119
|
+
throw new Error('Test failed: Expected diff clusters to be present');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let firstCluster = accessibilityResult.diffClusters[0];
|
|
123
|
+
if (!firstCluster.accessibilityMetadata) {
|
|
124
|
+
throw new Error('Test failed: Expected accessibilityMetadata to be present');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let meta = firstCluster.accessibilityMetadata;
|
|
128
|
+
|
|
129
|
+
// Validate RGB channel differences
|
|
130
|
+
if (typeof meta.avgRDiff !== 'number' || meta.avgRDiff < 0) {
|
|
131
|
+
throw new Error('Test failed: avgRDiff should be a non-negative number');
|
|
132
|
+
}
|
|
133
|
+
if (typeof meta.avgGDiff !== 'number' || meta.avgGDiff < 0) {
|
|
134
|
+
throw new Error('Test failed: avgGDiff should be a non-negative number');
|
|
135
|
+
}
|
|
136
|
+
if (typeof meta.avgBDiff !== 'number' || meta.avgBDiff < 0) {
|
|
137
|
+
throw new Error('Test failed: avgBDiff should be a non-negative number');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Validate dominant channel
|
|
141
|
+
if (!['R', 'G', 'B'].includes(meta.dominantChannel)) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Test failed: dominantChannel should be R, G, or B, got ${meta.dominantChannel}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Validate luminance values (0.0-1.0 range)
|
|
148
|
+
if (meta.avgBaselineLuminance < 0 || meta.avgBaselineLuminance > 1) {
|
|
149
|
+
throw new Error('Test failed: avgBaselineLuminance should be between 0 and 1');
|
|
150
|
+
}
|
|
151
|
+
if (meta.avgCurrentLuminance < 0 || meta.avgCurrentLuminance > 1) {
|
|
152
|
+
throw new Error('Test failed: avgCurrentLuminance should be between 0 and 1');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Validate contrast ratios (1.0-21.0 range)
|
|
156
|
+
if (meta.contrastRatio < 1 || meta.contrastRatio > 21) {
|
|
157
|
+
throw new Error('Test failed: contrastRatio should be between 1 and 21');
|
|
158
|
+
}
|
|
159
|
+
if (meta.minContrastRatio < 1 || meta.minContrastRatio > 21) {
|
|
160
|
+
throw new Error('Test failed: minContrastRatio should be between 1 and 21');
|
|
161
|
+
}
|
|
162
|
+
if (meta.maxContrastRatio < 1 || meta.maxContrastRatio > 21) {
|
|
163
|
+
throw new Error('Test failed: maxContrastRatio should be between 1 and 21');
|
|
164
|
+
}
|
|
165
|
+
if (meta.minContrastRatio > meta.maxContrastRatio) {
|
|
166
|
+
throw new Error('Test failed: minContrastRatio should be <= maxContrastRatio');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Validate colorBlindnessImpact is null when not requested
|
|
170
|
+
if (meta.colorBlindnessImpact !== null) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
'Test failed: colorBlindnessImpact should be null when checkColorBlindness is not enabled'
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(
|
|
177
|
+
` RGB diffs: R=${meta.avgRDiff.toFixed(1)}, G=${meta.avgGDiff.toFixed(1)}, B=${meta.avgBDiff.toFixed(1)}`
|
|
178
|
+
);
|
|
179
|
+
console.log(` Dominant channel: ${meta.dominantChannel}`);
|
|
180
|
+
console.log(
|
|
181
|
+
` Luminance: baseline=${meta.avgBaselineLuminance.toFixed(3)}, current=${meta.avgCurrentLuminance.toFixed(3)}`
|
|
182
|
+
);
|
|
183
|
+
console.log(
|
|
184
|
+
` WCAG contrast: ${meta.contrastRatio.toFixed(2)}:1 (range: ${meta.minContrastRatio.toFixed(2)}-${meta.maxContrastRatio.toFixed(2)})`
|
|
185
|
+
);
|
|
186
|
+
console.log(' All accessibility metadata tests passed!');
|
|
187
|
+
console.log();
|
|
188
|
+
|
|
189
|
+
// Example 8: Color blindness checking tests
|
|
190
|
+
console.log('8. Color blindness checking:');
|
|
191
|
+
let cbResult = await honeydiff.compare(
|
|
192
|
+
'../../tests/fixtures/screenshots/vizzly-baseline.png',
|
|
193
|
+
'../../tests/fixtures/screenshots/vizzly-with-diff.png',
|
|
194
|
+
{
|
|
195
|
+
includeClusters: true,
|
|
196
|
+
checkColorBlindness: true,
|
|
197
|
+
colorBlindnessThreshold: 30.0,
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Validate clusters and accessibility data
|
|
202
|
+
if (!cbResult.diffClusters || cbResult.diffClusters.length === 0) {
|
|
203
|
+
throw new Error('Test failed: Expected diff clusters to be present');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let cbCluster = cbResult.diffClusters[0];
|
|
207
|
+
if (!cbCluster.accessibilityMetadata) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
'Test failed: Expected accessibilityMetadata to be present when checkColorBlindness is enabled'
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let cbMeta = cbCluster.accessibilityMetadata;
|
|
214
|
+
|
|
215
|
+
// Validate colorBlindnessImpact is present
|
|
216
|
+
if (!cbMeta.colorBlindnessImpact) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
'Test failed: Expected colorBlindnessImpact to be present when checkColorBlindness is enabled'
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let impact = cbMeta.colorBlindnessImpact;
|
|
223
|
+
|
|
224
|
+
// Validate boolean flags
|
|
225
|
+
if (typeof impact.deuteranopiaVisible !== 'boolean') {
|
|
226
|
+
throw new Error('Test failed: deuteranopiaVisible should be a boolean');
|
|
227
|
+
}
|
|
228
|
+
if (typeof impact.protanopiaVisible !== 'boolean') {
|
|
229
|
+
throw new Error('Test failed: protanopiaVisible should be a boolean');
|
|
230
|
+
}
|
|
231
|
+
if (typeof impact.tritanopiaVisible !== 'boolean') {
|
|
232
|
+
throw new Error('Test failed: tritanopiaVisible should be a boolean');
|
|
233
|
+
}
|
|
234
|
+
if (typeof impact.achromatopsiaVisible !== 'boolean') {
|
|
235
|
+
throw new Error('Test failed: achromatopsiaVisible should be a boolean');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Validate visibility score (0.0-1.0 range)
|
|
239
|
+
if (impact.visibilityScore < 0 || impact.visibilityScore > 1) {
|
|
240
|
+
throw new Error('Test failed: visibilityScore should be between 0 and 1');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
console.log(` Visibility score: ${(impact.visibilityScore * 100).toFixed(0)}%`);
|
|
244
|
+
console.log(` Deuteranopia visible: ${impact.deuteranopiaVisible}`);
|
|
245
|
+
console.log(` Protanopia visible: ${impact.protanopiaVisible}`);
|
|
246
|
+
console.log(` Tritanopia visible: ${impact.tritanopiaVisible}`);
|
|
247
|
+
console.log(` Achromatopsia visible: ${impact.achromatopsiaVisible}`);
|
|
248
|
+
console.log(' All color blindness tests passed!');
|
|
249
|
+
console.log();
|
|
250
|
+
|
|
251
|
+
// Example 9: Test checkColorBlindness automatically enables includeAccessibilityData
|
|
252
|
+
console.log('9. Auto-enable test (checkColorBlindness enables includeAccessibilityData):');
|
|
253
|
+
let autoEnableResult = await honeydiff.compare(
|
|
254
|
+
'../../tests/fixtures/screenshots/vizzly-baseline.png',
|
|
255
|
+
'../../tests/fixtures/screenshots/vizzly-with-diff.png',
|
|
256
|
+
{
|
|
257
|
+
includeClusters: true,
|
|
258
|
+
checkColorBlindness: true,
|
|
259
|
+
// Note: NOT explicitly setting includeAccessibilityData
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
if (!autoEnableResult.diffClusters || autoEnableResult.diffClusters.length === 0) {
|
|
264
|
+
throw new Error('Test failed: Expected diff clusters to be present');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let autoCluster = autoEnableResult.diffClusters[0];
|
|
268
|
+
if (!autoCluster.accessibilityMetadata) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
'Test failed: checkColorBlindness should automatically enable accessibilityMetadata'
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Should have both accessibility data and color blindness impact
|
|
275
|
+
if (!autoCluster.accessibilityMetadata.colorBlindnessImpact) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
'Test failed: Expected colorBlindnessImpact when checkColorBlindness is enabled'
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
console.log(' Accessibility metadata auto-enabled: Yes');
|
|
282
|
+
console.log(' Color blindness data present: Yes');
|
|
283
|
+
console.log(' Auto-enable test passed!');
|
|
284
|
+
console.log();
|
|
285
|
+
|
|
286
|
+
// Example 10: Test accessibility data is null when not requested
|
|
287
|
+
console.log('10. Null handling test (accessibility data absent when not requested):');
|
|
288
|
+
let noAccessibilityResult = await honeydiff.compare(
|
|
289
|
+
'../../tests/fixtures/screenshots/vizzly-baseline.png',
|
|
290
|
+
'../../tests/fixtures/screenshots/vizzly-with-diff.png',
|
|
291
|
+
{
|
|
292
|
+
includeClusters: true,
|
|
293
|
+
// Note: NOT setting includeAccessibilityData or checkColorBlindness
|
|
294
|
+
}
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
if (!noAccessibilityResult.diffClusters || noAccessibilityResult.diffClusters.length === 0) {
|
|
298
|
+
throw new Error('Test failed: Expected diff clusters to be present');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let noAccessCluster = noAccessibilityResult.diffClusters[0];
|
|
302
|
+
if (noAccessCluster.accessibilityMetadata !== null) {
|
|
303
|
+
throw new Error('Test failed: accessibilityMetadata should be null when not requested');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
console.log(' Accessibility metadata: null (as expected)');
|
|
307
|
+
console.log(' Null handling test passed!');
|
|
308
|
+
console.log();
|
|
309
|
+
|
|
310
|
+
console.log('=== All accessibility binding tests passed! ===\n');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
main().catch((err) => {
|
|
314
|
+
console.error('Error:', err);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Using Buffers instead of file paths
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import * as honeydiff from '../index.js';
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
console.log('Buffer Example\n');
|
|
10
|
+
|
|
11
|
+
// Load images as buffers
|
|
12
|
+
let img1Buffer = fs.readFileSync('../../tests/fixtures/screenshots/vizzly-baseline.png');
|
|
13
|
+
let img2Buffer = fs.readFileSync('../../tests/fixtures/screenshots/vizzly-with-diff.png');
|
|
14
|
+
|
|
15
|
+
console.log(`Loaded buffers: ${img1Buffer.length} and ${img2Buffer.length} bytes\n`);
|
|
16
|
+
|
|
17
|
+
// Compare using buffers
|
|
18
|
+
let result = await honeydiff.compare(img1Buffer, img2Buffer, {
|
|
19
|
+
pixelTolerance: 0,
|
|
20
|
+
includeSSIM: true,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
console.log('Comparison result:');
|
|
24
|
+
console.log(` Different: ${result.isDifferent}`);
|
|
25
|
+
console.log(` Diff percentage: ${result.diffPercentage.toFixed(2)}%`);
|
|
26
|
+
console.log(` SSIM: ${result.perceptualScore?.toFixed(4) || 'N/A'}`);
|
|
27
|
+
|
|
28
|
+
// Mixed: buffer + file path
|
|
29
|
+
console.log('\nMixed comparison (buffer + file path):');
|
|
30
|
+
let mixedResult = await honeydiff.compare(
|
|
31
|
+
img1Buffer,
|
|
32
|
+
'../../tests/fixtures/screenshots/vizzly-with-diff.png'
|
|
33
|
+
);
|
|
34
|
+
console.log(` Diff percentage: ${mixedResult.diffPercentage.toFixed(2)}%`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
main().catch((err) => {
|
|
38
|
+
console.error('Error:', err);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
});
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color Vision Deficiency (CVD) Simulation Example
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates how to simulate color blindness for images and run
|
|
5
|
+
* accessibility analysis to ensure your UI works for all users.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import {
|
|
12
|
+
analyzeWcagAllCvd,
|
|
13
|
+
analyzeWcagForCvd,
|
|
14
|
+
getColorBlindnessTypes,
|
|
15
|
+
saveAllColorBlindnessSimulations,
|
|
16
|
+
saveColorBlindnessSimulation,
|
|
17
|
+
simulateColorBlindness,
|
|
18
|
+
} from '../index.js';
|
|
19
|
+
|
|
20
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
console.log('=== Color Vision Deficiency (CVD) Simulation Demo ===\n');
|
|
24
|
+
|
|
25
|
+
let testImage = path.join(__dirname, '../../../tests/fixtures/screenshots/vizzly-baseline.png');
|
|
26
|
+
|
|
27
|
+
// Example 1: Get information about CVD types
|
|
28
|
+
console.log('1. Available color blindness types:\n');
|
|
29
|
+
let types = getColorBlindnessTypes();
|
|
30
|
+
for (let cvd of types) {
|
|
31
|
+
console.log(` ${cvd.name} (${cvd.type})`);
|
|
32
|
+
console.log(` ${cvd.description}`);
|
|
33
|
+
console.log(` Affects: ${cvd.prevalence}\n`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Example 2: Simulate a single CVD type and get buffer
|
|
37
|
+
console.log('2. Simulating deuteranopia (red-green color blindness)...');
|
|
38
|
+
try {
|
|
39
|
+
let simulatedBuffer = await simulateColorBlindness(testImage, 'deuteranopia');
|
|
40
|
+
console.log(` Generated ${simulatedBuffer.length} bytes of simulated image data`);
|
|
41
|
+
|
|
42
|
+
// Save the buffer if you want
|
|
43
|
+
let outputPath = path.join(__dirname, '../cvd-deuteranopia.png');
|
|
44
|
+
fs.writeFileSync(outputPath, simulatedBuffer);
|
|
45
|
+
console.log(` Saved to: ${outputPath}\n`);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(' Error:', error.message, '\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Example 3: Save CVD simulation directly to file
|
|
51
|
+
console.log('3. Saving protanopia simulation to file...');
|
|
52
|
+
try {
|
|
53
|
+
let outputPath = path.join(__dirname, '../cvd-protanopia.png');
|
|
54
|
+
await saveColorBlindnessSimulation(testImage, 'protanopia', outputPath);
|
|
55
|
+
console.log(` Saved to: ${outputPath}\n`);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(' Error:', error.message, '\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Example 4: Generate all CVD simulations at once
|
|
61
|
+
console.log('4. Generating all CVD simulations...');
|
|
62
|
+
try {
|
|
63
|
+
let outputPrefix = path.join(__dirname, '../cvd-all');
|
|
64
|
+
await saveAllColorBlindnessSimulations(testImage, outputPrefix, 'png');
|
|
65
|
+
console.log(' Created files:');
|
|
66
|
+
console.log(` - ${outputPrefix}_protanopia.png`);
|
|
67
|
+
console.log(` - ${outputPrefix}_deuteranopia.png`);
|
|
68
|
+
console.log(` - ${outputPrefix}_tritanopia.png`);
|
|
69
|
+
console.log(` - ${outputPrefix}_achromatopsia.png\n`);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error(' Error:', error.message, '\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Example 5: WCAG analysis for a specific CVD type
|
|
75
|
+
console.log('5. Analyzing WCAG contrast for deuteranopia users...');
|
|
76
|
+
try {
|
|
77
|
+
let analysis = await analyzeWcagForCvd(testImage, 'deuteranopia', {
|
|
78
|
+
checkAA: true,
|
|
79
|
+
checkAAA: false,
|
|
80
|
+
edgeThreshold: 60,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
console.log(` Total edges analyzed: ${analysis.totalEdges}`);
|
|
84
|
+
console.log(` AA normal text pass rate: ${analysis.aaNormalPassPercentage.toFixed(1)}%`);
|
|
85
|
+
console.log(` Violations found: ${analysis.violations.length}`);
|
|
86
|
+
|
|
87
|
+
if (analysis.violations.length > 0) {
|
|
88
|
+
console.log('\n Top 3 violations:');
|
|
89
|
+
for (let i = 0; i < Math.min(3, analysis.violations.length); i++) {
|
|
90
|
+
let v = analysis.violations[i];
|
|
91
|
+
console.log(` ${i + 1}. Location: (${v.boundingBox.x}, ${v.boundingBox.y})`);
|
|
92
|
+
console.log(` Contrast: ${v.contrastRatio.toFixed(2)}:1`);
|
|
93
|
+
console.log(` Pixels affected: ${v.pixelCount}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
console.log();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(' Error:', error.message, '\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Example 6: Comprehensive CVD accessibility report
|
|
102
|
+
console.log('6. Running comprehensive CVD accessibility analysis...');
|
|
103
|
+
try {
|
|
104
|
+
let report = await analyzeWcagAllCvd(testImage, {
|
|
105
|
+
checkAA: true,
|
|
106
|
+
checkAAA: true,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
console.log('\n Accessibility Report:');
|
|
110
|
+
console.log(' ─────────────────────────────────────────────');
|
|
111
|
+
console.log(
|
|
112
|
+
` Normal Vision: ${report.normalVision.violations.length} violations ` +
|
|
113
|
+
`(${report.normalVision.aaNormalPassPercentage.toFixed(1)}% pass)`
|
|
114
|
+
);
|
|
115
|
+
console.log(
|
|
116
|
+
` Protanopia: ${report.protanopia.violations.length} violations ` +
|
|
117
|
+
`(${report.protanopia.aaNormalPassPercentage.toFixed(1)}% pass)`
|
|
118
|
+
);
|
|
119
|
+
console.log(
|
|
120
|
+
` Deuteranopia: ${report.deuteranopia.violations.length} violations ` +
|
|
121
|
+
`(${report.deuteranopia.aaNormalPassPercentage.toFixed(1)}% pass)`
|
|
122
|
+
);
|
|
123
|
+
console.log(
|
|
124
|
+
` Tritanopia: ${report.tritanopia.violations.length} violations ` +
|
|
125
|
+
`(${report.tritanopia.aaNormalPassPercentage.toFixed(1)}% pass)`
|
|
126
|
+
);
|
|
127
|
+
console.log(' ─────────────────────────────────────────────');
|
|
128
|
+
console.log(` Total violations: ${report.totalViolations}`);
|
|
129
|
+
console.log(` CVD-only violations: ~${report.cvdOnlyViolationCount}`);
|
|
130
|
+
console.log(` Has any violations: ${report.hasAnyViolations}`);
|
|
131
|
+
console.log();
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error(' Error:', error.message, '\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Example 7: Using short aliases
|
|
137
|
+
console.log('7. Using short CVD type aliases...');
|
|
138
|
+
try {
|
|
139
|
+
// These are equivalent:
|
|
140
|
+
// 'protanopia' === 'protan'
|
|
141
|
+
// 'deuteranopia' === 'deutan'
|
|
142
|
+
// 'tritanopia' === 'tritan'
|
|
143
|
+
// 'achromatopsia' === 'achroma' === 'monochromacy'
|
|
144
|
+
|
|
145
|
+
let outputPath = path.join(__dirname, '../cvd-tritan.png');
|
|
146
|
+
await saveColorBlindnessSimulation(testImage, 'tritan', outputPath);
|
|
147
|
+
console.log(` Saved tritanopia simulation using "tritan" alias`);
|
|
148
|
+
console.log(` Output: ${outputPath}\n`);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error(' Error:', error.message, '\n');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Example 8: Test integration example
|
|
154
|
+
console.log('8. Example test integration (pseudocode):');
|
|
155
|
+
console.log(`
|
|
156
|
+
// In your visual regression tests:
|
|
157
|
+
test('UI colors work for colorblind users', async () => {
|
|
158
|
+
let screenshot = await page.screenshot();
|
|
159
|
+
|
|
160
|
+
// Check accessibility for all CVD types
|
|
161
|
+
let report = await analyzeWcagAllCvd(screenshot, { checkAA: true });
|
|
162
|
+
|
|
163
|
+
// Assert no violations
|
|
164
|
+
expect(report.hasAnyViolations).toBe(false);
|
|
165
|
+
|
|
166
|
+
// Or check specific CVD types
|
|
167
|
+
expect(report.deuteranopia.violations.length).toBe(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('chart is readable for protanopia users', async () => {
|
|
171
|
+
let chart = await page.screenshot({ element: '[data-testid="chart"]' });
|
|
172
|
+
|
|
173
|
+
// Simulate and compare
|
|
174
|
+
let simulated = await simulateColorBlindness(chart, 'protanopia');
|
|
175
|
+
|
|
176
|
+
// Verify distinguishability
|
|
177
|
+
let diff = await compare(chart, simulated);
|
|
178
|
+
|
|
179
|
+
// Some difference is expected, but important info should remain visible
|
|
180
|
+
let analysis = await analyzeWcagForCvd(chart, 'protanopia');
|
|
181
|
+
expect(analysis.aaNormalPassPercentage).toBeGreaterThan(90);
|
|
182
|
+
});
|
|
183
|
+
`);
|
|
184
|
+
|
|
185
|
+
// Cleanup generated files
|
|
186
|
+
console.log('9. Cleaning up generated files...');
|
|
187
|
+
let filesToClean = [
|
|
188
|
+
path.join(__dirname, '../cvd-deuteranopia.png'),
|
|
189
|
+
path.join(__dirname, '../cvd-protanopia.png'),
|
|
190
|
+
path.join(__dirname, '../cvd-tritan.png'),
|
|
191
|
+
path.join(__dirname, '../cvd-all_protanopia.png'),
|
|
192
|
+
path.join(__dirname, '../cvd-all_deuteranopia.png'),
|
|
193
|
+
path.join(__dirname, '../cvd-all_tritanopia.png'),
|
|
194
|
+
path.join(__dirname, '../cvd-all_achromatopsia.png'),
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
for (let file of filesToClean) {
|
|
198
|
+
try {
|
|
199
|
+
if (fs.existsSync(file)) {
|
|
200
|
+
fs.unlinkSync(file);
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
// ignore cleanup errors
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
console.log(' Cleaned up generated files.\n');
|
|
207
|
+
|
|
208
|
+
console.log('=== Demo Complete ===');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript example with full type safety
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { type CompareOptions, compare, type DiffResult, quickCompare } from '../index';
|
|
6
|
+
|
|
7
|
+
async function main(): Promise<void> {
|
|
8
|
+
console.log('TypeScript Example\n');
|
|
9
|
+
|
|
10
|
+
// Quick comparison
|
|
11
|
+
let isDifferent: boolean = await quickCompare(
|
|
12
|
+
'../../tests/fixtures/screenshots/vizzly-baseline.png',
|
|
13
|
+
'../../tests/fixtures/screenshots/vizzly-with-diff.png'
|
|
14
|
+
);
|
|
15
|
+
console.log(`Quick result: ${isDifferent ? 'DIFFERENT' : 'IDENTICAL'}\n`);
|
|
16
|
+
|
|
17
|
+
// Detailed comparison with full typing
|
|
18
|
+
let options: CompareOptions = {
|
|
19
|
+
pixelTolerance: 5,
|
|
20
|
+
antialiasing: true,
|
|
21
|
+
includeSSIM: true,
|
|
22
|
+
includeClusters: true,
|
|
23
|
+
includeDiffPixels: false,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let result: DiffResult = await compare(
|
|
27
|
+
'../../tests/fixtures/screenshots/vizzly-baseline.png',
|
|
28
|
+
'../../tests/fixtures/screenshots/vizzly-with-diff.png',
|
|
29
|
+
options
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
console.log('Detailed results:');
|
|
33
|
+
console.log(` Diff: ${result.diffPercentage.toFixed(2)}%`);
|
|
34
|
+
console.log(` Different pixels: ${result.diffPixels} / ${result.totalPixels}`);
|
|
35
|
+
|
|
36
|
+
// Type-safe access to optional fields
|
|
37
|
+
if (result.perceptualScore !== null) {
|
|
38
|
+
let ssim: number = result.perceptualScore;
|
|
39
|
+
console.log(` SSIM: ${ssim.toFixed(4)}`);
|
|
40
|
+
|
|
41
|
+
if (ssim > 0.95) {
|
|
42
|
+
console.log(' → Perceptually very similar');
|
|
43
|
+
} else if (ssim > 0.8) {
|
|
44
|
+
console.log(' → Somewhat similar');
|
|
45
|
+
} else {
|
|
46
|
+
console.log(' → Perceptually different');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (result.boundingBox) {
|
|
51
|
+
console.log(
|
|
52
|
+
` Changes in region: (${result.boundingBox.x}, ${result.boundingBox.y}) ${result.boundingBox.width}x${result.boundingBox.height}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (result.diffClusters && result.diffClusters.length > 0) {
|
|
57
|
+
console.log(`\nFound ${result.diffClusters.length} separate diff regions:`);
|
|
58
|
+
result.diffClusters.slice(0, 5).forEach((cluster, i) => {
|
|
59
|
+
let [cx, cy] = cluster.centerOfMass;
|
|
60
|
+
console.log(
|
|
61
|
+
` ${i + 1}. ${cluster.pixelCount} pixels at (${cx.toFixed(0)}, ${cy.toFixed(0)})`
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (result.intensityStats) {
|
|
67
|
+
console.log('\nIntensity statistics:');
|
|
68
|
+
console.log(` Range: ${result.intensityStats.min} - ${result.intensityStats.max}`);
|
|
69
|
+
console.log(` Mean: ${result.intensityStats.mean.toFixed(1)}`);
|
|
70
|
+
console.log(` Median: ${result.intensityStats.median}`);
|
|
71
|
+
console.log(` Std Dev: ${result.intensityStats.stdDev.toFixed(1)}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
main().catch((err: Error) => {
|
|
76
|
+
console.error('Error:', err.message);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
});
|
package/examples/wcag.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WCAG Accessibility Testing Example
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates how to analyze images for WCAG color contrast violations
|
|
5
|
+
* and generate visual overlays showing problematic regions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { analyzeWcagContrast, saveWcagOverlay } from '../index.js';
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
console.log('=== WCAG Accessibility Testing Demo ===\n');
|
|
16
|
+
|
|
17
|
+
// Example 1: Basic WCAG analysis
|
|
18
|
+
console.log('1. Running basic WCAG AA analysis...');
|
|
19
|
+
try {
|
|
20
|
+
let analysis = await analyzeWcagContrast(
|
|
21
|
+
path.join(__dirname, '../../../tests/fixtures/screenshots/vizzly-baseline.png'),
|
|
22
|
+
{
|
|
23
|
+
edgeThreshold: 60,
|
|
24
|
+
minRegionSize: 50,
|
|
25
|
+
checkAA: true,
|
|
26
|
+
checkAAA: false,
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
console.log(` Total edges detected: ${analysis.totalEdges}`);
|
|
31
|
+
console.log(` AA normal text pass rate: ${analysis.aaNormalPassPercentage.toFixed(1)}%`);
|
|
32
|
+
console.log(` AA large text pass rate: ${analysis.aaLargePassPercentage.toFixed(1)}%`);
|
|
33
|
+
console.log(` Violations found: ${analysis.violations.length}\n`);
|
|
34
|
+
|
|
35
|
+
if (analysis.violations.length > 0) {
|
|
36
|
+
console.log(' Top 5 violations by size:');
|
|
37
|
+
for (let i = 0; i < Math.min(5, analysis.violations.length); i++) {
|
|
38
|
+
let violation = analysis.violations[i];
|
|
39
|
+
console.log(
|
|
40
|
+
` ${i + 1}. Region at (${violation.boundingBox.x}, ${violation.boundingBox.y})`
|
|
41
|
+
);
|
|
42
|
+
console.log(` Contrast ratio: ${violation.contrastRatio.toFixed(2)}:1`);
|
|
43
|
+
console.log(` Pixels affected: ${violation.pixelCount}`);
|
|
44
|
+
console.log(
|
|
45
|
+
` Fails AA normal: ${violation.failsAaNormal}, Fails AA large: ${violation.failsAaLarge}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
console.log();
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(' Error:', error.message);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Example 2: WCAG AAA analysis (stricter)
|
|
55
|
+
console.log('2. Running strict WCAG AAA analysis...');
|
|
56
|
+
try {
|
|
57
|
+
let analysis = await analyzeWcagContrast(
|
|
58
|
+
path.join(__dirname, '../../../tests/fixtures/screenshots/vizzly-baseline.png'),
|
|
59
|
+
{
|
|
60
|
+
edgeThreshold: 60,
|
|
61
|
+
minRegionSize: 50,
|
|
62
|
+
checkAA: true,
|
|
63
|
+
checkAAA: true,
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
console.log(` Total edges detected: ${analysis.totalEdges}`);
|
|
68
|
+
console.log(` AAA normal text pass rate: ${analysis.aaaNormalPassPercentage.toFixed(1)}%`);
|
|
69
|
+
console.log(` AAA large text pass rate: ${analysis.aaaLargePassPercentage.toFixed(1)}%`);
|
|
70
|
+
console.log(` AAA violations found: ${analysis.violations.length}\n`);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(' Error:', error.message);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Example 3: Generate visual overlay
|
|
76
|
+
console.log('3. Generating visual overlay...');
|
|
77
|
+
try {
|
|
78
|
+
let analysis = await analyzeWcagContrast(
|
|
79
|
+
path.join(__dirname, '../../../tests/fixtures/screenshots/vizzly-baseline.png'),
|
|
80
|
+
{
|
|
81
|
+
checkAA: true,
|
|
82
|
+
checkAAA: false,
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
let outputPath = path.join(__dirname, '../wcag-violations.png');
|
|
87
|
+
|
|
88
|
+
await saveWcagOverlay(
|
|
89
|
+
path.join(__dirname, '../../../tests/fixtures/screenshots/vizzly-baseline.png'),
|
|
90
|
+
analysis,
|
|
91
|
+
outputPath,
|
|
92
|
+
{
|
|
93
|
+
highlightColor: [255, 0, 0, 180], // Semi-transparent red
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
console.log(` ✓ Overlay saved to: ${outputPath}\n`);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error(' Error:', error.message);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Example 4: Synchronous API (for simple scripts)
|
|
103
|
+
console.log('4. Using synchronous API...');
|
|
104
|
+
try {
|
|
105
|
+
const { analyzeWcagContrastSync } = require('../index.js');
|
|
106
|
+
|
|
107
|
+
let analysis = analyzeWcagContrastSync(
|
|
108
|
+
path.join(__dirname, '../../../tests/fixtures/screenshots/vizzly-baseline.png'),
|
|
109
|
+
{
|
|
110
|
+
edgeThreshold: 80, // More lenient threshold
|
|
111
|
+
minRegionSize: 100, // Larger regions only
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
console.log(` Total edges: ${analysis.totalEdges}`);
|
|
116
|
+
console.log(` Violations: ${analysis.violations.length}`);
|
|
117
|
+
console.log(` Pass rate: ${analysis.aaNormalPassPercentage.toFixed(1)}%\n`);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(' Error:', error.message);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Example 5: Detailed violation analysis
|
|
123
|
+
console.log('5. Analyzing violation details...');
|
|
124
|
+
try {
|
|
125
|
+
let analysis = await analyzeWcagContrast(
|
|
126
|
+
path.join(__dirname, '../../../tests/fixtures/screenshots/vizzly-baseline.png')
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (analysis.violations.length > 0) {
|
|
130
|
+
let violation = analysis.violations[0];
|
|
131
|
+
|
|
132
|
+
console.log(' First violation details:');
|
|
133
|
+
console.log(` - Location: (${violation.boundingBox.x}, ${violation.boundingBox.y})`);
|
|
134
|
+
console.log(` - Size: ${violation.boundingBox.width}x${violation.boundingBox.height}`);
|
|
135
|
+
console.log(
|
|
136
|
+
` - Center: (${violation.centerOfMass[0].toFixed(1)}, ${violation.centerOfMass[1].toFixed(1)})`
|
|
137
|
+
);
|
|
138
|
+
console.log(` - Foreground color: rgba(${violation.foregroundColor.join(', ')})`);
|
|
139
|
+
console.log(` - Background color: rgba(${violation.backgroundColor.join(', ')})`);
|
|
140
|
+
console.log(` - Foreground luminance: ${violation.foregroundLuminance.toFixed(3)}`);
|
|
141
|
+
console.log(` - Background luminance: ${violation.backgroundLuminance.toFixed(3)}`);
|
|
142
|
+
console.log(
|
|
143
|
+
` - Contrast ratio: ${violation.contrastRatio.toFixed(2)}:1 (${violation.minContrastRatio.toFixed(2)} - ${violation.maxContrastRatio.toFixed(2)})`
|
|
144
|
+
);
|
|
145
|
+
console.log(` - Pixel count: ${violation.pixelCount}`);
|
|
146
|
+
console.log(' - WCAG Status:');
|
|
147
|
+
console.log(` AA Normal (4.5:1): ${violation.failsAaNormal ? '❌ FAIL' : '✓ PASS'}`);
|
|
148
|
+
console.log(` AA Large (3.0:1): ${violation.failsAaLarge ? '❌ FAIL' : '✓ PASS'}`);
|
|
149
|
+
console.log(` AAA Normal (7.0:1): ${violation.failsAaaNormal ? '❌ FAIL' : '✓ PASS'}`);
|
|
150
|
+
console.log(` AAA Large (4.5:1): ${violation.failsAaaLarge ? '❌ FAIL' : '✓ PASS'}`);
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error(' Error:', error.message);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log('\n=== Demo Complete ===');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
main().catch(console.error);
|
package/index.d.ts
CHANGED
|
@@ -91,12 +91,67 @@ export interface DiffResult {
|
|
|
91
91
|
intensityStats: IntensityStats | null;
|
|
92
92
|
/** SSIM (Structural Similarity Index) perceptual score 0.0-1.0 (null unless includeSSIM is enabled) */
|
|
93
93
|
perceptualScore: number | null;
|
|
94
|
+
/**
|
|
95
|
+
* GMSD (Gradient Magnitude Similarity Deviation) score (null unless includeGMSD is enabled)
|
|
96
|
+
* Lower values (closer to 0.0) indicate more similar images
|
|
97
|
+
* Typical range: 0.0 to ~0.3 for natural images
|
|
98
|
+
*/
|
|
99
|
+
gmsdScore: number | null;
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
// ============================================================================
|
|
97
103
|
// Options
|
|
98
104
|
// ============================================================================
|
|
99
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Options for merging nearby clusters
|
|
108
|
+
*
|
|
109
|
+
* These heuristics are designed to merge fragmented text regions (like individual
|
|
110
|
+
* characters in a date string) into logical regions while avoiding merging
|
|
111
|
+
* unrelated visual changes.
|
|
112
|
+
*
|
|
113
|
+
* Inspired by the Stroke Width Transform (SWT) text detection algorithm:
|
|
114
|
+
* Epshtein, B., Ofek, E., & Wexler, Y. (2010). "Detecting Text in Natural Scenes
|
|
115
|
+
* with Stroke Width Transform." CVPR 2010.
|
|
116
|
+
*/
|
|
117
|
+
export interface ClusterMergeOptions {
|
|
118
|
+
/**
|
|
119
|
+
* Maximum horizontal distance to merge clusters in same Y-band (pixels)
|
|
120
|
+
*
|
|
121
|
+
* Clusters within this horizontal distance and overlapping Y-ranges
|
|
122
|
+
* will be merged. Suitable for merging characters in text.
|
|
123
|
+
* @default 15
|
|
124
|
+
*/
|
|
125
|
+
horizontalDistance?: number;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Maximum vertical tolerance for "same Y-band" (pixels)
|
|
129
|
+
*
|
|
130
|
+
* Clusters with Y-ranges within this tolerance of each other are
|
|
131
|
+
* considered to be on the same line.
|
|
132
|
+
* @default 5
|
|
133
|
+
*/
|
|
134
|
+
yBandTolerance?: number;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Maximum height ratio between clusters to allow merging
|
|
138
|
+
*
|
|
139
|
+
* Prevents merging clusters of very different sizes (e.g., a word
|
|
140
|
+
* with a large image). Based on SWT heuristic.
|
|
141
|
+
* @default 2.0
|
|
142
|
+
*/
|
|
143
|
+
maxHeightRatio?: number;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Maximum width ratio between clusters to allow merging
|
|
147
|
+
*
|
|
148
|
+
* Additional SWT-inspired heuristic to prevent merging dissimilar
|
|
149
|
+
* regions.
|
|
150
|
+
* @default 3.0
|
|
151
|
+
*/
|
|
152
|
+
maxWidthRatio?: number;
|
|
153
|
+
}
|
|
154
|
+
|
|
100
155
|
export interface CompareOptions {
|
|
101
156
|
/**
|
|
102
157
|
* Perceptual color difference threshold using CIEDE2000 (Delta E units)
|
|
@@ -145,6 +200,23 @@ export interface CompareOptions {
|
|
|
145
200
|
*/
|
|
146
201
|
includeSSIM?: boolean;
|
|
147
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Calculate GMSD (Gradient Magnitude Similarity Deviation) score
|
|
205
|
+
*
|
|
206
|
+
* GMSD is very fast and highly sensitive to edge/structural changes.
|
|
207
|
+
* Useful for detecting border thickness changes, font weight shifts,
|
|
208
|
+
* and icon updates.
|
|
209
|
+
*
|
|
210
|
+
* **Note:** GMSD requires images with identical dimensions. For variable-height
|
|
211
|
+
* comparisons, `gmsdScore` will be `null`. Use SSIM for variable-height images.
|
|
212
|
+
*
|
|
213
|
+
* Reference: Xue et al. 2014 - "Gradient Magnitude Similarity Deviation:
|
|
214
|
+
* A Highly Efficient Perceptual Image Quality Index"
|
|
215
|
+
*
|
|
216
|
+
* @default false
|
|
217
|
+
*/
|
|
218
|
+
includeGMSD?: boolean;
|
|
219
|
+
|
|
148
220
|
/**
|
|
149
221
|
* Minimum cluster size to count as a real difference
|
|
150
222
|
*
|
|
@@ -161,6 +233,28 @@ export interface CompareOptions {
|
|
|
161
233
|
*/
|
|
162
234
|
minClusterSize?: number;
|
|
163
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Merge nearby clusters into logical regions
|
|
238
|
+
*
|
|
239
|
+
* When enabled, nearby clusters are merged using horizontal-biased heuristics
|
|
240
|
+
* that work well for text regions. This helps consolidate fragmented text
|
|
241
|
+
* changes (e.g., "2024-01-01" showing as 59 clusters) into logical regions.
|
|
242
|
+
*
|
|
243
|
+
* Automatically enables clustering when set.
|
|
244
|
+
*
|
|
245
|
+
* @default undefined (no merging)
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* ```typescript
|
|
249
|
+
* // Simple: enable merging with sensible defaults
|
|
250
|
+
* { clusterMerge: true }
|
|
251
|
+
*
|
|
252
|
+
* // Advanced: tune the merging behavior
|
|
253
|
+
* { clusterMerge: { horizontalDistance: 20, yBandTolerance: 10 } }
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
clusterMerge?: boolean | ClusterMergeOptions;
|
|
257
|
+
|
|
164
258
|
/**
|
|
165
259
|
* Path to save the diff image (highlighted differences)
|
|
166
260
|
* @default undefined
|
|
@@ -1049,3 +1143,131 @@ export function analyzeWcagAllCvdSync(img: ImageInput, options?: WcagOptions): C
|
|
|
1049
1143
|
* ```
|
|
1050
1144
|
*/
|
|
1051
1145
|
export function getColorBlindnessTypes(): ColorBlindnessTypeInfo[];
|
|
1146
|
+
|
|
1147
|
+
// ============================================================================
|
|
1148
|
+
// Diff Fingerprint API
|
|
1149
|
+
// ============================================================================
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Magnitude of a diff, bucketed into categories
|
|
1153
|
+
*/
|
|
1154
|
+
export type DiffMagnitude = 'tiny' | 'small' | 'medium' | 'large' | 'massive';
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* A compact fingerprint representing a diff's key characteristics.
|
|
1158
|
+
* Used for grouping similar diffs across comparisons.
|
|
1159
|
+
*/
|
|
1160
|
+
export interface DiffFingerprint {
|
|
1161
|
+
/** Number of clusters (distinct change regions) */
|
|
1162
|
+
clusterCount: number;
|
|
1163
|
+
|
|
1164
|
+
/** Normalized positions of cluster centers (0.0-1.0 relative to image dimensions) */
|
|
1165
|
+
clusterPositions: [number, number][];
|
|
1166
|
+
|
|
1167
|
+
/** Normalized sizes of clusters (area as ratio of total image area) */
|
|
1168
|
+
clusterSizes: number[];
|
|
1169
|
+
|
|
1170
|
+
/** Average intensity across all clusters (0-255 scale) */
|
|
1171
|
+
avgIntensity: number;
|
|
1172
|
+
|
|
1173
|
+
/** Average cluster density (pixels / bounding_box_area) */
|
|
1174
|
+
avgDensity: number;
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* Spatial distribution hash using a 4x4 grid (16 zones).
|
|
1178
|
+
* Each bit represents whether that zone contains a cluster center.
|
|
1179
|
+
*/
|
|
1180
|
+
zoneMask: number;
|
|
1181
|
+
|
|
1182
|
+
/** Overall diff percentage bucketed into categories */
|
|
1183
|
+
diffMagnitude: DiffMagnitude;
|
|
1184
|
+
|
|
1185
|
+
/** Pre-computed coarse hash for fast grouping (hex string) */
|
|
1186
|
+
hash: string;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* Compute a fingerprint from a diff result (synchronous)
|
|
1191
|
+
*
|
|
1192
|
+
* Creates a compact fingerprint that can be used to identify and group similar diffs
|
|
1193
|
+
* across different image comparisons. Requires that the diff result was computed with
|
|
1194
|
+
* `includeClusters: true`.
|
|
1195
|
+
*
|
|
1196
|
+
* @param diffResult - The result from a compare() call with clusters enabled
|
|
1197
|
+
* @param imageWidth - Width of the compared images in pixels
|
|
1198
|
+
* @param imageHeight - Height of the compared images in pixels
|
|
1199
|
+
* @returns A DiffFingerprint object, or null if no clusters are present
|
|
1200
|
+
*
|
|
1201
|
+
* @example
|
|
1202
|
+
* ```typescript
|
|
1203
|
+
* const result = await compare('baseline.png', 'current.png', {
|
|
1204
|
+
* includeClusters: true
|
|
1205
|
+
* });
|
|
1206
|
+
*
|
|
1207
|
+
* const fingerprint = computeFingerprintSync(result, 1920, 1080);
|
|
1208
|
+
* if (fingerprint) {
|
|
1209
|
+
* console.log(`Hash: ${fingerprint.hash}`);
|
|
1210
|
+
* console.log(`Zones: ${fingerprint.zoneMask.toString(2).padStart(16, '0')}`);
|
|
1211
|
+
* }
|
|
1212
|
+
* ```
|
|
1213
|
+
*/
|
|
1214
|
+
export function computeFingerprintSync(
|
|
1215
|
+
diffResult: DiffResult,
|
|
1216
|
+
imageWidth: number,
|
|
1217
|
+
imageHeight: number
|
|
1218
|
+
): DiffFingerprint | null;
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* Compare two fingerprints and return a similarity score (synchronous)
|
|
1222
|
+
*
|
|
1223
|
+
* Returns a score from 0.0 (completely different) to 1.0 (identical).
|
|
1224
|
+
* The similarity is computed using weighted factors:
|
|
1225
|
+
* - Zone mask overlap (40%): Same regions affected
|
|
1226
|
+
* - Cluster count similarity (20%): Similar number of change regions
|
|
1227
|
+
* - Diff magnitude match (15%): Similar overall change size
|
|
1228
|
+
* - Density similarity (15%): Similar change patterns (solid vs scattered)
|
|
1229
|
+
* - Intensity similarity (10%): Similar change intensity
|
|
1230
|
+
*
|
|
1231
|
+
* @param fp1 - First fingerprint to compare
|
|
1232
|
+
* @param fp2 - Second fingerprint to compare
|
|
1233
|
+
* @returns Similarity score from 0.0 to 1.0
|
|
1234
|
+
*
|
|
1235
|
+
* @example
|
|
1236
|
+
* ```typescript
|
|
1237
|
+
* const similarity = fingerprintSimilaritySync(fp1, fp2);
|
|
1238
|
+
* if (similarity > 0.8) {
|
|
1239
|
+
* console.log('These diffs are likely the same change!');
|
|
1240
|
+
* }
|
|
1241
|
+
* ```
|
|
1242
|
+
*/
|
|
1243
|
+
export function fingerprintSimilaritySync(fp1: DiffFingerprint, fp2: DiffFingerprint): number;
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* Get the coarse hash of a fingerprint (synchronous)
|
|
1247
|
+
*
|
|
1248
|
+
* Returns a hex string that can be used for fast exact-match grouping.
|
|
1249
|
+
* Diffs with the same coarse hash are very likely to be the same visual change.
|
|
1250
|
+
*
|
|
1251
|
+
* The hash combines:
|
|
1252
|
+
* - Zone mask (16 bits): Which regions are affected
|
|
1253
|
+
* - Diff magnitude (3 bits): Overall change size
|
|
1254
|
+
* - Cluster count bucket (2 bits): Number of change regions
|
|
1255
|
+
*
|
|
1256
|
+
* @param fingerprint - The fingerprint to hash
|
|
1257
|
+
* @returns A 16-character hex string
|
|
1258
|
+
*
|
|
1259
|
+
* @example
|
|
1260
|
+
* ```typescript
|
|
1261
|
+
* const hash = fingerprintHashSync(fingerprint);
|
|
1262
|
+
* console.log(`Hash: ${hash}`); // e.g., "0000000000000021"
|
|
1263
|
+
*
|
|
1264
|
+
* // Use for grouping
|
|
1265
|
+
* const groups = new Map<string, DiffFingerprint[]>();
|
|
1266
|
+
* for (const fp of fingerprints) {
|
|
1267
|
+
* const h = fingerprintHashSync(fp);
|
|
1268
|
+
* if (!groups.has(h)) groups.set(h, []);
|
|
1269
|
+
* groups.get(h)!.push(fp);
|
|
1270
|
+
* }
|
|
1271
|
+
* ```
|
|
1272
|
+
*/
|
|
1273
|
+
export function fingerprintHashSync(fingerprint: DiffFingerprint): string;
|
package/index.js
CHANGED
|
@@ -66,3 +66,8 @@ export const analyzeWcagForCvdSync = addon.analyzeWcagForCvdSync;
|
|
|
66
66
|
export const analyzeWcagAllCvd = addon.analyzeWcagAllCvd;
|
|
67
67
|
export const analyzeWcagAllCvdSync = addon.analyzeWcagAllCvdSync;
|
|
68
68
|
export const getColorBlindnessTypes = addon.getColorBlindnessTypes;
|
|
69
|
+
|
|
70
|
+
// Diff Fingerprint API
|
|
71
|
+
export const computeFingerprintSync = addon.computeFingerprintSync;
|
|
72
|
+
export const fingerprintSimilaritySync = addon.fingerprintSimilaritySync;
|
|
73
|
+
export const fingerprintHashSync = addon.fingerprintHashSync;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vizzly-testing/honeydiff",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "High-performance image diffing for Node.js - Native bindings to Honeydiff Rust library",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"index.d.ts",
|
|
49
49
|
"load-platform.cjs",
|
|
50
50
|
"platforms/*.node",
|
|
51
|
+
"examples/",
|
|
51
52
|
"README.md",
|
|
52
53
|
"CHANGELOG.md"
|
|
53
54
|
],
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|