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
@@ -0,0 +1,293 @@
1
+ /*!
2
+ * WebP container. Animation frames are blended with libwebp's integer arithmetic (Copyright (c) 2010 Google Inc., BSD
3
+ * license, see THIRD_PARTY_NOTICES.md)
4
+ */
5
+ import * as vp8l from "./vp8l.js";
6
+ import * as vp8 from "./vp8.js";
7
+ import { orientation as exifOrientation } from "./exif.js";
8
+ const fourCC = (s) => s.charCodeAt(0) | s.charCodeAt(1) << 8 | s.charCodeAt(2) << 16 | s.charCodeAt(3) << 24;
9
+ const RIFF = fourCC('RIFF'), WEBP = fourCC('WEBP');
10
+ const VP8 = fourCC('VP8 '), VP8L = fourCC('VP8L'), VP8X = fourCC('VP8X');
11
+ const ALPH = fourCC('ALPH'), ANIM = fourCC('ANIM'), ANMF = fourCC('ANMF'), EXIF = fourCC('EXIF');
12
+ function chunks(data, start, end) {
13
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
14
+ const out = [];
15
+ let offset = start;
16
+ while (offset + 8 <= end) {
17
+ const type = view.getUint32(offset, true);
18
+ const size = view.getUint32(offset + 4, true);
19
+ const dataStart = offset + 8;
20
+ out.push({ type, data: data.subarray(dataStart, Math.min(end, dataStart + size)) });
21
+ offset = dataStart + size + (size & 1);
22
+ }
23
+ return out;
24
+ }
25
+ function container(data) {
26
+ if (data.length < 12)
27
+ throw new Error('WebP: invalid header');
28
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
29
+ if (view.getUint32(0, true) !== RIFF || view.getUint32(8, true) !== WEBP)
30
+ throw new Error('WebP: invalid header');
31
+ const size = view.getUint32(4, true);
32
+ return chunks(data, 12, Math.min(data.length, 8 + size));
33
+ }
34
+ const u24 = (d, o) => d[o] | d[o + 1] << 8 | d[o + 2] << 16;
35
+ function decodeBitstream(list) {
36
+ let alpha = null;
37
+ for (const chunk of list) {
38
+ if (chunk.type === ALPH)
39
+ alpha ??= chunk;
40
+ else if (chunk.type === VP8L) {
41
+ const { width, height, argb } = vp8l.decode(chunk.data);
42
+ const pixels = new Uint8Array(width * height * 4);
43
+ const out = new DataView(pixels.buffer);
44
+ for (let i = 0; i < argb.length; i++) {
45
+ const p = argb[i];
46
+ out.setUint32(i * 4, (p << 8 | p >>> 24) >>> 0);
47
+ }
48
+ return { width, height, pixels };
49
+ }
50
+ else if (chunk.type === VP8) {
51
+ const image = vp8.decode(chunk.data);
52
+ if (alpha)
53
+ applyAlpha(image.pixels, image.width, image.height, alpha.data);
54
+ return image;
55
+ }
56
+ }
57
+ throw new Error('WebP: no image data');
58
+ }
59
+ function applyAlpha(rgba, width, height, chunk) {
60
+ if (!chunk.length)
61
+ return;
62
+ const header = chunk[0];
63
+ const compression = header & 3, filter = header >> 2 & 3;
64
+ const total = width * height;
65
+ let alpha;
66
+ if (compression === 0) {
67
+ alpha = new Uint8Array(total);
68
+ alpha.set(chunk.subarray(1, 1 + total));
69
+ }
70
+ else if (compression === 1) {
71
+ const argb = vp8l.decodeStream(new vp8l.BitReader(chunk, 1), width, height);
72
+ alpha = new Uint8Array(total);
73
+ for (let i = 0; i < total; i++)
74
+ alpha[i] = argb[i] >> 8 & 255;
75
+ }
76
+ else
77
+ throw new Error('WebP: invalid alpha compression');
78
+ if (filter)
79
+ unfilterAlpha(alpha, width, height, filter);
80
+ for (let i = 0; i < total; i++)
81
+ rgba[i * 4 + 3] = alpha[i];
82
+ }
83
+ function unfilterAlpha(a, width, height, filter) {
84
+ for (let y = 0; y < height; y++) {
85
+ const row = y * width;
86
+ for (let x = 0; x < width; x++) {
87
+ const i = row + x;
88
+ let prediction;
89
+ if (x === 0 && y === 0)
90
+ prediction = 0;
91
+ else if (y === 0)
92
+ prediction = a[i - 1];
93
+ else if (x === 0)
94
+ prediction = a[i - width];
95
+ else if (filter === 1)
96
+ prediction = a[i - 1];
97
+ else if (filter === 2)
98
+ prediction = a[i - width];
99
+ else {
100
+ const g = a[i - 1] + a[i - width] - a[i - width - 1];
101
+ prediction = g < 0 ? 0 : g > 255 ? 255 : g;
102
+ }
103
+ a[i] = a[i] + prediction & 255;
104
+ }
105
+ }
106
+ }
107
+ export function isAnimated(data) {
108
+ const list = container(data);
109
+ const header = list[0];
110
+ return header && header.type === VP8X && header.data.length >= 10 && (header.data[0] & 2) !== 0 && list.some(c => c.type === ANMF);
111
+ }
112
+ export function decode(data) {
113
+ const list = container(data);
114
+ if (!list.length)
115
+ throw new Error('WebP: no image data');
116
+ let orientation = 1;
117
+ const exif = list.find(c => c.type === EXIF);
118
+ if (exif)
119
+ orientation = exifOrientation(exif.data);
120
+ if (list.some(c => c.type === ANMF)) {
121
+ const animation = decodeAnimation(data, true);
122
+ const frame = composite(animation)[0];
123
+ return { width: animation.width, height: animation.height, pixels: frame.pixels, orientation };
124
+ }
125
+ const image = decodeBitstream(list);
126
+ if (list[0].type === VP8X && list[0].data.length >= 10) {
127
+ const width = u24(list[0].data, 4) + 1, height = u24(list[0].data, 7) + 1;
128
+ if (width !== image.width || height !== image.height)
129
+ throw new Error('WebP: canvas and image sizes differ');
130
+ }
131
+ return { ...image, orientation };
132
+ }
133
+ export function decodeAnimation(data, onlyFirstFrame = false) {
134
+ const list = container(data);
135
+ const header = list[0];
136
+ if (!header || header.type !== VP8X || !list.some(c => c.type === ANMF)) {
137
+ const { width, height, pixels } = decodeBitstream(list);
138
+ return { width, height, loopCount: 0, background: 0, frames: [{ x: 0, y: 0, width, height, duration: 0, blend: false, dispose: false, pixels }] };
139
+ }
140
+ if (header.data.length < 10)
141
+ throw new Error('WebP: invalid VP8X chunk');
142
+ const width = u24(header.data, 4) + 1, height = u24(header.data, 7) + 1;
143
+ let loopCount = 0, background = 0;
144
+ const frames = [];
145
+ for (const chunk of list) {
146
+ if (chunk.type === ANIM && chunk.data.length >= 6) {
147
+ const d = chunk.data;
148
+ background = (d[3] << 24 | d[2] << 16 | d[1] << 8 | d[0]) >>> 0;
149
+ loopCount = d[4] | d[5] << 8;
150
+ }
151
+ else if (chunk.type === ANMF) {
152
+ const d = chunk.data;
153
+ if (d.length < 16)
154
+ throw new Error('WebP: invalid ANMF chunk');
155
+ const frame = decodeBitstream(chunks(d, 16, d.length));
156
+ frames.push({
157
+ x: u24(d, 0) * 2,
158
+ y: u24(d, 3) * 2,
159
+ width: frame.width,
160
+ height: frame.height,
161
+ duration: u24(d, 12),
162
+ blend: (d[15] & 2) === 0,
163
+ dispose: (d[15] & 1) === 1,
164
+ pixels: frame.pixels
165
+ });
166
+ if (onlyFirstFrame)
167
+ break;
168
+ }
169
+ }
170
+ if (!frames.length)
171
+ throw new Error('WebP: animation without frames');
172
+ return { width, height, loopCount, background, frames };
173
+ }
174
+ export function composite({ width, height, frames }) {
175
+ const canvas = new Uint8Array(width * height * 4);
176
+ const output = [];
177
+ for (const frame of frames) {
178
+ const x0 = Math.min(frame.x, width), x1 = Math.min(frame.x + frame.width, width);
179
+ const y0 = Math.min(frame.y, height), y1 = Math.min(frame.y + frame.height, height);
180
+ for (let y = y0; y < y1; y++)
181
+ for (let x = x0; x < x1; x++) {
182
+ const s = ((y - frame.y) * frame.width + x - frame.x) * 4, d = (y * width + x) * 4;
183
+ const sa = frame.pixels[s + 3];
184
+ if (!frame.blend || sa === 255 || canvas[d + 3] === 0) {
185
+ canvas[d] = frame.pixels[s];
186
+ canvas[d + 1] = frame.pixels[s + 1];
187
+ canvas[d + 2] = frame.pixels[s + 2];
188
+ canvas[d + 3] = sa;
189
+ }
190
+ else if (sa !== 0)
191
+ blendOver(frame.pixels, s, canvas, d);
192
+ }
193
+ output.push({ pixels: canvas.slice(), duration: frame.duration });
194
+ if (frame.dispose)
195
+ for (let y = y0; y < y1; y++)
196
+ canvas.fill(0, (y * width + x0) * 4, (y * width + x1) * 4);
197
+ }
198
+ return output;
199
+ }
200
+ function blendOver(src, s, dst, d) {
201
+ const sa = src[s + 3], da = dst[d + 3];
202
+ const dstFactor = da * (256 - sa) >> 8;
203
+ const alpha = sa + dstFactor;
204
+ const scale = alpha ? Math.floor((1 << 24) / alpha) : 0;
205
+ for (let c = 0; c < 3; c++) {
206
+ const blended = src[s + c] * sa + dst[d + c] * dstFactor;
207
+ dst[d + c] = blended * scale >>> 24;
208
+ }
209
+ dst[d + 3] = alpha;
210
+ }
211
+ function riffChunk(type, body) {
212
+ const out = new Uint8Array(8 + body.length + (body.length & 1));
213
+ const view = new DataView(out.buffer);
214
+ view.setUint32(0, fourCC(type), true);
215
+ view.setUint32(4, body.length, true);
216
+ out.set(body, 8);
217
+ return out;
218
+ }
219
+ function riff(chunks) {
220
+ let size = 4;
221
+ for (const c of chunks)
222
+ size += c.length;
223
+ const out = new Uint8Array(8 + size);
224
+ const view = new DataView(out.buffer);
225
+ view.setUint32(0, RIFF, true);
226
+ view.setUint32(4, size, true);
227
+ view.setUint32(8, WEBP, true);
228
+ let offset = 12;
229
+ for (const c of chunks) {
230
+ out.set(c, offset);
231
+ offset += c.length;
232
+ }
233
+ return out;
234
+ }
235
+ function toARGB(rgba, count) {
236
+ const argb = new Uint32Array(count);
237
+ for (let i = 0, o = 0; i < count; i++, o += 4)
238
+ argb[i] = (rgba[o + 3] << 24 | rgba[o] << 16 | rgba[o + 1] << 8 | rgba[o + 2]) >>> 0;
239
+ return argb;
240
+ }
241
+ const set24 = (d, o, v) => { d[o] = v & 255; d[o + 1] = v >> 8 & 255; d[o + 2] = v >> 16 & 255; };
242
+ const hasAlpha = (rgba) => {
243
+ for (let i = 3; i < rgba.length; i += 4)
244
+ if (rgba[i] !== 255)
245
+ return true;
246
+ return false;
247
+ };
248
+ export function encode(rgba, width, height, exif) {
249
+ const bitstream = riffChunk('VP8L', vp8l.encode(toARGB(rgba, width * height), width, height));
250
+ if (!exif)
251
+ return riff([bitstream]);
252
+ const vp8x = new Uint8Array(10);
253
+ vp8x[0] = 8 | (hasAlpha(rgba.subarray(0, width * height * 4)) ? 16 : 0);
254
+ set24(vp8x, 4, width - 1);
255
+ set24(vp8x, 7, height - 1);
256
+ return riff([riffChunk('VP8X', vp8x), bitstream, riffChunk('EXIF', exif)]);
257
+ }
258
+ export function encodeAnimation(frames, width, height, plays, exif) {
259
+ if (!frames.length)
260
+ throw new Error('WebP: an animation needs at least one frame');
261
+ if (width > 1 << 24 || height > 1 << 24)
262
+ throw new RangeError('WebP: canvas too large');
263
+ const first = frames[0];
264
+ const alpha = first.x > 0 || first.y > 0 || first.width < width || first.height < height
265
+ || frames.some(f => f.dispose || hasAlpha(f.pixels));
266
+ const vp8x = new Uint8Array(10);
267
+ vp8x[0] = 2 | (alpha ? 16 : 0) | (exif ? 8 : 0);
268
+ set24(vp8x, 4, width - 1);
269
+ set24(vp8x, 7, height - 1);
270
+ const anim = new Uint8Array(6);
271
+ anim[4] = plays & 255;
272
+ anim[5] = plays >> 8 & 255;
273
+ const chunks = [riffChunk('VP8X', vp8x), riffChunk('ANIM', anim)];
274
+ for (const f of frames) {
275
+ if (f.x & 1 || f.y & 1)
276
+ throw new RangeError('WebP: frame offsets have to be even');
277
+ const bitstream = riffChunk('VP8L', vp8l.encode(toARGB(f.pixels, f.width * f.height), f.width, f.height));
278
+ const header = new Uint8Array(16);
279
+ set24(header, 0, f.x >> 1);
280
+ set24(header, 3, f.y >> 1);
281
+ set24(header, 6, f.width - 1);
282
+ set24(header, 9, f.height - 1);
283
+ set24(header, 12, Math.min(0xffffff, Math.max(0, Math.round(f.duration))));
284
+ header[15] = (f.blend === false ? 2 : 0) | (f.dispose ? 1 : 0);
285
+ const body = new Uint8Array(16 + bitstream.length);
286
+ body.set(header);
287
+ body.set(bitstream, 16);
288
+ chunks.push(riffChunk('ANMF', body));
289
+ }
290
+ if (exif)
291
+ chunks.push(riffChunk('EXIF', exif));
292
+ return riff(chunks);
293
+ }
package/dist/frame.d.ts CHANGED
@@ -1,29 +1,45 @@
1
- import { Image, ResizeMode } from './image.js';
2
- /** Names of the GIF frame disposal modes, see {@link Frame.disposalMode} */
1
+ import { Image, ResizeMode, ResizeOptions, Color, TrimOptions } from './image.js';
2
+ /** Names of the frame disposal modes, see {@link Frame.disposalMode} */
3
3
  export type FrameDisposalModeName = 'any' | 'keep' | 'previous' | 'background';
4
- /** Ids of the GIF frame disposal modes as stored in the file (0 any, 1 keep, 2 previous, 3 background) */
4
+ /** Ids of the frame disposal modes, as GIFs store them (0 any, 1 keep, 2 previous, 3 background) */
5
5
  export type FrameDisposalModeId = 0 | 1 | 2 | 3;
6
+ /** Options of {@link Frame} and {@link Frame.from} */
7
+ export interface FrameOptions {
8
+ /** The frame's duration in milliseconds. Default: `100` */
9
+ duration?: number;
10
+ /** The frame's offset on the x-axis. Default: `0` */
11
+ xOffset?: number;
12
+ /** The frame's offset on the y-axis. Default: `0` */
13
+ yOffset?: number;
14
+ /** The frame's disposal mode, see {@link Frame.disposalMode}. Default: `'keep'` */
15
+ disposalMode?: FrameDisposalModeName | FrameDisposalModeId;
16
+ }
6
17
  /**
7
- * Represents a frame in a GIF
18
+ * Represents a frame of an {@link Animation}
8
19
  */
9
20
  export declare class Frame extends Image {
10
21
  /**
11
- * Milliseconds. GIFs store hundredths of a second, so this is rounded to 10 ms when encoding;
22
+ * Milliseconds. GIFs store hundredths of a second, so this is rounded to 10 ms when encoding a GIF;
12
23
  * note that browsers play frames shorter than 20 ms with 100 ms
13
24
  */
14
25
  duration: number;
15
- /** The frame's horizontal offset in the GIF */
26
+ /** The frame's horizontal offset in the animation */
16
27
  xOffset: number;
17
- /** The frame's vertical offset in the GIF */
28
+ /** The frame's vertical offset in the animation */
18
29
  yOffset: number;
19
- /** GIF frame disposal mode ANY: the decoder decides (in practice like KEEP). For use with {@link Frame} */
30
+ /** Frame disposal mode ANY: the decoder decides (in practice like KEEP). For use with {@link Frame} */
20
31
  static get DISPOSAL_ANY(): 'any';
21
- /** GIF frame disposal mode KEEP. For use with {@link Frame} */
32
+ /** Frame disposal mode KEEP. For use with {@link Frame} */
22
33
  static get DISPOSAL_KEEP(): 'keep';
23
- /** GIF frame disposal mode PREVIOUS. For use with {@link Frame} */
34
+ /** Frame disposal mode PREVIOUS. For use with {@link Frame} */
24
35
  static get DISPOSAL_PREVIOUS(): 'previous';
25
- /** GIF frame disposal mode BACKGROUND. For use with {@link Frame} */
36
+ /** Frame disposal mode BACKGROUND. For use with {@link Frame} */
26
37
  static get DISPOSAL_BACKGROUND(): 'background';
38
+ /**
39
+ * Creates a new, blank frame
40
+ * @param options See {@link FrameOptions}
41
+ */
42
+ constructor(width: number, height: number, options?: FrameOptions);
27
43
  /**
28
44
  * Creates a new, blank frame
29
45
  * @param duration The frame's duration (in ms)
@@ -37,6 +53,12 @@ export declare class Frame extends Image {
37
53
  /** Sets the disposal mode by name or id (0-3) */
38
54
  set disposalMode(disposalMode: FrameDisposalModeName | FrameDisposalModeId);
39
55
  toString(): string;
56
+ /**
57
+ * Converts an Image instance to a Frame, cloning it in the process
58
+ * @param image The image to create the frame from
59
+ * @param options See {@link FrameOptions}
60
+ */
61
+ static from(image: Image, options?: FrameOptions): Frame;
40
62
  /**
41
63
  * Converts an Image instance to a Frame, cloning it in the process
42
64
  * @param image The image to create the frame from
@@ -50,8 +72,14 @@ export declare class Frame extends Image {
50
72
  * Clones the frame, including its duration, offsets and disposal mode
51
73
  */
52
74
  clone(): Frame;
75
+ /**
76
+ * Removes borders of one color, moving the frame's offsets so the remaining pixels stay in place
77
+ * @param color The border color, by default the color of the top left pixel
78
+ * @param options See {@link TrimOptions}
79
+ */
80
+ trim(color?: Color, options?: TrimOptions): this;
53
81
  /**
54
82
  * Resizes the frame, scaling its offsets along with it
55
83
  */
56
- resize(width: number, height: number, mode?: ResizeMode): this;
84
+ resize(width: number, height: number, modeOrOptions?: ResizeMode | ResizeOptions): this;
57
85
  }
package/dist/frame.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Image } from "./image.js";
2
+ import { isOptions } from "./util/options.js";
2
3
  const DISPOSAL_MODES = ['any', 'keep', 'previous', 'background'];
3
4
  export class Frame extends Image {
4
5
  duration;
@@ -23,14 +24,15 @@ export class Frame extends Image {
23
24
  throw new RangeError('Invalid disposal mode');
24
25
  return id;
25
26
  }
26
- constructor(width, height, duration = 100, xOffset = 0, yOffset = 0, disposalMode = Frame.DISPOSAL_KEEP) {
27
+ constructor(width, height, durationOrOptions, xOffset, yOffset, disposalMode) {
28
+ const { duration = 100, xOffset: x = 0, yOffset: y = 0, disposalMode: disposal = Frame.DISPOSAL_KEEP } = isOptions(durationOrOptions) ? durationOrOptions : { duration: durationOrOptions, xOffset, yOffset, disposalMode };
27
29
  if (isNaN(duration) || duration < 0)
28
30
  throw new RangeError('Invalid frame duration');
29
31
  super(width, height);
30
32
  this.duration = duration;
31
- this.xOffset = xOffset;
32
- this.yOffset = yOffset;
33
- this.disposalMode = disposalMode;
33
+ this.xOffset = x;
34
+ this.yOffset = y;
35
+ this.disposalMode = disposal;
34
36
  }
35
37
  get disposalMode() {
36
38
  return DISPOSAL_MODES[this.__disposalMode__];
@@ -44,20 +46,29 @@ export class Frame extends Image {
44
46
  toString() {
45
47
  return `Frame<${this.width}x${this.height}x${this.duration}ms>`;
46
48
  }
47
- static from(image, duration, xOffset, yOffset, disposalMode = Frame.DISPOSAL_KEEP) {
49
+ static from(image, durationOrOptions, xOffset, yOffset, disposalMode) {
48
50
  if (!(image instanceof Image))
49
51
  throw new TypeError('Invalid image passed');
50
- const frame = new Frame(image.width, image.height, duration, xOffset, yOffset, disposalMode);
52
+ const options = isOptions(durationOrOptions) ? durationOrOptions : { duration: durationOrOptions, xOffset, yOffset, disposalMode };
53
+ const frame = new Frame(image.width, image.height, options);
51
54
  frame.bitmap.set(image.bitmap);
52
55
  return frame;
53
56
  }
54
57
  clone() {
55
- return Frame.from(this, this.duration, this.xOffset, this.yOffset, this.disposalMode);
58
+ return Frame.from(this, { duration: this.duration, xOffset: this.xOffset, yOffset: this.yOffset, disposalMode: this.disposalMode });
56
59
  }
57
- resize(width, height, mode = Image.RESIZE_BILINEAR) {
60
+ trim(color, options = {}) {
61
+ const bounds = this.__trim_bounds__(color, options);
62
+ if (!bounds)
63
+ return this;
64
+ this.xOffset += bounds[0];
65
+ this.yOffset += bounds[1];
66
+ return this.__apply__(this.__crop__(...bounds));
67
+ }
68
+ resize(width, height, modeOrOptions) {
58
69
  const originalWidth = this.width;
59
70
  const originalHeight = this.height;
60
- super.resize(width, height, mode);
71
+ super.resize(width, height, modeOrOptions);
61
72
  this.xOffset = Math.round(this.xOffset * this.width / originalWidth);
62
73
  this.yOffset = Math.round(this.yOffset * this.height / originalHeight);
63
74
  return this;
package/dist/gif.d.ts CHANGED
@@ -1,44 +1,34 @@
1
1
  import { BinaryData } from './util/mem.js';
2
- import { ResizeMode } from './image.js';
3
2
  import { Frame } from './frame.js';
3
+ import { Animation, AnimationDecodeOptions, AnimationOptions, GIFEncodeOptions } from './animation.js';
4
4
  /**
5
- * Represents a GIF image as an array of frames.
5
+ * Represents a GIF image as an array of frames, an {@link Animation} whose `encode()` and `decode()` work with GIFs.
6
6
  * Array methods like map and filter return plain arrays.
7
+ * @deprecated Use {@link Animation}, which works the same and also handles APNG: `new Animation(frames, loopCount)`,
8
+ * {@link Animation.decode} and {@link Animation.encodeGIF}. `GIF` will be removed in 4.0
7
9
  */
8
- export declare class GIF extends Array<Frame> {
9
- /** How many times to repeat the GIF (-1 = forever, 0 = play once) */
10
- loopCount: number;
10
+ export declare class GIF extends Animation {
11
11
  /**
12
12
  * Creates a new GIF image
13
13
  * @param frames The frames to create the GIF from
14
- * @param loopCount How many times to repeat the GIF (-1 = forever, 0 = play once)
14
+ * @param loopCountOrOptions How many times to repeat the GIF (-1 = forever, 0 = play once), or see
15
+ * {@link AnimationOptions}
15
16
  */
16
- constructor(frames: Frame[], loopCount?: number);
17
- /** The GIF's width */
18
- get width(): number;
19
- /** The GIF's height */
20
- get height(): number;
21
- /** The GIF's duration (in ms) */
22
- get duration(): number;
17
+ constructor(frames: Frame[], loopCountOrOptions?: number | AnimationOptions);
23
18
  toString(): string;
24
- /**
25
- * Returns a new GIF containing a range of this GIF's frames (same semantics as Array.prototype.slice)
26
- */
27
- slice(start?: number, end?: number): GIF;
28
19
  /**
29
20
  * Encodes the GIF
30
- * @param quality 1-100, scales the palette size from 8 colors (1) to 256 colors (90 and above)
21
+ * @param qualityOrOptions 1-100, scales the palette size from 8 colors (1) to 256 colors (90 and above), or
22
+ * see {@link GIFEncodeOptions}
23
+ * @deprecated Use {@link Animation.encodeGIF}
31
24
  */
32
- encode(quality?: number): Promise<Uint8Array>;
25
+ encode(qualityOrOptions?: number | GIFEncodeOptions): Promise<Uint8Array>;
33
26
  /**
34
27
  * Decodes a GIF into fully composited frames
35
28
  * @param input The binary data to decode
36
- * @param onlyExtractFirstFrame Whether to end GIF decoding after the first frame
37
- */
38
- static decode(input: BinaryData, onlyExtractFirstFrame?: boolean): Promise<GIF>;
39
- /**
40
- * Resizes the GIF to the given dimensions, scaling every frame and its offsets proportionally.
41
- * Use {@link Image.RESIZE_AUTO} as either width or height to automatically preserve the aspect ratio.
29
+ * @param onlyFirstFrameOrOptions Whether to end GIF decoding after the first frame, or see
30
+ * {@link AnimationDecodeOptions}
31
+ * @deprecated Use {@link Animation.decode}, which decodes APNGs too
42
32
  */
43
- resize(width: number, height: number, mode?: ResizeMode): this;
33
+ static decode(input: BinaryData, onlyFirstFrameOrOptions?: boolean | AnimationDecodeOptions): Promise<GIF>;
44
34
  }
package/dist/gif.js CHANGED
@@ -1,93 +1,22 @@
1
- import * as codec from "./codecs/gif.js";
2
1
  import { view } from "./util/mem.js";
3
- import { Image } from "./image.js";
4
- import { Frame } from "./frame.js";
5
2
  import { ImageType } from "./image-type.js";
6
- export class GIF extends Array {
7
- loopCount;
8
- constructor(frames, loopCount = -1) {
9
- super();
10
- for (const frame of frames)
11
- this.push(frame);
12
- for (let i = 0; i < this.length; i++)
13
- if (!(this[i] instanceof Frame))
14
- throw new TypeError(`Frame ${i} is not an instance of Frame`);
15
- if (loopCount < -1 || isNaN(loopCount))
16
- throw new RangeError('Invalid loop count');
17
- this.loopCount = loopCount;
18
- }
19
- static get [Symbol.species]() {
20
- return Array;
21
- }
22
- get width() {
23
- let max = 0;
24
- for (const frame of this)
25
- max = Math.max(max, frame.width + frame.xOffset);
26
- return max;
27
- }
28
- get height() {
29
- let max = 0;
30
- for (const frame of this)
31
- max = Math.max(max, frame.height + frame.yOffset);
32
- return max;
33
- }
34
- get duration() {
35
- let duration = 0;
36
- for (const frame of this)
37
- duration += frame.duration;
38
- return duration;
3
+ import { Animation } from "./animation.js";
4
+ import { isOptions } from "./util/options.js";
5
+ export class GIF extends Animation {
6
+ constructor(frames, loopCountOrOptions) {
7
+ super(frames, loopCountOrOptions);
39
8
  }
40
9
  toString() {
41
10
  return `GIF<${this.width}x${this.height}x${this.duration}ms>`;
42
11
  }
43
- slice(start, end) {
44
- return new GIF(Array.prototype.slice.call(this, start, end), this.loopCount);
45
- }
46
- async encode(quality = 95) {
47
- const frames = [];
48
- for (const frame of this) {
49
- if (!(frame instanceof Frame))
50
- throw new Error('GIF contains invalid frames');
51
- frames.push({
52
- x: frame.xOffset,
53
- y: frame.yOffset,
54
- width: frame.width,
55
- height: frame.height,
56
- delay: Math.round(frame.duration / 10),
57
- dispose: frame.__disposal_id__,
58
- pixels: new Uint8Array(frame.bitmap.buffer, frame.bitmap.byteOffset, frame.bitmap.byteLength)
59
- });
60
- }
61
- return codec.encode({ width: this.width, height: this.height, loopCount: this.loopCount, frames }, quality);
12
+ async encode(qualityOrOptions) {
13
+ return this.encodeGIF(qualityOrOptions);
62
14
  }
63
- static async decode(input, onlyExtractFirstFrame = false) {
15
+ static async decode(input, onlyFirstFrameOrOptions = false) {
16
+ const onlyExtractFirstFrame = isOptions(onlyFirstFrameOrOptions) ? onlyFirstFrameOrOptions.onlyFirstFrame ?? false : onlyFirstFrameOrOptions;
64
17
  const data = view(input);
65
18
  if (!ImageType.isGIF(new DataView(data.buffer, data.byteOffset, data.byteLength)))
66
19
  throw new Error('Unsupported image type');
67
- const decoded = codec.decode(data, onlyExtractFirstFrame);
68
- const frames = codec.composite(decoded).map(({ pixels, delay }) => {
69
- const frame = new Frame(decoded.width, decoded.height, 10 * delay, 0, 0, Frame.DISPOSAL_BACKGROUND);
70
- frame.bitmap.set(pixels);
71
- return frame;
72
- });
73
- return new GIF(frames, decoded.loopCount);
74
- }
75
- resize(width, height, mode = Image.RESIZE_BILINEAR) {
76
- const originalWidth = this.width, originalHeight = this.height;
77
- if (width === Image.RESIZE_AUTO && height === Image.RESIZE_AUTO)
78
- throw new Error('RESIZE_AUTO can only be used for either width or height, not for both');
79
- if (width === Image.RESIZE_AUTO)
80
- width = originalWidth / originalHeight * height;
81
- if (height === Image.RESIZE_AUTO)
82
- height = originalHeight / originalWidth * width;
83
- const sx = width / originalWidth, sy = height / originalHeight;
84
- for (const frame of this) {
85
- const left = Math.round(frame.xOffset * sx), top = Math.round(frame.yOffset * sy);
86
- const right = Math.round((frame.xOffset + frame.width) * sx), bottom = Math.round((frame.yOffset + frame.height) * sy);
87
- Image.prototype.resize.call(frame, Math.max(1, right - left), Math.max(1, bottom - top), mode);
88
- frame.xOffset = left;
89
- frame.yOffset = top;
90
- }
91
- return this;
20
+ return Animation.__fromGIF__(data, onlyExtractFirstFrame, GIF);
92
21
  }
93
22
  }
@@ -1,8 +1,9 @@
1
1
  import { BinaryData } from './util/mem.js';
2
2
  /** Image formats recognized by {@link ImageType.getType} */
3
- export type ImageTypeName = 'png' | 'jpeg' | 'tiff' | 'gif';
3
+ export type ImageTypeName = 'png' | 'jpeg' | 'tiff' | 'gif' | 'webp' | 'tga';
4
4
  /** Detects image formats by their magic numbers */
5
5
  export declare class ImageType {
6
+ private constructor();
6
7
  /**
7
8
  * Gets an image's type from its magic number
8
9
  * @param data The image binary to get the type of
@@ -17,4 +18,8 @@ export declare class ImageType {
17
18
  static isTIFF(data: BinaryData): boolean;
18
19
  /** Whether the data starts like a GIF */
19
20
  static isGIF(data: BinaryData): boolean;
21
+ /** Whether the data starts like a WebP */
22
+ static isWebP(data: BinaryData): boolean;
23
+ /** Whether the data looks like a TGA (which has no magic number: its header has to be consistent) */
24
+ static isTGA(data: BinaryData): boolean;
20
25
  }
@@ -1,4 +1,5 @@
1
1
  import { view as bytes } from "./util/mem.js";
2
+ import { isTGA } from "./codecs/tga.js";
2
3
  const MAGIC_NUMBERS = {
3
4
  PNG: 0x89504e47,
4
5
  JPEG: 0xffd8ff,
@@ -6,7 +7,9 @@ const MAGIC_NUMBERS = {
6
7
  TIFF_BE: 0x4d4d002a,
7
8
  BIGTIFF_LE: 0x49492b00,
8
9
  BIGTIFF_BE: 0x4d4d002b,
9
- GIF: 0x474946
10
+ GIF: 0x474946,
11
+ RIFF: 0x52494646,
12
+ WEBP: 0x57454250
10
13
  };
11
14
  function dataView(data) {
12
15
  if (data instanceof DataView)
@@ -15,6 +18,7 @@ function dataView(data) {
15
18
  return new DataView(u8.buffer, u8.byteOffset, u8.byteLength);
16
19
  }
17
20
  export class ImageType {
21
+ constructor() { }
18
22
  static getType(data) {
19
23
  const view = dataView(data);
20
24
  if (this.isPNG(view))
@@ -25,6 +29,10 @@ export class ImageType {
25
29
  return 'tiff';
26
30
  if (this.isGIF(view))
27
31
  return 'gif';
32
+ if (this.isWebP(view))
33
+ return 'webp';
34
+ if (this.isTGA(view))
35
+ return 'tga';
28
36
  return null;
29
37
  }
30
38
  static isPNG(data) {
@@ -46,4 +54,12 @@ export class ImageType {
46
54
  const view = dataView(data);
47
55
  return view.byteLength >= 4 && (view.getUint32(0, false) >>> 8) === MAGIC_NUMBERS.GIF;
48
56
  }
57
+ static isWebP(data) {
58
+ const view = dataView(data);
59
+ return view.byteLength >= 12 && view.getUint32(0, false) === MAGIC_NUMBERS.RIFF && view.getUint32(8, false) === MAGIC_NUMBERS.WEBP;
60
+ }
61
+ static isTGA(data) {
62
+ const view = dataView(data);
63
+ return isTGA(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
64
+ }
49
65
  }