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