@vizzly-testing/honeydiff 0.8.1 → 0.9.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 CHANGED
@@ -5,6 +5,24 @@ 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.9.0] - 2026-01-26
9
+
10
+ ## What's Changed
11
+
12
+ ### Added
13
+ - **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).
14
+ - `computeFingerprintSync()` - Generate a compact fingerprint from a diff result
15
+ - `fingerprintSimilaritySync()` - Compare two fingerprints with a similarity score (0.0-1.0)
16
+ - `fingerprintHashSync()` - Get a coarse hash for fast O(1) grouping
17
+ - `DiffFingerprint` interface with rich metadata:
18
+ - `zoneMask` - 4x4 spatial grid showing which regions are affected
19
+ - `clusterCount`, `clusterPositions`, `clusterSizes` - Spatial distribution data
20
+ - `diffMagnitude` - Bucketed size categories (tiny/small/medium/large/massive)
21
+ - `avgIntensity`, `avgDensity` - Change characteristics
22
+ - `hash` - Pre-computed coarse hash for grouping
23
+
24
+ **Full Changelog**: https://github.com/vizzly-testing/honeydiff/compare/v0.8.1...v0.9.0
25
+
8
26
  ## [0.8.1] - 2026-01-09
9
27
 
10
28
  ## What's Changed
package/README.md CHANGED
@@ -285,6 +285,69 @@ let metadata = await getImageMetadataFromFile('screenshot.png');
285
285
  console.log(`${metadata.width}x${metadata.height}, ${metadata.fileSizeBytes} bytes`);
286
286
  ```
287
287
 
288
+ ### 11. Diff Fingerprinting
289
+
290
+ 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).
291
+
292
+ ```javascript
293
+ const { compare, computeFingerprintSync, fingerprintSimilaritySync, fingerprintHashSync } = require('@vizzly-testing/honeydiff');
294
+
295
+ // Compare images with clustering enabled
296
+ let result = await compare('baseline.png', 'current.png', {
297
+ includeClusters: true
298
+ });
299
+
300
+ // Compute fingerprint
301
+ let fingerprint = computeFingerprintSync(result, 1920, 1080);
302
+
303
+ if (fingerprint) {
304
+ console.log(`Hash: ${fingerprint.hash}`);
305
+ console.log(`Zones affected: ${fingerprint.zoneMask.toString(2).padStart(16, '0')}`);
306
+ console.log(`Cluster count: ${fingerprint.clusterCount}`);
307
+ console.log(`Magnitude: ${fingerprint.diffMagnitude}`);
308
+ }
309
+ ```
310
+
311
+ **Grouping similar diffs:**
312
+ ```javascript
313
+ // Group diffs by hash for batch approval
314
+ let groups = new Map();
315
+
316
+ for (let comparison of comparisons) {
317
+ let fp = computeFingerprintSync(comparison.result, width, height);
318
+ if (fp) {
319
+ let hash = fingerprintHashSync(fp);
320
+ if (!groups.has(hash)) groups.set(hash, []);
321
+ groups.get(hash).push(comparison);
322
+ }
323
+ }
324
+
325
+ // Find groups with multiple similar diffs
326
+ for (let [hash, items] of groups) {
327
+ if (items.length > 1) {
328
+ console.log(`Found ${items.length} similar diffs with hash ${hash}`);
329
+ }
330
+ }
331
+ ```
332
+
333
+ **Similarity scoring:**
334
+ ```javascript
335
+ // Compare two fingerprints (0.0 = different, 1.0 = identical)
336
+ let similarity = fingerprintSimilaritySync(fp1, fp2);
337
+
338
+ if (similarity > 0.8) {
339
+ console.log('These diffs are likely the same visual change');
340
+ }
341
+ ```
342
+
343
+ **Fingerprint properties:**
344
+ - `clusterCount` - Number of distinct change regions
345
+ - `clusterPositions` - Normalized positions (0.0-1.0)
346
+ - `clusterSizes` - Relative sizes (area ratio)
347
+ - `zoneMask` - 4x4 grid (16 zones) showing affected regions
348
+ - `diffMagnitude` - Bucketed size: `tiny`, `small`, `medium`, `large`, `massive`
349
+ - `hash` - Pre-computed coarse hash for fast grouping
350
+
288
351
  ## Accessibility Features
289
352
 
290
353
  Built-in WCAG color contrast analysis and color blindness simulation to catch accessibility violations in screenshots and UI designs.
@@ -470,6 +533,20 @@ Synchronous metadata retrieval from buffer.
470
533
 
471
534
  Synchronous metadata retrieval from file.
472
535
 
536
+ ### Diff Fingerprint Functions (Sync only)
537
+
538
+ **`computeFingerprintSync(diffResult, width, height): DiffFingerprint | null`**
539
+
540
+ Compute a fingerprint from a diff result. Requires `includeClusters: true` in compare options. Returns null if no clusters.
541
+
542
+ **`fingerprintSimilaritySync(fp1, fp2): number`**
543
+
544
+ Compare two fingerprints. Returns 0.0 (different) to 1.0 (identical).
545
+
546
+ **`fingerprintHashSync(fingerprint): string`**
547
+
548
+ Get coarse hash for fast grouping. Returns 16-char hex string.
549
+
473
550
  ### Input Types
474
551
 
475
552
  All functions accept:
@@ -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
+ });
@@ -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
@@ -1049,3 +1049,131 @@ export function analyzeWcagAllCvdSync(img: ImageInput, options?: WcagOptions): C
1049
1049
  * ```
1050
1050
  */
1051
1051
  export function getColorBlindnessTypes(): ColorBlindnessTypeInfo[];
1052
+
1053
+ // ============================================================================
1054
+ // Diff Fingerprint API
1055
+ // ============================================================================
1056
+
1057
+ /**
1058
+ * Magnitude of a diff, bucketed into categories
1059
+ */
1060
+ export type DiffMagnitude = 'tiny' | 'small' | 'medium' | 'large' | 'massive';
1061
+
1062
+ /**
1063
+ * A compact fingerprint representing a diff's key characteristics.
1064
+ * Used for grouping similar diffs across comparisons.
1065
+ */
1066
+ export interface DiffFingerprint {
1067
+ /** Number of clusters (distinct change regions) */
1068
+ clusterCount: number;
1069
+
1070
+ /** Normalized positions of cluster centers (0.0-1.0 relative to image dimensions) */
1071
+ clusterPositions: [number, number][];
1072
+
1073
+ /** Normalized sizes of clusters (area as ratio of total image area) */
1074
+ clusterSizes: number[];
1075
+
1076
+ /** Average intensity across all clusters (0-255 scale) */
1077
+ avgIntensity: number;
1078
+
1079
+ /** Average cluster density (pixels / bounding_box_area) */
1080
+ avgDensity: number;
1081
+
1082
+ /**
1083
+ * Spatial distribution hash using a 4x4 grid (16 zones).
1084
+ * Each bit represents whether that zone contains a cluster center.
1085
+ */
1086
+ zoneMask: number;
1087
+
1088
+ /** Overall diff percentage bucketed into categories */
1089
+ diffMagnitude: DiffMagnitude;
1090
+
1091
+ /** Pre-computed coarse hash for fast grouping (hex string) */
1092
+ hash: string;
1093
+ }
1094
+
1095
+ /**
1096
+ * Compute a fingerprint from a diff result (synchronous)
1097
+ *
1098
+ * Creates a compact fingerprint that can be used to identify and group similar diffs
1099
+ * across different image comparisons. Requires that the diff result was computed with
1100
+ * `includeClusters: true`.
1101
+ *
1102
+ * @param diffResult - The result from a compare() call with clusters enabled
1103
+ * @param imageWidth - Width of the compared images in pixels
1104
+ * @param imageHeight - Height of the compared images in pixels
1105
+ * @returns A DiffFingerprint object, or null if no clusters are present
1106
+ *
1107
+ * @example
1108
+ * ```typescript
1109
+ * const result = await compare('baseline.png', 'current.png', {
1110
+ * includeClusters: true
1111
+ * });
1112
+ *
1113
+ * const fingerprint = computeFingerprintSync(result, 1920, 1080);
1114
+ * if (fingerprint) {
1115
+ * console.log(`Hash: ${fingerprint.hash}`);
1116
+ * console.log(`Zones: ${fingerprint.zoneMask.toString(2).padStart(16, '0')}`);
1117
+ * }
1118
+ * ```
1119
+ */
1120
+ export function computeFingerprintSync(
1121
+ diffResult: DiffResult,
1122
+ imageWidth: number,
1123
+ imageHeight: number
1124
+ ): DiffFingerprint | null;
1125
+
1126
+ /**
1127
+ * Compare two fingerprints and return a similarity score (synchronous)
1128
+ *
1129
+ * Returns a score from 0.0 (completely different) to 1.0 (identical).
1130
+ * The similarity is computed using weighted factors:
1131
+ * - Zone mask overlap (40%): Same regions affected
1132
+ * - Cluster count similarity (20%): Similar number of change regions
1133
+ * - Diff magnitude match (15%): Similar overall change size
1134
+ * - Density similarity (15%): Similar change patterns (solid vs scattered)
1135
+ * - Intensity similarity (10%): Similar change intensity
1136
+ *
1137
+ * @param fp1 - First fingerprint to compare
1138
+ * @param fp2 - Second fingerprint to compare
1139
+ * @returns Similarity score from 0.0 to 1.0
1140
+ *
1141
+ * @example
1142
+ * ```typescript
1143
+ * const similarity = fingerprintSimilaritySync(fp1, fp2);
1144
+ * if (similarity > 0.8) {
1145
+ * console.log('These diffs are likely the same change!');
1146
+ * }
1147
+ * ```
1148
+ */
1149
+ export function fingerprintSimilaritySync(fp1: DiffFingerprint, fp2: DiffFingerprint): number;
1150
+
1151
+ /**
1152
+ * Get the coarse hash of a fingerprint (synchronous)
1153
+ *
1154
+ * Returns a hex string that can be used for fast exact-match grouping.
1155
+ * Diffs with the same coarse hash are very likely to be the same visual change.
1156
+ *
1157
+ * The hash combines:
1158
+ * - Zone mask (16 bits): Which regions are affected
1159
+ * - Diff magnitude (3 bits): Overall change size
1160
+ * - Cluster count bucket (2 bits): Number of change regions
1161
+ *
1162
+ * @param fingerprint - The fingerprint to hash
1163
+ * @returns A 16-character hex string
1164
+ *
1165
+ * @example
1166
+ * ```typescript
1167
+ * const hash = fingerprintHashSync(fingerprint);
1168
+ * console.log(`Hash: ${hash}`); // e.g., "0000000000000021"
1169
+ *
1170
+ * // Use for grouping
1171
+ * const groups = new Map<string, DiffFingerprint[]>();
1172
+ * for (const fp of fingerprints) {
1173
+ * const h = fingerprintHashSync(fp);
1174
+ * if (!groups.has(h)) groups.set(h, []);
1175
+ * groups.get(h)!.push(fp);
1176
+ * }
1177
+ * ```
1178
+ */
1179
+ 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.8.1",
3
+ "version": "0.9.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
  ],