cross-image 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/esm/src/image.js CHANGED
@@ -1,11 +1,14 @@
1
1
  import { resizeBilinear, resizeNearest } from "./utils/resize.js";
2
+ import { adjustBrightness, adjustContrast, adjustExposure, adjustSaturation, composite, crop, fillRect, grayscale, invert, } from "./utils/image_processing.js";
2
3
  import { PNGFormat } from "./formats/png.js";
3
4
  import { JPEGFormat } from "./formats/jpeg.js";
4
5
  import { WebPFormat } from "./formats/webp.js";
5
6
  import { GIFFormat } from "./formats/gif.js";
6
7
  import { TIFFFormat } from "./formats/tiff.js";
7
8
  import { BMPFormat } from "./formats/bmp.js";
8
- import { RAWFormat } from "./formats/raw.js";
9
+ import { DNGFormat } from "./formats/dng.js";
10
+ import { PAMFormat } from "./formats/pam.js";
11
+ import { PCXFormat } from "./formats/pcx.js";
9
12
  import { ASCIIFormat } from "./formats/ascii.js";
10
13
  import { validateImageDimensions } from "./utils/security.js";
11
14
  /**
@@ -151,12 +154,12 @@ export class Image {
151
154
  return Image.formats;
152
155
  }
153
156
  /**
154
- * Read an image from bytes
157
+ * Decode an image from bytes
155
158
  * @param data Raw image data
156
159
  * @param format Optional format hint (e.g., "png", "jpeg", "webp")
157
160
  * @returns Image instance
158
161
  */
159
- static async read(data, format) {
162
+ static async decode(data, format) {
160
163
  const image = new Image();
161
164
  // Try specified format first
162
165
  if (format) {
@@ -176,12 +179,22 @@ export class Image {
176
179
  throw new Error("Unsupported or unrecognized image format");
177
180
  }
178
181
  /**
179
- * Read all frames from a multi-frame image (GIF animation, multi-page TIFF)
182
+ * Read an image from bytes
183
+ * @deprecated Use `decode()` instead. This method will be removed in a future version.
184
+ * @param data Raw image data
185
+ * @param format Optional format hint (e.g., "png", "jpeg", "webp")
186
+ * @returns Image instance
187
+ */
188
+ static read(data, format) {
189
+ return Image.decode(data, format);
190
+ }
191
+ /**
192
+ * Decode all frames from a multi-frame image (GIF animation, multi-page TIFF)
180
193
  * @param data Raw image data
181
194
  * @param format Optional format hint (e.g., "gif", "tiff")
182
195
  * @returns MultiFrameImageData with all frames
183
196
  */
184
- static async readFrames(data, format) {
197
+ static async decodeFrames(data, format) {
185
198
  // Try specified format first
186
199
  if (format) {
187
200
  const handler = Image.formats.find((f) => f.name === format);
@@ -198,13 +211,23 @@ export class Image {
198
211
  throw new Error("Unsupported or unrecognized multi-frame image format");
199
212
  }
200
213
  /**
201
- * Save multi-frame image data to bytes in the specified format
214
+ * Read all frames from a multi-frame image (GIF animation, multi-page TIFF)
215
+ * @deprecated Use `decodeFrames()` instead. This method will be removed in a future version.
216
+ * @param data Raw image data
217
+ * @param format Optional format hint (e.g., "gif", "tiff")
218
+ * @returns MultiFrameImageData with all frames
219
+ */
220
+ static readFrames(data, format) {
221
+ return Image.decodeFrames(data, format);
222
+ }
223
+ /**
224
+ * Encode multi-frame image data to bytes in the specified format
202
225
  * @param format Format name (e.g., "gif", "tiff")
203
- * @param imageData Multi-frame image data to save
226
+ * @param imageData Multi-frame image data to encode
204
227
  * @param options Optional format-specific encoding options
205
228
  * @returns Encoded image bytes
206
229
  */
207
- static async saveFrames(format, imageData, options) {
230
+ static async encodeFrames(format, imageData, options) {
208
231
  const handler = Image.formats.find((f) => f.name === format);
209
232
  if (!handler) {
210
233
  throw new Error(`Unsupported format: ${format}`);
@@ -214,6 +237,17 @@ export class Image {
214
237
  }
215
238
  return await handler.encodeFrames(imageData, options);
216
239
  }
240
+ /**
241
+ * Save multi-frame image data to bytes in the specified format
242
+ * @deprecated Use `encodeFrames()` instead. This method will be removed in a future version.
243
+ * @param format Format name (e.g., "gif", "tiff")
244
+ * @param imageData Multi-frame image data to encode
245
+ * @param options Optional format-specific encoding options
246
+ * @returns Encoded image bytes
247
+ */
248
+ static saveFrames(format, imageData, options) {
249
+ return Image.encodeFrames(format, imageData, options);
250
+ }
217
251
  /**
218
252
  * Create an image from raw RGBA data
219
253
  * @param width Image width
@@ -231,6 +265,31 @@ export class Image {
231
265
  image.imageData = { width, height, data: new Uint8Array(data) };
232
266
  return image;
233
267
  }
268
+ /**
269
+ * Create a blank image with the specified dimensions and color
270
+ * @param width Image width
271
+ * @param height Image height
272
+ * @param r Red component (0-255, default: 0)
273
+ * @param g Green component (0-255, default: 0)
274
+ * @param b Blue component (0-255, default: 0)
275
+ * @param a Alpha component (0-255, default: 255)
276
+ * @returns Image instance
277
+ */
278
+ static create(width, height, r = 0, g = 0, b = 0, a = 255) {
279
+ // Validate dimensions for security (prevent integer overflow and heap exhaustion)
280
+ validateImageDimensions(width, height);
281
+ const data = new Uint8Array(width * height * 4);
282
+ // Fill with the specified color
283
+ for (let i = 0; i < data.length; i += 4) {
284
+ data[i] = r;
285
+ data[i + 1] = g;
286
+ data[i + 2] = b;
287
+ data[i + 3] = a;
288
+ }
289
+ const image = new Image();
290
+ image.imageData = { width, height, data };
291
+ return image;
292
+ }
234
293
  /**
235
294
  * Resize the image
236
295
  * @param options Resize options
@@ -270,12 +329,12 @@ export class Image {
270
329
  return this;
271
330
  }
272
331
  /**
273
- * Save the image to bytes in the specified format
332
+ * Encode the image to bytes in the specified format
274
333
  * @param format Format name (e.g., "png", "jpeg", "webp", "ascii")
275
334
  * @param options Optional format-specific encoding options
276
335
  * @returns Encoded image bytes
277
336
  */
278
- async save(format, options) {
337
+ async encode(format, options) {
279
338
  if (!this.imageData)
280
339
  throw new Error("No image loaded");
281
340
  const handler = Image.formats.find((f) => f.name === format);
@@ -284,6 +343,16 @@ export class Image {
284
343
  }
285
344
  return await handler.encode(this.imageData, options);
286
345
  }
346
+ /**
347
+ * Save the image to bytes in the specified format
348
+ * @deprecated Use `encode()` instead. This method will be removed in a future version.
349
+ * @param format Format name (e.g., "png", "jpeg", "webp", "ascii")
350
+ * @param options Optional format-specific encoding options
351
+ * @returns Encoded image bytes
352
+ */
353
+ save(format, options) {
354
+ return this.encode(format, options);
355
+ }
287
356
  /**
288
357
  * Clone this image
289
358
  * @returns New image instance with copied data and metadata
@@ -307,6 +376,174 @@ export class Image {
307
376
  };
308
377
  return image;
309
378
  }
379
+ /**
380
+ * Composite another image on top of this image at the specified position
381
+ * @param overlay Image to place on top
382
+ * @param x X position (can be negative)
383
+ * @param y Y position (can be negative)
384
+ * @param opacity Opacity of overlay (0-1, default: 1)
385
+ * @returns This image instance for chaining
386
+ */
387
+ composite(overlay, x, y, opacity = 1) {
388
+ if (!this.imageData)
389
+ throw new Error("No image loaded");
390
+ if (!overlay.imageData)
391
+ throw new Error("Overlay has no image loaded");
392
+ this.imageData.data = composite(this.imageData.data, this.imageData.width, this.imageData.height, overlay.imageData.data, overlay.imageData.width, overlay.imageData.height, x, y, opacity);
393
+ return this;
394
+ }
395
+ /**
396
+ * Adjust brightness of the image
397
+ * @param amount Brightness adjustment (-1 to 1, where 0 is no change)
398
+ * @returns This image instance for chaining
399
+ */
400
+ brightness(amount) {
401
+ if (!this.imageData)
402
+ throw new Error("No image loaded");
403
+ this.imageData.data = adjustBrightness(this.imageData.data, amount);
404
+ return this;
405
+ }
406
+ /**
407
+ * Adjust contrast of the image
408
+ * @param amount Contrast adjustment (-1 to 1, where 0 is no change)
409
+ * @returns This image instance for chaining
410
+ */
411
+ contrast(amount) {
412
+ if (!this.imageData)
413
+ throw new Error("No image loaded");
414
+ this.imageData.data = adjustContrast(this.imageData.data, amount);
415
+ return this;
416
+ }
417
+ /**
418
+ * Adjust exposure of the image
419
+ * @param amount Exposure adjustment in stops (-3 to 3, where 0 is no change)
420
+ * @returns This image instance for chaining
421
+ */
422
+ exposure(amount) {
423
+ if (!this.imageData)
424
+ throw new Error("No image loaded");
425
+ this.imageData.data = adjustExposure(this.imageData.data, amount);
426
+ return this;
427
+ }
428
+ /**
429
+ * Adjust saturation of the image
430
+ * @param amount Saturation adjustment (-1 to 1, where 0 is no change)
431
+ * @returns This image instance for chaining
432
+ */
433
+ saturation(amount) {
434
+ if (!this.imageData)
435
+ throw new Error("No image loaded");
436
+ this.imageData.data = adjustSaturation(this.imageData.data, amount);
437
+ return this;
438
+ }
439
+ /**
440
+ * Invert colors of the image
441
+ * @returns This image instance for chaining
442
+ */
443
+ invert() {
444
+ if (!this.imageData)
445
+ throw new Error("No image loaded");
446
+ this.imageData.data = invert(this.imageData.data);
447
+ return this;
448
+ }
449
+ /**
450
+ * Convert the image to grayscale
451
+ * @returns This image instance for chaining
452
+ */
453
+ grayscale() {
454
+ if (!this.imageData)
455
+ throw new Error("No image loaded");
456
+ this.imageData.data = grayscale(this.imageData.data);
457
+ return this;
458
+ }
459
+ /**
460
+ * Fill a rectangular region with a color
461
+ * @param x Starting X position
462
+ * @param y Starting Y position
463
+ * @param width Width of the fill region
464
+ * @param height Height of the fill region
465
+ * @param r Red component (0-255)
466
+ * @param g Green component (0-255)
467
+ * @param b Blue component (0-255)
468
+ * @param a Alpha component (0-255, default: 255)
469
+ * @returns This image instance for chaining
470
+ */
471
+ fillRect(x, y, width, height, r, g, b, a = 255) {
472
+ if (!this.imageData)
473
+ throw new Error("No image loaded");
474
+ this.imageData.data = fillRect(this.imageData.data, this.imageData.width, this.imageData.height, x, y, width, height, r, g, b, a);
475
+ return this;
476
+ }
477
+ /**
478
+ * Crop the image to a rectangular region
479
+ * @param x Starting X position
480
+ * @param y Starting Y position
481
+ * @param width Width of the crop region
482
+ * @param height Height of the crop region
483
+ * @returns This image instance for chaining
484
+ */
485
+ crop(x, y, width, height) {
486
+ if (!this.imageData)
487
+ throw new Error("No image loaded");
488
+ const result = crop(this.imageData.data, this.imageData.width, this.imageData.height, x, y, width, height);
489
+ this.imageData.width = result.width;
490
+ this.imageData.height = result.height;
491
+ this.imageData.data = result.data;
492
+ // Update physical dimensions if DPI is set
493
+ if (this.imageData.metadata) {
494
+ const metadata = this.imageData.metadata;
495
+ if (metadata.dpiX) {
496
+ this.imageData.metadata.physicalWidth = result.width / metadata.dpiX;
497
+ }
498
+ if (metadata.dpiY) {
499
+ this.imageData.metadata.physicalHeight = result.height / metadata.dpiY;
500
+ }
501
+ }
502
+ return this;
503
+ }
504
+ /**
505
+ * Get the pixel color at the specified position
506
+ * @param x X position
507
+ * @param y Y position
508
+ * @returns Object with r, g, b, a components (0-255) or undefined if out of bounds
509
+ */
510
+ getPixel(x, y) {
511
+ if (!this.imageData)
512
+ throw new Error("No image loaded");
513
+ if (x < 0 || x >= this.imageData.width || y < 0 || y >= this.imageData.height) {
514
+ return undefined;
515
+ }
516
+ const idx = (y * this.imageData.width + x) * 4;
517
+ return {
518
+ r: this.imageData.data[idx],
519
+ g: this.imageData.data[idx + 1],
520
+ b: this.imageData.data[idx + 2],
521
+ a: this.imageData.data[idx + 3],
522
+ };
523
+ }
524
+ /**
525
+ * Set the pixel color at the specified position
526
+ * @param x X position
527
+ * @param y Y position
528
+ * @param r Red component (0-255)
529
+ * @param g Green component (0-255)
530
+ * @param b Blue component (0-255)
531
+ * @param a Alpha component (0-255, default: 255)
532
+ * @returns This image instance for chaining
533
+ */
534
+ setPixel(x, y, r, g, b, a = 255) {
535
+ if (!this.imageData)
536
+ throw new Error("No image loaded");
537
+ if (x < 0 || x >= this.imageData.width || y < 0 || y >= this.imageData.height) {
538
+ return this;
539
+ }
540
+ const idx = (y * this.imageData.width + x) * 4;
541
+ this.imageData.data[idx] = r;
542
+ this.imageData.data[idx + 1] = g;
543
+ this.imageData.data[idx + 2] = b;
544
+ this.imageData.data[idx + 3] = a;
545
+ return this;
546
+ }
310
547
  }
311
548
  Object.defineProperty(Image, "formats", {
312
549
  enumerable: true,
@@ -319,7 +556,9 @@ Object.defineProperty(Image, "formats", {
319
556
  new GIFFormat(),
320
557
  new TIFFFormat(),
321
558
  new BMPFormat(),
322
- new RAWFormat(),
559
+ new DNGFormat(),
560
+ new PAMFormat(),
561
+ new PCXFormat(),
323
562
  new ASCIIFormat(),
324
563
  ]
325
564
  });
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Image processing utilities for common operations like compositing,
3
+ * level adjustments, and color manipulations.
4
+ */
5
+ /**
6
+ * Composite one image on top of another at a specified position
7
+ * @param base Base image data (RGBA)
8
+ * @param baseWidth Base image width
9
+ * @param baseHeight Base image height
10
+ * @param overlay Overlay image data (RGBA)
11
+ * @param overlayWidth Overlay image width
12
+ * @param overlayHeight Overlay image height
13
+ * @param x X position to place overlay (can be negative)
14
+ * @param y Y position to place overlay (can be negative)
15
+ * @param opacity Opacity of overlay (0-1, default: 1)
16
+ * @returns New image data with overlay composited on base
17
+ */
18
+ export declare function composite(base: Uint8Array, baseWidth: number, baseHeight: number, overlay: Uint8Array, overlayWidth: number, overlayHeight: number, x: number, y: number, opacity?: number): Uint8Array;
19
+ /**
20
+ * Adjust brightness of an image
21
+ * @param data Image data (RGBA)
22
+ * @param amount Brightness adjustment (-1 to 1, where 0 is no change)
23
+ * @returns New image data with adjusted brightness
24
+ */
25
+ export declare function adjustBrightness(data: Uint8Array, amount: number): Uint8Array;
26
+ /**
27
+ * Adjust contrast of an image
28
+ * @param data Image data (RGBA)
29
+ * @param amount Contrast adjustment (-1 to 1, where 0 is no change)
30
+ * @returns New image data with adjusted contrast
31
+ */
32
+ export declare function adjustContrast(data: Uint8Array, amount: number): Uint8Array;
33
+ /**
34
+ * Adjust exposure of an image
35
+ * @param data Image data (RGBA)
36
+ * @param amount Exposure adjustment in stops (-3 to 3, where 0 is no change)
37
+ * @returns New image data with adjusted exposure
38
+ */
39
+ export declare function adjustExposure(data: Uint8Array, amount: number): Uint8Array;
40
+ /**
41
+ * Adjust saturation of an image
42
+ * @param data Image data (RGBA)
43
+ * @param amount Saturation adjustment (-1 to 1, where 0 is no change)
44
+ * @returns New image data with adjusted saturation
45
+ */
46
+ export declare function adjustSaturation(data: Uint8Array, amount: number): Uint8Array;
47
+ /**
48
+ * Invert colors of an image
49
+ * @param data Image data (RGBA)
50
+ * @returns New image data with inverted colors
51
+ */
52
+ export declare function invert(data: Uint8Array): Uint8Array;
53
+ /**
54
+ * Convert image to grayscale
55
+ * @param data Image data (RGBA)
56
+ * @returns New image data in grayscale
57
+ */
58
+ export declare function grayscale(data: Uint8Array): Uint8Array;
59
+ /**
60
+ * Fill a rectangular region with a color
61
+ * @param data Image data (RGBA)
62
+ * @param width Image width
63
+ * @param height Image height
64
+ * @param x Starting X position
65
+ * @param y Starting Y position
66
+ * @param fillWidth Width of the fill region
67
+ * @param fillHeight Height of the fill region
68
+ * @param r Red component (0-255)
69
+ * @param g Green component (0-255)
70
+ * @param b Blue component (0-255)
71
+ * @param a Alpha component (0-255)
72
+ * @returns Modified image data
73
+ */
74
+ export declare function fillRect(data: Uint8Array, width: number, height: number, x: number, y: number, fillWidth: number, fillHeight: number, r: number, g: number, b: number, a: number): Uint8Array;
75
+ /**
76
+ * Crop an image to a rectangular region
77
+ * @param data Image data (RGBA)
78
+ * @param width Image width
79
+ * @param height Image height
80
+ * @param x Starting X position
81
+ * @param y Starting Y position
82
+ * @param cropWidth Width of the crop region
83
+ * @param cropHeight Height of the crop region
84
+ * @returns Cropped image data and dimensions
85
+ */
86
+ export declare function crop(data: Uint8Array, width: number, height: number, x: number, y: number, cropWidth: number, cropHeight: number): {
87
+ data: Uint8Array;
88
+ width: number;
89
+ height: number;
90
+ };
91
+ //# sourceMappingURL=image_processing.d.ts.map
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Image processing utilities for common operations like compositing,
3
+ * level adjustments, and color manipulations.
4
+ */
5
+ /**
6
+ * Composite one image on top of another at a specified position
7
+ * @param base Base image data (RGBA)
8
+ * @param baseWidth Base image width
9
+ * @param baseHeight Base image height
10
+ * @param overlay Overlay image data (RGBA)
11
+ * @param overlayWidth Overlay image width
12
+ * @param overlayHeight Overlay image height
13
+ * @param x X position to place overlay (can be negative)
14
+ * @param y Y position to place overlay (can be negative)
15
+ * @param opacity Opacity of overlay (0-1, default: 1)
16
+ * @returns New image data with overlay composited on base
17
+ */
18
+ export function composite(base, baseWidth, baseHeight, overlay, overlayWidth, overlayHeight, x, y, opacity = 1) {
19
+ const result = new Uint8Array(base);
20
+ // Clamp opacity to valid range
21
+ const finalOpacity = Math.max(0, Math.min(1, opacity));
22
+ // Calculate the region to composite
23
+ const startX = Math.max(0, x);
24
+ const startY = Math.max(0, y);
25
+ const endX = Math.min(baseWidth, x + overlayWidth);
26
+ const endY = Math.min(baseHeight, y + overlayHeight);
27
+ // Iterate over the overlapping region
28
+ for (let py = startY; py < endY; py++) {
29
+ for (let px = startX; px < endX; px++) {
30
+ // Calculate indices
31
+ const baseIdx = (py * baseWidth + px) * 4;
32
+ const overlayX = px - x;
33
+ const overlayY = py - y;
34
+ const overlayIdx = (overlayY * overlayWidth + overlayX) * 4;
35
+ // Get overlay pixel with opacity
36
+ const overlayR = overlay[overlayIdx];
37
+ const overlayG = overlay[overlayIdx + 1];
38
+ const overlayB = overlay[overlayIdx + 2];
39
+ const overlayA = (overlay[overlayIdx + 3] / 255) * finalOpacity;
40
+ // Get base pixel
41
+ const baseR = result[baseIdx];
42
+ const baseG = result[baseIdx + 1];
43
+ const baseB = result[baseIdx + 2];
44
+ const baseA = result[baseIdx + 3] / 255;
45
+ // Alpha compositing using "over" operation
46
+ const outA = overlayA + baseA * (1 - overlayA);
47
+ if (outA > 0) {
48
+ result[baseIdx] = Math.round((overlayR * overlayA + baseR * baseA * (1 - overlayA)) / outA);
49
+ result[baseIdx + 1] = Math.round((overlayG * overlayA + baseG * baseA * (1 - overlayA)) / outA);
50
+ result[baseIdx + 2] = Math.round((overlayB * overlayA + baseB * baseA * (1 - overlayA)) / outA);
51
+ result[baseIdx + 3] = Math.round(outA * 255);
52
+ }
53
+ }
54
+ }
55
+ return result;
56
+ }
57
+ /**
58
+ * Adjust brightness of an image
59
+ * @param data Image data (RGBA)
60
+ * @param amount Brightness adjustment (-1 to 1, where 0 is no change)
61
+ * @returns New image data with adjusted brightness
62
+ */
63
+ export function adjustBrightness(data, amount) {
64
+ const result = new Uint8Array(data.length);
65
+ const adjust = Math.max(-1, Math.min(1, amount)) * 255;
66
+ for (let i = 0; i < data.length; i += 4) {
67
+ result[i] = Math.max(0, Math.min(255, data[i] + adjust)); // R
68
+ result[i + 1] = Math.max(0, Math.min(255, data[i + 1] + adjust)); // G
69
+ result[i + 2] = Math.max(0, Math.min(255, data[i + 2] + adjust)); // B
70
+ result[i + 3] = data[i + 3]; // A
71
+ }
72
+ return result;
73
+ }
74
+ /**
75
+ * Adjust contrast of an image
76
+ * @param data Image data (RGBA)
77
+ * @param amount Contrast adjustment (-1 to 1, where 0 is no change)
78
+ * @returns New image data with adjusted contrast
79
+ */
80
+ export function adjustContrast(data, amount) {
81
+ const result = new Uint8Array(data.length);
82
+ const contrast = Math.max(-1, Math.min(1, amount));
83
+ const factor = (259 * (contrast * 255 + 255)) /
84
+ (255 * (259 - contrast * 255));
85
+ for (let i = 0; i < data.length; i += 4) {
86
+ result[i] = Math.max(0, Math.min(255, factor * (data[i] - 128) + 128)); // R
87
+ result[i + 1] = Math.max(0, Math.min(255, factor * (data[i + 1] - 128) + 128)); // G
88
+ result[i + 2] = Math.max(0, Math.min(255, factor * (data[i + 2] - 128) + 128)); // B
89
+ result[i + 3] = data[i + 3]; // A
90
+ }
91
+ return result;
92
+ }
93
+ /**
94
+ * Adjust exposure of an image
95
+ * @param data Image data (RGBA)
96
+ * @param amount Exposure adjustment in stops (-3 to 3, where 0 is no change)
97
+ * @returns New image data with adjusted exposure
98
+ */
99
+ export function adjustExposure(data, amount) {
100
+ const result = new Uint8Array(data.length);
101
+ const stops = Math.max(-3, Math.min(3, amount));
102
+ const multiplier = Math.pow(2, stops);
103
+ for (let i = 0; i < data.length; i += 4) {
104
+ result[i] = Math.max(0, Math.min(255, data[i] * multiplier)); // R
105
+ result[i + 1] = Math.max(0, Math.min(255, data[i + 1] * multiplier)); // G
106
+ result[i + 2] = Math.max(0, Math.min(255, data[i + 2] * multiplier)); // B
107
+ result[i + 3] = data[i + 3]; // A
108
+ }
109
+ return result;
110
+ }
111
+ /**
112
+ * Adjust saturation of an image
113
+ * @param data Image data (RGBA)
114
+ * @param amount Saturation adjustment (-1 to 1, where 0 is no change)
115
+ * @returns New image data with adjusted saturation
116
+ */
117
+ export function adjustSaturation(data, amount) {
118
+ const result = new Uint8Array(data.length);
119
+ const sat = Math.max(-1, Math.min(1, amount)) + 1; // Convert to 0-2 range
120
+ for (let i = 0; i < data.length; i += 4) {
121
+ const r = data[i];
122
+ const g = data[i + 1];
123
+ const b = data[i + 2];
124
+ // Calculate grayscale value using luminosity method
125
+ const gray = 0.299 * r + 0.587 * g + 0.114 * b;
126
+ // Interpolate between gray and original color based on saturation
127
+ result[i] = Math.max(0, Math.min(255, gray + (r - gray) * sat));
128
+ result[i + 1] = Math.max(0, Math.min(255, gray + (g - gray) * sat));
129
+ result[i + 2] = Math.max(0, Math.min(255, gray + (b - gray) * sat));
130
+ result[i + 3] = data[i + 3];
131
+ }
132
+ return result;
133
+ }
134
+ /**
135
+ * Invert colors of an image
136
+ * @param data Image data (RGBA)
137
+ * @returns New image data with inverted colors
138
+ */
139
+ export function invert(data) {
140
+ const result = new Uint8Array(data.length);
141
+ for (let i = 0; i < data.length; i += 4) {
142
+ result[i] = 255 - data[i]; // R
143
+ result[i + 1] = 255 - data[i + 1]; // G
144
+ result[i + 2] = 255 - data[i + 2]; // B
145
+ result[i + 3] = data[i + 3]; // A
146
+ }
147
+ return result;
148
+ }
149
+ /**
150
+ * Convert image to grayscale
151
+ * @param data Image data (RGBA)
152
+ * @returns New image data in grayscale
153
+ */
154
+ export function grayscale(data) {
155
+ const result = new Uint8Array(data.length);
156
+ for (let i = 0; i < data.length; i += 4) {
157
+ // Using luminosity method for grayscale conversion
158
+ const gray = Math.round(0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]);
159
+ result[i] = gray; // R
160
+ result[i + 1] = gray; // G
161
+ result[i + 2] = gray; // B
162
+ result[i + 3] = data[i + 3]; // A
163
+ }
164
+ return result;
165
+ }
166
+ /**
167
+ * Fill a rectangular region with a color
168
+ * @param data Image data (RGBA)
169
+ * @param width Image width
170
+ * @param height Image height
171
+ * @param x Starting X position
172
+ * @param y Starting Y position
173
+ * @param fillWidth Width of the fill region
174
+ * @param fillHeight Height of the fill region
175
+ * @param r Red component (0-255)
176
+ * @param g Green component (0-255)
177
+ * @param b Blue component (0-255)
178
+ * @param a Alpha component (0-255)
179
+ * @returns Modified image data
180
+ */
181
+ export function fillRect(data, width, height, x, y, fillWidth, fillHeight, r, g, b, a) {
182
+ const result = new Uint8Array(data);
183
+ // Calculate bounds
184
+ const startX = Math.max(0, x);
185
+ const startY = Math.max(0, y);
186
+ const endX = Math.min(width, x + fillWidth);
187
+ const endY = Math.min(height, y + fillHeight);
188
+ // Fill the region
189
+ for (let py = startY; py < endY; py++) {
190
+ for (let px = startX; px < endX; px++) {
191
+ const idx = (py * width + px) * 4;
192
+ result[idx] = r;
193
+ result[idx + 1] = g;
194
+ result[idx + 2] = b;
195
+ result[idx + 3] = a;
196
+ }
197
+ }
198
+ return result;
199
+ }
200
+ /**
201
+ * Crop an image to a rectangular region
202
+ * @param data Image data (RGBA)
203
+ * @param width Image width
204
+ * @param height Image height
205
+ * @param x Starting X position
206
+ * @param y Starting Y position
207
+ * @param cropWidth Width of the crop region
208
+ * @param cropHeight Height of the crop region
209
+ * @returns Cropped image data and dimensions
210
+ */
211
+ export function crop(data, width, height, x, y, cropWidth, cropHeight) {
212
+ // Clamp crop region to image bounds
213
+ const startX = Math.max(0, x);
214
+ const startY = Math.max(0, y);
215
+ const endX = Math.min(width, x + cropWidth);
216
+ const endY = Math.min(height, y + cropHeight);
217
+ const actualWidth = endX - startX;
218
+ const actualHeight = endY - startY;
219
+ const result = new Uint8Array(actualWidth * actualHeight * 4);
220
+ for (let py = 0; py < actualHeight; py++) {
221
+ for (let px = 0; px < actualWidth; px++) {
222
+ const srcIdx = ((startY + py) * width + (startX + px)) * 4;
223
+ const dstIdx = (py * actualWidth + px) * 4;
224
+ result[dstIdx] = data[srcIdx];
225
+ result[dstIdx + 1] = data[srcIdx + 1];
226
+ result[dstIdx + 2] = data[srcIdx + 2];
227
+ result[dstIdx + 3] = data[srcIdx + 3];
228
+ }
229
+ }
230
+ return { data: result, width: actualWidth, height: actualHeight };
231
+ }