imagescript 3.1.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 +12 -4
- package/dist/animation.d.ts +92 -7
- package/dist/animation.js +49 -19
- 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 +85 -21
- package/dist/codecs/quantize.js +2 -2
- package/dist/codecs/tga.js +47 -4
- package/dist/codecs/vp8l.js +27 -30
- package/dist/codecs/webp.js +23 -10
- package/dist/frame.d.ts +30 -2
- package/dist/frame.js +20 -9
- package/dist/gif.d.ts +10 -7
- package/dist/gif.js +7 -5
- package/dist/image-type.d.ts +1 -0
- package/dist/image-type.js +1 -0
- package/dist/image.d.ts +231 -31
- package/dist/image.js +111 -50
- package/dist/index.d.ts +14 -4
- package/dist/index.js +6 -4
- package/dist/metadata.d.ts +35 -0
- package/dist/metadata.js +43 -0
- package/dist/render/svg.js +39 -21
- 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/package.json +9 -2
- 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 -97
- package/dist/codecs/quantize.d.ts +0 -25
- package/dist/codecs/tga.d.ts +0 -16
- package/dist/codecs/tiff.d.ts +0 -6
- package/dist/codecs/vp8-tables.d.ts +0 -10
- package/dist/codecs/vp8.d.ts +0 -8
- package/dist/codecs/vp8l.d.ts +0 -62
- package/dist/codecs/webp.d.ts +0 -67
- package/dist/codecs/zlib.d.ts +0 -22
- package/dist/render/font.d.ts +0 -92
- package/dist/render/hinting.d.ts +0 -110
- package/dist/render/svg.d.ts +0 -11
- package/dist/render/text.d.ts +0 -63
- package/dist/util/blur.d.ts +0 -13
- package/dist/util/resample.d.ts +0 -12
- package/dist/util/utf8.d.ts +0 -5
- package/dist/version.d.ts +0 -1
package/dist/codecs/png.js
CHANGED
|
@@ -41,15 +41,24 @@ export async function encode(data, { text, width, height, channels, depth = 8, l
|
|
|
41
41
|
chunks.push(chunk(PLTE, palette));
|
|
42
42
|
if (transparency)
|
|
43
43
|
chunks.push(chunk(tRNS, transparency));
|
|
44
|
+
chunks.push(...textChunks(text));
|
|
44
45
|
chunks.push(chunk(IDAT, await compressAsync(filtered, level)));
|
|
46
|
+
chunks.push(chunk(IEND, new Uint8Array(0)));
|
|
47
|
+
return concat([HEAD, ...chunks]);
|
|
48
|
+
}
|
|
49
|
+
function textChunks(text) {
|
|
50
|
+
const chunks = [];
|
|
51
|
+
const latin1 = (s) => Array.from(s, c => c.charCodeAt(0));
|
|
45
52
|
for (const key in text) {
|
|
46
53
|
const value = text[key];
|
|
47
54
|
if (!value)
|
|
48
55
|
continue;
|
|
49
|
-
|
|
56
|
+
if (/^[\x00-\xff]*$/.test(value))
|
|
57
|
+
chunks.push(chunk(tEXt, concat([latin1(key), [0], latin1(value)])));
|
|
58
|
+
else
|
|
59
|
+
chunks.push(chunk(iTXt, concat([latin1(key), [0, 0, 0, 0, 0], utf8(value)])));
|
|
50
60
|
}
|
|
51
|
-
chunks
|
|
52
|
-
return concat([HEAD, ...chunks]);
|
|
61
|
+
return chunks;
|
|
53
62
|
}
|
|
54
63
|
function filter(data, rowBytes, height, bpp) {
|
|
55
64
|
const out = new Uint8Array(height * (rowBytes + 1));
|
|
@@ -66,7 +75,8 @@ function filter(data, rowBytes, height, bpp) {
|
|
|
66
75
|
for (let i = bpp; i < rowBytes; i++) {
|
|
67
76
|
const a = data[row + i - bpp], b = data[row + i - rowBytes], c = data[row + i - rowBytes - bpp];
|
|
68
77
|
const pa = Math.abs(b - c), pb = Math.abs(a - c), pc = Math.abs(a + b - 2 * c);
|
|
69
|
-
|
|
78
|
+
const notA = (pb - pa | pc - pa) >> 31, takeC = (pc - pb) >> 31;
|
|
79
|
+
out[o + i] = data[row + i] - (a ^ (a ^ b ^ (b ^ c) & takeC) & notA);
|
|
70
80
|
}
|
|
71
81
|
}
|
|
72
82
|
return out;
|
|
@@ -80,10 +90,37 @@ function chunk(type, body) {
|
|
|
80
90
|
view.setUint32(body.length + 8, crc32(out.subarray(4, body.length + 8)));
|
|
81
91
|
return out;
|
|
82
92
|
}
|
|
93
|
+
export function alphaCost(images) {
|
|
94
|
+
let grey = true;
|
|
95
|
+
const colors = new Set();
|
|
96
|
+
scan: for (const rgba of images) {
|
|
97
|
+
const pixels = new Uint32Array(rgba.buffer, rgba.byteOffset, rgba.length >> 2);
|
|
98
|
+
let previous = -1;
|
|
99
|
+
for (let i = 0; i < pixels.length; i++) {
|
|
100
|
+
const value = pixels[i];
|
|
101
|
+
if (value === previous)
|
|
102
|
+
continue;
|
|
103
|
+
previous = value;
|
|
104
|
+
const o = i * 4;
|
|
105
|
+
if (rgba[o + 3] === 0)
|
|
106
|
+
continue;
|
|
107
|
+
if (rgba[o] !== rgba[o + 1] || rgba[o] !== rgba[o + 2])
|
|
108
|
+
grey = false;
|
|
109
|
+
if (colors.size <= 256)
|
|
110
|
+
colors.add(value);
|
|
111
|
+
else if (!grey)
|
|
112
|
+
break scan;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const palette = (entries) => (entries <= 2 ? 1 : entries <= 4 ? 2 : entries <= 16 ? 4 : 8) / 8;
|
|
116
|
+
const transparent = colors.size < 256 ? palette(colors.size + 1) : grey ? 2 : 4;
|
|
117
|
+
const opaque = colors.size <= 256 ? palette(colors.size) : grey ? 1 : 3;
|
|
118
|
+
return transparent / opaque;
|
|
119
|
+
}
|
|
83
120
|
function pixelFormat(images) {
|
|
84
121
|
let opaque = true, grey = true;
|
|
85
122
|
let colors = new Map();
|
|
86
|
-
for (const rgba of images) {
|
|
123
|
+
scan: for (const rgba of images) {
|
|
87
124
|
const pixels = new Uint32Array(rgba.buffer, rgba.byteOffset, rgba.length >> 2);
|
|
88
125
|
let previous = -1;
|
|
89
126
|
for (let i = 0; i < pixels.length; i++) {
|
|
@@ -102,6 +139,8 @@ function pixelFormat(images) {
|
|
|
102
139
|
else
|
|
103
140
|
colors.set(value, colors.size);
|
|
104
141
|
}
|
|
142
|
+
if (!colors && !opaque && !grey)
|
|
143
|
+
break scan;
|
|
105
144
|
}
|
|
106
145
|
}
|
|
107
146
|
if (colors) {
|
|
@@ -144,18 +183,22 @@ function pixelFormat(images) {
|
|
|
144
183
|
return rgba;
|
|
145
184
|
const count = width * height;
|
|
146
185
|
const data = new Uint8Array(count * channels);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
data[o
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
186
|
+
if (channels === 3) {
|
|
187
|
+
for (let s = 0, o = 0; o < data.length; s += 4, o += 3) {
|
|
188
|
+
data[o] = rgba[s];
|
|
189
|
+
data[o + 1] = rgba[s + 1];
|
|
190
|
+
data[o + 2] = rgba[s + 2];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else if (channels === 2) {
|
|
194
|
+
for (let s = 0, o = 0; o < data.length; s += 4, o += 2) {
|
|
195
|
+
data[o] = rgba[s];
|
|
196
|
+
data[o + 1] = rgba[s + 3];
|
|
155
197
|
}
|
|
156
|
-
if (!opaque)
|
|
157
|
-
data[o++] = rgba[s + 3];
|
|
158
198
|
}
|
|
199
|
+
else
|
|
200
|
+
for (let s = 0, o = 0; o < data.length; s += 4, o++)
|
|
201
|
+
data[o] = rgba[s];
|
|
159
202
|
return data;
|
|
160
203
|
}
|
|
161
204
|
};
|
|
@@ -179,7 +222,7 @@ async function compressRows(data, width, height, channels, depth, level, filterR
|
|
|
179
222
|
}
|
|
180
223
|
return compressAsync(filtered, level);
|
|
181
224
|
}
|
|
182
|
-
export async function encodeAnimation(frames, width, height, plays, level) {
|
|
225
|
+
export async function encodeAnimation(frames, width, height, plays, level, text) {
|
|
183
226
|
if (!frames.length)
|
|
184
227
|
throw new Error('PNG: an animation needs at least one frame');
|
|
185
228
|
const first = frames[0];
|
|
@@ -212,6 +255,7 @@ export async function encodeAnimation(frames, width, height, plays, level) {
|
|
|
212
255
|
chunks.push(chunk(PLTE, palette));
|
|
213
256
|
if (transparency)
|
|
214
257
|
chunks.push(chunk(tRNS, transparency));
|
|
258
|
+
chunks.push(...textChunks(text));
|
|
215
259
|
let sequence = 0;
|
|
216
260
|
for (let i = 0; i < frames.length; i++) {
|
|
217
261
|
const f = frames[i];
|
|
@@ -232,7 +276,7 @@ export async function encodeAnimation(frames, width, height, plays, level) {
|
|
|
232
276
|
v.setUint16(22, 100);
|
|
233
277
|
}
|
|
234
278
|
fctl[24] = f.dispose;
|
|
235
|
-
fctl[25] = 1;
|
|
279
|
+
fctl[25] = f.blend === false ? 0 : 1;
|
|
236
280
|
chunks.push(chunk(fcTL, fctl));
|
|
237
281
|
const data = await compressRows(format.convert(images[i], f.width, f.height), f.width, f.height, channels, depth, level, filterRows);
|
|
238
282
|
if (i === 0)
|
|
@@ -552,7 +596,8 @@ function unfilter(data, offset, height, rowBytes, bpp) {
|
|
|
552
596
|
for (let i = row + bpp; i < row + rowBytes; i++) {
|
|
553
597
|
const a = out[i - bpp], b = out[i - rowBytes], c = out[i - rowBytes - bpp];
|
|
554
598
|
const pa = Math.abs(b - c), pb = Math.abs(a - c), pc = Math.abs(a + b - 2 * c);
|
|
555
|
-
|
|
599
|
+
const notA = (pb - pa | pc - pa) >> 31, takeC = (pc - pb) >> 31;
|
|
600
|
+
out[i] = data[src + i] + (a ^ (a ^ b ^ (b ^ c) & takeC) & notA);
|
|
556
601
|
}
|
|
557
602
|
}
|
|
558
603
|
}
|
|
@@ -614,6 +659,20 @@ function createConverter(colorType, depth, palette, transparency) {
|
|
|
614
659
|
const key = transparency && transparency.length >= 6
|
|
615
660
|
? [transparency[0] << 8 | transparency[1], transparency[2] << 8 | transparency[3], transparency[4] << 8 | transparency[5]].map(v => v & maxValue)
|
|
616
661
|
: null;
|
|
662
|
+
if (depth === 8) {
|
|
663
|
+
const [kr, kg, kb] = key || [-1, -1, -1];
|
|
664
|
+
return (pixels, rowBytes, w, h, out, place) => {
|
|
665
|
+
for (let y = 0; y < h; y++) {
|
|
666
|
+
for (let x = 0, s = y * rowBytes, o = place.row(y), step = place.step; x < w; x++, s += 3, o += step) {
|
|
667
|
+
const r = pixels[s], g = pixels[s + 1], b = pixels[s + 2];
|
|
668
|
+
out[o] = r;
|
|
669
|
+
out[o + 1] = g;
|
|
670
|
+
out[o + 2] = b;
|
|
671
|
+
out[o + 3] = r === kr && g === kg && b === kb ? 0 : 0xff;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
}
|
|
617
676
|
return (pixels, rowBytes, w, h, out, place) => {
|
|
618
677
|
for (let y = 0; y < h; y++) {
|
|
619
678
|
const row = pixels.subarray(y * rowBytes, (y + 1) * rowBytes);
|
|
@@ -645,9 +704,14 @@ function createConverter(colorType, depth, palette, transparency) {
|
|
|
645
704
|
return (pixels, rowBytes, w, h, out, place) => {
|
|
646
705
|
if (place.step === 4 && w * 4 === rowBytes && place.row(0) === 0 && place.row(1) === rowBytes)
|
|
647
706
|
return out.set(pixels);
|
|
648
|
-
for (let y = 0; y < h; y++)
|
|
649
|
-
for (let x = 0, o = place.row(y); x < w; x++, o +=
|
|
650
|
-
out
|
|
707
|
+
for (let y = 0; y < h; y++) {
|
|
708
|
+
for (let x = 0, s = y * rowBytes, o = place.row(y), step = place.step; x < w; x++, s += 4, o += step) {
|
|
709
|
+
out[o] = pixels[s];
|
|
710
|
+
out[o + 1] = pixels[s + 1];
|
|
711
|
+
out[o + 2] = pixels[s + 2];
|
|
712
|
+
out[o + 3] = pixels[s + 3];
|
|
713
|
+
}
|
|
714
|
+
}
|
|
651
715
|
};
|
|
652
716
|
}
|
|
653
717
|
return (pixels, rowBytes, w, h, out, place) => {
|
package/dist/codecs/quantize.js
CHANGED
|
@@ -9,9 +9,9 @@ export function dominantColor(pixels, include, clusters = 8) {
|
|
|
9
9
|
best = i;
|
|
10
10
|
return [Math.round(palette[best * 3]), Math.round(palette[best * 3 + 1]), Math.round(palette[best * 3 + 2])];
|
|
11
11
|
}
|
|
12
|
-
export function quantize(pixels, width, { maxColors = 256, quality = 95 } = {}) {
|
|
12
|
+
export function quantize(pixels, width, { maxColors = 256, quality = 95, transparent = false } = {}) {
|
|
13
13
|
const length = pixels.length >> 2;
|
|
14
|
-
let hasTransparency =
|
|
14
|
+
let hasTransparency = transparent;
|
|
15
15
|
for (let i = 3; i < pixels.length; i += 4)
|
|
16
16
|
if (pixels[i] < 128) {
|
|
17
17
|
hasTransparency = true;
|
package/dist/codecs/tga.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { encode as utf8 } from "../util/utf8.js";
|
|
1
2
|
const SIGNATURE = 'TRUEVISION-XFILE.\0';
|
|
2
3
|
function readHeader(d) {
|
|
3
4
|
if (d.length < 18)
|
|
@@ -147,7 +148,38 @@ export function decode(d) {
|
|
|
147
148
|
}
|
|
148
149
|
return { width, height, pixels: stored };
|
|
149
150
|
}
|
|
150
|
-
|
|
151
|
+
function pieces(value, max) {
|
|
152
|
+
const bytes = utf8(value);
|
|
153
|
+
const out = [];
|
|
154
|
+
for (let start = 0; start < bytes.length;) {
|
|
155
|
+
let end = Math.min(bytes.length, start + max);
|
|
156
|
+
while (end < bytes.length && end > start && (bytes[end] & 0xc0) === 0x80)
|
|
157
|
+
end--;
|
|
158
|
+
out.push(bytes.subarray(start, end));
|
|
159
|
+
start = end;
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
function extensionArea({ author, comment, time, software }, alpha) {
|
|
164
|
+
const area = new Uint8Array(495);
|
|
165
|
+
const view = new DataView(area.buffer);
|
|
166
|
+
view.setUint16(0, 495, true);
|
|
167
|
+
if (author)
|
|
168
|
+
area.set(pieces(author, 40)[0], 2);
|
|
169
|
+
if (comment) {
|
|
170
|
+
const lines = comment.split(/\r?\n/).flatMap(line => line ? pieces(line, 80) : [new Uint8Array(0)]).slice(0, 4);
|
|
171
|
+
lines.forEach((line, i) => area.set(line, 43 + i * 81));
|
|
172
|
+
}
|
|
173
|
+
if (time)
|
|
174
|
+
[time.getUTCMonth() + 1, time.getUTCDate(), time.getUTCFullYear(), time.getUTCHours(), time.getUTCMinutes(), time.getUTCSeconds()]
|
|
175
|
+
.forEach((v, i) => view.setUint16(367 + i * 2, v, true));
|
|
176
|
+
if (software)
|
|
177
|
+
area.set(pieces(software, 40)[0], 426);
|
|
178
|
+
area[469] = 0x20;
|
|
179
|
+
area[494] = alpha ? 3 : 0;
|
|
180
|
+
return area;
|
|
181
|
+
}
|
|
182
|
+
export function encode(rgba, width, height, extension) {
|
|
151
183
|
if (width < 1 || height < 1 || width > 65535 || height > 65535)
|
|
152
184
|
throw new RangeError('TGA: dimensions must be 1-65535');
|
|
153
185
|
let opaque = true;
|
|
@@ -161,7 +193,7 @@ export function encode(rgba, width, height) {
|
|
|
161
193
|
if (rgba.byteOffset % 4)
|
|
162
194
|
rgba = rgba.slice(0, count * 4);
|
|
163
195
|
const pixels = new Uint32Array(rgba.buffer, rgba.byteOffset, count);
|
|
164
|
-
const out = new Uint8Array(18 + count * bpp + Math.ceil(
|
|
196
|
+
const out = new Uint8Array(18 + count * bpp + height * Math.ceil(width / 128) + 495 + 26);
|
|
165
197
|
out[2] = 10;
|
|
166
198
|
out[12] = width & 255;
|
|
167
199
|
out[13] = width >> 8;
|
|
@@ -179,8 +211,9 @@ export function encode(rgba, width, height) {
|
|
|
179
211
|
out[o++] = rgba[p + 3];
|
|
180
212
|
};
|
|
181
213
|
for (let i = 0; i < count;) {
|
|
214
|
+
const end = (Math.floor(i / width) + 1) * width;
|
|
182
215
|
let run = 1;
|
|
183
|
-
while (run < 128 && i + run <
|
|
216
|
+
while (run < 128 && i + run < end && pixels[i + run] === pixels[i])
|
|
184
217
|
run++;
|
|
185
218
|
if (run >= 2) {
|
|
186
219
|
out[o++] = 128 | run - 1;
|
|
@@ -189,13 +222,23 @@ export function encode(rgba, width, height) {
|
|
|
189
222
|
continue;
|
|
190
223
|
}
|
|
191
224
|
let raw = 1;
|
|
192
|
-
while (raw < 128 && i + raw <
|
|
225
|
+
while (raw < 128 && i + raw < end && !(i + raw + 1 < end && pixels[i + raw] === pixels[i + raw + 1]))
|
|
193
226
|
raw++;
|
|
194
227
|
out[o++] = raw - 1;
|
|
195
228
|
for (let k = 0; k < raw; k++)
|
|
196
229
|
put(i + k);
|
|
197
230
|
i += raw;
|
|
198
231
|
}
|
|
232
|
+
let extensionOffset = 0;
|
|
233
|
+
if (extension && (extension.author || extension.comment || extension.time || extension.software)) {
|
|
234
|
+
extensionOffset = o;
|
|
235
|
+
out.set(extensionArea(extension, bpp === 4), o);
|
|
236
|
+
o += 495;
|
|
237
|
+
}
|
|
238
|
+
out[o] = extensionOffset & 255;
|
|
239
|
+
out[o + 1] = extensionOffset >> 8 & 255;
|
|
240
|
+
out[o + 2] = extensionOffset >> 16 & 255;
|
|
241
|
+
out[o + 3] = extensionOffset >>> 24;
|
|
199
242
|
o += 8;
|
|
200
243
|
for (let i = 0; i < SIGNATURE.length; i++)
|
|
201
244
|
out[o++] = SIGNATURE.charCodeAt(i);
|
package/dist/codecs/vp8l.js
CHANGED
|
@@ -293,30 +293,24 @@ function decodeImage(reader, width, height, isMain) {
|
|
|
293
293
|
return pixels;
|
|
294
294
|
}
|
|
295
295
|
const average2 = (a, b) => (((a ^ b) & 0xfefefefe) >>> 1) + (a & b) >>> 0;
|
|
296
|
+
const clamp255 = (v) => v < 0 ? 0 : v > 255 ? 255 : v;
|
|
296
297
|
function clampAddSubtractFull(a, b, c) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
return out >>> 0;
|
|
298
|
+
return (clamp255((a & 255) + (b & 255) - (c & 255))
|
|
299
|
+
| clamp255((a >>> 8 & 255) + (b >>> 8 & 255) - (c >>> 8 & 255)) << 8
|
|
300
|
+
| clamp255((a >>> 16 & 255) + (b >>> 16 & 255) - (c >>> 16 & 255)) << 16
|
|
301
|
+
| clamp255((a >>> 24) + (b >>> 24) - (c >>> 24)) << 24) >>> 0;
|
|
303
302
|
}
|
|
304
303
|
function clampAddSubtractHalf(a, b) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
return out >>> 0;
|
|
304
|
+
const a0 = a & 255, a1 = a >>> 8 & 255, a2 = a >>> 16 & 255, a3 = a >>> 24;
|
|
305
|
+
return (clamp255(a0 + ((a0 - (b & 255)) / 2 | 0))
|
|
306
|
+
| clamp255(a1 + ((a1 - (b >>> 8 & 255)) / 2 | 0)) << 8
|
|
307
|
+
| clamp255(a2 + ((a2 - (b >>> 16 & 255)) / 2 | 0)) << 16
|
|
308
|
+
| clamp255(a3 + ((a3 - (b >>> 24)) / 2 | 0)) << 24) >>> 0;
|
|
312
309
|
}
|
|
313
310
|
function select(l, t, tl) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
pl += Math.abs(tc - tlc);
|
|
318
|
-
pt += Math.abs(lc - tlc);
|
|
319
|
-
}
|
|
311
|
+
const tl0 = tl & 255, tl1 = tl >>> 8 & 255, tl2 = tl >>> 16 & 255, tl3 = tl >>> 24;
|
|
312
|
+
const pl = Math.abs((t & 255) - tl0) + Math.abs((t >>> 8 & 255) - tl1) + Math.abs((t >>> 16 & 255) - tl2) + Math.abs((t >>> 24) - tl3);
|
|
313
|
+
const pt = Math.abs((l & 255) - tl0) + Math.abs((l >>> 8 & 255) - tl1) + Math.abs((l >>> 16 & 255) - tl2) + Math.abs((l >>> 24) - tl3);
|
|
320
314
|
return pl < pt ? l : t;
|
|
321
315
|
}
|
|
322
316
|
const addPixels = (a, b) => ((a & 0xff00ff00) + (b & 0xff00ff00) & 0xff00ff00 | (a & 0x00ff00ff) + (b & 0x00ff00ff) & 0x00ff00ff) >>> 0;
|
|
@@ -712,17 +706,24 @@ function tokenize(pixels, width, cacheBits) {
|
|
|
712
706
|
let cached = 0;
|
|
713
707
|
for (let i = 0; i < n;) {
|
|
714
708
|
let bestLength = 0, bestDistance = 0;
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
continue;
|
|
718
|
-
const l = matchLength(i, i - d);
|
|
709
|
+
if (i >= 1) {
|
|
710
|
+
const l = matchLength(i, i - 1);
|
|
719
711
|
if (l > bestLength) {
|
|
720
712
|
bestLength = l;
|
|
721
|
-
bestDistance =
|
|
713
|
+
bestDistance = 1;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (width <= i) {
|
|
717
|
+
const l = matchLength(i, i - width);
|
|
718
|
+
if (l > bestLength) {
|
|
719
|
+
bestLength = l;
|
|
720
|
+
bestDistance = width;
|
|
722
721
|
}
|
|
723
722
|
}
|
|
724
723
|
if (i + 1 < n && bestLength < MAX_LENGTH) {
|
|
725
724
|
for (let j = head[hash(i)], k = 0; j >= 0 && k < CHAIN && i - j <= WINDOW; j = chain[j], k++) {
|
|
725
|
+
if (i - j >= bestDistance && (i + bestLength >= n || pixels[j + bestLength] !== pixels[i + bestLength]))
|
|
726
|
+
continue;
|
|
726
727
|
const l = matchLength(i, j);
|
|
727
728
|
if (l > bestLength || l === bestLength && l > 0 && i - j < bestDistance) {
|
|
728
729
|
bestLength = l;
|
|
@@ -810,12 +811,8 @@ function writeImage(w, pixels, width, isMain) {
|
|
|
810
811
|
}
|
|
811
812
|
const subPixels = (a, b) => ((a | 0x00ff00ff) - (b & 0xff00ff00) & 0xff00ff00 | (a | 0xff00ff00) - (b & 0x00ff00ff) & 0x00ff00ff) >>> 0;
|
|
812
813
|
function residualCost(r) {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
const c = r >>> shift & 255;
|
|
816
|
-
cost += c < 128 ? c : 256 - c;
|
|
817
|
-
}
|
|
818
|
-
return cost;
|
|
814
|
+
const c0 = r & 255, c1 = r >>> 8 & 255, c2 = r >>> 16 & 255, c3 = r >>> 24;
|
|
815
|
+
return (c0 < 128 ? c0 : 256 - c0) + (c1 < 128 ? c1 : 256 - c1) + (c2 < 128 ? c2 : 256 - c2) + (c3 < 128 ? c3 : 256 - c3);
|
|
819
816
|
}
|
|
820
817
|
function applyPredictor(p, width, height, bits) {
|
|
821
818
|
const blocksX = subSize(width, bits), blocksY = subSize(height, bits);
|
package/dist/codecs/webp.js
CHANGED
|
@@ -239,21 +239,32 @@ function toARGB(rgba, count) {
|
|
|
239
239
|
return argb;
|
|
240
240
|
}
|
|
241
241
|
const set24 = (d, o, v) => { d[o] = v & 255; d[o + 1] = v >> 8 & 255; d[o + 2] = v >> 16 & 255; };
|
|
242
|
-
|
|
243
|
-
|
|
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)]);
|
|
244
257
|
}
|
|
245
|
-
export function encodeAnimation(frames, width, height, plays) {
|
|
258
|
+
export function encodeAnimation(frames, width, height, plays, exif) {
|
|
246
259
|
if (!frames.length)
|
|
247
260
|
throw new Error('WebP: an animation needs at least one frame');
|
|
248
261
|
if (width > 1 << 24 || height > 1 << 24)
|
|
249
262
|
throw new RangeError('WebP: canvas too large');
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (f.pixels[i] !== 255)
|
|
254
|
-
alpha = true;
|
|
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));
|
|
255
266
|
const vp8x = new Uint8Array(10);
|
|
256
|
-
vp8x[0] = 2 | (alpha ? 16 : 0);
|
|
267
|
+
vp8x[0] = 2 | (alpha ? 16 : 0) | (exif ? 8 : 0);
|
|
257
268
|
set24(vp8x, 4, width - 1);
|
|
258
269
|
set24(vp8x, 7, height - 1);
|
|
259
270
|
const anim = new Uint8Array(6);
|
|
@@ -270,11 +281,13 @@ export function encodeAnimation(frames, width, height, plays) {
|
|
|
270
281
|
set24(header, 6, f.width - 1);
|
|
271
282
|
set24(header, 9, f.height - 1);
|
|
272
283
|
set24(header, 12, Math.min(0xffffff, Math.max(0, Math.round(f.duration))));
|
|
273
|
-
header[15] = f.dispose ? 1 : 0;
|
|
284
|
+
header[15] = (f.blend === false ? 2 : 0) | (f.dispose ? 1 : 0);
|
|
274
285
|
const body = new Uint8Array(16 + bitstream.length);
|
|
275
286
|
body.set(header);
|
|
276
287
|
body.set(bitstream, 16);
|
|
277
288
|
chunks.push(riffChunk('ANMF', body));
|
|
278
289
|
}
|
|
290
|
+
if (exif)
|
|
291
|
+
chunks.push(riffChunk('EXIF', exif));
|
|
279
292
|
return riff(chunks);
|
|
280
293
|
}
|
package/dist/frame.d.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
import { Image, ResizeMode } from './image.js';
|
|
1
|
+
import { Image, ResizeMode, ResizeOptions, Color, TrimOptions } from './image.js';
|
|
2
2
|
/** Names of the frame disposal modes, see {@link Frame.disposalMode} */
|
|
3
3
|
export type FrameDisposalModeName = 'any' | 'keep' | 'previous' | 'background';
|
|
4
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
18
|
* Represents a frame of an {@link Animation}
|
|
8
19
|
*/
|
|
@@ -24,6 +35,11 @@ export declare class Frame extends Image {
|
|
|
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,6 +1,6 @@
|
|
|
1
1
|
import { BinaryData } from './util/mem.js';
|
|
2
2
|
import { Frame } from './frame.js';
|
|
3
|
-
import { Animation } from './animation.js';
|
|
3
|
+
import { Animation, AnimationDecodeOptions, AnimationOptions, GIFEncodeOptions } from './animation.js';
|
|
4
4
|
/**
|
|
5
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.
|
|
@@ -11,21 +11,24 @@ 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
|
+
constructor(frames: Frame[], loopCountOrOptions?: number | AnimationOptions);
|
|
17
18
|
toString(): string;
|
|
18
19
|
/**
|
|
19
20
|
* Encodes the GIF
|
|
20
|
-
* @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}
|
|
21
23
|
* @deprecated Use {@link Animation.encodeGIF}
|
|
22
24
|
*/
|
|
23
|
-
encode(
|
|
25
|
+
encode(qualityOrOptions?: number | GIFEncodeOptions): Promise<Uint8Array>;
|
|
24
26
|
/**
|
|
25
27
|
* Decodes a GIF into fully composited frames
|
|
26
28
|
* @param input The binary data to decode
|
|
27
|
-
* @param
|
|
29
|
+
* @param onlyFirstFrameOrOptions Whether to end GIF decoding after the first frame, or see
|
|
30
|
+
* {@link AnimationDecodeOptions}
|
|
28
31
|
* @deprecated Use {@link Animation.decode}, which decodes APNGs too
|
|
29
32
|
*/
|
|
30
|
-
static decode(input: BinaryData,
|
|
33
|
+
static decode(input: BinaryData, onlyFirstFrameOrOptions?: boolean | AnimationDecodeOptions): Promise<GIF>;
|
|
31
34
|
}
|
package/dist/gif.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { view } from "./util/mem.js";
|
|
2
2
|
import { ImageType } from "./image-type.js";
|
|
3
3
|
import { Animation } from "./animation.js";
|
|
4
|
+
import { isOptions } from "./util/options.js";
|
|
4
5
|
export class GIF extends Animation {
|
|
5
|
-
constructor(frames,
|
|
6
|
-
super(frames,
|
|
6
|
+
constructor(frames, loopCountOrOptions) {
|
|
7
|
+
super(frames, loopCountOrOptions);
|
|
7
8
|
}
|
|
8
9
|
toString() {
|
|
9
10
|
return `GIF<${this.width}x${this.height}x${this.duration}ms>`;
|
|
10
11
|
}
|
|
11
|
-
async encode(
|
|
12
|
-
return this.encodeGIF(
|
|
12
|
+
async encode(qualityOrOptions) {
|
|
13
|
+
return this.encodeGIF(qualityOrOptions);
|
|
13
14
|
}
|
|
14
|
-
static async decode(input,
|
|
15
|
+
static async decode(input, onlyFirstFrameOrOptions = false) {
|
|
16
|
+
const onlyExtractFirstFrame = isOptions(onlyFirstFrameOrOptions) ? onlyFirstFrameOrOptions.onlyFirstFrame ?? false : onlyFirstFrameOrOptions;
|
|
15
17
|
const data = view(input);
|
|
16
18
|
if (!ImageType.isGIF(new DataView(data.buffer, data.byteOffset, data.byteLength)))
|
|
17
19
|
throw new Error('Unsupported image type');
|
package/dist/image-type.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { BinaryData } from './util/mem.js';
|
|
|
3
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
|
package/dist/image-type.js
CHANGED