imagescript 3.0.0 → 3.1.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 +19 -7
- package/THIRD_PARTY_NOTICES.md +82 -0
- package/dist/animation.d.ts +55 -0
- package/dist/animation.js +167 -0
- package/dist/codecs/png.d.ts +58 -5
- package/dist/codecs/png.js +276 -54
- package/dist/codecs/tga.d.ts +16 -0
- package/dist/codecs/tga.js +203 -0
- package/dist/codecs/vp8-tables.d.ts +10 -0
- package/dist/codecs/vp8-tables.js +17 -0
- package/dist/codecs/vp8.d.ts +8 -0
- package/dist/codecs/vp8.js +711 -0
- package/dist/codecs/vp8l.d.ts +62 -0
- package/dist/codecs/vp8l.js +909 -0
- package/dist/codecs/webp.d.ts +67 -0
- package/dist/codecs/webp.js +280 -0
- package/dist/frame.d.ts +10 -10
- package/dist/gif.d.ts +7 -20
- package/dist/gif.js +5 -78
- package/dist/image-type.d.ts +5 -1
- package/dist/image-type.js +16 -1
- package/dist/image.d.ts +34 -2
- package/dist/image.js +40 -2
- package/dist/index.d.ts +8 -4
- package/dist/index.js +8 -1
- package/dist/render/font.d.ts +29 -0
- package/dist/render/font.js +99 -2
- package/dist/render/hinting.d.ts +110 -0
- package/dist/render/hinting.js +1236 -0
- package/dist/render/text.d.ts +3 -1
- package/dist/render/text.js +32 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/licenses/FTL.txt +169 -0
- package/package.json +2 -2
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] };
|
|
@@ -77,32 +80,30 @@ function chunk(type, body) {
|
|
|
77
80
|
view.setUint32(body.length + 8, crc32(out.subarray(4, body.length + 8)));
|
|
78
81
|
return out;
|
|
79
82
|
}
|
|
80
|
-
|
|
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);
|
|
83
|
+
function pixelFormat(images) {
|
|
85
84
|
let opaque = true, grey = true;
|
|
86
85
|
let colors = new Map();
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
86
|
+
for (const rgba of images) {
|
|
87
|
+
const pixels = new Uint32Array(rgba.buffer, rgba.byteOffset, rgba.length >> 2);
|
|
88
|
+
let previous = -1;
|
|
89
|
+
for (let i = 0; i < pixels.length; i++) {
|
|
90
|
+
const value = pixels[i];
|
|
91
|
+
if (value === previous)
|
|
92
|
+
continue;
|
|
93
|
+
previous = value;
|
|
94
|
+
const o = i * 4;
|
|
95
|
+
if (rgba[o + 3] !== 255)
|
|
96
|
+
opaque = false;
|
|
97
|
+
if (rgba[o] !== rgba[o + 1] || rgba[o] !== rgba[o + 2])
|
|
98
|
+
grey = false;
|
|
99
|
+
if (colors && !colors.has(value)) {
|
|
100
|
+
if (colors.size === 256)
|
|
101
|
+
colors = null;
|
|
102
|
+
else
|
|
103
|
+
colors.set(value, colors.size);
|
|
104
|
+
}
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
|
-
const options = { width, height, level, text, filter: level > 0 };
|
|
106
107
|
if (colors) {
|
|
107
108
|
const entries = [...colors.keys()].map(value => new Uint8Array(new Uint32Array([value]).buffer));
|
|
108
109
|
entries.sort((a, b) => a[3] - b[3]);
|
|
@@ -113,41 +114,140 @@ export async function encodeRGBA(rgba, width, height, level, text) {
|
|
|
113
114
|
const translucent = entries.filter(bytes => bytes[3] !== 255).length;
|
|
114
115
|
const transparency = translucent ? Uint8Array.from(entries.slice(0, translucent), bytes => bytes[3]) : undefined;
|
|
115
116
|
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
|
-
|
|
117
|
+
return {
|
|
118
|
+
channels: 1, depth, palette, transparency,
|
|
119
|
+
convert(rgba, width, height) {
|
|
120
|
+
const pixels = new Uint32Array(rgba.buffer, rgba.byteOffset, width * height);
|
|
121
|
+
const rowBytes = Math.ceil(width * depth / 8);
|
|
122
|
+
const perByte = 8 / depth;
|
|
123
|
+
const data = new Uint8Array(rowBytes * height);
|
|
124
|
+
let last = -1, lastIndex = 0;
|
|
125
|
+
for (let y = 0; y < height; y++) {
|
|
126
|
+
for (let x = 0; x < width; x++) {
|
|
127
|
+
const value = pixels[y * width + x];
|
|
128
|
+
if (value !== last) {
|
|
129
|
+
last = value;
|
|
130
|
+
lastIndex = index.get(value);
|
|
131
|
+
}
|
|
132
|
+
data[y * rowBytes + (x / perByte | 0)] |= lastIndex << (8 - depth - (x % perByte) * depth);
|
|
133
|
+
}
|
|
126
134
|
}
|
|
127
|
-
data
|
|
135
|
+
return data;
|
|
128
136
|
}
|
|
129
|
-
}
|
|
130
|
-
return encode(data, { ...options, channels: 1, depth, palette, transparency, filter: false });
|
|
137
|
+
};
|
|
131
138
|
}
|
|
132
139
|
const channels = (grey ? 1 : 3) + (opaque ? 0 : 1);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
data
|
|
140
|
+
return {
|
|
141
|
+
channels, depth: 8,
|
|
142
|
+
convert(rgba, width, height) {
|
|
143
|
+
if (channels === 4)
|
|
144
|
+
return rgba;
|
|
145
|
+
const count = width * height;
|
|
146
|
+
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];
|
|
155
|
+
}
|
|
156
|
+
if (!opaque)
|
|
157
|
+
data[o++] = rgba[s + 3];
|
|
158
|
+
}
|
|
159
|
+
return data;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const aligned = (rgba) => rgba.byteOffset % 4 ? rgba.slice() : rgba;
|
|
164
|
+
export async function encodeRGBA(rgba, width, height, level, text) {
|
|
165
|
+
rgba = aligned(rgba.subarray(0, width * height * 4));
|
|
166
|
+
const format = pixelFormat([rgba]);
|
|
167
|
+
const { channels, depth, palette, transparency } = format;
|
|
168
|
+
return encode(format.convert(rgba, width, height), { width, height, level, text, channels, depth, palette, transparency, filter: level > 0 && !palette });
|
|
169
|
+
}
|
|
170
|
+
async function compressRows(data, width, height, channels, depth, level, filterRows) {
|
|
171
|
+
const rowBytes = Math.ceil(width * channels * depth / 8);
|
|
172
|
+
let filtered;
|
|
173
|
+
if (filterRows)
|
|
174
|
+
filtered = filter(data, rowBytes, height, Math.max(1, channels * depth / 8));
|
|
175
|
+
else {
|
|
176
|
+
filtered = new Uint8Array(height * (rowBytes + 1));
|
|
177
|
+
for (let y = 0; y < height; y++)
|
|
178
|
+
filtered.set(data.subarray(y * rowBytes, (y + 1) * rowBytes), y * (rowBytes + 1) + 1);
|
|
179
|
+
}
|
|
180
|
+
return compressAsync(filtered, level);
|
|
181
|
+
}
|
|
182
|
+
export async function encodeAnimation(frames, width, height, plays, level) {
|
|
183
|
+
if (!frames.length)
|
|
184
|
+
throw new Error('PNG: an animation needs at least one frame');
|
|
185
|
+
const first = frames[0];
|
|
186
|
+
if (first.x !== 0 || first.y !== 0 || first.width !== width || first.height !== height) {
|
|
187
|
+
const pixels = new Uint8Array(width * height * 4);
|
|
188
|
+
for (let y = 0; y < first.height && first.y + y < height; y++) {
|
|
189
|
+
const row = first.pixels.subarray(y * first.width * 4, (y * first.width + Math.min(first.width, width - first.x)) * 4);
|
|
190
|
+
pixels.set(row, ((first.y + y) * width + first.x) * 4);
|
|
191
|
+
}
|
|
192
|
+
frames = [{ ...first, x: 0, y: 0, width, height, pixels }, ...frames.slice(1)];
|
|
193
|
+
}
|
|
194
|
+
for (const f of frames)
|
|
195
|
+
if (f.x < 0 || f.y < 0 || f.x + f.width > width || f.y + f.height > height || !f.width || !f.height)
|
|
196
|
+
throw new RangeError('PNG: APNG frames have to lie within the canvas');
|
|
197
|
+
const images = frames.map(f => aligned(f.pixels.subarray(0, f.width * f.height * 4)));
|
|
198
|
+
const format = pixelFormat(images);
|
|
199
|
+
const { channels, depth, palette, transparency } = format;
|
|
200
|
+
const filterRows = level > 0 && !palette;
|
|
201
|
+
const header = new Uint8Array(13);
|
|
202
|
+
const hv = new DataView(header.buffer);
|
|
203
|
+
hv.setUint32(0, width);
|
|
204
|
+
hv.setUint32(4, height);
|
|
205
|
+
header[8] = depth;
|
|
206
|
+
header[9] = palette ? 3 : CHANNELS_TO_COLOR_TYPE[channels];
|
|
207
|
+
const actl = new Uint8Array(8);
|
|
208
|
+
new DataView(actl.buffer).setUint32(0, frames.length);
|
|
209
|
+
new DataView(actl.buffer).setUint32(4, plays);
|
|
210
|
+
const chunks = [chunk(IHDR, header), chunk(acTL, actl)];
|
|
211
|
+
if (palette)
|
|
212
|
+
chunks.push(chunk(PLTE, palette));
|
|
213
|
+
if (transparency)
|
|
214
|
+
chunks.push(chunk(tRNS, transparency));
|
|
215
|
+
let sequence = 0;
|
|
216
|
+
for (let i = 0; i < frames.length; i++) {
|
|
217
|
+
const f = frames[i];
|
|
218
|
+
const fctl = new Uint8Array(26);
|
|
219
|
+
const v = new DataView(fctl.buffer);
|
|
220
|
+
v.setUint32(0, sequence++);
|
|
221
|
+
v.setUint32(4, f.width);
|
|
222
|
+
v.setUint32(8, f.height);
|
|
223
|
+
v.setUint32(12, f.x);
|
|
224
|
+
v.setUint32(16, f.y);
|
|
225
|
+
const ms = Math.max(0, Math.round(f.duration));
|
|
226
|
+
if (ms <= 65535) {
|
|
227
|
+
v.setUint16(20, ms);
|
|
228
|
+
v.setUint16(22, 1000);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
v.setUint16(20, Math.min(65535, Math.round(ms / 10)));
|
|
232
|
+
v.setUint16(22, 100);
|
|
233
|
+
}
|
|
234
|
+
fctl[24] = f.dispose;
|
|
235
|
+
fctl[25] = 1;
|
|
236
|
+
chunks.push(chunk(fcTL, fctl));
|
|
237
|
+
const data = await compressRows(format.convert(images[i], f.width, f.height), f.width, f.height, channels, depth, level, filterRows);
|
|
238
|
+
if (i === 0)
|
|
239
|
+
chunks.push(chunk(IDAT, data));
|
|
140
240
|
else {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
data
|
|
241
|
+
const body = new Uint8Array(data.length + 4);
|
|
242
|
+
new DataView(body.buffer).setUint32(0, sequence++);
|
|
243
|
+
body.set(data, 4);
|
|
244
|
+
chunks.push(chunk(fdAT, body));
|
|
144
245
|
}
|
|
145
|
-
if (!opaque)
|
|
146
|
-
data[o++] = rgba[s + 3];
|
|
147
246
|
}
|
|
148
|
-
|
|
247
|
+
chunks.push(chunk(IEND, new Uint8Array(0)));
|
|
248
|
+
return concat([HEAD, ...chunks]);
|
|
149
249
|
}
|
|
150
|
-
|
|
250
|
+
function parse(array) {
|
|
151
251
|
const view = new DataView(array.buffer, array.byteOffset, array.byteLength);
|
|
152
252
|
for (let i = 0; i < HEAD.length; i++)
|
|
153
253
|
if (array[i] !== HEAD[i])
|
|
@@ -158,6 +258,11 @@ export async function decode(array) {
|
|
|
158
258
|
const idat = [];
|
|
159
259
|
let exif = null;
|
|
160
260
|
const text = [];
|
|
261
|
+
let animation = null;
|
|
262
|
+
const frames = [];
|
|
263
|
+
let current = null;
|
|
264
|
+
let sequence = 0;
|
|
265
|
+
let animationError = false;
|
|
161
266
|
let offset = 8;
|
|
162
267
|
while (offset + 8 <= array.length) {
|
|
163
268
|
const length = view.getUint32(offset);
|
|
@@ -193,8 +298,46 @@ export async function decode(array) {
|
|
|
193
298
|
throw new Error('PNG: tRNS can only occur once in an image');
|
|
194
299
|
transparency = chunk;
|
|
195
300
|
}
|
|
196
|
-
else if (type === IDAT)
|
|
301
|
+
else if (type === IDAT) {
|
|
197
302
|
idat.push(chunk);
|
|
303
|
+
if (current && !frames.length)
|
|
304
|
+
frames.push(current);
|
|
305
|
+
}
|
|
306
|
+
else if (type === acTL) {
|
|
307
|
+
if (animation || idat.length || length < 8)
|
|
308
|
+
animationError = true;
|
|
309
|
+
else
|
|
310
|
+
animation = { frames: view.getUint32(start), plays: view.getUint32(start + 4) };
|
|
311
|
+
}
|
|
312
|
+
else if (type === fcTL) {
|
|
313
|
+
if (length < 26 || view.getUint32(start) !== sequence++)
|
|
314
|
+
animationError = true;
|
|
315
|
+
if (animationError) {
|
|
316
|
+
offset = end + 4;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const num = view.getUint16(start + 20), den = view.getUint16(start + 22) || 100;
|
|
320
|
+
current = {
|
|
321
|
+
control: {
|
|
322
|
+
width: view.getUint32(start + 4),
|
|
323
|
+
height: view.getUint32(start + 8),
|
|
324
|
+
x: view.getUint32(start + 12),
|
|
325
|
+
y: view.getUint32(start + 16),
|
|
326
|
+
delay: num * 1000 / den,
|
|
327
|
+
dispose: chunk[24],
|
|
328
|
+
blend: chunk[25]
|
|
329
|
+
},
|
|
330
|
+
data: idat.length ? [] : idat
|
|
331
|
+
};
|
|
332
|
+
if (idat.length)
|
|
333
|
+
frames.push(current);
|
|
334
|
+
}
|
|
335
|
+
else if (type === fdAT) {
|
|
336
|
+
if (length < 4 || view.getUint32(start) !== sequence++ || !current || current.data === idat)
|
|
337
|
+
animationError = true;
|
|
338
|
+
else
|
|
339
|
+
current.data.push(chunk.subarray(4));
|
|
340
|
+
}
|
|
198
341
|
else if (type === eXIf)
|
|
199
342
|
exif ??= chunk;
|
|
200
343
|
else if (type === tEXt || type === zTXt || type === iTXt)
|
|
@@ -207,7 +350,7 @@ export async function decode(array) {
|
|
|
207
350
|
throw new Error('PNG: missing IHDR');
|
|
208
351
|
if (!idat.length)
|
|
209
352
|
throw new Error('PNG: missing IDAT');
|
|
210
|
-
const { width, height, depth, colorType
|
|
353
|
+
const { width, height, depth, colorType } = header;
|
|
211
354
|
const channels = COLOR_TYPE_CHANNELS[colorType];
|
|
212
355
|
if (!channels)
|
|
213
356
|
throw new Error(`PNG: invalid color type ${colorType}`);
|
|
@@ -217,6 +360,17 @@ export async function decode(array) {
|
|
|
217
360
|
throw new Error('PNG: invalid dimensions');
|
|
218
361
|
if (colorType === 3 && !palette)
|
|
219
362
|
throw new Error('PNG: indexed color image has no PLTE');
|
|
363
|
+
if (animation) {
|
|
364
|
+
if (frames.length > animation.frames)
|
|
365
|
+
frames.length = animation.frames;
|
|
366
|
+
if (!frames.length || frames.length < animation.frames || frames.some(f => !f.data.length))
|
|
367
|
+
animationError = true;
|
|
368
|
+
}
|
|
369
|
+
return { header, palette, transparency, idat, exif, text, animation, frames, animationError };
|
|
370
|
+
}
|
|
371
|
+
async function decodeImageData(p, chunks, width, height) {
|
|
372
|
+
const { depth, colorType, interlace } = p.header;
|
|
373
|
+
const channels = COLOR_TYPE_CHANNELS[colorType];
|
|
220
374
|
const bitsPerPixel = channels * depth;
|
|
221
375
|
const bpp = Math.max(1, bitsPerPixel >> 3);
|
|
222
376
|
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 +380,13 @@ export async function decode(array) {
|
|
|
226
380
|
if (pw > 0 && ph > 0)
|
|
227
381
|
expected += ph * (1 + Math.ceil(pw * bitsPerPixel / 8));
|
|
228
382
|
}
|
|
229
|
-
|
|
383
|
+
if (!chunks.length)
|
|
384
|
+
throw new Error('PNG: missing image data');
|
|
385
|
+
const data = await decompressAsync(chunks.length === 1 ? chunks[0] : concat(chunks), expected);
|
|
230
386
|
if (data.length < expected)
|
|
231
387
|
throw new Error('PNG: image data is truncated');
|
|
232
388
|
const rgba = new Uint8Array(width * height * 4);
|
|
233
|
-
const toRGBA = createConverter(colorType, depth, palette, transparency);
|
|
389
|
+
const toRGBA = createConverter(colorType, depth, p.palette, p.transparency);
|
|
234
390
|
let dataOffset = 0;
|
|
235
391
|
for (const [xs, ys, dx, dy, pw, ph] of passes) {
|
|
236
392
|
if (pw <= 0 || ph <= 0)
|
|
@@ -241,7 +397,73 @@ export async function decode(array) {
|
|
|
241
397
|
dataOffset += size + ph;
|
|
242
398
|
toRGBA(pixels, rowBytes, pw, ph, rgba, { row: y => ((ys + y * dy) * width + xs) * 4, step: dx * 4 });
|
|
243
399
|
}
|
|
244
|
-
return
|
|
400
|
+
return rgba;
|
|
401
|
+
}
|
|
402
|
+
export async function decode(array) {
|
|
403
|
+
const p = parse(array);
|
|
404
|
+
const { width, height } = p.header;
|
|
405
|
+
const pixels = await decodeImageData(p, p.idat, width, height);
|
|
406
|
+
return { width, height, pixels, orientation: p.exif ? exifOrientation(p.exif) : await textOrientation(p.text) };
|
|
407
|
+
}
|
|
408
|
+
export function isAnimated(array) {
|
|
409
|
+
try {
|
|
410
|
+
const p = parse(array);
|
|
411
|
+
return p.animation !== null && !p.animationError;
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
export async function decodeAnimation(array, onlyFirstFrame = false) {
|
|
418
|
+
const p = parse(array);
|
|
419
|
+
const { width, height } = p.header;
|
|
420
|
+
if (!p.animation || p.animationError) {
|
|
421
|
+
const pixels = await decodeImageData(p, p.idat, width, height);
|
|
422
|
+
return { width, height, plays: 0, frames: [{ x: 0, y: 0, width, height, duration: 0, dispose: 0, blend: 0, pixels }] };
|
|
423
|
+
}
|
|
424
|
+
const frames = [];
|
|
425
|
+
for (const { control: c, data } of p.frames) {
|
|
426
|
+
if (!c.width || !c.height || c.x + c.width > width || c.y + c.height > height)
|
|
427
|
+
throw new Error('PNG: invalid APNG frame region');
|
|
428
|
+
frames.push({
|
|
429
|
+
x: c.x, y: c.y, width: c.width, height: c.height, duration: c.delay,
|
|
430
|
+
dispose: !frames.length && c.dispose === 2 ? 1 : c.dispose,
|
|
431
|
+
blend: c.blend,
|
|
432
|
+
pixels: await decodeImageData(p, data, c.width, c.height)
|
|
433
|
+
});
|
|
434
|
+
if (onlyFirstFrame)
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
return { width, height, plays: p.animation.plays, frames };
|
|
438
|
+
}
|
|
439
|
+
export function compositeAnimation({ width, height, frames }) {
|
|
440
|
+
const canvas = new Uint8Array(width * height * 4);
|
|
441
|
+
const output = [];
|
|
442
|
+
for (const f of frames) {
|
|
443
|
+
const previous = f.dispose === 2 ? canvas.slice() : null;
|
|
444
|
+
for (let y = 0; y < f.height; y++)
|
|
445
|
+
for (let x = 0; x < f.width; x++) {
|
|
446
|
+
const s = (y * f.width + x) * 4, d = ((f.y + y) * width + f.x + x) * 4;
|
|
447
|
+
const sa = f.pixels[s + 3], da = canvas[d + 3];
|
|
448
|
+
if (f.blend === 0 || sa === 255 || da === 0) {
|
|
449
|
+
if (f.blend === 0 || sa !== 0)
|
|
450
|
+
canvas.set(f.pixels.subarray(s, s + 4), d);
|
|
451
|
+
}
|
|
452
|
+
else if (sa !== 0) {
|
|
453
|
+
const u = sa * 255, v = (255 - sa) * da, al = u + v;
|
|
454
|
+
for (let c = 0; c < 3; c++)
|
|
455
|
+
canvas[d + c] = (f.pixels[s + c] * u + canvas[d + c] * v) / al | 0;
|
|
456
|
+
canvas[d + 3] = al / 255 | 0;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
output.push({ pixels: canvas.slice(), duration: f.duration });
|
|
460
|
+
if (f.dispose === 1)
|
|
461
|
+
for (let y = 0; y < f.height; y++)
|
|
462
|
+
canvas.fill(0, ((f.y + y) * width + f.x) * 4, ((f.y + y) * width + f.x + f.width) * 4);
|
|
463
|
+
else if (previous)
|
|
464
|
+
canvas.set(previous);
|
|
465
|
+
}
|
|
466
|
+
return output;
|
|
245
467
|
}
|
|
246
468
|
const RAW_EXIF = /^Raw profile type (exif|APP1)$/;
|
|
247
469
|
const XMP = 'XML:com.adobe.xmp';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the data looks like a TGA. Older TGAs have no signature, so this checks that the header is consistent
|
|
3
|
+
*/
|
|
4
|
+
export declare function isTGA(d: Uint8Array): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Decodes a TGA into RGBA
|
|
7
|
+
*/
|
|
8
|
+
export declare function decode(d: Uint8Array): {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
pixels: Uint8Array;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Encodes RGBA pixels into a run length encoded TGA (32 bit with alpha, 24 bit if opaque), top left origin
|
|
15
|
+
*/
|
|
16
|
+
export declare function encode(rgba: Uint8Array, width: number, height: number): Uint8Array;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
const SIGNATURE = 'TRUEVISION-XFILE.\0';
|
|
2
|
+
function readHeader(d) {
|
|
3
|
+
if (d.length < 18)
|
|
4
|
+
return null;
|
|
5
|
+
return {
|
|
6
|
+
idLength: d[0],
|
|
7
|
+
colorMapType: d[1],
|
|
8
|
+
imageType: d[2],
|
|
9
|
+
mapFirst: d[3] | d[4] << 8,
|
|
10
|
+
mapLength: d[5] | d[6] << 8,
|
|
11
|
+
mapEntrySize: d[7],
|
|
12
|
+
width: d[12] | d[13] << 8,
|
|
13
|
+
height: d[14] | d[15] << 8,
|
|
14
|
+
depth: d[16],
|
|
15
|
+
alphaBits: d[17] & 15,
|
|
16
|
+
rightToLeft: (d[17] & 16) !== 0,
|
|
17
|
+
topToBottom: (d[17] & 32) !== 0
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function hasFooter(d) {
|
|
21
|
+
if (d.length < 44)
|
|
22
|
+
return false;
|
|
23
|
+
for (let i = 0; i < SIGNATURE.length; i++)
|
|
24
|
+
if (d[d.length - 18 + i] !== SIGNATURE.charCodeAt(i))
|
|
25
|
+
return false;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
export function isTGA(d) {
|
|
29
|
+
const h = readHeader(d);
|
|
30
|
+
if (!h)
|
|
31
|
+
return false;
|
|
32
|
+
if (hasFooter(d))
|
|
33
|
+
return true;
|
|
34
|
+
const base = h.imageType & ~8;
|
|
35
|
+
if (h.colorMapType > 1 || ![1, 2, 3].includes(base) || h.imageType > 11 || (h.imageType & 4))
|
|
36
|
+
return false;
|
|
37
|
+
if (!h.width || !h.height || d[17] & 0xc0)
|
|
38
|
+
return false;
|
|
39
|
+
if (base === 1 && (h.colorMapType !== 1 || (h.depth !== 8 && h.depth !== 16)))
|
|
40
|
+
return false;
|
|
41
|
+
if (base === 2 && ![15, 16, 24, 32].includes(h.depth))
|
|
42
|
+
return false;
|
|
43
|
+
if (base === 3 && h.depth !== 8 && h.depth !== 16)
|
|
44
|
+
return false;
|
|
45
|
+
if (h.colorMapType === 1 && ![8, 15, 16, 24, 32].includes(h.mapEntrySize))
|
|
46
|
+
return false;
|
|
47
|
+
const mapBytes = h.colorMapType ? h.mapLength * Math.ceil(h.mapEntrySize / 8) : 0;
|
|
48
|
+
const dataStart = 18 + h.idLength + mapBytes;
|
|
49
|
+
const pixelBytes = h.width * h.height * Math.ceil(h.depth / 8);
|
|
50
|
+
return h.imageType & 8 ? d.length > dataStart : d.length >= dataStart + pixelBytes;
|
|
51
|
+
}
|
|
52
|
+
function readColor(d, o, bits, alphaBits, out, p) {
|
|
53
|
+
if (bits === 8) {
|
|
54
|
+
out[p] = out[p + 1] = out[p + 2] = d[o];
|
|
55
|
+
out[p + 3] = 255;
|
|
56
|
+
}
|
|
57
|
+
else if (bits === 15 || bits === 16) {
|
|
58
|
+
const v = d[o] | d[o + 1] << 8;
|
|
59
|
+
out[p] = (v >> 10 & 31) * 255 / 31 | 0;
|
|
60
|
+
out[p + 1] = (v >> 5 & 31) * 255 / 31 | 0;
|
|
61
|
+
out[p + 2] = (v & 31) * 255 / 31 | 0;
|
|
62
|
+
out[p + 3] = 255;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
out[p] = d[o + 2];
|
|
66
|
+
out[p + 1] = d[o + 1];
|
|
67
|
+
out[p + 2] = d[o];
|
|
68
|
+
out[p + 3] = bits === 32 && alphaBits ? d[o + 3] : 255;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function decode(d) {
|
|
72
|
+
const h = readHeader(d);
|
|
73
|
+
if (!h || !isTGA(d))
|
|
74
|
+
throw new Error('TGA: invalid header');
|
|
75
|
+
const { width, height, depth } = h;
|
|
76
|
+
const base = h.imageType & ~8, rle = (h.imageType & 8) !== 0;
|
|
77
|
+
const bytesPerPixel = Math.ceil(depth / 8);
|
|
78
|
+
let offset = 18 + h.idLength;
|
|
79
|
+
let palette = null;
|
|
80
|
+
if (h.colorMapType === 1) {
|
|
81
|
+
const entryBytes = Math.ceil(h.mapEntrySize / 8);
|
|
82
|
+
palette = new Uint8Array((h.mapFirst + h.mapLength) * 4);
|
|
83
|
+
for (let i = 0; i < h.mapLength; i++) {
|
|
84
|
+
if (offset + entryBytes > d.length)
|
|
85
|
+
throw new Error('TGA: truncated color map');
|
|
86
|
+
readColor(d, offset, h.mapEntrySize, h.alphaBits, palette, (h.mapFirst + i) * 4);
|
|
87
|
+
offset += entryBytes;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (base === 1 && !palette)
|
|
91
|
+
throw new Error('TGA: color mapped image without color map');
|
|
92
|
+
const count = width * height;
|
|
93
|
+
const stored = new Uint8Array(count * 4);
|
|
94
|
+
const pixel = (o, p) => {
|
|
95
|
+
if (base === 1) {
|
|
96
|
+
const index = depth === 16 ? d[o] | d[o + 1] << 8 : d[o];
|
|
97
|
+
const pal = palette;
|
|
98
|
+
if (index * 4 + 3 < pal.length)
|
|
99
|
+
stored.set(pal.subarray(index * 4, index * 4 + 4), p);
|
|
100
|
+
else
|
|
101
|
+
stored[p + 3] = 255;
|
|
102
|
+
}
|
|
103
|
+
else if (base === 3) {
|
|
104
|
+
stored[p] = stored[p + 1] = stored[p + 2] = d[o];
|
|
105
|
+
stored[p + 3] = depth === 16 ? d[o + 1] : 255;
|
|
106
|
+
}
|
|
107
|
+
else
|
|
108
|
+
readColor(d, o, depth, h.alphaBits, stored, p);
|
|
109
|
+
};
|
|
110
|
+
if (!rle) {
|
|
111
|
+
if (offset + count * bytesPerPixel > d.length)
|
|
112
|
+
throw new Error('TGA: truncated image data');
|
|
113
|
+
for (let i = 0; i < count; i++)
|
|
114
|
+
pixel(offset + i * bytesPerPixel, i * 4);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
for (let i = 0; i < count;) {
|
|
118
|
+
if (offset >= d.length)
|
|
119
|
+
throw new Error('TGA: truncated image data');
|
|
120
|
+
const header = d[offset++];
|
|
121
|
+
const n = (header & 127) + 1;
|
|
122
|
+
if (offset + (header & 128 ? 1 : n) * bytesPerPixel > d.length)
|
|
123
|
+
throw new Error('TGA: truncated image data');
|
|
124
|
+
if (header & 128) {
|
|
125
|
+
pixel(offset, i * 4);
|
|
126
|
+
for (let k = 1; k < n && i + k < count; k++)
|
|
127
|
+
stored.copyWithin((i + k) * 4, i * 4, i * 4 + 4);
|
|
128
|
+
offset += bytesPerPixel;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
for (let k = 0; k < n && i + k < count; k++)
|
|
132
|
+
pixel(offset + k * bytesPerPixel, (i + k) * 4);
|
|
133
|
+
offset += n * bytesPerPixel;
|
|
134
|
+
}
|
|
135
|
+
i += n;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!h.topToBottom || h.rightToLeft) {
|
|
139
|
+
const pixels = new Uint8Array(count * 4);
|
|
140
|
+
const src = new Uint32Array(stored.buffer), dst = new Uint32Array(pixels.buffer);
|
|
141
|
+
for (let y = 0; y < height; y++) {
|
|
142
|
+
const sy = h.topToBottom ? y : height - 1 - y;
|
|
143
|
+
for (let x = 0; x < width; x++)
|
|
144
|
+
dst[y * width + x] = src[sy * width + (h.rightToLeft ? width - 1 - x : x)];
|
|
145
|
+
}
|
|
146
|
+
return { width, height, pixels };
|
|
147
|
+
}
|
|
148
|
+
return { width, height, pixels: stored };
|
|
149
|
+
}
|
|
150
|
+
export function encode(rgba, width, height) {
|
|
151
|
+
if (width < 1 || height < 1 || width > 65535 || height > 65535)
|
|
152
|
+
throw new RangeError('TGA: dimensions must be 1-65535');
|
|
153
|
+
let opaque = true;
|
|
154
|
+
for (let i = 3; i < rgba.length; i += 4)
|
|
155
|
+
if (rgba[i] !== 255) {
|
|
156
|
+
opaque = false;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
const bpp = opaque ? 3 : 4;
|
|
160
|
+
const count = width * height;
|
|
161
|
+
if (rgba.byteOffset % 4)
|
|
162
|
+
rgba = rgba.slice(0, count * 4);
|
|
163
|
+
const pixels = new Uint32Array(rgba.buffer, rgba.byteOffset, count);
|
|
164
|
+
const out = new Uint8Array(18 + count * bpp + Math.ceil(count / 128) + 26);
|
|
165
|
+
out[2] = 10;
|
|
166
|
+
out[12] = width & 255;
|
|
167
|
+
out[13] = width >> 8;
|
|
168
|
+
out[14] = height & 255;
|
|
169
|
+
out[15] = height >> 8;
|
|
170
|
+
out[16] = bpp * 8;
|
|
171
|
+
out[17] = 32 | (opaque ? 0 : 8);
|
|
172
|
+
let o = 18;
|
|
173
|
+
const put = (i) => {
|
|
174
|
+
const p = i * 4;
|
|
175
|
+
out[o++] = rgba[p + 2];
|
|
176
|
+
out[o++] = rgba[p + 1];
|
|
177
|
+
out[o++] = rgba[p];
|
|
178
|
+
if (bpp === 4)
|
|
179
|
+
out[o++] = rgba[p + 3];
|
|
180
|
+
};
|
|
181
|
+
for (let i = 0; i < count;) {
|
|
182
|
+
let run = 1;
|
|
183
|
+
while (run < 128 && i + run < count && pixels[i + run] === pixels[i])
|
|
184
|
+
run++;
|
|
185
|
+
if (run >= 2) {
|
|
186
|
+
out[o++] = 128 | run - 1;
|
|
187
|
+
put(i);
|
|
188
|
+
i += run;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
let raw = 1;
|
|
192
|
+
while (raw < 128 && i + raw < count && !(i + raw + 1 < count && pixels[i + raw] === pixels[i + raw + 1]))
|
|
193
|
+
raw++;
|
|
194
|
+
out[o++] = raw - 1;
|
|
195
|
+
for (let k = 0; k < raw; k++)
|
|
196
|
+
put(i + k);
|
|
197
|
+
i += raw;
|
|
198
|
+
}
|
|
199
|
+
o += 8;
|
|
200
|
+
for (let i = 0; i < SIGNATURE.length; i++)
|
|
201
|
+
out[o++] = SIGNATURE.charCodeAt(i);
|
|
202
|
+
return out.slice(0, o);
|
|
203
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* VP8 constant tables, extracted from RFC 6386 ("VP8 Data Format and Decoding Guide", sections 9.6, 11.5, 13.4,
|
|
3
|
+
* 13.5 and 14.1). Copyright (c) 2010, 2011 Google Inc., BSD license (see THIRD_PARTY_NOTICES.md)
|
|
4
|
+
*/
|
|
5
|
+
export declare const KF_BMODE_PROBS: Uint8Array<ArrayBuffer>;
|
|
6
|
+
export declare const COEFF_UPDATE_PROBS: Uint8Array<ArrayBuffer>;
|
|
7
|
+
export declare const DEFAULT_COEFF_PROBS: Uint8Array<ArrayBuffer>;
|
|
8
|
+
export declare const DC_QLOOKUP: Uint8Array<ArrayBuffer>;
|
|
9
|
+
export declare const AC_QLOOKUP: Int16Array<ArrayBuffer>;
|
|
10
|
+
export declare const PCAT: Uint8Array<ArrayBuffer>[];
|