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
@@ -3,7 +3,7 @@ const DISPOSAL_TO_GIF = [0, 1, 3, 2];
3
3
  const DISPOSAL_FROM_GIF = [0, 1, 3, 2, 0, 0, 0, 0];
4
4
  const MAX_CODE = 4095;
5
5
  const UNEXPECTED_END = new Error('GIF: unexpected end of data');
6
- export function encode({ width, height, loopCount = -1, frames }, quality = 95) {
6
+ export function encode({ width, height, loopCount = -1, frames }, quality = 95, comments = []) {
7
7
  const out = new ByteWriter();
8
8
  out.bytes(0x47, 0x49, 0x46, 0x38, 0x39, 0x61);
9
9
  out.u16(width);
@@ -16,8 +16,14 @@ export function encode({ width, height, loopCount = -1, frames }, quality = 95)
16
16
  out.u16(loopCount === -1 ? 0 : loopCount);
17
17
  out.bytes(0x00);
18
18
  }
19
- for (const frame of frames) {
20
- const { palette, indices, transparentIndex } = quantize(frame.pixels, frame.width, { quality });
19
+ for (const comment of comments) {
20
+ out.bytes(0x21, 0xfe);
21
+ writeSubBlocks(out, comment);
22
+ }
23
+ const restores = frames.some(frame => frame.dispose === 3);
24
+ for (const [i, frame] of frames.entries()) {
25
+ const transparent = frame.dispose === 3 || (i === 0 && restores);
26
+ const { palette, indices, transparentIndex } = quantize(frame.pixels, frame.width, { quality, transparent });
21
27
  const entries = palette.length / 3;
22
28
  const bits = Math.max(1, Math.ceil(Math.log2(entries)));
23
29
  out.bytes(0x21, 0xf9, 0x04);
@@ -184,20 +190,19 @@ export function decode(data, onlyFirstFrame = false) {
184
190
  continue;
185
191
  const pixels = new Uint8Array(fw * fh * 4);
186
192
  const rows = interlaced ? interlacedRows(fh) : null;
193
+ const colors = new Uint8Array(256 * 4);
194
+ for (let index = 0; index < 256; index++) {
195
+ if (index === transparentIndex)
196
+ continue;
197
+ if (table && index * 3 + 2 < table.length)
198
+ colors.set(table.subarray(index * 3, index * 3 + 3), index * 4);
199
+ colors[index * 4 + 3] = 0xff;
200
+ }
201
+ const lookup = new Uint32Array(colors.buffer), out = new Uint32Array(pixels.buffer);
187
202
  for (let row = 0; row < fh; row++) {
188
- const targetRow = rows ? rows[row] : row;
189
- for (let column = 0; column < fw; column++) {
190
- const index = indices[row * fw + column];
191
- if (index === transparentIndex)
192
- continue;
193
- const o = (targetRow * fw + column) * 4;
194
- if (table && index * 3 + 2 < table.length) {
195
- pixels[o] = table[index * 3];
196
- pixels[o + 1] = table[index * 3 + 1];
197
- pixels[o + 2] = table[index * 3 + 2];
198
- }
199
- pixels[o + 3] = 0xff;
200
- }
203
+ const target = (rows ? rows[row] : row) * fw, source = row * fw;
204
+ for (let column = 0; column < fw; column++)
205
+ out[target + column] = lookup[indices[source + column]];
201
206
  }
202
207
  frames.push({ x, y, width: fw, height: fh, delay, dispose, pixels });
203
208
  if (onlyFirstFrame)
@@ -287,10 +292,18 @@ function lzwDecode(data, minCodeSize, pixelCount) {
287
292
  const length = lengths[entry];
288
293
  const end = Math.min(out + length, pixelCount);
289
294
  let c = entry;
290
- for (let i = out + length - 1; i >= out; i--) {
291
- if (i < end)
295
+ if (end === out + length) {
296
+ for (let i = end - 1; i >= out; i--) {
292
297
  output[i] = suffix[c];
293
- c = prefix[c];
298
+ c = prefix[c];
299
+ }
300
+ }
301
+ else {
302
+ for (let i = out + length - 1; i >= out; i--) {
303
+ if (i < end)
304
+ output[i] = suffix[c];
305
+ c = prefix[c];
306
+ }
294
307
  }
295
308
  let written = end - out;
296
309
  if (code === next) {
@@ -5,6 +5,7 @@
5
5
  * (Copyright 2014 Mozilla Foundation, Apache License 2.0). See THIRD_PARTY_NOTICES.md
6
6
  */
7
7
  import { orientation as exifOrientation } from "./exif.js";
8
+ import { concat } from "../util/mem.js";
8
9
  const ZIGZAG = Int32Array.of(0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63);
9
10
  function clamp8(v) {
10
11
  return v < 0 ? 0 : v > 255 ? 255 : v;
@@ -684,7 +685,7 @@ const CHROMA_QUANTIZATION = [
684
685
  99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99
685
686
  ];
686
687
  const AAN_SCALE = [1, 1.387039845, 1.306562965, 1.175875602, 1, 0.785694958, 0.5411961, 0.275899379];
687
- export function encode(pixels, width, height, quality = 90) {
688
+ export function encode(pixels, width, height, quality = 90, { exif, comment } = {}) {
688
689
  if (!width || !height || width > 0xffff || height > 0xffff)
689
690
  throw new RangeError('JPEG: invalid dimensions');
690
691
  quality = Math.min(100, Math.max(1, Math.round(quality)));
@@ -769,6 +770,17 @@ export function encode(pixels, width, height, quality = 90) {
769
770
  const writer = new BitWriter(width * height);
770
771
  writer.bytes(0xff, 0xd8);
771
772
  writer.bytes(0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00);
773
+ const segment = (marker, body) => {
774
+ if (body.length > 65533)
775
+ throw new RangeError('JPEG: metadata too large');
776
+ writer.bytes(0xff, marker, body.length + 2 >> 8, body.length + 2 & 255);
777
+ for (let i = 0; i < body.length; i++)
778
+ writer.bytes(body[i]);
779
+ };
780
+ if (exif)
781
+ segment(0xe1, concat([[0x45, 0x78, 0x69, 0x66, 0, 0], exif]));
782
+ if (comment)
783
+ segment(0xfe, comment);
772
784
  writer.bytes(0xff, 0xdb, 0x00, 0x84);
773
785
  for (let t = 0; t < 2; t++) {
774
786
  writer.bytes(t);
@@ -13,6 +13,9 @@ const eXIf = 0x65584966;
13
13
  const tEXt = 0x74455874;
14
14
  const zTXt = 0x7a545874;
15
15
  const iTXt = 0x69545874;
16
+ const acTL = 0x6163544c;
17
+ const fcTL = 0x6663544c;
18
+ const fdAT = 0x66644154;
16
19
  const COLOR_TYPE_CHANNELS = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };
17
20
  const CHANNELS_TO_COLOR_TYPE = { 1: 0, 2: 4, 3: 2, 4: 6 };
18
21
  const VALID_DEPTHS = { 0: [1, 2, 4, 8, 16], 2: [8, 16], 3: [1, 2, 4, 8], 4: [8, 16], 6: [8, 16] };
@@ -38,15 +41,24 @@ export async function encode(data, { text, width, height, channels, depth = 8, l
38
41
  chunks.push(chunk(PLTE, palette));
39
42
  if (transparency)
40
43
  chunks.push(chunk(tRNS, transparency));
44
+ chunks.push(...textChunks(text));
41
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));
42
52
  for (const key in text) {
43
53
  const value = text[key];
44
54
  if (!value)
45
55
  continue;
46
- 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)])));
47
60
  }
48
- chunks.push(chunk(IEND, new Uint8Array(0)));
49
- return concat([HEAD, ...chunks]);
61
+ return chunks;
50
62
  }
51
63
  function filter(data, rowBytes, height, bpp) {
52
64
  const out = new Uint8Array(height * (rowBytes + 1));
@@ -63,7 +75,8 @@ function filter(data, rowBytes, height, bpp) {
63
75
  for (let i = bpp; i < rowBytes; i++) {
64
76
  const a = data[row + i - bpp], b = data[row + i - rowBytes], c = data[row + i - rowBytes - bpp];
65
77
  const pa = Math.abs(b - c), pb = Math.abs(a - c), pc = Math.abs(a + b - 2 * c);
66
- 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);
67
80
  }
68
81
  }
69
82
  return out;
@@ -77,32 +90,59 @@ function chunk(type, body) {
77
90
  view.setUint32(body.length + 8, crc32(out.subarray(4, body.length + 8)));
78
91
  return out;
79
92
  }
80
- export async function encodeRGBA(rgba, width, height, level, text) {
81
- const count = width * height;
82
- if (rgba.byteOffset % 4)
83
- rgba = rgba.slice(0, count * 4);
84
- const pixels = new Uint32Array(rgba.buffer, rgba.byteOffset, count);
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
+ }
120
+ function pixelFormat(images) {
85
121
  let opaque = true, grey = true;
86
122
  let colors = new Map();
87
- let previous = -1;
88
- for (let i = 0; i < count; i++) {
89
- const value = pixels[i];
90
- if (value === previous)
91
- continue;
92
- previous = value;
93
- const o = i * 4;
94
- if (rgba[o + 3] !== 255)
95
- opaque = false;
96
- if (rgba[o] !== rgba[o + 1] || rgba[o] !== rgba[o + 2])
97
- grey = false;
98
- if (colors && !colors.has(value)) {
99
- if (colors.size === 256)
100
- colors = null;
101
- else
102
- colors.set(value, colors.size);
123
+ scan: for (const rgba of images) {
124
+ const pixels = new Uint32Array(rgba.buffer, rgba.byteOffset, rgba.length >> 2);
125
+ let previous = -1;
126
+ for (let i = 0; i < pixels.length; i++) {
127
+ const value = pixels[i];
128
+ if (value === previous)
129
+ continue;
130
+ previous = value;
131
+ const o = i * 4;
132
+ if (rgba[o + 3] !== 255)
133
+ opaque = false;
134
+ if (rgba[o] !== rgba[o + 1] || rgba[o] !== rgba[o + 2])
135
+ grey = false;
136
+ if (colors && !colors.has(value)) {
137
+ if (colors.size === 256)
138
+ colors = null;
139
+ else
140
+ colors.set(value, colors.size);
141
+ }
142
+ if (!colors && !opaque && !grey)
143
+ break scan;
103
144
  }
104
145
  }
105
- const options = { width, height, level, text, filter: level > 0 };
106
146
  if (colors) {
107
147
  const entries = [...colors.keys()].map(value => new Uint8Array(new Uint32Array([value]).buffer));
108
148
  entries.sort((a, b) => a[3] - b[3]);
@@ -113,41 +153,145 @@ export async function encodeRGBA(rgba, width, height, level, text) {
113
153
  const translucent = entries.filter(bytes => bytes[3] !== 255).length;
114
154
  const transparency = translucent ? Uint8Array.from(entries.slice(0, translucent), bytes => bytes[3]) : undefined;
115
155
  const depth = entries.length <= 2 ? 1 : entries.length <= 4 ? 2 : entries.length <= 16 ? 4 : 8;
116
- const rowBytes = Math.ceil(width * depth / 8);
117
- const perByte = 8 / depth;
118
- const data = new Uint8Array(rowBytes * height);
119
- let last = -1, lastIndex = 0;
120
- for (let y = 0; y < height; y++) {
121
- for (let x = 0; x < width; x++) {
122
- const value = pixels[y * width + x];
123
- if (value !== last) {
124
- last = value;
125
- lastIndex = index.get(value);
156
+ return {
157
+ channels: 1, depth, palette, transparency,
158
+ convert(rgba, width, height) {
159
+ const pixels = new Uint32Array(rgba.buffer, rgba.byteOffset, width * height);
160
+ const rowBytes = Math.ceil(width * depth / 8);
161
+ const perByte = 8 / depth;
162
+ const data = new Uint8Array(rowBytes * height);
163
+ let last = -1, lastIndex = 0;
164
+ for (let y = 0; y < height; y++) {
165
+ for (let x = 0; x < width; x++) {
166
+ const value = pixels[y * width + x];
167
+ if (value !== last) {
168
+ last = value;
169
+ lastIndex = index.get(value);
170
+ }
171
+ data[y * rowBytes + (x / perByte | 0)] |= lastIndex << (8 - depth - (x % perByte) * depth);
172
+ }
126
173
  }
127
- data[y * rowBytes + (x / perByte | 0)] |= lastIndex << (8 - depth - (x % perByte) * depth);
174
+ return data;
128
175
  }
129
- }
130
- return encode(data, { ...options, channels: 1, depth, palette, transparency, filter: false });
176
+ };
131
177
  }
132
178
  const channels = (grey ? 1 : 3) + (opaque ? 0 : 1);
133
- if (channels === 4)
134
- return encode(rgba, { ...options, channels });
135
- const data = new Uint8Array(count * channels);
136
- for (let i = 0, o = 0; i < count; i++) {
137
- const s = i * 4;
138
- if (grey)
139
- data[o++] = rgba[s];
179
+ return {
180
+ channels, depth: 8,
181
+ convert(rgba, width, height) {
182
+ if (channels === 4)
183
+ return rgba;
184
+ const count = width * height;
185
+ const data = new Uint8Array(count * channels);
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];
197
+ }
198
+ }
199
+ else
200
+ for (let s = 0, o = 0; o < data.length; s += 4, o++)
201
+ data[o] = rgba[s];
202
+ return data;
203
+ }
204
+ };
205
+ }
206
+ const aligned = (rgba) => rgba.byteOffset % 4 ? rgba.slice() : rgba;
207
+ export async function encodeRGBA(rgba, width, height, level, text) {
208
+ rgba = aligned(rgba.subarray(0, width * height * 4));
209
+ const format = pixelFormat([rgba]);
210
+ const { channels, depth, palette, transparency } = format;
211
+ return encode(format.convert(rgba, width, height), { width, height, level, text, channels, depth, palette, transparency, filter: level > 0 && !palette });
212
+ }
213
+ async function compressRows(data, width, height, channels, depth, level, filterRows) {
214
+ const rowBytes = Math.ceil(width * channels * depth / 8);
215
+ let filtered;
216
+ if (filterRows)
217
+ filtered = filter(data, rowBytes, height, Math.max(1, channels * depth / 8));
218
+ else {
219
+ filtered = new Uint8Array(height * (rowBytes + 1));
220
+ for (let y = 0; y < height; y++)
221
+ filtered.set(data.subarray(y * rowBytes, (y + 1) * rowBytes), y * (rowBytes + 1) + 1);
222
+ }
223
+ return compressAsync(filtered, level);
224
+ }
225
+ export async function encodeAnimation(frames, width, height, plays, level, text) {
226
+ if (!frames.length)
227
+ throw new Error('PNG: an animation needs at least one frame');
228
+ const first = frames[0];
229
+ if (first.x !== 0 || first.y !== 0 || first.width !== width || first.height !== height) {
230
+ const pixels = new Uint8Array(width * height * 4);
231
+ for (let y = 0; y < first.height && first.y + y < height; y++) {
232
+ const row = first.pixels.subarray(y * first.width * 4, (y * first.width + Math.min(first.width, width - first.x)) * 4);
233
+ pixels.set(row, ((first.y + y) * width + first.x) * 4);
234
+ }
235
+ frames = [{ ...first, x: 0, y: 0, width, height, pixels }, ...frames.slice(1)];
236
+ }
237
+ for (const f of frames)
238
+ if (f.x < 0 || f.y < 0 || f.x + f.width > width || f.y + f.height > height || !f.width || !f.height)
239
+ throw new RangeError('PNG: APNG frames have to lie within the canvas');
240
+ const images = frames.map(f => aligned(f.pixels.subarray(0, f.width * f.height * 4)));
241
+ const format = pixelFormat(images);
242
+ const { channels, depth, palette, transparency } = format;
243
+ const filterRows = level > 0 && !palette;
244
+ const header = new Uint8Array(13);
245
+ const hv = new DataView(header.buffer);
246
+ hv.setUint32(0, width);
247
+ hv.setUint32(4, height);
248
+ header[8] = depth;
249
+ header[9] = palette ? 3 : CHANNELS_TO_COLOR_TYPE[channels];
250
+ const actl = new Uint8Array(8);
251
+ new DataView(actl.buffer).setUint32(0, frames.length);
252
+ new DataView(actl.buffer).setUint32(4, plays);
253
+ const chunks = [chunk(IHDR, header), chunk(acTL, actl)];
254
+ if (palette)
255
+ chunks.push(chunk(PLTE, palette));
256
+ if (transparency)
257
+ chunks.push(chunk(tRNS, transparency));
258
+ chunks.push(...textChunks(text));
259
+ let sequence = 0;
260
+ for (let i = 0; i < frames.length; i++) {
261
+ const f = frames[i];
262
+ const fctl = new Uint8Array(26);
263
+ const v = new DataView(fctl.buffer);
264
+ v.setUint32(0, sequence++);
265
+ v.setUint32(4, f.width);
266
+ v.setUint32(8, f.height);
267
+ v.setUint32(12, f.x);
268
+ v.setUint32(16, f.y);
269
+ const ms = Math.max(0, Math.round(f.duration));
270
+ if (ms <= 65535) {
271
+ v.setUint16(20, ms);
272
+ v.setUint16(22, 1000);
273
+ }
274
+ else {
275
+ v.setUint16(20, Math.min(65535, Math.round(ms / 10)));
276
+ v.setUint16(22, 100);
277
+ }
278
+ fctl[24] = f.dispose;
279
+ fctl[25] = f.blend === false ? 0 : 1;
280
+ chunks.push(chunk(fcTL, fctl));
281
+ const data = await compressRows(format.convert(images[i], f.width, f.height), f.width, f.height, channels, depth, level, filterRows);
282
+ if (i === 0)
283
+ chunks.push(chunk(IDAT, data));
140
284
  else {
141
- data[o++] = rgba[s];
142
- data[o++] = rgba[s + 1];
143
- data[o++] = rgba[s + 2];
285
+ const body = new Uint8Array(data.length + 4);
286
+ new DataView(body.buffer).setUint32(0, sequence++);
287
+ body.set(data, 4);
288
+ chunks.push(chunk(fdAT, body));
144
289
  }
145
- if (!opaque)
146
- data[o++] = rgba[s + 3];
147
290
  }
148
- return encode(data, { ...options, channels });
291
+ chunks.push(chunk(IEND, new Uint8Array(0)));
292
+ return concat([HEAD, ...chunks]);
149
293
  }
150
- export async function decode(array) {
294
+ function parse(array) {
151
295
  const view = new DataView(array.buffer, array.byteOffset, array.byteLength);
152
296
  for (let i = 0; i < HEAD.length; i++)
153
297
  if (array[i] !== HEAD[i])
@@ -158,6 +302,11 @@ export async function decode(array) {
158
302
  const idat = [];
159
303
  let exif = null;
160
304
  const text = [];
305
+ let animation = null;
306
+ const frames = [];
307
+ let current = null;
308
+ let sequence = 0;
309
+ let animationError = false;
161
310
  let offset = 8;
162
311
  while (offset + 8 <= array.length) {
163
312
  const length = view.getUint32(offset);
@@ -193,8 +342,46 @@ export async function decode(array) {
193
342
  throw new Error('PNG: tRNS can only occur once in an image');
194
343
  transparency = chunk;
195
344
  }
196
- else if (type === IDAT)
345
+ else if (type === IDAT) {
197
346
  idat.push(chunk);
347
+ if (current && !frames.length)
348
+ frames.push(current);
349
+ }
350
+ else if (type === acTL) {
351
+ if (animation || idat.length || length < 8)
352
+ animationError = true;
353
+ else
354
+ animation = { frames: view.getUint32(start), plays: view.getUint32(start + 4) };
355
+ }
356
+ else if (type === fcTL) {
357
+ if (length < 26 || view.getUint32(start) !== sequence++)
358
+ animationError = true;
359
+ if (animationError) {
360
+ offset = end + 4;
361
+ continue;
362
+ }
363
+ const num = view.getUint16(start + 20), den = view.getUint16(start + 22) || 100;
364
+ current = {
365
+ control: {
366
+ width: view.getUint32(start + 4),
367
+ height: view.getUint32(start + 8),
368
+ x: view.getUint32(start + 12),
369
+ y: view.getUint32(start + 16),
370
+ delay: num * 1000 / den,
371
+ dispose: chunk[24],
372
+ blend: chunk[25]
373
+ },
374
+ data: idat.length ? [] : idat
375
+ };
376
+ if (idat.length)
377
+ frames.push(current);
378
+ }
379
+ else if (type === fdAT) {
380
+ if (length < 4 || view.getUint32(start) !== sequence++ || !current || current.data === idat)
381
+ animationError = true;
382
+ else
383
+ current.data.push(chunk.subarray(4));
384
+ }
198
385
  else if (type === eXIf)
199
386
  exif ??= chunk;
200
387
  else if (type === tEXt || type === zTXt || type === iTXt)
@@ -207,7 +394,7 @@ export async function decode(array) {
207
394
  throw new Error('PNG: missing IHDR');
208
395
  if (!idat.length)
209
396
  throw new Error('PNG: missing IDAT');
210
- const { width, height, depth, colorType, interlace } = header;
397
+ const { width, height, depth, colorType } = header;
211
398
  const channels = COLOR_TYPE_CHANNELS[colorType];
212
399
  if (!channels)
213
400
  throw new Error(`PNG: invalid color type ${colorType}`);
@@ -217,6 +404,17 @@ export async function decode(array) {
217
404
  throw new Error('PNG: invalid dimensions');
218
405
  if (colorType === 3 && !palette)
219
406
  throw new Error('PNG: indexed color image has no PLTE');
407
+ if (animation) {
408
+ if (frames.length > animation.frames)
409
+ frames.length = animation.frames;
410
+ if (!frames.length || frames.length < animation.frames || frames.some(f => !f.data.length))
411
+ animationError = true;
412
+ }
413
+ return { header, palette, transparency, idat, exif, text, animation, frames, animationError };
414
+ }
415
+ async function decodeImageData(p, chunks, width, height) {
416
+ const { depth, colorType, interlace } = p.header;
417
+ const channels = COLOR_TYPE_CHANNELS[colorType];
220
418
  const bitsPerPixel = channels * depth;
221
419
  const bpp = Math.max(1, bitsPerPixel >> 3);
222
420
  const passes = interlace ? ADAM7.map(([xs, ys, dx, dy]) => [xs, ys, dx, dy, Math.ceil((width - xs) / dx), Math.ceil((height - ys) / dy)]) : [[0, 0, 1, 1, width, height]];
@@ -226,11 +424,13 @@ export async function decode(array) {
226
424
  if (pw > 0 && ph > 0)
227
425
  expected += ph * (1 + Math.ceil(pw * bitsPerPixel / 8));
228
426
  }
229
- const data = await decompressAsync(idat.length === 1 ? idat[0] : concat(idat), expected);
427
+ if (!chunks.length)
428
+ throw new Error('PNG: missing image data');
429
+ const data = await decompressAsync(chunks.length === 1 ? chunks[0] : concat(chunks), expected);
230
430
  if (data.length < expected)
231
431
  throw new Error('PNG: image data is truncated');
232
432
  const rgba = new Uint8Array(width * height * 4);
233
- const toRGBA = createConverter(colorType, depth, palette, transparency);
433
+ const toRGBA = createConverter(colorType, depth, p.palette, p.transparency);
234
434
  let dataOffset = 0;
235
435
  for (const [xs, ys, dx, dy, pw, ph] of passes) {
236
436
  if (pw <= 0 || ph <= 0)
@@ -241,7 +441,73 @@ export async function decode(array) {
241
441
  dataOffset += size + ph;
242
442
  toRGBA(pixels, rowBytes, pw, ph, rgba, { row: y => ((ys + y * dy) * width + xs) * 4, step: dx * 4 });
243
443
  }
244
- return { width, height, pixels: rgba, orientation: exif ? exifOrientation(exif) : await textOrientation(text) };
444
+ return rgba;
445
+ }
446
+ export async function decode(array) {
447
+ const p = parse(array);
448
+ const { width, height } = p.header;
449
+ const pixels = await decodeImageData(p, p.idat, width, height);
450
+ return { width, height, pixels, orientation: p.exif ? exifOrientation(p.exif) : await textOrientation(p.text) };
451
+ }
452
+ export function isAnimated(array) {
453
+ try {
454
+ const p = parse(array);
455
+ return p.animation !== null && !p.animationError;
456
+ }
457
+ catch {
458
+ return false;
459
+ }
460
+ }
461
+ export async function decodeAnimation(array, onlyFirstFrame = false) {
462
+ const p = parse(array);
463
+ const { width, height } = p.header;
464
+ if (!p.animation || p.animationError) {
465
+ const pixels = await decodeImageData(p, p.idat, width, height);
466
+ return { width, height, plays: 0, frames: [{ x: 0, y: 0, width, height, duration: 0, dispose: 0, blend: 0, pixels }] };
467
+ }
468
+ const frames = [];
469
+ for (const { control: c, data } of p.frames) {
470
+ if (!c.width || !c.height || c.x + c.width > width || c.y + c.height > height)
471
+ throw new Error('PNG: invalid APNG frame region');
472
+ frames.push({
473
+ x: c.x, y: c.y, width: c.width, height: c.height, duration: c.delay,
474
+ dispose: !frames.length && c.dispose === 2 ? 1 : c.dispose,
475
+ blend: c.blend,
476
+ pixels: await decodeImageData(p, data, c.width, c.height)
477
+ });
478
+ if (onlyFirstFrame)
479
+ break;
480
+ }
481
+ return { width, height, plays: p.animation.plays, frames };
482
+ }
483
+ export function compositeAnimation({ width, height, frames }) {
484
+ const canvas = new Uint8Array(width * height * 4);
485
+ const output = [];
486
+ for (const f of frames) {
487
+ const previous = f.dispose === 2 ? canvas.slice() : null;
488
+ for (let y = 0; y < f.height; y++)
489
+ for (let x = 0; x < f.width; x++) {
490
+ const s = (y * f.width + x) * 4, d = ((f.y + y) * width + f.x + x) * 4;
491
+ const sa = f.pixels[s + 3], da = canvas[d + 3];
492
+ if (f.blend === 0 || sa === 255 || da === 0) {
493
+ if (f.blend === 0 || sa !== 0)
494
+ canvas.set(f.pixels.subarray(s, s + 4), d);
495
+ }
496
+ else if (sa !== 0) {
497
+ const u = sa * 255, v = (255 - sa) * da, al = u + v;
498
+ for (let c = 0; c < 3; c++)
499
+ canvas[d + c] = (f.pixels[s + c] * u + canvas[d + c] * v) / al | 0;
500
+ canvas[d + 3] = al / 255 | 0;
501
+ }
502
+ }
503
+ output.push({ pixels: canvas.slice(), duration: f.duration });
504
+ if (f.dispose === 1)
505
+ for (let y = 0; y < f.height; y++)
506
+ canvas.fill(0, ((f.y + y) * width + f.x) * 4, ((f.y + y) * width + f.x + f.width) * 4);
507
+ else if (previous)
508
+ canvas.set(previous);
509
+ }
510
+ return output;
245
511
  }
246
512
  const RAW_EXIF = /^Raw profile type (exif|APP1)$/;
247
513
  const XMP = 'XML:com.adobe.xmp';
@@ -330,7 +596,8 @@ function unfilter(data, offset, height, rowBytes, bpp) {
330
596
  for (let i = row + bpp; i < row + rowBytes; i++) {
331
597
  const a = out[i - bpp], b = out[i - rowBytes], c = out[i - rowBytes - bpp];
332
598
  const pa = Math.abs(b - c), pb = Math.abs(a - c), pc = Math.abs(a + b - 2 * c);
333
- 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);
334
601
  }
335
602
  }
336
603
  }
@@ -392,6 +659,20 @@ function createConverter(colorType, depth, palette, transparency) {
392
659
  const key = transparency && transparency.length >= 6
393
660
  ? [transparency[0] << 8 | transparency[1], transparency[2] << 8 | transparency[3], transparency[4] << 8 | transparency[5]].map(v => v & maxValue)
394
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
+ }
395
676
  return (pixels, rowBytes, w, h, out, place) => {
396
677
  for (let y = 0; y < h; y++) {
397
678
  const row = pixels.subarray(y * rowBytes, (y + 1) * rowBytes);
@@ -423,9 +704,14 @@ function createConverter(colorType, depth, palette, transparency) {
423
704
  return (pixels, rowBytes, w, h, out, place) => {
424
705
  if (place.step === 4 && w * 4 === rowBytes && place.row(0) === 0 && place.row(1) === rowBytes)
425
706
  return out.set(pixels);
426
- for (let y = 0; y < h; y++)
427
- for (let x = 0, o = place.row(y); x < w; x++, o += place.step)
428
- 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
+ }
429
715
  };
430
716
  }
431
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;