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
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export interface WebPFrame {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
/** milliseconds */
|
|
7
|
+
duration: number;
|
|
8
|
+
/** whether the frame is alpha blended onto the canvas (false: it replaces the covered area) */
|
|
9
|
+
blend: boolean;
|
|
10
|
+
/** whether the frame's area is cleared to transparent after it was shown */
|
|
11
|
+
dispose: boolean;
|
|
12
|
+
/** RGBA */
|
|
13
|
+
pixels: Uint8Array;
|
|
14
|
+
}
|
|
15
|
+
export interface WebPAnimation {
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
/** 0 = forever */
|
|
19
|
+
loopCount: number;
|
|
20
|
+
/** ARGB background color hint (decoders, like browsers, are free to ignore it) */
|
|
21
|
+
background: number;
|
|
22
|
+
frames: WebPFrame[];
|
|
23
|
+
}
|
|
24
|
+
/** Whether the WebP is animated */
|
|
25
|
+
export declare function isAnimated(data: Uint8Array): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Decodes a still WebP into RGBA pixels. Animated WebPs decode to their first frame composited on the canvas
|
|
28
|
+
*/
|
|
29
|
+
export declare function decode(data: Uint8Array): {
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
pixels: Uint8Array;
|
|
33
|
+
orientation: number;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Decodes an animated WebP into its frames (not composited); still WebPs become a single frame
|
|
37
|
+
*/
|
|
38
|
+
export declare function decodeAnimation(data: Uint8Array, onlyFirstFrame?: boolean): WebPAnimation;
|
|
39
|
+
/**
|
|
40
|
+
* Composites animation frames onto the canvas like browsers do (transparent background, the background color hint
|
|
41
|
+
* is ignored as the spec allows)
|
|
42
|
+
*/
|
|
43
|
+
export declare function composite({ width, height, frames }: WebPAnimation): Array<{
|
|
44
|
+
pixels: Uint8Array;
|
|
45
|
+
duration: number;
|
|
46
|
+
}>;
|
|
47
|
+
/**
|
|
48
|
+
* Encodes RGBA pixels into a lossless WebP
|
|
49
|
+
*/
|
|
50
|
+
export declare function encode(rgba: Uint8Array, width: number, height: number): Uint8Array;
|
|
51
|
+
export interface WebPEncodeFrame {
|
|
52
|
+
x: number;
|
|
53
|
+
y: number;
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
/** milliseconds */
|
|
57
|
+
duration: number;
|
|
58
|
+
/** whether the frame's area is cleared after it was shown */
|
|
59
|
+
dispose: boolean;
|
|
60
|
+
/** RGBA, alpha blended onto the canvas */
|
|
61
|
+
pixels: Uint8Array;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Encodes frames into a lossless animated WebP. Frame offsets have to be even (the format stores them halved)
|
|
65
|
+
* @param plays How often to play the animation, 0 = forever
|
|
66
|
+
*/
|
|
67
|
+
export declare function encodeAnimation(frames: WebPEncodeFrame[], width: number, height: number, plays: number): Uint8Array;
|
|
@@ -0,0 +1,280 @@
|
|
|
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
|
+
export function encode(rgba, width, height) {
|
|
243
|
+
return riff([riffChunk('VP8L', vp8l.encode(toARGB(rgba, width * height), width, height))]);
|
|
244
|
+
}
|
|
245
|
+
export function encodeAnimation(frames, width, height, plays) {
|
|
246
|
+
if (!frames.length)
|
|
247
|
+
throw new Error('WebP: an animation needs at least one frame');
|
|
248
|
+
if (width > 1 << 24 || height > 1 << 24)
|
|
249
|
+
throw new RangeError('WebP: canvas too large');
|
|
250
|
+
let alpha = false;
|
|
251
|
+
for (const f of frames)
|
|
252
|
+
for (let i = 3; i < f.pixels.length && !alpha; i += 4)
|
|
253
|
+
if (f.pixels[i] !== 255)
|
|
254
|
+
alpha = true;
|
|
255
|
+
const vp8x = new Uint8Array(10);
|
|
256
|
+
vp8x[0] = 2 | (alpha ? 16 : 0);
|
|
257
|
+
set24(vp8x, 4, width - 1);
|
|
258
|
+
set24(vp8x, 7, height - 1);
|
|
259
|
+
const anim = new Uint8Array(6);
|
|
260
|
+
anim[4] = plays & 255;
|
|
261
|
+
anim[5] = plays >> 8 & 255;
|
|
262
|
+
const chunks = [riffChunk('VP8X', vp8x), riffChunk('ANIM', anim)];
|
|
263
|
+
for (const f of frames) {
|
|
264
|
+
if (f.x & 1 || f.y & 1)
|
|
265
|
+
throw new RangeError('WebP: frame offsets have to be even');
|
|
266
|
+
const bitstream = riffChunk('VP8L', vp8l.encode(toARGB(f.pixels, f.width * f.height), f.width, f.height));
|
|
267
|
+
const header = new Uint8Array(16);
|
|
268
|
+
set24(header, 0, f.x >> 1);
|
|
269
|
+
set24(header, 3, f.y >> 1);
|
|
270
|
+
set24(header, 6, f.width - 1);
|
|
271
|
+
set24(header, 9, f.height - 1);
|
|
272
|
+
set24(header, 12, Math.min(0xffffff, Math.max(0, Math.round(f.duration))));
|
|
273
|
+
header[15] = f.dispose ? 1 : 0;
|
|
274
|
+
const body = new Uint8Array(16 + bitstream.length);
|
|
275
|
+
body.set(header);
|
|
276
|
+
body.set(bitstream, 16);
|
|
277
|
+
chunks.push(riffChunk('ANMF', body));
|
|
278
|
+
}
|
|
279
|
+
return riff(chunks);
|
|
280
|
+
}
|
package/dist/frame.d.ts
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
import { Image, ResizeMode } from './image.js';
|
|
2
|
-
/** Names of the
|
|
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
6
|
/**
|
|
7
|
-
* Represents a frame
|
|
7
|
+
* Represents a frame of an {@link Animation}
|
|
8
8
|
*/
|
|
9
9
|
export declare class Frame extends Image {
|
|
10
10
|
/**
|
|
11
|
-
* Milliseconds. GIFs store hundredths of a second, so this is rounded to 10 ms when encoding;
|
|
11
|
+
* Milliseconds. GIFs store hundredths of a second, so this is rounded to 10 ms when encoding a GIF;
|
|
12
12
|
* note that browsers play frames shorter than 20 ms with 100 ms
|
|
13
13
|
*/
|
|
14
14
|
duration: number;
|
|
15
|
-
/** The frame's horizontal offset in the
|
|
15
|
+
/** The frame's horizontal offset in the animation */
|
|
16
16
|
xOffset: number;
|
|
17
|
-
/** The frame's vertical offset in the
|
|
17
|
+
/** The frame's vertical offset in the animation */
|
|
18
18
|
yOffset: number;
|
|
19
|
-
/**
|
|
19
|
+
/** Frame disposal mode ANY: the decoder decides (in practice like KEEP). For use with {@link Frame} */
|
|
20
20
|
static get DISPOSAL_ANY(): 'any';
|
|
21
|
-
/**
|
|
21
|
+
/** Frame disposal mode KEEP. For use with {@link Frame} */
|
|
22
22
|
static get DISPOSAL_KEEP(): 'keep';
|
|
23
|
-
/**
|
|
23
|
+
/** Frame disposal mode PREVIOUS. For use with {@link Frame} */
|
|
24
24
|
static get DISPOSAL_PREVIOUS(): 'previous';
|
|
25
|
-
/**
|
|
25
|
+
/** Frame disposal mode BACKGROUND. For use with {@link Frame} */
|
|
26
26
|
static get DISPOSAL_BACKGROUND(): 'background';
|
|
27
27
|
/**
|
|
28
28
|
* Creates a new, blank frame
|
package/dist/gif.d.ts
CHANGED
|
@@ -1,44 +1,31 @@
|
|
|
1
1
|
import { BinaryData } from './util/mem.js';
|
|
2
|
-
import { ResizeMode } from './image.js';
|
|
3
2
|
import { Frame } from './frame.js';
|
|
3
|
+
import { Animation } 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
14
|
* @param loopCount How many times to repeat the GIF (-1 = forever, 0 = play once)
|
|
15
15
|
*/
|
|
16
16
|
constructor(frames: Frame[], loopCount?: number);
|
|
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;
|
|
23
17
|
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
18
|
/**
|
|
29
19
|
* Encodes the GIF
|
|
30
20
|
* @param quality 1-100, scales the palette size from 8 colors (1) to 256 colors (90 and above)
|
|
21
|
+
* @deprecated Use {@link Animation.encodeGIF}
|
|
31
22
|
*/
|
|
32
23
|
encode(quality?: number): Promise<Uint8Array>;
|
|
33
24
|
/**
|
|
34
25
|
* Decodes a GIF into fully composited frames
|
|
35
26
|
* @param input The binary data to decode
|
|
36
27
|
* @param onlyExtractFirstFrame Whether to end GIF decoding after the first frame
|
|
28
|
+
* @deprecated Use {@link Animation.decode}, which decodes APNGs too
|
|
37
29
|
*/
|
|
38
30
|
static decode(input: BinaryData, onlyExtractFirstFrame?: boolean): Promise<GIF>;
|
|
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.
|
|
42
|
-
*/
|
|
43
|
-
resize(width: number, height: number, mode?: ResizeMode): this;
|
|
44
31
|
}
|
package/dist/gif.js
CHANGED
|
@@ -1,93 +1,20 @@
|
|
|
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
|
-
|
|
3
|
+
import { Animation } from "./animation.js";
|
|
4
|
+
export class GIF extends Animation {
|
|
8
5
|
constructor(frames, loopCount = -1) {
|
|
9
|
-
super();
|
|
10
|
-
for (const frame of frames)
|
|
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;
|
|
6
|
+
super(frames, loopCount);
|
|
39
7
|
}
|
|
40
8
|
toString() {
|
|
41
9
|
return `GIF<${this.width}x${this.height}x${this.duration}ms>`;
|
|
42
10
|
}
|
|
43
|
-
slice(start, end) {
|
|
44
|
-
return new GIF(Array.prototype.slice.call(this, start, end), this.loopCount);
|
|
45
|
-
}
|
|
46
11
|
async encode(quality = 95) {
|
|
47
|
-
|
|
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
|
+
return this.encodeGIF(quality);
|
|
62
13
|
}
|
|
63
14
|
static async decode(input, onlyExtractFirstFrame = false) {
|
|
64
15
|
const data = view(input);
|
|
65
16
|
if (!ImageType.isGIF(new DataView(data.buffer, data.byteOffset, data.byteLength)))
|
|
66
17
|
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;
|
|
18
|
+
return Animation.__fromGIF__(data, onlyExtractFirstFrame, GIF);
|
|
92
19
|
}
|
|
93
20
|
}
|
package/dist/image-type.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
6
|
/**
|
|
@@ -17,4 +17,8 @@ export declare class ImageType {
|
|
|
17
17
|
static isTIFF(data: BinaryData): boolean;
|
|
18
18
|
/** Whether the data starts like a GIF */
|
|
19
19
|
static isGIF(data: BinaryData): boolean;
|
|
20
|
+
/** Whether the data starts like a WebP */
|
|
21
|
+
static isWebP(data: BinaryData): boolean;
|
|
22
|
+
/** Whether the data looks like a TGA (which has no magic number: its header has to be consistent) */
|
|
23
|
+
static isTGA(data: BinaryData): boolean;
|
|
20
24
|
}
|
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)
|
|
@@ -25,6 +28,10 @@ export class ImageType {
|
|
|
25
28
|
return 'tiff';
|
|
26
29
|
if (this.isGIF(view))
|
|
27
30
|
return 'gif';
|
|
31
|
+
if (this.isWebP(view))
|
|
32
|
+
return 'webp';
|
|
33
|
+
if (this.isTGA(view))
|
|
34
|
+
return 'tga';
|
|
28
35
|
return null;
|
|
29
36
|
}
|
|
30
37
|
static isPNG(data) {
|
|
@@ -46,4 +53,12 @@ export class ImageType {
|
|
|
46
53
|
const view = dataView(data);
|
|
47
54
|
return view.byteLength >= 4 && (view.getUint32(0, false) >>> 8) === MAGIC_NUMBERS.GIF;
|
|
48
55
|
}
|
|
56
|
+
static isWebP(data) {
|
|
57
|
+
const view = dataView(data);
|
|
58
|
+
return view.byteLength >= 12 && view.getUint32(0, false) === MAGIC_NUMBERS.RIFF && view.getUint32(8, false) === MAGIC_NUMBERS.WEBP;
|
|
59
|
+
}
|
|
60
|
+
static isTGA(data) {
|
|
61
|
+
const view = dataView(data);
|
|
62
|
+
return isTGA(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
|
|
63
|
+
}
|
|
49
64
|
}
|
package/dist/image.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface CompositeOptions {
|
|
|
19
19
|
}
|
|
20
20
|
/** Options of {@link Image.decode} */
|
|
21
21
|
export interface DecodeOptions {
|
|
22
|
-
/** Whether to rotate/mirror JPEGs, TIFFs and
|
|
22
|
+
/** Whether to rotate/mirror JPEGs, TIFFs, PNGs and WebPs as their orientation tag (Exif or XMP) says. Default: `true` */
|
|
23
23
|
applyOrientation?: boolean;
|
|
24
24
|
}
|
|
25
25
|
/** Text style options of {@link Image.renderText} */
|
|
@@ -28,6 +28,12 @@ export interface TextStyle {
|
|
|
28
28
|
strokeWidth?: number;
|
|
29
29
|
/** Color of the outline. Default: `0x000000ff` */
|
|
30
30
|
strokeColor?: Color;
|
|
31
|
+
/**
|
|
32
|
+
* Whether to run the hinting instructions of TrueType fonts, which align stems and edges with the pixel grid for
|
|
33
|
+
* crisper text at small sizes (like Windows renders text); the glyphs of hinted fonts are then placed on whole
|
|
34
|
+
* pixels. Fonts without instructions (and CFF/OpenType outlines) aren't affected. Default: `true`
|
|
35
|
+
*/
|
|
36
|
+
hinting?: boolean;
|
|
31
37
|
}
|
|
32
38
|
/** Options of {@link Image.cropCircle} */
|
|
33
39
|
export interface CropCircleOptions {
|
|
@@ -50,6 +56,13 @@ export interface RotateOptions {
|
|
|
50
56
|
/** Whether to resize the image so it fits all pixels or just ignore outlying pixels. Default: `true` */
|
|
51
57
|
resize?: boolean;
|
|
52
58
|
}
|
|
59
|
+
/** Options of {@link Image.colorToAlpha} */
|
|
60
|
+
export interface ColorToAlphaOptions {
|
|
61
|
+
/** Pixels at most this far from the color (0-1) become fully transparent. Default: 0 */
|
|
62
|
+
transparencyThreshold?: number;
|
|
63
|
+
/** Pixels at least this far from the color (0-1) stay fully opaque. Default: 1 */
|
|
64
|
+
opacityThreshold?: number;
|
|
65
|
+
}
|
|
53
66
|
/** Text metadata written into PNGs by {@link Image.encode} */
|
|
54
67
|
export interface PNGMetadata {
|
|
55
68
|
/** The image's title */
|
|
@@ -399,6 +412,16 @@ export declare class Image {
|
|
|
399
412
|
* Applies a sepia tone
|
|
400
413
|
*/
|
|
401
414
|
sepia(): this;
|
|
415
|
+
/**
|
|
416
|
+
* Makes a color transparent, like GIMP's "Color to Alpha": the color is removed from every pixel and replaced by
|
|
417
|
+
* transparency, so pixels that are partly the color (e.g. antialiased edges against a background) become
|
|
418
|
+
* semi-transparent instead of keeping a fringe of it.
|
|
419
|
+
* The color is removed everywhere in the image, not only from a connected background: removing a white
|
|
420
|
+
* background also makes white areas inside the subject (e.g. eyes in a logo) transparent
|
|
421
|
+
* @param color The color to remove, by default the color of the top left pixel (e.g. a background)
|
|
422
|
+
* @param options See {@link ColorToAlphaOptions}
|
|
423
|
+
*/
|
|
424
|
+
colorToAlpha(color?: Color, options?: ColorToAlphaOptions): this;
|
|
402
425
|
/**
|
|
403
426
|
* Gets the average color of the image, weighting each pixel by its opacity
|
|
404
427
|
* @returns The average color (fully opaque), black for fully transparent images
|
|
@@ -478,7 +501,16 @@ export declare class Image {
|
|
|
478
501
|
*/
|
|
479
502
|
encodeJPEG(quality?: number): Promise<Uint8Array>;
|
|
480
503
|
/**
|
|
481
|
-
*
|
|
504
|
+
* Encodes the image as lossless WebP (usually 20-30% smaller than PNG, but slower to encode)
|
|
505
|
+
*/
|
|
506
|
+
encodeWebP(): Promise<Uint8Array>;
|
|
507
|
+
/**
|
|
508
|
+
* Encodes the image as run length encoded TGA (32 bit, or 24 bit if the image is opaque)
|
|
509
|
+
*/
|
|
510
|
+
encodeTGA(): Promise<Uint8Array>;
|
|
511
|
+
/**
|
|
512
|
+
* Decodes an image (PNG, JPEG, TIFF, WebP or TGA). For APNGs this is the default image; animated WebPs and GIFs
|
|
513
|
+
* have no single image, decode those with {@link Animation.decode}
|
|
482
514
|
* @param input The binary data to decode
|
|
483
515
|
* @param options See {@link DecodeOptions}. JPEGs, TIFFs and PNGs are rotated/mirrored as their orientation tag
|
|
484
516
|
* (Exif, for PNGs also XMP) says by default: photos taken with a rotated camera are stored sideways
|