imagescript 3.1.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 (51) hide show
  1. package/README.md +12 -4
  2. package/dist/animation.d.ts +92 -7
  3. package/dist/animation.js +49 -19
  4. package/dist/codecs/exif.js +62 -0
  5. package/dist/codecs/gif.js +32 -19
  6. package/dist/codecs/jpeg.js +13 -1
  7. package/dist/codecs/png.js +85 -21
  8. package/dist/codecs/quantize.js +2 -2
  9. package/dist/codecs/tga.js +47 -4
  10. package/dist/codecs/vp8l.js +27 -30
  11. package/dist/codecs/webp.js +23 -10
  12. package/dist/frame.d.ts +30 -2
  13. package/dist/frame.js +20 -9
  14. package/dist/gif.d.ts +10 -7
  15. package/dist/gif.js +7 -5
  16. package/dist/image-type.d.ts +1 -0
  17. package/dist/image-type.js +1 -0
  18. package/dist/image.d.ts +231 -31
  19. package/dist/image.js +111 -50
  20. package/dist/index.d.ts +14 -4
  21. package/dist/index.js +6 -4
  22. package/dist/metadata.d.ts +35 -0
  23. package/dist/metadata.js +43 -0
  24. package/dist/render/svg.js +39 -21
  25. package/dist/util/optimize.js +264 -0
  26. package/dist/util/options.js +3 -0
  27. package/dist/util/resample.js +44 -26
  28. package/dist/util/utf8.js +4 -51
  29. package/dist/version.js +1 -1
  30. package/package.json +9 -2
  31. package/dist/codecs/crc.d.ts +0 -9
  32. package/dist/codecs/exif.d.ts +0 -11
  33. package/dist/codecs/gif.d.ts +0 -44
  34. package/dist/codecs/jpeg.d.ts +0 -35
  35. package/dist/codecs/png.d.ts +0 -97
  36. package/dist/codecs/quantize.d.ts +0 -25
  37. package/dist/codecs/tga.d.ts +0 -16
  38. package/dist/codecs/tiff.d.ts +0 -6
  39. package/dist/codecs/vp8-tables.d.ts +0 -10
  40. package/dist/codecs/vp8.d.ts +0 -8
  41. package/dist/codecs/vp8l.d.ts +0 -62
  42. package/dist/codecs/webp.d.ts +0 -67
  43. package/dist/codecs/zlib.d.ts +0 -22
  44. package/dist/render/font.d.ts +0 -92
  45. package/dist/render/hinting.d.ts +0 -110
  46. package/dist/render/svg.d.ts +0 -11
  47. package/dist/render/text.d.ts +0 -63
  48. package/dist/util/blur.d.ts +0 -13
  49. package/dist/util/resample.d.ts +0 -12
  50. package/dist/util/utf8.d.ts +0 -5
  51. package/dist/version.d.ts +0 -1
@@ -41,15 +41,24 @@ export async function encode(data, { text, width, height, channels, depth = 8, l
41
41
  chunks.push(chunk(PLTE, palette));
42
42
  if (transparency)
43
43
  chunks.push(chunk(tRNS, transparency));
44
+ chunks.push(...textChunks(text));
44
45
  chunks.push(chunk(IDAT, await compressAsync(filtered, level)));
46
+ chunks.push(chunk(IEND, new Uint8Array(0)));
47
+ return concat([HEAD, ...chunks]);
48
+ }
49
+ function textChunks(text) {
50
+ const chunks = [];
51
+ const latin1 = (s) => Array.from(s, c => c.charCodeAt(0));
45
52
  for (const key in text) {
46
53
  const value = text[key];
47
54
  if (!value)
48
55
  continue;
49
- chunks.push(chunk(0x74455874, concat([utf8(key), [0], utf8(value)])));
56
+ if (/^[\x00-\xff]*$/.test(value))
57
+ chunks.push(chunk(tEXt, concat([latin1(key), [0], latin1(value)])));
58
+ else
59
+ chunks.push(chunk(iTXt, concat([latin1(key), [0, 0, 0, 0, 0], utf8(value)])));
50
60
  }
51
- chunks.push(chunk(IEND, new Uint8Array(0)));
52
- return concat([HEAD, ...chunks]);
61
+ return chunks;
53
62
  }
54
63
  function filter(data, rowBytes, height, bpp) {
55
64
  const out = new Uint8Array(height * (rowBytes + 1));
@@ -66,7 +75,8 @@ function filter(data, rowBytes, height, bpp) {
66
75
  for (let i = bpp; i < rowBytes; i++) {
67
76
  const a = data[row + i - bpp], b = data[row + i - rowBytes], c = data[row + i - rowBytes - bpp];
68
77
  const pa = Math.abs(b - c), pb = Math.abs(a - c), pc = Math.abs(a + b - 2 * c);
69
- out[o + i] = data[row + i] - (pa <= pb && pa <= pc ? a : pb <= pc ? b : c);
78
+ const notA = (pb - pa | pc - pa) >> 31, takeC = (pc - pb) >> 31;
79
+ out[o + i] = data[row + i] - (a ^ (a ^ b ^ (b ^ c) & takeC) & notA);
70
80
  }
71
81
  }
72
82
  return out;
@@ -80,10 +90,37 @@ function chunk(type, body) {
80
90
  view.setUint32(body.length + 8, crc32(out.subarray(4, body.length + 8)));
81
91
  return out;
82
92
  }
93
+ export function alphaCost(images) {
94
+ let grey = true;
95
+ const colors = new Set();
96
+ scan: for (const rgba of images) {
97
+ const pixels = new Uint32Array(rgba.buffer, rgba.byteOffset, rgba.length >> 2);
98
+ let previous = -1;
99
+ for (let i = 0; i < pixels.length; i++) {
100
+ const value = pixels[i];
101
+ if (value === previous)
102
+ continue;
103
+ previous = value;
104
+ const o = i * 4;
105
+ if (rgba[o + 3] === 0)
106
+ continue;
107
+ if (rgba[o] !== rgba[o + 1] || rgba[o] !== rgba[o + 2])
108
+ grey = false;
109
+ if (colors.size <= 256)
110
+ colors.add(value);
111
+ else if (!grey)
112
+ break scan;
113
+ }
114
+ }
115
+ const palette = (entries) => (entries <= 2 ? 1 : entries <= 4 ? 2 : entries <= 16 ? 4 : 8) / 8;
116
+ const transparent = colors.size < 256 ? palette(colors.size + 1) : grey ? 2 : 4;
117
+ const opaque = colors.size <= 256 ? palette(colors.size) : grey ? 1 : 3;
118
+ return transparent / opaque;
119
+ }
83
120
  function pixelFormat(images) {
84
121
  let opaque = true, grey = true;
85
122
  let colors = new Map();
86
- for (const rgba of images) {
123
+ scan: for (const rgba of images) {
87
124
  const pixels = new Uint32Array(rgba.buffer, rgba.byteOffset, rgba.length >> 2);
88
125
  let previous = -1;
89
126
  for (let i = 0; i < pixels.length; i++) {
@@ -102,6 +139,8 @@ function pixelFormat(images) {
102
139
  else
103
140
  colors.set(value, colors.size);
104
141
  }
142
+ if (!colors && !opaque && !grey)
143
+ break scan;
105
144
  }
106
145
  }
107
146
  if (colors) {
@@ -144,18 +183,22 @@ function pixelFormat(images) {
144
183
  return rgba;
145
184
  const count = width * height;
146
185
  const data = new Uint8Array(count * channels);
147
- for (let i = 0, o = 0; i < count; i++) {
148
- const s = i * 4;
149
- if (grey)
150
- data[o++] = rgba[s];
151
- else {
152
- data[o++] = rgba[s];
153
- data[o++] = rgba[s + 1];
154
- data[o++] = rgba[s + 2];
186
+ if (channels === 3) {
187
+ for (let s = 0, o = 0; o < data.length; s += 4, o += 3) {
188
+ data[o] = rgba[s];
189
+ data[o + 1] = rgba[s + 1];
190
+ data[o + 2] = rgba[s + 2];
191
+ }
192
+ }
193
+ else if (channels === 2) {
194
+ for (let s = 0, o = 0; o < data.length; s += 4, o += 2) {
195
+ data[o] = rgba[s];
196
+ data[o + 1] = rgba[s + 3];
155
197
  }
156
- if (!opaque)
157
- data[o++] = rgba[s + 3];
158
198
  }
199
+ else
200
+ for (let s = 0, o = 0; o < data.length; s += 4, o++)
201
+ data[o] = rgba[s];
159
202
  return data;
160
203
  }
161
204
  };
@@ -179,7 +222,7 @@ async function compressRows(data, width, height, channels, depth, level, filterR
179
222
  }
180
223
  return compressAsync(filtered, level);
181
224
  }
182
- export async function encodeAnimation(frames, width, height, plays, level) {
225
+ export async function encodeAnimation(frames, width, height, plays, level, text) {
183
226
  if (!frames.length)
184
227
  throw new Error('PNG: an animation needs at least one frame');
185
228
  const first = frames[0];
@@ -212,6 +255,7 @@ export async function encodeAnimation(frames, width, height, plays, level) {
212
255
  chunks.push(chunk(PLTE, palette));
213
256
  if (transparency)
214
257
  chunks.push(chunk(tRNS, transparency));
258
+ chunks.push(...textChunks(text));
215
259
  let sequence = 0;
216
260
  for (let i = 0; i < frames.length; i++) {
217
261
  const f = frames[i];
@@ -232,7 +276,7 @@ export async function encodeAnimation(frames, width, height, plays, level) {
232
276
  v.setUint16(22, 100);
233
277
  }
234
278
  fctl[24] = f.dispose;
235
- fctl[25] = 1;
279
+ fctl[25] = f.blend === false ? 0 : 1;
236
280
  chunks.push(chunk(fcTL, fctl));
237
281
  const data = await compressRows(format.convert(images[i], f.width, f.height), f.width, f.height, channels, depth, level, filterRows);
238
282
  if (i === 0)
@@ -552,7 +596,8 @@ function unfilter(data, offset, height, rowBytes, bpp) {
552
596
  for (let i = row + bpp; i < row + rowBytes; i++) {
553
597
  const a = out[i - bpp], b = out[i - rowBytes], c = out[i - rowBytes - bpp];
554
598
  const pa = Math.abs(b - c), pb = Math.abs(a - c), pc = Math.abs(a + b - 2 * c);
555
- out[i] = data[src + i] + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c);
599
+ const notA = (pb - pa | pc - pa) >> 31, takeC = (pc - pb) >> 31;
600
+ out[i] = data[src + i] + (a ^ (a ^ b ^ (b ^ c) & takeC) & notA);
556
601
  }
557
602
  }
558
603
  }
@@ -614,6 +659,20 @@ function createConverter(colorType, depth, palette, transparency) {
614
659
  const key = transparency && transparency.length >= 6
615
660
  ? [transparency[0] << 8 | transparency[1], transparency[2] << 8 | transparency[3], transparency[4] << 8 | transparency[5]].map(v => v & maxValue)
616
661
  : null;
662
+ if (depth === 8) {
663
+ const [kr, kg, kb] = key || [-1, -1, -1];
664
+ return (pixels, rowBytes, w, h, out, place) => {
665
+ for (let y = 0; y < h; y++) {
666
+ for (let x = 0, s = y * rowBytes, o = place.row(y), step = place.step; x < w; x++, s += 3, o += step) {
667
+ const r = pixels[s], g = pixels[s + 1], b = pixels[s + 2];
668
+ out[o] = r;
669
+ out[o + 1] = g;
670
+ out[o + 2] = b;
671
+ out[o + 3] = r === kr && g === kg && b === kb ? 0 : 0xff;
672
+ }
673
+ }
674
+ };
675
+ }
617
676
  return (pixels, rowBytes, w, h, out, place) => {
618
677
  for (let y = 0; y < h; y++) {
619
678
  const row = pixels.subarray(y * rowBytes, (y + 1) * rowBytes);
@@ -645,9 +704,14 @@ function createConverter(colorType, depth, palette, transparency) {
645
704
  return (pixels, rowBytes, w, h, out, place) => {
646
705
  if (place.step === 4 && w * 4 === rowBytes && place.row(0) === 0 && place.row(1) === rowBytes)
647
706
  return out.set(pixels);
648
- for (let y = 0; y < h; y++)
649
- for (let x = 0, o = place.row(y); x < w; x++, o += place.step)
650
- out.set(pixels.subarray(y * rowBytes + x * 4, y * rowBytes + x * 4 + 4), o);
707
+ for (let y = 0; y < h; y++) {
708
+ for (let x = 0, s = y * rowBytes, o = place.row(y), step = place.step; x < w; x++, s += 4, o += step) {
709
+ out[o] = pixels[s];
710
+ out[o + 1] = pixels[s + 1];
711
+ out[o + 2] = pixels[s + 2];
712
+ out[o + 3] = pixels[s + 3];
713
+ }
714
+ }
651
715
  };
652
716
  }
653
717
  return (pixels, rowBytes, w, h, out, place) => {
@@ -9,9 +9,9 @@ export function dominantColor(pixels, include, clusters = 8) {
9
9
  best = i;
10
10
  return [Math.round(palette[best * 3]), Math.round(palette[best * 3 + 1]), Math.round(palette[best * 3 + 2])];
11
11
  }
12
- export function quantize(pixels, width, { maxColors = 256, quality = 95 } = {}) {
12
+ export function quantize(pixels, width, { maxColors = 256, quality = 95, transparent = false } = {}) {
13
13
  const length = pixels.length >> 2;
14
- let hasTransparency = false;
14
+ let hasTransparency = transparent;
15
15
  for (let i = 3; i < pixels.length; i += 4)
16
16
  if (pixels[i] < 128) {
17
17
  hasTransparency = true;
@@ -1,3 +1,4 @@
1
+ import { encode as utf8 } from "../util/utf8.js";
1
2
  const SIGNATURE = 'TRUEVISION-XFILE.\0';
2
3
  function readHeader(d) {
3
4
  if (d.length < 18)
@@ -147,7 +148,38 @@ export function decode(d) {
147
148
  }
148
149
  return { width, height, pixels: stored };
149
150
  }
150
- export function encode(rgba, width, height) {
151
+ function pieces(value, max) {
152
+ const bytes = utf8(value);
153
+ const out = [];
154
+ for (let start = 0; start < bytes.length;) {
155
+ let end = Math.min(bytes.length, start + max);
156
+ while (end < bytes.length && end > start && (bytes[end] & 0xc0) === 0x80)
157
+ end--;
158
+ out.push(bytes.subarray(start, end));
159
+ start = end;
160
+ }
161
+ return out;
162
+ }
163
+ function extensionArea({ author, comment, time, software }, alpha) {
164
+ const area = new Uint8Array(495);
165
+ const view = new DataView(area.buffer);
166
+ view.setUint16(0, 495, true);
167
+ if (author)
168
+ area.set(pieces(author, 40)[0], 2);
169
+ if (comment) {
170
+ const lines = comment.split(/\r?\n/).flatMap(line => line ? pieces(line, 80) : [new Uint8Array(0)]).slice(0, 4);
171
+ lines.forEach((line, i) => area.set(line, 43 + i * 81));
172
+ }
173
+ if (time)
174
+ [time.getUTCMonth() + 1, time.getUTCDate(), time.getUTCFullYear(), time.getUTCHours(), time.getUTCMinutes(), time.getUTCSeconds()]
175
+ .forEach((v, i) => view.setUint16(367 + i * 2, v, true));
176
+ if (software)
177
+ area.set(pieces(software, 40)[0], 426);
178
+ area[469] = 0x20;
179
+ area[494] = alpha ? 3 : 0;
180
+ return area;
181
+ }
182
+ export function encode(rgba, width, height, extension) {
151
183
  if (width < 1 || height < 1 || width > 65535 || height > 65535)
152
184
  throw new RangeError('TGA: dimensions must be 1-65535');
153
185
  let opaque = true;
@@ -161,7 +193,7 @@ export function encode(rgba, width, height) {
161
193
  if (rgba.byteOffset % 4)
162
194
  rgba = rgba.slice(0, count * 4);
163
195
  const pixels = new Uint32Array(rgba.buffer, rgba.byteOffset, count);
164
- const out = new Uint8Array(18 + count * bpp + Math.ceil(count / 128) + 26);
196
+ const out = new Uint8Array(18 + count * bpp + height * Math.ceil(width / 128) + 495 + 26);
165
197
  out[2] = 10;
166
198
  out[12] = width & 255;
167
199
  out[13] = width >> 8;
@@ -179,8 +211,9 @@ export function encode(rgba, width, height) {
179
211
  out[o++] = rgba[p + 3];
180
212
  };
181
213
  for (let i = 0; i < count;) {
214
+ const end = (Math.floor(i / width) + 1) * width;
182
215
  let run = 1;
183
- while (run < 128 && i + run < count && pixels[i + run] === pixels[i])
216
+ while (run < 128 && i + run < end && pixels[i + run] === pixels[i])
184
217
  run++;
185
218
  if (run >= 2) {
186
219
  out[o++] = 128 | run - 1;
@@ -189,13 +222,23 @@ export function encode(rgba, width, height) {
189
222
  continue;
190
223
  }
191
224
  let raw = 1;
192
- while (raw < 128 && i + raw < count && !(i + raw + 1 < count && pixels[i + raw] === pixels[i + raw + 1]))
225
+ while (raw < 128 && i + raw < end && !(i + raw + 1 < end && pixels[i + raw] === pixels[i + raw + 1]))
193
226
  raw++;
194
227
  out[o++] = raw - 1;
195
228
  for (let k = 0; k < raw; k++)
196
229
  put(i + k);
197
230
  i += raw;
198
231
  }
232
+ let extensionOffset = 0;
233
+ if (extension && (extension.author || extension.comment || extension.time || extension.software)) {
234
+ extensionOffset = o;
235
+ out.set(extensionArea(extension, bpp === 4), o);
236
+ o += 495;
237
+ }
238
+ out[o] = extensionOffset & 255;
239
+ out[o + 1] = extensionOffset >> 8 & 255;
240
+ out[o + 2] = extensionOffset >> 16 & 255;
241
+ out[o + 3] = extensionOffset >>> 24;
199
242
  o += 8;
200
243
  for (let i = 0; i < SIGNATURE.length; i++)
201
244
  out[o++] = SIGNATURE.charCodeAt(i);
@@ -293,30 +293,24 @@ function decodeImage(reader, width, height, isMain) {
293
293
  return pixels;
294
294
  }
295
295
  const average2 = (a, b) => (((a ^ b) & 0xfefefefe) >>> 1) + (a & b) >>> 0;
296
+ const clamp255 = (v) => v < 0 ? 0 : v > 255 ? 255 : v;
296
297
  function clampAddSubtractFull(a, b, c) {
297
- let out = 0;
298
- for (let shift = 0; shift < 32; shift += 8) {
299
- const v = (a >>> shift & 255) + (b >>> shift & 255) - (c >>> shift & 255);
300
- out |= (v < 0 ? 0 : v > 255 ? 255 : v) << shift;
301
- }
302
- return out >>> 0;
298
+ return (clamp255((a & 255) + (b & 255) - (c & 255))
299
+ | clamp255((a >>> 8 & 255) + (b >>> 8 & 255) - (c >>> 8 & 255)) << 8
300
+ | clamp255((a >>> 16 & 255) + (b >>> 16 & 255) - (c >>> 16 & 255)) << 16
301
+ | clamp255((a >>> 24) + (b >>> 24) - (c >>> 24)) << 24) >>> 0;
303
302
  }
304
303
  function clampAddSubtractHalf(a, b) {
305
- let out = 0;
306
- for (let shift = 0; shift < 32; shift += 8) {
307
- const ac = a >>> shift & 255, d = ac - (b >>> shift & 255);
308
- const v = ac + (d / 2 | 0);
309
- out |= (v < 0 ? 0 : v > 255 ? 255 : v) << shift;
310
- }
311
- return out >>> 0;
304
+ const a0 = a & 255, a1 = a >>> 8 & 255, a2 = a >>> 16 & 255, a3 = a >>> 24;
305
+ return (clamp255(a0 + ((a0 - (b & 255)) / 2 | 0))
306
+ | clamp255(a1 + ((a1 - (b >>> 8 & 255)) / 2 | 0)) << 8
307
+ | clamp255(a2 + ((a2 - (b >>> 16 & 255)) / 2 | 0)) << 16
308
+ | clamp255(a3 + ((a3 - (b >>> 24)) / 2 | 0)) << 24) >>> 0;
312
309
  }
313
310
  function select(l, t, tl) {
314
- let pl = 0, pt = 0;
315
- for (let shift = 0; shift < 32; shift += 8) {
316
- const lc = l >>> shift & 255, tc = t >>> shift & 255, tlc = tl >>> shift & 255;
317
- pl += Math.abs(tc - tlc);
318
- pt += Math.abs(lc - tlc);
319
- }
311
+ const tl0 = tl & 255, tl1 = tl >>> 8 & 255, tl2 = tl >>> 16 & 255, tl3 = tl >>> 24;
312
+ const pl = Math.abs((t & 255) - tl0) + Math.abs((t >>> 8 & 255) - tl1) + Math.abs((t >>> 16 & 255) - tl2) + Math.abs((t >>> 24) - tl3);
313
+ const pt = Math.abs((l & 255) - tl0) + Math.abs((l >>> 8 & 255) - tl1) + Math.abs((l >>> 16 & 255) - tl2) + Math.abs((l >>> 24) - tl3);
320
314
  return pl < pt ? l : t;
321
315
  }
322
316
  const addPixels = (a, b) => ((a & 0xff00ff00) + (b & 0xff00ff00) & 0xff00ff00 | (a & 0x00ff00ff) + (b & 0x00ff00ff) & 0x00ff00ff) >>> 0;
@@ -712,17 +706,24 @@ function tokenize(pixels, width, cacheBits) {
712
706
  let cached = 0;
713
707
  for (let i = 0; i < n;) {
714
708
  let bestLength = 0, bestDistance = 0;
715
- for (const d of [1, width]) {
716
- if (d > i)
717
- continue;
718
- const l = matchLength(i, i - d);
709
+ if (i >= 1) {
710
+ const l = matchLength(i, i - 1);
719
711
  if (l > bestLength) {
720
712
  bestLength = l;
721
- bestDistance = d;
713
+ bestDistance = 1;
714
+ }
715
+ }
716
+ if (width <= i) {
717
+ const l = matchLength(i, i - width);
718
+ if (l > bestLength) {
719
+ bestLength = l;
720
+ bestDistance = width;
722
721
  }
723
722
  }
724
723
  if (i + 1 < n && bestLength < MAX_LENGTH) {
725
724
  for (let j = head[hash(i)], k = 0; j >= 0 && k < CHAIN && i - j <= WINDOW; j = chain[j], k++) {
725
+ if (i - j >= bestDistance && (i + bestLength >= n || pixels[j + bestLength] !== pixels[i + bestLength]))
726
+ continue;
726
727
  const l = matchLength(i, j);
727
728
  if (l > bestLength || l === bestLength && l > 0 && i - j < bestDistance) {
728
729
  bestLength = l;
@@ -810,12 +811,8 @@ function writeImage(w, pixels, width, isMain) {
810
811
  }
811
812
  const subPixels = (a, b) => ((a | 0x00ff00ff) - (b & 0xff00ff00) & 0xff00ff00 | (a | 0xff00ff00) - (b & 0x00ff00ff) & 0x00ff00ff) >>> 0;
812
813
  function residualCost(r) {
813
- let cost = 0;
814
- for (let shift = 0; shift < 32; shift += 8) {
815
- const c = r >>> shift & 255;
816
- cost += c < 128 ? c : 256 - c;
817
- }
818
- return cost;
814
+ const c0 = r & 255, c1 = r >>> 8 & 255, c2 = r >>> 16 & 255, c3 = r >>> 24;
815
+ return (c0 < 128 ? c0 : 256 - c0) + (c1 < 128 ? c1 : 256 - c1) + (c2 < 128 ? c2 : 256 - c2) + (c3 < 128 ? c3 : 256 - c3);
819
816
  }
820
817
  function applyPredictor(p, width, height, bits) {
821
818
  const blocksX = subSize(width, bits), blocksY = subSize(height, bits);
@@ -239,21 +239,32 @@ function toARGB(rgba, count) {
239
239
  return argb;
240
240
  }
241
241
  const set24 = (d, o, v) => { d[o] = v & 255; d[o + 1] = v >> 8 & 255; d[o + 2] = v >> 16 & 255; };
242
- export function encode(rgba, width, height) {
243
- return riff([riffChunk('VP8L', vp8l.encode(toARGB(rgba, width * height), width, height))]);
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)]);
244
257
  }
245
- export function encodeAnimation(frames, width, height, plays) {
258
+ export function encodeAnimation(frames, width, height, plays, exif) {
246
259
  if (!frames.length)
247
260
  throw new Error('WebP: an animation needs at least one frame');
248
261
  if (width > 1 << 24 || height > 1 << 24)
249
262
  throw new RangeError('WebP: canvas too large');
250
- let alpha = false;
251
- for (const f of frames)
252
- for (let i = 3; i < f.pixels.length && !alpha; i += 4)
253
- if (f.pixels[i] !== 255)
254
- alpha = true;
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));
255
266
  const vp8x = new Uint8Array(10);
256
- vp8x[0] = 2 | (alpha ? 16 : 0);
267
+ vp8x[0] = 2 | (alpha ? 16 : 0) | (exif ? 8 : 0);
257
268
  set24(vp8x, 4, width - 1);
258
269
  set24(vp8x, 7, height - 1);
259
270
  const anim = new Uint8Array(6);
@@ -270,11 +281,13 @@ export function encodeAnimation(frames, width, height, plays) {
270
281
  set24(header, 6, f.width - 1);
271
282
  set24(header, 9, f.height - 1);
272
283
  set24(header, 12, Math.min(0xffffff, Math.max(0, Math.round(f.duration))));
273
- header[15] = f.dispose ? 1 : 0;
284
+ header[15] = (f.blend === false ? 2 : 0) | (f.dispose ? 1 : 0);
274
285
  const body = new Uint8Array(16 + bitstream.length);
275
286
  body.set(header);
276
287
  body.set(bitstream, 16);
277
288
  chunks.push(riffChunk('ANMF', body));
278
289
  }
290
+ if (exif)
291
+ chunks.push(riffChunk('EXIF', exif));
279
292
  return riff(chunks);
280
293
  }
package/dist/frame.d.ts CHANGED
@@ -1,8 +1,19 @@
1
- import { Image, ResizeMode } from './image.js';
1
+ import { Image, ResizeMode, ResizeOptions, Color, TrimOptions } from './image.js';
2
2
  /** Names of the frame disposal modes, see {@link Frame.disposalMode} */
3
3
  export type FrameDisposalModeName = 'any' | 'keep' | 'previous' | 'background';
4
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
18
  * Represents a frame of an {@link Animation}
8
19
  */
@@ -24,6 +35,11 @@ export declare class Frame extends Image {
24
35
  static get DISPOSAL_PREVIOUS(): 'previous';
25
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,6 +1,6 @@
1
1
  import { BinaryData } from './util/mem.js';
2
2
  import { Frame } from './frame.js';
3
- import { Animation } from './animation.js';
3
+ import { Animation, AnimationDecodeOptions, AnimationOptions, GIFEncodeOptions } from './animation.js';
4
4
  /**
5
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.
@@ -11,21 +11,24 @@ 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
+ constructor(frames: Frame[], loopCountOrOptions?: number | AnimationOptions);
17
18
  toString(): string;
18
19
  /**
19
20
  * Encodes the GIF
20
- * @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}
21
23
  * @deprecated Use {@link Animation.encodeGIF}
22
24
  */
23
- encode(quality?: number): Promise<Uint8Array>;
25
+ encode(qualityOrOptions?: number | GIFEncodeOptions): Promise<Uint8Array>;
24
26
  /**
25
27
  * Decodes a GIF into fully composited frames
26
28
  * @param input The binary data to decode
27
- * @param onlyExtractFirstFrame Whether to end GIF decoding after the first frame
29
+ * @param onlyFirstFrameOrOptions Whether to end GIF decoding after the first frame, or see
30
+ * {@link AnimationDecodeOptions}
28
31
  * @deprecated Use {@link Animation.decode}, which decodes APNGs too
29
32
  */
30
- static decode(input: BinaryData, onlyExtractFirstFrame?: boolean): Promise<GIF>;
33
+ static decode(input: BinaryData, onlyFirstFrameOrOptions?: boolean | AnimationDecodeOptions): Promise<GIF>;
31
34
  }
package/dist/gif.js CHANGED
@@ -1,17 +1,19 @@
1
1
  import { view } from "./util/mem.js";
2
2
  import { ImageType } from "./image-type.js";
3
3
  import { Animation } from "./animation.js";
4
+ import { isOptions } from "./util/options.js";
4
5
  export class GIF extends Animation {
5
- constructor(frames, loopCount = -1) {
6
- super(frames, loopCount);
6
+ constructor(frames, loopCountOrOptions) {
7
+ super(frames, loopCountOrOptions);
7
8
  }
8
9
  toString() {
9
10
  return `GIF<${this.width}x${this.height}x${this.duration}ms>`;
10
11
  }
11
- async encode(quality = 95) {
12
- return this.encodeGIF(quality);
12
+ async encode(qualityOrOptions) {
13
+ return this.encodeGIF(qualityOrOptions);
13
14
  }
14
- static async decode(input, onlyExtractFirstFrame = false) {
15
+ static async decode(input, onlyFirstFrameOrOptions = false) {
16
+ const onlyExtractFirstFrame = isOptions(onlyFirstFrameOrOptions) ? onlyFirstFrameOrOptions.onlyFirstFrame ?? false : onlyFirstFrameOrOptions;
15
17
  const data = view(input);
16
18
  if (!ImageType.isGIF(new DataView(data.buffer, data.byteOffset, data.byteLength)))
17
19
  throw new Error('Unsupported image type');
@@ -3,6 +3,7 @@ import { BinaryData } from './util/mem.js';
3
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
@@ -18,6 +18,7 @@ function dataView(data) {
18
18
  return new DataView(u8.buffer, u8.byteOffset, u8.byteLength);
19
19
  }
20
20
  export class ImageType {
21
+ constructor() { }
21
22
  static getType(data) {
22
23
  const view = dataView(data);
23
24
  if (this.isPNG(view))