@blazediff/matcher 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +347 -0
- package/dist/index.d.mts +206 -0
- package/dist/index.d.ts +206 -0
- package/dist/index.js +3 -0
- package/dist/index.mjs +3 -0
- package/package.json +59 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Teimur Gasanov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
# @blazediff/matcher
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@blazediff/matcher)
|
|
6
|
+
[](https://www.npmjs.com/package/@blazediff/matcher)
|
|
7
|
+
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
Core matcher logic for visual regression testing. Provides snapshot comparison with multiple algorithms, framework-agnostic APIs, and snapshot state tracking.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- **Multiple comparison methods**: `core`, `bin`, `ssim`, `msssim`, `hitchhikers-ssim`, `gmsd`
|
|
15
|
+
- **Flexible input types**: File paths or image buffers
|
|
16
|
+
- **Snapshot state tracking**: Reports added/matched/updated/failed status
|
|
17
|
+
- **Configurable thresholds**: Pixel count or percentage-based
|
|
18
|
+
- **Framework-agnostic**: Core logic for Jest, Vitest, Bun integrations
|
|
19
|
+
- **Rich comparison results**: Diff counts, percentages, and similarity scores
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @blazediff/matcher
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { getOrCreateSnapshot } from '@blazediff/matcher';
|
|
31
|
+
|
|
32
|
+
const result = await getOrCreateSnapshot(
|
|
33
|
+
imageBuffer, // or file path
|
|
34
|
+
{
|
|
35
|
+
method: 'core',
|
|
36
|
+
failureThreshold: 0.01,
|
|
37
|
+
failureThresholdType: 'percent',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
testPath: '/path/to/test.spec.ts',
|
|
41
|
+
testName: 'should render correctly',
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
console.log(result.pass); // true/false
|
|
46
|
+
console.log(result.snapshotStatus); // 'added' | 'matched' | 'updated' | 'failed'
|
|
47
|
+
console.log(result.diffPercentage); // e.g., 0.05
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## API Reference
|
|
51
|
+
|
|
52
|
+
### getOrCreateSnapshot(received, options, testContext)
|
|
53
|
+
|
|
54
|
+
Main function for snapshot comparison.
|
|
55
|
+
|
|
56
|
+
<table>
|
|
57
|
+
<tr>
|
|
58
|
+
<th width="500">Parameter</th>
|
|
59
|
+
<th width="500">Type</th>
|
|
60
|
+
<th width="500">Description</th>
|
|
61
|
+
</tr>
|
|
62
|
+
<tr>
|
|
63
|
+
<td><code>received</code></td>
|
|
64
|
+
<td>ImageInput</td>
|
|
65
|
+
<td>Image to compare (file path or buffer with dimensions)</td>
|
|
66
|
+
</tr>
|
|
67
|
+
<tr>
|
|
68
|
+
<td><code>options</code></td>
|
|
69
|
+
<td>MatcherOptions</td>
|
|
70
|
+
<td>Comparison options (see below)</td>
|
|
71
|
+
</tr>
|
|
72
|
+
<tr>
|
|
73
|
+
<td><code>testContext</code></td>
|
|
74
|
+
<td>TestContext</td>
|
|
75
|
+
<td>Test information (testPath, testName)</td>
|
|
76
|
+
</tr>
|
|
77
|
+
</table>
|
|
78
|
+
|
|
79
|
+
**Returns:** `Promise<ComparisonResult>`
|
|
80
|
+
|
|
81
|
+
### MatcherOptions
|
|
82
|
+
|
|
83
|
+
<table>
|
|
84
|
+
<tr>
|
|
85
|
+
<th width="500">Option</th>
|
|
86
|
+
<th width="500">Type</th>
|
|
87
|
+
<th width="500">Default</th>
|
|
88
|
+
<th width="500">Description</th>
|
|
89
|
+
</tr>
|
|
90
|
+
<tr>
|
|
91
|
+
<td><code>method</code></td>
|
|
92
|
+
<td>ComparisonMethod</td>
|
|
93
|
+
<td>-</td>
|
|
94
|
+
<td>Comparison algorithm to use</td>
|
|
95
|
+
</tr>
|
|
96
|
+
<tr>
|
|
97
|
+
<td><code>failureThreshold</code></td>
|
|
98
|
+
<td>number</td>
|
|
99
|
+
<td>0</td>
|
|
100
|
+
<td>Number of pixels or percentage difference allowed</td>
|
|
101
|
+
</tr>
|
|
102
|
+
<tr>
|
|
103
|
+
<td><code>failureThresholdType</code></td>
|
|
104
|
+
<td>'pixel' | 'percent'</td>
|
|
105
|
+
<td>'pixel'</td>
|
|
106
|
+
<td>How to interpret failureThreshold</td>
|
|
107
|
+
</tr>
|
|
108
|
+
<tr>
|
|
109
|
+
<td><code>snapshotsDir</code></td>
|
|
110
|
+
<td>string</td>
|
|
111
|
+
<td>'__snapshots__'</td>
|
|
112
|
+
<td>Directory to store snapshots (relative to test file)</td>
|
|
113
|
+
</tr>
|
|
114
|
+
<tr>
|
|
115
|
+
<td><code>snapshotIdentifier</code></td>
|
|
116
|
+
<td>string</td>
|
|
117
|
+
<td>-</td>
|
|
118
|
+
<td>Custom identifier for the snapshot file</td>
|
|
119
|
+
</tr>
|
|
120
|
+
<tr>
|
|
121
|
+
<td><code>updateSnapshots</code></td>
|
|
122
|
+
<td>boolean</td>
|
|
123
|
+
<td>false</td>
|
|
124
|
+
<td>Force update snapshots (like running with -u flag)</td>
|
|
125
|
+
</tr>
|
|
126
|
+
<tr>
|
|
127
|
+
<td><code>threshold</code></td>
|
|
128
|
+
<td>number</td>
|
|
129
|
+
<td>0.1</td>
|
|
130
|
+
<td>Color difference threshold for core/bin methods (0-1)</td>
|
|
131
|
+
</tr>
|
|
132
|
+
<tr>
|
|
133
|
+
<td><code>antialiasing</code></td>
|
|
134
|
+
<td>boolean</td>
|
|
135
|
+
<td>false</td>
|
|
136
|
+
<td>Enable anti-aliasing detection (bin method)</td>
|
|
137
|
+
</tr>
|
|
138
|
+
<tr>
|
|
139
|
+
<td><code>includeAA</code></td>
|
|
140
|
+
<td>boolean</td>
|
|
141
|
+
<td>false</td>
|
|
142
|
+
<td>Include anti-aliased pixels in diff count (core method)</td>
|
|
143
|
+
</tr>
|
|
144
|
+
<tr>
|
|
145
|
+
<td><code>windowSize</code></td>
|
|
146
|
+
<td>number</td>
|
|
147
|
+
<td>11</td>
|
|
148
|
+
<td>Window size for SSIM variants</td>
|
|
149
|
+
</tr>
|
|
150
|
+
<tr>
|
|
151
|
+
<td><code>k1</code></td>
|
|
152
|
+
<td>number</td>
|
|
153
|
+
<td>0.01</td>
|
|
154
|
+
<td>k1 constant for SSIM</td>
|
|
155
|
+
</tr>
|
|
156
|
+
<tr>
|
|
157
|
+
<td><code>k2</code></td>
|
|
158
|
+
<td>number</td>
|
|
159
|
+
<td>0.03</td>
|
|
160
|
+
<td>k2 constant for SSIM</td>
|
|
161
|
+
</tr>
|
|
162
|
+
<tr>
|
|
163
|
+
<td><code>downsample</code></td>
|
|
164
|
+
<td>0 | 1</td>
|
|
165
|
+
<td>0</td>
|
|
166
|
+
<td>Downsample factor for GMSD</td>
|
|
167
|
+
</tr>
|
|
168
|
+
</table>
|
|
169
|
+
|
|
170
|
+
### ComparisonResult
|
|
171
|
+
|
|
172
|
+
<table>
|
|
173
|
+
<tr>
|
|
174
|
+
<th width="500">Field</th>
|
|
175
|
+
<th width="500">Type</th>
|
|
176
|
+
<th width="500">Description</th>
|
|
177
|
+
</tr>
|
|
178
|
+
<tr>
|
|
179
|
+
<td><code>pass</code></td>
|
|
180
|
+
<td>boolean</td>
|
|
181
|
+
<td>Whether the comparison passed</td>
|
|
182
|
+
</tr>
|
|
183
|
+
<tr>
|
|
184
|
+
<td><code>message</code></td>
|
|
185
|
+
<td>string</td>
|
|
186
|
+
<td>Human-readable message describing the result</td>
|
|
187
|
+
</tr>
|
|
188
|
+
<tr>
|
|
189
|
+
<td><code>snapshotStatus</code></td>
|
|
190
|
+
<td>SnapshotStatus</td>
|
|
191
|
+
<td>'added' | 'matched' | 'updated' | 'failed'</td>
|
|
192
|
+
</tr>
|
|
193
|
+
<tr>
|
|
194
|
+
<td><code>diffCount</code></td>
|
|
195
|
+
<td>number</td>
|
|
196
|
+
<td>Number of different pixels (pixel-based methods)</td>
|
|
197
|
+
</tr>
|
|
198
|
+
<tr>
|
|
199
|
+
<td><code>diffPercentage</code></td>
|
|
200
|
+
<td>number</td>
|
|
201
|
+
<td>Percentage of different pixels</td>
|
|
202
|
+
</tr>
|
|
203
|
+
<tr>
|
|
204
|
+
<td><code>score</code></td>
|
|
205
|
+
<td>number</td>
|
|
206
|
+
<td>Similarity score (SSIM: 1 = identical, GMSD: 0 = identical)</td>
|
|
207
|
+
</tr>
|
|
208
|
+
<tr>
|
|
209
|
+
<td><code>baselinePath</code></td>
|
|
210
|
+
<td>string</td>
|
|
211
|
+
<td>Path to baseline snapshot</td>
|
|
212
|
+
</tr>
|
|
213
|
+
<tr>
|
|
214
|
+
<td><code>receivedPath</code></td>
|
|
215
|
+
<td>string</td>
|
|
216
|
+
<td>Path to received image (saved for debugging on failure)</td>
|
|
217
|
+
</tr>
|
|
218
|
+
<tr>
|
|
219
|
+
<td><code>diffPath</code></td>
|
|
220
|
+
<td>string</td>
|
|
221
|
+
<td>Path to diff visualization</td>
|
|
222
|
+
</tr>
|
|
223
|
+
</table>
|
|
224
|
+
|
|
225
|
+
### ImageInput
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
type ImageInput =
|
|
229
|
+
| string // File path
|
|
230
|
+
| {
|
|
231
|
+
data: Uint8Array | Uint8ClampedArray | Buffer;
|
|
232
|
+
width: number;
|
|
233
|
+
height: number;
|
|
234
|
+
};
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Comparison Methods
|
|
238
|
+
|
|
239
|
+
### `bin`
|
|
240
|
+
Rust-native comparison via N-API bindings. **Fastest method** with native performance.
|
|
241
|
+
- **Input**: File paths only
|
|
242
|
+
- **Algorithm**: YIQ color space with block-based optimization
|
|
243
|
+
- **Best for**: Production testing, large images
|
|
244
|
+
|
|
245
|
+
### `core`
|
|
246
|
+
Pixel-by-pixel comparison in JavaScript. Pure JS implementation with no native dependencies.
|
|
247
|
+
- **Input**: File paths or buffers
|
|
248
|
+
- **Algorithm**: YIQ color space with anti-aliasing detection
|
|
249
|
+
- **Best for**: Cross-platform compatibility
|
|
250
|
+
|
|
251
|
+
### `ssim`
|
|
252
|
+
Structural Similarity Index. Measures perceptual similarity.
|
|
253
|
+
- **Input**: File paths or buffers
|
|
254
|
+
- **Algorithm**: Standard SSIM with configurable window size
|
|
255
|
+
- **Best for**: Perceptual quality assessment
|
|
256
|
+
- **Score**: 1 = identical, lower = more different
|
|
257
|
+
|
|
258
|
+
### `msssim`
|
|
259
|
+
Multi-Scale Structural Similarity Index.
|
|
260
|
+
- **Input**: File paths or buffers
|
|
261
|
+
- **Algorithm**: SSIM across multiple scales
|
|
262
|
+
- **Best for**: Images with varying resolutions or scales
|
|
263
|
+
|
|
264
|
+
### `hitchhikers-ssim`
|
|
265
|
+
Fast SSIM approximation from Hitchhiker's Guide.
|
|
266
|
+
- **Input**: File paths or buffers
|
|
267
|
+
- **Algorithm**: Optimized SSIM calculation
|
|
268
|
+
- **Best for**: Faster SSIM with acceptable accuracy trade-off
|
|
269
|
+
|
|
270
|
+
### `gmsd`
|
|
271
|
+
Gradient Magnitude Similarity Deviation.
|
|
272
|
+
- **Input**: File paths or buffers
|
|
273
|
+
- **Algorithm**: Gradient-based perceptual similarity
|
|
274
|
+
- **Best for**: Detecting structural changes
|
|
275
|
+
- **Score**: 0 = identical, higher = more different
|
|
276
|
+
|
|
277
|
+
## Usage Examples
|
|
278
|
+
|
|
279
|
+
### With File Paths
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
const result = await getOrCreateSnapshot(
|
|
283
|
+
'/path/to/screenshot.png',
|
|
284
|
+
{ method: 'bin' },
|
|
285
|
+
{ testPath: __filename, testName: 'test name' }
|
|
286
|
+
);
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### With Image Buffers
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
const imageData = {
|
|
293
|
+
data: new Uint8Array([...]),
|
|
294
|
+
width: 800,
|
|
295
|
+
height: 600,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const result = await getOrCreateSnapshot(
|
|
299
|
+
imageData,
|
|
300
|
+
{ method: 'core' },
|
|
301
|
+
{ testPath: __filename, testName: 'test name' }
|
|
302
|
+
);
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Custom Threshold
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
const result = await getOrCreateSnapshot(
|
|
309
|
+
imagePath,
|
|
310
|
+
{
|
|
311
|
+
method: 'core',
|
|
312
|
+
failureThreshold: 0.1,
|
|
313
|
+
failureThresholdType: 'percent', // Allow 0.1% difference
|
|
314
|
+
},
|
|
315
|
+
{ testPath: __filename, testName: 'test name' }
|
|
316
|
+
);
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Different Comparison Methods
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
// Fastest - Rust native (file paths only)
|
|
323
|
+
await getOrCreateSnapshot(imagePath, { method: 'bin' }, context);
|
|
324
|
+
|
|
325
|
+
// Pure JavaScript
|
|
326
|
+
await getOrCreateSnapshot(imageBuffer, { method: 'core' }, context);
|
|
327
|
+
|
|
328
|
+
// Perceptual similarity
|
|
329
|
+
await getOrCreateSnapshot(imageBuffer, { method: 'ssim' }, context);
|
|
330
|
+
|
|
331
|
+
// Gradient-based
|
|
332
|
+
await getOrCreateSnapshot(imageBuffer, { method: 'gmsd' }, context);
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Framework Integrations
|
|
336
|
+
|
|
337
|
+
This package provides the core logic for framework-specific integrations:
|
|
338
|
+
|
|
339
|
+
- [@blazediff/jest](https://www.npmjs.com/package/@blazediff/jest) - Jest matcher
|
|
340
|
+
- [@blazediff/vitest](https://www.npmjs.com/package/@blazediff/vitest) - Vitest matcher
|
|
341
|
+
- [@blazediff/bun](https://www.npmjs.com/package/@blazediff/bun) - Bun test matcher
|
|
342
|
+
|
|
343
|
+
## Links
|
|
344
|
+
|
|
345
|
+
- [GitHub Repository](https://github.com/teimurjan/blazediff)
|
|
346
|
+
- [Documentation](https://blazediff.dev/docs/matcher)
|
|
347
|
+
- [NPM Package](https://www.npmjs.com/package/@blazediff/matcher)
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comparison methods available in blazediff
|
|
3
|
+
*/
|
|
4
|
+
type ComparisonMethod = "bin" | "core" | "ssim" | "msssim" | "hitchhikers-ssim" | "gmsd";
|
|
5
|
+
/**
|
|
6
|
+
* Status of a snapshot operation
|
|
7
|
+
*/
|
|
8
|
+
type SnapshotStatus = "added" | "matched" | "updated" | "failed";
|
|
9
|
+
/**
|
|
10
|
+
* Image input - either a file path or a buffer with dimensions
|
|
11
|
+
*/
|
|
12
|
+
type ImageInput = string | {
|
|
13
|
+
data: Uint8Array | Uint8ClampedArray | Buffer;
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Options for the matcher
|
|
19
|
+
*/
|
|
20
|
+
interface MatcherOptions {
|
|
21
|
+
/** Comparison method to use */
|
|
22
|
+
method: ComparisonMethod;
|
|
23
|
+
/**
|
|
24
|
+
* Failure threshold - number of pixels or percentage difference allowed
|
|
25
|
+
* @default 0
|
|
26
|
+
*/
|
|
27
|
+
failureThreshold?: number;
|
|
28
|
+
/**
|
|
29
|
+
* How to interpret failureThreshold
|
|
30
|
+
* @default 'pixel'
|
|
31
|
+
*/
|
|
32
|
+
failureThresholdType?: "pixel" | "percent";
|
|
33
|
+
/**
|
|
34
|
+
* Directory to store snapshots relative to test file
|
|
35
|
+
* @default '__snapshots__'
|
|
36
|
+
*/
|
|
37
|
+
snapshotsDir?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Custom identifier for the snapshot file
|
|
40
|
+
* If not provided, derived from test name
|
|
41
|
+
*/
|
|
42
|
+
snapshotIdentifier?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Force update snapshots (like running with -u flag)
|
|
45
|
+
* @default false
|
|
46
|
+
*/
|
|
47
|
+
updateSnapshots?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Color difference threshold for core/bin methods (0-1)
|
|
50
|
+
* Lower = more strict
|
|
51
|
+
* @default 0.1
|
|
52
|
+
*/
|
|
53
|
+
threshold?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Enable anti-aliasing detection (bin method)
|
|
56
|
+
* @default false
|
|
57
|
+
*/
|
|
58
|
+
antialiasing?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Include anti-aliased pixels in diff count (core method)
|
|
61
|
+
* @default false
|
|
62
|
+
*/
|
|
63
|
+
includeAA?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Window size for SSIM variants
|
|
66
|
+
* @default 11
|
|
67
|
+
*/
|
|
68
|
+
windowSize?: number;
|
|
69
|
+
/**
|
|
70
|
+
* k1 constant for SSIM
|
|
71
|
+
* @default 0.01
|
|
72
|
+
*/
|
|
73
|
+
k1?: number;
|
|
74
|
+
/**
|
|
75
|
+
* k2 constant for SSIM
|
|
76
|
+
* @default 0.03
|
|
77
|
+
*/
|
|
78
|
+
k2?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Downsample factor for GMSD (0 or 1)
|
|
81
|
+
* @default 0
|
|
82
|
+
*/
|
|
83
|
+
downsample?: 0 | 1;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Result of a comparison operation
|
|
87
|
+
*/
|
|
88
|
+
interface ComparisonResult {
|
|
89
|
+
/** Whether the comparison passed */
|
|
90
|
+
pass: boolean;
|
|
91
|
+
/** Human-readable message describing the result */
|
|
92
|
+
message: string;
|
|
93
|
+
/** Number of different pixels (for pixel-based methods) */
|
|
94
|
+
diffCount?: number;
|
|
95
|
+
/** Percentage of different pixels */
|
|
96
|
+
diffPercentage?: number;
|
|
97
|
+
/** Similarity score (for SSIM/GMSD - 1 = identical for SSIM, 0 = identical for GMSD) */
|
|
98
|
+
score?: number;
|
|
99
|
+
/** Path to baseline snapshot */
|
|
100
|
+
baselinePath?: string;
|
|
101
|
+
/** Path to received image (saved for debugging) */
|
|
102
|
+
receivedPath?: string;
|
|
103
|
+
/** Path to diff visualization */
|
|
104
|
+
diffPath?: string;
|
|
105
|
+
/** Status of the snapshot operation */
|
|
106
|
+
snapshotStatus?: SnapshotStatus;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Context provided by test frameworks
|
|
110
|
+
*/
|
|
111
|
+
interface TestContext {
|
|
112
|
+
/** Absolute path to the test file */
|
|
113
|
+
testPath: string;
|
|
114
|
+
/** Name of the current test */
|
|
115
|
+
testName: string;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Image data with dimensions
|
|
119
|
+
*/
|
|
120
|
+
interface ImageData {
|
|
121
|
+
data: Uint8Array;
|
|
122
|
+
width: number;
|
|
123
|
+
height: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface RunComparisonResult {
|
|
127
|
+
/** Number of different pixels (for pixel-based methods) */
|
|
128
|
+
diffCount?: number;
|
|
129
|
+
/** Percentage of different pixels */
|
|
130
|
+
diffPercentage?: number;
|
|
131
|
+
/** Score for perceptual methods (SSIM: 1=identical, GMSD: 0=identical) */
|
|
132
|
+
score?: number;
|
|
133
|
+
/** Diff visualization output buffer */
|
|
134
|
+
diffOutput?: Uint8Array;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Validate that the comparison method supports the given input type
|
|
138
|
+
*/
|
|
139
|
+
declare function validateMethodSupportsInput(method: ComparisonMethod, input: ImageInput): void;
|
|
140
|
+
/**
|
|
141
|
+
* Run comparison using the specified method
|
|
142
|
+
*/
|
|
143
|
+
declare function runComparison(received: ImageInput, baseline: ImageInput, method: ComparisonMethod, options: MatcherOptions, diffOutputPath?: string): Promise<RunComparisonResult>;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if input is a file path
|
|
147
|
+
*/
|
|
148
|
+
declare function isFilePath(input: ImageInput): input is string;
|
|
149
|
+
/**
|
|
150
|
+
* Check if input is an image buffer with dimensions
|
|
151
|
+
*/
|
|
152
|
+
declare function isImageBuffer(input: ImageInput): input is {
|
|
153
|
+
data: Uint8Array | Uint8ClampedArray | Buffer;
|
|
154
|
+
width: number;
|
|
155
|
+
height: number;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Load a PNG image from file path
|
|
159
|
+
*/
|
|
160
|
+
declare function loadPNG(filePath: string): Promise<ImageData>;
|
|
161
|
+
/**
|
|
162
|
+
* Save image data to a PNG file
|
|
163
|
+
*/
|
|
164
|
+
declare function savePNG(filePath: string, data: Uint8Array | Uint8ClampedArray | Buffer, width: number, height: number): Promise<void>;
|
|
165
|
+
/**
|
|
166
|
+
* Normalize image input to ImageData
|
|
167
|
+
* If input is a file path, loads the image
|
|
168
|
+
* If input is already a buffer, returns it with normalized Uint8Array
|
|
169
|
+
*/
|
|
170
|
+
declare function normalizeImageInput(input: ImageInput): Promise<ImageData>;
|
|
171
|
+
/**
|
|
172
|
+
* Check if a file exists
|
|
173
|
+
*/
|
|
174
|
+
declare function fileExists(filePath: string): boolean;
|
|
175
|
+
|
|
176
|
+
interface FormatOptions {
|
|
177
|
+
pass: boolean;
|
|
178
|
+
method: string;
|
|
179
|
+
isNewSnapshot: boolean;
|
|
180
|
+
paths: {
|
|
181
|
+
baselinePath: string;
|
|
182
|
+
receivedPath: string;
|
|
183
|
+
diffPath: string;
|
|
184
|
+
};
|
|
185
|
+
result: {
|
|
186
|
+
diffCount?: number;
|
|
187
|
+
diffPercentage?: number;
|
|
188
|
+
score?: number;
|
|
189
|
+
};
|
|
190
|
+
threshold: number;
|
|
191
|
+
thresholdType: "pixel" | "percent";
|
|
192
|
+
isSsim: boolean;
|
|
193
|
+
isGmsd: boolean;
|
|
194
|
+
}
|
|
195
|
+
declare function formatMessage(opts: FormatOptions): string;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Main snapshot comparison function
|
|
199
|
+
*/
|
|
200
|
+
declare function getOrCreateSnapshot(received: ImageInput, options: MatcherOptions, testContext: TestContext): Promise<ComparisonResult>;
|
|
201
|
+
/**
|
|
202
|
+
* Compare two images directly without snapshot management
|
|
203
|
+
*/
|
|
204
|
+
declare function compareImages(received: ImageInput, baseline: ImageInput, options: MatcherOptions): Promise<ComparisonResult>;
|
|
205
|
+
|
|
206
|
+
export { type ComparisonMethod, type ComparisonResult, type FormatOptions, type ImageData, type ImageInput, type MatcherOptions, type TestContext, compareImages, fileExists, formatMessage as formatReport, getOrCreateSnapshot, isFilePath, isImageBuffer, loadPNG, normalizeImageInput, runComparison, savePNG, validateMethodSupportsInput };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comparison methods available in blazediff
|
|
3
|
+
*/
|
|
4
|
+
type ComparisonMethod = "bin" | "core" | "ssim" | "msssim" | "hitchhikers-ssim" | "gmsd";
|
|
5
|
+
/**
|
|
6
|
+
* Status of a snapshot operation
|
|
7
|
+
*/
|
|
8
|
+
type SnapshotStatus = "added" | "matched" | "updated" | "failed";
|
|
9
|
+
/**
|
|
10
|
+
* Image input - either a file path or a buffer with dimensions
|
|
11
|
+
*/
|
|
12
|
+
type ImageInput = string | {
|
|
13
|
+
data: Uint8Array | Uint8ClampedArray | Buffer;
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Options for the matcher
|
|
19
|
+
*/
|
|
20
|
+
interface MatcherOptions {
|
|
21
|
+
/** Comparison method to use */
|
|
22
|
+
method: ComparisonMethod;
|
|
23
|
+
/**
|
|
24
|
+
* Failure threshold - number of pixels or percentage difference allowed
|
|
25
|
+
* @default 0
|
|
26
|
+
*/
|
|
27
|
+
failureThreshold?: number;
|
|
28
|
+
/**
|
|
29
|
+
* How to interpret failureThreshold
|
|
30
|
+
* @default 'pixel'
|
|
31
|
+
*/
|
|
32
|
+
failureThresholdType?: "pixel" | "percent";
|
|
33
|
+
/**
|
|
34
|
+
* Directory to store snapshots relative to test file
|
|
35
|
+
* @default '__snapshots__'
|
|
36
|
+
*/
|
|
37
|
+
snapshotsDir?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Custom identifier for the snapshot file
|
|
40
|
+
* If not provided, derived from test name
|
|
41
|
+
*/
|
|
42
|
+
snapshotIdentifier?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Force update snapshots (like running with -u flag)
|
|
45
|
+
* @default false
|
|
46
|
+
*/
|
|
47
|
+
updateSnapshots?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Color difference threshold for core/bin methods (0-1)
|
|
50
|
+
* Lower = more strict
|
|
51
|
+
* @default 0.1
|
|
52
|
+
*/
|
|
53
|
+
threshold?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Enable anti-aliasing detection (bin method)
|
|
56
|
+
* @default false
|
|
57
|
+
*/
|
|
58
|
+
antialiasing?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Include anti-aliased pixels in diff count (core method)
|
|
61
|
+
* @default false
|
|
62
|
+
*/
|
|
63
|
+
includeAA?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Window size for SSIM variants
|
|
66
|
+
* @default 11
|
|
67
|
+
*/
|
|
68
|
+
windowSize?: number;
|
|
69
|
+
/**
|
|
70
|
+
* k1 constant for SSIM
|
|
71
|
+
* @default 0.01
|
|
72
|
+
*/
|
|
73
|
+
k1?: number;
|
|
74
|
+
/**
|
|
75
|
+
* k2 constant for SSIM
|
|
76
|
+
* @default 0.03
|
|
77
|
+
*/
|
|
78
|
+
k2?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Downsample factor for GMSD (0 or 1)
|
|
81
|
+
* @default 0
|
|
82
|
+
*/
|
|
83
|
+
downsample?: 0 | 1;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Result of a comparison operation
|
|
87
|
+
*/
|
|
88
|
+
interface ComparisonResult {
|
|
89
|
+
/** Whether the comparison passed */
|
|
90
|
+
pass: boolean;
|
|
91
|
+
/** Human-readable message describing the result */
|
|
92
|
+
message: string;
|
|
93
|
+
/** Number of different pixels (for pixel-based methods) */
|
|
94
|
+
diffCount?: number;
|
|
95
|
+
/** Percentage of different pixels */
|
|
96
|
+
diffPercentage?: number;
|
|
97
|
+
/** Similarity score (for SSIM/GMSD - 1 = identical for SSIM, 0 = identical for GMSD) */
|
|
98
|
+
score?: number;
|
|
99
|
+
/** Path to baseline snapshot */
|
|
100
|
+
baselinePath?: string;
|
|
101
|
+
/** Path to received image (saved for debugging) */
|
|
102
|
+
receivedPath?: string;
|
|
103
|
+
/** Path to diff visualization */
|
|
104
|
+
diffPath?: string;
|
|
105
|
+
/** Status of the snapshot operation */
|
|
106
|
+
snapshotStatus?: SnapshotStatus;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Context provided by test frameworks
|
|
110
|
+
*/
|
|
111
|
+
interface TestContext {
|
|
112
|
+
/** Absolute path to the test file */
|
|
113
|
+
testPath: string;
|
|
114
|
+
/** Name of the current test */
|
|
115
|
+
testName: string;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Image data with dimensions
|
|
119
|
+
*/
|
|
120
|
+
interface ImageData {
|
|
121
|
+
data: Uint8Array;
|
|
122
|
+
width: number;
|
|
123
|
+
height: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface RunComparisonResult {
|
|
127
|
+
/** Number of different pixels (for pixel-based methods) */
|
|
128
|
+
diffCount?: number;
|
|
129
|
+
/** Percentage of different pixels */
|
|
130
|
+
diffPercentage?: number;
|
|
131
|
+
/** Score for perceptual methods (SSIM: 1=identical, GMSD: 0=identical) */
|
|
132
|
+
score?: number;
|
|
133
|
+
/** Diff visualization output buffer */
|
|
134
|
+
diffOutput?: Uint8Array;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Validate that the comparison method supports the given input type
|
|
138
|
+
*/
|
|
139
|
+
declare function validateMethodSupportsInput(method: ComparisonMethod, input: ImageInput): void;
|
|
140
|
+
/**
|
|
141
|
+
* Run comparison using the specified method
|
|
142
|
+
*/
|
|
143
|
+
declare function runComparison(received: ImageInput, baseline: ImageInput, method: ComparisonMethod, options: MatcherOptions, diffOutputPath?: string): Promise<RunComparisonResult>;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if input is a file path
|
|
147
|
+
*/
|
|
148
|
+
declare function isFilePath(input: ImageInput): input is string;
|
|
149
|
+
/**
|
|
150
|
+
* Check if input is an image buffer with dimensions
|
|
151
|
+
*/
|
|
152
|
+
declare function isImageBuffer(input: ImageInput): input is {
|
|
153
|
+
data: Uint8Array | Uint8ClampedArray | Buffer;
|
|
154
|
+
width: number;
|
|
155
|
+
height: number;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Load a PNG image from file path
|
|
159
|
+
*/
|
|
160
|
+
declare function loadPNG(filePath: string): Promise<ImageData>;
|
|
161
|
+
/**
|
|
162
|
+
* Save image data to a PNG file
|
|
163
|
+
*/
|
|
164
|
+
declare function savePNG(filePath: string, data: Uint8Array | Uint8ClampedArray | Buffer, width: number, height: number): Promise<void>;
|
|
165
|
+
/**
|
|
166
|
+
* Normalize image input to ImageData
|
|
167
|
+
* If input is a file path, loads the image
|
|
168
|
+
* If input is already a buffer, returns it with normalized Uint8Array
|
|
169
|
+
*/
|
|
170
|
+
declare function normalizeImageInput(input: ImageInput): Promise<ImageData>;
|
|
171
|
+
/**
|
|
172
|
+
* Check if a file exists
|
|
173
|
+
*/
|
|
174
|
+
declare function fileExists(filePath: string): boolean;
|
|
175
|
+
|
|
176
|
+
interface FormatOptions {
|
|
177
|
+
pass: boolean;
|
|
178
|
+
method: string;
|
|
179
|
+
isNewSnapshot: boolean;
|
|
180
|
+
paths: {
|
|
181
|
+
baselinePath: string;
|
|
182
|
+
receivedPath: string;
|
|
183
|
+
diffPath: string;
|
|
184
|
+
};
|
|
185
|
+
result: {
|
|
186
|
+
diffCount?: number;
|
|
187
|
+
diffPercentage?: number;
|
|
188
|
+
score?: number;
|
|
189
|
+
};
|
|
190
|
+
threshold: number;
|
|
191
|
+
thresholdType: "pixel" | "percent";
|
|
192
|
+
isSsim: boolean;
|
|
193
|
+
isGmsd: boolean;
|
|
194
|
+
}
|
|
195
|
+
declare function formatMessage(opts: FormatOptions): string;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Main snapshot comparison function
|
|
199
|
+
*/
|
|
200
|
+
declare function getOrCreateSnapshot(received: ImageInput, options: MatcherOptions, testContext: TestContext): Promise<ComparisonResult>;
|
|
201
|
+
/**
|
|
202
|
+
* Compare two images directly without snapshot management
|
|
203
|
+
*/
|
|
204
|
+
declare function compareImages(received: ImageInput, baseline: ImageInput, options: MatcherOptions): Promise<ComparisonResult>;
|
|
205
|
+
|
|
206
|
+
export { type ComparisonMethod, type ComparisonResult, type FormatOptions, type ImageData, type ImageInput, type MatcherOptions, type TestContext, compareImages, fileExists, formatMessage as formatReport, getOrCreateSnapshot, isFilePath, isImageBuffer, loadPNG, normalizeImageInput, runComparison, savePNG, validateMethodSupportsInput };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
'use strict';var fs=require('fs'),path=require('path'),pngjsTransformer=require('@blazediff/pngjs-transformer'),bin=require('@blazediff/bin'),core=require('@blazediff/core'),gmsd=require('@blazediff/gmsd'),ssim=require('@blazediff/ssim'),hitchhikersSsim=require('@blazediff/ssim/hitchhikers-ssim'),msssim=require('@blazediff/ssim/msssim'),a=require('picocolors');function _interopDefault(e){return e&&e.__esModule?e:{default:e}}var a__default=/*#__PURE__*/_interopDefault(a);function h(t){return typeof t=="string"}function B(t){return typeof t=="object"&&t!==null&&"data"in t&&"width"in t&&"height"in t}async function I(t){if(!fs.existsSync(t))throw new Error(`Image file not found: ${t}`);let e=await pngjsTransformer.pngjsTransformer.read(t);return {data:new Uint8Array(e.data),width:e.width,height:e.height}}async function c(t,e,n,r){let i=path.dirname(t);fs.existsSync(i)||fs.mkdirSync(i,{recursive:true}),await pngjsTransformer.pngjsTransformer.write({data:e,width:n,height:r},t);}async function g(t){return h(t)?I(t):{data:new Uint8Array(t.data),width:t.width,height:t.height}}function M(t){return fs.existsSync(t)}function k(t){fs.existsSync(t)||fs.mkdirSync(t,{recursive:true});}async function $(t,e,n,r){if(!h(t))throw new Error("Method 'bin' only supports file paths, but received a buffer. Use method 'core', 'ssim', or 'gmsd' for buffer inputs.");if(!h(e))throw new Error("Method 'bin' only supports file paths for baseline, but received a buffer. Use method 'core', 'ssim', or 'gmsd' for buffer inputs.");let i=await bin.compare(t,e,n,{threshold:r.threshold,antialiasing:r.antialiasing});if(i.match)return {diffCount:0,diffPercentage:0};if(i.reason==="layout-diff")return {diffCount:Number.MAX_SAFE_INTEGER,diffPercentage:100};if(i.reason==="pixel-diff")return {diffCount:i.diffCount,diffPercentage:i.diffPercentage};if(i.reason==="file-not-exists")throw new Error(`Image file not found: ${i.file}`);return {diffCount:0,diffPercentage:0}}function O(t,e,n,r){let{width:i,height:o}=t,m=i*o;if(t.width!==e.width||t.height!==e.height)return {diffCount:m,diffPercentage:100};let u=n?new Uint8Array(m*4):void 0,f=core.diff(t.data,e.data,u,i,o,{threshold:r.threshold??.1,includeAA:r.includeAA??false});return {diffCount:f,diffPercentage:f/m*100,diffOutput:u}}function D(t,e,n,r){let{width:i,height:o}=t,m=i*o;if(t.width!==e.width||t.height!==e.height)return {score:1};let u=n?new Uint8Array(m*4):void 0;return {score:gmsd.gmsd(t.data,e.data,u,i,o,{downsample:r.downsample}),diffOutput:u}}function E(t,e,n,r,i){let{width:o,height:m}=t,u=o*m;if(t.width!==e.width||t.height!==e.height)return {score:0};let f=r?new Uint8Array(u*4):void 0,s={windowSize:i.windowSize,k1:i.k1,k2:i.k2},p;switch(n){case "ssim":p=ssim.ssim(t.data,e.data,f,o,m,s);break;case "msssim":p=msssim.msssim(t.data,e.data,f,o,m,s);break;case "hitchhikers-ssim":p=hitchhikersSsim.hitchhikersSSIM(t.data,e.data,f,o,m,s);break;default:throw new Error(`Unknown SSIM method: ${n}`)}return {score:p,diffOutput:f}}function l(t){return t==="ssim"||t==="msssim"||t==="hitchhikers-ssim"}function A(t,e){if(t==="bin"&&!h(e))throw new Error("Method 'bin' only supports file paths, but received a buffer. Use method 'core', 'ssim', or 'gmsd' for buffer inputs.")}async function P(t,e,n,r,i){if(A(n,t),A(n,e),n==="bin"){let s=await $(t,e,i,r);return {diffCount:s.diffCount,diffPercentage:s.diffPercentage}}let o=await g(t),m=await g(e),u=i!==void 0;if(l(n)){let s=E(o,m,n,u,r);return {score:s.score,diffOutput:s.diffOutput}}if(n==="gmsd"){let s=D(o,m,u,r);return {score:s.score,diffOutput:s.diffOutput}}let f=O(o,m,u,r);return {diffCount:f.diffCount,diffPercentage:f.diffPercentage,diffOutput:f.diffOutput}}var w={success:a__default.default.isColorSupported?"\u2714":"\u221A",error:a__default.default.isColorSupported?"\u2716":"\xD7",info:a__default.default.isColorSupported?"\u2139":"i",arrow:a__default.default.isColorSupported?"\u2514\u2500":"'-"};function R(t){return t.isNewSnapshot?Z(t.paths.baselinePath):t.pass?q():H(t)}function Z(t){return [`${a__default.default.green(w.success)} ${a__default.default.green("New snapshot created")}`,` ${a__default.default.dim(w.arrow)} ${a__default.default.dim(t)}`].join(`
|
|
2
|
+
`)}function q(){return `${a__default.default.green(w.success)} ${a__default.default.green("Image matches snapshot")}`}function H(t){let{method:e,paths:n,result:r,threshold:i,thresholdType:o,isSsim:m,isGmsd:u}=t,f=[`${a__default.default.red(w.error)} ${a__default.default.red(a__default.default.bold("Image snapshot mismatch"))}`,""],s=12;if(f.push(` ${a__default.default.dim("Method".padEnd(s))}${e}`),f.push(` ${a__default.default.dim("Baseline".padEnd(s))}${a__default.default.dim(n.baselinePath)}`),f.push(` ${a__default.default.dim("Received".padEnd(s))}${a__default.default.dim(n.receivedPath)}`),f.push(` ${a__default.default.dim("Diff".padEnd(s))}${a__default.default.dim(n.diffPath)}`),f.push(""),m){let d=r.score??0,S=((1-d)*100).toFixed(2);f.push(` ${a__default.default.dim("SSIM Score".padEnd(s))}${a__default.default.yellow(d.toFixed(4))} ${a__default.default.dim("(1.0 = identical)")}`),f.push(` ${a__default.default.dim("Difference".padEnd(s))}${a__default.default.yellow(S+"%")}`);}else if(u){let d=r.score??0;f.push(` ${a__default.default.dim("GMSD Score".padEnd(s))}${a__default.default.yellow(d.toFixed(4))} ${a__default.default.dim("(0.0 = identical)")}`);}else {let d=r.diffCount??0,S=r.diffPercentage?.toFixed(2)??"0.00";f.push(` ${a__default.default.dim("Difference".padEnd(s))}${a__default.default.yellow(d.toLocaleString())} pixels ${a__default.default.dim(`(${S}%)`)}`);}let p=o==="percent"?"%":"pixels";return f.push(` ${a__default.default.dim("Threshold".padEnd(s))}${i} ${p}`),f.push(""),f.push(` ${a__default.default.cyan(w.info)} ${a__default.default.cyan("Run with --update to update the snapshot")}`),f.join(`
|
|
3
|
+
`)}function Q(t){return t.testName.replace(/[^a-zA-Z0-9-_\s]/g,"").replace(/\s+/g,"-").toLowerCase()||"snapshot"}function V(t,e){let n=path.dirname(t.testPath),r=e.snapshotsDir??"__snapshots__",i=path.isAbsolute(r)?r:path.join(n,r),o=e.snapshotIdentifier??Q(t);return {snapshotDir:i,baselinePath:path.join(i,`${o}.png`),receivedPath:path.join(i,`${o}.received.png`),diffPath:path.join(i,`${o}.diff.png`)}}function F(t,e,n){let r=n.failureThreshold??0,i=n.failureThresholdType??"pixel";if(l(t)){let o=e.score??0;return i==="percent"?(1-o)*100<=r:o>=1-r/100}if(t==="gmsd"){let o=e.score??0;return i==="percent"?o*100<=r:o<=r/100}return i==="percent"?(e.diffPercentage??0)<=r:(e.diffCount??0)<=r}function T(t,e,n,r,i,o){return R({pass:t,method:e,isNewSnapshot:o,paths:i,result:n,threshold:r.failureThreshold??0,thresholdType:r.failureThresholdType??"pixel",isSsim:l(e),isGmsd:e==="gmsd"})}async function Y(t,e,n){let r=V(n,e),{snapshotDir:i,baselinePath:o,receivedPath:m,diffPath:u}=r;k(i);let f=M(o);if(e.updateSnapshots||!f){if(h(t)){let d=await I(t);await c(o,d.data,d.width,d.height);}else await c(o,t.data,t.width,t.height);return fs.existsSync(m)&&fs.unlinkSync(m),fs.existsSync(u)&&fs.unlinkSync(u),{pass:true,message:T(true,e.method,{},e,r,true),baselinePath:o,snapshotStatus:f?"updated":"added"}}let s=await P(t,o,e.method,e,u),p=F(e.method,s,e);if(p)return fs.existsSync(m)&&fs.unlinkSync(m),fs.existsSync(u)&&fs.unlinkSync(u),{pass:true,message:T(p,e.method,s,e,r,false),diffCount:s.diffCount,diffPercentage:s.diffPercentage,score:s.score,baselinePath:o,snapshotStatus:"matched"};if(h(t)){let d=await I(t);await c(m,d.data,d.width,d.height);}else await c(m,t.data,t.width,t.height);if(s.diffOutput){let d=await g(t);await c(u,s.diffOutput,d.width,d.height);}return {pass:false,message:T(p,e.method,s,e,r,false),diffCount:s.diffCount,diffPercentage:s.diffPercentage,score:s.score,baselinePath:o,receivedPath:m,diffPath:u,snapshotStatus:"failed"}}async function v(t,e,n){let r=await P(t,e,n.method,n),i=F(n.method,r,n);return {pass:i,message:i?"Images match.":`Images differ: ${r.diffCount??r.score} ${r.diffCount!==void 0?"pixels":"score"}`,diffCount:r.diffCount,diffPercentage:r.diffPercentage,score:r.score}}exports.compareImages=v;exports.fileExists=M;exports.formatReport=R;exports.getOrCreateSnapshot=Y;exports.isFilePath=h;exports.isImageBuffer=B;exports.loadPNG=I;exports.normalizeImageInput=g;exports.runComparison=P;exports.savePNG=c;exports.validateMethodSupportsInput=A;
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import {existsSync,mkdirSync,unlinkSync}from'fs';import {dirname,isAbsolute,join}from'path';import {pngjsTransformer}from'@blazediff/pngjs-transformer';import {compare}from'@blazediff/bin';import {diff}from'@blazediff/core';import {gmsd}from'@blazediff/gmsd';import {ssim}from'@blazediff/ssim';import {hitchhikersSSIM}from'@blazediff/ssim/hitchhikers-ssim';import {msssim}from'@blazediff/ssim/msssim';import a from'picocolors';function h(t){return typeof t=="string"}function B(t){return typeof t=="object"&&t!==null&&"data"in t&&"width"in t&&"height"in t}async function I(t){if(!existsSync(t))throw new Error(`Image file not found: ${t}`);let e=await pngjsTransformer.read(t);return {data:new Uint8Array(e.data),width:e.width,height:e.height}}async function c(t,e,n,r){let i=dirname(t);existsSync(i)||mkdirSync(i,{recursive:true}),await pngjsTransformer.write({data:e,width:n,height:r},t);}async function g(t){return h(t)?I(t):{data:new Uint8Array(t.data),width:t.width,height:t.height}}function M(t){return existsSync(t)}function k(t){existsSync(t)||mkdirSync(t,{recursive:true});}async function $(t,e,n,r){if(!h(t))throw new Error("Method 'bin' only supports file paths, but received a buffer. Use method 'core', 'ssim', or 'gmsd' for buffer inputs.");if(!h(e))throw new Error("Method 'bin' only supports file paths for baseline, but received a buffer. Use method 'core', 'ssim', or 'gmsd' for buffer inputs.");let i=await compare(t,e,n,{threshold:r.threshold,antialiasing:r.antialiasing});if(i.match)return {diffCount:0,diffPercentage:0};if(i.reason==="layout-diff")return {diffCount:Number.MAX_SAFE_INTEGER,diffPercentage:100};if(i.reason==="pixel-diff")return {diffCount:i.diffCount,diffPercentage:i.diffPercentage};if(i.reason==="file-not-exists")throw new Error(`Image file not found: ${i.file}`);return {diffCount:0,diffPercentage:0}}function O(t,e,n,r){let{width:i,height:o}=t,m=i*o;if(t.width!==e.width||t.height!==e.height)return {diffCount:m,diffPercentage:100};let u=n?new Uint8Array(m*4):void 0,f=diff(t.data,e.data,u,i,o,{threshold:r.threshold??.1,includeAA:r.includeAA??false});return {diffCount:f,diffPercentage:f/m*100,diffOutput:u}}function D(t,e,n,r){let{width:i,height:o}=t,m=i*o;if(t.width!==e.width||t.height!==e.height)return {score:1};let u=n?new Uint8Array(m*4):void 0;return {score:gmsd(t.data,e.data,u,i,o,{downsample:r.downsample}),diffOutput:u}}function E(t,e,n,r,i){let{width:o,height:m}=t,u=o*m;if(t.width!==e.width||t.height!==e.height)return {score:0};let f=r?new Uint8Array(u*4):void 0,s={windowSize:i.windowSize,k1:i.k1,k2:i.k2},p;switch(n){case "ssim":p=ssim(t.data,e.data,f,o,m,s);break;case "msssim":p=msssim(t.data,e.data,f,o,m,s);break;case "hitchhikers-ssim":p=hitchhikersSSIM(t.data,e.data,f,o,m,s);break;default:throw new Error(`Unknown SSIM method: ${n}`)}return {score:p,diffOutput:f}}function l(t){return t==="ssim"||t==="msssim"||t==="hitchhikers-ssim"}function A(t,e){if(t==="bin"&&!h(e))throw new Error("Method 'bin' only supports file paths, but received a buffer. Use method 'core', 'ssim', or 'gmsd' for buffer inputs.")}async function P(t,e,n,r,i){if(A(n,t),A(n,e),n==="bin"){let s=await $(t,e,i,r);return {diffCount:s.diffCount,diffPercentage:s.diffPercentage}}let o=await g(t),m=await g(e),u=i!==void 0;if(l(n)){let s=E(o,m,n,u,r);return {score:s.score,diffOutput:s.diffOutput}}if(n==="gmsd"){let s=D(o,m,u,r);return {score:s.score,diffOutput:s.diffOutput}}let f=O(o,m,u,r);return {diffCount:f.diffCount,diffPercentage:f.diffPercentage,diffOutput:f.diffOutput}}var w={success:a.isColorSupported?"\u2714":"\u221A",error:a.isColorSupported?"\u2716":"\xD7",info:a.isColorSupported?"\u2139":"i",arrow:a.isColorSupported?"\u2514\u2500":"'-"};function R(t){return t.isNewSnapshot?Z(t.paths.baselinePath):t.pass?q():H(t)}function Z(t){return [`${a.green(w.success)} ${a.green("New snapshot created")}`,` ${a.dim(w.arrow)} ${a.dim(t)}`].join(`
|
|
2
|
+
`)}function q(){return `${a.green(w.success)} ${a.green("Image matches snapshot")}`}function H(t){let{method:e,paths:n,result:r,threshold:i,thresholdType:o,isSsim:m,isGmsd:u}=t,f=[`${a.red(w.error)} ${a.red(a.bold("Image snapshot mismatch"))}`,""],s=12;if(f.push(` ${a.dim("Method".padEnd(s))}${e}`),f.push(` ${a.dim("Baseline".padEnd(s))}${a.dim(n.baselinePath)}`),f.push(` ${a.dim("Received".padEnd(s))}${a.dim(n.receivedPath)}`),f.push(` ${a.dim("Diff".padEnd(s))}${a.dim(n.diffPath)}`),f.push(""),m){let d=r.score??0,S=((1-d)*100).toFixed(2);f.push(` ${a.dim("SSIM Score".padEnd(s))}${a.yellow(d.toFixed(4))} ${a.dim("(1.0 = identical)")}`),f.push(` ${a.dim("Difference".padEnd(s))}${a.yellow(S+"%")}`);}else if(u){let d=r.score??0;f.push(` ${a.dim("GMSD Score".padEnd(s))}${a.yellow(d.toFixed(4))} ${a.dim("(0.0 = identical)")}`);}else {let d=r.diffCount??0,S=r.diffPercentage?.toFixed(2)??"0.00";f.push(` ${a.dim("Difference".padEnd(s))}${a.yellow(d.toLocaleString())} pixels ${a.dim(`(${S}%)`)}`);}let p=o==="percent"?"%":"pixels";return f.push(` ${a.dim("Threshold".padEnd(s))}${i} ${p}`),f.push(""),f.push(` ${a.cyan(w.info)} ${a.cyan("Run with --update to update the snapshot")}`),f.join(`
|
|
3
|
+
`)}function Q(t){return t.testName.replace(/[^a-zA-Z0-9-_\s]/g,"").replace(/\s+/g,"-").toLowerCase()||"snapshot"}function V(t,e){let n=dirname(t.testPath),r=e.snapshotsDir??"__snapshots__",i=isAbsolute(r)?r:join(n,r),o=e.snapshotIdentifier??Q(t);return {snapshotDir:i,baselinePath:join(i,`${o}.png`),receivedPath:join(i,`${o}.received.png`),diffPath:join(i,`${o}.diff.png`)}}function F(t,e,n){let r=n.failureThreshold??0,i=n.failureThresholdType??"pixel";if(l(t)){let o=e.score??0;return i==="percent"?(1-o)*100<=r:o>=1-r/100}if(t==="gmsd"){let o=e.score??0;return i==="percent"?o*100<=r:o<=r/100}return i==="percent"?(e.diffPercentage??0)<=r:(e.diffCount??0)<=r}function T(t,e,n,r,i,o){return R({pass:t,method:e,isNewSnapshot:o,paths:i,result:n,threshold:r.failureThreshold??0,thresholdType:r.failureThresholdType??"pixel",isSsim:l(e),isGmsd:e==="gmsd"})}async function Y(t,e,n){let r=V(n,e),{snapshotDir:i,baselinePath:o,receivedPath:m,diffPath:u}=r;k(i);let f=M(o);if(e.updateSnapshots||!f){if(h(t)){let d=await I(t);await c(o,d.data,d.width,d.height);}else await c(o,t.data,t.width,t.height);return existsSync(m)&&unlinkSync(m),existsSync(u)&&unlinkSync(u),{pass:true,message:T(true,e.method,{},e,r,true),baselinePath:o,snapshotStatus:f?"updated":"added"}}let s=await P(t,o,e.method,e,u),p=F(e.method,s,e);if(p)return existsSync(m)&&unlinkSync(m),existsSync(u)&&unlinkSync(u),{pass:true,message:T(p,e.method,s,e,r,false),diffCount:s.diffCount,diffPercentage:s.diffPercentage,score:s.score,baselinePath:o,snapshotStatus:"matched"};if(h(t)){let d=await I(t);await c(m,d.data,d.width,d.height);}else await c(m,t.data,t.width,t.height);if(s.diffOutput){let d=await g(t);await c(u,s.diffOutput,d.width,d.height);}return {pass:false,message:T(p,e.method,s,e,r,false),diffCount:s.diffCount,diffPercentage:s.diffPercentage,score:s.score,baselinePath:o,receivedPath:m,diffPath:u,snapshotStatus:"failed"}}async function v(t,e,n){let r=await P(t,e,n.method,n),i=F(n.method,r,n);return {pass:i,message:i?"Images match.":`Images differ: ${r.diffCount??r.score} ${r.diffCount!==void 0?"pixels":"score"}`,diffCount:r.diffCount,diffPercentage:r.diffPercentage,score:r.score}}export{v as compareImages,M as fileExists,R as formatReport,Y as getOrCreateSnapshot,h as isFilePath,B as isImageBuffer,I as loadPNG,g as normalizeImageInput,P as runComparison,c as savePNG,A as validateMethodSupportsInput};
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@blazediff/matcher",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Core matcher logic for visual regression testing with blazediff",
|
|
5
|
+
"private": false,
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"module": "dist/index.mjs",
|
|
11
|
+
"types": "dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.mjs",
|
|
16
|
+
"require": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"image",
|
|
24
|
+
"comparison",
|
|
25
|
+
"diff",
|
|
26
|
+
"visual-testing",
|
|
27
|
+
"matcher",
|
|
28
|
+
"snapshot"
|
|
29
|
+
],
|
|
30
|
+
"author": "Teimur Gasanov <me@teimurjan.dev> (https://github.com/teimurjan)",
|
|
31
|
+
"repository": "https://github.com/teimurjan/blazediff",
|
|
32
|
+
"homepage": "https://blazediff.dev",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"picocolors": "^1.1.1",
|
|
36
|
+
"@blazediff/bin": "3.1.0",
|
|
37
|
+
"@blazediff/core": "1.9.0",
|
|
38
|
+
"@blazediff/gmsd": "1.7.0",
|
|
39
|
+
"@blazediff/pngjs-transformer": "2.1.0",
|
|
40
|
+
"@blazediff/ssim": "1.7.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^24.3.0",
|
|
44
|
+
"@types/pngjs": "^6.0.5",
|
|
45
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
46
|
+
"tsup": "8.5.0",
|
|
47
|
+
"typescript": "5.9.2",
|
|
48
|
+
"vitest": "^3.2.4"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"build": "tsup",
|
|
53
|
+
"dev": "tsup --watch",
|
|
54
|
+
"clean": "rm -rf dist",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"test:watch": "vitest",
|
|
57
|
+
"test:coverage": "vitest run --coverage"
|
|
58
|
+
}
|
|
59
|
+
}
|