imagescript 3.0.0 → 3.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.
Files changed (52) hide show
  1. package/README.md +29 -9
  2. package/THIRD_PARTY_NOTICES.md +82 -0
  3. package/dist/animation.d.ts +140 -0
  4. package/dist/animation.js +197 -0
  5. package/dist/codecs/exif.js +62 -0
  6. package/dist/codecs/gif.js +32 -19
  7. package/dist/codecs/jpeg.js +13 -1
  8. package/dist/codecs/png.js +348 -62
  9. package/dist/codecs/quantize.js +2 -2
  10. package/dist/codecs/tga.js +246 -0
  11. package/dist/codecs/vp8-tables.js +17 -0
  12. package/dist/codecs/vp8.js +711 -0
  13. package/dist/codecs/vp8l.js +906 -0
  14. package/dist/codecs/webp.js +293 -0
  15. package/dist/frame.d.ts +40 -12
  16. package/dist/frame.js +20 -9
  17. package/dist/gif.d.ts +16 -26
  18. package/dist/gif.js +10 -81
  19. package/dist/image-type.d.ts +6 -1
  20. package/dist/image-type.js +17 -1
  21. package/dist/image.d.ts +262 -30
  22. package/dist/image.js +144 -45
  23. package/dist/index.d.ts +20 -6
  24. package/dist/index.js +13 -4
  25. package/dist/metadata.d.ts +35 -0
  26. package/dist/metadata.js +43 -0
  27. package/dist/render/font.js +99 -2
  28. package/dist/render/hinting.js +1236 -0
  29. package/dist/render/svg.js +39 -21
  30. package/dist/render/text.js +32 -7
  31. package/dist/util/optimize.js +264 -0
  32. package/dist/util/options.js +3 -0
  33. package/dist/util/resample.js +44 -26
  34. package/dist/util/utf8.js +4 -51
  35. package/dist/version.js +1 -1
  36. package/licenses/FTL.txt +169 -0
  37. package/package.json +10 -3
  38. package/dist/codecs/crc.d.ts +0 -9
  39. package/dist/codecs/exif.d.ts +0 -11
  40. package/dist/codecs/gif.d.ts +0 -44
  41. package/dist/codecs/jpeg.d.ts +0 -35
  42. package/dist/codecs/png.d.ts +0 -44
  43. package/dist/codecs/quantize.d.ts +0 -25
  44. package/dist/codecs/tiff.d.ts +0 -6
  45. package/dist/codecs/zlib.d.ts +0 -22
  46. package/dist/render/font.d.ts +0 -63
  47. package/dist/render/svg.d.ts +0 -11
  48. package/dist/render/text.d.ts +0 -61
  49. package/dist/util/blur.d.ts +0 -13
  50. package/dist/util/resample.d.ts +0 -12
  51. package/dist/util/utf8.d.ts +0 -5
  52. 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 120 kB (compressed) and runs in Node.js 22.13 or newer (including Alpine/musl, ARM and bundled
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
@@ -25,26 +25,36 @@ deno add jsr:@matmen/imagescript # Deno (npm:imagescript works too)
25
25
  - PNGs (grayscale, RGB, indexed colors, 1-16 bit, interlaced) with and without alpha channels
26
26
  - JPEGs (baseline and progressive; grayscale, RGB, CMYK)
27
27
  - TIFFs (strips and tiles; uncompressed, LZW, deflate, PackBits and JPEG compressed; BigTIFF)
28
- - JPEGs, TIFFs and PNGs are rotated as their Exif (or, for PNGs, XMP) orientation says
29
- - [Decoding GIFs](https://imagescript.matmen.dev/classes/GIF.html#decode) with all frames, durations and disposal modes
28
+ - WebPs (lossy, lossless and with alpha; bit-exact with libwebp)
29
+ - TGAs (color mapped, truecolor and greyscale; uncompressed and run length encoded)
30
+ - JPEGs, TIFFs, PNGs and WebPs are rotated as their Exif (or, for PNGs, XMP) orientation says
31
+ - [Decoding animations](https://imagescript.matmen.dev/classes/Animation.html#decode) (GIF, APNG and WebP) with all
32
+ frames, durations and disposal modes
30
33
  - [Rendering SVGs](https://imagescript.matmen.dev/classes/Image.html#rendersvg) (shapes, paths, gradients, strokes, clip paths,
31
34
  masks, CSS; no text, images or filters)
32
- - [Rendering vector fonts](https://imagescript.matmen.dev/classes/Image.html#rendertext) (TrueType and OpenType, with kerning and outlines)
35
+ - [Rendering vector fonts](https://imagescript.matmen.dev/classes/Image.html#rendertext) (TrueType and OpenType, with kerning,
36
+ outlines and TrueType hinting)
33
37
  - [Resizing](https://imagescript.matmen.dev/classes/Image.html#resize) (bilinear, bicubic, area averaging and nearest neighbor)
34
38
  - Image manipulation functions ([crop](https://imagescript.matmen.dev/classes/Image.html#crop)
35
39
  , [rotate](https://imagescript.matmen.dev/classes/Image.html#rotate)
36
40
  , [cropCircle](https://imagescript.matmen.dev/classes/Image.html#cropcircle)
37
- , [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), ...)
38
43
  - Filters and color adjustments ([blur](https://imagescript.matmen.dev/classes/Image.html#blur)
39
44
  , [sharpen](https://imagescript.matmen.dev/classes/Image.html#sharpen)
40
45
  , [brightness](https://imagescript.matmen.dev/classes/Image.html#brightness)
41
46
  , [hueShift](https://imagescript.matmen.dev/classes/Image.html#hueshift), ...)
42
- - Compositing with opacity and blend modes ([composite](https://imagescript.matmen.dev/classes/Image.html#composite))
47
+ - Compositing with opacity and blend modes ([composite](https://imagescript.matmen.dev/classes/Image.html#composite)),
48
+ removing backgrounds ([colorToAlpha](https://imagescript.matmen.dev/classes/Image.html#colortoalpha))
43
49
  - Color information functions ([averageColor](https://imagescript.matmen.dev/classes/Image.html#averagecolor)
44
50
  , [dominantColor](https://imagescript.matmen.dev/classes/Image.html#dominantcolor), ...)
45
51
  - Encoding images as [PNGs](https://imagescript.matmen.dev/classes/Image.html#encode) (smallest lossless color type)
46
52
  , [JPEGs](https://imagescript.matmen.dev/classes/Image.html#encodejpeg)
47
- and animated [GIFs](https://imagescript.matmen.dev/classes/GIF.html#encode)
53
+ , lossless [WebPs](https://imagescript.matmen.dev/classes/Image.html#encodewebp)
54
+ and [TGAs](https://imagescript.matmen.dev/classes/Image.html#encodetga)
55
+ - Encoding animations as [GIF](https://imagescript.matmen.dev/classes/Animation.html#encodegif),
56
+ [APNG](https://imagescript.matmen.dev/classes/Animation.html#encodeapng) and lossless
57
+ [WebP](https://imagescript.matmen.dev/classes/Animation.html#encodewebp), storing only what changes from frame to frame
48
58
 
49
59
  ---
50
60
 
@@ -68,7 +78,7 @@ const { Image } = require('imagescript');
68
78
  All coordinates are 0-based, `(0, 0)` is the top left pixel.
69
79
 
70
80
  A bigger example is the [README image generation](https://github.com/matmen/ImageScript/blob/master/tests/readme.js).
71
- 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+, ...),
72
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
73
83
  `imagescript/v2` API.
74
84
 
@@ -76,13 +86,23 @@ see the [changelog](https://github.com/matmen/ImageScript/blob/master/CHANGELOG.
76
86
 
77
87
  ---
78
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
+
79
96
  ### Credits
80
97
 
81
98
  The deflate/inflate implementation is derived from [fflate](https://github.com/101arrowz/fflate) and the CRC-32 from
82
99
  [Fast CRC32 in JavaScript](https://gist.github.com/101arrowz/e58695f7ccfdf74f60ba22018093edea), both by Arjun Barrett.
83
100
  The JPEG codec is based in part on the work of the Independent JPEG Group (libjpeg) and on
84
101
  [pdf.js](https://github.com/mozilla/pdf.js), the glyph rasterizer is translated from
85
- [font-rs](https://github.com/raphlinus/font-rs). See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for their licenses.
102
+ [font-rs](https://github.com/raphlinus/font-rs). The WebP codecs follow [RFC 9649](https://www.rfc-editor.org/rfc/rfc9649) and [RFC 6386](https://www.rfc-editor.org/rfc/rfc6386)
103
+ and reproduce [libwebp](https://chromium.googlesource.com/webm/libwebp)'s color conversion, the TrueType hinting follows
104
+ [FreeType](https://freetype.org)'s interpreter (portions of this software are copyright © 2026 The FreeType Project).
105
+ See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for their licenses.
86
106
 
87
107
  ---
88
108
 
@@ -10,6 +10,10 @@ following projects, under their own licenses. Each derived file says so in its h
10
10
  | [libjpeg](https://ijg.org/) / [libjpeg-turbo](https://libjpeg-turbo.org/) by the Independent JPEG Group | `src/codecs/jpeg.ts` (IDCT, FDCT, upsampling, color conversion, progressive refinement, optimal Huffman tables) | IJG |
11
11
  | [pdf.js](https://github.com/mozilla/pdf.js) (forked from [jpgjs](https://github.com/notmasteryet/jpgjs) by notmasteryet) | `src/codecs/jpeg.ts` (structure of the scan decoder) | Apache-2.0 |
12
12
  | [font-rs](https://github.com/raphlinus/font-rs) by Raph Levien | `src/render/text.ts` (the `Rasterizer` class, also used for SVGs) | Apache-2.0 |
13
+ | [RFC 9649](https://www.rfc-editor.org/rfc/rfc9649) (WebP Image Format) | `src/codecs/vp8l.ts` (lossless bitstream, short distance code table), `src/codecs/webp.ts` (container) | BSD-3-Clause (Revised BSD) |
14
+ | [RFC 6386](https://www.rfc-editor.org/rfc/rfc6386) (VP8 Data Format and Decoding Guide, with the libvpx reference decoder) | `src/codecs/vp8-tables.ts` (probability and quantizer tables), `src/codecs/vp8.ts` (decoding procedures) | BSD-3-Clause |
15
+ | [libwebp](https://chromium.googlesource.com/webm/libwebp) | `src/codecs/vp8.ts` (YUV to RGB conversion, "fancy" chroma upsampling), `src/codecs/webp.ts` (animation frame blending) | BSD-3-Clause |
16
+ | [FreeType](https://freetype.org) | `src/render/hinting.ts` (behavior and parts of the logic of the TrueType interpreter) | FreeType License |
13
17
 
14
18
  ## fflate, Fast CRC32 in JavaScript
15
19
 
@@ -72,3 +76,81 @@ The scan decoder of `src/codecs/jpeg.ts` follows the structure of pdf.js' `src/c
72
76
  one decode function per scan type). The `Rasterizer` class in `src/render/text.ts` is a translation of font-rs'
73
77
  `Raster` from Rust to TypeScript (`draw_line`, `draw_quad`, `accumulate`); its `cubic` method is adapted from
74
78
  `draw_quad`. The full license text is in [licenses/Apache-2.0.txt](licenses/Apache-2.0.txt).
79
+
80
+ ## RFC 6386, libwebp
81
+
82
+ The VP8 decoder follows the procedures of RFC 6386 and its reference decoder, its constant tables are extracted from
83
+ the RFC. To decode exactly like libwebp, the YUV to RGB conversion, the chroma upsampling and the blending of animation
84
+ frames reproduce libwebp's integer arithmetic.
85
+
86
+ ```
87
+ RFC 6386 reference decoder: Copyright (c) 2010, 2011, Google Inc. All rights reserved.
88
+ libwebp: Copyright (c) 2010, Google Inc. All rights reserved.
89
+
90
+ Redistribution and use in source and binary forms, with or without
91
+ modification, are permitted provided that the following conditions are
92
+ met:
93
+
94
+ * Redistributions of source code must retain the above copyright
95
+ notice, this list of conditions and the following disclaimer.
96
+
97
+ * Redistributions in binary form must reproduce the above copyright
98
+ notice, this list of conditions and the following disclaimer in
99
+ the documentation and/or other materials provided with the
100
+ distribution.
101
+
102
+ * Neither the name of Google nor the names of its contributors may
103
+ be used to endorse or promote products derived from this software
104
+ without specific prior written permission.
105
+
106
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
107
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
108
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
109
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
110
+ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
111
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
112
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
113
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
114
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
115
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
116
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
117
+ ```
118
+
119
+ The RFC's reference decoder additionally comes with Google's patent grant for VP8 (RFC 6386 section 20.27).
120
+
121
+ ## RFC 9649
122
+
123
+ The WebP container and the lossless (VP8L) codec follow RFC 9649, the short distance code table is taken from it.
124
+
125
+ ```
126
+ Copyright (c) 2024 IETF Trust and the persons identified as authors of the code. All rights reserved.
127
+
128
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
129
+ following conditions are met:
130
+
131
+ - Redistributions of source code must retain the above copyright notice, this list of conditions and the following
132
+ disclaimer.
133
+ - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
134
+ following disclaimer in the documentation and/or other materials provided with the distribution.
135
+ - Neither the name of Internet Society, IETF or IETF Trust, nor the names of specific contributors, may be used to
136
+ endorse or promote products derived from this software without specific prior written permission.
137
+
138
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
139
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
140
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
141
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
142
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
143
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
144
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
145
+ ```
146
+
147
+ ## FreeType
148
+
149
+ Portions of this software are copyright © 2026 The FreeType Project (https://freetype.org). All rights reserved.
150
+
151
+ The TrueType hinting interpreter in `src/render/hinting.ts` is written after the TrueType instruction specification.
152
+ Where the specification leaves details open, its behavior and parts of its logic follow FreeType's interpreter
153
+ (`src/truetype/ttinterp.c`): the fixed point rounding, measuring original distances on the unscaled outline, the
154
+ interpolation of untouched points (IUP), the handling of near-perpendicular freedom and projection vectors, and the
155
+ tolerance of invalid references. FreeType is used under the FreeType License, included as
156
+ [licenses/FTL.txt](licenses/FTL.txt).
@@ -0,0 +1,140 @@
1
+ import { BinaryData } from './util/mem.js';
2
+ import { ResizeMode, ResizeOptions } from './image.js';
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
+ }
36
+ /**
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.
42
+ */
43
+ export declare class Animation extends Array<Frame> {
44
+ /** How many times to repeat the animation (-1 = forever, 0 = play once) */
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);
52
+ /**
53
+ * Creates a new animation
54
+ * @param frames The frames to create the animation from
55
+ * @param loopCount How many times to repeat the animation (-1 = forever, 0 = play once)
56
+ */
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[]);
65
+ /** The animation's width */
66
+ get width(): number;
67
+ /** The animation's height */
68
+ get height(): number;
69
+ /** The animation's duration (in ms) */
70
+ get duration(): number;
71
+ toString(): string;
72
+ /**
73
+ * Returns a new animation of the same class containing a range of this one's frames
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
77
+ */
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;
84
+ /**
85
+ * Resizes the animation to the given dimensions, scaling every frame and its offsets proportionally.
86
+ * Use {@link Image.RESIZE_AUTO} as either width or height to automatically preserve the aspect ratio.
87
+ */
88
+ resize(width: number, height: number, mode?: ResizeMode): this;
89
+ /**
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
102
+ * @param quality 1-100, scales the palette size from 8 colors (1) to 256 colors (90 and above)
103
+ */
104
+ encodeGIF(quality?: number): Promise<Uint8Array>;
105
+ /**
106
+ * Encodes the animation as APNG (animated PNG, lossless and with full alpha). Viewers without APNG support show
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
118
+ * @param compression The zlib compression level (0-9), higher is smaller but slower
119
+ */
120
+ encodeAPNG(compression?: number): Promise<Uint8Array>;
121
+ /**
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}
132
+ */
133
+ static decode(input: BinaryData, options?: AnimationDecodeOptions): Promise<Animation>;
134
+ /**
135
+ * Decodes an animated GIF, APNG or WebP into fully composited frames (still images become a single frame)
136
+ * @param input The binary data to decode
137
+ * @param onlyExtractFirstFrame Whether to stop decoding after the first frame
138
+ */
139
+ static decode(input: BinaryData, onlyExtractFirstFrame?: boolean): Promise<Animation>;
140
+ }
@@ -0,0 +1,197 @@
1
+ import * as gifCodec from "./codecs/gif.js";
2
+ import * as png from "./codecs/png.js";
3
+ import * as webp from "./codecs/webp.js";
4
+ import { view } from "./util/mem.js";
5
+ import { Image } from "./image.js";
6
+ import { Frame } from "./frame.js";
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";
11
+ const toPlays = (loopCount) => loopCount < 0 ? 0 : loopCount + 1;
12
+ const fromPlays = (plays) => plays === 0 ? -1 : plays - 1;
13
+ function canvasFrames(width, height, frames) {
14
+ return frames.map(({ pixels, duration }) => {
15
+ const frame = new Frame(width, height, duration, 0, 0, Frame.DISPOSAL_BACKGROUND);
16
+ frame.bitmap.set(pixels);
17
+ return frame;
18
+ });
19
+ }
20
+ export class Animation extends Array {
21
+ loopCount;
22
+ constructor(frames, loopCountOrOptions = -1) {
23
+ super();
24
+ const { loopCount = -1 } = isOptions(loopCountOrOptions) ? loopCountOrOptions : { loopCount: loopCountOrOptions };
25
+ for (const frame of frames)
26
+ this.push(frame);
27
+ for (let i = 0; i < this.length; i++)
28
+ if (!(this[i] instanceof Frame))
29
+ throw new TypeError(`Frame ${i} is not an instance of Frame`);
30
+ if (loopCount < -1 || isNaN(loopCount))
31
+ throw new RangeError('Invalid loop count');
32
+ this.loopCount = loopCount;
33
+ }
34
+ static get [Symbol.species]() {
35
+ return Array;
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
+ }
50
+ get width() {
51
+ let max = 0;
52
+ for (const frame of this)
53
+ max = Math.max(max, frame.width + frame.xOffset);
54
+ return max;
55
+ }
56
+ get height() {
57
+ let max = 0;
58
+ for (const frame of this)
59
+ max = Math.max(max, frame.height + frame.yOffset);
60
+ return max;
61
+ }
62
+ get duration() {
63
+ let duration = 0;
64
+ for (const frame of this)
65
+ duration += frame.duration;
66
+ return duration;
67
+ }
68
+ toString() {
69
+ return `Animation<${this.width}x${this.height}x${this.duration}ms>`;
70
+ }
71
+ slice(start, end) {
72
+ const Class = this.constructor;
73
+ return new Class(Array.prototype.slice.call(this, start, end), this.loopCount);
74
+ }
75
+ resize(width, height, modeOrOptions) {
76
+ const mode = (isOptions(modeOrOptions) ? modeOrOptions.mode : modeOrOptions) ?? Image.RESIZE_BILINEAR;
77
+ const originalWidth = this.width, originalHeight = this.height;
78
+ if (width === Image.RESIZE_AUTO && height === Image.RESIZE_AUTO)
79
+ throw new Error('RESIZE_AUTO can only be used for either width or height, not for both');
80
+ if (width === Image.RESIZE_AUTO)
81
+ width = originalWidth / originalHeight * height;
82
+ if (height === Image.RESIZE_AUTO)
83
+ height = originalHeight / originalWidth * width;
84
+ const sx = width / originalWidth, sy = height / originalHeight;
85
+ for (const frame of this) {
86
+ const left = Math.round(frame.xOffset * sx), top = Math.round(frame.yOffset * sy);
87
+ const right = Math.round((frame.xOffset + frame.width) * sx), bottom = Math.round((frame.yOffset + frame.height) * sy);
88
+ Image.prototype.resize.call(frame, Math.max(1, right - left), Math.max(1, bottom - top), mode);
89
+ frame.xOffset = left;
90
+ frame.yOffset = top;
91
+ }
92
+ return this;
93
+ }
94
+ __validate__() {
95
+ for (const frame of this)
96
+ if (!(frame instanceof Frame))
97
+ throw new Error('Animation contains invalid frames');
98
+ }
99
+ async encodeGIF(qualityOrOptions = 95) {
100
+ const { quality = 95, metadata = {}, optimize = true } = isOptions(qualityOrOptions) ? qualityOrOptions : { quality: qualityOrOptions };
101
+ this.__validate__();
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 => ({
105
+ x: frame.xOffset,
106
+ y: frame.yOffset,
107
+ width: frame.width,
108
+ height: frame.height,
109
+ duration: frame.duration,
110
+ dispose: frame.__disposal_id__,
111
+ pixels: new Uint8Array(frame.bitmap.buffer, frame.bitmap.byteOffset, frame.bitmap.byteLength)
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)));
115
+ }
116
+ async encodeAPNG(compressionOrOptions = 1) {
117
+ const { compression = 1, metadata = {}, optimize = true } = isOptions(compressionOrOptions) ? compressionOrOptions : { compression: compressionOrOptions };
118
+ this.__validate__();
119
+ if (!this.length)
120
+ throw new Error('An animation needs at least one frame');
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)));
125
+ }
126
+ async encodeWebP({ metadata = {}, optimize = true } = {}) {
127
+ this.__validate__();
128
+ if (!this.length)
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__() {
134
+ const width = this.width, height = this.height;
135
+ const fullFrames = this.some(f => f.disposalMode === 'previous' || (f.disposalMode === 'background' && (f.xOffset & 1 || f.yOffset & 1)));
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 => {
140
+ let { xOffset: x, yOffset: y, width: w, height: h } = frame;
141
+ let pixels = new Uint8Array(frame.bitmap.buffer, frame.bitmap.byteOffset, frame.bitmap.byteLength);
142
+ if (x & 1 || y & 1) {
143
+ const dx = x & 1, dy = y & 1;
144
+ const padded = new Uint8Array((w + dx) * (h + dy) * 4);
145
+ for (let row = 0; row < h; row++)
146
+ padded.set(pixels.subarray(row * w * 4, (row + 1) * w * 4), ((row + dy) * (w + dx) + dx) * 4);
147
+ pixels = padded;
148
+ x -= dx;
149
+ y -= dy;
150
+ w += dx;
151
+ h += dy;
152
+ }
153
+ return { x, y, width: w, height: h, duration: frame.duration, dispose: frame.disposalMode === 'background', pixels };
154
+ });
155
+ }
156
+ __optimized__(options) {
157
+ return optimize(this.__apngFrames__(), this.width, this.height, options);
158
+ }
159
+ __apngFrames__() {
160
+ const dispose = [0, 0, 2, 1];
161
+ return Array.from(this, frame => ({
162
+ x: frame.xOffset,
163
+ y: frame.yOffset,
164
+ width: frame.width,
165
+ height: frame.height,
166
+ duration: frame.duration,
167
+ dispose: dispose[frame.__disposal_id__],
168
+ blend: 1,
169
+ pixels: new Uint8Array(frame.bitmap.buffer, frame.bitmap.byteOffset, frame.bitmap.byteLength)
170
+ }));
171
+ }
172
+ static async decode(input, onlyFirstFrameOrOptions = false) {
173
+ const onlyExtractFirstFrame = isOptions(onlyFirstFrameOrOptions) ? onlyFirstFrameOrOptions.onlyFirstFrame ?? false : onlyFirstFrameOrOptions;
174
+ const data = view(input);
175
+ const type = ImageType.getType(data);
176
+ if (type === 'gif')
177
+ return Animation.__fromGIF__(data, onlyExtractFirstFrame, Animation);
178
+ if (type === 'webp') {
179
+ const decoded = webp.decodeAnimation(data, onlyExtractFirstFrame);
180
+ const frames = canvasFrames(decoded.width, decoded.height, webp.composite(decoded));
181
+ return new Animation(frames, fromPlays(decoded.loopCount));
182
+ }
183
+ if (type === 'png') {
184
+ const decoded = await png.decodeAnimation(data, onlyExtractFirstFrame);
185
+ const frames = canvasFrames(decoded.width, decoded.height, png.compositeAnimation(decoded));
186
+ return new Animation(frames, decoded.frames.length > 1 ? fromPlays(decoded.plays) : -1);
187
+ }
188
+ if (type)
189
+ return new Animation([Frame.from(await Image.decode(data), 0)]);
190
+ throw new Error('Unsupported image type');
191
+ }
192
+ static __fromGIF__(data, onlyExtractFirstFrame, Class) {
193
+ const decoded = gifCodec.decode(data, onlyExtractFirstFrame);
194
+ const frames = gifCodec.composite(decoded).map(({ pixels, delay }) => ({ pixels, duration: 10 * delay }));
195
+ return new Class(canvasFrames(decoded.width, decoded.height, frames), decoded.loopCount);
196
+ }
197
+ }
@@ -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
+ }