cross-image 0.1.5 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -18
- package/esm/mod.d.ts +30 -4
- package/esm/mod.js +30 -4
- package/esm/src/formats/apng.d.ts +50 -0
- package/esm/src/formats/apng.js +364 -0
- package/esm/src/formats/bmp.d.ts +0 -6
- package/esm/src/formats/bmp.js +24 -47
- package/esm/src/formats/dng.d.ts +27 -0
- package/esm/src/formats/dng.js +191 -0
- package/esm/src/formats/gif.d.ts +0 -2
- package/esm/src/formats/gif.js +10 -16
- package/esm/src/formats/ico.d.ts +41 -0
- package/esm/src/formats/ico.js +214 -0
- package/esm/src/formats/pam.d.ts +43 -0
- package/esm/src/formats/pam.js +177 -0
- package/esm/src/formats/pcx.d.ts +13 -0
- package/esm/src/formats/pcx.js +204 -0
- package/esm/src/formats/png.d.ts +2 -21
- package/esm/src/formats/png.js +5 -429
- package/esm/src/formats/png_base.d.ts +108 -0
- package/esm/src/formats/png_base.js +487 -0
- package/esm/src/formats/tiff.d.ts +7 -7
- package/esm/src/formats/webp.d.ts +0 -1
- package/esm/src/formats/webp.js +4 -7
- package/esm/src/image.d.ts +99 -0
- package/esm/src/image.js +204 -2
- package/esm/src/utils/byte_utils.d.ts +30 -0
- package/esm/src/utils/byte_utils.js +50 -0
- package/esm/src/utils/gif_encoder.d.ts +3 -2
- package/esm/src/utils/gif_encoder.js +115 -48
- package/esm/src/utils/image_processing.d.ts +91 -0
- package/esm/src/utils/image_processing.js +231 -0
- package/esm/src/utils/webp_decoder.js +47 -12
- package/esm/src/utils/webp_encoder.js +97 -39
- package/package.json +4 -1
- package/script/mod.d.ts +30 -4
- package/script/mod.js +36 -6
- package/script/src/formats/apng.d.ts +50 -0
- package/script/src/formats/apng.js +368 -0
- package/script/src/formats/bmp.d.ts +0 -6
- package/script/src/formats/bmp.js +24 -47
- package/script/src/formats/dng.d.ts +27 -0
- package/script/src/formats/dng.js +195 -0
- package/script/src/formats/gif.d.ts +0 -2
- package/script/src/formats/gif.js +10 -16
- package/script/src/formats/ico.d.ts +41 -0
- package/script/src/formats/ico.js +218 -0
- package/script/src/formats/pam.d.ts +43 -0
- package/script/src/formats/pam.js +181 -0
- package/script/src/formats/pcx.d.ts +13 -0
- package/script/src/formats/pcx.js +208 -0
- package/script/src/formats/png.d.ts +2 -21
- package/script/src/formats/png.js +5 -429
- package/script/src/formats/png_base.d.ts +108 -0
- package/script/src/formats/png_base.js +491 -0
- package/script/src/formats/tiff.d.ts +7 -7
- package/script/src/formats/webp.d.ts +0 -1
- package/script/src/formats/webp.js +4 -7
- package/script/src/image.d.ts +99 -0
- package/script/src/image.js +204 -2
- package/script/src/utils/byte_utils.d.ts +30 -0
- package/script/src/utils/byte_utils.js +58 -0
- package/script/src/utils/gif_encoder.d.ts +3 -2
- package/script/src/utils/gif_encoder.js +115 -48
- package/script/src/utils/image_processing.d.ts +91 -0
- package/script/src/utils/image_processing.js +242 -0
- package/script/src/utils/webp_decoder.js +47 -12
- package/script/src/utils/webp_encoder.js +97 -39
- package/esm/src/formats/raw.d.ts +0 -40
- package/esm/src/formats/raw.js +0 -118
- package/script/src/formats/raw.d.ts +0 -40
- package/script/src/formats/raw.js +0 -122
package/esm/src/image.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
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";
|
|
4
|
+
import { APNGFormat } from "./formats/apng.js";
|
|
3
5
|
import { JPEGFormat } from "./formats/jpeg.js";
|
|
4
6
|
import { WebPFormat } from "./formats/webp.js";
|
|
5
7
|
import { GIFFormat } from "./formats/gif.js";
|
|
6
8
|
import { TIFFFormat } from "./formats/tiff.js";
|
|
7
9
|
import { BMPFormat } from "./formats/bmp.js";
|
|
8
|
-
import {
|
|
10
|
+
import { ICOFormat } from "./formats/ico.js";
|
|
11
|
+
import { DNGFormat } from "./formats/dng.js";
|
|
12
|
+
import { PAMFormat } from "./formats/pam.js";
|
|
13
|
+
import { PCXFormat } from "./formats/pcx.js";
|
|
9
14
|
import { ASCIIFormat } from "./formats/ascii.js";
|
|
10
15
|
import { validateImageDimensions } from "./utils/security.js";
|
|
11
16
|
/**
|
|
@@ -262,6 +267,31 @@ export class Image {
|
|
|
262
267
|
image.imageData = { width, height, data: new Uint8Array(data) };
|
|
263
268
|
return image;
|
|
264
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* Create a blank image with the specified dimensions and color
|
|
272
|
+
* @param width Image width
|
|
273
|
+
* @param height Image height
|
|
274
|
+
* @param r Red component (0-255, default: 0)
|
|
275
|
+
* @param g Green component (0-255, default: 0)
|
|
276
|
+
* @param b Blue component (0-255, default: 0)
|
|
277
|
+
* @param a Alpha component (0-255, default: 255)
|
|
278
|
+
* @returns Image instance
|
|
279
|
+
*/
|
|
280
|
+
static create(width, height, r = 0, g = 0, b = 0, a = 255) {
|
|
281
|
+
// Validate dimensions for security (prevent integer overflow and heap exhaustion)
|
|
282
|
+
validateImageDimensions(width, height);
|
|
283
|
+
const data = new Uint8Array(width * height * 4);
|
|
284
|
+
// Fill with the specified color
|
|
285
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
286
|
+
data[i] = r;
|
|
287
|
+
data[i + 1] = g;
|
|
288
|
+
data[i + 2] = b;
|
|
289
|
+
data[i + 3] = a;
|
|
290
|
+
}
|
|
291
|
+
const image = new Image();
|
|
292
|
+
image.imageData = { width, height, data };
|
|
293
|
+
return image;
|
|
294
|
+
}
|
|
265
295
|
/**
|
|
266
296
|
* Resize the image
|
|
267
297
|
* @param options Resize options
|
|
@@ -348,6 +378,174 @@ export class Image {
|
|
|
348
378
|
};
|
|
349
379
|
return image;
|
|
350
380
|
}
|
|
381
|
+
/**
|
|
382
|
+
* Composite another image on top of this image at the specified position
|
|
383
|
+
* @param overlay Image to place on top
|
|
384
|
+
* @param x X position (can be negative)
|
|
385
|
+
* @param y Y position (can be negative)
|
|
386
|
+
* @param opacity Opacity of overlay (0-1, default: 1)
|
|
387
|
+
* @returns This image instance for chaining
|
|
388
|
+
*/
|
|
389
|
+
composite(overlay, x, y, opacity = 1) {
|
|
390
|
+
if (!this.imageData)
|
|
391
|
+
throw new Error("No image loaded");
|
|
392
|
+
if (!overlay.imageData)
|
|
393
|
+
throw new Error("Overlay has no image loaded");
|
|
394
|
+
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);
|
|
395
|
+
return this;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Adjust brightness of the image
|
|
399
|
+
* @param amount Brightness adjustment (-1 to 1, where 0 is no change)
|
|
400
|
+
* @returns This image instance for chaining
|
|
401
|
+
*/
|
|
402
|
+
brightness(amount) {
|
|
403
|
+
if (!this.imageData)
|
|
404
|
+
throw new Error("No image loaded");
|
|
405
|
+
this.imageData.data = adjustBrightness(this.imageData.data, amount);
|
|
406
|
+
return this;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Adjust contrast of the image
|
|
410
|
+
* @param amount Contrast adjustment (-1 to 1, where 0 is no change)
|
|
411
|
+
* @returns This image instance for chaining
|
|
412
|
+
*/
|
|
413
|
+
contrast(amount) {
|
|
414
|
+
if (!this.imageData)
|
|
415
|
+
throw new Error("No image loaded");
|
|
416
|
+
this.imageData.data = adjustContrast(this.imageData.data, amount);
|
|
417
|
+
return this;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Adjust exposure of the image
|
|
421
|
+
* @param amount Exposure adjustment in stops (-3 to 3, where 0 is no change)
|
|
422
|
+
* @returns This image instance for chaining
|
|
423
|
+
*/
|
|
424
|
+
exposure(amount) {
|
|
425
|
+
if (!this.imageData)
|
|
426
|
+
throw new Error("No image loaded");
|
|
427
|
+
this.imageData.data = adjustExposure(this.imageData.data, amount);
|
|
428
|
+
return this;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Adjust saturation of the image
|
|
432
|
+
* @param amount Saturation adjustment (-1 to 1, where 0 is no change)
|
|
433
|
+
* @returns This image instance for chaining
|
|
434
|
+
*/
|
|
435
|
+
saturation(amount) {
|
|
436
|
+
if (!this.imageData)
|
|
437
|
+
throw new Error("No image loaded");
|
|
438
|
+
this.imageData.data = adjustSaturation(this.imageData.data, amount);
|
|
439
|
+
return this;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Invert colors of the image
|
|
443
|
+
* @returns This image instance for chaining
|
|
444
|
+
*/
|
|
445
|
+
invert() {
|
|
446
|
+
if (!this.imageData)
|
|
447
|
+
throw new Error("No image loaded");
|
|
448
|
+
this.imageData.data = invert(this.imageData.data);
|
|
449
|
+
return this;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Convert the image to grayscale
|
|
453
|
+
* @returns This image instance for chaining
|
|
454
|
+
*/
|
|
455
|
+
grayscale() {
|
|
456
|
+
if (!this.imageData)
|
|
457
|
+
throw new Error("No image loaded");
|
|
458
|
+
this.imageData.data = grayscale(this.imageData.data);
|
|
459
|
+
return this;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Fill a rectangular region with a color
|
|
463
|
+
* @param x Starting X position
|
|
464
|
+
* @param y Starting Y position
|
|
465
|
+
* @param width Width of the fill region
|
|
466
|
+
* @param height Height of the fill region
|
|
467
|
+
* @param r Red component (0-255)
|
|
468
|
+
* @param g Green component (0-255)
|
|
469
|
+
* @param b Blue component (0-255)
|
|
470
|
+
* @param a Alpha component (0-255, default: 255)
|
|
471
|
+
* @returns This image instance for chaining
|
|
472
|
+
*/
|
|
473
|
+
fillRect(x, y, width, height, r, g, b, a = 255) {
|
|
474
|
+
if (!this.imageData)
|
|
475
|
+
throw new Error("No image loaded");
|
|
476
|
+
this.imageData.data = fillRect(this.imageData.data, this.imageData.width, this.imageData.height, x, y, width, height, r, g, b, a);
|
|
477
|
+
return this;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Crop the image to a rectangular region
|
|
481
|
+
* @param x Starting X position
|
|
482
|
+
* @param y Starting Y position
|
|
483
|
+
* @param width Width of the crop region
|
|
484
|
+
* @param height Height of the crop region
|
|
485
|
+
* @returns This image instance for chaining
|
|
486
|
+
*/
|
|
487
|
+
crop(x, y, width, height) {
|
|
488
|
+
if (!this.imageData)
|
|
489
|
+
throw new Error("No image loaded");
|
|
490
|
+
const result = crop(this.imageData.data, this.imageData.width, this.imageData.height, x, y, width, height);
|
|
491
|
+
this.imageData.width = result.width;
|
|
492
|
+
this.imageData.height = result.height;
|
|
493
|
+
this.imageData.data = result.data;
|
|
494
|
+
// Update physical dimensions if DPI is set
|
|
495
|
+
if (this.imageData.metadata) {
|
|
496
|
+
const metadata = this.imageData.metadata;
|
|
497
|
+
if (metadata.dpiX) {
|
|
498
|
+
this.imageData.metadata.physicalWidth = result.width / metadata.dpiX;
|
|
499
|
+
}
|
|
500
|
+
if (metadata.dpiY) {
|
|
501
|
+
this.imageData.metadata.physicalHeight = result.height / metadata.dpiY;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return this;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Get the pixel color at the specified position
|
|
508
|
+
* @param x X position
|
|
509
|
+
* @param y Y position
|
|
510
|
+
* @returns Object with r, g, b, a components (0-255) or undefined if out of bounds
|
|
511
|
+
*/
|
|
512
|
+
getPixel(x, y) {
|
|
513
|
+
if (!this.imageData)
|
|
514
|
+
throw new Error("No image loaded");
|
|
515
|
+
if (x < 0 || x >= this.imageData.width || y < 0 || y >= this.imageData.height) {
|
|
516
|
+
return undefined;
|
|
517
|
+
}
|
|
518
|
+
const idx = (y * this.imageData.width + x) * 4;
|
|
519
|
+
return {
|
|
520
|
+
r: this.imageData.data[idx],
|
|
521
|
+
g: this.imageData.data[idx + 1],
|
|
522
|
+
b: this.imageData.data[idx + 2],
|
|
523
|
+
a: this.imageData.data[idx + 3],
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Set the pixel color at the specified position
|
|
528
|
+
* @param x X position
|
|
529
|
+
* @param y Y position
|
|
530
|
+
* @param r Red component (0-255)
|
|
531
|
+
* @param g Green component (0-255)
|
|
532
|
+
* @param b Blue component (0-255)
|
|
533
|
+
* @param a Alpha component (0-255, default: 255)
|
|
534
|
+
* @returns This image instance for chaining
|
|
535
|
+
*/
|
|
536
|
+
setPixel(x, y, r, g, b, a = 255) {
|
|
537
|
+
if (!this.imageData)
|
|
538
|
+
throw new Error("No image loaded");
|
|
539
|
+
if (x < 0 || x >= this.imageData.width || y < 0 || y >= this.imageData.height) {
|
|
540
|
+
return this;
|
|
541
|
+
}
|
|
542
|
+
const idx = (y * this.imageData.width + x) * 4;
|
|
543
|
+
this.imageData.data[idx] = r;
|
|
544
|
+
this.imageData.data[idx + 1] = g;
|
|
545
|
+
this.imageData.data[idx + 2] = b;
|
|
546
|
+
this.imageData.data[idx + 3] = a;
|
|
547
|
+
return this;
|
|
548
|
+
}
|
|
351
549
|
}
|
|
352
550
|
Object.defineProperty(Image, "formats", {
|
|
353
551
|
enumerable: true,
|
|
@@ -355,12 +553,16 @@ Object.defineProperty(Image, "formats", {
|
|
|
355
553
|
writable: true,
|
|
356
554
|
value: [
|
|
357
555
|
new PNGFormat(),
|
|
556
|
+
new APNGFormat(),
|
|
358
557
|
new JPEGFormat(),
|
|
359
558
|
new WebPFormat(),
|
|
360
559
|
new GIFFormat(),
|
|
361
560
|
new TIFFFormat(),
|
|
362
561
|
new BMPFormat(),
|
|
363
|
-
new
|
|
562
|
+
new ICOFormat(),
|
|
563
|
+
new DNGFormat(),
|
|
564
|
+
new PAMFormat(),
|
|
565
|
+
new PCXFormat(),
|
|
364
566
|
new ASCIIFormat(),
|
|
365
567
|
]
|
|
366
568
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared byte-level read/write utilities for image formats
|
|
3
|
+
* These functions handle reading and writing multi-byte integers
|
|
4
|
+
* in little-endian byte order, commonly used in BMP, ICO, GIF, and other formats.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Read a 16-bit unsigned integer in little-endian format
|
|
8
|
+
*/
|
|
9
|
+
export declare function readUint16LE(data: Uint8Array, offset: number): number;
|
|
10
|
+
/**
|
|
11
|
+
* Read a 32-bit unsigned integer in little-endian format
|
|
12
|
+
*/
|
|
13
|
+
export declare function readUint32LE(data: Uint8Array, offset: number): number;
|
|
14
|
+
/**
|
|
15
|
+
* Read a 32-bit signed integer in little-endian format
|
|
16
|
+
*/
|
|
17
|
+
export declare function readInt32LE(data: Uint8Array, offset: number): number;
|
|
18
|
+
/**
|
|
19
|
+
* Write a 16-bit unsigned integer in little-endian format
|
|
20
|
+
*/
|
|
21
|
+
export declare function writeUint16LE(data: Uint8Array, offset: number, value: number): void;
|
|
22
|
+
/**
|
|
23
|
+
* Write a 32-bit unsigned integer in little-endian format
|
|
24
|
+
*/
|
|
25
|
+
export declare function writeUint32LE(data: Uint8Array, offset: number, value: number): void;
|
|
26
|
+
/**
|
|
27
|
+
* Write a 32-bit signed integer in little-endian format
|
|
28
|
+
*/
|
|
29
|
+
export declare function writeInt32LE(data: Uint8Array, offset: number, value: number): void;
|
|
30
|
+
//# sourceMappingURL=byte_utils.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared byte-level read/write utilities for image formats
|
|
3
|
+
* These functions handle reading and writing multi-byte integers
|
|
4
|
+
* in little-endian byte order, commonly used in BMP, ICO, GIF, and other formats.
|
|
5
|
+
*/
|
|
6
|
+
// Constants for signed/unsigned integer conversion
|
|
7
|
+
const INT32_MAX = 0x7fffffff;
|
|
8
|
+
const UINT32_RANGE = 0x100000000;
|
|
9
|
+
/**
|
|
10
|
+
* Read a 16-bit unsigned integer in little-endian format
|
|
11
|
+
*/
|
|
12
|
+
export function readUint16LE(data, offset) {
|
|
13
|
+
return data[offset] | (data[offset + 1] << 8);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Read a 32-bit unsigned integer in little-endian format
|
|
17
|
+
*/
|
|
18
|
+
export function readUint32LE(data, offset) {
|
|
19
|
+
return data[offset] | (data[offset + 1] << 8) |
|
|
20
|
+
(data[offset + 2] << 16) | (data[offset + 3] << 24);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Read a 32-bit signed integer in little-endian format
|
|
24
|
+
*/
|
|
25
|
+
export function readInt32LE(data, offset) {
|
|
26
|
+
const value = readUint32LE(data, offset);
|
|
27
|
+
return value > INT32_MAX ? value - UINT32_RANGE : value;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Write a 16-bit unsigned integer in little-endian format
|
|
31
|
+
*/
|
|
32
|
+
export function writeUint16LE(data, offset, value) {
|
|
33
|
+
data[offset] = value & 0xff;
|
|
34
|
+
data[offset + 1] = (value >>> 8) & 0xff;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Write a 32-bit unsigned integer in little-endian format
|
|
38
|
+
*/
|
|
39
|
+
export function writeUint32LE(data, offset, value) {
|
|
40
|
+
data[offset] = value & 0xff;
|
|
41
|
+
data[offset + 1] = (value >>> 8) & 0xff;
|
|
42
|
+
data[offset + 2] = (value >>> 16) & 0xff;
|
|
43
|
+
data[offset + 3] = (value >>> 24) & 0xff;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Write a 32-bit signed integer in little-endian format
|
|
47
|
+
*/
|
|
48
|
+
export function writeInt32LE(data, offset, value) {
|
|
49
|
+
writeUint32LE(data, offset, value < 0 ? value + UINT32_RANGE : value);
|
|
50
|
+
}
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
export declare class GIFEncoder {
|
|
6
6
|
private width;
|
|
7
7
|
private height;
|
|
8
|
-
private
|
|
9
|
-
constructor(width: number, height: number, data
|
|
8
|
+
private frames;
|
|
9
|
+
constructor(width: number, height: number, data?: Uint8Array);
|
|
10
|
+
addFrame(data: Uint8Array, delay?: number): void;
|
|
10
11
|
private writeBytes;
|
|
11
12
|
private writeUint16LE;
|
|
12
13
|
private writeString;
|
|
@@ -17,15 +17,20 @@ export class GIFEncoder {
|
|
|
17
17
|
writable: true,
|
|
18
18
|
value: void 0
|
|
19
19
|
});
|
|
20
|
-
Object.defineProperty(this, "
|
|
20
|
+
Object.defineProperty(this, "frames", {
|
|
21
21
|
enumerable: true,
|
|
22
22
|
configurable: true,
|
|
23
23
|
writable: true,
|
|
24
|
-
value:
|
|
24
|
+
value: []
|
|
25
25
|
});
|
|
26
26
|
this.width = width;
|
|
27
27
|
this.height = height;
|
|
28
|
-
|
|
28
|
+
if (data) {
|
|
29
|
+
this.addFrame(data);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
addFrame(data, delay = 0) {
|
|
33
|
+
this.frames.push({ data, delay });
|
|
29
34
|
}
|
|
30
35
|
writeBytes(output, bytes) {
|
|
31
36
|
output.push(...bytes);
|
|
@@ -54,7 +59,7 @@ export class GIFEncoder {
|
|
|
54
59
|
/**
|
|
55
60
|
* Quantize RGBA image to 256 colors using median cut algorithm
|
|
56
61
|
*/
|
|
57
|
-
quantize() {
|
|
62
|
+
quantize(data) {
|
|
58
63
|
// Simple quantization: collect unique colors and build palette
|
|
59
64
|
const colorMap = new Map();
|
|
60
65
|
const colors = [];
|
|
@@ -65,10 +70,10 @@ export class GIFEncoder {
|
|
|
65
70
|
const rgStep = 255 / RG_LEVELS; // Step size for R/G quantization
|
|
66
71
|
const bStep = 255 / B_LEVELS; // Step size for B quantization
|
|
67
72
|
// Collect unique colors
|
|
68
|
-
for (let i = 0; i <
|
|
69
|
-
const r =
|
|
70
|
-
const g =
|
|
71
|
-
const b =
|
|
73
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
74
|
+
const r = data[i];
|
|
75
|
+
const g = data[i + 1];
|
|
76
|
+
const b = data[i + 2];
|
|
72
77
|
const key = `${r},${g},${b}`;
|
|
73
78
|
if (!colorMap.has(key) && colors.length < 256) {
|
|
74
79
|
colorMap.set(key, colors.length);
|
|
@@ -83,12 +88,12 @@ export class GIFEncoder {
|
|
|
83
88
|
colorMap.clear();
|
|
84
89
|
colors.length = 0;
|
|
85
90
|
useColorReduction = true;
|
|
86
|
-
for (let i = 0; i <
|
|
91
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
87
92
|
// Reduce color depth: 3 bits for R/G channels, 2 bits for B channel
|
|
88
93
|
// This gives us 8 bits total = 256 possible colors
|
|
89
|
-
const r = this.quantizeChannel(
|
|
90
|
-
const g = this.quantizeChannel(
|
|
91
|
-
const b = this.quantizeChannel(
|
|
94
|
+
const r = this.quantizeChannel(data[i], RG_LEVELS, rgStep);
|
|
95
|
+
const g = this.quantizeChannel(data[i + 1], RG_LEVELS, rgStep);
|
|
96
|
+
const b = this.quantizeChannel(data[i + 2], B_LEVELS, bStep);
|
|
92
97
|
const key = `${r},${g},${b}`;
|
|
93
98
|
if (!colorMap.has(key)) {
|
|
94
99
|
if (colors.length < 256) {
|
|
@@ -112,10 +117,10 @@ export class GIFEncoder {
|
|
|
112
117
|
}
|
|
113
118
|
// Create indexed data
|
|
114
119
|
const indexed = new Uint8Array(this.width * this.height);
|
|
115
|
-
for (let i = 0, j = 0; i <
|
|
116
|
-
let r =
|
|
117
|
-
let g =
|
|
118
|
-
let b =
|
|
120
|
+
for (let i = 0, j = 0; i < data.length; i += 4, j++) {
|
|
121
|
+
let r = data[i];
|
|
122
|
+
let g = data[i + 1];
|
|
123
|
+
let b = data[i + 2];
|
|
119
124
|
// Apply color reduction if it was used for building the palette
|
|
120
125
|
if (useColorReduction) {
|
|
121
126
|
r = this.quantizeChannel(r, RG_LEVELS, rgStep);
|
|
@@ -161,10 +166,14 @@ export class GIFEncoder {
|
|
|
161
166
|
return Math.max(2, bits);
|
|
162
167
|
}
|
|
163
168
|
encode() {
|
|
169
|
+
if (this.frames.length === 0) {
|
|
170
|
+
throw new Error("No frames to encode");
|
|
171
|
+
}
|
|
164
172
|
const output = [];
|
|
165
|
-
// Quantize
|
|
166
|
-
const
|
|
167
|
-
const
|
|
173
|
+
// Quantize first frame for Global Color Table
|
|
174
|
+
const firstFrame = this.frames[0];
|
|
175
|
+
const { palette: globalPalette, indexed: firstIndexed } = this.quantize(firstFrame.data);
|
|
176
|
+
const paletteSize = globalPalette.length / 3;
|
|
168
177
|
const bitsPerColor = this.getBitsPerColor(paletteSize);
|
|
169
178
|
// Header
|
|
170
179
|
this.writeString(output, "GIF89a");
|
|
@@ -187,38 +196,96 @@ export class GIFEncoder {
|
|
|
187
196
|
// So we need to write that many colors, padding if necessary
|
|
188
197
|
const gctSize = 1 << bitsPerColor;
|
|
189
198
|
const paddedPalette = new Uint8Array(gctSize * 3);
|
|
190
|
-
paddedPalette.set(
|
|
199
|
+
paddedPalette.set(globalPalette);
|
|
191
200
|
this.writeBytes(output, paddedPalette);
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
201
|
+
// Netscape Application Extension (Looping)
|
|
202
|
+
if (this.frames.length > 1) {
|
|
203
|
+
output.push(0x21); // Extension Introducer
|
|
204
|
+
output.push(0xff); // Application Extension Label
|
|
205
|
+
output.push(11); // Block Size
|
|
206
|
+
this.writeString(output, "NETSCAPE2.0");
|
|
207
|
+
output.push(3); // Sub-block Size
|
|
208
|
+
output.push(1); // Loop Indicator (1 = loop)
|
|
209
|
+
this.writeUint16LE(output, 0); // Loop Count (0 = infinite)
|
|
210
|
+
output.push(0); // Block Terminator
|
|
211
|
+
}
|
|
212
|
+
// Encode frames
|
|
213
|
+
for (let i = 0; i < this.frames.length; i++) {
|
|
214
|
+
const frame = this.frames[i];
|
|
215
|
+
let indexed;
|
|
216
|
+
let useLocalPalette = false;
|
|
217
|
+
let localPalette = null;
|
|
218
|
+
let localBitsPerColor = bitsPerColor;
|
|
219
|
+
if (i === 0) {
|
|
220
|
+
indexed = firstIndexed;
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
// Quantize subsequent frames
|
|
224
|
+
// For simplicity, we use a Local Color Table for each frame to ensure colors are correct
|
|
225
|
+
const result = this.quantize(frame.data);
|
|
226
|
+
indexed = result.indexed;
|
|
227
|
+
localPalette = result.palette;
|
|
228
|
+
useLocalPalette = true;
|
|
229
|
+
const localPaletteSize = localPalette.length / 3;
|
|
230
|
+
localBitsPerColor = this.getBitsPerColor(localPaletteSize);
|
|
218
231
|
}
|
|
232
|
+
// Graphic Control Extension
|
|
233
|
+
output.push(0x21); // Extension Introducer
|
|
234
|
+
output.push(0xf9); // Graphic Control Label
|
|
235
|
+
output.push(4); // Byte Size
|
|
236
|
+
// Packed Field
|
|
237
|
+
// Reserved (3 bits)
|
|
238
|
+
// Disposal Method (3 bits): 2 (Restore to background) - usually safe for animation
|
|
239
|
+
// User Input Flag (1 bit): 0
|
|
240
|
+
// Transparent Color Flag (1 bit): 0
|
|
241
|
+
output.push(0x08); // Disposal method 2 (Restore to background)
|
|
242
|
+
// Delay Time (1/100ths of a second)
|
|
243
|
+
// Default to 10 (100ms) if not specified
|
|
244
|
+
const delay = frame.delay > 0 ? Math.round(frame.delay / 10) : 10;
|
|
245
|
+
this.writeUint16LE(output, delay);
|
|
246
|
+
// Transparent Color Index
|
|
247
|
+
output.push(0);
|
|
248
|
+
output.push(0); // Block Terminator
|
|
249
|
+
// Image Descriptor
|
|
250
|
+
output.push(0x2c); // Image Separator
|
|
251
|
+
this.writeUint16LE(output, 0); // Left
|
|
252
|
+
this.writeUint16LE(output, 0); // Top
|
|
253
|
+
this.writeUint16LE(output, this.width);
|
|
254
|
+
this.writeUint16LE(output, this.height);
|
|
255
|
+
// Packed Field
|
|
256
|
+
if (useLocalPalette && localPalette) {
|
|
257
|
+
// LCT Flag: 1
|
|
258
|
+
// Interlace: 0
|
|
259
|
+
// Sort: 0
|
|
260
|
+
// Reserved: 0
|
|
261
|
+
// Size of LCT: localBitsPerColor - 1
|
|
262
|
+
const lctPacked = 0x80 | (localBitsPerColor - 1);
|
|
263
|
+
output.push(lctPacked);
|
|
264
|
+
// Write Local Color Table
|
|
265
|
+
const lctSize = 1 << localBitsPerColor;
|
|
266
|
+
const paddedLct = new Uint8Array(lctSize * 3);
|
|
267
|
+
paddedLct.set(localPalette);
|
|
268
|
+
this.writeBytes(output, paddedLct);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
output.push(0); // No LCT
|
|
272
|
+
}
|
|
273
|
+
// LZW Minimum Code Size
|
|
274
|
+
const minCodeSize = Math.max(2, useLocalPalette ? localBitsPerColor : bitsPerColor);
|
|
275
|
+
output.push(minCodeSize);
|
|
276
|
+
// Compress image data with LZW
|
|
277
|
+
const encoder = new LZWEncoder(minCodeSize);
|
|
278
|
+
const compressed = encoder.compress(indexed);
|
|
279
|
+
// Write compressed data in sub-blocks (max 255 bytes per block)
|
|
280
|
+
for (let k = 0; k < compressed.length; k += 255) {
|
|
281
|
+
const blockSize = Math.min(255, compressed.length - k);
|
|
282
|
+
output.push(blockSize);
|
|
283
|
+
for (let j = 0; j < blockSize; j++) {
|
|
284
|
+
output.push(compressed[k + j]);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
output.push(0); // Block Terminator
|
|
219
288
|
}
|
|
220
|
-
// Block Terminator
|
|
221
|
-
output.push(0);
|
|
222
289
|
// Trailer
|
|
223
290
|
output.push(0x3b);
|
|
224
291
|
return new Uint8Array(output);
|
|
@@ -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
|