cross-image 0.1.2 → 0.1.5
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 +143 -606
- package/esm/mod.d.ts +5 -5
- package/esm/mod.js +5 -5
- package/esm/src/formats/ascii.d.ts +11 -0
- package/esm/src/formats/ascii.js +14 -0
- package/esm/src/formats/bmp.d.ts +17 -0
- package/esm/src/formats/bmp.js +20 -0
- package/esm/src/formats/gif.d.ts +21 -0
- package/esm/src/formats/gif.js +26 -0
- package/esm/src/formats/jpeg.d.ts +17 -0
- package/esm/src/formats/jpeg.js +20 -0
- package/esm/src/formats/png.d.ts +17 -0
- package/esm/src/formats/png.js +20 -0
- package/esm/src/formats/raw.d.ts +17 -0
- package/esm/src/formats/raw.js +20 -0
- package/esm/src/formats/tiff.d.ts +16 -0
- package/esm/src/formats/tiff.js +37 -12
- package/esm/src/formats/webp.d.ts +18 -0
- package/esm/src/formats/webp.js +21 -0
- package/esm/src/image.d.ts +34 -1
- package/esm/src/image.js +55 -9
- package/esm/src/utils/security.d.ts +28 -0
- package/esm/src/utils/security.js +48 -0
- package/esm/src/utils/webp_decoder.js +3 -0
- package/package.json +1 -4
- package/script/mod.d.ts +5 -5
- package/script/mod.js +5 -5
- package/script/src/formats/ascii.d.ts +11 -0
- package/script/src/formats/ascii.js +14 -0
- package/script/src/formats/bmp.d.ts +17 -0
- package/script/src/formats/bmp.js +20 -0
- package/script/src/formats/gif.d.ts +21 -0
- package/script/src/formats/gif.js +26 -0
- package/script/src/formats/jpeg.d.ts +17 -0
- package/script/src/formats/jpeg.js +20 -0
- package/script/src/formats/png.d.ts +17 -0
- package/script/src/formats/png.js +20 -0
- package/script/src/formats/raw.d.ts +17 -0
- package/script/src/formats/raw.js +20 -0
- package/script/src/formats/tiff.d.ts +16 -0
- package/script/src/formats/tiff.js +37 -12
- package/script/src/formats/webp.d.ts +18 -0
- package/script/src/formats/webp.js +21 -0
- package/script/src/image.d.ts +34 -1
- package/script/src/image.js +55 -9
- package/script/src/utils/security.d.ts +28 -0
- package/script/src/utils/security.js +53 -0
- package/script/src/utils/webp_decoder.js +3 -0
- package/esm/mod.d.ts.map +0 -1
- package/esm/src/formats/ascii.d.ts.map +0 -1
- package/esm/src/formats/bmp.d.ts.map +0 -1
- package/esm/src/formats/gif.d.ts.map +0 -1
- package/esm/src/formats/jpeg.d.ts.map +0 -1
- package/esm/src/formats/png.d.ts.map +0 -1
- package/esm/src/formats/raw.d.ts.map +0 -1
- package/esm/src/formats/tiff.d.ts.map +0 -1
- package/esm/src/formats/webp.d.ts.map +0 -1
- package/esm/src/image.d.ts.map +0 -1
- package/esm/src/types.d.ts.map +0 -1
- package/esm/src/utils/gif_decoder.d.ts.map +0 -1
- package/esm/src/utils/gif_encoder.d.ts.map +0 -1
- package/esm/src/utils/jpeg_decoder.d.ts.map +0 -1
- package/esm/src/utils/jpeg_encoder.d.ts.map +0 -1
- package/esm/src/utils/lzw.d.ts.map +0 -1
- package/esm/src/utils/resize.d.ts.map +0 -1
- package/esm/src/utils/tiff_lzw.d.ts.map +0 -1
- package/esm/src/utils/webp_decoder.d.ts.map +0 -1
- package/esm/src/utils/webp_encoder.d.ts.map +0 -1
- package/script/mod.d.ts.map +0 -1
- package/script/src/formats/ascii.d.ts.map +0 -1
- package/script/src/formats/bmp.d.ts.map +0 -1
- package/script/src/formats/gif.d.ts.map +0 -1
- package/script/src/formats/jpeg.d.ts.map +0 -1
- package/script/src/formats/png.d.ts.map +0 -1
- package/script/src/formats/raw.d.ts.map +0 -1
- package/script/src/formats/tiff.d.ts.map +0 -1
- package/script/src/formats/webp.d.ts.map +0 -1
- package/script/src/image.d.ts.map +0 -1
- package/script/src/types.d.ts.map +0 -1
- package/script/src/utils/gif_decoder.d.ts.map +0 -1
- package/script/src/utils/gif_encoder.d.ts.map +0 -1
- package/script/src/utils/jpeg_decoder.d.ts.map +0 -1
- package/script/src/utils/jpeg_encoder.d.ts.map +0 -1
- package/script/src/utils/lzw.d.ts.map +0 -1
- package/script/src/utils/resize.d.ts.map +0 -1
- package/script/src/utils/tiff_lzw.d.ts.map +0 -1
- package/script/src/utils/webp_decoder.d.ts.map +0 -1
- package/script/src/utils/webp_encoder.d.ts.map +0 -1
package/esm/src/formats/tiff.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TIFFLZWDecoder, TIFFLZWEncoder } from "../utils/tiff_lzw.js";
|
|
2
|
+
import { validateImageDimensions } from "../utils/security.js";
|
|
2
3
|
// Constants for unit conversions
|
|
3
4
|
const DEFAULT_DPI = 72;
|
|
4
5
|
/**
|
|
@@ -10,12 +11,14 @@ const DEFAULT_DPI = 72;
|
|
|
10
11
|
*/
|
|
11
12
|
export class TIFFFormat {
|
|
12
13
|
constructor() {
|
|
14
|
+
/** Format name identifier */
|
|
13
15
|
Object.defineProperty(this, "name", {
|
|
14
16
|
enumerable: true,
|
|
15
17
|
configurable: true,
|
|
16
18
|
writable: true,
|
|
17
19
|
value: "tiff"
|
|
18
20
|
});
|
|
21
|
+
/** MIME type for TIFF images */
|
|
19
22
|
Object.defineProperty(this, "mimeType", {
|
|
20
23
|
enumerable: true,
|
|
21
24
|
configurable: true,
|
|
@@ -23,9 +26,18 @@ export class TIFFFormat {
|
|
|
23
26
|
value: "image/tiff"
|
|
24
27
|
});
|
|
25
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Check if this format supports multiple frames (pages)
|
|
31
|
+
* @returns true for TIFF format
|
|
32
|
+
*/
|
|
26
33
|
supportsMultipleFrames() {
|
|
27
34
|
return true;
|
|
28
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Check if the given data is a TIFF image
|
|
38
|
+
* @param data Raw image data to check
|
|
39
|
+
* @returns true if data has TIFF signature
|
|
40
|
+
*/
|
|
29
41
|
canDecode(data) {
|
|
30
42
|
// TIFF signature: "II" (little-endian) or "MM" (big-endian) followed by 42
|
|
31
43
|
return data.length >= 4 &&
|
|
@@ -35,6 +47,11 @@ export class TIFFFormat {
|
|
|
35
47
|
data[3] === 0x2a) // "MM\0*"
|
|
36
48
|
);
|
|
37
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Decode TIFF image data to RGBA (first page only)
|
|
52
|
+
* @param data Raw TIFF image data
|
|
53
|
+
* @returns Decoded image data with RGBA pixels of first page
|
|
54
|
+
*/
|
|
38
55
|
async decode(data) {
|
|
39
56
|
if (!this.canDecode(data)) {
|
|
40
57
|
throw new Error("Invalid TIFF signature");
|
|
@@ -144,7 +161,7 @@ export class TIFFFormat {
|
|
|
144
161
|
// IFD (Image File Directory)
|
|
145
162
|
const ifdStart = result.length;
|
|
146
163
|
// Count number of entries (including metadata)
|
|
147
|
-
let numEntries =
|
|
164
|
+
let numEntries = 12; // Base entries (including ExtraSamples)
|
|
148
165
|
if (metadata?.description)
|
|
149
166
|
numEntries++;
|
|
150
167
|
if (metadata?.author)
|
|
@@ -184,6 +201,8 @@ export class TIFFFormat {
|
|
|
184
201
|
const yResOffset = dataOffset;
|
|
185
202
|
this.writeIFDEntry(result, 0x011b, 5, 1, yResOffset);
|
|
186
203
|
dataOffset += 8;
|
|
204
|
+
// ExtraSamples (0x0152) - 2 = unassociated alpha
|
|
205
|
+
this.writeIFDEntry(result, 0x0152, 3, 1, 2);
|
|
187
206
|
// Optional metadata entries
|
|
188
207
|
if (metadata?.description) {
|
|
189
208
|
const descBytes = new TextEncoder().encode(metadata.description + "\0");
|
|
@@ -210,6 +229,11 @@ export class TIFFFormat {
|
|
|
210
229
|
// Next IFD offset (0 = no more IFDs)
|
|
211
230
|
this.writeUint32LE(result, 0);
|
|
212
231
|
// Write variable-length data
|
|
232
|
+
// BitsPerSample values (must be written first to match offset calculation)
|
|
233
|
+
this.writeUint16LE(result, 8);
|
|
234
|
+
this.writeUint16LE(result, 8);
|
|
235
|
+
this.writeUint16LE(result, 8);
|
|
236
|
+
this.writeUint16LE(result, 8);
|
|
213
237
|
// XResolution value (rational)
|
|
214
238
|
const dpiX = metadata?.dpiX ?? DEFAULT_DPI;
|
|
215
239
|
this.writeUint32LE(result, dpiX);
|
|
@@ -218,11 +242,6 @@ export class TIFFFormat {
|
|
|
218
242
|
const dpiY = metadata?.dpiY ?? DEFAULT_DPI;
|
|
219
243
|
this.writeUint32LE(result, dpiY);
|
|
220
244
|
this.writeUint32LE(result, 1);
|
|
221
|
-
// BitsPerSample values
|
|
222
|
-
this.writeUint16LE(result, 8);
|
|
223
|
-
this.writeUint16LE(result, 8);
|
|
224
|
-
this.writeUint16LE(result, 8);
|
|
225
|
-
this.writeUint16LE(result, 8);
|
|
226
245
|
// Write metadata strings
|
|
227
246
|
if (metadata?.description) {
|
|
228
247
|
const descBytes = new TextEncoder().encode(metadata.description + "\0");
|
|
@@ -358,7 +377,7 @@ export class TIFFFormat {
|
|
|
358
377
|
ifdOffsets.push(currentOffset);
|
|
359
378
|
const ifdStart = result.length;
|
|
360
379
|
// Count number of entries (including metadata only for first page)
|
|
361
|
-
let numEntries =
|
|
380
|
+
let numEntries = 12; // Base entries (including ExtraSamples)
|
|
362
381
|
if (i === 0 && imageData.metadata) {
|
|
363
382
|
if (imageData.metadata.description)
|
|
364
383
|
numEntries++;
|
|
@@ -402,6 +421,8 @@ export class TIFFFormat {
|
|
|
402
421
|
const yResOffset = dataOffset;
|
|
403
422
|
this.writeIFDEntry(result, 0x011b, 5, 1, yResOffset);
|
|
404
423
|
dataOffset += 8;
|
|
424
|
+
// ExtraSamples (0x0152) - 2 = unassociated alpha
|
|
425
|
+
this.writeIFDEntry(result, 0x0152, 3, 1, 2);
|
|
405
426
|
// Metadata (only for first page)
|
|
406
427
|
if (i === 0 && imageData.metadata) {
|
|
407
428
|
if (imageData.metadata.description) {
|
|
@@ -432,6 +453,11 @@ export class TIFFFormat {
|
|
|
432
453
|
this.writeUint32LE(result, nextIFDOffset);
|
|
433
454
|
currentOffset = dataOffset;
|
|
434
455
|
// Write variable-length data
|
|
456
|
+
// BitsPerSample values (must be written first to match offset calculation)
|
|
457
|
+
this.writeUint16LE(result, 8);
|
|
458
|
+
this.writeUint16LE(result, 8);
|
|
459
|
+
this.writeUint16LE(result, 8);
|
|
460
|
+
this.writeUint16LE(result, 8);
|
|
435
461
|
// XResolution value (rational)
|
|
436
462
|
const dpiX = (i === 0 && imageData.metadata?.dpiX) ||
|
|
437
463
|
DEFAULT_DPI;
|
|
@@ -442,11 +468,6 @@ export class TIFFFormat {
|
|
|
442
468
|
DEFAULT_DPI;
|
|
443
469
|
this.writeUint32LE(result, dpiY);
|
|
444
470
|
this.writeUint32LE(result, 1);
|
|
445
|
-
// BitsPerSample values
|
|
446
|
-
this.writeUint16LE(result, 8);
|
|
447
|
-
this.writeUint16LE(result, 8);
|
|
448
|
-
this.writeUint16LE(result, 8);
|
|
449
|
-
this.writeUint16LE(result, 8);
|
|
450
471
|
// Write metadata strings (only for first page)
|
|
451
472
|
if (i === 0 && imageData.metadata) {
|
|
452
473
|
if (imageData.metadata.description) {
|
|
@@ -707,6 +728,8 @@ export class TIFFFormat {
|
|
|
707
728
|
// Uncompressed
|
|
708
729
|
pixelData = data.slice(stripOffset, stripOffset + stripByteCount);
|
|
709
730
|
}
|
|
731
|
+
// Validate dimensions for security (prevent integer overflow and heap exhaustion)
|
|
732
|
+
validateImageDimensions(width, height);
|
|
710
733
|
// Convert to RGBA
|
|
711
734
|
const rgba = new Uint8Array(width * height * 4);
|
|
712
735
|
let srcPos = 0;
|
|
@@ -770,6 +793,8 @@ export class TIFFFormat {
|
|
|
770
793
|
// Uncompressed
|
|
771
794
|
pixelData = data.slice(stripOffset, stripOffset + stripByteCount);
|
|
772
795
|
}
|
|
796
|
+
// Validate dimensions for security (prevent integer overflow and heap exhaustion)
|
|
797
|
+
validateImageDimensions(width, height);
|
|
773
798
|
// Convert to RGBA
|
|
774
799
|
const rgba = new Uint8Array(width * height * 4);
|
|
775
800
|
let srcPos = 0;
|
|
@@ -4,10 +4,28 @@ import type { ImageData, ImageFormat, WebPEncodeOptions } from "../types.js";
|
|
|
4
4
|
* Implements a basic WebP decoder and encoder
|
|
5
5
|
*/
|
|
6
6
|
export declare class WebPFormat implements ImageFormat {
|
|
7
|
+
/** Format name identifier */
|
|
7
8
|
readonly name = "webp";
|
|
9
|
+
/** MIME type for WebP images */
|
|
8
10
|
readonly mimeType = "image/webp";
|
|
11
|
+
/**
|
|
12
|
+
* Check if the given data is a WebP image
|
|
13
|
+
* @param data Raw image data to check
|
|
14
|
+
* @returns true if data has WebP signature
|
|
15
|
+
*/
|
|
9
16
|
canDecode(data: Uint8Array): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Decode WebP image data to RGBA
|
|
19
|
+
* @param data Raw WebP image data
|
|
20
|
+
* @returns Decoded image data with RGBA pixels
|
|
21
|
+
*/
|
|
10
22
|
decode(data: Uint8Array): Promise<ImageData>;
|
|
23
|
+
/**
|
|
24
|
+
* Encode RGBA image data to WebP format
|
|
25
|
+
* @param imageData Image data to encode
|
|
26
|
+
* @param options Optional WebP encoding options
|
|
27
|
+
* @returns Encoded WebP image bytes
|
|
28
|
+
*/
|
|
11
29
|
encode(imageData: ImageData, options?: WebPEncodeOptions): Promise<Uint8Array>;
|
|
12
30
|
private readUint32LE;
|
|
13
31
|
private readUint24LE;
|
package/esm/src/formats/webp.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateImageDimensions } from "../utils/security.js";
|
|
1
2
|
// Default quality for WebP encoding when not specified
|
|
2
3
|
const DEFAULT_WEBP_QUALITY = 90;
|
|
3
4
|
/**
|
|
@@ -6,12 +7,14 @@ const DEFAULT_WEBP_QUALITY = 90;
|
|
|
6
7
|
*/
|
|
7
8
|
export class WebPFormat {
|
|
8
9
|
constructor() {
|
|
10
|
+
/** Format name identifier */
|
|
9
11
|
Object.defineProperty(this, "name", {
|
|
10
12
|
enumerable: true,
|
|
11
13
|
configurable: true,
|
|
12
14
|
writable: true,
|
|
13
15
|
value: "webp"
|
|
14
16
|
});
|
|
17
|
+
/** MIME type for WebP images */
|
|
15
18
|
Object.defineProperty(this, "mimeType", {
|
|
16
19
|
enumerable: true,
|
|
17
20
|
configurable: true,
|
|
@@ -19,6 +22,11 @@ export class WebPFormat {
|
|
|
19
22
|
value: "image/webp"
|
|
20
23
|
});
|
|
21
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Check if the given data is a WebP image
|
|
27
|
+
* @param data Raw image data to check
|
|
28
|
+
* @returns true if data has WebP signature
|
|
29
|
+
*/
|
|
22
30
|
canDecode(data) {
|
|
23
31
|
// WebP signature: "RIFF" + size + "WEBP"
|
|
24
32
|
return data.length >= 12 &&
|
|
@@ -27,6 +35,11 @@ export class WebPFormat {
|
|
|
27
35
|
data[8] === 0x57 && data[9] === 0x45 && // "WE"
|
|
28
36
|
data[10] === 0x42 && data[11] === 0x50; // "BP"
|
|
29
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Decode WebP image data to RGBA
|
|
40
|
+
* @param data Raw WebP image data
|
|
41
|
+
* @returns Decoded image data with RGBA pixels
|
|
42
|
+
*/
|
|
30
43
|
async decode(data) {
|
|
31
44
|
if (!this.canDecode(data)) {
|
|
32
45
|
throw new Error("Invalid WebP signature");
|
|
@@ -89,6 +102,8 @@ export class WebPFormat {
|
|
|
89
102
|
if (width === 0 || height === 0) {
|
|
90
103
|
throw new Error("Could not determine WebP dimensions");
|
|
91
104
|
}
|
|
105
|
+
// Validate dimensions for security (prevent integer overflow and heap exhaustion)
|
|
106
|
+
validateImageDimensions(width, height);
|
|
92
107
|
// For a pure JS implementation, we'd need to implement full WebP decoding
|
|
93
108
|
// which is very complex. Instead, we'll use the browser/runtime's decoder.
|
|
94
109
|
const rgba = await this.decodeUsingRuntime(data, width, height);
|
|
@@ -99,6 +114,12 @@ export class WebPFormat {
|
|
|
99
114
|
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
100
115
|
};
|
|
101
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Encode RGBA image data to WebP format
|
|
119
|
+
* @param imageData Image data to encode
|
|
120
|
+
* @param options Optional WebP encoding options
|
|
121
|
+
* @returns Encoded WebP image bytes
|
|
122
|
+
*/
|
|
102
123
|
async encode(imageData, options) {
|
|
103
124
|
const { width, height, data, metadata } = imageData;
|
|
104
125
|
const quality = options?.quality ?? DEFAULT_WEBP_QUALITY;
|
package/esm/src/image.d.ts
CHANGED
|
@@ -72,24 +72,49 @@ export declare class Image {
|
|
|
72
72
|
* Get all registered formats
|
|
73
73
|
*/
|
|
74
74
|
static getFormats(): readonly ImageFormat[];
|
|
75
|
+
/**
|
|
76
|
+
* Decode an image from bytes
|
|
77
|
+
* @param data Raw image data
|
|
78
|
+
* @param format Optional format hint (e.g., "png", "jpeg", "webp")
|
|
79
|
+
* @returns Image instance
|
|
80
|
+
*/
|
|
81
|
+
static decode(data: Uint8Array, format?: string): Promise<Image>;
|
|
75
82
|
/**
|
|
76
83
|
* Read an image from bytes
|
|
84
|
+
* @deprecated Use `decode()` instead. This method will be removed in a future version.
|
|
77
85
|
* @param data Raw image data
|
|
78
86
|
* @param format Optional format hint (e.g., "png", "jpeg", "webp")
|
|
79
87
|
* @returns Image instance
|
|
80
88
|
*/
|
|
81
89
|
static read(data: Uint8Array, format?: string): Promise<Image>;
|
|
90
|
+
/**
|
|
91
|
+
* Decode all frames from a multi-frame image (GIF animation, multi-page TIFF)
|
|
92
|
+
* @param data Raw image data
|
|
93
|
+
* @param format Optional format hint (e.g., "gif", "tiff")
|
|
94
|
+
* @returns MultiFrameImageData with all frames
|
|
95
|
+
*/
|
|
96
|
+
static decodeFrames(data: Uint8Array, format?: string): Promise<MultiFrameImageData>;
|
|
82
97
|
/**
|
|
83
98
|
* Read all frames from a multi-frame image (GIF animation, multi-page TIFF)
|
|
99
|
+
* @deprecated Use `decodeFrames()` instead. This method will be removed in a future version.
|
|
84
100
|
* @param data Raw image data
|
|
85
101
|
* @param format Optional format hint (e.g., "gif", "tiff")
|
|
86
102
|
* @returns MultiFrameImageData with all frames
|
|
87
103
|
*/
|
|
88
104
|
static readFrames(data: Uint8Array, format?: string): Promise<MultiFrameImageData>;
|
|
105
|
+
/**
|
|
106
|
+
* Encode multi-frame image data to bytes in the specified format
|
|
107
|
+
* @param format Format name (e.g., "gif", "tiff")
|
|
108
|
+
* @param imageData Multi-frame image data to encode
|
|
109
|
+
* @param options Optional format-specific encoding options
|
|
110
|
+
* @returns Encoded image bytes
|
|
111
|
+
*/
|
|
112
|
+
static encodeFrames(format: string, imageData: MultiFrameImageData, options?: unknown): Promise<Uint8Array>;
|
|
89
113
|
/**
|
|
90
114
|
* Save multi-frame image data to bytes in the specified format
|
|
115
|
+
* @deprecated Use `encodeFrames()` instead. This method will be removed in a future version.
|
|
91
116
|
* @param format Format name (e.g., "gif", "tiff")
|
|
92
|
-
* @param imageData Multi-frame image data to
|
|
117
|
+
* @param imageData Multi-frame image data to encode
|
|
93
118
|
* @param options Optional format-specific encoding options
|
|
94
119
|
* @returns Encoded image bytes
|
|
95
120
|
*/
|
|
@@ -108,8 +133,16 @@ export declare class Image {
|
|
|
108
133
|
* @returns This image instance for chaining
|
|
109
134
|
*/
|
|
110
135
|
resize(options: ResizeOptions): this;
|
|
136
|
+
/**
|
|
137
|
+
* Encode the image to bytes in the specified format
|
|
138
|
+
* @param format Format name (e.g., "png", "jpeg", "webp", "ascii")
|
|
139
|
+
* @param options Optional format-specific encoding options
|
|
140
|
+
* @returns Encoded image bytes
|
|
141
|
+
*/
|
|
142
|
+
encode(format: string, options?: unknown): Promise<Uint8Array>;
|
|
111
143
|
/**
|
|
112
144
|
* Save the image to bytes in the specified format
|
|
145
|
+
* @deprecated Use `encode()` instead. This method will be removed in a future version.
|
|
113
146
|
* @param format Format name (e.g., "png", "jpeg", "webp", "ascii")
|
|
114
147
|
* @param options Optional format-specific encoding options
|
|
115
148
|
* @returns Encoded image bytes
|
package/esm/src/image.js
CHANGED
|
@@ -7,6 +7,7 @@ import { TIFFFormat } from "./formats/tiff.js";
|
|
|
7
7
|
import { BMPFormat } from "./formats/bmp.js";
|
|
8
8
|
import { RAWFormat } from "./formats/raw.js";
|
|
9
9
|
import { ASCIIFormat } from "./formats/ascii.js";
|
|
10
|
+
import { validateImageDimensions } from "./utils/security.js";
|
|
10
11
|
/**
|
|
11
12
|
* Main Image class for reading, manipulating, and saving images
|
|
12
13
|
*/
|
|
@@ -150,12 +151,12 @@ export class Image {
|
|
|
150
151
|
return Image.formats;
|
|
151
152
|
}
|
|
152
153
|
/**
|
|
153
|
-
*
|
|
154
|
+
* Decode an image from bytes
|
|
154
155
|
* @param data Raw image data
|
|
155
156
|
* @param format Optional format hint (e.g., "png", "jpeg", "webp")
|
|
156
157
|
* @returns Image instance
|
|
157
158
|
*/
|
|
158
|
-
static async
|
|
159
|
+
static async decode(data, format) {
|
|
159
160
|
const image = new Image();
|
|
160
161
|
// Try specified format first
|
|
161
162
|
if (format) {
|
|
@@ -175,12 +176,22 @@ export class Image {
|
|
|
175
176
|
throw new Error("Unsupported or unrecognized image format");
|
|
176
177
|
}
|
|
177
178
|
/**
|
|
178
|
-
* Read
|
|
179
|
+
* Read an image from bytes
|
|
180
|
+
* @deprecated Use `decode()` instead. This method will be removed in a future version.
|
|
181
|
+
* @param data Raw image data
|
|
182
|
+
* @param format Optional format hint (e.g., "png", "jpeg", "webp")
|
|
183
|
+
* @returns Image instance
|
|
184
|
+
*/
|
|
185
|
+
static read(data, format) {
|
|
186
|
+
return Image.decode(data, format);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Decode all frames from a multi-frame image (GIF animation, multi-page TIFF)
|
|
179
190
|
* @param data Raw image data
|
|
180
191
|
* @param format Optional format hint (e.g., "gif", "tiff")
|
|
181
192
|
* @returns MultiFrameImageData with all frames
|
|
182
193
|
*/
|
|
183
|
-
static async
|
|
194
|
+
static async decodeFrames(data, format) {
|
|
184
195
|
// Try specified format first
|
|
185
196
|
if (format) {
|
|
186
197
|
const handler = Image.formats.find((f) => f.name === format);
|
|
@@ -197,13 +208,23 @@ export class Image {
|
|
|
197
208
|
throw new Error("Unsupported or unrecognized multi-frame image format");
|
|
198
209
|
}
|
|
199
210
|
/**
|
|
200
|
-
*
|
|
211
|
+
* Read all frames from a multi-frame image (GIF animation, multi-page TIFF)
|
|
212
|
+
* @deprecated Use `decodeFrames()` instead. This method will be removed in a future version.
|
|
213
|
+
* @param data Raw image data
|
|
214
|
+
* @param format Optional format hint (e.g., "gif", "tiff")
|
|
215
|
+
* @returns MultiFrameImageData with all frames
|
|
216
|
+
*/
|
|
217
|
+
static readFrames(data, format) {
|
|
218
|
+
return Image.decodeFrames(data, format);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Encode multi-frame image data to bytes in the specified format
|
|
201
222
|
* @param format Format name (e.g., "gif", "tiff")
|
|
202
|
-
* @param imageData Multi-frame image data to
|
|
223
|
+
* @param imageData Multi-frame image data to encode
|
|
203
224
|
* @param options Optional format-specific encoding options
|
|
204
225
|
* @returns Encoded image bytes
|
|
205
226
|
*/
|
|
206
|
-
static async
|
|
227
|
+
static async encodeFrames(format, imageData, options) {
|
|
207
228
|
const handler = Image.formats.find((f) => f.name === format);
|
|
208
229
|
if (!handler) {
|
|
209
230
|
throw new Error(`Unsupported format: ${format}`);
|
|
@@ -213,6 +234,17 @@ export class Image {
|
|
|
213
234
|
}
|
|
214
235
|
return await handler.encodeFrames(imageData, options);
|
|
215
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Save multi-frame image data to bytes in the specified format
|
|
239
|
+
* @deprecated Use `encodeFrames()` instead. This method will be removed in a future version.
|
|
240
|
+
* @param format Format name (e.g., "gif", "tiff")
|
|
241
|
+
* @param imageData Multi-frame image data to encode
|
|
242
|
+
* @param options Optional format-specific encoding options
|
|
243
|
+
* @returns Encoded image bytes
|
|
244
|
+
*/
|
|
245
|
+
static saveFrames(format, imageData, options) {
|
|
246
|
+
return Image.encodeFrames(format, imageData, options);
|
|
247
|
+
}
|
|
216
248
|
/**
|
|
217
249
|
* Create an image from raw RGBA data
|
|
218
250
|
* @param width Image width
|
|
@@ -221,6 +253,8 @@ export class Image {
|
|
|
221
253
|
* @returns Image instance
|
|
222
254
|
*/
|
|
223
255
|
static fromRGBA(width, height, data) {
|
|
256
|
+
// Validate dimensions for security (prevent integer overflow and heap exhaustion)
|
|
257
|
+
validateImageDimensions(width, height);
|
|
224
258
|
if (data.length !== width * height * 4) {
|
|
225
259
|
throw new Error(`Data length mismatch: expected ${width * height * 4}, got ${data.length}`);
|
|
226
260
|
}
|
|
@@ -237,6 +271,8 @@ export class Image {
|
|
|
237
271
|
if (!this.imageData)
|
|
238
272
|
throw new Error("No image loaded");
|
|
239
273
|
const { width, height, method = "bilinear" } = options;
|
|
274
|
+
// Validate new dimensions for security (prevent integer overflow and heap exhaustion)
|
|
275
|
+
validateImageDimensions(width, height);
|
|
240
276
|
const { data: srcData, width: srcWidth, height: srcHeight } = this.imageData;
|
|
241
277
|
let resizedData;
|
|
242
278
|
if (method === "nearest") {
|
|
@@ -265,12 +301,12 @@ export class Image {
|
|
|
265
301
|
return this;
|
|
266
302
|
}
|
|
267
303
|
/**
|
|
268
|
-
*
|
|
304
|
+
* Encode the image to bytes in the specified format
|
|
269
305
|
* @param format Format name (e.g., "png", "jpeg", "webp", "ascii")
|
|
270
306
|
* @param options Optional format-specific encoding options
|
|
271
307
|
* @returns Encoded image bytes
|
|
272
308
|
*/
|
|
273
|
-
async
|
|
309
|
+
async encode(format, options) {
|
|
274
310
|
if (!this.imageData)
|
|
275
311
|
throw new Error("No image loaded");
|
|
276
312
|
const handler = Image.formats.find((f) => f.name === format);
|
|
@@ -279,6 +315,16 @@ export class Image {
|
|
|
279
315
|
}
|
|
280
316
|
return await handler.encode(this.imageData, options);
|
|
281
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Save the image to bytes in the specified format
|
|
320
|
+
* @deprecated Use `encode()` instead. This method will be removed in a future version.
|
|
321
|
+
* @param format Format name (e.g., "png", "jpeg", "webp", "ascii")
|
|
322
|
+
* @param options Optional format-specific encoding options
|
|
323
|
+
* @returns Encoded image bytes
|
|
324
|
+
*/
|
|
325
|
+
save(format, options) {
|
|
326
|
+
return this.encode(format, options);
|
|
327
|
+
}
|
|
282
328
|
/**
|
|
283
329
|
* Clone this image
|
|
284
330
|
* @returns New image instance with copied data and metadata
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security utilities for image processing
|
|
3
|
+
* Prevents integer overflow, heap exhaustion, and decompression bombs
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Maximum safe image dimensions
|
|
7
|
+
* These limits prevent integer overflow and heap exhaustion attacks
|
|
8
|
+
*/
|
|
9
|
+
export declare const MAX_IMAGE_DIMENSION = 65535;
|
|
10
|
+
export declare const MAX_IMAGE_PIXELS = 178956970;
|
|
11
|
+
/**
|
|
12
|
+
* Validates image dimensions to prevent security vulnerabilities
|
|
13
|
+
*
|
|
14
|
+
* @param width Image width in pixels
|
|
15
|
+
* @param height Image height in pixels
|
|
16
|
+
* @throws Error if dimensions are invalid or unsafe
|
|
17
|
+
*/
|
|
18
|
+
export declare function validateImageDimensions(width: number, height: number): void;
|
|
19
|
+
/**
|
|
20
|
+
* Safely calculates buffer size for RGBA image data
|
|
21
|
+
*
|
|
22
|
+
* @param width Image width in pixels
|
|
23
|
+
* @param height Image height in pixels
|
|
24
|
+
* @returns Buffer size in bytes (width * height * 4)
|
|
25
|
+
* @throws Error if calculation would overflow
|
|
26
|
+
*/
|
|
27
|
+
export declare function calculateBufferSize(width: number, height: number): number;
|
|
28
|
+
//# sourceMappingURL=security.d.ts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security utilities for image processing
|
|
3
|
+
* Prevents integer overflow, heap exhaustion, and decompression bombs
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Maximum safe image dimensions
|
|
7
|
+
* These limits prevent integer overflow and heap exhaustion attacks
|
|
8
|
+
*/
|
|
9
|
+
export const MAX_IMAGE_DIMENSION = 65535; // 2^16 - 1 (reasonable for most use cases)
|
|
10
|
+
export const MAX_IMAGE_PIXELS = 178956970; // ~179 megapixels (fits safely in memory)
|
|
11
|
+
/**
|
|
12
|
+
* Validates image dimensions to prevent security vulnerabilities
|
|
13
|
+
*
|
|
14
|
+
* @param width Image width in pixels
|
|
15
|
+
* @param height Image height in pixels
|
|
16
|
+
* @throws Error if dimensions are invalid or unsafe
|
|
17
|
+
*/
|
|
18
|
+
export function validateImageDimensions(width, height) {
|
|
19
|
+
// Check for negative or zero dimensions
|
|
20
|
+
if (width <= 0 || height <= 0) {
|
|
21
|
+
throw new Error(`Invalid image dimensions: ${width}x${height} (dimensions must be positive)`);
|
|
22
|
+
}
|
|
23
|
+
// Check if dimensions are integers
|
|
24
|
+
if (!Number.isInteger(width) || !Number.isInteger(height)) {
|
|
25
|
+
throw new Error(`Invalid image dimensions: ${width}x${height} (dimensions must be integers)`);
|
|
26
|
+
}
|
|
27
|
+
// Check individual dimension limits
|
|
28
|
+
if (width > MAX_IMAGE_DIMENSION || height > MAX_IMAGE_DIMENSION) {
|
|
29
|
+
throw new Error(`Image dimensions too large: ${width}x${height} (maximum ${MAX_IMAGE_DIMENSION}x${MAX_IMAGE_DIMENSION})`);
|
|
30
|
+
}
|
|
31
|
+
// Check total pixel count to prevent excessive memory allocation
|
|
32
|
+
const pixelCount = width * height;
|
|
33
|
+
if (pixelCount > MAX_IMAGE_PIXELS) {
|
|
34
|
+
throw new Error(`Image size too large: ${width}x${height} (${pixelCount} pixels exceeds maximum ${MAX_IMAGE_PIXELS})`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Safely calculates buffer size for RGBA image data
|
|
39
|
+
*
|
|
40
|
+
* @param width Image width in pixels
|
|
41
|
+
* @param height Image height in pixels
|
|
42
|
+
* @returns Buffer size in bytes (width * height * 4)
|
|
43
|
+
* @throws Error if calculation would overflow
|
|
44
|
+
*/
|
|
45
|
+
export function calculateBufferSize(width, height) {
|
|
46
|
+
validateImageDimensions(width, height);
|
|
47
|
+
return width * height * 4;
|
|
48
|
+
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* @see https://developers.google.com/speed/webp/docs/riff_container
|
|
20
20
|
* @see https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification
|
|
21
21
|
*/
|
|
22
|
+
import { validateImageDimensions } from "./security.js";
|
|
22
23
|
// Helper to read little-endian values
|
|
23
24
|
function readUint24LE(data, offset) {
|
|
24
25
|
return data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16);
|
|
@@ -252,6 +253,8 @@ export class WebPDecoder {
|
|
|
252
253
|
}
|
|
253
254
|
// Read Huffman codes
|
|
254
255
|
const huffmanTables = this.readHuffmanCodes(reader, useColorCache, colorCacheBits);
|
|
256
|
+
// Validate dimensions for security (prevent integer overflow and heap exhaustion)
|
|
257
|
+
validateImageDimensions(width, height);
|
|
255
258
|
// Decode the image using Huffman codes
|
|
256
259
|
const pixelData = new Uint8Array(width * height * 4);
|
|
257
260
|
let pixelIndex = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cross-image",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "A pure JavaScript, dependency-free, cross-runtime image processing library for Deno, Node.js, and Bun.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"image",
|
|
@@ -34,8 +34,5 @@
|
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
36
|
"scripts": {},
|
|
37
|
-
"devDependencies": {
|
|
38
|
-
"@types/node": "^20.9.0"
|
|
39
|
-
},
|
|
40
37
|
"_generatedBy": "dnt@dev"
|
|
41
38
|
}
|
package/script/mod.d.ts
CHANGED
|
@@ -2,21 +2,21 @@
|
|
|
2
2
|
* @module @cross/image
|
|
3
3
|
*
|
|
4
4
|
* A pure JavaScript, dependency-free, cross-runtime image processing library.
|
|
5
|
-
* Supports
|
|
5
|
+
* Supports decoding, resizing, and encoding common image formats (PNG, JPEG, WebP, GIF, TIFF, BMP, RAW).
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* ```ts
|
|
9
9
|
* import { Image } from "@cross/image";
|
|
10
10
|
*
|
|
11
|
-
* //
|
|
11
|
+
* // Decode an image
|
|
12
12
|
* const data = await Deno.readFile("input.png");
|
|
13
|
-
* const image = await Image.
|
|
13
|
+
* const image = await Image.decode(data);
|
|
14
14
|
*
|
|
15
15
|
* // Resize it
|
|
16
16
|
* image.resize({ width: 200, height: 200 });
|
|
17
17
|
*
|
|
18
|
-
* //
|
|
19
|
-
* const output = await image.
|
|
18
|
+
* // Encode as different format
|
|
19
|
+
* const output = await image.encode("jpeg");
|
|
20
20
|
* await Deno.writeFile("output.jpg", output);
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
package/script/mod.js
CHANGED
|
@@ -3,21 +3,21 @@
|
|
|
3
3
|
* @module @cross/image
|
|
4
4
|
*
|
|
5
5
|
* A pure JavaScript, dependency-free, cross-runtime image processing library.
|
|
6
|
-
* Supports
|
|
6
|
+
* Supports decoding, resizing, and encoding common image formats (PNG, JPEG, WebP, GIF, TIFF, BMP, RAW).
|
|
7
7
|
*
|
|
8
8
|
* @example
|
|
9
9
|
* ```ts
|
|
10
10
|
* import { Image } from "@cross/image";
|
|
11
11
|
*
|
|
12
|
-
* //
|
|
12
|
+
* // Decode an image
|
|
13
13
|
* const data = await Deno.readFile("input.png");
|
|
14
|
-
* const image = await Image.
|
|
14
|
+
* const image = await Image.decode(data);
|
|
15
15
|
*
|
|
16
16
|
* // Resize it
|
|
17
17
|
* image.resize({ width: 200, height: 200 });
|
|
18
18
|
*
|
|
19
|
-
* //
|
|
20
|
-
* const output = await image.
|
|
19
|
+
* // Encode as different format
|
|
20
|
+
* const output = await image.encode("jpeg");
|
|
21
21
|
* await Deno.writeFile("output.jpg", output);
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
@@ -17,7 +17,18 @@ export declare class ASCIIFormat implements ImageFormat {
|
|
|
17
17
|
private readonly MAGIC_BYTES;
|
|
18
18
|
private readonly CHARSETS;
|
|
19
19
|
canDecode(data: Uint8Array): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Decode ASCII art to a basic grayscale RGBA image
|
|
22
|
+
* @param data Raw ASCII art data
|
|
23
|
+
* @returns Decoded image data with grayscale RGBA pixels
|
|
24
|
+
*/
|
|
20
25
|
decode(data: Uint8Array): Promise<ImageData>;
|
|
26
|
+
/**
|
|
27
|
+
* Encode RGBA image data to ASCII art
|
|
28
|
+
* @param imageData Image data to encode
|
|
29
|
+
* @param options Optional ASCII encoding options
|
|
30
|
+
* @returns Encoded ASCII art as UTF-8 bytes
|
|
31
|
+
*/
|
|
21
32
|
encode(imageData: ImageData, options?: ASCIIOptions): Promise<Uint8Array>;
|
|
22
33
|
/**
|
|
23
34
|
* Parse options from the options line
|