@vizzly-testing/honeydiff 0.0.2 β 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -2,28 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@vizzly-testing/honeydiff)
|
|
4
4
|
[](https://www.npmjs.com/package/@vizzly-testing/honeydiff)
|
|
5
|
-
[](../../LICENSE)
|
|
5
|
+
[](https://github.com/vizzly-testing/honeydiff/blob/main/LICENSE)
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
Blazingly fast image comparison for Node.js. Perfect for visual regression testing, screenshot
|
|
8
|
+
validation, and detecting visual changes.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
- **β‘ Async + Sync APIs** - Choose between non-blocking promises or synchronous operations
|
|
17
|
-
- **π― Full TypeScript Support** - Complete type definitions included
|
|
18
|
-
- **π Advanced Analysis**:
|
|
19
|
-
- Anti-aliasing detection
|
|
20
|
-
- SSIM perceptual similarity scoring
|
|
21
|
-
- Spatial clustering of diff regions
|
|
22
|
-
- Intensity statistics (min, max, mean, median, std dev)
|
|
23
|
-
- Bounding box detection
|
|
24
|
-
- **πΈ Variable Height Support** - Compare scrollable screenshots with different heights
|
|
25
|
-
- **π¨ Visual Artifacts** - Generate diff, mask, and overlay images
|
|
26
|
-
- **βοΈ Flexible Options** - Pixel tolerance, color thresholds, custom diff colors
|
|
10
|
+
**Why Honeydiff?**
|
|
11
|
+
- **Fast:** Native performance processing 230M+ pixels/sec
|
|
12
|
+
- **Smart:** Intelligent anti-aliasing detection filters rendering artifacts
|
|
13
|
+
- **Flexible:** File paths or Buffers, async or sync APIs
|
|
14
|
+
- **Complete:** TypeScript types, pre-built binaries for all platforms
|
|
15
|
+
- **Advanced:** SSIM perceptual scoring, spatial clustering, variable-height screenshots
|
|
27
16
|
|
|
28
17
|
## Installation
|
|
29
18
|
|
|
@@ -31,25 +20,18 @@ Blazing fast image comparison with support for anti-aliasing detection, perceptu
|
|
|
31
20
|
npm install @vizzly-testing/honeydiff
|
|
32
21
|
```
|
|
33
22
|
|
|
34
|
-
**Requirements:**
|
|
35
|
-
- Node.js 22+
|
|
36
|
-
|
|
37
|
-
**Pre-built Binaries:**
|
|
38
|
-
This package ships with pre-built native binaries for:
|
|
39
|
-
- macOS (Apple Silicon arm64)
|
|
40
|
-
- Linux (x64 and arm64)
|
|
41
|
-
- Windows (x64)
|
|
23
|
+
**Requirements:** Node.js 22+
|
|
42
24
|
|
|
43
|
-
|
|
25
|
+
Pre-built binaries included for macOS (ARM64), Linux (x64/ARM64), and Windows (x64). No compilation
|
|
26
|
+
required.
|
|
44
27
|
|
|
45
28
|
## Quick Start
|
|
46
29
|
|
|
47
|
-
|
|
48
|
-
|
|
30
|
+
**JavaScript**
|
|
49
31
|
```javascript
|
|
50
32
|
const { compare, quickCompare } = require('@vizzly-testing/honeydiff');
|
|
51
33
|
|
|
52
|
-
// Quick
|
|
34
|
+
// Quick check
|
|
53
35
|
if (await quickCompare('baseline.png', 'current.png')) {
|
|
54
36
|
console.log('Visual regression detected!');
|
|
55
37
|
}
|
|
@@ -60,112 +42,64 @@ const result = await compare('baseline.png', 'current.png', {
|
|
|
60
42
|
includeSSIM: true
|
|
61
43
|
});
|
|
62
44
|
|
|
63
|
-
console.log(`Diff: ${result.diffPercentage.toFixed(2)}
|
|
64
|
-
|
|
45
|
+
console.log(`Diff: ${result.diffPercentage.toFixed(2)}%, SSIM: ${result.perceptualScore}`);
|
|
46
|
+
// Output: Diff: 3.55%, SSIM: 0.9234
|
|
65
47
|
```
|
|
66
48
|
|
|
67
|
-
|
|
49
|
+
**TypeScript**
|
|
68
50
|
|
|
69
51
|
```typescript
|
|
70
52
|
import { compare, CompareOptions, DiffResult } from '@vizzly-testing/honeydiff';
|
|
71
53
|
|
|
72
|
-
const
|
|
54
|
+
const result: DiffResult = await compare('img1.png', 'img2.png', {
|
|
73
55
|
pixelTolerance: 5,
|
|
74
56
|
includeSSIM: true,
|
|
75
57
|
includeClusters: true
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const result: DiffResult = await compare('img1.png', 'img2.png', options);
|
|
58
|
+
});
|
|
79
59
|
|
|
80
60
|
if (result.diffClusters) {
|
|
81
61
|
console.log(`Found ${result.diffClusters.length} diff regions`);
|
|
82
62
|
}
|
|
63
|
+
// Output: Found 3 diff regions
|
|
83
64
|
```
|
|
84
65
|
|
|
85
|
-
## API
|
|
86
|
-
|
|
87
|
-
### Async API (Recommended)
|
|
88
|
-
|
|
89
|
-
#### `compare(img1, img2, options?): Promise<DiffResult>`
|
|
90
|
-
|
|
91
|
-
Compare two images asynchronously. Non-blocking operation.
|
|
92
|
-
|
|
93
|
-
**Parameters:**
|
|
94
|
-
- `img1`: `string | Buffer` - First image (file path or Buffer)
|
|
95
|
-
- `img2`: `string | Buffer` - Second image (file path or Buffer)
|
|
96
|
-
- `options`: `CompareOptions` (optional) - Comparison configuration
|
|
97
|
-
|
|
98
|
-
**Returns:** `Promise<DiffResult>` - Detailed diff results
|
|
99
|
-
|
|
100
|
-
**Example:**
|
|
101
|
-
```javascript
|
|
102
|
-
const result = await compare('baseline.png', 'current.png', {
|
|
103
|
-
pixelTolerance: 10,
|
|
104
|
-
antialiasing: true,
|
|
105
|
-
includeSSIM: true,
|
|
106
|
-
diffPath: '/tmp/diff.png'
|
|
107
|
-
});
|
|
108
|
-
```
|
|
66
|
+
## API
|
|
109
67
|
|
|
110
|
-
|
|
68
|
+
**Async (recommended)**
|
|
69
|
+
- `compare(img1, img2, options?): Promise<DiffResult>` - Full comparison
|
|
70
|
+
- `quickCompare(img1, img2): Promise<boolean>` - Quick check
|
|
111
71
|
|
|
112
|
-
|
|
72
|
+
**Sync (blocks event loop)**
|
|
73
|
+
- `compareSync(img1, img2, options?): DiffResult`
|
|
74
|
+
- `quickCompareSync(img1, img2): boolean`
|
|
113
75
|
|
|
114
|
-
|
|
76
|
+
Inputs: `string` (file path) or `Buffer`
|
|
115
77
|
|
|
116
|
-
|
|
78
|
+
## Options & Results
|
|
117
79
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
Synchronous comparison (blocks event loop). Use only when necessary.
|
|
121
|
-
|
|
122
|
-
#### `quickCompareSync(img1, img2): boolean`
|
|
123
|
-
|
|
124
|
-
Synchronous quick comparison returning a boolean.
|
|
125
|
-
|
|
126
|
-
## Options
|
|
80
|
+
<details>
|
|
81
|
+
<summary><strong>CompareOptions</strong></summary>
|
|
127
82
|
|
|
128
83
|
```typescript
|
|
129
84
|
interface CompareOptions {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
/** Max differences to find before stopping (default: unlimited) */
|
|
143
|
-
maxDiffs?: number;
|
|
144
|
-
|
|
145
|
-
/** Include detailed pixel list (default: false) */
|
|
146
|
-
includeDiffPixels?: boolean;
|
|
147
|
-
|
|
148
|
-
/** Include spatial clusters (default: false) */
|
|
149
|
-
includeClusters?: boolean;
|
|
150
|
-
|
|
151
|
-
/** Calculate SSIM score (default: false) */
|
|
152
|
-
includeSSIM?: boolean;
|
|
153
|
-
|
|
154
|
-
/** Path to save diff image */
|
|
155
|
-
diffPath?: string;
|
|
156
|
-
|
|
157
|
-
/** Path to save binary mask */
|
|
158
|
-
maskPath?: string;
|
|
159
|
-
|
|
160
|
-
/** Path to save side-by-side overlay */
|
|
161
|
-
overlayPath?: string;
|
|
162
|
-
|
|
163
|
-
/** Overwrite existing files (default: false) */
|
|
164
|
-
overwrite?: boolean;
|
|
85
|
+
pixelTolerance?: number; // 0-255 (default: 0)
|
|
86
|
+
colorThreshold?: number; // 0.0-1.0 for YIQ mode (default: 0.0 = RGB)
|
|
87
|
+
antialiasing?: boolean; // Enable AA detection (default: true)
|
|
88
|
+
ignoreColors?: boolean; // Brightness only (default: false)
|
|
89
|
+
maxDiffs?: number; // Stop after N diffs (default: unlimited)
|
|
90
|
+
includeDiffPixels?: boolean; // Detailed pixel list (default: false)
|
|
91
|
+
includeClusters?: boolean; // Spatial clusters (default: false)
|
|
92
|
+
includeSSIM?: boolean; // SSIM score (default: false)
|
|
93
|
+
diffPath?: string; // Save diff image
|
|
94
|
+
maskPath?: string; // Save mask image
|
|
95
|
+
overlayPath?: string; // Save overlay image
|
|
96
|
+
overwrite?: boolean; // Overwrite files (default: false)
|
|
165
97
|
}
|
|
166
98
|
```
|
|
99
|
+
</details>
|
|
167
100
|
|
|
168
|
-
|
|
101
|
+
<details>
|
|
102
|
+
<summary><strong>DiffResult</strong></summary>
|
|
169
103
|
|
|
170
104
|
```typescript
|
|
171
105
|
interface DiffResult {
|
|
@@ -180,148 +114,172 @@ interface DiffResult {
|
|
|
180
114
|
diffPixelsList: DiffPixel[] | null;
|
|
181
115
|
diffClusters: DiffCluster[] | null;
|
|
182
116
|
intensityStats: IntensityStats | null;
|
|
183
|
-
perceptualScore: number | null;
|
|
117
|
+
perceptualScore: number | null; // SSIM 0.0-1.0
|
|
184
118
|
}
|
|
185
119
|
```
|
|
120
|
+
</details>
|
|
186
121
|
|
|
187
|
-
##
|
|
188
|
-
|
|
189
|
-
### With Buffers
|
|
122
|
+
## Examples
|
|
190
123
|
|
|
124
|
+
**Buffers**
|
|
191
125
|
```javascript
|
|
192
126
|
const fs = require('fs');
|
|
193
127
|
const { compare } = require('@vizzly-testing/honeydiff');
|
|
194
128
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const result = await compare(img1, img2, {
|
|
199
|
-
pixelTolerance: 5
|
|
200
|
-
});
|
|
129
|
+
let img1 = fs.readFileSync('image1.png');
|
|
130
|
+
let img2 = fs.readFileSync('image2.png');
|
|
131
|
+
let result = await compare(img1, img2, { pixelTolerance: 5 });
|
|
201
132
|
```
|
|
202
133
|
|
|
203
|
-
|
|
134
|
+
**Diff Images**
|
|
204
135
|
|
|
205
136
|
```javascript
|
|
206
|
-
|
|
207
|
-
diffPath: './
|
|
208
|
-
maskPath: './
|
|
209
|
-
overlayPath: './
|
|
137
|
+
let result = await compare('baseline.png', 'current.png', {
|
|
138
|
+
diffPath: './diff.png',
|
|
139
|
+
maskPath: './mask.png',
|
|
140
|
+
overlayPath: './overlay.png',
|
|
210
141
|
overwrite: true
|
|
211
142
|
});
|
|
212
|
-
|
|
213
|
-
console.log(`Diff images saved with ${result.diffPercentage}% difference`);
|
|
214
143
|
```
|
|
215
144
|
|
|
216
|
-
|
|
217
|
-
|
|
145
|
+
**Clustering & Metrics**
|
|
218
146
|
```javascript
|
|
219
|
-
|
|
220
|
-
includeClusters: true
|
|
147
|
+
let result = await compare('img1.png', 'img2.png', {
|
|
148
|
+
includeClusters: true,
|
|
149
|
+
includeSSIM: true,
|
|
150
|
+
includeDiffPixels: true
|
|
221
151
|
});
|
|
222
152
|
|
|
153
|
+
// Spatial clusters
|
|
223
154
|
if (result.diffClusters) {
|
|
224
|
-
result.diffClusters.forEach((
|
|
225
|
-
console.log(`
|
|
226
|
-
console.log(` Pixels: ${cluster.pixelCount}`);
|
|
227
|
-
console.log(` Center: (${cluster.centerOfMass[0]}, ${cluster.centerOfMass[1]})`);
|
|
228
|
-
console.log(` Bounds: ${cluster.boundingBox.width}x${cluster.boundingBox.height}`);
|
|
155
|
+
result.diffClusters.forEach((c, i) => {
|
|
156
|
+
console.log(`Region ${i + 1}: ${c.pixelCount} pixels at (${c.centerOfMass[0]}, ${c.centerOfMass[1]})`);
|
|
229
157
|
});
|
|
230
158
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
```javascript
|
|
236
|
-
const result = await compare('img1.png', 'img2.png', {
|
|
237
|
-
includeSSIM: true
|
|
238
|
-
});
|
|
159
|
+
// Output:
|
|
160
|
+
// Region 1: 12450 pixels at (245, 189)
|
|
161
|
+
// Region 2: 3891 pixels at (672, 401)
|
|
162
|
+
// Region 3: 1023 pixels at (1156, 823)
|
|
239
163
|
|
|
164
|
+
// SSIM score
|
|
240
165
|
if (result.perceptualScore !== null) {
|
|
241
|
-
|
|
242
|
-
console.log('Perceptually very similar');
|
|
243
|
-
} else if (result.perceptualScore > 0.80) {
|
|
244
|
-
console.log('Somewhat similar');
|
|
245
|
-
} else {
|
|
246
|
-
console.log('Perceptually different');
|
|
247
|
-
}
|
|
166
|
+
console.log(`SSIM: ${result.perceptualScore.toFixed(3)}`);
|
|
248
167
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
### Intensity Statistics
|
|
252
|
-
|
|
253
|
-
```javascript
|
|
254
|
-
const result = await compare('img1.png', 'img2.png', {
|
|
255
|
-
includeDiffPixels: true // Enables intensity stats
|
|
256
|
-
});
|
|
168
|
+
// Output: SSIM: 0.923
|
|
257
169
|
|
|
170
|
+
// Intensity stats
|
|
258
171
|
if (result.intensityStats) {
|
|
259
|
-
console.log(`
|
|
260
|
-
console.log(`Mean: ${result.intensityStats.mean.toFixed(1)}`);
|
|
261
|
-
console.log(`Median: ${result.intensityStats.median}`);
|
|
262
|
-
console.log(`Std Dev: ${result.intensityStats.stdDev.toFixed(1)}`);
|
|
172
|
+
console.log(`Range: ${result.intensityStats.min}-${result.intensityStats.max}, Mean: ${result.intensityStats.mean.toFixed(1)}`);
|
|
263
173
|
}
|
|
174
|
+
// Output: Range: 12-255, Mean: 87.3
|
|
264
175
|
```
|
|
265
176
|
|
|
266
|
-
|
|
177
|
+
**Complete Result Example**
|
|
178
|
+
```javascript
|
|
179
|
+
let result = await compare('baseline.png', 'current.png', {
|
|
180
|
+
pixelTolerance: 10,
|
|
181
|
+
includeClusters: true
|
|
182
|
+
});
|
|
267
183
|
|
|
268
|
-
|
|
184
|
+
console.log(JSON.stringify(result, null, 2));
|
|
185
|
+
```
|
|
269
186
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
187
|
+
<details>
|
|
188
|
+
<summary>Example output (click to expand)</summary>
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"isDifferent": true,
|
|
193
|
+
"diffPercentage": 3.55,
|
|
194
|
+
"totalPixels": 18309000,
|
|
195
|
+
"diffPixels": 650086,
|
|
196
|
+
"aaPixelsIgnored": 1952904,
|
|
197
|
+
"aaPercentage": 10.67,
|
|
198
|
+
"boundingBox": {
|
|
199
|
+
"x": 124,
|
|
200
|
+
"y": 89,
|
|
201
|
+
"width": 1456,
|
|
202
|
+
"height": 2301
|
|
203
|
+
},
|
|
204
|
+
"heightDiff": {
|
|
205
|
+
"height1": 24162,
|
|
206
|
+
"height2": 24412,
|
|
207
|
+
"extraPixels": 187500
|
|
208
|
+
},
|
|
209
|
+
"diffPixelsList": null,
|
|
210
|
+
"diffClusters": [
|
|
211
|
+
{
|
|
212
|
+
"pixelCount": 12450,
|
|
213
|
+
"centerOfMass": [245, 189]
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"pixelCount": 3891,
|
|
217
|
+
"centerOfMass": [672, 401]
|
|
218
|
+
}
|
|
219
|
+
],
|
|
220
|
+
"intensityStats": {
|
|
221
|
+
"min": 12,
|
|
222
|
+
"max": 255,
|
|
223
|
+
"mean": 87.3,
|
|
224
|
+
"median": 76,
|
|
225
|
+
"stdDev": 45.2
|
|
226
|
+
},
|
|
227
|
+
"perceptualScore": null
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
</details>
|
|
231
|
+
```
|
|
274
232
|
|
|
275
|
-
|
|
233
|
+
## Use Cases
|
|
276
234
|
|
|
277
|
-
|
|
235
|
+
- **Visual Regression Testing:** Detect unintended UI changes in your test suite
|
|
236
|
+
- **Screenshot Validation:** Compare generated screenshots against baselines
|
|
237
|
+
- **Design QA:** Validate UI implementations match designs
|
|
238
|
+
- **Cross-browser Testing:** Detect rendering differences across browsers
|
|
239
|
+
- **Scrollable Content:** Compare full-page screenshots with variable heights
|
|
278
240
|
|
|
279
|
-
|
|
241
|
+
## Performance
|
|
280
242
|
|
|
281
|
-
|
|
282
|
-
const result = await compare('short.png', 'tall.png');
|
|
243
|
+
Tested on real-world tall screenshots (750 x 24,162 pixels = 18.3M pixels):
|
|
283
244
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
```
|
|
245
|
+
| Operation | Time | Throughput |
|
|
246
|
+
|-----------|------|------------|
|
|
247
|
+
| Basic comparison | ~79ms | 231.8M pixels/sec |
|
|
248
|
+
| With artifacts (diff/mask/overlay) | ~432ms | 42.4M pixels/sec |
|
|
289
249
|
|
|
290
|
-
|
|
250
|
+
**6x faster than odiff** on large screenshots with excellent multi-core parallelization.
|
|
291
251
|
|
|
292
|
-
|
|
252
|
+
*Benchmarked on Apple M-series Mac.*
|
|
293
253
|
|
|
294
|
-
|
|
295
|
-
- Rust toolchain (install from https://rustup.rs)
|
|
296
|
-
- Node.js 22+
|
|
254
|
+
## Advanced Features
|
|
297
255
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
cd honeydiff/crates/honeydiff-node
|
|
256
|
+
**Variable Height Screenshots**
|
|
257
|
+
Compare scrollable content with different heights. Overlapping regions are compared pixel-by-pixel,
|
|
258
|
+
extra rows counted as differences.
|
|
302
259
|
|
|
303
|
-
|
|
304
|
-
|
|
260
|
+
**Color Spaces**
|
|
261
|
+
- RGB mode (default): Exact matching, fastest performance
|
|
262
|
+
- YIQ mode: Perceptual matching weighted for human vision
|
|
305
263
|
|
|
306
|
-
|
|
307
|
-
|
|
264
|
+
**Spatial Clustering**
|
|
265
|
+
Group nearby differences into regions using 8-connectivity clustering. Useful for identifying distinct changed areas.
|
|
308
266
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
```
|
|
267
|
+
**SSIM Scoring**
|
|
268
|
+
Structural Similarity Index (0.0-1.0) provides perceptual similarity measurement beyond pixel differences.
|
|
312
269
|
|
|
313
|
-
|
|
270
|
+
## Troubleshooting
|
|
314
271
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
npm run cross-build
|
|
318
|
-
```
|
|
272
|
+
**Module not found errors**
|
|
273
|
+
Ensure you're on Node.js 22+ and your platform is supported (macOS ARM64, Linux x64/ARM64, Windows x64).
|
|
319
274
|
|
|
320
|
-
|
|
275
|
+
**Large memory usage with `includeDiffPixels`**
|
|
276
|
+
This option stores every different pixel's position and intensity. Disable for large images with many differences.
|
|
321
277
|
|
|
322
|
-
|
|
323
|
-
|
|
278
|
+
**Slow performance with `includeSSIM`**
|
|
279
|
+
SSIM calculation is O(width Γ height Γ windowΒ²). Only enable when perceptual scoring is needed.
|
|
324
280
|
|
|
325
281
|
## License
|
|
326
282
|
|
|
327
|
-
MIT
|
|
283
|
+
MIT License
|
|
284
|
+
|
|
285
|
+
Built with native Rust for maximum performance.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vizzly-testing/honeydiff",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "High-performance image diffing for Node.js - Native bindings to Honeydiff Rust library",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -45,6 +45,16 @@
|
|
|
45
45
|
"index.d.ts",
|
|
46
46
|
"load-platform.js",
|
|
47
47
|
"platforms/*.node",
|
|
48
|
+
"README.md",
|
|
49
|
+
"index.js",
|
|
50
|
+
"index.d.ts",
|
|
51
|
+
"load-platform.js",
|
|
52
|
+
"platforms/*.node",
|
|
53
|
+
"README.md",
|
|
54
|
+
"index.js",
|
|
55
|
+
"index.d.ts",
|
|
56
|
+
"load-platform.js",
|
|
57
|
+
"platforms/*.node",
|
|
48
58
|
"README.md"
|
|
49
59
|
],
|
|
50
60
|
"engines": {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|