cross-image 0.1.2
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 -0
- package/README.md +606 -0
- package/esm/mod.d.ts +33 -0
- package/esm/mod.d.ts.map +1 -0
- package/esm/mod.js +31 -0
- package/esm/package.json +3 -0
- package/esm/src/formats/ascii.d.ts +27 -0
- package/esm/src/formats/ascii.d.ts.map +1 -0
- package/esm/src/formats/ascii.js +172 -0
- package/esm/src/formats/bmp.d.ts +19 -0
- package/esm/src/formats/bmp.d.ts.map +1 -0
- package/esm/src/formats/bmp.js +174 -0
- package/esm/src/formats/gif.d.ts +40 -0
- package/esm/src/formats/gif.d.ts.map +1 -0
- package/esm/src/formats/gif.js +385 -0
- package/esm/src/formats/jpeg.d.ts +18 -0
- package/esm/src/formats/jpeg.d.ts.map +1 -0
- package/esm/src/formats/jpeg.js +414 -0
- package/esm/src/formats/png.d.ts +33 -0
- package/esm/src/formats/png.d.ts.map +1 -0
- package/esm/src/formats/png.js +544 -0
- package/esm/src/formats/raw.d.ts +23 -0
- package/esm/src/formats/raw.d.ts.map +1 -0
- package/esm/src/formats/raw.js +98 -0
- package/esm/src/formats/tiff.d.ts +58 -0
- package/esm/src/formats/tiff.d.ts.map +1 -0
- package/esm/src/formats/tiff.js +791 -0
- package/esm/src/formats/webp.d.ts +22 -0
- package/esm/src/formats/webp.d.ts.map +1 -0
- package/esm/src/formats/webp.js +403 -0
- package/esm/src/image.d.ts +124 -0
- package/esm/src/image.d.ts.map +1 -0
- package/esm/src/image.js +320 -0
- package/esm/src/types.d.ts +167 -0
- package/esm/src/types.d.ts.map +1 -0
- package/esm/src/types.js +1 -0
- package/esm/src/utils/gif_decoder.d.ts +42 -0
- package/esm/src/utils/gif_decoder.d.ts.map +1 -0
- package/esm/src/utils/gif_decoder.js +374 -0
- package/esm/src/utils/gif_encoder.d.ts +29 -0
- package/esm/src/utils/gif_encoder.d.ts.map +1 -0
- package/esm/src/utils/gif_encoder.js +226 -0
- package/esm/src/utils/jpeg_decoder.d.ts +39 -0
- package/esm/src/utils/jpeg_decoder.d.ts.map +1 -0
- package/esm/src/utils/jpeg_decoder.js +580 -0
- package/esm/src/utils/jpeg_encoder.d.ts +33 -0
- package/esm/src/utils/jpeg_encoder.d.ts.map +1 -0
- package/esm/src/utils/jpeg_encoder.js +1017 -0
- package/esm/src/utils/lzw.d.ts +43 -0
- package/esm/src/utils/lzw.d.ts.map +1 -0
- package/esm/src/utils/lzw.js +309 -0
- package/esm/src/utils/resize.d.ts +9 -0
- package/esm/src/utils/resize.d.ts.map +1 -0
- package/esm/src/utils/resize.js +52 -0
- package/esm/src/utils/tiff_lzw.d.ts +44 -0
- package/esm/src/utils/tiff_lzw.d.ts.map +1 -0
- package/esm/src/utils/tiff_lzw.js +306 -0
- package/esm/src/utils/webp_decoder.d.ts +39 -0
- package/esm/src/utils/webp_decoder.d.ts.map +1 -0
- package/esm/src/utils/webp_decoder.js +493 -0
- package/esm/src/utils/webp_encoder.d.ts +72 -0
- package/esm/src/utils/webp_encoder.d.ts.map +1 -0
- package/esm/src/utils/webp_encoder.js +627 -0
- package/package.json +41 -0
- package/script/mod.d.ts +33 -0
- package/script/mod.d.ts.map +1 -0
- package/script/mod.js +43 -0
- package/script/package.json +3 -0
- package/script/src/formats/ascii.d.ts +27 -0
- package/script/src/formats/ascii.d.ts.map +1 -0
- package/script/src/formats/ascii.js +176 -0
- package/script/src/formats/bmp.d.ts +19 -0
- package/script/src/formats/bmp.d.ts.map +1 -0
- package/script/src/formats/bmp.js +178 -0
- package/script/src/formats/gif.d.ts +40 -0
- package/script/src/formats/gif.d.ts.map +1 -0
- package/script/src/formats/gif.js +389 -0
- package/script/src/formats/jpeg.d.ts +18 -0
- package/script/src/formats/jpeg.d.ts.map +1 -0
- package/script/src/formats/jpeg.js +451 -0
- package/script/src/formats/png.d.ts +33 -0
- package/script/src/formats/png.d.ts.map +1 -0
- package/script/src/formats/png.js +548 -0
- package/script/src/formats/raw.d.ts +23 -0
- package/script/src/formats/raw.d.ts.map +1 -0
- package/script/src/formats/raw.js +102 -0
- package/script/src/formats/tiff.d.ts +58 -0
- package/script/src/formats/tiff.d.ts.map +1 -0
- package/script/src/formats/tiff.js +795 -0
- package/script/src/formats/webp.d.ts +22 -0
- package/script/src/formats/webp.d.ts.map +1 -0
- package/script/src/formats/webp.js +440 -0
- package/script/src/image.d.ts +124 -0
- package/script/src/image.d.ts.map +1 -0
- package/script/src/image.js +324 -0
- package/script/src/types.d.ts +167 -0
- package/script/src/types.d.ts.map +1 -0
- package/script/src/types.js +2 -0
- package/script/src/utils/gif_decoder.d.ts +42 -0
- package/script/src/utils/gif_decoder.d.ts.map +1 -0
- package/script/src/utils/gif_decoder.js +378 -0
- package/script/src/utils/gif_encoder.d.ts +29 -0
- package/script/src/utils/gif_encoder.d.ts.map +1 -0
- package/script/src/utils/gif_encoder.js +230 -0
- package/script/src/utils/jpeg_decoder.d.ts +39 -0
- package/script/src/utils/jpeg_decoder.d.ts.map +1 -0
- package/script/src/utils/jpeg_decoder.js +584 -0
- package/script/src/utils/jpeg_encoder.d.ts +33 -0
- package/script/src/utils/jpeg_encoder.d.ts.map +1 -0
- package/script/src/utils/jpeg_encoder.js +1021 -0
- package/script/src/utils/lzw.d.ts +43 -0
- package/script/src/utils/lzw.d.ts.map +1 -0
- package/script/src/utils/lzw.js +314 -0
- package/script/src/utils/resize.d.ts +9 -0
- package/script/src/utils/resize.d.ts.map +1 -0
- package/script/src/utils/resize.js +56 -0
- package/script/src/utils/tiff_lzw.d.ts +44 -0
- package/script/src/utils/tiff_lzw.d.ts.map +1 -0
- package/script/src/utils/tiff_lzw.js +311 -0
- package/script/src/utils/webp_decoder.d.ts +39 -0
- package/script/src/utils/webp_decoder.d.ts.map +1 -0
- package/script/src/utils/webp_decoder.js +497 -0
- package/script/src/utils/webp_encoder.d.ts +72 -0
- package/script/src/utils/webp_encoder.d.ts.map +1 -0
- package/script/src/utils/webp_encoder.js +631 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 @cross
|
|
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,606 @@
|
|
|
1
|
+
# @cross/image
|
|
2
|
+
|
|
3
|
+
A pure JavaScript, dependency-free, cross-runtime image processing library for
|
|
4
|
+
Deno, Node.js, and Bun.
|
|
5
|
+
|
|
6
|
+
📚 **[Full Documentation](https://cross-org.github.io/image/)**
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- 🚀 **Pure JavaScript** - No native dependencies
|
|
11
|
+
- 🔌 **Pluggable formats** - Easy to extend with custom formats
|
|
12
|
+
- 📦 **Cross-runtime** - Works on Deno, Node.js (18+), and Bun
|
|
13
|
+
- 🎨 **Multiple formats** - PNG, JPEG, WebP, GIF, TIFF, BMP, and RAW support
|
|
14
|
+
- ✂️ **Image manipulation** - Resize with multiple algorithms
|
|
15
|
+
- 🔧 **Simple API** - Easy to use, intuitive interface
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
### Deno
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { Image } from "jsr:@cross/image";
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Node.js
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install cross-image
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { Image } from "cross-image";
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Bun
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bun add cross-image
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { Image } from "cross-image";
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
### Reading and Saving Images
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { Image } from "@cross/image";
|
|
51
|
+
|
|
52
|
+
// Read an image (auto-detects format)
|
|
53
|
+
const data = await Deno.readFile("input.png");
|
|
54
|
+
const image = await Image.read(data);
|
|
55
|
+
|
|
56
|
+
console.log(`Image size: ${image.width}x${image.height}`);
|
|
57
|
+
|
|
58
|
+
// Save as different format
|
|
59
|
+
const jpeg = await image.save("jpeg");
|
|
60
|
+
await Deno.writeFile("output.jpg", jpeg);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Resizing Images
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { Image } from "@cross/image";
|
|
67
|
+
|
|
68
|
+
const data = await Deno.readFile("input.png");
|
|
69
|
+
const image = await Image.read(data);
|
|
70
|
+
|
|
71
|
+
// Resize with bilinear interpolation (default)
|
|
72
|
+
image.resize({ width: 800, height: 600 });
|
|
73
|
+
|
|
74
|
+
// Or use nearest neighbor for faster, pixelated results
|
|
75
|
+
image.resize({ width: 400, height: 300, method: "nearest" });
|
|
76
|
+
|
|
77
|
+
// Save the result
|
|
78
|
+
const output = await image.save("png");
|
|
79
|
+
await Deno.writeFile("resized.png", output);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Creating Images from Scratch
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { Image } from "@cross/image";
|
|
86
|
+
|
|
87
|
+
// Create a 100x100 red square
|
|
88
|
+
const width = 100;
|
|
89
|
+
const height = 100;
|
|
90
|
+
const data = new Uint8Array(width * height * 4);
|
|
91
|
+
|
|
92
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
93
|
+
data[i] = 255; // R
|
|
94
|
+
data[i + 1] = 0; // G
|
|
95
|
+
data[i + 2] = 0; // B
|
|
96
|
+
data[i + 3] = 255; // A
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const image = Image.fromRGBA(width, height, data);
|
|
100
|
+
const png = await image.save("png");
|
|
101
|
+
await Deno.writeFile("red-square.png", png);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Using TIFF with LZW Compression
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { Image } from "@cross/image";
|
|
108
|
+
|
|
109
|
+
const data = await Deno.readFile("input.png");
|
|
110
|
+
const image = await Image.read(data);
|
|
111
|
+
|
|
112
|
+
// Save as uncompressed TIFF
|
|
113
|
+
const uncompressed = await image.save("tiff");
|
|
114
|
+
await Deno.writeFile("output.tiff", uncompressed);
|
|
115
|
+
|
|
116
|
+
// Save with LZW compression (smaller file size)
|
|
117
|
+
const compressed = await image.save("tiff", { compression: "lzw" });
|
|
118
|
+
await Deno.writeFile("output-compressed.tiff", compressed);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Using WebP with Quality Settings
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { Image } from "@cross/image";
|
|
125
|
+
|
|
126
|
+
const data = await Deno.readFile("input.png");
|
|
127
|
+
const image = await Image.read(data);
|
|
128
|
+
|
|
129
|
+
// Save as lossless WebP (quality = 100)
|
|
130
|
+
const lossless = await image.save("webp", { quality: 100 });
|
|
131
|
+
await Deno.writeFile("output-lossless.webp", lossless);
|
|
132
|
+
|
|
133
|
+
// Save as lossy WebP with high quality (smaller file size)
|
|
134
|
+
const highQuality = await image.save("webp", { quality: 90 });
|
|
135
|
+
await Deno.writeFile("output-hq.webp", highQuality);
|
|
136
|
+
|
|
137
|
+
// Save as lossy WebP with medium quality (much smaller)
|
|
138
|
+
const mediumQuality = await image.save("webp", { quality: 75 });
|
|
139
|
+
await Deno.writeFile("output-med.webp", mediumQuality);
|
|
140
|
+
|
|
141
|
+
// Force lossless even with quality < 100 (pure-JS VP8L)
|
|
142
|
+
const forcedLossless = await image.save("webp", {
|
|
143
|
+
quality: 80,
|
|
144
|
+
lossless: true,
|
|
145
|
+
});
|
|
146
|
+
await Deno.writeFile("output-forced.webp", forcedLossless);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Converting to ASCII Art
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { type ASCIIOptions, Image } from "@cross/image";
|
|
153
|
+
|
|
154
|
+
const data = await Deno.readFile("photo.jpg");
|
|
155
|
+
const image = await Image.read(data);
|
|
156
|
+
|
|
157
|
+
// Convert to ASCII art with simple characters
|
|
158
|
+
const ascii = await image.save("ascii", { width: 80, charset: "simple" });
|
|
159
|
+
console.log(new TextDecoder().decode(ascii));
|
|
160
|
+
|
|
161
|
+
// Or use block characters for better gradients
|
|
162
|
+
const blocks = await image.save("ascii", {
|
|
163
|
+
width: 60,
|
|
164
|
+
charset: "blocks",
|
|
165
|
+
aspectRatio: 0.5,
|
|
166
|
+
});
|
|
167
|
+
console.log(new TextDecoder().decode(blocks));
|
|
168
|
+
|
|
169
|
+
// Save ASCII art to file
|
|
170
|
+
await Deno.writeFile("output.txt", ascii);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Working with Multi-Frame Images
|
|
174
|
+
|
|
175
|
+
Read and manipulate animated GIFs and multi-page TIFFs:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { Image } from "@cross/image";
|
|
179
|
+
|
|
180
|
+
// Read all frames from an animated GIF
|
|
181
|
+
const gifData = await Deno.readFile("animated.gif");
|
|
182
|
+
const multiFrame = await Image.readFrames(gifData);
|
|
183
|
+
|
|
184
|
+
console.log(`Canvas: ${multiFrame.width}x${multiFrame.height}`);
|
|
185
|
+
console.log(`Number of frames: ${multiFrame.frames.length}`);
|
|
186
|
+
|
|
187
|
+
// Access individual frames
|
|
188
|
+
for (let i = 0; i < multiFrame.frames.length; i++) {
|
|
189
|
+
const frame = multiFrame.frames[i];
|
|
190
|
+
console.log(`Frame ${i}: ${frame.width}x${frame.height}`);
|
|
191
|
+
console.log(` Delay: ${frame.frameMetadata?.delay}ms`);
|
|
192
|
+
console.log(` Disposal: ${frame.frameMetadata?.disposal}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Create a multi-page TIFF from multiple images
|
|
196
|
+
const page1 = Image.fromRGBA(100, 100, new Uint8Array(100 * 100 * 4));
|
|
197
|
+
const page2 = Image.fromRGBA(100, 100, new Uint8Array(100 * 100 * 4));
|
|
198
|
+
|
|
199
|
+
const multiPageTiff = {
|
|
200
|
+
width: 100,
|
|
201
|
+
height: 100,
|
|
202
|
+
frames: [
|
|
203
|
+
{ width: 100, height: 100, data: page1.data },
|
|
204
|
+
{ width: 100, height: 100, data: page2.data },
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Save as multi-page TIFF with LZW compression
|
|
209
|
+
const tiffData = await Image.saveFrames("tiff", multiPageTiff, {
|
|
210
|
+
compression: "lzw",
|
|
211
|
+
});
|
|
212
|
+
await Deno.writeFile("multipage.tiff", tiffData);
|
|
213
|
+
|
|
214
|
+
// Read all pages from a multi-page TIFF
|
|
215
|
+
const pages = await Image.readFrames(tiffData);
|
|
216
|
+
console.log(`Read ${pages.frames.length} pages from TIFF`);
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Chaining Operations
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import { Image } from "@cross/image";
|
|
223
|
+
|
|
224
|
+
const data = await Deno.readFile("input.png");
|
|
225
|
+
const image = await Image.read(data);
|
|
226
|
+
|
|
227
|
+
// Chain multiple operations
|
|
228
|
+
image
|
|
229
|
+
.resize({ width: 1920, height: 1080 })
|
|
230
|
+
.resize({ width: 800, height: 600 });
|
|
231
|
+
|
|
232
|
+
const output = await image.save("webp");
|
|
233
|
+
await Deno.writeFile("output.webp", output);
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Supported Formats
|
|
237
|
+
|
|
238
|
+
### Format Support Matrix
|
|
239
|
+
|
|
240
|
+
This table shows which image formats are supported and their implementation
|
|
241
|
+
status:
|
|
242
|
+
|
|
243
|
+
| Format | Read | Write | Pure-JS Decode | Pure-JS Encode | Native API Decode | Native API Encode | Notes |
|
|
244
|
+
| ------ | ---- | ----- | -------------- | -------------- | ----------------- | ------------------ | -------------------------------------------- |
|
|
245
|
+
| PNG | ✅ | ✅ | ✅ Full | ✅ Full | ✅ ImageDecoder | ✅ OffscreenCanvas | Complete pure-JS implementation |
|
|
246
|
+
| BMP | ✅ | ✅ | ✅ Full | ✅ Full | ✅ ImageDecoder | ✅ OffscreenCanvas | Complete pure-JS implementation |
|
|
247
|
+
| RAW | ✅ | ✅ | ✅ Full | ✅ Full | N/A | N/A | Uncompressed RGBA (no metadata) |
|
|
248
|
+
| ASCII | ✅ | ✅ | ✅ Full | ✅ Full | N/A | N/A | Text-based ASCII art representation |
|
|
249
|
+
| JPEG | ✅ | ✅ | ⚠️ Baseline | ⚠️ Baseline | ✅ ImageDecoder | ✅ OffscreenCanvas | Pure-JS for baseline DCT only |
|
|
250
|
+
| GIF | ✅ | ✅ | ✅ Full | ✅ Full | ✅ ImageDecoder | ✅ OffscreenCanvas | Complete pure-JS implementation |
|
|
251
|
+
| WebP | ✅ | ✅ | ⚠️ Lossless | ⚠️ Quantized | ✅ ImageDecoder | ✅ OffscreenCanvas | Pure-JS VP8L with quality-based quantization |
|
|
252
|
+
| TIFF | ✅ | ✅ | ⚠️ Basic | ⚠️ Basic | ✅ ImageDecoder | ✅ OffscreenCanvas | Pure-JS for uncompressed & LZW RGB/RGBA |
|
|
253
|
+
|
|
254
|
+
**Legend:**
|
|
255
|
+
|
|
256
|
+
- ✅ **Full support** - Complete implementation with all common features
|
|
257
|
+
- ⚠️ **Limited support** - Partial implementation with restrictions
|
|
258
|
+
- ❌ **Not supported** - Feature not available in pure-JS, requires native APIs
|
|
259
|
+
- **Pure-JS** - Works in all JavaScript runtimes without native dependencies
|
|
260
|
+
- **Native API** - Uses runtime APIs like ImageDecoder (decode) or
|
|
261
|
+
OffscreenCanvas (encode)
|
|
262
|
+
|
|
263
|
+
### Format Specifications Supported
|
|
264
|
+
|
|
265
|
+
This table shows which format standards and variants are supported:
|
|
266
|
+
|
|
267
|
+
| Format | Specification/Variant | Support Level | Implementation |
|
|
268
|
+
| ------ | ----------------------------------- | ----------------- | -------------- |
|
|
269
|
+
| PNG | PNG 1.2 (ISO/IEC 15948) | ✅ Full | Pure-JS |
|
|
270
|
+
| | - Interlaced (Adam7) | ❌ Not Yet | - |
|
|
271
|
+
| | - Color types: Grayscale, RGB, RGBA | ✅ Full | Pure-JS |
|
|
272
|
+
| | - Metadata: pHYs, tEXt, iTXt, eXIf | ✅ Full | Pure-JS |
|
|
273
|
+
| BMP | Windows BMP (BITMAPINFOHEADER) | ✅ Full | Pure-JS |
|
|
274
|
+
| | - 24-bit RGB | ✅ Full | Pure-JS |
|
|
275
|
+
| | - 32-bit RGBA | ✅ Full | Pure-JS |
|
|
276
|
+
| | - Compressed variants (RLE) | ❌ Not Yet | - |
|
|
277
|
+
| JPEG | JPEG/JFIF Baseline DCT | ✅ Full | Pure-JS |
|
|
278
|
+
| | Progressive DCT | ⚠️ Native only | ImageDecoder |
|
|
279
|
+
| | - EXIF metadata | ✅ Full | Pure-JS |
|
|
280
|
+
| | - JFIF (APP0) with DPI | ✅ Full | Pure-JS |
|
|
281
|
+
| WebP | WebP Lossless (VP8L) | ⚠️ Basic | Pure-JS |
|
|
282
|
+
| | - Simple Huffman coding | ✅ Full | Pure-JS |
|
|
283
|
+
| | - LZ77 backward references | ❌ Not in encoder | - |
|
|
284
|
+
| | - Color cache | ❌ Not in encoder | - |
|
|
285
|
+
| | - Transforms (predictor, etc.) | ❌ Not Yet | - |
|
|
286
|
+
| | WebP Lossy (VP8L with quantization) | ✅ Quality-based | Pure-JS |
|
|
287
|
+
| | - Color quantization for lossy | ✅ Full | Pure-JS |
|
|
288
|
+
| | WebP Lossy (VP8) | ⚠️ Native only | ImageDecoder |
|
|
289
|
+
| | - EXIF, XMP metadata | ✅ Full | Pure-JS |
|
|
290
|
+
| TIFF | TIFF 6.0 - Uncompressed RGB/RGBA | ✅ Full | Pure-JS |
|
|
291
|
+
| | TIFF 6.0 - LZW compressed RGB/RGBA | ✅ Full | Pure-JS |
|
|
292
|
+
| | - JPEG, PackBits compression | ⚠️ Native only | ImageDecoder |
|
|
293
|
+
| | - Multi-page/IFD (decode & encode) | ✅ Full | Pure-JS |
|
|
294
|
+
| | - EXIF, Artist, Copyright metadata | ✅ Full | Pure-JS |
|
|
295
|
+
| GIF | GIF87a, GIF89a | ✅ Full | Pure-JS |
|
|
296
|
+
| | - LZW compression/decompression | ✅ Full | Pure-JS |
|
|
297
|
+
| | - Color quantization (encoding) | ✅ Full | Pure-JS |
|
|
298
|
+
| | - Transparency support | ✅ Full | Pure-JS |
|
|
299
|
+
| | - Interlacing support | ✅ Full | Pure-JS |
|
|
300
|
+
| | - Animation (multi-frame decode) | ✅ Full | Pure-JS |
|
|
301
|
+
| | - Animation (encode first frame) | ⚠️ Single frame | Pure-JS |
|
|
302
|
+
| | - Comment extensions, XMP | ✅ Full | Pure-JS |
|
|
303
|
+
| RAW | Uncompressed RGBA | ✅ Full | Pure-JS |
|
|
304
|
+
| ASCII | Text-based ASCII art | ✅ Full | Pure-JS |
|
|
305
|
+
| | - Multiple character sets | ✅ Full | Pure-JS |
|
|
306
|
+
| | - Configurable width & aspect ratio | ✅ Full | Pure-JS |
|
|
307
|
+
| | - Brightness inversion | ✅ Full | Pure-JS |
|
|
308
|
+
|
|
309
|
+
### Runtime Compatibility by Format
|
|
310
|
+
|
|
311
|
+
| Format | Deno 2.x | Node.js 18+ | Node.js 20+ | Bun | Notes |
|
|
312
|
+
| ------ | -------- | ----------- | ----------- | --- | -------------------------------------------- |
|
|
313
|
+
| PNG | ✅ | ✅ | ✅ | ✅ | Pure-JS works everywhere |
|
|
314
|
+
| BMP | ✅ | ✅ | ✅ | ✅ | Pure-JS works everywhere |
|
|
315
|
+
| RAW | ✅ | ✅ | ✅ | ✅ | Pure-JS works everywhere |
|
|
316
|
+
| ASCII | ✅ | ✅ | ✅ | ✅ | Pure-JS works everywhere |
|
|
317
|
+
| GIF | ✅ | ✅ | ✅ | ✅ | Pure-JS works everywhere |
|
|
318
|
+
| JPEG | ✅ | ⚠️ Baseline | ✅ | ✅ | Node 18: pure-JS baseline only, 20+: full |
|
|
319
|
+
| WebP | ✅ | ⚠️ Lossless | ✅ | ✅ | Node 18: pure-JS lossless only, 20+: full |
|
|
320
|
+
| TIFF | ✅ | ✅ | ✅ | ✅ | Node 18: pure-JS uncompressed+LZW, 20+: full |
|
|
321
|
+
|
|
322
|
+
**Note**: For maximum compatibility across all runtimes, use PNG, BMP, GIF,
|
|
323
|
+
ASCII or RAW formats which have complete pure-JS implementations.
|
|
324
|
+
|
|
325
|
+
## Extending with Custom Formats
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
import { Image, type ImageData, type ImageFormat } from "@cross/image";
|
|
329
|
+
|
|
330
|
+
class MyCustomFormat implements ImageFormat {
|
|
331
|
+
readonly name = "custom";
|
|
332
|
+
readonly mimeType = "image/custom";
|
|
333
|
+
|
|
334
|
+
canDecode(data: Uint8Array): boolean {
|
|
335
|
+
// Check if data matches your format
|
|
336
|
+
return data[0] === 0x42; // Example magic byte
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async decode(data: Uint8Array): Promise<ImageData> {
|
|
340
|
+
// Decode your format to RGBA
|
|
341
|
+
return {
|
|
342
|
+
width: 100,
|
|
343
|
+
height: 100,
|
|
344
|
+
data: new Uint8Array(100 * 100 * 4),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async encode(imageData: ImageData): Promise<Uint8Array> {
|
|
349
|
+
// Encode RGBA to your format
|
|
350
|
+
return new Uint8Array([0x42 /* ... */]);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Register the format
|
|
355
|
+
Image.registerFormat(new MyCustomFormat());
|
|
356
|
+
|
|
357
|
+
// Now you can use it
|
|
358
|
+
const image = await Image.read(customData, "custom");
|
|
359
|
+
const output = await image.save("custom");
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## API Reference
|
|
363
|
+
|
|
364
|
+
### `Image`
|
|
365
|
+
|
|
366
|
+
The main class for working with images.
|
|
367
|
+
|
|
368
|
+
#### Static Methods
|
|
369
|
+
|
|
370
|
+
- `Image.read(data: Uint8Array, format?: string): Promise<Image>` - Read an
|
|
371
|
+
image from bytes
|
|
372
|
+
- `Image.readFrames(data: Uint8Array, format?: string): Promise<MultiFrameImageData>` -
|
|
373
|
+
Read all frames from a multi-frame image (animated GIF or multi-page TIFF)
|
|
374
|
+
- `Image.saveFrames(format: string, imageData: MultiFrameImageData, options?: unknown): Promise<Uint8Array>` -
|
|
375
|
+
Save multi-frame image data to bytes in the specified format
|
|
376
|
+
- `Image.fromRGBA(width: number, height: number, data: Uint8Array): Image` -
|
|
377
|
+
Create an image from raw RGBA data
|
|
378
|
+
- `Image.registerFormat(format: ImageFormat): void` - Register a custom format
|
|
379
|
+
- `Image.getFormats(): readonly ImageFormat[]` - Get all registered formats
|
|
380
|
+
|
|
381
|
+
#### Instance Properties
|
|
382
|
+
|
|
383
|
+
- `width: number` - Image width in pixels (read-only)
|
|
384
|
+
- `height: number` - Image height in pixels (read-only)
|
|
385
|
+
- `data: Uint8Array` - Raw RGBA pixel data (read-only)
|
|
386
|
+
|
|
387
|
+
#### Instance Methods
|
|
388
|
+
|
|
389
|
+
- `resize(options: ResizeOptions): this` - Resize the image (chainable)
|
|
390
|
+
- `save(format: string, options?: unknown): Promise<Uint8Array>` - Save to bytes
|
|
391
|
+
in specified format with optional format-specific options
|
|
392
|
+
- `clone(): Image` - Create a copy of the image
|
|
393
|
+
|
|
394
|
+
### Types
|
|
395
|
+
|
|
396
|
+
#### `ResizeOptions`
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
interface ResizeOptions {
|
|
400
|
+
width: number; // Target width
|
|
401
|
+
height: number; // Target height
|
|
402
|
+
method?: "nearest" | "bilinear"; // Resize algorithm (default: "bilinear")
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
#### `ASCIIOptions`
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
interface ASCIIOptions {
|
|
410
|
+
width?: number; // Target width in characters (default: 80)
|
|
411
|
+
charset?: "simple" | "extended" | "blocks" | "detailed"; // Character set (default: "simple")
|
|
412
|
+
aspectRatio?: number; // Aspect ratio correction for terminal (default: 0.5)
|
|
413
|
+
invert?: boolean; // Invert brightness (default: false)
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**Character sets:**
|
|
418
|
+
|
|
419
|
+
- `simple`: 10 characters (`.:-=+*#%@`) - good for basic art
|
|
420
|
+
- `extended`: 70 characters - detailed gradients
|
|
421
|
+
- `blocks`: 5 block characters (`░▒▓█`) - smooth gradients
|
|
422
|
+
- `detailed`: 92 characters - maximum detail
|
|
423
|
+
|
|
424
|
+
**Usage:**
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
const ascii = await image.save("ascii", {
|
|
428
|
+
width: 60,
|
|
429
|
+
charset: "blocks",
|
|
430
|
+
aspectRatio: 0.5,
|
|
431
|
+
invert: false,
|
|
432
|
+
});
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
#### `WebPEncodeOptions`
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
interface WebPEncodeOptions {
|
|
439
|
+
quality?: number; // Encoding quality 1-100 (default: 90)
|
|
440
|
+
lossless?: boolean; // Force lossless encoding (default: false)
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
**Quality levels:**
|
|
445
|
+
|
|
446
|
+
- `100`: Lossless encoding (VP8L without quantization)
|
|
447
|
+
- `90-99`: Very high quality with minimal quantization
|
|
448
|
+
- `70-89`: High quality with light quantization
|
|
449
|
+
- `50-69`: Medium quality with noticeable quantization
|
|
450
|
+
- `30-49`: Lower quality with heavy quantization
|
|
451
|
+
- `1-29`: Low quality with very heavy quantization
|
|
452
|
+
|
|
453
|
+
**Lossless flag:**
|
|
454
|
+
|
|
455
|
+
- When `lossless: true`, forces lossless VP8L encoding even if quality < 100
|
|
456
|
+
- Useful when you want pure-JS encoding without quantization
|
|
457
|
+
|
|
458
|
+
**Usage:**
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
// Lossless WebP
|
|
462
|
+
const lossless = await image.save("webp", { quality: 100 });
|
|
463
|
+
|
|
464
|
+
// Lossy WebP with high quality
|
|
465
|
+
const lossy = await image.save("webp", { quality: 85 });
|
|
466
|
+
|
|
467
|
+
// Force lossless in pure-JS
|
|
468
|
+
const forcedLossless = await image.save("webp", {
|
|
469
|
+
quality: 80,
|
|
470
|
+
lossless: true,
|
|
471
|
+
});
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
**Note:** When OffscreenCanvas is available (Deno, modern browsers, Bun), the
|
|
475
|
+
runtime's native WebP encoder is used for better compression and quality. In
|
|
476
|
+
pure-JS mode (Node.js without OffscreenCanvas), VP8L format with quality-based
|
|
477
|
+
color quantization is used for lossy encoding.
|
|
478
|
+
|
|
479
|
+
#### `TIFFEncodeOptions`
|
|
480
|
+
|
|
481
|
+
```ts
|
|
482
|
+
interface TIFFEncodeOptions {
|
|
483
|
+
compression?: "none" | "lzw"; // Compression method (default: "none")
|
|
484
|
+
}
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**Compression methods:**
|
|
488
|
+
|
|
489
|
+
- `none`: Uncompressed TIFF - larger file size, fastest encoding
|
|
490
|
+
- `lzw`: LZW compression - smaller file size for most images, lossless
|
|
491
|
+
|
|
492
|
+
**Usage:**
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
// Save as uncompressed TIFF (default)
|
|
496
|
+
const uncompressed = await image.save("tiff");
|
|
497
|
+
|
|
498
|
+
// Save with LZW compression
|
|
499
|
+
const compressed = await image.save("tiff", { compression: "lzw" });
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
#### `ImageData`
|
|
503
|
+
|
|
504
|
+
```ts
|
|
505
|
+
interface ImageData {
|
|
506
|
+
width: number; // Image width in pixels
|
|
507
|
+
height: number; // Image height in pixels
|
|
508
|
+
data: Uint8Array; // Raw RGBA data (4 bytes per pixel)
|
|
509
|
+
metadata?: ImageMetadata; // Optional metadata
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
#### `MultiFrameImageData`
|
|
514
|
+
|
|
515
|
+
```ts
|
|
516
|
+
interface MultiFrameImageData {
|
|
517
|
+
width: number; // Canvas width in pixels
|
|
518
|
+
height: number; // Canvas height in pixels
|
|
519
|
+
frames: ImageFrame[]; // Array of frames
|
|
520
|
+
metadata?: ImageMetadata; // Optional global metadata
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
#### `ImageFrame`
|
|
525
|
+
|
|
526
|
+
```ts
|
|
527
|
+
interface ImageFrame {
|
|
528
|
+
width: number; // Frame width in pixels
|
|
529
|
+
height: number; // Frame height in pixels
|
|
530
|
+
data: Uint8Array; // Raw RGBA data (4 bytes per pixel)
|
|
531
|
+
frameMetadata?: FrameMetadata; // Optional frame-specific metadata
|
|
532
|
+
}
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
#### `FrameMetadata`
|
|
536
|
+
|
|
537
|
+
```ts
|
|
538
|
+
interface FrameMetadata {
|
|
539
|
+
delay?: number; // Frame delay in milliseconds (for animations)
|
|
540
|
+
disposal?: "none" | "background" | "previous"; // Frame disposal method
|
|
541
|
+
left?: number; // X offset of frame within canvas
|
|
542
|
+
top?: number; // Y offset of frame within canvas
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
#### `ImageFormat`
|
|
547
|
+
|
|
548
|
+
```ts
|
|
549
|
+
interface ImageFormat {
|
|
550
|
+
readonly name: string; // Format name (e.g., "png")
|
|
551
|
+
readonly mimeType: string; // MIME type (e.g., "image/png")
|
|
552
|
+
canDecode(data: Uint8Array): boolean; // Check if data is in this format
|
|
553
|
+
decode(data: Uint8Array): Promise<ImageData>; // Decode to RGBA
|
|
554
|
+
encode(imageData: ImageData): Promise<Uint8Array>; // Encode from RGBA
|
|
555
|
+
decodeFrames?(data: Uint8Array): Promise<MultiFrameImageData>; // Decode all frames
|
|
556
|
+
encodeFrames?(
|
|
557
|
+
imageData: MultiFrameImageData,
|
|
558
|
+
options?: unknown,
|
|
559
|
+
): Promise<Uint8Array>; // Encode multi-frame
|
|
560
|
+
supportsMultipleFrames?(): boolean; // Check if format supports multiple frames
|
|
561
|
+
}
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
## Runtime Compatibility
|
|
565
|
+
|
|
566
|
+
- **Deno 2.x** - Full support for all formats
|
|
567
|
+
- **Node.js 18+** - Full support with pure-JS fallbacks for formats without
|
|
568
|
+
ImageDecoder
|
|
569
|
+
- **Node.js 20+** - Full support including ImageDecoder API for all formats
|
|
570
|
+
- **Bun** - Full support for all formats
|
|
571
|
+
|
|
572
|
+
The library automatically selects the best available implementation:
|
|
573
|
+
|
|
574
|
+
1. Pure-JS decoders/encoders are tried first when available
|
|
575
|
+
2. Native APIs (ImageDecoder, OffscreenCanvas) are used as fallbacks or for
|
|
576
|
+
formats without pure-JS support
|
|
577
|
+
3. Graceful degradation ensures maximum compatibility across runtimes
|
|
578
|
+
|
|
579
|
+
## Development
|
|
580
|
+
|
|
581
|
+
### Running Tests
|
|
582
|
+
|
|
583
|
+
```bash
|
|
584
|
+
deno test -A
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Linting and Formatting
|
|
588
|
+
|
|
589
|
+
```bash
|
|
590
|
+
deno fmt --check
|
|
591
|
+
deno lint
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### Type Checking
|
|
595
|
+
|
|
596
|
+
```bash
|
|
597
|
+
deno check mod.ts
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
## License
|
|
601
|
+
|
|
602
|
+
MIT License - see LICENSE file for details.
|
|
603
|
+
|
|
604
|
+
## Contributing
|
|
605
|
+
|
|
606
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
package/esm/mod.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @cross/image
|
|
3
|
+
*
|
|
4
|
+
* A pure JavaScript, dependency-free, cross-runtime image processing library.
|
|
5
|
+
* Supports reading, resizing, and saving common image formats (PNG, JPEG, WebP, GIF, TIFF, BMP, RAW).
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { Image } from "@cross/image";
|
|
10
|
+
*
|
|
11
|
+
* // Read an image
|
|
12
|
+
* const data = await Deno.readFile("input.png");
|
|
13
|
+
* const image = await Image.read(data);
|
|
14
|
+
*
|
|
15
|
+
* // Resize it
|
|
16
|
+
* image.resize({ width: 200, height: 200 });
|
|
17
|
+
*
|
|
18
|
+
* // Save as different format
|
|
19
|
+
* const output = await image.save("jpeg");
|
|
20
|
+
* await Deno.writeFile("output.jpg", output);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export { Image } from "./src/image.js";
|
|
24
|
+
export type { ASCIIOptions, FrameMetadata, ImageData, ImageFormat, ImageFrame, ImageMetadata, MultiFrameImageData, ResizeOptions, WebPEncodeOptions, } from "./src/types.js";
|
|
25
|
+
export { PNGFormat } from "./src/formats/png.js";
|
|
26
|
+
export { JPEGFormat } from "./src/formats/jpeg.js";
|
|
27
|
+
export { WebPFormat } from "./src/formats/webp.js";
|
|
28
|
+
export { GIFFormat } from "./src/formats/gif.js";
|
|
29
|
+
export { type TIFFEncodeOptions, TIFFFormat } from "./src/formats/tiff.js";
|
|
30
|
+
export { BMPFormat } from "./src/formats/bmp.js";
|
|
31
|
+
export { RAWFormat } from "./src/formats/raw.js";
|
|
32
|
+
export { ASCIIFormat } from "./src/formats/ascii.js";
|
|
33
|
+
//# sourceMappingURL=mod.d.ts.map
|
package/esm/mod.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACvC,YAAY,EACV,YAAY,EACZ,aAAa,EACb,SAAS,EACT,WAAW,EACX,UAAU,EACV,aAAa,EACb,mBAAmB,EACnB,aAAa,EACb,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,KAAK,iBAAiB,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAC3E,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC"}
|
package/esm/mod.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @cross/image
|
|
3
|
+
*
|
|
4
|
+
* A pure JavaScript, dependency-free, cross-runtime image processing library.
|
|
5
|
+
* Supports reading, resizing, and saving common image formats (PNG, JPEG, WebP, GIF, TIFF, BMP, RAW).
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { Image } from "@cross/image";
|
|
10
|
+
*
|
|
11
|
+
* // Read an image
|
|
12
|
+
* const data = await Deno.readFile("input.png");
|
|
13
|
+
* const image = await Image.read(data);
|
|
14
|
+
*
|
|
15
|
+
* // Resize it
|
|
16
|
+
* image.resize({ width: 200, height: 200 });
|
|
17
|
+
*
|
|
18
|
+
* // Save as different format
|
|
19
|
+
* const output = await image.save("jpeg");
|
|
20
|
+
* await Deno.writeFile("output.jpg", output);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export { Image } from "./src/image.js";
|
|
24
|
+
export { PNGFormat } from "./src/formats/png.js";
|
|
25
|
+
export { JPEGFormat } from "./src/formats/jpeg.js";
|
|
26
|
+
export { WebPFormat } from "./src/formats/webp.js";
|
|
27
|
+
export { GIFFormat } from "./src/formats/gif.js";
|
|
28
|
+
export { TIFFFormat } from "./src/formats/tiff.js";
|
|
29
|
+
export { BMPFormat } from "./src/formats/bmp.js";
|
|
30
|
+
export { RAWFormat } from "./src/formats/raw.js";
|
|
31
|
+
export { ASCIIFormat } from "./src/formats/ascii.js";
|
package/esm/package.json
ADDED