cross-image 0.2.4 → 0.4.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 +21 -21
- package/README.md +615 -333
- package/esm/mod.d.ts +6 -4
- package/esm/mod.js +4 -2
- package/esm/src/formats/apng.d.ts +7 -5
- package/esm/src/formats/apng.js +15 -10
- package/esm/src/formats/ascii.d.ts +3 -3
- package/esm/src/formats/ascii.js +1 -1
- package/esm/src/formats/avif.d.ts +3 -3
- package/esm/src/formats/avif.js +17 -7
- package/esm/src/formats/bmp.d.ts +3 -3
- package/esm/src/formats/bmp.js +2 -2
- package/esm/src/formats/dng.d.ts +1 -1
- package/esm/src/formats/dng.js +1 -1
- package/esm/src/formats/gif.d.ts +5 -5
- package/esm/src/formats/gif.js +17 -13
- package/esm/src/formats/heic.d.ts +3 -3
- package/esm/src/formats/heic.js +17 -7
- package/esm/src/formats/ico.d.ts +3 -3
- package/esm/src/formats/ico.js +4 -4
- package/esm/src/formats/jpeg.d.ts +3 -3
- package/esm/src/formats/jpeg.js +23 -11
- package/esm/src/formats/pam.d.ts +3 -3
- package/esm/src/formats/pam.js +2 -2
- package/esm/src/formats/pcx.d.ts +3 -3
- package/esm/src/formats/pcx.js +2 -2
- package/esm/src/formats/png.d.ts +4 -3
- package/esm/src/formats/png.js +9 -3
- package/esm/src/formats/png_base.d.ts +42 -1
- package/esm/src/formats/png_base.js +200 -10
- package/esm/src/formats/ppm.d.ts +3 -3
- package/esm/src/formats/ppm.js +2 -2
- package/esm/src/formats/tiff.d.ts +7 -18
- package/esm/src/formats/tiff.js +162 -27
- package/esm/src/formats/webp.d.ts +3 -3
- package/esm/src/formats/webp.js +11 -8
- package/esm/src/image.d.ts +26 -3
- package/esm/src/image.js +66 -22
- package/esm/src/types.d.ts +122 -4
- package/esm/src/utils/base64.d.ts +32 -0
- package/esm/src/utils/base64.js +173 -0
- package/esm/src/utils/gif_decoder.d.ts +4 -1
- package/esm/src/utils/gif_decoder.js +91 -65
- package/esm/src/utils/gif_encoder.d.ts +3 -1
- package/esm/src/utils/gif_encoder.js +4 -2
- package/esm/src/utils/image_processing.d.ts +31 -0
- package/esm/src/utils/image_processing.js +232 -70
- package/esm/src/utils/jpeg_decoder.d.ts +17 -4
- package/esm/src/utils/jpeg_decoder.js +448 -83
- package/esm/src/utils/jpeg_encoder.d.ts +15 -1
- package/esm/src/utils/jpeg_encoder.js +263 -24
- package/esm/src/utils/resize.js +51 -20
- package/esm/src/utils/tiff_deflate.d.ts +18 -0
- package/esm/src/utils/tiff_deflate.js +27 -0
- package/esm/src/utils/tiff_packbits.d.ts +24 -0
- package/esm/src/utils/tiff_packbits.js +90 -0
- package/esm/src/utils/webp_decoder.d.ts +3 -1
- package/esm/src/utils/webp_decoder.js +144 -63
- package/esm/src/utils/webp_encoder.js +5 -11
- package/package.json +1 -1
- package/script/mod.d.ts +6 -4
- package/script/mod.js +13 -3
- package/script/src/formats/apng.d.ts +7 -5
- package/script/src/formats/apng.js +15 -10
- package/script/src/formats/ascii.d.ts +3 -3
- package/script/src/formats/ascii.js +1 -1
- package/script/src/formats/avif.d.ts +3 -3
- package/script/src/formats/avif.js +17 -7
- package/script/src/formats/bmp.d.ts +3 -3
- package/script/src/formats/bmp.js +2 -2
- package/script/src/formats/dng.d.ts +1 -1
- package/script/src/formats/dng.js +1 -1
- package/script/src/formats/gif.d.ts +5 -5
- package/script/src/formats/gif.js +17 -13
- package/script/src/formats/heic.d.ts +3 -3
- package/script/src/formats/heic.js +17 -7
- package/script/src/formats/ico.d.ts +3 -3
- package/script/src/formats/ico.js +4 -4
- package/script/src/formats/jpeg.d.ts +3 -3
- package/script/src/formats/jpeg.js +23 -11
- package/script/src/formats/pam.d.ts +3 -3
- package/script/src/formats/pam.js +2 -2
- package/script/src/formats/pcx.d.ts +3 -3
- package/script/src/formats/pcx.js +2 -2
- package/script/src/formats/png.d.ts +4 -3
- package/script/src/formats/png.js +9 -3
- package/script/src/formats/png_base.d.ts +42 -1
- package/script/src/formats/png_base.js +200 -10
- package/script/src/formats/ppm.d.ts +3 -3
- package/script/src/formats/ppm.js +2 -2
- package/script/src/formats/tiff.d.ts +7 -18
- package/script/src/formats/tiff.js +162 -27
- package/script/src/formats/webp.d.ts +3 -3
- package/script/src/formats/webp.js +11 -8
- package/script/src/image.d.ts +26 -3
- package/script/src/image.js +64 -20
- package/script/src/types.d.ts +122 -4
- package/script/src/utils/base64.d.ts +32 -0
- package/script/src/utils/base64.js +179 -0
- package/script/src/utils/gif_decoder.d.ts +4 -1
- package/script/src/utils/gif_decoder.js +91 -65
- package/script/src/utils/gif_encoder.d.ts +3 -1
- package/script/src/utils/gif_encoder.js +4 -2
- package/script/src/utils/image_processing.d.ts +31 -0
- package/script/src/utils/image_processing.js +236 -70
- package/script/src/utils/jpeg_decoder.d.ts +17 -4
- package/script/src/utils/jpeg_decoder.js +448 -83
- package/script/src/utils/jpeg_encoder.d.ts +15 -1
- package/script/src/utils/jpeg_encoder.js +263 -24
- package/script/src/utils/resize.js +51 -20
- package/script/src/utils/tiff_deflate.d.ts +18 -0
- package/script/src/utils/tiff_deflate.js +31 -0
- package/script/src/utils/tiff_packbits.d.ts +24 -0
- package/script/src/utils/tiff_packbits.js +94 -0
- package/script/src/utils/webp_decoder.d.ts +3 -1
- package/script/src/utils/webp_decoder.js +144 -63
- package/script/src/utils/webp_encoder.js +5 -11
package/README.md
CHANGED
|
@@ -1,333 +1,615 @@
|
|
|
1
|
-
# @cross/image
|
|
2
|
-
|
|
3
|
-
A pure JavaScript, dependency-free, cross-runtime image processing library for
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.
|
|
75
|
-
.
|
|
76
|
-
.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
|
123
|
-
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
**
|
|
161
|
-
|
|
162
|
-
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
-
|
|
171
|
-
|
|
172
|
-
-
|
|
173
|
-
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const data = await Deno.readFile("photo.jpg");
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
Use
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
1
|
+
# @cross/image
|
|
2
|
+
|
|
3
|
+
A pure JavaScript, dependency-free, cross-runtime image processing library for Deno, Node.js, and
|
|
4
|
+
Bun. Decode, encode, manipulate, and process images in multiple formats including PNG, JPEG, WebP,
|
|
5
|
+
GIF, and more—all without native dependencies.
|
|
6
|
+
|
|
7
|
+
📚 **[Full Documentation](https://cross-image.56k.guru/)**
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 🚀 **Pure JavaScript** - No native dependencies
|
|
12
|
+
- 🔌 **Pluggable formats** - Easy to extend with custom formats
|
|
13
|
+
- 📦 **Cross-runtime** - Works on Deno, Node.js (18+), and Bun
|
|
14
|
+
- 🎨 **Multiple formats** - PNG, APNG, JPEG, WebP, GIF, TIFF, BMP, ICO, DNG, PAM, PPM, PCX, ASCII,
|
|
15
|
+
HEIC, and AVIF support
|
|
16
|
+
- ✂️ **Image manipulation** - Resize, crop, composite, and more
|
|
17
|
+
- 🎛️ **Image processing** - Chainable filters including `brightness`, `contrast`, `saturation`,
|
|
18
|
+
`hue`, `exposure`, `blur`, `sharpen`, `sepia`, and more
|
|
19
|
+
- 🖌️ **Drawing operations** - Create, fill, and manipulate pixels
|
|
20
|
+
- 🧩 **Multi-frame** - Decode/encode animated GIFs, APNGs and multi-page TIFFs
|
|
21
|
+
- 🔧 **Simple API** - Easy to use, intuitive interface
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
### Deno
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { Image } from "jsr:@cross/image";
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Node.js
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install cross-image
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { Image } from "cross-image";
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Bun
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install cross-image
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { Image } from "cross-image";
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
### Deno
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { Image } from "jsr:@cross/image";
|
|
57
|
+
|
|
58
|
+
// Decode an image (auto-detects format)
|
|
59
|
+
const data = await Deno.readFile("input.png");
|
|
60
|
+
const image = await Image.decode(data);
|
|
61
|
+
|
|
62
|
+
console.log(`Image size: ${image.width}x${image.height}`);
|
|
63
|
+
|
|
64
|
+
// Create a new blank image
|
|
65
|
+
const canvas = Image.create(800, 600, 255, 255, 255); // white background
|
|
66
|
+
|
|
67
|
+
// Composite the loaded image on top
|
|
68
|
+
canvas.composite(image, 50, 50);
|
|
69
|
+
|
|
70
|
+
// Apply image processing filters
|
|
71
|
+
canvas
|
|
72
|
+
.brightness(0.1)
|
|
73
|
+
.contrast(0.2)
|
|
74
|
+
.saturation(-0.1)
|
|
75
|
+
.blur(1)
|
|
76
|
+
.sharpen(0.3);
|
|
77
|
+
|
|
78
|
+
// Encode in a different format
|
|
79
|
+
const jpeg = await canvas.encode("jpeg");
|
|
80
|
+
await Deno.writeFile("output.jpg", jpeg);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Node.js
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { Image } from "cross-image";
|
|
87
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
88
|
+
|
|
89
|
+
// Read an image (auto-detects format)
|
|
90
|
+
const data = await readFile("input.png");
|
|
91
|
+
const image = await Image.decode(data);
|
|
92
|
+
|
|
93
|
+
console.log(`Image size: ${image.width}x${image.height}`);
|
|
94
|
+
|
|
95
|
+
// Resize the image
|
|
96
|
+
image.resize({ width: 800, height: 600 });
|
|
97
|
+
|
|
98
|
+
// Save in a different format
|
|
99
|
+
const jpeg = await image.encode("jpeg");
|
|
100
|
+
await writeFile("output.jpg", jpeg);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Bun
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { Image } from "cross-image";
|
|
107
|
+
|
|
108
|
+
// Read an image (auto-detects format)
|
|
109
|
+
const data = await Bun.file("input.png").arrayBuffer();
|
|
110
|
+
const image = await Image.decode(new Uint8Array(data));
|
|
111
|
+
|
|
112
|
+
console.log(`Image size: ${image.width}x${image.height}`);
|
|
113
|
+
|
|
114
|
+
// Resize the image and save
|
|
115
|
+
image.resize({ width: 800, height: 600 });
|
|
116
|
+
const jpeg = await image.encode("jpeg");
|
|
117
|
+
await Bun.write("output.jpg", jpeg);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Supported Formats
|
|
121
|
+
|
|
122
|
+
| Format | Pure-JS | Notes |
|
|
123
|
+
| ------ | ------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
124
|
+
| PNG | ✅ Full | Complete pure-JS implementation |
|
|
125
|
+
| APNG | ✅ Full | Animated PNG with multi-frame |
|
|
126
|
+
| BMP | ✅ Full | Complete pure-JS implementation |
|
|
127
|
+
| ICO | ✅ Full | Windows Icon format |
|
|
128
|
+
| GIF | ✅ Full | Animated GIF with multi-frame |
|
|
129
|
+
| DNG | ✅ Full | Linear DNG (Uncompressed RGBA) |
|
|
130
|
+
| PAM | ✅ Full | Netpbm PAM format |
|
|
131
|
+
| PPM | ✅ Full | Netpbm PPM format (P3/P6) |
|
|
132
|
+
| PCX | ✅ Full | ZSoft PCX (RLE compressed) |
|
|
133
|
+
| ASCII | ✅ Full | Text-based ASCII art |
|
|
134
|
+
| JPEG | ⚠️ Baseline & Progressive | Pure-JS baseline & progressive DCT: decode with spectral selection; encode with 2-scan (DC+AC) |
|
|
135
|
+
| WebP | ⚠️ Lossless | Pure-JS lossless VP8L |
|
|
136
|
+
| TIFF | ⚠️ Basic | Pure-JS uncompressed, LZW, PackBits, & Deflate; grayscale & RGB/RGBA |
|
|
137
|
+
| HEIC | 🔌 Runtime | Requires ImageDecoder/OffscreenCanvas API support |
|
|
138
|
+
| AVIF | 🔌 Runtime | Requires ImageDecoder/OffscreenCanvas API support |
|
|
139
|
+
|
|
140
|
+
See the [full format support documentation](https://cross-image.56k.guru/formats/) for detailed
|
|
141
|
+
compatibility information.
|
|
142
|
+
|
|
143
|
+
## Advanced Usage
|
|
144
|
+
|
|
145
|
+
Most users should stick to the `Image` API (`Image.decode`, `image.encode`, etc.).
|
|
146
|
+
|
|
147
|
+
If you need to work with format handlers directly (e.g. `new PNGFormat()`) or register your own
|
|
148
|
+
`ImageFormat` implementation via `Image.registerFormat(...)`, see the API reference section
|
|
149
|
+
“Exported Format Classes”:
|
|
150
|
+
|
|
151
|
+
- https://cross-image.56k.guru/api/
|
|
152
|
+
|
|
153
|
+
## JPEG Tolerant Decoding
|
|
154
|
+
|
|
155
|
+
The JPEG decoder includes a tolerant decoding mode (enabled by default) that gracefully handles
|
|
156
|
+
partially corrupted images or complex encoding patterns from mobile phone cameras. When enabled, the
|
|
157
|
+
decoder will continue processing even if some blocks fail to decode, filling failed blocks with
|
|
158
|
+
neutral values.
|
|
159
|
+
|
|
160
|
+
**Features:**
|
|
161
|
+
|
|
162
|
+
- **Enabled by default** - Handles real-world JPEGs from various devices
|
|
163
|
+
- **Progressive JPEG support** - Decodes both baseline and progressive JPEGs
|
|
164
|
+
- **Configurable** - Can be disabled for strict validation
|
|
165
|
+
- **Fault-tolerant** - Recovers partial image data instead of failing completely
|
|
166
|
+
- **Zero configuration** - Works automatically with the standard `Image.decode()` API
|
|
167
|
+
|
|
168
|
+
**When to use:**
|
|
169
|
+
|
|
170
|
+
- Mobile phone JPEGs with complex encoding patterns
|
|
171
|
+
- Progressive JPEG images from web sources
|
|
172
|
+
- Images from various camera manufacturers
|
|
173
|
+
- Partially corrupted JPEG files
|
|
174
|
+
- Production applications requiring maximum compatibility
|
|
175
|
+
|
|
176
|
+
**Example:**
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import { Image } from "jsr:@cross/image";
|
|
180
|
+
|
|
181
|
+
const data = await Deno.readFile("mobile-photo.jpg");
|
|
182
|
+
// Default behavior - tolerant decoding enabled
|
|
183
|
+
const image = await Image.decode(data);
|
|
184
|
+
|
|
185
|
+
// Strict mode - fail fast on decode errors
|
|
186
|
+
const strictImage = await Image.decode(data, { tolerantDecoding: false });
|
|
187
|
+
|
|
188
|
+
// Optional: receive warnings during partial decode (pure-JS decoder paths)
|
|
189
|
+
const imageWithWarnings = await Image.decode(data, {
|
|
190
|
+
tolerantDecoding: true,
|
|
191
|
+
runtimeDecoding: "never",
|
|
192
|
+
onWarning: (message, details) => {
|
|
193
|
+
console.log(`JPEG Warning: ${message}`, details);
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Note:** When using `Image.decode()`, the library automatically tries runtime-optimized decoders
|
|
199
|
+
|
|
200
|
+
## CMYK Color Space Support
|
|
201
|
+
|
|
202
|
+
The library provides utilities for working with CMYK (Cyan, Magenta, Yellow, Key/Black) color space,
|
|
203
|
+
commonly used in professional printing and color manipulation.
|
|
204
|
+
|
|
205
|
+
### Color Conversion Utilities
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
import { cmykToRgb, rgbToCmyk } from "jsr:@cross/image";
|
|
209
|
+
|
|
210
|
+
// Convert RGB to CMYK
|
|
211
|
+
const [c, m, y, k] = rgbToCmyk(255, 0, 0); // Red
|
|
212
|
+
console.log({ c, m, y, k }); // { c: 0, m: 1, y: 1, k: 0 }
|
|
213
|
+
|
|
214
|
+
// Convert CMYK back to RGB
|
|
215
|
+
const [r, g, b] = cmykToRgb(c, m, y, k);
|
|
216
|
+
console.log({ r, g, b }); // { r: 255, g: 0, b: 0 }
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Image-Level CMYK Operations
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import { Image } from "jsr:@cross/image";
|
|
223
|
+
|
|
224
|
+
// Load an image and convert to CMYK
|
|
225
|
+
const data = await Deno.readFile("photo.jpg");
|
|
226
|
+
const image = await Image.decode(data);
|
|
227
|
+
|
|
228
|
+
// Get CMYK representation (Float32Array with 4 values per pixel)
|
|
229
|
+
const cmykData = image.toCMYK();
|
|
230
|
+
|
|
231
|
+
// Create an image from CMYK data
|
|
232
|
+
const restored = Image.fromCMYK(cmykData, image.width, image.height);
|
|
233
|
+
|
|
234
|
+
// Save the restored image
|
|
235
|
+
await Deno.writeFile("output.png", await restored.encode("png"));
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Batch Conversion
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
import { cmykToRgba, rgbaToCmyk } from "jsr:@cross/image";
|
|
242
|
+
|
|
243
|
+
// Convert entire image data to CMYK
|
|
244
|
+
const rgbaData = new Uint8Array([255, 0, 0, 255]); // Red pixel
|
|
245
|
+
const cmykData = rgbaToCmyk(rgbaData);
|
|
246
|
+
|
|
247
|
+
// Convert CMYK data back to RGBA
|
|
248
|
+
const rgbaRestored = cmykToRgba(cmykData);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Use Cases:**
|
|
252
|
+
|
|
253
|
+
- Pre-press and print preparation workflows
|
|
254
|
+
- Color space conversion for professional printing
|
|
255
|
+
- Color analysis and manipulation in CMYK space
|
|
256
|
+
- Educational tools for understanding color models
|
|
257
|
+
|
|
258
|
+
### CMYK TIFF Support
|
|
259
|
+
|
|
260
|
+
TIFF format has native support for CMYK images:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
import { Image } from "jsr:@cross/image";
|
|
264
|
+
|
|
265
|
+
// Decode CMYK TIFF (automatically converted to RGBA)
|
|
266
|
+
const cmykTiff = await Deno.readFile("cmyk-image.tif");
|
|
267
|
+
const image = await Image.decode(cmykTiff); // Automatic CMYK → RGBA conversion
|
|
268
|
+
|
|
269
|
+
// Encode image as CMYK TIFF
|
|
270
|
+
const output = await image.encode("tiff", { cmyk: true });
|
|
271
|
+
await Deno.writeFile("output-cmyk.tif", output);
|
|
272
|
+
|
|
273
|
+
// CMYK works with all TIFF compression methods
|
|
274
|
+
const compressed = await image.encode("tiff", {
|
|
275
|
+
cmyk: true,
|
|
276
|
+
compression: "lzw", // or "packbits", "deflate", "none"
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Benefits:**
|
|
281
|
+
|
|
282
|
+
- **Seamless workflow**: CMYK TIFFs decode automatically, no special handling needed
|
|
283
|
+
- **Print-ready output**: Generate CMYK TIFFs for professional printing workflows
|
|
284
|
+
- **Full compression support**: CMYK works with all TIFF compression methods
|
|
285
|
+
- **Industry standard**: TIFF is the preferred format for CMYK images in print production
|
|
286
|
+
|
|
287
|
+
## Base64 / Data URLs
|
|
288
|
+
|
|
289
|
+
The library includes small utilities for working with base64 and `data:` URLs.
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
import { Image, parseDataUrl, toDataUrl } from "jsr:@cross/image";
|
|
293
|
+
|
|
294
|
+
const image = Image.create(2, 2, 255, 0, 0);
|
|
295
|
+
const pngBytes = await image.encode("png");
|
|
296
|
+
|
|
297
|
+
const dataUrl = toDataUrl("image/png", pngBytes);
|
|
298
|
+
const parsed = parseDataUrl(dataUrl);
|
|
299
|
+
|
|
300
|
+
// parsed.bytes is a Uint8Array containing the PNG
|
|
301
|
+
const roundtrip = await Image.decode(parsed.bytes, "png");
|
|
302
|
+
console.log(roundtrip.width, roundtrip.height);
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
(ImageDecoder API) first, falling back to the pure JS decoder with tolerant mode for maximum
|
|
306
|
+
compatibility.
|
|
307
|
+
|
|
308
|
+
## Fault-Tolerant Decoding for Other Formats
|
|
309
|
+
|
|
310
|
+
In addition to JPEG, @cross/image provides fault-tolerant decoding for several other formats that
|
|
311
|
+
commonly encounter corruption or complex encoding patterns:
|
|
312
|
+
|
|
313
|
+
### GIF Fault-Tolerant Decoding
|
|
314
|
+
|
|
315
|
+
The GIF decoder supports frame-level tolerance for animated GIFs. When enabled (default), corrupted
|
|
316
|
+
frames are skipped instead of causing complete decode failure.
|
|
317
|
+
|
|
318
|
+
**Features:**
|
|
319
|
+
|
|
320
|
+
- **Enabled by default** - Handles multi-frame GIFs with some corrupted frames
|
|
321
|
+
- **Frame-level recovery** - Skips bad frames, preserves good ones
|
|
322
|
+
- **LZW decompression errors** - Continues past compression errors
|
|
323
|
+
|
|
324
|
+
**Example:**
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
import { Image } from "jsr:@cross/image";
|
|
328
|
+
|
|
329
|
+
const data = await Deno.readFile("animated.gif");
|
|
330
|
+
|
|
331
|
+
// Tolerant mode (default) - skips corrupted frames
|
|
332
|
+
const tolerantFrames = await Image.decodeFrames(data);
|
|
333
|
+
|
|
334
|
+
// Strict mode - throws on first corrupted frame
|
|
335
|
+
const strictFrames = await Image.decodeFrames(data, {
|
|
336
|
+
tolerantDecoding: false,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Optional: receive warnings when frames are skipped
|
|
340
|
+
const framesWithWarnings = await Image.decodeFrames(data, {
|
|
341
|
+
tolerantDecoding: true,
|
|
342
|
+
runtimeDecoding: "never",
|
|
343
|
+
onWarning: (message, details) => {
|
|
344
|
+
console.log(`GIF Warning: ${message}`, details);
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### WebP Fault-Tolerant Decoding (VP8L Lossless)
|
|
350
|
+
|
|
351
|
+
The WebP VP8L (lossless) decoder supports pixel-level tolerance. When enabled (default), decoding
|
|
352
|
+
errors result in gray pixels for remaining data instead of complete failure.
|
|
353
|
+
|
|
354
|
+
**Features:**
|
|
355
|
+
|
|
356
|
+
- **Enabled by default** - Handles VP8L images with Huffman/LZ77 errors
|
|
357
|
+
- **Pixel-level recovery** - Fills remaining pixels with neutral gray
|
|
358
|
+
- **Huffman decode errors** - Continues past invalid codes
|
|
359
|
+
|
|
360
|
+
**Example:**
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
import { Image } from "jsr:@cross/image";
|
|
364
|
+
|
|
365
|
+
const data = await Deno.readFile("image.webp");
|
|
366
|
+
|
|
367
|
+
// Tolerant mode (default) - fills bad pixels with gray
|
|
368
|
+
const tolerantImage = await Image.decode(data);
|
|
369
|
+
|
|
370
|
+
// Strict mode - throws on first decode error
|
|
371
|
+
const strictImage = await Image.decode(data, { tolerantDecoding: false });
|
|
372
|
+
|
|
373
|
+
// Optional: receive warnings during partial decode
|
|
374
|
+
const imageWithWarnings = await Image.decode(data, {
|
|
375
|
+
tolerantDecoding: true,
|
|
376
|
+
runtimeDecoding: "never",
|
|
377
|
+
onWarning: (message, details) => {
|
|
378
|
+
console.log(`WebP Warning: ${message}`, details);
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
void imageWithWarnings;
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### When to Use Fault-Tolerant Modes
|
|
385
|
+
|
|
386
|
+
**Use tolerant decoding (default) when:**
|
|
387
|
+
|
|
388
|
+
- Processing user-uploaded images from various sources
|
|
389
|
+
- Building production applications requiring maximum compatibility
|
|
390
|
+
- Handling images from mobile devices or cameras
|
|
391
|
+
- Recovering data from partially corrupted files
|
|
392
|
+
- Batch processing where some failures are acceptable
|
|
393
|
+
|
|
394
|
+
**Use strict decoding when:**
|
|
395
|
+
|
|
396
|
+
- Validating image file integrity
|
|
397
|
+
- Quality control in professional workflows
|
|
398
|
+
- Detecting file corruption explicitly
|
|
399
|
+
- Testing image encoder implementations
|
|
400
|
+
|
|
401
|
+
### Warning Callbacks
|
|
402
|
+
|
|
403
|
+
Decoding APIs accept an optional `onWarning` callback that gets invoked when non-fatal issues occur
|
|
404
|
+
during decoding. This is useful for logging, monitoring, or debugging decoding issues without using
|
|
405
|
+
`console` methods.
|
|
406
|
+
|
|
407
|
+
**Example:**
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
import { Image } from "jsr:@cross/image";
|
|
411
|
+
|
|
412
|
+
const data = await Deno.readFile("input.webp");
|
|
413
|
+
|
|
414
|
+
const image = await Image.decode(data, {
|
|
415
|
+
tolerantDecoding: true,
|
|
416
|
+
runtimeDecoding: "never",
|
|
417
|
+
onWarning: (message, details) => {
|
|
418
|
+
// Log to your preferred logging system
|
|
419
|
+
myLogger.warn(message, details);
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
void image;
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
The callback receives:
|
|
426
|
+
|
|
427
|
+
- `message`: A human-readable description of the warning
|
|
428
|
+
- `details`: Optional additional context (usually the error object)
|
|
429
|
+
|
|
430
|
+
## Metadata Support
|
|
431
|
+
|
|
432
|
+
@cross/image provides comprehensive EXIF 3.0 compliant metadata support for image files, including
|
|
433
|
+
camera information, GPS coordinates, and InteropIFD compatibility markers.
|
|
434
|
+
|
|
435
|
+
### Supported Metadata Fields
|
|
436
|
+
|
|
437
|
+
**Basic Metadata:**
|
|
438
|
+
|
|
439
|
+
- `title`, `description`, `author`, `copyright`
|
|
440
|
+
- `creationDate` - Date/time image was created
|
|
441
|
+
|
|
442
|
+
**Camera Settings (JPEG, TIFF, WebP via XMP):**
|
|
443
|
+
|
|
444
|
+
- `cameraMake`, `cameraModel` - Camera manufacturer and model
|
|
445
|
+
- `lensMake`, `lensModel` - Lens information
|
|
446
|
+
- `iso` - ISO speed rating
|
|
447
|
+
- `exposureTime` - Shutter speed in seconds
|
|
448
|
+
- `fNumber` - Aperture (f-number)
|
|
449
|
+
- `focalLength` - Focal length in mm
|
|
450
|
+
- `flash`, `whiteBalance` - Capture settings
|
|
451
|
+
- `orientation` - Image orientation (1=normal, 3=180°, 6=90°CW, 8=90°CCW)
|
|
452
|
+
- `software` - Software used
|
|
453
|
+
- `userComment` - User notes
|
|
454
|
+
|
|
455
|
+
**GPS Coordinates (All EXIF formats: JPEG, PNG, WebP, TIFF):**
|
|
456
|
+
|
|
457
|
+
- `latitude`, `longitude` - GPS coordinates in decimal degrees
|
|
458
|
+
- Full microsecond precision with DMS (degrees-minutes-seconds) conversion
|
|
459
|
+
|
|
460
|
+
**DPI (JPEG, PNG, TIFF):**
|
|
461
|
+
|
|
462
|
+
- `dpiX`, `dpiY` - Dots per inch for printing
|
|
463
|
+
|
|
464
|
+
### EXIF 3.0 Specification Compliance
|
|
465
|
+
|
|
466
|
+
The library implements the EXIF 3.0 specification with:
|
|
467
|
+
|
|
468
|
+
- **50+ Exif Sub-IFD tags** for comprehensive camera metadata
|
|
469
|
+
- **30+ IFD0 tags** for image information
|
|
470
|
+
- **InteropIFD support** for format compatibility (R98/sRGB, R03/Adobe RGB, THM/thumbnail)
|
|
471
|
+
- **GPS IFD** with proper coordinate conversion
|
|
472
|
+
- All EXIF data types (BYTE, ASCII, SHORT, LONG, RATIONAL, etc.)
|
|
473
|
+
|
|
474
|
+
### Example Usage
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
import { Image } from "jsr:@cross/image";
|
|
478
|
+
|
|
479
|
+
// Load an image
|
|
480
|
+
const data = await Deno.readFile("photo.jpg");
|
|
481
|
+
const image = await Image.decode(data);
|
|
482
|
+
|
|
483
|
+
// Set metadata
|
|
484
|
+
image.setMetadata({
|
|
485
|
+
author: "Jane Photographer",
|
|
486
|
+
copyright: "© 2024",
|
|
487
|
+
cameraMake: "Canon",
|
|
488
|
+
cameraModel: "EOS R5",
|
|
489
|
+
iso: 800,
|
|
490
|
+
exposureTime: 0.004, // 1/250s
|
|
491
|
+
fNumber: 2.8,
|
|
492
|
+
focalLength: 50,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// Set GPS coordinates
|
|
496
|
+
image.setPosition(40.7128, -74.0060); // NYC
|
|
497
|
+
|
|
498
|
+
// Check what metadata a format supports
|
|
499
|
+
const jpegSupports = Image.getSupportedMetadata("jpeg");
|
|
500
|
+
console.log(jpegSupports); // Includes ISO, camera info, GPS, etc.
|
|
501
|
+
|
|
502
|
+
// Save with metadata
|
|
503
|
+
const jpeg = await image.encode("jpeg");
|
|
504
|
+
await Deno.writeFile("output.jpg", jpeg);
|
|
505
|
+
|
|
506
|
+
// Metadata is preserved on reload!
|
|
507
|
+
const loaded = await Image.decode(jpeg);
|
|
508
|
+
console.log(loaded.metadata?.cameraMake); // "Canon"
|
|
509
|
+
console.log(loaded.getPosition()); // { latitude: 40.7128, longitude: -74.0060 }
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### Extracting Metadata Without Decoding
|
|
513
|
+
|
|
514
|
+
For quickly reading metadata from images without the overhead of decoding pixel data, use
|
|
515
|
+
`Image.extractMetadata()`. This is particularly useful for:
|
|
516
|
+
|
|
517
|
+
- Reading EXIF data from large images or photos
|
|
518
|
+
- Extracting metadata from images with unsupported compression
|
|
519
|
+
- Building image catalogs or galleries
|
|
520
|
+
- Processing metadata in batch operations
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
import { Image } from "jsr:@cross/image";
|
|
524
|
+
|
|
525
|
+
// Extract metadata without decoding pixels
|
|
526
|
+
const data = await Deno.readFile("large-photo.jpg");
|
|
527
|
+
const metadata = await Image.extractMetadata(data);
|
|
528
|
+
|
|
529
|
+
console.log(metadata?.cameraMake); // "Canon"
|
|
530
|
+
console.log(metadata?.iso); // 800
|
|
531
|
+
console.log(metadata?.exposureTime); // 0.004
|
|
532
|
+
|
|
533
|
+
// Works with auto-detection
|
|
534
|
+
const metadata2 = await Image.extractMetadata(data); // Detects JPEG
|
|
535
|
+
|
|
536
|
+
// Or specify format explicitly
|
|
537
|
+
const metadata3 = await Image.extractMetadata(data, "jpeg");
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
This method is significantly faster than full decode when you only need metadata, as it:
|
|
541
|
+
|
|
542
|
+
- Skips pixel data decompression
|
|
543
|
+
- Only parses metadata chunks/markers
|
|
544
|
+
- Returns `undefined` for unsupported formats
|
|
545
|
+
- Works with JPEG, PNG, WebP, TIFF, HEIC, and AVIF formats
|
|
546
|
+
|
|
547
|
+
### Format-Specific Support
|
|
548
|
+
|
|
549
|
+
Use `Image.getSupportedMetadata(format)` to check which fields are supported:
|
|
550
|
+
|
|
551
|
+
```typescript
|
|
552
|
+
Image.getSupportedMetadata("jpeg"); // Full camera metadata + GPS (21 fields)
|
|
553
|
+
Image.getSupportedMetadata("tiff"); // Comprehensive EXIF + GPS + InteropIFD (23+ fields)
|
|
554
|
+
Image.getSupportedMetadata("png"); // DateTime, GPS, DPI, basic text (9 fields)
|
|
555
|
+
Image.getSupportedMetadata("webp"); // Enhanced XMP + GPS (15 fields - includes camera metadata!)
|
|
556
|
+
Image.getSupportedMetadata("heic"); // Full camera metadata + GPS (19 fields)
|
|
557
|
+
Image.getSupportedMetadata("avif"); // Full camera metadata + GPS (19 fields)
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
**Format Highlights:**
|
|
561
|
+
|
|
562
|
+
- **JPEG**: Most comprehensive EXIF support, including all camera settings and GPS
|
|
563
|
+
- **TIFF**: Full EXIF 3.0 support with IFD structure, InteropIFD compatibility
|
|
564
|
+
- **WebP**: Enhanced XMP implementation with Dublin Core, EXIF, and TIFF namespaces
|
|
565
|
+
- **PNG**: Basic EXIF support via eXIf chunk plus GPS coordinates
|
|
566
|
+
- **HEIC**: Full EXIF metadata extraction including camera settings, GPS, and image info
|
|
567
|
+
(runtime-dependent encoding)
|
|
568
|
+
- **AVIF**: Full EXIF metadata extraction including camera settings, GPS, and image info
|
|
569
|
+
(runtime-dependent encoding)
|
|
570
|
+
|
|
571
|
+
## Documentation
|
|
572
|
+
|
|
573
|
+
- **[API Reference](https://cross-image.56k.guru/api/)** - Complete API documentation
|
|
574
|
+
- **[Format Support](https://cross-image.56k.guru/formats/)** - Supported formats and specifications
|
|
575
|
+
- **[Image Processing](https://cross-image.56k.guru/processing/)** - Filters, manipulation, and
|
|
576
|
+
color adjustments
|
|
577
|
+
- [Filters](https://cross-image.56k.guru/processing/filters/) - Blur, sharpen, and noise reduction
|
|
578
|
+
- [Manipulation](https://cross-image.56k.guru/processing/manipulation/) - Resize, crop, composite,
|
|
579
|
+
and draw
|
|
580
|
+
- [Color Adjustments](https://cross-image.56k.guru/processing/color-adjustments/) - Brightness,
|
|
581
|
+
contrast, saturation, and more
|
|
582
|
+
- **[Examples](https://cross-image.56k.guru/examples/)** - Practical examples for common tasks
|
|
583
|
+
- [Decoding & Encoding](https://cross-image.56k.guru/examples/decoding-encoding/) -
|
|
584
|
+
Format-specific examples
|
|
585
|
+
- [Using Filters](https://cross-image.56k.guru/examples/filters/) - Filter workflows and
|
|
586
|
+
techniques
|
|
587
|
+
- [Manipulation](https://cross-image.56k.guru/examples/manipulation/) - Resizing, cropping, and
|
|
588
|
+
compositing
|
|
589
|
+
- [Multi-Frame Images](https://cross-image.56k.guru/examples/multi-frame/) - Animated GIFs, APNGs,
|
|
590
|
+
and TIFFs
|
|
591
|
+
- **[JPEG Implementation](https://cross-image.56k.guru/implementation/jpeg-implementation/)** -
|
|
592
|
+
Technical details for JPEG
|
|
593
|
+
- **[WebP Implementation](https://cross-image.56k.guru/implementation/webp-implementation/)** -
|
|
594
|
+
Technical details for WebP
|
|
595
|
+
- **[TIFF Implementation](https://cross-image.56k.guru/implementation/tiff-implementation/)** -
|
|
596
|
+
Technical details for TIFF
|
|
597
|
+
- **[GIF Implementation](https://cross-image.56k.guru/implementation/gif-implementation/)** -
|
|
598
|
+
Technical details for GIF
|
|
599
|
+
|
|
600
|
+
## Development
|
|
601
|
+
|
|
602
|
+
```bash
|
|
603
|
+
deno task precommit
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
CI validates cross-runtime compatibility (Deno, Bun, Node). See CONTRIBUTING.md for contributor
|
|
607
|
+
workflow details.
|
|
608
|
+
|
|
609
|
+
## License
|
|
610
|
+
|
|
611
|
+
MIT License - see LICENSE file for details.
|
|
612
|
+
|
|
613
|
+
## Contributing
|
|
614
|
+
|
|
615
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|