imagescript 3.1.0 → 3.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 +12 -4
- package/dist/animation.d.ts +92 -7
- package/dist/animation.js +50 -20
- package/dist/codecs/exif.js +62 -0
- package/dist/codecs/gif.js +43 -27
- package/dist/codecs/jpeg.js +13 -1
- package/dist/codecs/png.js +85 -21
- package/dist/codecs/quantize.js +2 -2
- package/dist/codecs/tga.js +47 -4
- package/dist/codecs/vp8l.js +27 -30
- package/dist/codecs/webp.js +23 -10
- package/dist/frame.d.ts +30 -2
- package/dist/frame.js +20 -9
- package/dist/gif.d.ts +10 -7
- package/dist/gif.js +7 -5
- package/dist/image-type.d.ts +1 -0
- package/dist/image-type.js +1 -0
- package/dist/image.d.ts +231 -31
- package/dist/image.js +118 -53
- package/dist/index.d.ts +14 -4
- package/dist/index.js +6 -4
- package/dist/metadata.d.ts +35 -0
- package/dist/metadata.js +43 -0
- package/dist/render/svg.js +39 -21
- package/dist/util/blur.js +88 -0
- package/dist/util/optimize.js +264 -0
- package/dist/util/options.js +3 -0
- package/dist/util/resample.js +244 -63
- package/dist/util/utf8.js +4 -51
- package/dist/version.js +1 -1
- package/package.json +9 -2
- package/dist/codecs/crc.d.ts +0 -9
- package/dist/codecs/exif.d.ts +0 -11
- package/dist/codecs/gif.d.ts +0 -44
- package/dist/codecs/jpeg.d.ts +0 -35
- package/dist/codecs/png.d.ts +0 -97
- package/dist/codecs/quantize.d.ts +0 -25
- package/dist/codecs/tga.d.ts +0 -16
- package/dist/codecs/tiff.d.ts +0 -6
- package/dist/codecs/vp8-tables.d.ts +0 -10
- package/dist/codecs/vp8.d.ts +0 -8
- package/dist/codecs/vp8l.d.ts +0 -62
- package/dist/codecs/webp.d.ts +0 -67
- package/dist/codecs/zlib.d.ts +0 -22
- package/dist/render/font.d.ts +0 -92
- package/dist/render/hinting.d.ts +0 -110
- package/dist/render/svg.d.ts +0 -11
- package/dist/render/text.d.ts +0 -63
- package/dist/util/blur.d.ts +0 -13
- package/dist/util/resample.d.ts +0 -12
- package/dist/util/utf8.d.ts +0 -5
- package/dist/version.d.ts +0 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
**ImageScript** is a zero-dependency alternative to common JavaScript bitmap image manipulation tools. Every codec and
|
|
11
11
|
renderer is written in TypeScript: no WebAssembly, no native binaries, no npm dependencies, no install scripts. The
|
|
12
|
-
package is about
|
|
12
|
+
package is about 160 kB (compressed) and runs in Node.js 22.13 or newer (including Alpine/musl, ARM and bundled
|
|
13
13
|
serverless functions), Deno, Bun and browsers. It works with both `import` and `require()`, type definitions are included.
|
|
14
14
|
|
|
15
15
|
```sh
|
|
@@ -38,7 +38,8 @@ deno add jsr:@matmen/imagescript # Deno (npm:imagescript works too)
|
|
|
38
38
|
- Image manipulation functions ([crop](https://imagescript.matmen.dev/classes/Image.html#crop)
|
|
39
39
|
, [rotate](https://imagescript.matmen.dev/classes/Image.html#rotate)
|
|
40
40
|
, [cropCircle](https://imagescript.matmen.dev/classes/Image.html#cropcircle)
|
|
41
|
-
, [roundCorners](https://imagescript.matmen.dev/classes/Image.html#roundcorners)
|
|
41
|
+
, [roundCorners](https://imagescript.matmen.dev/classes/Image.html#roundcorners)
|
|
42
|
+
, [trim](https://imagescript.matmen.dev/classes/Image.html#trim), ...)
|
|
42
43
|
- Filters and color adjustments ([blur](https://imagescript.matmen.dev/classes/Image.html#blur)
|
|
43
44
|
, [sharpen](https://imagescript.matmen.dev/classes/Image.html#sharpen)
|
|
44
45
|
, [brightness](https://imagescript.matmen.dev/classes/Image.html#brightness)
|
|
@@ -53,7 +54,7 @@ deno add jsr:@matmen/imagescript # Deno (npm:imagescript works too)
|
|
|
53
54
|
and [TGAs](https://imagescript.matmen.dev/classes/Image.html#encodetga)
|
|
54
55
|
- Encoding animations as [GIF](https://imagescript.matmen.dev/classes/Animation.html#encodegif),
|
|
55
56
|
[APNG](https://imagescript.matmen.dev/classes/Animation.html#encodeapng) and lossless
|
|
56
|
-
[WebP](https://imagescript.matmen.dev/classes/Animation.html#encodewebp)
|
|
57
|
+
[WebP](https://imagescript.matmen.dev/classes/Animation.html#encodewebp), storing only what changes from frame to frame
|
|
57
58
|
|
|
58
59
|
---
|
|
59
60
|
|
|
@@ -77,7 +78,7 @@ const { Image } = require('imagescript');
|
|
|
77
78
|
All coordinates are 0-based, `(0, 0)` is the top left pixel.
|
|
78
79
|
|
|
79
80
|
A bigger example is the [README image generation](https://github.com/matmen/ImageScript/blob/master/tests/readme.js).
|
|
80
|
-
Upgrading from 1.x? Version 3 has breaking changes (0-based coordinates, removed WebP encoding, Node.js 22.13+, ...),
|
|
81
|
+
Upgrading from 1.x? Version 3 has breaking changes (0-based coordinates, removed lossy WebP encoding, Node.js 22.13+, ...),
|
|
81
82
|
see the [changelog](https://github.com/matmen/ImageScript/blob/master/CHANGELOG.md). There is no 2.x release, the number was skipped because of the old experimental
|
|
82
83
|
`imagescript/v2` API.
|
|
83
84
|
|
|
@@ -85,6 +86,13 @@ see the [changelog](https://github.com/matmen/ImageScript/blob/master/CHANGELOG.
|
|
|
85
86
|
|
|
86
87
|
---
|
|
87
88
|
|
|
89
|
+
### Benchmarks
|
|
90
|
+
|
|
91
|
+
[bench/](bench/README.md) compares ImageScript to Jimp, image-js, Photon, sharp and @napi-rs/canvas: speed of decoding,
|
|
92
|
+
encoding, image operations and rendering, encoded file sizes and install sizes. It runs for every release.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
88
96
|
### Credits
|
|
89
97
|
|
|
90
98
|
The deflate/inflate implementation is derived from [fflate](https://github.com/101arrowz/fflate) and the CRC-32 from
|
package/dist/animation.d.ts
CHANGED
|
@@ -1,19 +1,67 @@
|
|
|
1
1
|
import { BinaryData } from './util/mem.js';
|
|
2
|
-
import { ResizeMode } from './image.js';
|
|
2
|
+
import { ResizeMode, ResizeOptions } from './image.js';
|
|
3
3
|
import { Frame } from './frame.js';
|
|
4
|
+
import { Metadata } from './metadata.js';
|
|
5
|
+
/** Options of {@link Animation} */
|
|
6
|
+
export interface AnimationOptions {
|
|
7
|
+
/** How many times to repeat the animation (-1 = forever, 0 = play once). Default: `-1` */
|
|
8
|
+
loopCount?: number;
|
|
9
|
+
}
|
|
10
|
+
/** Options of {@link Animation.decode} */
|
|
11
|
+
export interface AnimationDecodeOptions {
|
|
12
|
+
/** Whether to stop decoding after the first frame. Default: `false` */
|
|
13
|
+
onlyFirstFrame?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Options of {@link Animation.encodeWebP}, and those all animation encoders have */
|
|
16
|
+
export interface AnimationEncodeOptions {
|
|
17
|
+
/** Metadata to include, see {@link Metadata} */
|
|
18
|
+
metadata?: Metadata;
|
|
19
|
+
/**
|
|
20
|
+
* Whether to store each frame as the area that changed since the frame before and merge identical frames, which
|
|
21
|
+
* usually makes files a lot smaller. The file shows the same animation either way, but only decodes to the same
|
|
22
|
+
* frames without it, which also encodes faster when little changes. Default: `true`
|
|
23
|
+
*/
|
|
24
|
+
optimize?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** Options of {@link Animation.encodeGIF} */
|
|
27
|
+
export interface GIFEncodeOptions extends AnimationEncodeOptions {
|
|
28
|
+
/** 1-100, scales the palette size from 8 colors (1) to 256 colors (90 and above). Default: `95` */
|
|
29
|
+
quality?: number;
|
|
30
|
+
}
|
|
31
|
+
/** Options of {@link Animation.encodeAPNG} */
|
|
32
|
+
export interface APNGEncodeOptions extends AnimationEncodeOptions {
|
|
33
|
+
/** The zlib compression level (0-9), higher is smaller but slower. Default: `1` */
|
|
34
|
+
compression?: number;
|
|
35
|
+
}
|
|
4
36
|
/**
|
|
5
|
-
* An animation:
|
|
6
|
-
*
|
|
37
|
+
* An animation: frames ({@link Animation.frames}) that can be decoded from and encoded to GIF, APNG and WebP.
|
|
38
|
+
*
|
|
39
|
+
* The animation itself is an array of its frames too (`animation[0]`, `animation.map(...)`), which is deprecated: in 4.0
|
|
40
|
+
* it won't extend `Array` anymore, use {@link Animation.frames} instead. Array methods like map and filter return
|
|
41
|
+
* plain arrays.
|
|
7
42
|
*/
|
|
8
43
|
export declare class Animation extends Array<Frame> {
|
|
9
44
|
/** How many times to repeat the animation (-1 = forever, 0 = play once) */
|
|
10
45
|
loopCount: number;
|
|
46
|
+
/**
|
|
47
|
+
* Creates a new animation
|
|
48
|
+
* @param frames The frames to create the animation from
|
|
49
|
+
* @param options See {@link AnimationOptions}
|
|
50
|
+
*/
|
|
51
|
+
constructor(frames: Frame[], options?: AnimationOptions);
|
|
11
52
|
/**
|
|
12
53
|
* Creates a new animation
|
|
13
54
|
* @param frames The frames to create the animation from
|
|
14
55
|
* @param loopCount How many times to repeat the animation (-1 = forever, 0 = play once)
|
|
15
56
|
*/
|
|
16
57
|
constructor(frames: Frame[], loopCount?: number);
|
|
58
|
+
/**
|
|
59
|
+
* The animation's frames: an array that changes the animation when it's changed (e.g. `push()`, `splice()`, or
|
|
60
|
+
* setting an index), and can be replaced as a whole
|
|
61
|
+
*/
|
|
62
|
+
get frames(): Frame[];
|
|
63
|
+
/** Replaces the animation's frames */
|
|
64
|
+
set frames(frames: Frame[]);
|
|
17
65
|
/** The animation's width */
|
|
18
66
|
get width(): number;
|
|
19
67
|
/** The animation's height */
|
|
@@ -24,28 +72,65 @@ export declare class Animation extends Array<Frame> {
|
|
|
24
72
|
/**
|
|
25
73
|
* Returns a new animation of the same class containing a range of this one's frames
|
|
26
74
|
* (same semantics as Array.prototype.slice)
|
|
75
|
+
* @deprecated Use `new Animation(animation.frames.slice(start, end), animation.loopCount)`: in 4.0 animations
|
|
76
|
+
* aren't arrays anymore
|
|
27
77
|
*/
|
|
28
78
|
slice(start?: number, end?: number): this;
|
|
79
|
+
/**
|
|
80
|
+
* Resizes the animation to the given dimensions, scaling every frame and its offsets proportionally.
|
|
81
|
+
* Use {@link Image.RESIZE_AUTO} as either width or height to automatically preserve the aspect ratio.
|
|
82
|
+
*/
|
|
83
|
+
resize(width: number, height: number, options?: ResizeOptions): this;
|
|
29
84
|
/**
|
|
30
85
|
* Resizes the animation to the given dimensions, scaling every frame and its offsets proportionally.
|
|
31
86
|
* Use {@link Image.RESIZE_AUTO} as either width or height to automatically preserve the aspect ratio.
|
|
32
87
|
*/
|
|
33
88
|
resize(width: number, height: number, mode?: ResizeMode): this;
|
|
34
89
|
/**
|
|
35
|
-
* Encodes the animation as GIF
|
|
90
|
+
* Encodes the animation as GIF. Like all encoders, it stores each frame as the area that changed since the frame
|
|
91
|
+
* before and merges identical frames (unless the `optimize` option is false), so the file decodes to what the
|
|
92
|
+
* animation shows, but not necessarily to the same frames. Semi-transparent pixels become opaque or transparent
|
|
93
|
+
* after the frames are composited
|
|
94
|
+
* @param options See {@link GIFEncodeOptions}
|
|
95
|
+
*/
|
|
96
|
+
encodeGIF(options?: GIFEncodeOptions): Promise<Uint8Array>;
|
|
97
|
+
/**
|
|
98
|
+
* Encodes the animation as GIF. Like all encoders, it stores each frame as the area that changed since the frame
|
|
99
|
+
* before and merges identical frames (unless the `optimize` option is false), so the file decodes to what the
|
|
100
|
+
* animation shows, but not necessarily to the same frames. Semi-transparent pixels become opaque or transparent
|
|
101
|
+
* after the frames are composited
|
|
36
102
|
* @param quality 1-100, scales the palette size from 8 colors (1) to 256 colors (90 and above)
|
|
37
103
|
*/
|
|
38
104
|
encodeGIF(quality?: number): Promise<Uint8Array>;
|
|
39
105
|
/**
|
|
40
106
|
* Encodes the animation as APNG (animated PNG, lossless and with full alpha). Viewers without APNG support show
|
|
41
|
-
* the first frame
|
|
107
|
+
* the first frame. Each frame is stored as the area that changed since the frame before, identical frames are
|
|
108
|
+
* merged (unless the `optimize` option is false): the file shows exactly the same animation, but doesn't
|
|
109
|
+
* necessarily decode to the same frames
|
|
110
|
+
* @param options See {@link APNGEncodeOptions}
|
|
111
|
+
*/
|
|
112
|
+
encodeAPNG(options?: APNGEncodeOptions): Promise<Uint8Array>;
|
|
113
|
+
/**
|
|
114
|
+
* Encodes the animation as APNG (animated PNG, lossless and with full alpha). Viewers without APNG support show
|
|
115
|
+
* the first frame. Each frame is stored as the area that changed since the frame before, identical frames are
|
|
116
|
+
* merged (unless the `optimize` option is false): the file shows exactly the same animation, but doesn't
|
|
117
|
+
* necessarily decode to the same frames
|
|
42
118
|
* @param compression The zlib compression level (0-9), higher is smaller but slower
|
|
43
119
|
*/
|
|
44
120
|
encodeAPNG(compression?: number): Promise<Uint8Array>;
|
|
45
121
|
/**
|
|
46
|
-
* Encodes the animation as lossless animated WebP
|
|
122
|
+
* Encodes the animation as lossless animated WebP. Each frame is stored as the area that changed since the frame
|
|
123
|
+
* before, identical frames are merged (unless the `optimize` option is false): the file shows exactly the same
|
|
124
|
+
* animation, but doesn't necessarily decode to the same frames
|
|
125
|
+
* @param options See {@link AnimationEncodeOptions}
|
|
126
|
+
*/
|
|
127
|
+
encodeWebP({ metadata, optimize }?: AnimationEncodeOptions): Promise<Uint8Array>;
|
|
128
|
+
/**
|
|
129
|
+
* Decodes an animated GIF, APNG or WebP into fully composited frames (still images become a single frame)
|
|
130
|
+
* @param input The binary data to decode
|
|
131
|
+
* @param options See {@link AnimationDecodeOptions}
|
|
47
132
|
*/
|
|
48
|
-
|
|
133
|
+
static decode(input: BinaryData, options?: AnimationDecodeOptions): Promise<Animation>;
|
|
49
134
|
/**
|
|
50
135
|
* Decodes an animated GIF, APNG or WebP into fully composited frames (still images become a single frame)
|
|
51
136
|
* @param input The binary data to decode
|
package/dist/animation.js
CHANGED
|
@@ -5,19 +5,23 @@ import { view } from "./util/mem.js";
|
|
|
5
5
|
import { Image } from "./image.js";
|
|
6
6
|
import { Frame } from "./frame.js";
|
|
7
7
|
import { ImageType } from "./image-type.js";
|
|
8
|
+
import { optimize } from "./util/optimize.js";
|
|
9
|
+
import { resolveMetadata, pngText, exifData, gifComments } from "./metadata.js";
|
|
10
|
+
import { isOptions } from "./util/options.js";
|
|
8
11
|
const toPlays = (loopCount) => loopCount < 0 ? 0 : loopCount + 1;
|
|
9
12
|
const fromPlays = (plays) => plays === 0 ? -1 : plays - 1;
|
|
10
13
|
function canvasFrames(width, height, frames) {
|
|
11
14
|
return frames.map(({ pixels, duration }) => {
|
|
12
15
|
const frame = new Frame(width, height, duration, 0, 0, Frame.DISPOSAL_BACKGROUND);
|
|
13
|
-
frame.bitmap
|
|
16
|
+
frame.bitmap = pixels;
|
|
14
17
|
return frame;
|
|
15
18
|
});
|
|
16
19
|
}
|
|
17
20
|
export class Animation extends Array {
|
|
18
21
|
loopCount;
|
|
19
|
-
constructor(frames,
|
|
22
|
+
constructor(frames, loopCountOrOptions = -1) {
|
|
20
23
|
super();
|
|
24
|
+
const { loopCount = -1 } = isOptions(loopCountOrOptions) ? loopCountOrOptions : { loopCount: loopCountOrOptions };
|
|
21
25
|
for (const frame of frames)
|
|
22
26
|
this.push(frame);
|
|
23
27
|
for (let i = 0; i < this.length; i++)
|
|
@@ -30,6 +34,19 @@ export class Animation extends Array {
|
|
|
30
34
|
static get [Symbol.species]() {
|
|
31
35
|
return Array;
|
|
32
36
|
}
|
|
37
|
+
get frames() {
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
set frames(frames) {
|
|
41
|
+
frames = Array.from(frames);
|
|
42
|
+
frames.forEach((frame, i) => {
|
|
43
|
+
if (!(frame instanceof Frame))
|
|
44
|
+
throw new TypeError(`Frame ${i} is not an instance of Frame`);
|
|
45
|
+
});
|
|
46
|
+
this.length = 0;
|
|
47
|
+
for (const frame of frames)
|
|
48
|
+
this.push(frame);
|
|
49
|
+
}
|
|
33
50
|
get width() {
|
|
34
51
|
let max = 0;
|
|
35
52
|
for (const frame of this)
|
|
@@ -55,7 +72,8 @@ export class Animation extends Array {
|
|
|
55
72
|
const Class = this.constructor;
|
|
56
73
|
return new Class(Array.prototype.slice.call(this, start, end), this.loopCount);
|
|
57
74
|
}
|
|
58
|
-
resize(width, height,
|
|
75
|
+
resize(width, height, modeOrOptions) {
|
|
76
|
+
const mode = (isOptions(modeOrOptions) ? modeOrOptions.mode : modeOrOptions) ?? Image.RESIZE_BILINEAR;
|
|
59
77
|
const originalWidth = this.width, originalHeight = this.height;
|
|
60
78
|
if (width === Image.RESIZE_AUTO && height === Image.RESIZE_AUTO)
|
|
61
79
|
throw new Error('RESIZE_AUTO can only be used for either width or height, not for both');
|
|
@@ -78,38 +96,47 @@ export class Animation extends Array {
|
|
|
78
96
|
if (!(frame instanceof Frame))
|
|
79
97
|
throw new Error('Animation contains invalid frames');
|
|
80
98
|
}
|
|
81
|
-
async encodeGIF(
|
|
99
|
+
async encodeGIF(qualityOrOptions = 95) {
|
|
100
|
+
const { quality = 95, metadata = {}, optimize = true } = isOptions(qualityOrOptions) ? qualityOrOptions : { quality: qualityOrOptions };
|
|
82
101
|
this.__validate__();
|
|
83
|
-
const frames =
|
|
84
|
-
|
|
85
|
-
|
|
102
|
+
const frames = optimize
|
|
103
|
+
? this.__optimized__({ replace: false, even: false, alpha: 'frame', palette: 256, cover: true }).map(f => ({ ...f, dispose: f.dispose ? 3 : 1 }))
|
|
104
|
+
: Array.from(this, frame => ({
|
|
86
105
|
x: frame.xOffset,
|
|
87
106
|
y: frame.yOffset,
|
|
88
107
|
width: frame.width,
|
|
89
108
|
height: frame.height,
|
|
90
|
-
|
|
109
|
+
duration: frame.duration,
|
|
91
110
|
dispose: frame.__disposal_id__,
|
|
92
111
|
pixels: new Uint8Array(frame.bitmap.buffer, frame.bitmap.byteOffset, frame.bitmap.byteLength)
|
|
93
|
-
});
|
|
94
|
-
|
|
112
|
+
}));
|
|
113
|
+
const gifFrames = frames.map(({ duration, ...f }) => ({ ...f, delay: Math.round(duration / 10) }));
|
|
114
|
+
return gifCodec.encode({ width: this.width, height: this.height, loopCount: this.loopCount, frames: gifFrames }, quality, gifComments(resolveMetadata(metadata)));
|
|
95
115
|
}
|
|
96
|
-
async encodeAPNG(
|
|
116
|
+
async encodeAPNG(compressionOrOptions = 1) {
|
|
117
|
+
const { compression = 1, metadata = {}, optimize = true } = isOptions(compressionOrOptions) ? compressionOrOptions : { compression: compressionOrOptions };
|
|
97
118
|
this.__validate__();
|
|
98
119
|
if (!this.length)
|
|
99
120
|
throw new Error('An animation needs at least one frame');
|
|
100
|
-
|
|
121
|
+
const frames = optimize
|
|
122
|
+
? this.__optimized__({ replace: true, even: false, alpha: frames => png.alphaCost(frames.map(f => f.pixels)) }).map(f => ({ ...f, dispose: f.dispose ? 1 : 0 }))
|
|
123
|
+
: this.__apngFrames__().map(({ blend: _, ...f }) => f);
|
|
124
|
+
return png.encodeAnimation(frames, this.width, this.height, toPlays(this.loopCount), compression, pngText(resolveMetadata(metadata)));
|
|
101
125
|
}
|
|
102
|
-
async encodeWebP() {
|
|
126
|
+
async encodeWebP({ metadata = {}, optimize = true } = {}) {
|
|
103
127
|
this.__validate__();
|
|
104
128
|
if (!this.length)
|
|
105
129
|
throw new Error('An animation needs at least one frame');
|
|
130
|
+
const frames = optimize ? this.__optimized__({ replace: true, even: true, alpha: 'frame' }) : this.__webpFrames__();
|
|
131
|
+
return webp.encodeAnimation(frames, this.width, this.height, toPlays(this.loopCount), exifData(resolveMetadata(metadata)));
|
|
132
|
+
}
|
|
133
|
+
__webpFrames__() {
|
|
106
134
|
const width = this.width, height = this.height;
|
|
107
135
|
const fullFrames = this.some(f => f.disposalMode === 'previous' || (f.disposalMode === 'background' && (f.xOffset & 1 || f.yOffset & 1)));
|
|
108
|
-
if (fullFrames)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const frames = Array.from(this, frame => {
|
|
136
|
+
if (fullFrames)
|
|
137
|
+
return png.compositeAnimation({ width, height, frames: this.__apngFrames__() })
|
|
138
|
+
.map(c => ({ x: 0, y: 0, width, height, duration: c.duration, dispose: true, pixels: c.pixels }));
|
|
139
|
+
return Array.from(this, frame => {
|
|
113
140
|
let { xOffset: x, yOffset: y, width: w, height: h } = frame;
|
|
114
141
|
let pixels = new Uint8Array(frame.bitmap.buffer, frame.bitmap.byteOffset, frame.bitmap.byteLength);
|
|
115
142
|
if (x & 1 || y & 1) {
|
|
@@ -125,7 +152,9 @@ export class Animation extends Array {
|
|
|
125
152
|
}
|
|
126
153
|
return { x, y, width: w, height: h, duration: frame.duration, dispose: frame.disposalMode === 'background', pixels };
|
|
127
154
|
});
|
|
128
|
-
|
|
155
|
+
}
|
|
156
|
+
__optimized__(options) {
|
|
157
|
+
return optimize(this.__apngFrames__(), this.width, this.height, options);
|
|
129
158
|
}
|
|
130
159
|
__apngFrames__() {
|
|
131
160
|
const dispose = [0, 0, 2, 1];
|
|
@@ -140,7 +169,8 @@ export class Animation extends Array {
|
|
|
140
169
|
pixels: new Uint8Array(frame.bitmap.buffer, frame.bitmap.byteOffset, frame.bitmap.byteLength)
|
|
141
170
|
}));
|
|
142
171
|
}
|
|
143
|
-
static async decode(input,
|
|
172
|
+
static async decode(input, onlyFirstFrameOrOptions = false) {
|
|
173
|
+
const onlyExtractFirstFrame = isOptions(onlyFirstFrameOrOptions) ? onlyFirstFrameOrOptions.onlyFirstFrame ?? false : onlyFirstFrameOrOptions;
|
|
144
174
|
const data = view(input);
|
|
145
175
|
const type = ImageType.getType(data);
|
|
146
176
|
if (type === 'gif')
|
package/dist/codecs/exif.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { concat } from "../util/mem.js";
|
|
2
|
+
import { encode as utf8 } from "../util/utf8.js";
|
|
1
3
|
export function orientation(data) {
|
|
2
4
|
if (data.length >= 6 && data[0] === 0x45 && data[1] === 0x78 && data[2] === 0x69 && data[3] === 0x66 && data[4] === 0 && data[5] === 0)
|
|
3
5
|
data = data.subarray(6);
|
|
@@ -25,3 +27,63 @@ export function xmpOrientation(xmp) {
|
|
|
25
27
|
const match = /tiff:Orientation\s*(?:=\s*["']|>\s*)([1-8])\b/.exec(xmp);
|
|
26
28
|
return match ? +match[1] : 1;
|
|
27
29
|
}
|
|
30
|
+
const ASCII = 2, LONG = 4, UNDEFINED = 7;
|
|
31
|
+
const ascii = (tag, text) => {
|
|
32
|
+
const data = concat([utf8(text), [0]]);
|
|
33
|
+
return { tag, type: ASCII, count: data.length, data };
|
|
34
|
+
};
|
|
35
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
36
|
+
export function encode(fields) {
|
|
37
|
+
const ifd0 = [];
|
|
38
|
+
const exifIFD = [];
|
|
39
|
+
if (fields.description)
|
|
40
|
+
ifd0.push(ascii(0x010e, fields.description));
|
|
41
|
+
if (fields.software)
|
|
42
|
+
ifd0.push(ascii(0x0131, fields.software));
|
|
43
|
+
if (fields.dateTime) {
|
|
44
|
+
const d = fields.dateTime;
|
|
45
|
+
const time = `${d.getUTCFullYear()}:${pad(d.getUTCMonth() + 1)}:${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
|
|
46
|
+
ifd0.push(ascii(0x0132, time));
|
|
47
|
+
exifIFD.push({ tag: 0x9000, type: UNDEFINED, count: 4, data: Uint8Array.of(0x30, 0x32, 0x33, 0x32) });
|
|
48
|
+
exifIFD.push(ascii(0x9003, time));
|
|
49
|
+
exifIFD.push(ascii(0x9010, '+00:00'));
|
|
50
|
+
exifIFD.push(ascii(0x9011, '+00:00'));
|
|
51
|
+
}
|
|
52
|
+
if (fields.artist)
|
|
53
|
+
ifd0.push(ascii(0x013b, fields.artist));
|
|
54
|
+
if (fields.copyright)
|
|
55
|
+
ifd0.push(ascii(0x8298, fields.copyright));
|
|
56
|
+
if (exifIFD.length)
|
|
57
|
+
ifd0.push({ tag: 0x8769, type: LONG, count: 1, data: new Uint8Array(4) });
|
|
58
|
+
const size = (entries) => 2 + entries.length * 12 + 4 + entries.reduce((n, e) => n + (e.data.length > 4 ? e.data.length + (e.data.length & 1) : 0), 0);
|
|
59
|
+
const ifd0Offset = 8;
|
|
60
|
+
const exifOffset = ifd0Offset + size(ifd0);
|
|
61
|
+
const out = new Uint8Array(exifOffset + (exifIFD.length ? size(exifIFD) : 0));
|
|
62
|
+
const view = new DataView(out.buffer);
|
|
63
|
+
out.set([0x4d, 0x4d, 0x00, 0x2a]);
|
|
64
|
+
view.setUint32(4, ifd0Offset);
|
|
65
|
+
const pointer = ifd0.find(e => e.tag === 0x8769);
|
|
66
|
+
if (pointer)
|
|
67
|
+
new DataView(pointer.data.buffer).setUint32(0, exifOffset);
|
|
68
|
+
const write = (entries, offset) => {
|
|
69
|
+
view.setUint16(offset, entries.length);
|
|
70
|
+
let data = offset + 2 + entries.length * 12 + 4;
|
|
71
|
+
entries.forEach((e, i) => {
|
|
72
|
+
const at = offset + 2 + i * 12;
|
|
73
|
+
view.setUint16(at, e.tag);
|
|
74
|
+
view.setUint16(at + 2, e.type);
|
|
75
|
+
view.setUint32(at + 4, e.count);
|
|
76
|
+
if (e.data.length <= 4)
|
|
77
|
+
out.set(e.data, at + 8);
|
|
78
|
+
else {
|
|
79
|
+
view.setUint32(at + 8, data);
|
|
80
|
+
out.set(e.data, data);
|
|
81
|
+
data += e.data.length + (e.data.length & 1);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
write(ifd0, ifd0Offset);
|
|
86
|
+
if (exifIFD.length)
|
|
87
|
+
write(exifIFD, exifOffset);
|
|
88
|
+
return out;
|
|
89
|
+
}
|
package/dist/codecs/gif.js
CHANGED
|
@@ -3,7 +3,7 @@ const DISPOSAL_TO_GIF = [0, 1, 3, 2];
|
|
|
3
3
|
const DISPOSAL_FROM_GIF = [0, 1, 3, 2, 0, 0, 0, 0];
|
|
4
4
|
const MAX_CODE = 4095;
|
|
5
5
|
const UNEXPECTED_END = new Error('GIF: unexpected end of data');
|
|
6
|
-
export function encode({ width, height, loopCount = -1, frames }, quality = 95) {
|
|
6
|
+
export function encode({ width, height, loopCount = -1, frames }, quality = 95, comments = []) {
|
|
7
7
|
const out = new ByteWriter();
|
|
8
8
|
out.bytes(0x47, 0x49, 0x46, 0x38, 0x39, 0x61);
|
|
9
9
|
out.u16(width);
|
|
@@ -16,8 +16,14 @@ export function encode({ width, height, loopCount = -1, frames }, quality = 95)
|
|
|
16
16
|
out.u16(loopCount === -1 ? 0 : loopCount);
|
|
17
17
|
out.bytes(0x00);
|
|
18
18
|
}
|
|
19
|
-
for (const
|
|
20
|
-
|
|
19
|
+
for (const comment of comments) {
|
|
20
|
+
out.bytes(0x21, 0xfe);
|
|
21
|
+
writeSubBlocks(out, comment);
|
|
22
|
+
}
|
|
23
|
+
const restores = frames.some(frame => frame.dispose === 3);
|
|
24
|
+
for (const [i, frame] of frames.entries()) {
|
|
25
|
+
const transparent = frame.dispose === 3 || (i === 0 && restores);
|
|
26
|
+
const { palette, indices, transparentIndex } = quantize(frame.pixels, frame.width, { quality, transparent });
|
|
21
27
|
const entries = palette.length / 3;
|
|
22
28
|
const bits = Math.max(1, Math.ceil(Math.log2(entries)));
|
|
23
29
|
out.bytes(0x21, 0xf9, 0x04);
|
|
@@ -182,24 +188,20 @@ export function decode(data, onlyFirstFrame = false) {
|
|
|
182
188
|
control = null;
|
|
183
189
|
if (!fw || !fh)
|
|
184
190
|
continue;
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
pixels[o + 2] = table[index * 3 + 2];
|
|
198
|
-
}
|
|
199
|
-
pixels[o + 3] = 0xff;
|
|
200
|
-
}
|
|
191
|
+
const colors = new Uint32Array(256), bytes = new Uint8Array(colors.buffer);
|
|
192
|
+
for (let index = 0; index < 256; index++) {
|
|
193
|
+
if (table && index * 3 + 2 < table.length)
|
|
194
|
+
bytes.set(table.subarray(index * 3, index * 3 + 3), index * 4);
|
|
195
|
+
bytes[index * 4 + 3] = 0xff;
|
|
196
|
+
}
|
|
197
|
+
let ordered = indices;
|
|
198
|
+
if (interlaced) {
|
|
199
|
+
const rows = interlacedRows(fh);
|
|
200
|
+
ordered = new Uint8Array(indices.length);
|
|
201
|
+
for (let row = 0; row < fh; row++)
|
|
202
|
+
ordered.set(indices.subarray(row * fw, (row + 1) * fw), rows[row] * fw);
|
|
201
203
|
}
|
|
202
|
-
frames.push({ x, y, width: fw, height: fh, delay, dispose,
|
|
204
|
+
frames.push({ x, y, width: fw, height: fh, delay, dispose, indices: ordered, colors, transparentIndex });
|
|
203
205
|
if (onlyFirstFrame)
|
|
204
206
|
break;
|
|
205
207
|
}
|
|
@@ -210,15 +212,21 @@ export function composite({ width, height, frames }) {
|
|
|
210
212
|
const output = [];
|
|
211
213
|
for (const frame of frames) {
|
|
212
214
|
const previous = frame.dispose === 2 ? canvas.slice() : null;
|
|
213
|
-
const
|
|
215
|
+
const { indices, colors, transparentIndex } = frame;
|
|
214
216
|
const x0 = Math.min(frame.x, width), x1 = Math.min(frame.x + frame.width, width);
|
|
215
217
|
const y0 = Math.min(frame.y, height), y1 = Math.min(frame.y + frame.height, height);
|
|
216
218
|
for (let y = y0; y < y1; y++) {
|
|
217
219
|
const srcRow = (y - frame.y) * frame.width - frame.x;
|
|
218
220
|
const dstRow = y * width;
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
canvas[dstRow + x] =
|
|
221
|
+
if (transparentIndex < 0)
|
|
222
|
+
for (let x = x0; x < x1; x++)
|
|
223
|
+
canvas[dstRow + x] = colors[indices[srcRow + x]];
|
|
224
|
+
else
|
|
225
|
+
for (let x = x0; x < x1; x++) {
|
|
226
|
+
const index = indices[srcRow + x];
|
|
227
|
+
if (index !== transparentIndex)
|
|
228
|
+
canvas[dstRow + x] = colors[index];
|
|
229
|
+
}
|
|
222
230
|
}
|
|
223
231
|
output.push({ pixels: new Uint8Array(canvas.slice().buffer), delay: frame.delay });
|
|
224
232
|
if (frame.dispose === 3) {
|
|
@@ -287,10 +295,18 @@ function lzwDecode(data, minCodeSize, pixelCount) {
|
|
|
287
295
|
const length = lengths[entry];
|
|
288
296
|
const end = Math.min(out + length, pixelCount);
|
|
289
297
|
let c = entry;
|
|
290
|
-
|
|
291
|
-
|
|
298
|
+
if (end === out + length) {
|
|
299
|
+
for (let i = end - 1; i >= out; i--) {
|
|
292
300
|
output[i] = suffix[c];
|
|
293
|
-
|
|
301
|
+
c = prefix[c];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
for (let i = out + length - 1; i >= out; i--) {
|
|
306
|
+
if (i < end)
|
|
307
|
+
output[i] = suffix[c];
|
|
308
|
+
c = prefix[c];
|
|
309
|
+
}
|
|
294
310
|
}
|
|
295
311
|
let written = end - out;
|
|
296
312
|
if (code === next) {
|
package/dist/codecs/jpeg.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* (Copyright 2014 Mozilla Foundation, Apache License 2.0). See THIRD_PARTY_NOTICES.md
|
|
6
6
|
*/
|
|
7
7
|
import { orientation as exifOrientation } from "./exif.js";
|
|
8
|
+
import { concat } from "../util/mem.js";
|
|
8
9
|
const ZIGZAG = Int32Array.of(0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63);
|
|
9
10
|
function clamp8(v) {
|
|
10
11
|
return v < 0 ? 0 : v > 255 ? 255 : v;
|
|
@@ -684,7 +685,7 @@ const CHROMA_QUANTIZATION = [
|
|
|
684
685
|
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99
|
|
685
686
|
];
|
|
686
687
|
const AAN_SCALE = [1, 1.387039845, 1.306562965, 1.175875602, 1, 0.785694958, 0.5411961, 0.275899379];
|
|
687
|
-
export function encode(pixels, width, height, quality = 90) {
|
|
688
|
+
export function encode(pixels, width, height, quality = 90, { exif, comment } = {}) {
|
|
688
689
|
if (!width || !height || width > 0xffff || height > 0xffff)
|
|
689
690
|
throw new RangeError('JPEG: invalid dimensions');
|
|
690
691
|
quality = Math.min(100, Math.max(1, Math.round(quality)));
|
|
@@ -769,6 +770,17 @@ export function encode(pixels, width, height, quality = 90) {
|
|
|
769
770
|
const writer = new BitWriter(width * height);
|
|
770
771
|
writer.bytes(0xff, 0xd8);
|
|
771
772
|
writer.bytes(0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00);
|
|
773
|
+
const segment = (marker, body) => {
|
|
774
|
+
if (body.length > 65533)
|
|
775
|
+
throw new RangeError('JPEG: metadata too large');
|
|
776
|
+
writer.bytes(0xff, marker, body.length + 2 >> 8, body.length + 2 & 255);
|
|
777
|
+
for (let i = 0; i < body.length; i++)
|
|
778
|
+
writer.bytes(body[i]);
|
|
779
|
+
};
|
|
780
|
+
if (exif)
|
|
781
|
+
segment(0xe1, concat([[0x45, 0x78, 0x69, 0x66, 0, 0], exif]));
|
|
782
|
+
if (comment)
|
|
783
|
+
segment(0xfe, comment);
|
|
772
784
|
writer.bytes(0xff, 0xdb, 0x00, 0x84);
|
|
773
785
|
for (let t = 0; t < 2; t++) {
|
|
774
786
|
writer.bytes(t);
|