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.
- package/README.md +29 -9
- package/THIRD_PARTY_NOTICES.md +82 -0
- package/dist/animation.d.ts +140 -0
- package/dist/animation.js +197 -0
- package/dist/codecs/exif.js +62 -0
- package/dist/codecs/gif.js +32 -19
- package/dist/codecs/jpeg.js +13 -1
- package/dist/codecs/png.js +348 -62
- package/dist/codecs/quantize.js +2 -2
- package/dist/codecs/tga.js +246 -0
- package/dist/codecs/vp8-tables.js +17 -0
- package/dist/codecs/vp8.js +711 -0
- package/dist/codecs/vp8l.js +906 -0
- package/dist/codecs/webp.js +293 -0
- package/dist/frame.d.ts +40 -12
- package/dist/frame.js +20 -9
- package/dist/gif.d.ts +16 -26
- package/dist/gif.js +10 -81
- package/dist/image-type.d.ts +6 -1
- package/dist/image-type.js +17 -1
- package/dist/image.d.ts +262 -30
- package/dist/image.js +144 -45
- package/dist/index.d.ts +20 -6
- package/dist/index.js +13 -4
- package/dist/metadata.d.ts +35 -0
- package/dist/metadata.js +43 -0
- package/dist/render/font.js +99 -2
- package/dist/render/hinting.js +1236 -0
- package/dist/render/svg.js +39 -21
- package/dist/render/text.js +32 -7
- package/dist/util/optimize.js +264 -0
- package/dist/util/options.js +3 -0
- package/dist/util/resample.js +44 -26
- package/dist/util/utf8.js +4 -51
- package/dist/version.js +1 -1
- package/licenses/FTL.txt +169 -0
- package/package.json +10 -3
- package/dist/codecs/crc.d.ts +0 -9
- package/dist/codecs/exif.d.ts +0 -11
- package/dist/codecs/gif.d.ts +0 -44
- package/dist/codecs/jpeg.d.ts +0 -35
- package/dist/codecs/png.d.ts +0 -44
- package/dist/codecs/quantize.d.ts +0 -25
- package/dist/codecs/tiff.d.ts +0 -6
- package/dist/codecs/zlib.d.ts +0 -22
- package/dist/render/font.d.ts +0 -63
- package/dist/render/svg.d.ts +0 -11
- package/dist/render/text.d.ts +0 -61
- package/dist/util/blur.d.ts +0 -13
- package/dist/util/resample.d.ts +0 -12
- package/dist/util/utf8.d.ts +0 -5
- package/dist/version.d.ts +0 -1
package/dist/codecs/gif.js
CHANGED
|
@@ -3,7 +3,7 @@ const DISPOSAL_TO_GIF = [0, 1, 3, 2];
|
|
|
3
3
|
const DISPOSAL_FROM_GIF = [0, 1, 3, 2, 0, 0, 0, 0];
|
|
4
4
|
const MAX_CODE = 4095;
|
|
5
5
|
const UNEXPECTED_END = new Error('GIF: unexpected end of data');
|
|
6
|
-
export function encode({ width, height, loopCount = -1, frames }, quality = 95) {
|
|
6
|
+
export function encode({ width, height, loopCount = -1, frames }, quality = 95, comments = []) {
|
|
7
7
|
const out = new ByteWriter();
|
|
8
8
|
out.bytes(0x47, 0x49, 0x46, 0x38, 0x39, 0x61);
|
|
9
9
|
out.u16(width);
|
|
@@ -16,8 +16,14 @@ export function encode({ width, height, loopCount = -1, frames }, quality = 95)
|
|
|
16
16
|
out.u16(loopCount === -1 ? 0 : loopCount);
|
|
17
17
|
out.bytes(0x00);
|
|
18
18
|
}
|
|
19
|
-
for (const
|
|
20
|
-
|
|
19
|
+
for (const comment of comments) {
|
|
20
|
+
out.bytes(0x21, 0xfe);
|
|
21
|
+
writeSubBlocks(out, comment);
|
|
22
|
+
}
|
|
23
|
+
const restores = frames.some(frame => frame.dispose === 3);
|
|
24
|
+
for (const [i, frame] of frames.entries()) {
|
|
25
|
+
const transparent = frame.dispose === 3 || (i === 0 && restores);
|
|
26
|
+
const { palette, indices, transparentIndex } = quantize(frame.pixels, frame.width, { quality, transparent });
|
|
21
27
|
const entries = palette.length / 3;
|
|
22
28
|
const bits = Math.max(1, Math.ceil(Math.log2(entries)));
|
|
23
29
|
out.bytes(0x21, 0xf9, 0x04);
|
|
@@ -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
|
|
189
|
-
for (let column = 0; column < fw; column++)
|
|
190
|
-
|
|
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
|
-
|
|
291
|
-
|
|
295
|
+
if (end === out + length) {
|
|
296
|
+
for (let i = end - 1; i >= out; i--) {
|
|
292
297
|
output[i] = suffix[c];
|
|
293
|
-
|
|
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) {
|
package/dist/codecs/jpeg.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* (Copyright 2014 Mozilla Foundation, Apache License 2.0). See THIRD_PARTY_NOTICES.md
|
|
6
6
|
*/
|
|
7
7
|
import { orientation as exifOrientation } from "./exif.js";
|
|
8
|
+
import { concat } from "../util/mem.js";
|
|
8
9
|
const ZIGZAG = Int32Array.of(0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63);
|
|
9
10
|
function clamp8(v) {
|
|
10
11
|
return v < 0 ? 0 : v > 255 ? 255 : v;
|
|
@@ -684,7 +685,7 @@ const CHROMA_QUANTIZATION = [
|
|
|
684
685
|
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99
|
|
685
686
|
];
|
|
686
687
|
const AAN_SCALE = [1, 1.387039845, 1.306562965, 1.175875602, 1, 0.785694958, 0.5411961, 0.275899379];
|
|
687
|
-
export function encode(pixels, width, height, quality = 90) {
|
|
688
|
+
export function encode(pixels, width, height, quality = 90, { exif, comment } = {}) {
|
|
688
689
|
if (!width || !height || width > 0xffff || height > 0xffff)
|
|
689
690
|
throw new RangeError('JPEG: invalid dimensions');
|
|
690
691
|
quality = Math.min(100, Math.max(1, Math.round(quality)));
|
|
@@ -769,6 +770,17 @@ export function encode(pixels, width, height, quality = 90) {
|
|
|
769
770
|
const writer = new BitWriter(width * height);
|
|
770
771
|
writer.bytes(0xff, 0xd8);
|
|
771
772
|
writer.bytes(0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00);
|
|
773
|
+
const segment = (marker, body) => {
|
|
774
|
+
if (body.length > 65533)
|
|
775
|
+
throw new RangeError('JPEG: metadata too large');
|
|
776
|
+
writer.bytes(0xff, marker, body.length + 2 >> 8, body.length + 2 & 255);
|
|
777
|
+
for (let i = 0; i < body.length; i++)
|
|
778
|
+
writer.bytes(body[i]);
|
|
779
|
+
};
|
|
780
|
+
if (exif)
|
|
781
|
+
segment(0xe1, concat([[0x45, 0x78, 0x69, 0x66, 0, 0], exif]));
|
|
782
|
+
if (comment)
|
|
783
|
+
segment(0xfe, comment);
|
|
772
784
|
writer.bytes(0xff, 0xdb, 0x00, 0x84);
|
|
773
785
|
for (let t = 0; t < 2; t++) {
|
|
774
786
|
writer.bytes(t);
|
package/dist/codecs/png.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
data
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
data
|
|
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
|
-
|
|
291
|
+
chunks.push(chunk(IEND, new Uint8Array(0)));
|
|
292
|
+
return concat([HEAD, ...chunks]);
|
|
149
293
|
}
|
|
150
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 +=
|
|
428
|
-
out
|
|
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) => {
|
package/dist/codecs/quantize.js
CHANGED
|
@@ -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 =
|
|
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;
|