imagescript 1.2.15 → 1.2.17
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/ImageScript.d.ts +233 -223
- package/ImageScript.js +1658 -1646
- package/LICENSE +12 -619
- package/LICENSE.AGPL-3.0 +619 -0
- package/LICENSE.MIT +19 -0
- package/README.md +51 -46
- package/bench/_.mjs +165 -165
- package/bench/all.mjs +3 -3
- package/bench/blur.mjs +58 -58
- package/bench/flip.mjs +62 -62
- package/bench/overlay.mjs +34 -34
- package/bench/package.json +6 -6
- package/bench/readme.md +63 -63
- package/bench/resize.mjs +51 -51
- package/codecs/LICENSE +20 -20
- package/codecs/lib.d.ts +49 -49
- package/codecs/lib.js +5 -5
- package/codecs/node/index.js +2 -2
- package/codecs/node/index.mjs +5 -5
- package/codecs/package.json +13 -13
- package/codecs/readme.md +37 -37
- package/package.json +41 -41
- package/png/node.js +1137 -1137
- package/png/src/crc.mjs +48 -48
- package/png/src/mem.mjs +23 -23
- package/png/src/png.mjs +254 -254
- package/png/src/zlib.mjs +617 -617
- package/tsconfig.json +21 -21
- package/utils/mem.js +25 -25
- package/v2/codecs/magic.js +24 -24
- package/v2/framebuffer.d.ts +66 -66
- package/v2/framebuffer.js +2352 -2352
- package/v2/framebuffer.mjs +125 -125
- package/v2/framebuffer.rs +1263 -1263
- package/v2/index.d.ts +83 -83
- package/v2/index.js +260 -260
- package/v2/ops/blur.mjs +302 -302
- package/v2/ops/color.mjs +276 -276
- package/v2/ops/crop.mjs +63 -63
- package/v2/ops/fill.mjs +32 -32
- package/v2/ops/flip.mjs +32 -32
- package/v2/ops/index.mjs +8 -8
- package/v2/ops/iterator.mjs +33 -33
- package/v2/ops/overlay.mjs +62 -62
- package/v2/ops/resize.mjs +165 -165
- package/v2/ops/rotate.mjs +116 -116
- package/v2/util/mem.mjs +23 -23
- package/wasm/node/font.js +141 -141
- package/wasm/node/gif.js +76 -76
- package/wasm/node/jpeg.js +91 -91
- package/wasm/node/png.js +49 -49
- package/wasm/node/svg.js +55 -55
- package/wasm/node/tiff.js +46 -46
package/ImageScript.js
CHANGED
|
@@ -1,1646 +1,1658 @@
|
|
|
1
|
-
const png = require('./png/node.js');
|
|
2
|
-
const mem = require('./utils/mem.js');
|
|
3
|
-
const {version} = require('./package.json');
|
|
4
|
-
const codecs = require('./codecs/node/index.js');
|
|
5
|
-
const { default: v2 } = require('./v2/framebuffer.js');
|
|
6
|
-
|
|
7
|
-
// old
|
|
8
|
-
const svglib = require('./wasm/node/svg.js');
|
|
9
|
-
const giflib = require('./wasm/node/gif.js');
|
|
10
|
-
const pnglib = require('./wasm/node/png.js');
|
|
11
|
-
const fontlib = require('./wasm/node/font.js');
|
|
12
|
-
const jpeglib = require('./wasm/node/jpeg.js');
|
|
13
|
-
const tifflib = require('./wasm/node/tiff.js');
|
|
14
|
-
|
|
15
|
-
const MAGIC_NUMBERS = {
|
|
16
|
-
PNG: 0x89504e47,
|
|
17
|
-
JPEG: 0xffd8ff,
|
|
18
|
-
TIFF: 0x49492a00,
|
|
19
|
-
GIF: 0x474946
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Represents an image; provides utility functions
|
|
24
|
-
*/
|
|
25
|
-
class Image {
|
|
26
|
-
/**
|
|
27
|
-
* Creates a new image with the given dimensions
|
|
28
|
-
* @param {number} width
|
|
29
|
-
* @param {number} height
|
|
30
|
-
* @returns {Image}
|
|
31
|
-
*/
|
|
32
|
-
constructor(width, height) {
|
|
33
|
-
width = ~~width;
|
|
34
|
-
height = ~~height;
|
|
35
|
-
|
|
36
|
-
if (width < 1)
|
|
37
|
-
throw new RangeError('Image has to be at least 1 pixel wide');
|
|
38
|
-
if (height < 1)
|
|
39
|
-
throw new RangeError('Image has to be at least 1 pixel high');
|
|
40
|
-
|
|
41
|
-
/** @private */
|
|
42
|
-
this.__width__ = width;
|
|
43
|
-
/** @private */
|
|
44
|
-
this.__height__ = height;
|
|
45
|
-
/** @private */
|
|
46
|
-
this.__buffer__ = new ArrayBuffer(width * height * 4);
|
|
47
|
-
/** @private */
|
|
48
|
-
this.__view__ = new DataView(this.__buffer__);
|
|
49
|
-
/** @private */
|
|
50
|
-
this.__u32__ = new Uint32Array(this.__buffer__);
|
|
51
|
-
/**
|
|
52
|
-
* The images RGBA pixel data
|
|
53
|
-
* @type {Uint8ClampedArray}
|
|
54
|
-
*/
|
|
55
|
-
this.bitmap = new Uint8ClampedArray(this.__buffer__);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* @private
|
|
60
|
-
* @returns {string}
|
|
61
|
-
*/
|
|
62
|
-
toString() {
|
|
63
|
-
return `Image<${this.width}x${this.height}>`;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* The images width
|
|
68
|
-
* @returns {number}
|
|
69
|
-
*/
|
|
70
|
-
get width() {
|
|
71
|
-
return this.__width__;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* The images height
|
|
76
|
-
* @returns {number}
|
|
77
|
-
*/
|
|
78
|
-
get height() {
|
|
79
|
-
return this.__height__;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Yields an [x, y] array for every pixel in the image
|
|
84
|
-
* @yields {number[]} The coordinates of the pixel ([x, y])
|
|
85
|
-
* @returns {void}
|
|
86
|
-
*/
|
|
87
|
-
*[Symbol.iterator]() {
|
|
88
|
-
for (const x of new v2(this.width, this.height, this.bitmap)[Symbol.iterator]()) yield (x[0]++, x[1]++, x)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Yields an [x, y, color] array for every pixel in the image
|
|
93
|
-
* @yields {number[]} The coordinates and color of the pixel ([x, y, color])
|
|
94
|
-
*/
|
|
95
|
-
*iterateWithColors() {
|
|
96
|
-
for (const x of new v2(this.width, this.height, this.bitmap).pixels('int')) yield (x[0]++, x[1]++, x);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Converts RGBA components to an RGBA value
|
|
101
|
-
* @param {number} r red (0..255)
|
|
102
|
-
* @param {number} g green (0..255)
|
|
103
|
-
* @param {number} b blue (0..255)
|
|
104
|
-
* @param {number} a alpha (0..255)
|
|
105
|
-
* @returns {number} RGBA value
|
|
106
|
-
*/
|
|
107
|
-
static rgbaToColor(r, g, b, a) {
|
|
108
|
-
return (((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | (a & 0xff)) >>> 0;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Converts RGB components to an RGBA value (assuming alpha = 255)
|
|
113
|
-
* @param {number} r red (0..255)
|
|
114
|
-
* @param {number} g green (0..255)
|
|
115
|
-
* @param {number} b blue (0..255)
|
|
116
|
-
* @returns {number} RGBA value
|
|
117
|
-
*/
|
|
118
|
-
static rgbToColor(r, g, b) {
|
|
119
|
-
return Image.rgbaToColor(r, g, b, 0xff);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Converts HSLA colors to RGBA colors
|
|
124
|
-
* @param {number} h hue (0..1)
|
|
125
|
-
* @param {number} s saturation (0..1)
|
|
126
|
-
* @param {number} l lightness (0..1)
|
|
127
|
-
* @param {number} a opacity (0..1)
|
|
128
|
-
* @returns {number} color
|
|
129
|
-
*/
|
|
130
|
-
static hslaToColor(h, s, l, a) {
|
|
131
|
-
h %= 1;
|
|
132
|
-
s = Math.min(1, Math.max(0, s));
|
|
133
|
-
l = Math.min(1, Math.max(0, l));
|
|
134
|
-
a = Math.min(1, Math.max(0, a));
|
|
135
|
-
|
|
136
|
-
let r, g, b;
|
|
137
|
-
|
|
138
|
-
if (s === 0) {
|
|
139
|
-
r = g = b = l;
|
|
140
|
-
} else {
|
|
141
|
-
const hue2rgb = (p, q, t) => {
|
|
142
|
-
if (t < 0) t += 1;
|
|
143
|
-
if (t > 1) t -= 1;
|
|
144
|
-
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
145
|
-
if (t < 1 / 2) return q;
|
|
146
|
-
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
147
|
-
return p;
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
151
|
-
const p = 2 * l - q;
|
|
152
|
-
|
|
153
|
-
r = hue2rgb(p, q, h + 1 / 3);
|
|
154
|
-
g = hue2rgb(p, q, h);
|
|
155
|
-
b = hue2rgb(p, q, h - 1 / 3);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return Image.rgbaToColor(r * 255, g * 255, b * 255, a * 255);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Converts HSL colors to RGBA colors (assuming an opacity of 255)
|
|
163
|
-
* @param {number} h hue (0..1)
|
|
164
|
-
* @param {number} s saturation (0..1)
|
|
165
|
-
* @param {number} l lightness (0..1)
|
|
166
|
-
* @returns {number} color
|
|
167
|
-
*/
|
|
168
|
-
static hslToColor(h, s, l) {
|
|
169
|
-
return Image.hslaToColor(h, s, l, 1);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Converts an RGBA value to an array of HSLA values
|
|
174
|
-
* @param r {number} (0..255)
|
|
175
|
-
* @param g {number} (0..255)
|
|
176
|
-
* @param b {number} (0..255)
|
|
177
|
-
* @param a {number} (0..255)
|
|
178
|
-
* @returns {number[]} The HSLA values ([H, S, L, A])
|
|
179
|
-
*/
|
|
180
|
-
static rgbaToHSLA(r, g, b, a) {
|
|
181
|
-
r /= 255;
|
|
182
|
-
g /= 255;
|
|
183
|
-
b /= 255;
|
|
184
|
-
|
|
185
|
-
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
186
|
-
let h, s, l = (max + min) / 2;
|
|
187
|
-
|
|
188
|
-
if (max === min) {
|
|
189
|
-
h = s = 0;
|
|
190
|
-
} else {
|
|
191
|
-
const d = max - min;
|
|
192
|
-
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
193
|
-
switch (max) {
|
|
194
|
-
case r:
|
|
195
|
-
h = (g - b) / d + (g < b ? 6 : 0);
|
|
196
|
-
break;
|
|
197
|
-
case g:
|
|
198
|
-
h = (b - r) / d + 2;
|
|
199
|
-
break;
|
|
200
|
-
case b:
|
|
201
|
-
h = (r - g) / d + 4;
|
|
202
|
-
break;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
h /= 6;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return [h, s, l, a / 255];
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Converts a color value to an array of RGBA values
|
|
213
|
-
* @param {number} color The color value to convert
|
|
214
|
-
* @returns {number[]} The RGBA values ([R, G, B, A])
|
|
215
|
-
*/
|
|
216
|
-
static colorToRGBA(color) {
|
|
217
|
-
return [(color >> 24) & 0xff, (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff];
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Converts a color value to an array of RGB values (ignoring the colors alpha)
|
|
222
|
-
* @param {number} color The color value to convert
|
|
223
|
-
* @returns {number[]} The RGB values ([R, G, B])
|
|
224
|
-
*/
|
|
225
|
-
static colorToRGB(color) {
|
|
226
|
-
return Image.colorToRGBA(color).slice(0, 3);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Gets the pixel color at the specified position
|
|
231
|
-
* @param {number} x
|
|
232
|
-
* @param {number} y
|
|
233
|
-
* @returns {number} The color value
|
|
234
|
-
*/
|
|
235
|
-
getPixelAt(x, y) {
|
|
236
|
-
this.__check_boundaries__(x, y);
|
|
237
|
-
return this.__view__.getUint32(((~~y - 1) * this.width + (~~x - 1)) * 4, false);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Gets the pixel color at the specified position
|
|
242
|
-
* @param {number} x
|
|
243
|
-
* @param {number} y
|
|
244
|
-
* @returns {Uint8ClampedArray} The RGBA value
|
|
245
|
-
*/
|
|
246
|
-
getRGBAAt(x, y) {
|
|
247
|
-
this.__check_boundaries__(x, y);
|
|
248
|
-
const idx = ((~~y - 1) * this.width + (~~x - 1)) * 4;
|
|
249
|
-
return this.bitmap.subarray(idx, idx + 4);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Sets the pixel color for the specified position
|
|
254
|
-
* @param {number} x
|
|
255
|
-
* @param {number} y
|
|
256
|
-
* @param {number} pixelColor
|
|
257
|
-
*/
|
|
258
|
-
setPixelAt(x, y, pixelColor) {
|
|
259
|
-
x = ~~x;
|
|
260
|
-
y = ~~y;
|
|
261
|
-
this.__check_boundaries__(x, y);
|
|
262
|
-
this.__set_pixel__(x, y, pixelColor);
|
|
263
|
-
return this;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* @private
|
|
268
|
-
* @param {number} x
|
|
269
|
-
* @param {number} y
|
|
270
|
-
* @param {number} pixelColor
|
|
271
|
-
*/
|
|
272
|
-
__set_pixel__(x, y, pixelColor) {
|
|
273
|
-
this.__view__.setUint32(((y - 1) * this.width + (x - 1)) * 4, pixelColor, false);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* @private
|
|
278
|
-
* @param {number} x
|
|
279
|
-
* @param {number} y
|
|
280
|
-
*/
|
|
281
|
-
__check_boundaries__(x, y) {
|
|
282
|
-
if (isNaN(x)) throw new TypeError(`Invalid pixel coordinates (x=${x})`);
|
|
283
|
-
if (isNaN(y)) throw new TypeError(`Invalid pixel coordinates (y=${y})`);
|
|
284
|
-
if (x < 1)
|
|
285
|
-
throw new RangeError(`${Image.__out_of_bounds__} (x=${x})<1`);
|
|
286
|
-
if (x > this.width)
|
|
287
|
-
throw new RangeError(`${Image.__out_of_bounds__} (x=${x})>(width=${this.width})`);
|
|
288
|
-
if (y < 1)
|
|
289
|
-
throw new RangeError(`${Image.__out_of_bounds__} (y=${y})<1`);
|
|
290
|
-
if (y > this.height)
|
|
291
|
-
throw new RangeError(`${Image.__out_of_bounds__} (y=${y})>(height=${this.height})`);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* @private
|
|
296
|
-
*/
|
|
297
|
-
static get __out_of_bounds__() {
|
|
298
|
-
return 'Tried referencing a pixel outside of the images boundaries:';
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* @callback colorFunction
|
|
303
|
-
* @param {number} x
|
|
304
|
-
* @param {number} y
|
|
305
|
-
* @returns {number} pixel color
|
|
306
|
-
*/
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Fills the image data with the supplied color
|
|
310
|
-
* @param {number|colorFunction} color
|
|
311
|
-
* @returns {Image}
|
|
312
|
-
*/
|
|
313
|
-
fill(color) {
|
|
314
|
-
new v2(this.width, this.height, this.bitmap).fill(color);
|
|
315
|
-
|
|
316
|
-
return this;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Clones the current image
|
|
321
|
-
* @returns {Image}
|
|
322
|
-
*/
|
|
323
|
-
clone() {
|
|
324
|
-
const image = new Image(this.width, this.height);
|
|
325
|
-
image.bitmap.set(this.bitmap);
|
|
326
|
-
return image;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Use {@link https://en.wikipedia.org/wiki/Image_scaling#Nearest-neighbor_interpolation Nearest-neighbor} resizing.
|
|
331
|
-
* @returns {string}
|
|
332
|
-
*/
|
|
333
|
-
static get RESIZE_NEAREST_NEIGHBOR() {
|
|
334
|
-
return 'RESIZE_NEAREST_NEIGHBOR';
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Used for automatically preserving an images aspect ratio when resizing.
|
|
339
|
-
* @returns {number}
|
|
340
|
-
*/
|
|
341
|
-
static get RESIZE_AUTO() {
|
|
342
|
-
return -1;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Resizes the image by the given factor
|
|
347
|
-
* @param {number} factor The factor to resize the image with
|
|
348
|
-
* @param {string} [mode=Image.RESIZE_NEAREST_NEIGHBOR] The resizing mode to use
|
|
349
|
-
* @returns {Image}
|
|
350
|
-
*/
|
|
351
|
-
scale(factor, mode = Image.RESIZE_NEAREST_NEIGHBOR) {
|
|
352
|
-
const image = this.__scale__(factor, mode);
|
|
353
|
-
return this.__apply__(image);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/** @private */
|
|
357
|
-
__scale__(factor, mode = Image.RESIZE_NEAREST_NEIGHBOR) {
|
|
358
|
-
if (factor === 1) return this;
|
|
359
|
-
return this.__resize__(this.width * factor, this.height * factor, mode);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Resizes the image to the given dimensions.
|
|
364
|
-
* Use {@link Image.RESIZE_AUTO} as either width or height to automatically preserve the aspect ratio.
|
|
365
|
-
* @param {number} width The new width
|
|
366
|
-
* @param {number} height The new height
|
|
367
|
-
* @param {string} [mode=Image.RESIZE_NEAREST_NEIGHBOR] The resizing mode to use
|
|
368
|
-
* @returns {Image} The resized image
|
|
369
|
-
*/
|
|
370
|
-
resize(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) {
|
|
371
|
-
const image = this.__resize__(width, height, mode);
|
|
372
|
-
return this.__apply__(image);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Resizes the image so it is contained in the given bounding box.
|
|
377
|
-
* Can return an image with one axis smaller than the given bounding box.
|
|
378
|
-
* @param {number} width The width of the bounding box
|
|
379
|
-
* @param {number} height The height of the bounding box
|
|
380
|
-
* @param {string} [mode=Image.RESIZE_NEAREST_NEIGHBOR] The resizing mode to use
|
|
381
|
-
* @returns {Image} The resized image
|
|
382
|
-
*/
|
|
383
|
-
contain(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) {
|
|
384
|
-
const scaleFactor = width / height > this.width / this.height ? height / this.height : width / this.width;
|
|
385
|
-
return this.scale(scaleFactor, mode);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Resizes the image so it is contained in the given bounding box, placing it in the center of the given bounding box.
|
|
390
|
-
* Always returns the exact dimensions of the bounding box.
|
|
391
|
-
* @param {number} width The width of the bounding box
|
|
392
|
-
* @param {number} height The height of the bounding box
|
|
393
|
-
* @param {string} [mode=Image.RESIZE_NEAREST_NEIGHBOR] The resizing mode to use
|
|
394
|
-
* @returns {Image} The resized image
|
|
395
|
-
*/
|
|
396
|
-
fit(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) {
|
|
397
|
-
const result = new Image(width, height);
|
|
398
|
-
this.contain(width, height, mode);
|
|
399
|
-
result.composite(this, (width - this.width) / 2, (height - this.height) / 2);
|
|
400
|
-
return this.__apply__(result);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Resizes the image so it covers the given bounding box, cropping the overflowing edges.
|
|
405
|
-
* Always returns the exact dimensions of the bounding box.
|
|
406
|
-
* @param {number} width The width of the bounding box
|
|
407
|
-
* @param {number} height The height of the bounding box
|
|
408
|
-
* @param {string} [mode=Image.RESIZE_NEAREST_NEIGHBOR] The resizing mode to use
|
|
409
|
-
* @returns {Image} The resized image
|
|
410
|
-
*/
|
|
411
|
-
cover(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) {
|
|
412
|
-
const scaleFactor = width / height > this.width / this.height ? width / this.width : height / this.height;
|
|
413
|
-
const result = this.scale(scaleFactor, mode);
|
|
414
|
-
return result.crop((result.width - width) / 2, (result.height - height) / 2, width, height);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/** @private */
|
|
418
|
-
__resize__(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) {
|
|
419
|
-
if (width === Image.RESIZE_AUTO && height === Image.RESIZE_AUTO) throw new Error('RESIZE_AUTO can only be used for either width or height, not for both');
|
|
420
|
-
else if (width === Image.RESIZE_AUTO) width = this.width / this.height * height;
|
|
421
|
-
else if (height === Image.RESIZE_AUTO) height = this.height / this.width * width;
|
|
422
|
-
|
|
423
|
-
width = Math.floor(width);
|
|
424
|
-
height = Math.floor(height);
|
|
425
|
-
if (width < 1)
|
|
426
|
-
throw new RangeError('Image has to be at least 1 pixel wide');
|
|
427
|
-
if (height < 1)
|
|
428
|
-
throw new RangeError('Image has to be at least 1 pixel high');
|
|
429
|
-
|
|
430
|
-
let image;
|
|
431
|
-
if (mode === Image.RESIZE_NEAREST_NEIGHBOR)
|
|
432
|
-
image = this.__resize_nearest_neighbor__(width, height);
|
|
433
|
-
else throw new Error('Invalid resize mode');
|
|
434
|
-
|
|
435
|
-
return image;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* @private
|
|
440
|
-
* @param {number} width The new width
|
|
441
|
-
* @param {number} height The new height
|
|
442
|
-
*/
|
|
443
|
-
__resize_nearest_neighbor__(width, height) {
|
|
444
|
-
const image = new this.constructor(width, height);
|
|
445
|
-
const frame = new v2(this.width, this.height, this.bitmap).resize('nearest', width, height);
|
|
446
|
-
|
|
447
|
-
image.bitmap.set(frame.u8);
|
|
448
|
-
|
|
449
|
-
return image;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Crops an image to the specified dimensions
|
|
454
|
-
* @param {number} x The x offset
|
|
455
|
-
* @param {number} y The y offset
|
|
456
|
-
* @param {number} width The new images width
|
|
457
|
-
* @param {number} height The new images height
|
|
458
|
-
* @returns {Image}
|
|
459
|
-
*/
|
|
460
|
-
crop(x, y, width, height) {
|
|
461
|
-
if (width > this.width) width = this.width;
|
|
462
|
-
if (height > this.height) height = this.height;
|
|
463
|
-
|
|
464
|
-
return this.__apply__(this.__crop__(~~x, ~~y, ~~width, ~~height));
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/**
|
|
468
|
-
* @param {number} x
|
|
469
|
-
* @param {number} y
|
|
470
|
-
* @param {number} width
|
|
471
|
-
* @param {number} height
|
|
472
|
-
* @returns {Image}
|
|
473
|
-
* @private
|
|
474
|
-
*/
|
|
475
|
-
__crop__(x, y, width, height) {
|
|
476
|
-
x = ~~x;
|
|
477
|
-
y = ~~y;
|
|
478
|
-
|
|
479
|
-
const image = new this.constructor(width, height);
|
|
480
|
-
|
|
481
|
-
for (let tY = 0; tY < height; tY++) {
|
|
482
|
-
const idx = (tY + y) * this.width + x;
|
|
483
|
-
image.__u32__.set(this.__u32__.subarray(idx, idx + width), tY * width);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
return image;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Draws a box at the specified coordinates
|
|
491
|
-
* @param {number} x The x offset
|
|
492
|
-
* @param {number} y The y offset
|
|
493
|
-
* @param {number} width The box width
|
|
494
|
-
* @param {number} height The box height
|
|
495
|
-
* @param {number|colorFunction} color The color to fill the box in with
|
|
496
|
-
* @returns {Image}
|
|
497
|
-
*/
|
|
498
|
-
drawBox(x, y, width, height, color) {
|
|
499
|
-
x = ~~(x - 1);
|
|
500
|
-
y = ~~(y - 1);
|
|
501
|
-
width = ~~width;
|
|
502
|
-
height = ~~height;
|
|
503
|
-
|
|
504
|
-
if (typeof color === 'function') {
|
|
505
|
-
for (let tY = 1; tY <= height; tY++) {
|
|
506
|
-
for (let tX = 1; tX <= width; tX++) {
|
|
507
|
-
const nX = tX + x;
|
|
508
|
-
const nY = tY + y;
|
|
509
|
-
if (Math.min(nX, nY) < 1 || nX > this.width || nY > this.height)
|
|
510
|
-
continue;
|
|
511
|
-
|
|
512
|
-
const tC = color(tX, tY);
|
|
513
|
-
this.__set_pixel__(nX, nY, tC);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
} else return this.__fast_box__(x, y, width, height, color);
|
|
517
|
-
|
|
518
|
-
return this;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/**
|
|
522
|
-
* @private
|
|
523
|
-
* @param {number} x
|
|
524
|
-
* @param {number} y
|
|
525
|
-
* @param {number} width
|
|
526
|
-
* @param {number} height
|
|
527
|
-
* @param {number} color
|
|
528
|
-
*/
|
|
529
|
-
__fast_box__(x, y, width, height, color) {
|
|
530
|
-
if (x < 0) {
|
|
531
|
-
width += x;
|
|
532
|
-
x = 0;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
if (y < 0) {
|
|
536
|
-
height += y;
|
|
537
|
-
y = 0;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const right = Math.max(Math.min(x + width, this.width), 1);
|
|
541
|
-
let xPos = right;
|
|
542
|
-
while (x <= --xPos)
|
|
543
|
-
this.__view__.setUint32(4 * (xPos + y * this.width), color);
|
|
544
|
-
const end = 4 * (right + y * this.width);
|
|
545
|
-
const start = 4 * (x + y * this.width);
|
|
546
|
-
|
|
547
|
-
let bottom = Math.max(Math.min(y + height, this.height), 1);
|
|
548
|
-
while (y < --bottom)
|
|
549
|
-
this.bitmap.copyWithin(4 * (x + bottom * this.width), start, end);
|
|
550
|
-
|
|
551
|
-
return this;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
/**
|
|
555
|
-
* Draws a circle at the specified coordinates with the specified radius
|
|
556
|
-
* @param {number} x The center x position
|
|
557
|
-
* @param {number} y The center y position
|
|
558
|
-
* @param {number} radius The circles radius
|
|
559
|
-
* @param {number|colorFunction} color
|
|
560
|
-
* @returns {Image}
|
|
561
|
-
*/
|
|
562
|
-
drawCircle(x, y, radius, color) {
|
|
563
|
-
const radSquared = radius ** 2;
|
|
564
|
-
for (let currentY = Math.max(1, y - radius); currentY <= Math.min(y + radius, this.height); currentY++) {
|
|
565
|
-
for (let currentX = Math.max(1, x - radius); currentX <= Math.min(x + radius, this.width); currentX++) {
|
|
566
|
-
if ((currentX - x) ** 2 + (currentY - y) ** 2 < radSquared)
|
|
567
|
-
this.__set_pixel__(currentX, currentY, typeof color === 'function' ? color(currentX - x + radius, currentY - y + radius) : color);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
return this;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
/**
|
|
575
|
-
* Crops the image into a circle
|
|
576
|
-
* @param {boolean} [max=false] Whether to use the larger dimension for the size
|
|
577
|
-
* @param {number} [feathering=0] How much feathering to apply to the edges
|
|
578
|
-
* @returns {Image}
|
|
579
|
-
*/
|
|
580
|
-
cropCircle(max = false, feathering = 0) {
|
|
581
|
-
new v2(this.width, this.height, this.bitmap).crop('circle', feathering);
|
|
582
|
-
|
|
583
|
-
return this;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* Sets the images opacity
|
|
588
|
-
* @param {number} opacity The opacity to apply (0..1)
|
|
589
|
-
* @param {boolean} absolute Whether to scale the current opacity (false) or just set the new opacity (true)
|
|
590
|
-
* @returns {Image}
|
|
591
|
-
*/
|
|
592
|
-
opacity(opacity, absolute = false) {
|
|
593
|
-
if (isNaN(opacity) || opacity < 0)
|
|
594
|
-
throw new RangeError('Invalid opacity value');
|
|
595
|
-
|
|
596
|
-
this.__set_channel_value__(opacity, absolute, 3);
|
|
597
|
-
|
|
598
|
-
return this;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
/**
|
|
602
|
-
* Sets the red channels saturation
|
|
603
|
-
* @param {number} saturation The saturation to apply (0..1)
|
|
604
|
-
* @param {boolean} absolute Whether to scale the current saturation (false) or just set the new saturation (true)
|
|
605
|
-
* @returns {Image}
|
|
606
|
-
*/
|
|
607
|
-
red(saturation, absolute = false) {
|
|
608
|
-
if (isNaN(saturation) || saturation < 0)
|
|
609
|
-
throw new RangeError('Invalid saturation value');
|
|
610
|
-
|
|
611
|
-
this.__set_channel_value__(saturation, absolute, 0);
|
|
612
|
-
|
|
613
|
-
return this;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
/**
|
|
617
|
-
* Sets the green channels saturation
|
|
618
|
-
* @param {number} saturation The saturation to apply (0..1)
|
|
619
|
-
* @param {boolean} absolute Whether to scale the current saturation (false) or just set the new saturation (true)
|
|
620
|
-
* @returns {Image}
|
|
621
|
-
*/
|
|
622
|
-
green(saturation, absolute = false) {
|
|
623
|
-
if (isNaN(saturation) || saturation < 0)
|
|
624
|
-
throw new RangeError('Invalid saturation value');
|
|
625
|
-
|
|
626
|
-
this.__set_channel_value__(saturation, absolute, 1);
|
|
627
|
-
|
|
628
|
-
return this;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* Sets the blue channels saturation
|
|
633
|
-
* @param {number} saturation The saturation to apply (0..1)
|
|
634
|
-
* @param {boolean} absolute Whether to scale the current saturation (false) or just set the new saturation (true)
|
|
635
|
-
* @returns {Image}
|
|
636
|
-
*/
|
|
637
|
-
blue(saturation, absolute = false) {
|
|
638
|
-
if (isNaN(saturation) || saturation < 0)
|
|
639
|
-
throw new RangeError('Invalid saturation value');
|
|
640
|
-
|
|
641
|
-
this.__set_channel_value__(saturation, absolute, 2);
|
|
642
|
-
|
|
643
|
-
return this;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
/**
|
|
647
|
-
* @private
|
|
648
|
-
* @param {number} value
|
|
649
|
-
* @param {boolean} absolute
|
|
650
|
-
* @param {number} offset
|
|
651
|
-
*/
|
|
652
|
-
__set_channel_value__(value, absolute, offset) {
|
|
653
|
-
for (let i = offset; i < this.bitmap.length; i += 4)
|
|
654
|
-
this.bitmap[i] = value * (absolute ? 255 : this.bitmap[i]);
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
* Sets the brightness of the image
|
|
659
|
-
* @param {number} value The lightness to apply (0..1)
|
|
660
|
-
* @param {boolean} absolute Whether to scale the current lightness (false) or just set the new lightness (true)
|
|
661
|
-
* @returns {Image}
|
|
662
|
-
*/
|
|
663
|
-
lightness(value, absolute = false) {
|
|
664
|
-
if (isNaN(value) || value < 0)
|
|
665
|
-
throw new RangeError('Invalid lightness value');
|
|
666
|
-
|
|
667
|
-
return this.fill((x, y) => {
|
|
668
|
-
const [h, s, l, a] = Image.rgbaToHSLA(...this.getRGBAAt(x, y));
|
|
669
|
-
return Image.hslaToColor(h, s, value * (absolute ? 1 : l), a);
|
|
670
|
-
});
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* Sets the saturation of the image
|
|
675
|
-
* @param {number} value The saturation to apply (0..1)
|
|
676
|
-
* @param {boolean} absolute Whether to scale the current saturation (false) or just set the new saturation (true)
|
|
677
|
-
* @returns {Image}
|
|
678
|
-
*/
|
|
679
|
-
saturation(value, absolute = false) {
|
|
680
|
-
if (isNaN(value) || value < 0)
|
|
681
|
-
throw new RangeError('Invalid saturation value');
|
|
682
|
-
|
|
683
|
-
return this.fill((x, y) => {
|
|
684
|
-
const [h, s, l, a] = Image.rgbaToHSLA(...this.getRGBAAt(x, y));
|
|
685
|
-
return Image.hslaToColor(h, value * (absolute ? 1 : s), l, a);
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
/**
|
|
690
|
-
* Composites (overlays) the source onto this image at the specified coordinates
|
|
691
|
-
* @param {Image} source The image to place
|
|
692
|
-
* @param {number} [x=0] The x position to place the image at
|
|
693
|
-
* @param {number} [y=0] The y position to place the image at
|
|
694
|
-
* @returns {Image}
|
|
695
|
-
*/
|
|
696
|
-
composite(source, x = 0, y = 0) {
|
|
697
|
-
new v2(this.width, this.height, this.bitmap).overlay(new v2(source.width, source.height, source.bitmap), x, y);
|
|
698
|
-
|
|
699
|
-
return this;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
/**
|
|
703
|
-
* Inverts the images colors
|
|
704
|
-
* @returns {Image}
|
|
705
|
-
*/
|
|
706
|
-
invert() {
|
|
707
|
-
for (const [x, y, color] of this.iterateWithColors())
|
|
708
|
-
this.__set_pixel__(x, y, ((0xffffffff - color) & 0xffffff00) | (color & 0xff));
|
|
709
|
-
|
|
710
|
-
return this;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
/**
|
|
714
|
-
* Inverts the images value (lightness)
|
|
715
|
-
* @returns {Image}
|
|
716
|
-
*/
|
|
717
|
-
invertValue() {
|
|
718
|
-
for (const [x, y, color] of this.iterateWithColors()) {
|
|
719
|
-
const [h, s, l, a] = Image.rgbaToHSLA(...Image.colorToRGBA(color));
|
|
720
|
-
this.__set_pixel__(x, y, Image.hslaToColor(h, s, 1 - l, a));
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
return this;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
/**
|
|
727
|
-
* Inverts the images saturation
|
|
728
|
-
* @returns {Image}
|
|
729
|
-
*/
|
|
730
|
-
invertSaturation() {
|
|
731
|
-
for (const [x, y, color] of this.iterateWithColors()) {
|
|
732
|
-
const [h, s, l, a] = Image.rgbaToHSLA(...Image.colorToRGBA(color));
|
|
733
|
-
this.__set_pixel__(x, y, Image.hslaToColor(h, 1 - s, l, a));
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
return this;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
/**
|
|
740
|
-
* Inverts the images hue
|
|
741
|
-
* @returns {Image}
|
|
742
|
-
*/
|
|
743
|
-
invertHue() {
|
|
744
|
-
for (const [x, y, color] of this.iterateWithColors()) {
|
|
745
|
-
const [h, s, l, a] = Image.rgbaToHSLA(...Image.colorToRGBA(color));
|
|
746
|
-
this.__set_pixel__(x, y, Image.hslaToColor(1 - h, s, l, a));
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
return this;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
/**
|
|
753
|
-
* Shifts the images hue
|
|
754
|
-
* @param {number} degrees How many degrees to shift the hue by
|
|
755
|
-
*/
|
|
756
|
-
hueShift(degrees) {
|
|
757
|
-
for (const [x, y, color] of this.iterateWithColors()) {
|
|
758
|
-
const [h, s, l, a] = Image.rgbaToHSLA(...Image.colorToRGBA(color));
|
|
759
|
-
this.__set_pixel__(x, y, Image.hslaToColor(h + degrees / 360, s, l, a));
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
return this;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
/**
|
|
766
|
-
* Gets the average color of the image
|
|
767
|
-
* @returns {number}
|
|
768
|
-
*/
|
|
769
|
-
averageColor() {
|
|
770
|
-
let colorAvg = [0, 0, 0];
|
|
771
|
-
let divisor = 0;
|
|
772
|
-
for (let idx = 0; idx < this.bitmap.length; idx += 4) {
|
|
773
|
-
const rgba = this.bitmap.subarray(idx, idx + 4);
|
|
774
|
-
for (let i = 0; i < 3; i++)
|
|
775
|
-
colorAvg[i] += rgba[i];
|
|
776
|
-
divisor += rgba[3] / 255;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
return Image.rgbaToColor(...colorAvg.map(v => v / divisor), 0xff);
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
/**
|
|
783
|
-
* Gets the images dominant color
|
|
784
|
-
* @param {boolean} [ignoreBlack=true] Whether to ignore dark colors below the threshold
|
|
785
|
-
* @param {boolean} [ignoreWhite=true] Whether to ignore light colors above the threshold
|
|
786
|
-
* @param {number} [bwThreshold=0xf] The black/white threshold (0-64)
|
|
787
|
-
* @return {number} The images dominant color
|
|
788
|
-
*/
|
|
789
|
-
dominantColor(ignoreBlack = true, ignoreWhite = true, bwThreshold = 0xf) {
|
|
790
|
-
const colorCounts = new Array(0x3ffff);
|
|
791
|
-
for (let i = 0; i < this.bitmap.length; i += 4) {
|
|
792
|
-
const color = this.__view__.getUint32(i, false);
|
|
793
|
-
const [h, s, l] = Image.rgbaToHSLA(...Image.colorToRGBA(color)).map(v => (~~(v * 0x3f)));
|
|
794
|
-
if (ignoreBlack && l < bwThreshold) continue;
|
|
795
|
-
if (ignoreWhite && l > 0x3f - bwThreshold) continue;
|
|
796
|
-
const key = h << 12 | s << 6 | l;
|
|
797
|
-
colorCounts[key] = (colorCounts[key] || 0) + 1;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
let maxColorCount = -1;
|
|
801
|
-
let mostProminentValue = 0;
|
|
802
|
-
colorCounts.forEach((el, i) => {
|
|
803
|
-
if (el < maxColorCount) return;
|
|
804
|
-
maxColorCount = el;
|
|
805
|
-
mostProminentValue = i;
|
|
806
|
-
});
|
|
807
|
-
|
|
808
|
-
if (mostProminentValue === -1)
|
|
809
|
-
return this.dominantColor(ignoreBlack, ignoreWhite, bwThreshold - 1);
|
|
810
|
-
|
|
811
|
-
const h = (mostProminentValue >>> 12) & 0x3f;
|
|
812
|
-
const s = (mostProminentValue >>> 6) & 0x3f;
|
|
813
|
-
const l = mostProminentValue & 0x3f;
|
|
814
|
-
|
|
815
|
-
return Image.hslaToColor(h / 0x3f, s / 0x3f, l / 0x3f, 1);
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
/**
|
|
819
|
-
* Rotates the image the given amount of degrees
|
|
820
|
-
* @param {number} angle The angle to rotate the image for (in degrees)
|
|
821
|
-
* @param {boolean} resize Whether to resize the image so it fits all pixels or just ignore outlying pixels
|
|
822
|
-
*/
|
|
823
|
-
rotate(angle, resize = true) {
|
|
824
|
-
const frame = new v2(this.width, this.height, this.bitmap).rotate(360 - (angle % 360), resize);
|
|
825
|
-
|
|
826
|
-
const out = new Image(frame.width, frame.height);
|
|
827
|
-
|
|
828
|
-
out.bitmap.set(frame.u8);
|
|
829
|
-
return this.__apply__(out);
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
/**
|
|
833
|
-
* @private
|
|
834
|
-
* @param {Image|Frame} image
|
|
835
|
-
* @returns {Image|Frame}
|
|
836
|
-
*/
|
|
837
|
-
__apply__(image) {
|
|
838
|
-
this.__width__ = image.__width__;
|
|
839
|
-
this.__height__ = image.__height__;
|
|
840
|
-
this.__view__ = image.__view__;
|
|
841
|
-
this.__u32__ = image.__u32__;
|
|
842
|
-
this.bitmap = image.bitmap;
|
|
843
|
-
|
|
844
|
-
if (image instanceof Frame)
|
|
845
|
-
return Frame.from(this, image.duration, image.xOffset, image.yOffset, image.disposalMode);
|
|
846
|
-
|
|
847
|
-
return this;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
/**
|
|
851
|
-
* Creates a multi-point gradient generator
|
|
852
|
-
* @param {Object<number, number>} colors The gradient points to use (e.g. `{0: 0xff0000ff, 1: 0x00ff00ff}`)
|
|
853
|
-
* @return {(function(number): number)} The gradient generator. The function argument is the position in the gradient (0..1).
|
|
854
|
-
*/
|
|
855
|
-
static gradient(colors) {
|
|
856
|
-
const entries = Object.entries(colors).sort((a, b) => a[0] - b[0]);
|
|
857
|
-
const positions = entries.map(e => parseFloat(e[0]));
|
|
858
|
-
const values = entries.map(e => e[1]);
|
|
859
|
-
|
|
860
|
-
if (positions.length === 0) throw new RangeError('Invalid gradient point count');
|
|
861
|
-
else if (positions.length === 1) {
|
|
862
|
-
return () => values[0];
|
|
863
|
-
} else if (positions.length === 2) {
|
|
864
|
-
const gradient = this.__gradient__(values[0], values[1]);
|
|
865
|
-
return position => {
|
|
866
|
-
if (position <= positions[0]) return values[0];
|
|
867
|
-
if (position >= positions[1]) return values[1];
|
|
868
|
-
return gradient((position - positions[0]) / (positions[1] - positions[0]));
|
|
869
|
-
};
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
const minDef = Math.min(...positions);
|
|
873
|
-
const maxDef = Math.max(...positions);
|
|
874
|
-
let gradients = [];
|
|
875
|
-
|
|
876
|
-
for (let i = 0; i < positions.length; i++) {
|
|
877
|
-
let minPos = positions[i - 1];
|
|
878
|
-
if (minPos === undefined) continue;
|
|
879
|
-
|
|
880
|
-
let maxPos = positions[i];
|
|
881
|
-
|
|
882
|
-
let minVal = values[i - 1];
|
|
883
|
-
if (minVal === undefined) minVal = values[i];
|
|
884
|
-
|
|
885
|
-
const maxVal = values[i];
|
|
886
|
-
const gradient = this.__gradient__(minVal, maxVal);
|
|
887
|
-
|
|
888
|
-
gradients.push({min: minPos, max: maxPos, gradient});
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
return position => {
|
|
892
|
-
if (position <= minDef) return gradients[0].gradient(0);
|
|
893
|
-
if (position >= maxDef) return gradients[gradients.length - 1].gradient(1);
|
|
894
|
-
|
|
895
|
-
for (const gradient of gradients)
|
|
896
|
-
if (position >= gradient.min && position <= gradient.max)
|
|
897
|
-
return gradient.gradient((position - gradient.min) / (gradient.max - gradient.min));
|
|
898
|
-
throw new RangeError(`Invalid gradient position: ${position}`);
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
/**
|
|
903
|
-
* Rounds the images corners
|
|
904
|
-
* @param {number} [radius=min(width,height)/4] The radius of the corners
|
|
905
|
-
* @return {Image}
|
|
906
|
-
*/
|
|
907
|
-
roundCorners(radius = Math.min(this.width, this.height) / 4) {
|
|
908
|
-
const radSquared = radius ** 2;
|
|
909
|
-
for (let x = 1; x <= radius; x++) {
|
|
910
|
-
const xRad = (x - radius) ** 2;
|
|
911
|
-
for (let y = 1; y <= radius; y++) {
|
|
912
|
-
if (xRad + (y - radius) ** 2 > radSquared)
|
|
913
|
-
this.bitmap[((y - 1) * this.width + x - 1) * 4 + 3] = 0;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
for (let x = 1; x <= radius; x++) {
|
|
918
|
-
const xRad = (x - radius) ** 2;
|
|
919
|
-
for (let y = this.height - radius; y <= this.height; y++) {
|
|
920
|
-
if (xRad + ((this.height - y) - radius) ** 2 > radSquared)
|
|
921
|
-
this.bitmap[((y - 1) * this.width + x - 1) * 4 + 3] = 0;
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
for (let x = this.width - radius; x <= this.width; x++) {
|
|
926
|
-
const xRad = ((this.width - x) - radius) ** 2;
|
|
927
|
-
for (let y = 1; y <= radius; y++) {
|
|
928
|
-
if (xRad + (y - radius) ** 2 > radSquared)
|
|
929
|
-
this.bitmap[((y - 1) * this.width + x - 1) * 4 + 3] = 0;
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
for (let x = this.width - radius; x <= this.width; x++) {
|
|
934
|
-
const xRad = ((this.width - x) - radius) ** 2;
|
|
935
|
-
for (let y = this.height - radius; y <= this.height; y++) {
|
|
936
|
-
if (xRad + ((this.height - y) - radius) ** 2 > radSquared)
|
|
937
|
-
this.bitmap[((y - 1) * this.width + x - 1) * 4 + 3] = 0;
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
return this;
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
/**
|
|
945
|
-
* @private
|
|
946
|
-
*/
|
|
947
|
-
static __gradient__(startColor, endColor) {
|
|
948
|
-
const sr = startColor >>> 24;
|
|
949
|
-
const sg = startColor >> 16 & 0xff;
|
|
950
|
-
const sb = startColor >> 8 & 0xff;
|
|
951
|
-
const sa = startColor & 0xff;
|
|
952
|
-
const er = (endColor >>> 24) - sr;
|
|
953
|
-
const eg = (endColor >> 16 & 0xff) - sg;
|
|
954
|
-
const eb = (endColor >> 8 & 0xff) - sb;
|
|
955
|
-
const ea = (endColor & 0xff) - sa;
|
|
956
|
-
|
|
957
|
-
return position => {
|
|
958
|
-
const r = sr + position * er;
|
|
959
|
-
const g = sg + position * eg;
|
|
960
|
-
const b = sb + position * eb;
|
|
961
|
-
const a = sa + position * ea;
|
|
962
|
-
return (((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | (a & 0xff));
|
|
963
|
-
};
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
fisheye(radius = 2) {
|
|
967
|
-
const r = new Image(this.width, this.height);
|
|
968
|
-
|
|
969
|
-
const w = this.width;
|
|
970
|
-
const h = this.height;
|
|
971
|
-
const tu32 = this.__u32__;
|
|
972
|
-
const ru32 = r.__u32__;
|
|
973
|
-
const iw = 1 / w;
|
|
974
|
-
const ih = 1 / h;
|
|
975
|
-
|
|
976
|
-
for (const [x, y] of this) {
|
|
977
|
-
const xco = x * iw - .5;
|
|
978
|
-
const yco = y * ih - .5;
|
|
979
|
-
const dfc = Math.sqrt(xco ** 2 + yco ** 2);
|
|
980
|
-
const dis = 2 * dfc ** radius;
|
|
981
|
-
const nx = ((dis * xco / dfc + 0.5) * w) | 0;
|
|
982
|
-
const ny = ((dis * yco / dfc + 0.5) * h) | 0;
|
|
983
|
-
|
|
984
|
-
if (nx < 1 || nx > w || ny < 1 || ny > h || isNaN(nx) || isNaN(ny))
|
|
985
|
-
continue;
|
|
986
|
-
|
|
987
|
-
ru32[y * w + x] = tu32[w * ny + nx];
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
const cO = tu32.length * .5 + w / 2;
|
|
991
|
-
ru32[cO] = tu32[cO];
|
|
992
|
-
|
|
993
|
-
return this.__apply__(r);
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
/**
|
|
997
|
-
* @typedef {object} PNGMetadata
|
|
998
|
-
* @property {string} [title] The images title
|
|
999
|
-
* @property {string} [author] The images author
|
|
1000
|
-
* @property {string} [description] The images description
|
|
1001
|
-
* @property {string} [copyright] The images copyright info
|
|
1002
|
-
* @property {string|number|Date} [creationTime=Date.now()] The images creation timestamp
|
|
1003
|
-
* @property {string} [software="github.com/matmen/ImageScript vX.X.X"] The software used to create this image
|
|
1004
|
-
* @property {string} [disclaimer] A disclaimer for the image
|
|
1005
|
-
* @property {string} [warning] A warning for the image
|
|
1006
|
-
* @property {string} [source] The images source
|
|
1007
|
-
* @property {string} [comment] A comment for the image
|
|
1008
|
-
*/
|
|
1009
|
-
|
|
1010
|
-
/**
|
|
1011
|
-
* Encodes the image into a PNG
|
|
1012
|
-
* @param {number} compression The compression level to use (0-9)
|
|
1013
|
-
* @param {PNGMetadata} [meta={}] Image metadata
|
|
1014
|
-
* @return {Promise<Uint8Array>} The encoded data
|
|
1015
|
-
*/
|
|
1016
|
-
async encode(compression = 1, {
|
|
1017
|
-
title,
|
|
1018
|
-
author,
|
|
1019
|
-
description,
|
|
1020
|
-
copyright,
|
|
1021
|
-
creationTime,
|
|
1022
|
-
software,
|
|
1023
|
-
disclaimer,
|
|
1024
|
-
warning,
|
|
1025
|
-
source,
|
|
1026
|
-
comment
|
|
1027
|
-
} = {}) {
|
|
1028
|
-
return png.encode(this.bitmap, {
|
|
1029
|
-
width: this.width,
|
|
1030
|
-
height: this.height,
|
|
1031
|
-
level: compression,
|
|
1032
|
-
channels: 4,
|
|
1033
|
-
text: {
|
|
1034
|
-
Title: title,
|
|
1035
|
-
Author: author,
|
|
1036
|
-
Description: description,
|
|
1037
|
-
Copyright: copyright,
|
|
1038
|
-
'Creation Time': new Date(creationTime === undefined ? Date.now() : creationTime).toUTCString(),
|
|
1039
|
-
Software: software === undefined ? `github.com/matmen/ImageScript v${version}` : software,
|
|
1040
|
-
Disclaimer: disclaimer,
|
|
1041
|
-
Warning: warning,
|
|
1042
|
-
Source: source,
|
|
1043
|
-
Comment: comment
|
|
1044
|
-
}
|
|
1045
|
-
});
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
/**
|
|
1049
|
-
* Encodes the image into a JPEG
|
|
1050
|
-
* @param {number} [quality=90] The JPEG quality to use (1-100)
|
|
1051
|
-
* @return {Promise<Uint8Array>}
|
|
1052
|
-
*/
|
|
1053
|
-
async encodeJPEG(quality = 90) {
|
|
1054
|
-
return codecs.jpeg.encode_async(this.bitmap, {
|
|
1055
|
-
quality,
|
|
1056
|
-
width: this.width,
|
|
1057
|
-
height: this.height,
|
|
1058
|
-
});
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
/**
|
|
1062
|
-
* Encodes the image into a WEBP
|
|
1063
|
-
* @param {null|number} [quality=null] The WEBP quality to use (0-100) (null is lossless)
|
|
1064
|
-
* @return {Promise<Uint8Array>}
|
|
1065
|
-
*/
|
|
1066
|
-
async encodeWEBP(quality = null) {
|
|
1067
|
-
return codecs.webp.encode_async(this.bitmap, {
|
|
1068
|
-
quality,
|
|
1069
|
-
width: this.width,
|
|
1070
|
-
height: this.height,
|
|
1071
|
-
});
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
/**
|
|
1075
|
-
* Decodes an image (PNG, JPEG or TIFF)
|
|
1076
|
-
* @param {Buffer|Uint8Array} data The binary data to decode
|
|
1077
|
-
* @return {Promise<Image>} The decoded image
|
|
1078
|
-
*/
|
|
1079
|
-
static async decode(data) {
|
|
1080
|
-
let image;
|
|
1081
|
-
|
|
1082
|
-
data = mem.view(data);
|
|
1083
|
-
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
1084
|
-
|
|
1085
|
-
if (ImageType.isPNG(view)) { // PNG
|
|
1086
|
-
const {width, height, framebuffer} = (await pnglib.init()).decode(data);
|
|
1087
|
-
image = new Image(width, height);
|
|
1088
|
-
image.bitmap.set(framebuffer);
|
|
1089
|
-
} else if (ImageType.isJPEG(view)) { // JPEG
|
|
1090
|
-
const framebuffer = (await jpeglib.init()).decode(data);
|
|
1091
|
-
|
|
1092
|
-
const width = framebuffer.width;
|
|
1093
|
-
const height = framebuffer.height;
|
|
1094
|
-
const pixelType = framebuffer.format;
|
|
1095
|
-
|
|
1096
|
-
image = new Image(width, height);
|
|
1097
|
-
const buffer = framebuffer.buffer;
|
|
1098
|
-
|
|
1099
|
-
if (pixelType === 0) {
|
|
1100
|
-
const view = new DataView(image.bitmap.buffer);
|
|
1101
|
-
|
|
1102
|
-
for (let i = 0; i < buffer.length; i++) {
|
|
1103
|
-
const pixel = buffer[i];
|
|
1104
|
-
view.setUint32(i * 4, pixel << 24 | pixel << 16 | pixel << 8 | 0xff, false);
|
|
1105
|
-
}
|
|
1106
|
-
} else if (pixelType === 1) {
|
|
1107
|
-
image.bitmap.fill(0xff);
|
|
1108
|
-
for (let i = 0; i < width * height; i++)
|
|
1109
|
-
image.bitmap.set(buffer.subarray(i * 3, i * 3 + 3), i * 4);
|
|
1110
|
-
} else if (pixelType === 2) {
|
|
1111
|
-
for (let i = 0; i < buffer.length; i += 4) {
|
|
1112
|
-
image.bitmap[i] = 0xff * (1 - buffer[i] / 0xff) * (1 - buffer[i + 3] / 0xff);
|
|
1113
|
-
image.bitmap[i + 1] = 0xff * (1 - buffer[i + 1] / 0xff) * (1 - buffer[i + 3] / 0xff);
|
|
1114
|
-
image.bitmap[i + 2] = 0xff * (1 - buffer[i + 2] / 0xff) * (1 - buffer[i + 3] / 0xff);
|
|
1115
|
-
image.bitmap[i + 3] = 0xff;
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
} else if (ImageType.isTIFF(view)) { // TIFF
|
|
1119
|
-
const framebuffer = (await tifflib.init()).decode(data);
|
|
1120
|
-
image = new Image(framebuffer.width, framebuffer.height);
|
|
1121
|
-
|
|
1122
|
-
image.bitmap.set(framebuffer.buffer);
|
|
1123
|
-
} else throw new Error('Unsupported image type');
|
|
1124
|
-
|
|
1125
|
-
return image;
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
/**
|
|
1129
|
-
* Scale the SVG by the given amount. For use with {@link Image.renderSVG}
|
|
1130
|
-
* @return {number}
|
|
1131
|
-
*/
|
|
1132
|
-
static get SVG_MODE_SCALE() {
|
|
1133
|
-
return 1;
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
/**
|
|
1137
|
-
* Scale the SVG to fit the given width. For use with {@link Image.renderSVG}
|
|
1138
|
-
* @return {number}
|
|
1139
|
-
*/
|
|
1140
|
-
static get SVG_MODE_WIDTH() {
|
|
1141
|
-
return 2;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
/**
|
|
1145
|
-
* Scale the SVG to fit the given height. For use with {@link Image.renderSVG}
|
|
1146
|
-
* @return {number}
|
|
1147
|
-
*/
|
|
1148
|
-
static get SVG_MODE_HEIGHT() {
|
|
1149
|
-
return 3;
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
/**
|
|
1153
|
-
* Creates a new image from the given SVG
|
|
1154
|
-
* @param {string} svg The SVG content
|
|
1155
|
-
* @param {number} size The size to use
|
|
1156
|
-
* @param {number} mode The SVG resizing mode to use (one of {@link SVG_MODE_SCALE}, {@link SVG_MODE_WIDTH}, {@link SVG_MODE_HEIGHT})
|
|
1157
|
-
* @return {Promise<Image>} The rendered SVG graphic
|
|
1158
|
-
*/
|
|
1159
|
-
static async renderSVG(svg, size = 1, mode = this.SVG_MODE_SCALE) {
|
|
1160
|
-
if (![this.SVG_MODE_WIDTH, this.SVG_MODE_HEIGHT, this.SVG_MODE_SCALE].includes(mode))
|
|
1161
|
-
throw new Error('Invalid SVG scaling mode');
|
|
1162
|
-
|
|
1163
|
-
if (mode === this.SVG_MODE_SCALE && size <= 0)
|
|
1164
|
-
throw new RangeError('SVG scale must be > 0');
|
|
1165
|
-
if (mode !== this.SVG_MODE_SCALE && size < 1)
|
|
1166
|
-
throw new RangeError('SVG size must be >= 1')
|
|
1167
|
-
|
|
1168
|
-
if (typeof svg === 'string') svg = Buffer.from(svg);
|
|
1169
|
-
const framebuffer = (await svglib.init()).rasterize(svg, mode, size);
|
|
1170
|
-
|
|
1171
|
-
const image = new Image(framebuffer.width, framebuffer.height);
|
|
1172
|
-
|
|
1173
|
-
image.bitmap.set(framebuffer.buffer);
|
|
1174
|
-
|
|
1175
|
-
return image;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
/**
|
|
1179
|
-
* Creates a new image containing the rendered text.
|
|
1180
|
-
* @param {Uint8Array} font TrueType (ttf/ttc) or OpenType (otf) font buffer to use
|
|
1181
|
-
* @param {number} scale Font size to use
|
|
1182
|
-
* @param {string} text Text to render
|
|
1183
|
-
* @param {number} [color=0xffffffff] Text color to use
|
|
1184
|
-
* @param {TextLayout} [layout] The text layout to use
|
|
1185
|
-
* @return {Promise<Image>} The rendered text
|
|
1186
|
-
*/
|
|
1187
|
-
static async renderText(font, scale, text, color = 0xffffffff, layout = new TextLayout()) {
|
|
1188
|
-
const { Font, Layout } = await fontlib.init();
|
|
1189
|
-
|
|
1190
|
-
font = new Font(scale, font);
|
|
1191
|
-
const [r, g, b, a] = Image.colorToRGBA(color);
|
|
1192
|
-
|
|
1193
|
-
const layoutOptions = new Layout();
|
|
1194
|
-
|
|
1195
|
-
layoutOptions.reset({
|
|
1196
|
-
max_width: layout.maxWidth,
|
|
1197
|
-
max_height: layout.maxHeight,
|
|
1198
|
-
wrap_style: layout.wrapStyle,
|
|
1199
|
-
vertical_align: layout.verticalAlign,
|
|
1200
|
-
horizontal_align: layout.horizontalAlign,
|
|
1201
|
-
wrap_hard_breaks: layout.wrapHardBreaks
|
|
1202
|
-
});
|
|
1203
|
-
|
|
1204
|
-
layoutOptions.append(font, text, {scale});
|
|
1205
|
-
const framebuffer = layoutOptions.rasterize(r, g, b);
|
|
1206
|
-
const image = new Image(framebuffer.width, framebuffer.height);
|
|
1207
|
-
|
|
1208
|
-
image.bitmap.set(framebuffer.buffer);
|
|
1209
|
-
|
|
1210
|
-
if (image.height > layout.maxHeight)
|
|
1211
|
-
image.crop(0, 0, image.width, Math.floor(layoutOptions.lines() / image.height * layout.maxHeight) * (image.height / layoutOptions.lines()));
|
|
1212
|
-
|
|
1213
|
-
font.free();
|
|
1214
|
-
layoutOptions.free();
|
|
1215
|
-
return image.opacity(a / 0xff);
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
/**
|
|
1221
|
-
* Represents a frame in a GIF
|
|
1222
|
-
* @extends Image
|
|
1223
|
-
*/
|
|
1224
|
-
class Frame extends Image {
|
|
1225
|
-
/**
|
|
1226
|
-
* GIF frame disposal mode KEEP. For use with {@link Frame}
|
|
1227
|
-
* @returns {string}
|
|
1228
|
-
*/
|
|
1229
|
-
static get DISPOSAL_KEEP() {
|
|
1230
|
-
return 'keep';
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
/**
|
|
1234
|
-
* GIF frame disposal mode PREVIOUS. For use with {@link Frame}
|
|
1235
|
-
* @returns {string}
|
|
1236
|
-
*/
|
|
1237
|
-
static get DISPOSAL_PREVIOUS() {
|
|
1238
|
-
return 'previous';
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
/**
|
|
1242
|
-
* GIF frame disposal mode BACKGROUND. For use with {@link Frame}
|
|
1243
|
-
* @returns {string}
|
|
1244
|
-
*/
|
|
1245
|
-
static get DISPOSAL_BACKGROUND() {
|
|
1246
|
-
return 'background';
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
static __convert_disposal_mode__(mode) {
|
|
1250
|
-
if (typeof mode === 'string')
|
|
1251
|
-
mode = ['any', 'keep', 'previous', 'background'].indexOf(mode);
|
|
1252
|
-
if (mode < 0 || mode > 3)
|
|
1253
|
-
throw new RangeError('Invalid disposal mode');
|
|
1254
|
-
|
|
1255
|
-
return mode;
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
/**
|
|
1259
|
-
* Creates a new, blank frame
|
|
1260
|
-
* @param {number} width
|
|
1261
|
-
* @param {number} height
|
|
1262
|
-
* @param {number} [duration = 100] The frames duration (in ms)
|
|
1263
|
-
* @param {number} [xOffset=0] The frames offset on the x-axis
|
|
1264
|
-
* @param {number} [yOffset=0] The frames offset on the y-axis
|
|
1265
|
-
* @param {string|number} [disposalMode=Frame.DISPOSAL_KEEP] The frame's disposal mode ({@link Frame.DISPOSAL_KEEP}, {@link Frame.DISPOSAL_PREVIOUS} or {@link Frame.DISPOSAL_BACKGROUND})
|
|
1266
|
-
* @return {Frame}
|
|
1267
|
-
*/
|
|
1268
|
-
constructor(width, height, duration = 100, xOffset = 0, yOffset = 0, disposalMode = Frame.DISPOSAL_KEEP) {
|
|
1269
|
-
if (isNaN(duration) || duration < 0)
|
|
1270
|
-
throw new RangeError('Invalid frame duration');
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
this.
|
|
1276
|
-
this.
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
*
|
|
1289
|
-
* @param {number}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
return
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
this.
|
|
1563
|
-
if (
|
|
1564
|
-
throw new RangeError('Invalid
|
|
1565
|
-
|
|
1566
|
-
this.
|
|
1567
|
-
if (
|
|
1568
|
-
throw new RangeError('Invalid
|
|
1569
|
-
|
|
1570
|
-
this.
|
|
1571
|
-
if (
|
|
1572
|
-
throw new
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1
|
+
const png = require('./png/node.js');
|
|
2
|
+
const mem = require('./utils/mem.js');
|
|
3
|
+
const {version} = require('./package.json');
|
|
4
|
+
const codecs = require('./codecs/node/index.js');
|
|
5
|
+
const { default: v2 } = require('./v2/framebuffer.js');
|
|
6
|
+
|
|
7
|
+
// old
|
|
8
|
+
const svglib = require('./wasm/node/svg.js');
|
|
9
|
+
const giflib = require('./wasm/node/gif.js');
|
|
10
|
+
const pnglib = require('./wasm/node/png.js');
|
|
11
|
+
const fontlib = require('./wasm/node/font.js');
|
|
12
|
+
const jpeglib = require('./wasm/node/jpeg.js');
|
|
13
|
+
const tifflib = require('./wasm/node/tiff.js');
|
|
14
|
+
|
|
15
|
+
const MAGIC_NUMBERS = {
|
|
16
|
+
PNG: 0x89504e47,
|
|
17
|
+
JPEG: 0xffd8ff,
|
|
18
|
+
TIFF: 0x49492a00,
|
|
19
|
+
GIF: 0x474946
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Represents an image; provides utility functions
|
|
24
|
+
*/
|
|
25
|
+
class Image {
|
|
26
|
+
/**
|
|
27
|
+
* Creates a new image with the given dimensions
|
|
28
|
+
* @param {number} width
|
|
29
|
+
* @param {number} height
|
|
30
|
+
* @returns {Image}
|
|
31
|
+
*/
|
|
32
|
+
constructor(width, height) {
|
|
33
|
+
width = ~~width;
|
|
34
|
+
height = ~~height;
|
|
35
|
+
|
|
36
|
+
if (width < 1)
|
|
37
|
+
throw new RangeError('Image has to be at least 1 pixel wide');
|
|
38
|
+
if (height < 1)
|
|
39
|
+
throw new RangeError('Image has to be at least 1 pixel high');
|
|
40
|
+
|
|
41
|
+
/** @private */
|
|
42
|
+
this.__width__ = width;
|
|
43
|
+
/** @private */
|
|
44
|
+
this.__height__ = height;
|
|
45
|
+
/** @private */
|
|
46
|
+
this.__buffer__ = new ArrayBuffer(width * height * 4);
|
|
47
|
+
/** @private */
|
|
48
|
+
this.__view__ = new DataView(this.__buffer__);
|
|
49
|
+
/** @private */
|
|
50
|
+
this.__u32__ = new Uint32Array(this.__buffer__);
|
|
51
|
+
/**
|
|
52
|
+
* The images RGBA pixel data
|
|
53
|
+
* @type {Uint8ClampedArray}
|
|
54
|
+
*/
|
|
55
|
+
this.bitmap = new Uint8ClampedArray(this.__buffer__);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @private
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
toString() {
|
|
63
|
+
return `Image<${this.width}x${this.height}>`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The images width
|
|
68
|
+
* @returns {number}
|
|
69
|
+
*/
|
|
70
|
+
get width() {
|
|
71
|
+
return this.__width__;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The images height
|
|
76
|
+
* @returns {number}
|
|
77
|
+
*/
|
|
78
|
+
get height() {
|
|
79
|
+
return this.__height__;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Yields an [x, y] array for every pixel in the image
|
|
84
|
+
* @yields {number[]} The coordinates of the pixel ([x, y])
|
|
85
|
+
* @returns {void}
|
|
86
|
+
*/
|
|
87
|
+
*[Symbol.iterator]() {
|
|
88
|
+
for (const x of new v2(this.width, this.height, this.bitmap)[Symbol.iterator]()) yield (x[0]++, x[1]++, x)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Yields an [x, y, color] array for every pixel in the image
|
|
93
|
+
* @yields {number[]} The coordinates and color of the pixel ([x, y, color])
|
|
94
|
+
*/
|
|
95
|
+
*iterateWithColors() {
|
|
96
|
+
for (const x of new v2(this.width, this.height, this.bitmap).pixels('int')) yield (x[0]++, x[1]++, x);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Converts RGBA components to an RGBA value
|
|
101
|
+
* @param {number} r red (0..255)
|
|
102
|
+
* @param {number} g green (0..255)
|
|
103
|
+
* @param {number} b blue (0..255)
|
|
104
|
+
* @param {number} a alpha (0..255)
|
|
105
|
+
* @returns {number} RGBA value
|
|
106
|
+
*/
|
|
107
|
+
static rgbaToColor(r, g, b, a) {
|
|
108
|
+
return (((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | (a & 0xff)) >>> 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Converts RGB components to an RGBA value (assuming alpha = 255)
|
|
113
|
+
* @param {number} r red (0..255)
|
|
114
|
+
* @param {number} g green (0..255)
|
|
115
|
+
* @param {number} b blue (0..255)
|
|
116
|
+
* @returns {number} RGBA value
|
|
117
|
+
*/
|
|
118
|
+
static rgbToColor(r, g, b) {
|
|
119
|
+
return Image.rgbaToColor(r, g, b, 0xff);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Converts HSLA colors to RGBA colors
|
|
124
|
+
* @param {number} h hue (0..1)
|
|
125
|
+
* @param {number} s saturation (0..1)
|
|
126
|
+
* @param {number} l lightness (0..1)
|
|
127
|
+
* @param {number} a opacity (0..1)
|
|
128
|
+
* @returns {number} color
|
|
129
|
+
*/
|
|
130
|
+
static hslaToColor(h, s, l, a) {
|
|
131
|
+
h %= 1;
|
|
132
|
+
s = Math.min(1, Math.max(0, s));
|
|
133
|
+
l = Math.min(1, Math.max(0, l));
|
|
134
|
+
a = Math.min(1, Math.max(0, a));
|
|
135
|
+
|
|
136
|
+
let r, g, b;
|
|
137
|
+
|
|
138
|
+
if (s === 0) {
|
|
139
|
+
r = g = b = l;
|
|
140
|
+
} else {
|
|
141
|
+
const hue2rgb = (p, q, t) => {
|
|
142
|
+
if (t < 0) t += 1;
|
|
143
|
+
if (t > 1) t -= 1;
|
|
144
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
145
|
+
if (t < 1 / 2) return q;
|
|
146
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
147
|
+
return p;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
151
|
+
const p = 2 * l - q;
|
|
152
|
+
|
|
153
|
+
r = hue2rgb(p, q, h + 1 / 3);
|
|
154
|
+
g = hue2rgb(p, q, h);
|
|
155
|
+
b = hue2rgb(p, q, h - 1 / 3);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return Image.rgbaToColor(r * 255, g * 255, b * 255, a * 255);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Converts HSL colors to RGBA colors (assuming an opacity of 255)
|
|
163
|
+
* @param {number} h hue (0..1)
|
|
164
|
+
* @param {number} s saturation (0..1)
|
|
165
|
+
* @param {number} l lightness (0..1)
|
|
166
|
+
* @returns {number} color
|
|
167
|
+
*/
|
|
168
|
+
static hslToColor(h, s, l) {
|
|
169
|
+
return Image.hslaToColor(h, s, l, 1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Converts an RGBA value to an array of HSLA values
|
|
174
|
+
* @param r {number} (0..255)
|
|
175
|
+
* @param g {number} (0..255)
|
|
176
|
+
* @param b {number} (0..255)
|
|
177
|
+
* @param a {number} (0..255)
|
|
178
|
+
* @returns {number[]} The HSLA values ([H, S, L, A])
|
|
179
|
+
*/
|
|
180
|
+
static rgbaToHSLA(r, g, b, a) {
|
|
181
|
+
r /= 255;
|
|
182
|
+
g /= 255;
|
|
183
|
+
b /= 255;
|
|
184
|
+
|
|
185
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
186
|
+
let h, s, l = (max + min) / 2;
|
|
187
|
+
|
|
188
|
+
if (max === min) {
|
|
189
|
+
h = s = 0;
|
|
190
|
+
} else {
|
|
191
|
+
const d = max - min;
|
|
192
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
193
|
+
switch (max) {
|
|
194
|
+
case r:
|
|
195
|
+
h = (g - b) / d + (g < b ? 6 : 0);
|
|
196
|
+
break;
|
|
197
|
+
case g:
|
|
198
|
+
h = (b - r) / d + 2;
|
|
199
|
+
break;
|
|
200
|
+
case b:
|
|
201
|
+
h = (r - g) / d + 4;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
h /= 6;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return [h, s, l, a / 255];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Converts a color value to an array of RGBA values
|
|
213
|
+
* @param {number} color The color value to convert
|
|
214
|
+
* @returns {number[]} The RGBA values ([R, G, B, A])
|
|
215
|
+
*/
|
|
216
|
+
static colorToRGBA(color) {
|
|
217
|
+
return [(color >> 24) & 0xff, (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Converts a color value to an array of RGB values (ignoring the colors alpha)
|
|
222
|
+
* @param {number} color The color value to convert
|
|
223
|
+
* @returns {number[]} The RGB values ([R, G, B])
|
|
224
|
+
*/
|
|
225
|
+
static colorToRGB(color) {
|
|
226
|
+
return Image.colorToRGBA(color).slice(0, 3);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Gets the pixel color at the specified position
|
|
231
|
+
* @param {number} x
|
|
232
|
+
* @param {number} y
|
|
233
|
+
* @returns {number} The color value
|
|
234
|
+
*/
|
|
235
|
+
getPixelAt(x, y) {
|
|
236
|
+
this.__check_boundaries__(x, y);
|
|
237
|
+
return this.__view__.getUint32(((~~y - 1) * this.width + (~~x - 1)) * 4, false);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Gets the pixel color at the specified position
|
|
242
|
+
* @param {number} x
|
|
243
|
+
* @param {number} y
|
|
244
|
+
* @returns {Uint8ClampedArray} The RGBA value
|
|
245
|
+
*/
|
|
246
|
+
getRGBAAt(x, y) {
|
|
247
|
+
this.__check_boundaries__(x, y);
|
|
248
|
+
const idx = ((~~y - 1) * this.width + (~~x - 1)) * 4;
|
|
249
|
+
return this.bitmap.subarray(idx, idx + 4);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Sets the pixel color for the specified position
|
|
254
|
+
* @param {number} x
|
|
255
|
+
* @param {number} y
|
|
256
|
+
* @param {number} pixelColor
|
|
257
|
+
*/
|
|
258
|
+
setPixelAt(x, y, pixelColor) {
|
|
259
|
+
x = ~~x;
|
|
260
|
+
y = ~~y;
|
|
261
|
+
this.__check_boundaries__(x, y);
|
|
262
|
+
this.__set_pixel__(x, y, pixelColor);
|
|
263
|
+
return this;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* @private
|
|
268
|
+
* @param {number} x
|
|
269
|
+
* @param {number} y
|
|
270
|
+
* @param {number} pixelColor
|
|
271
|
+
*/
|
|
272
|
+
__set_pixel__(x, y, pixelColor) {
|
|
273
|
+
this.__view__.setUint32(((y - 1) * this.width + (x - 1)) * 4, pixelColor, false);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* @private
|
|
278
|
+
* @param {number} x
|
|
279
|
+
* @param {number} y
|
|
280
|
+
*/
|
|
281
|
+
__check_boundaries__(x, y) {
|
|
282
|
+
if (isNaN(x)) throw new TypeError(`Invalid pixel coordinates (x=${x})`);
|
|
283
|
+
if (isNaN(y)) throw new TypeError(`Invalid pixel coordinates (y=${y})`);
|
|
284
|
+
if (x < 1)
|
|
285
|
+
throw new RangeError(`${Image.__out_of_bounds__} (x=${x})<1`);
|
|
286
|
+
if (x > this.width)
|
|
287
|
+
throw new RangeError(`${Image.__out_of_bounds__} (x=${x})>(width=${this.width})`);
|
|
288
|
+
if (y < 1)
|
|
289
|
+
throw new RangeError(`${Image.__out_of_bounds__} (y=${y})<1`);
|
|
290
|
+
if (y > this.height)
|
|
291
|
+
throw new RangeError(`${Image.__out_of_bounds__} (y=${y})>(height=${this.height})`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* @private
|
|
296
|
+
*/
|
|
297
|
+
static get __out_of_bounds__() {
|
|
298
|
+
return 'Tried referencing a pixel outside of the images boundaries:';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @callback colorFunction
|
|
303
|
+
* @param {number} x
|
|
304
|
+
* @param {number} y
|
|
305
|
+
* @returns {number} pixel color
|
|
306
|
+
*/
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Fills the image data with the supplied color
|
|
310
|
+
* @param {number|colorFunction} color
|
|
311
|
+
* @returns {Image}
|
|
312
|
+
*/
|
|
313
|
+
fill(color) {
|
|
314
|
+
new v2(this.width, this.height, this.bitmap).fill(color);
|
|
315
|
+
|
|
316
|
+
return this;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Clones the current image
|
|
321
|
+
* @returns {Image}
|
|
322
|
+
*/
|
|
323
|
+
clone() {
|
|
324
|
+
const image = new Image(this.width, this.height);
|
|
325
|
+
image.bitmap.set(this.bitmap);
|
|
326
|
+
return image;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Use {@link https://en.wikipedia.org/wiki/Image_scaling#Nearest-neighbor_interpolation Nearest-neighbor} resizing.
|
|
331
|
+
* @returns {string}
|
|
332
|
+
*/
|
|
333
|
+
static get RESIZE_NEAREST_NEIGHBOR() {
|
|
334
|
+
return 'RESIZE_NEAREST_NEIGHBOR';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Used for automatically preserving an images aspect ratio when resizing.
|
|
339
|
+
* @returns {number}
|
|
340
|
+
*/
|
|
341
|
+
static get RESIZE_AUTO() {
|
|
342
|
+
return -1;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Resizes the image by the given factor
|
|
347
|
+
* @param {number} factor The factor to resize the image with
|
|
348
|
+
* @param {string} [mode=Image.RESIZE_NEAREST_NEIGHBOR] The resizing mode to use
|
|
349
|
+
* @returns {Image}
|
|
350
|
+
*/
|
|
351
|
+
scale(factor, mode = Image.RESIZE_NEAREST_NEIGHBOR) {
|
|
352
|
+
const image = this.__scale__(factor, mode);
|
|
353
|
+
return this.__apply__(image);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** @private */
|
|
357
|
+
__scale__(factor, mode = Image.RESIZE_NEAREST_NEIGHBOR) {
|
|
358
|
+
if (factor === 1) return this;
|
|
359
|
+
return this.__resize__(this.width * factor, this.height * factor, mode);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Resizes the image to the given dimensions.
|
|
364
|
+
* Use {@link Image.RESIZE_AUTO} as either width or height to automatically preserve the aspect ratio.
|
|
365
|
+
* @param {number} width The new width
|
|
366
|
+
* @param {number} height The new height
|
|
367
|
+
* @param {string} [mode=Image.RESIZE_NEAREST_NEIGHBOR] The resizing mode to use
|
|
368
|
+
* @returns {Image} The resized image
|
|
369
|
+
*/
|
|
370
|
+
resize(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) {
|
|
371
|
+
const image = this.__resize__(width, height, mode);
|
|
372
|
+
return this.__apply__(image);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Resizes the image so it is contained in the given bounding box.
|
|
377
|
+
* Can return an image with one axis smaller than the given bounding box.
|
|
378
|
+
* @param {number} width The width of the bounding box
|
|
379
|
+
* @param {number} height The height of the bounding box
|
|
380
|
+
* @param {string} [mode=Image.RESIZE_NEAREST_NEIGHBOR] The resizing mode to use
|
|
381
|
+
* @returns {Image} The resized image
|
|
382
|
+
*/
|
|
383
|
+
contain(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) {
|
|
384
|
+
const scaleFactor = width / height > this.width / this.height ? height / this.height : width / this.width;
|
|
385
|
+
return this.scale(scaleFactor, mode);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Resizes the image so it is contained in the given bounding box, placing it in the center of the given bounding box.
|
|
390
|
+
* Always returns the exact dimensions of the bounding box.
|
|
391
|
+
* @param {number} width The width of the bounding box
|
|
392
|
+
* @param {number} height The height of the bounding box
|
|
393
|
+
* @param {string} [mode=Image.RESIZE_NEAREST_NEIGHBOR] The resizing mode to use
|
|
394
|
+
* @returns {Image} The resized image
|
|
395
|
+
*/
|
|
396
|
+
fit(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) {
|
|
397
|
+
const result = new Image(width, height);
|
|
398
|
+
this.contain(width, height, mode);
|
|
399
|
+
result.composite(this, (width - this.width) / 2, (height - this.height) / 2);
|
|
400
|
+
return this.__apply__(result);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Resizes the image so it covers the given bounding box, cropping the overflowing edges.
|
|
405
|
+
* Always returns the exact dimensions of the bounding box.
|
|
406
|
+
* @param {number} width The width of the bounding box
|
|
407
|
+
* @param {number} height The height of the bounding box
|
|
408
|
+
* @param {string} [mode=Image.RESIZE_NEAREST_NEIGHBOR] The resizing mode to use
|
|
409
|
+
* @returns {Image} The resized image
|
|
410
|
+
*/
|
|
411
|
+
cover(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) {
|
|
412
|
+
const scaleFactor = width / height > this.width / this.height ? width / this.width : height / this.height;
|
|
413
|
+
const result = this.scale(scaleFactor, mode);
|
|
414
|
+
return result.crop((result.width - width) / 2, (result.height - height) / 2, width, height);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** @private */
|
|
418
|
+
__resize__(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) {
|
|
419
|
+
if (width === Image.RESIZE_AUTO && height === Image.RESIZE_AUTO) throw new Error('RESIZE_AUTO can only be used for either width or height, not for both');
|
|
420
|
+
else if (width === Image.RESIZE_AUTO) width = this.width / this.height * height;
|
|
421
|
+
else if (height === Image.RESIZE_AUTO) height = this.height / this.width * width;
|
|
422
|
+
|
|
423
|
+
width = Math.floor(width);
|
|
424
|
+
height = Math.floor(height);
|
|
425
|
+
if (width < 1)
|
|
426
|
+
throw new RangeError('Image has to be at least 1 pixel wide');
|
|
427
|
+
if (height < 1)
|
|
428
|
+
throw new RangeError('Image has to be at least 1 pixel high');
|
|
429
|
+
|
|
430
|
+
let image;
|
|
431
|
+
if (mode === Image.RESIZE_NEAREST_NEIGHBOR)
|
|
432
|
+
image = this.__resize_nearest_neighbor__(width, height);
|
|
433
|
+
else throw new Error('Invalid resize mode');
|
|
434
|
+
|
|
435
|
+
return image;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* @private
|
|
440
|
+
* @param {number} width The new width
|
|
441
|
+
* @param {number} height The new height
|
|
442
|
+
*/
|
|
443
|
+
__resize_nearest_neighbor__(width, height) {
|
|
444
|
+
const image = new this.constructor(width, height);
|
|
445
|
+
const frame = new v2(this.width, this.height, this.bitmap).resize('nearest', width, height);
|
|
446
|
+
|
|
447
|
+
image.bitmap.set(frame.u8);
|
|
448
|
+
|
|
449
|
+
return image;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Crops an image to the specified dimensions
|
|
454
|
+
* @param {number} x The x offset
|
|
455
|
+
* @param {number} y The y offset
|
|
456
|
+
* @param {number} width The new images width
|
|
457
|
+
* @param {number} height The new images height
|
|
458
|
+
* @returns {Image}
|
|
459
|
+
*/
|
|
460
|
+
crop(x, y, width, height) {
|
|
461
|
+
if (width > this.width) width = this.width;
|
|
462
|
+
if (height > this.height) height = this.height;
|
|
463
|
+
|
|
464
|
+
return this.__apply__(this.__crop__(~~x, ~~y, ~~width, ~~height));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* @param {number} x
|
|
469
|
+
* @param {number} y
|
|
470
|
+
* @param {number} width
|
|
471
|
+
* @param {number} height
|
|
472
|
+
* @returns {Image}
|
|
473
|
+
* @private
|
|
474
|
+
*/
|
|
475
|
+
__crop__(x, y, width, height) {
|
|
476
|
+
x = ~~x;
|
|
477
|
+
y = ~~y;
|
|
478
|
+
|
|
479
|
+
const image = new this.constructor(width, height);
|
|
480
|
+
|
|
481
|
+
for (let tY = 0; tY < height; tY++) {
|
|
482
|
+
const idx = (tY + y) * this.width + x;
|
|
483
|
+
image.__u32__.set(this.__u32__.subarray(idx, idx + width), tY * width);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return image;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Draws a box at the specified coordinates
|
|
491
|
+
* @param {number} x The x offset
|
|
492
|
+
* @param {number} y The y offset
|
|
493
|
+
* @param {number} width The box width
|
|
494
|
+
* @param {number} height The box height
|
|
495
|
+
* @param {number|colorFunction} color The color to fill the box in with
|
|
496
|
+
* @returns {Image}
|
|
497
|
+
*/
|
|
498
|
+
drawBox(x, y, width, height, color) {
|
|
499
|
+
x = ~~(x - 1);
|
|
500
|
+
y = ~~(y - 1);
|
|
501
|
+
width = ~~width;
|
|
502
|
+
height = ~~height;
|
|
503
|
+
|
|
504
|
+
if (typeof color === 'function') {
|
|
505
|
+
for (let tY = 1; tY <= height; tY++) {
|
|
506
|
+
for (let tX = 1; tX <= width; tX++) {
|
|
507
|
+
const nX = tX + x;
|
|
508
|
+
const nY = tY + y;
|
|
509
|
+
if (Math.min(nX, nY) < 1 || nX > this.width || nY > this.height)
|
|
510
|
+
continue;
|
|
511
|
+
|
|
512
|
+
const tC = color(tX, tY);
|
|
513
|
+
this.__set_pixel__(nX, nY, tC);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
} else return this.__fast_box__(x, y, width, height, color);
|
|
517
|
+
|
|
518
|
+
return this;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* @private
|
|
523
|
+
* @param {number} x
|
|
524
|
+
* @param {number} y
|
|
525
|
+
* @param {number} width
|
|
526
|
+
* @param {number} height
|
|
527
|
+
* @param {number} color
|
|
528
|
+
*/
|
|
529
|
+
__fast_box__(x, y, width, height, color) {
|
|
530
|
+
if (x < 0) {
|
|
531
|
+
width += x;
|
|
532
|
+
x = 0;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (y < 0) {
|
|
536
|
+
height += y;
|
|
537
|
+
y = 0;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const right = Math.max(Math.min(x + width, this.width), 1);
|
|
541
|
+
let xPos = right;
|
|
542
|
+
while (x <= --xPos)
|
|
543
|
+
this.__view__.setUint32(4 * (xPos + y * this.width), color);
|
|
544
|
+
const end = 4 * (right + y * this.width);
|
|
545
|
+
const start = 4 * (x + y * this.width);
|
|
546
|
+
|
|
547
|
+
let bottom = Math.max(Math.min(y + height, this.height), 1);
|
|
548
|
+
while (y < --bottom)
|
|
549
|
+
this.bitmap.copyWithin(4 * (x + bottom * this.width), start, end);
|
|
550
|
+
|
|
551
|
+
return this;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Draws a circle at the specified coordinates with the specified radius
|
|
556
|
+
* @param {number} x The center x position
|
|
557
|
+
* @param {number} y The center y position
|
|
558
|
+
* @param {number} radius The circles radius
|
|
559
|
+
* @param {number|colorFunction} color
|
|
560
|
+
* @returns {Image}
|
|
561
|
+
*/
|
|
562
|
+
drawCircle(x, y, radius, color) {
|
|
563
|
+
const radSquared = radius ** 2;
|
|
564
|
+
for (let currentY = Math.max(1, y - radius); currentY <= Math.min(y + radius, this.height); currentY++) {
|
|
565
|
+
for (let currentX = Math.max(1, x - radius); currentX <= Math.min(x + radius, this.width); currentX++) {
|
|
566
|
+
if ((currentX - x) ** 2 + (currentY - y) ** 2 < radSquared)
|
|
567
|
+
this.__set_pixel__(currentX, currentY, typeof color === 'function' ? color(currentX - x + radius, currentY - y + radius) : color);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return this;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Crops the image into a circle
|
|
576
|
+
* @param {boolean} [max=false] Whether to use the larger dimension for the size
|
|
577
|
+
* @param {number} [feathering=0] How much feathering to apply to the edges
|
|
578
|
+
* @returns {Image}
|
|
579
|
+
*/
|
|
580
|
+
cropCircle(max = false, feathering = 0) {
|
|
581
|
+
new v2(this.width, this.height, this.bitmap).crop('circle', feathering);
|
|
582
|
+
|
|
583
|
+
return this;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Sets the images opacity
|
|
588
|
+
* @param {number} opacity The opacity to apply (0..1)
|
|
589
|
+
* @param {boolean} absolute Whether to scale the current opacity (false) or just set the new opacity (true)
|
|
590
|
+
* @returns {Image}
|
|
591
|
+
*/
|
|
592
|
+
opacity(opacity, absolute = false) {
|
|
593
|
+
if (isNaN(opacity) || opacity < 0)
|
|
594
|
+
throw new RangeError('Invalid opacity value');
|
|
595
|
+
|
|
596
|
+
this.__set_channel_value__(opacity, absolute, 3);
|
|
597
|
+
|
|
598
|
+
return this;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Sets the red channels saturation
|
|
603
|
+
* @param {number} saturation The saturation to apply (0..1)
|
|
604
|
+
* @param {boolean} absolute Whether to scale the current saturation (false) or just set the new saturation (true)
|
|
605
|
+
* @returns {Image}
|
|
606
|
+
*/
|
|
607
|
+
red(saturation, absolute = false) {
|
|
608
|
+
if (isNaN(saturation) || saturation < 0)
|
|
609
|
+
throw new RangeError('Invalid saturation value');
|
|
610
|
+
|
|
611
|
+
this.__set_channel_value__(saturation, absolute, 0);
|
|
612
|
+
|
|
613
|
+
return this;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Sets the green channels saturation
|
|
618
|
+
* @param {number} saturation The saturation to apply (0..1)
|
|
619
|
+
* @param {boolean} absolute Whether to scale the current saturation (false) or just set the new saturation (true)
|
|
620
|
+
* @returns {Image}
|
|
621
|
+
*/
|
|
622
|
+
green(saturation, absolute = false) {
|
|
623
|
+
if (isNaN(saturation) || saturation < 0)
|
|
624
|
+
throw new RangeError('Invalid saturation value');
|
|
625
|
+
|
|
626
|
+
this.__set_channel_value__(saturation, absolute, 1);
|
|
627
|
+
|
|
628
|
+
return this;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Sets the blue channels saturation
|
|
633
|
+
* @param {number} saturation The saturation to apply (0..1)
|
|
634
|
+
* @param {boolean} absolute Whether to scale the current saturation (false) or just set the new saturation (true)
|
|
635
|
+
* @returns {Image}
|
|
636
|
+
*/
|
|
637
|
+
blue(saturation, absolute = false) {
|
|
638
|
+
if (isNaN(saturation) || saturation < 0)
|
|
639
|
+
throw new RangeError('Invalid saturation value');
|
|
640
|
+
|
|
641
|
+
this.__set_channel_value__(saturation, absolute, 2);
|
|
642
|
+
|
|
643
|
+
return this;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* @private
|
|
648
|
+
* @param {number} value
|
|
649
|
+
* @param {boolean} absolute
|
|
650
|
+
* @param {number} offset
|
|
651
|
+
*/
|
|
652
|
+
__set_channel_value__(value, absolute, offset) {
|
|
653
|
+
for (let i = offset; i < this.bitmap.length; i += 4)
|
|
654
|
+
this.bitmap[i] = value * (absolute ? 255 : this.bitmap[i]);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Sets the brightness of the image
|
|
659
|
+
* @param {number} value The lightness to apply (0..1)
|
|
660
|
+
* @param {boolean} absolute Whether to scale the current lightness (false) or just set the new lightness (true)
|
|
661
|
+
* @returns {Image}
|
|
662
|
+
*/
|
|
663
|
+
lightness(value, absolute = false) {
|
|
664
|
+
if (isNaN(value) || value < 0)
|
|
665
|
+
throw new RangeError('Invalid lightness value');
|
|
666
|
+
|
|
667
|
+
return this.fill((x, y) => {
|
|
668
|
+
const [h, s, l, a] = Image.rgbaToHSLA(...this.getRGBAAt(x, y));
|
|
669
|
+
return Image.hslaToColor(h, s, value * (absolute ? 1 : l), a);
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Sets the saturation of the image
|
|
675
|
+
* @param {number} value The saturation to apply (0..1)
|
|
676
|
+
* @param {boolean} absolute Whether to scale the current saturation (false) or just set the new saturation (true)
|
|
677
|
+
* @returns {Image}
|
|
678
|
+
*/
|
|
679
|
+
saturation(value, absolute = false) {
|
|
680
|
+
if (isNaN(value) || value < 0)
|
|
681
|
+
throw new RangeError('Invalid saturation value');
|
|
682
|
+
|
|
683
|
+
return this.fill((x, y) => {
|
|
684
|
+
const [h, s, l, a] = Image.rgbaToHSLA(...this.getRGBAAt(x, y));
|
|
685
|
+
return Image.hslaToColor(h, value * (absolute ? 1 : s), l, a);
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Composites (overlays) the source onto this image at the specified coordinates
|
|
691
|
+
* @param {Image} source The image to place
|
|
692
|
+
* @param {number} [x=0] The x position to place the image at
|
|
693
|
+
* @param {number} [y=0] The y position to place the image at
|
|
694
|
+
* @returns {Image}
|
|
695
|
+
*/
|
|
696
|
+
composite(source, x = 0, y = 0) {
|
|
697
|
+
new v2(this.width, this.height, this.bitmap).overlay(new v2(source.width, source.height, source.bitmap), x, y);
|
|
698
|
+
|
|
699
|
+
return this;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Inverts the images colors
|
|
704
|
+
* @returns {Image}
|
|
705
|
+
*/
|
|
706
|
+
invert() {
|
|
707
|
+
for (const [x, y, color] of this.iterateWithColors())
|
|
708
|
+
this.__set_pixel__(x, y, ((0xffffffff - color) & 0xffffff00) | (color & 0xff));
|
|
709
|
+
|
|
710
|
+
return this;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Inverts the images value (lightness)
|
|
715
|
+
* @returns {Image}
|
|
716
|
+
*/
|
|
717
|
+
invertValue() {
|
|
718
|
+
for (const [x, y, color] of this.iterateWithColors()) {
|
|
719
|
+
const [h, s, l, a] = Image.rgbaToHSLA(...Image.colorToRGBA(color));
|
|
720
|
+
this.__set_pixel__(x, y, Image.hslaToColor(h, s, 1 - l, a));
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return this;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Inverts the images saturation
|
|
728
|
+
* @returns {Image}
|
|
729
|
+
*/
|
|
730
|
+
invertSaturation() {
|
|
731
|
+
for (const [x, y, color] of this.iterateWithColors()) {
|
|
732
|
+
const [h, s, l, a] = Image.rgbaToHSLA(...Image.colorToRGBA(color));
|
|
733
|
+
this.__set_pixel__(x, y, Image.hslaToColor(h, 1 - s, l, a));
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return this;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Inverts the images hue
|
|
741
|
+
* @returns {Image}
|
|
742
|
+
*/
|
|
743
|
+
invertHue() {
|
|
744
|
+
for (const [x, y, color] of this.iterateWithColors()) {
|
|
745
|
+
const [h, s, l, a] = Image.rgbaToHSLA(...Image.colorToRGBA(color));
|
|
746
|
+
this.__set_pixel__(x, y, Image.hslaToColor(1 - h, s, l, a));
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return this;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Shifts the images hue
|
|
754
|
+
* @param {number} degrees How many degrees to shift the hue by
|
|
755
|
+
*/
|
|
756
|
+
hueShift(degrees) {
|
|
757
|
+
for (const [x, y, color] of this.iterateWithColors()) {
|
|
758
|
+
const [h, s, l, a] = Image.rgbaToHSLA(...Image.colorToRGBA(color));
|
|
759
|
+
this.__set_pixel__(x, y, Image.hslaToColor(h + degrees / 360, s, l, a));
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return this;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Gets the average color of the image
|
|
767
|
+
* @returns {number}
|
|
768
|
+
*/
|
|
769
|
+
averageColor() {
|
|
770
|
+
let colorAvg = [0, 0, 0];
|
|
771
|
+
let divisor = 0;
|
|
772
|
+
for (let idx = 0; idx < this.bitmap.length; idx += 4) {
|
|
773
|
+
const rgba = this.bitmap.subarray(idx, idx + 4);
|
|
774
|
+
for (let i = 0; i < 3; i++)
|
|
775
|
+
colorAvg[i] += rgba[i];
|
|
776
|
+
divisor += rgba[3] / 255;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return Image.rgbaToColor(...colorAvg.map(v => v / divisor), 0xff);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Gets the images dominant color
|
|
784
|
+
* @param {boolean} [ignoreBlack=true] Whether to ignore dark colors below the threshold
|
|
785
|
+
* @param {boolean} [ignoreWhite=true] Whether to ignore light colors above the threshold
|
|
786
|
+
* @param {number} [bwThreshold=0xf] The black/white threshold (0-64)
|
|
787
|
+
* @return {number} The images dominant color
|
|
788
|
+
*/
|
|
789
|
+
dominantColor(ignoreBlack = true, ignoreWhite = true, bwThreshold = 0xf) {
|
|
790
|
+
const colorCounts = new Array(0x3ffff);
|
|
791
|
+
for (let i = 0; i < this.bitmap.length; i += 4) {
|
|
792
|
+
const color = this.__view__.getUint32(i, false);
|
|
793
|
+
const [h, s, l] = Image.rgbaToHSLA(...Image.colorToRGBA(color)).map(v => (~~(v * 0x3f)));
|
|
794
|
+
if (ignoreBlack && l < bwThreshold) continue;
|
|
795
|
+
if (ignoreWhite && l > 0x3f - bwThreshold) continue;
|
|
796
|
+
const key = h << 12 | s << 6 | l;
|
|
797
|
+
colorCounts[key] = (colorCounts[key] || 0) + 1;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
let maxColorCount = -1;
|
|
801
|
+
let mostProminentValue = 0;
|
|
802
|
+
colorCounts.forEach((el, i) => {
|
|
803
|
+
if (el < maxColorCount) return;
|
|
804
|
+
maxColorCount = el;
|
|
805
|
+
mostProminentValue = i;
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
if (mostProminentValue === -1)
|
|
809
|
+
return this.dominantColor(ignoreBlack, ignoreWhite, bwThreshold - 1);
|
|
810
|
+
|
|
811
|
+
const h = (mostProminentValue >>> 12) & 0x3f;
|
|
812
|
+
const s = (mostProminentValue >>> 6) & 0x3f;
|
|
813
|
+
const l = mostProminentValue & 0x3f;
|
|
814
|
+
|
|
815
|
+
return Image.hslaToColor(h / 0x3f, s / 0x3f, l / 0x3f, 1);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Rotates the image the given amount of degrees
|
|
820
|
+
* @param {number} angle The angle to rotate the image for (in degrees)
|
|
821
|
+
* @param {boolean} resize Whether to resize the image so it fits all pixels or just ignore outlying pixels
|
|
822
|
+
*/
|
|
823
|
+
rotate(angle, resize = true) {
|
|
824
|
+
const frame = new v2(this.width, this.height, this.bitmap).rotate(360 - (angle % 360), resize);
|
|
825
|
+
|
|
826
|
+
const out = new Image(frame.width, frame.height);
|
|
827
|
+
|
|
828
|
+
out.bitmap.set(frame.u8);
|
|
829
|
+
return this.__apply__(out);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* @private
|
|
834
|
+
* @param {Image|Frame} image
|
|
835
|
+
* @returns {Image|Frame}
|
|
836
|
+
*/
|
|
837
|
+
__apply__(image) {
|
|
838
|
+
this.__width__ = image.__width__;
|
|
839
|
+
this.__height__ = image.__height__;
|
|
840
|
+
this.__view__ = image.__view__;
|
|
841
|
+
this.__u32__ = image.__u32__;
|
|
842
|
+
this.bitmap = image.bitmap;
|
|
843
|
+
|
|
844
|
+
if (image instanceof Frame)
|
|
845
|
+
return Frame.from(this, image.duration, image.xOffset, image.yOffset, image.disposalMode);
|
|
846
|
+
|
|
847
|
+
return this;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Creates a multi-point gradient generator
|
|
852
|
+
* @param {Object<number, number>} colors The gradient points to use (e.g. `{0: 0xff0000ff, 1: 0x00ff00ff}`)
|
|
853
|
+
* @return {(function(number): number)} The gradient generator. The function argument is the position in the gradient (0..1).
|
|
854
|
+
*/
|
|
855
|
+
static gradient(colors) {
|
|
856
|
+
const entries = Object.entries(colors).sort((a, b) => a[0] - b[0]);
|
|
857
|
+
const positions = entries.map(e => parseFloat(e[0]));
|
|
858
|
+
const values = entries.map(e => e[1]);
|
|
859
|
+
|
|
860
|
+
if (positions.length === 0) throw new RangeError('Invalid gradient point count');
|
|
861
|
+
else if (positions.length === 1) {
|
|
862
|
+
return () => values[0];
|
|
863
|
+
} else if (positions.length === 2) {
|
|
864
|
+
const gradient = this.__gradient__(values[0], values[1]);
|
|
865
|
+
return position => {
|
|
866
|
+
if (position <= positions[0]) return values[0];
|
|
867
|
+
if (position >= positions[1]) return values[1];
|
|
868
|
+
return gradient((position - positions[0]) / (positions[1] - positions[0]));
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const minDef = Math.min(...positions);
|
|
873
|
+
const maxDef = Math.max(...positions);
|
|
874
|
+
let gradients = [];
|
|
875
|
+
|
|
876
|
+
for (let i = 0; i < positions.length; i++) {
|
|
877
|
+
let minPos = positions[i - 1];
|
|
878
|
+
if (minPos === undefined) continue;
|
|
879
|
+
|
|
880
|
+
let maxPos = positions[i];
|
|
881
|
+
|
|
882
|
+
let minVal = values[i - 1];
|
|
883
|
+
if (minVal === undefined) minVal = values[i];
|
|
884
|
+
|
|
885
|
+
const maxVal = values[i];
|
|
886
|
+
const gradient = this.__gradient__(minVal, maxVal);
|
|
887
|
+
|
|
888
|
+
gradients.push({min: minPos, max: maxPos, gradient});
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
return position => {
|
|
892
|
+
if (position <= minDef) return gradients[0].gradient(0);
|
|
893
|
+
if (position >= maxDef) return gradients[gradients.length - 1].gradient(1);
|
|
894
|
+
|
|
895
|
+
for (const gradient of gradients)
|
|
896
|
+
if (position >= gradient.min && position <= gradient.max)
|
|
897
|
+
return gradient.gradient((position - gradient.min) / (gradient.max - gradient.min));
|
|
898
|
+
throw new RangeError(`Invalid gradient position: ${position}`);
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Rounds the images corners
|
|
904
|
+
* @param {number} [radius=min(width,height)/4] The radius of the corners
|
|
905
|
+
* @return {Image}
|
|
906
|
+
*/
|
|
907
|
+
roundCorners(radius = Math.min(this.width, this.height) / 4) {
|
|
908
|
+
const radSquared = radius ** 2;
|
|
909
|
+
for (let x = 1; x <= radius; x++) {
|
|
910
|
+
const xRad = (x - radius) ** 2;
|
|
911
|
+
for (let y = 1; y <= radius; y++) {
|
|
912
|
+
if (xRad + (y - radius) ** 2 > radSquared)
|
|
913
|
+
this.bitmap[((y - 1) * this.width + x - 1) * 4 + 3] = 0;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
for (let x = 1; x <= radius; x++) {
|
|
918
|
+
const xRad = (x - radius) ** 2;
|
|
919
|
+
for (let y = this.height - radius; y <= this.height; y++) {
|
|
920
|
+
if (xRad + ((this.height - y) - radius) ** 2 > radSquared)
|
|
921
|
+
this.bitmap[((y - 1) * this.width + x - 1) * 4 + 3] = 0;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
for (let x = this.width - radius; x <= this.width; x++) {
|
|
926
|
+
const xRad = ((this.width - x) - radius) ** 2;
|
|
927
|
+
for (let y = 1; y <= radius; y++) {
|
|
928
|
+
if (xRad + (y - radius) ** 2 > radSquared)
|
|
929
|
+
this.bitmap[((y - 1) * this.width + x - 1) * 4 + 3] = 0;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
for (let x = this.width - radius; x <= this.width; x++) {
|
|
934
|
+
const xRad = ((this.width - x) - radius) ** 2;
|
|
935
|
+
for (let y = this.height - radius; y <= this.height; y++) {
|
|
936
|
+
if (xRad + ((this.height - y) - radius) ** 2 > radSquared)
|
|
937
|
+
this.bitmap[((y - 1) * this.width + x - 1) * 4 + 3] = 0;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
return this;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* @private
|
|
946
|
+
*/
|
|
947
|
+
static __gradient__(startColor, endColor) {
|
|
948
|
+
const sr = startColor >>> 24;
|
|
949
|
+
const sg = startColor >> 16 & 0xff;
|
|
950
|
+
const sb = startColor >> 8 & 0xff;
|
|
951
|
+
const sa = startColor & 0xff;
|
|
952
|
+
const er = (endColor >>> 24) - sr;
|
|
953
|
+
const eg = (endColor >> 16 & 0xff) - sg;
|
|
954
|
+
const eb = (endColor >> 8 & 0xff) - sb;
|
|
955
|
+
const ea = (endColor & 0xff) - sa;
|
|
956
|
+
|
|
957
|
+
return position => {
|
|
958
|
+
const r = sr + position * er;
|
|
959
|
+
const g = sg + position * eg;
|
|
960
|
+
const b = sb + position * eb;
|
|
961
|
+
const a = sa + position * ea;
|
|
962
|
+
return (((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | (a & 0xff));
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
fisheye(radius = 2) {
|
|
967
|
+
const r = new Image(this.width, this.height);
|
|
968
|
+
|
|
969
|
+
const w = this.width;
|
|
970
|
+
const h = this.height;
|
|
971
|
+
const tu32 = this.__u32__;
|
|
972
|
+
const ru32 = r.__u32__;
|
|
973
|
+
const iw = 1 / w;
|
|
974
|
+
const ih = 1 / h;
|
|
975
|
+
|
|
976
|
+
for (const [x, y] of this) {
|
|
977
|
+
const xco = x * iw - .5;
|
|
978
|
+
const yco = y * ih - .5;
|
|
979
|
+
const dfc = Math.sqrt(xco ** 2 + yco ** 2);
|
|
980
|
+
const dis = 2 * dfc ** radius;
|
|
981
|
+
const nx = ((dis * xco / dfc + 0.5) * w) | 0;
|
|
982
|
+
const ny = ((dis * yco / dfc + 0.5) * h) | 0;
|
|
983
|
+
|
|
984
|
+
if (nx < 1 || nx > w || ny < 1 || ny > h || isNaN(nx) || isNaN(ny))
|
|
985
|
+
continue;
|
|
986
|
+
|
|
987
|
+
ru32[y * w + x] = tu32[w * ny + nx];
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const cO = tu32.length * .5 + w / 2;
|
|
991
|
+
ru32[cO] = tu32[cO];
|
|
992
|
+
|
|
993
|
+
return this.__apply__(r);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* @typedef {object} PNGMetadata
|
|
998
|
+
* @property {string} [title] The images title
|
|
999
|
+
* @property {string} [author] The images author
|
|
1000
|
+
* @property {string} [description] The images description
|
|
1001
|
+
* @property {string} [copyright] The images copyright info
|
|
1002
|
+
* @property {string|number|Date} [creationTime=Date.now()] The images creation timestamp
|
|
1003
|
+
* @property {string} [software="github.com/matmen/ImageScript vX.X.X"] The software used to create this image
|
|
1004
|
+
* @property {string} [disclaimer] A disclaimer for the image
|
|
1005
|
+
* @property {string} [warning] A warning for the image
|
|
1006
|
+
* @property {string} [source] The images source
|
|
1007
|
+
* @property {string} [comment] A comment for the image
|
|
1008
|
+
*/
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Encodes the image into a PNG
|
|
1012
|
+
* @param {number} compression The compression level to use (0-9)
|
|
1013
|
+
* @param {PNGMetadata} [meta={}] Image metadata
|
|
1014
|
+
* @return {Promise<Uint8Array>} The encoded data
|
|
1015
|
+
*/
|
|
1016
|
+
async encode(compression = 1, {
|
|
1017
|
+
title,
|
|
1018
|
+
author,
|
|
1019
|
+
description,
|
|
1020
|
+
copyright,
|
|
1021
|
+
creationTime,
|
|
1022
|
+
software,
|
|
1023
|
+
disclaimer,
|
|
1024
|
+
warning,
|
|
1025
|
+
source,
|
|
1026
|
+
comment
|
|
1027
|
+
} = {}) {
|
|
1028
|
+
return png.encode(this.bitmap, {
|
|
1029
|
+
width: this.width,
|
|
1030
|
+
height: this.height,
|
|
1031
|
+
level: compression,
|
|
1032
|
+
channels: 4,
|
|
1033
|
+
text: {
|
|
1034
|
+
Title: title,
|
|
1035
|
+
Author: author,
|
|
1036
|
+
Description: description,
|
|
1037
|
+
Copyright: copyright,
|
|
1038
|
+
'Creation Time': new Date(creationTime === undefined ? Date.now() : creationTime).toUTCString(),
|
|
1039
|
+
Software: software === undefined ? `github.com/matmen/ImageScript v${version}` : software,
|
|
1040
|
+
Disclaimer: disclaimer,
|
|
1041
|
+
Warning: warning,
|
|
1042
|
+
Source: source,
|
|
1043
|
+
Comment: comment
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Encodes the image into a JPEG
|
|
1050
|
+
* @param {number} [quality=90] The JPEG quality to use (1-100)
|
|
1051
|
+
* @return {Promise<Uint8Array>}
|
|
1052
|
+
*/
|
|
1053
|
+
async encodeJPEG(quality = 90) {
|
|
1054
|
+
return codecs.jpeg.encode_async(this.bitmap, {
|
|
1055
|
+
quality,
|
|
1056
|
+
width: this.width,
|
|
1057
|
+
height: this.height,
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Encodes the image into a WEBP
|
|
1063
|
+
* @param {null|number} [quality=null] The WEBP quality to use (0-100) (null is lossless)
|
|
1064
|
+
* @return {Promise<Uint8Array>}
|
|
1065
|
+
*/
|
|
1066
|
+
async encodeWEBP(quality = null) {
|
|
1067
|
+
return codecs.webp.encode_async(this.bitmap, {
|
|
1068
|
+
quality,
|
|
1069
|
+
width: this.width,
|
|
1070
|
+
height: this.height,
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Decodes an image (PNG, JPEG or TIFF)
|
|
1076
|
+
* @param {Buffer|Uint8Array} data The binary data to decode
|
|
1077
|
+
* @return {Promise<Image>} The decoded image
|
|
1078
|
+
*/
|
|
1079
|
+
static async decode(data) {
|
|
1080
|
+
let image;
|
|
1081
|
+
|
|
1082
|
+
data = mem.view(data);
|
|
1083
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
1084
|
+
|
|
1085
|
+
if (ImageType.isPNG(view)) { // PNG
|
|
1086
|
+
const {width, height, framebuffer} = (await pnglib.init()).decode(data);
|
|
1087
|
+
image = new Image(width, height);
|
|
1088
|
+
image.bitmap.set(framebuffer);
|
|
1089
|
+
} else if (ImageType.isJPEG(view)) { // JPEG
|
|
1090
|
+
const framebuffer = (await jpeglib.init()).decode(data);
|
|
1091
|
+
|
|
1092
|
+
const width = framebuffer.width;
|
|
1093
|
+
const height = framebuffer.height;
|
|
1094
|
+
const pixelType = framebuffer.format;
|
|
1095
|
+
|
|
1096
|
+
image = new Image(width, height);
|
|
1097
|
+
const buffer = framebuffer.buffer;
|
|
1098
|
+
|
|
1099
|
+
if (pixelType === 0) {
|
|
1100
|
+
const view = new DataView(image.bitmap.buffer);
|
|
1101
|
+
|
|
1102
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
1103
|
+
const pixel = buffer[i];
|
|
1104
|
+
view.setUint32(i * 4, pixel << 24 | pixel << 16 | pixel << 8 | 0xff, false);
|
|
1105
|
+
}
|
|
1106
|
+
} else if (pixelType === 1) {
|
|
1107
|
+
image.bitmap.fill(0xff);
|
|
1108
|
+
for (let i = 0; i < width * height; i++)
|
|
1109
|
+
image.bitmap.set(buffer.subarray(i * 3, i * 3 + 3), i * 4);
|
|
1110
|
+
} else if (pixelType === 2) {
|
|
1111
|
+
for (let i = 0; i < buffer.length; i += 4) {
|
|
1112
|
+
image.bitmap[i] = 0xff * (1 - buffer[i] / 0xff) * (1 - buffer[i + 3] / 0xff);
|
|
1113
|
+
image.bitmap[i + 1] = 0xff * (1 - buffer[i + 1] / 0xff) * (1 - buffer[i + 3] / 0xff);
|
|
1114
|
+
image.bitmap[i + 2] = 0xff * (1 - buffer[i + 2] / 0xff) * (1 - buffer[i + 3] / 0xff);
|
|
1115
|
+
image.bitmap[i + 3] = 0xff;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
} else if (ImageType.isTIFF(view)) { // TIFF
|
|
1119
|
+
const framebuffer = (await tifflib.init()).decode(data);
|
|
1120
|
+
image = new Image(framebuffer.width, framebuffer.height);
|
|
1121
|
+
|
|
1122
|
+
image.bitmap.set(framebuffer.buffer);
|
|
1123
|
+
} else throw new Error('Unsupported image type');
|
|
1124
|
+
|
|
1125
|
+
return image;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Scale the SVG by the given amount. For use with {@link Image.renderSVG}
|
|
1130
|
+
* @return {number}
|
|
1131
|
+
*/
|
|
1132
|
+
static get SVG_MODE_SCALE() {
|
|
1133
|
+
return 1;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* Scale the SVG to fit the given width. For use with {@link Image.renderSVG}
|
|
1138
|
+
* @return {number}
|
|
1139
|
+
*/
|
|
1140
|
+
static get SVG_MODE_WIDTH() {
|
|
1141
|
+
return 2;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Scale the SVG to fit the given height. For use with {@link Image.renderSVG}
|
|
1146
|
+
* @return {number}
|
|
1147
|
+
*/
|
|
1148
|
+
static get SVG_MODE_HEIGHT() {
|
|
1149
|
+
return 3;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* Creates a new image from the given SVG
|
|
1154
|
+
* @param {string} svg The SVG content
|
|
1155
|
+
* @param {number} size The size to use
|
|
1156
|
+
* @param {number} mode The SVG resizing mode to use (one of {@link SVG_MODE_SCALE}, {@link SVG_MODE_WIDTH}, {@link SVG_MODE_HEIGHT})
|
|
1157
|
+
* @return {Promise<Image>} The rendered SVG graphic
|
|
1158
|
+
*/
|
|
1159
|
+
static async renderSVG(svg, size = 1, mode = this.SVG_MODE_SCALE) {
|
|
1160
|
+
if (![this.SVG_MODE_WIDTH, this.SVG_MODE_HEIGHT, this.SVG_MODE_SCALE].includes(mode))
|
|
1161
|
+
throw new Error('Invalid SVG scaling mode');
|
|
1162
|
+
|
|
1163
|
+
if (mode === this.SVG_MODE_SCALE && size <= 0)
|
|
1164
|
+
throw new RangeError('SVG scale must be > 0');
|
|
1165
|
+
if (mode !== this.SVG_MODE_SCALE && size < 1)
|
|
1166
|
+
throw new RangeError('SVG size must be >= 1')
|
|
1167
|
+
|
|
1168
|
+
if (typeof svg === 'string') svg = Buffer.from(svg);
|
|
1169
|
+
const framebuffer = (await svglib.init()).rasterize(svg, mode, size);
|
|
1170
|
+
|
|
1171
|
+
const image = new Image(framebuffer.width, framebuffer.height);
|
|
1172
|
+
|
|
1173
|
+
image.bitmap.set(framebuffer.buffer);
|
|
1174
|
+
|
|
1175
|
+
return image;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Creates a new image containing the rendered text.
|
|
1180
|
+
* @param {Uint8Array} font TrueType (ttf/ttc) or OpenType (otf) font buffer to use
|
|
1181
|
+
* @param {number} scale Font size to use
|
|
1182
|
+
* @param {string} text Text to render
|
|
1183
|
+
* @param {number} [color=0xffffffff] Text color to use
|
|
1184
|
+
* @param {TextLayout} [layout] The text layout to use
|
|
1185
|
+
* @return {Promise<Image>} The rendered text
|
|
1186
|
+
*/
|
|
1187
|
+
static async renderText(font, scale, text, color = 0xffffffff, layout = new TextLayout()) {
|
|
1188
|
+
const { Font, Layout } = await fontlib.init();
|
|
1189
|
+
|
|
1190
|
+
font = new Font(scale, font);
|
|
1191
|
+
const [r, g, b, a] = Image.colorToRGBA(color);
|
|
1192
|
+
|
|
1193
|
+
const layoutOptions = new Layout();
|
|
1194
|
+
|
|
1195
|
+
layoutOptions.reset({
|
|
1196
|
+
max_width: layout.maxWidth,
|
|
1197
|
+
max_height: layout.maxHeight,
|
|
1198
|
+
wrap_style: layout.wrapStyle,
|
|
1199
|
+
vertical_align: layout.verticalAlign,
|
|
1200
|
+
horizontal_align: layout.horizontalAlign,
|
|
1201
|
+
wrap_hard_breaks: layout.wrapHardBreaks
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
layoutOptions.append(font, text, {scale});
|
|
1205
|
+
const framebuffer = layoutOptions.rasterize(r, g, b);
|
|
1206
|
+
const image = new Image(framebuffer.width, framebuffer.height);
|
|
1207
|
+
|
|
1208
|
+
image.bitmap.set(framebuffer.buffer);
|
|
1209
|
+
|
|
1210
|
+
if (image.height > layout.maxHeight)
|
|
1211
|
+
image.crop(0, 0, image.width, Math.floor(layoutOptions.lines() / image.height * layout.maxHeight) * (image.height / layoutOptions.lines()));
|
|
1212
|
+
|
|
1213
|
+
font.free();
|
|
1214
|
+
layoutOptions.free();
|
|
1215
|
+
return image.opacity(a / 0xff);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* Represents a frame in a GIF
|
|
1222
|
+
* @extends Image
|
|
1223
|
+
*/
|
|
1224
|
+
class Frame extends Image {
|
|
1225
|
+
/**
|
|
1226
|
+
* GIF frame disposal mode KEEP. For use with {@link Frame}
|
|
1227
|
+
* @returns {string}
|
|
1228
|
+
*/
|
|
1229
|
+
static get DISPOSAL_KEEP() {
|
|
1230
|
+
return 'keep';
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* GIF frame disposal mode PREVIOUS. For use with {@link Frame}
|
|
1235
|
+
* @returns {string}
|
|
1236
|
+
*/
|
|
1237
|
+
static get DISPOSAL_PREVIOUS() {
|
|
1238
|
+
return 'previous';
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* GIF frame disposal mode BACKGROUND. For use with {@link Frame}
|
|
1243
|
+
* @returns {string}
|
|
1244
|
+
*/
|
|
1245
|
+
static get DISPOSAL_BACKGROUND() {
|
|
1246
|
+
return 'background';
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
static __convert_disposal_mode__(mode) {
|
|
1250
|
+
if (typeof mode === 'string')
|
|
1251
|
+
mode = ['any', 'keep', 'previous', 'background'].indexOf(mode);
|
|
1252
|
+
if (mode < 0 || mode > 3)
|
|
1253
|
+
throw new RangeError('Invalid disposal mode');
|
|
1254
|
+
|
|
1255
|
+
return mode;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Creates a new, blank frame
|
|
1260
|
+
* @param {number} width
|
|
1261
|
+
* @param {number} height
|
|
1262
|
+
* @param {number} [duration = 100] The frames duration (in ms)
|
|
1263
|
+
* @param {number} [xOffset=0] The frames offset on the x-axis
|
|
1264
|
+
* @param {number} [yOffset=0] The frames offset on the y-axis
|
|
1265
|
+
* @param {string|number} [disposalMode=Frame.DISPOSAL_KEEP] The frame's disposal mode ({@link Frame.DISPOSAL_KEEP}, {@link Frame.DISPOSAL_PREVIOUS} or {@link Frame.DISPOSAL_BACKGROUND})
|
|
1266
|
+
* @return {Frame}
|
|
1267
|
+
*/
|
|
1268
|
+
constructor(width, height, duration = 100, xOffset = 0, yOffset = 0, disposalMode = Frame.DISPOSAL_KEEP) {
|
|
1269
|
+
if (isNaN(duration) || duration < 0)
|
|
1270
|
+
throw new RangeError('Invalid frame duration');
|
|
1271
|
+
|
|
1272
|
+
super(width, height);
|
|
1273
|
+
this.duration = duration;
|
|
1274
|
+
this.xOffset = xOffset;
|
|
1275
|
+
this.yOffset = yOffset;
|
|
1276
|
+
this.disposalMode = disposalMode;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* The Frame's disposal mode
|
|
1281
|
+
* @returns {number}
|
|
1282
|
+
*/
|
|
1283
|
+
get disposalMode() {
|
|
1284
|
+
return this.__disposalMode__;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Sets the frame's disposal mode, converting it to the internal numeric value.
|
|
1289
|
+
* @param {string|number} disposalMode The frame's disposal mode
|
|
1290
|
+
*/
|
|
1291
|
+
set disposalMode(disposalMode) {
|
|
1292
|
+
this.__disposalMode__ = Frame.__convert_disposal_mode__(disposalMode);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
toString() {
|
|
1296
|
+
return `Frame<${this.width}x${this.height}x${this.duration}ms>`;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/**
|
|
1300
|
+
* Converts an Image instance to a Frame, cloning it in the process
|
|
1301
|
+
* @param {Image} image The image to create the frame from
|
|
1302
|
+
* @param {number} [duration = 100] The frames duration (in ms)
|
|
1303
|
+
* @param {number} [xOffset=0] The frames offset on the x-axis
|
|
1304
|
+
* @param {number} [yOffset=0] The frames offset on the y-axis
|
|
1305
|
+
* @param {string|number} [disposalMode=Frame.DISPOSAL_KEEP] The frames disposal mode ({@link Frame.DISPOSAL_KEEP}, {@link Frame.DISPOSAL_PREVIOUS} or {@link Frame.DISPOSAL_BACKGROUND})
|
|
1306
|
+
* @return {Frame}
|
|
1307
|
+
*/
|
|
1308
|
+
static from(image, duration, xOffset, yOffset, disposalMode = Frame.DISPOSAL_KEEP) {
|
|
1309
|
+
if (!(image instanceof Image))
|
|
1310
|
+
throw new TypeError('Invalid image passed');
|
|
1311
|
+
|
|
1312
|
+
const frame = new Frame(image.width, image.height, duration, xOffset, yOffset, disposalMode);
|
|
1313
|
+
frame.bitmap.set(image.bitmap);
|
|
1314
|
+
|
|
1315
|
+
return frame;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
resize(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) {
|
|
1319
|
+
const originalWidth = this.width;
|
|
1320
|
+
const originalHeight = this.height;
|
|
1321
|
+
|
|
1322
|
+
const result = super.resize(width, height, mode);
|
|
1323
|
+
|
|
1324
|
+
this.xOffset *= result.width / originalWidth;
|
|
1325
|
+
this.yOffset *= result.height / originalHeight;
|
|
1326
|
+
|
|
1327
|
+
return result;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* Represents a GIF image as an array of frames
|
|
1333
|
+
* @extends Array<Frame>
|
|
1334
|
+
*/
|
|
1335
|
+
class GIF extends Array {
|
|
1336
|
+
/**
|
|
1337
|
+
* Creates a new GIF image.
|
|
1338
|
+
* @param {Frame[]} frames The frames to create the GIF from
|
|
1339
|
+
* @param {number} [loopCount=0] How often to loop the GIF for (-1 = unlimited)
|
|
1340
|
+
* @property {number} loopCount How often the GIF will loop for
|
|
1341
|
+
*/
|
|
1342
|
+
constructor(frames, loopCount = -1) {
|
|
1343
|
+
super(...frames);
|
|
1344
|
+
|
|
1345
|
+
for (const frame of this)
|
|
1346
|
+
if (!(frame instanceof Frame))
|
|
1347
|
+
throw new TypeError(`Frame ${this.indexOf(frame)} is not an instance of Frame`);
|
|
1348
|
+
|
|
1349
|
+
if (loopCount < -1 || isNaN(loopCount))
|
|
1350
|
+
throw new RangeError('Invalid loop count');
|
|
1351
|
+
|
|
1352
|
+
this.loopCount = loopCount;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/**
|
|
1356
|
+
* The GIFs width
|
|
1357
|
+
* @returns {number}
|
|
1358
|
+
*/
|
|
1359
|
+
get width() {
|
|
1360
|
+
let max = 0;
|
|
1361
|
+
for (const frame of this) {
|
|
1362
|
+
let width = frame.width + frame.xOffset;
|
|
1363
|
+
if (max < width)
|
|
1364
|
+
max = width;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
return max;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* The GIFs height
|
|
1372
|
+
* @returns {number}
|
|
1373
|
+
*/
|
|
1374
|
+
get height() {
|
|
1375
|
+
let max = 0;
|
|
1376
|
+
for (const frame of this) {
|
|
1377
|
+
let height = frame.height + frame.yOffset;
|
|
1378
|
+
if (max < height)
|
|
1379
|
+
max = height;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
return max;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
toString() {
|
|
1386
|
+
return `GIF<${this.width}x${this.height}x${this.duration}ms>`;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* @returns {Generator<Frame, void, *>}
|
|
1391
|
+
*/
|
|
1392
|
+
* [Symbol.iterator]() {
|
|
1393
|
+
for (let i = 0; i < this.length; i++)
|
|
1394
|
+
yield this[i];
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
slice(start, end) {
|
|
1398
|
+
if (end === Infinity)
|
|
1399
|
+
end = this.length;
|
|
1400
|
+
const frames = new Array(end - start);
|
|
1401
|
+
for (let i = 0; i < frames.length; i++)
|
|
1402
|
+
frames[i] = this[i + start];
|
|
1403
|
+
return new GIF(frames, this.loopCount);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* The GIFs duration (in ms)
|
|
1408
|
+
* @return {number}
|
|
1409
|
+
*/
|
|
1410
|
+
get duration() {
|
|
1411
|
+
return this.reduce((acc, frame) => acc + frame.duration, 0);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
/**
|
|
1415
|
+
* Encodes the image into a GIF
|
|
1416
|
+
* @param {number} [quality=95] GIF quality 0-100
|
|
1417
|
+
* @return {Promise<Uint8Array>} The encoded data
|
|
1418
|
+
*/
|
|
1419
|
+
async encode(quality = 95) {
|
|
1420
|
+
const encoder = new codecs.gif.encoder(this.width, this.height);
|
|
1421
|
+
|
|
1422
|
+
for (const frame of this) {
|
|
1423
|
+
if (!(frame instanceof Frame)) throw new Error('GIF contains invalid frames');
|
|
1424
|
+
|
|
1425
|
+
encoder.add(frame.bitmap, {
|
|
1426
|
+
quality,
|
|
1427
|
+
x: frame.xOffset,
|
|
1428
|
+
y: frame.yOffset,
|
|
1429
|
+
width: frame.width,
|
|
1430
|
+
speed: null, // 1-10
|
|
1431
|
+
height: frame.height,
|
|
1432
|
+
colors: null, // 2-256
|
|
1433
|
+
delay: ~~(frame.duration / 10),
|
|
1434
|
+
dispose: ['any', 'keep', 'previous', 'background'][frame.disposalMode],
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
return encoder.finish({ repeat: -1 === this.loopCount ? null : this.loopCount });
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/**
|
|
1442
|
+
* Decodes a GIF image
|
|
1443
|
+
* @param {Buffer|Uint8Array} data The binary data to decode
|
|
1444
|
+
* @param {boolean} [onlyExtractFirstFrame=false] Whether to end GIF decoding after the first frame
|
|
1445
|
+
* @return {Promise<GIF>} The decoded GIF
|
|
1446
|
+
*/
|
|
1447
|
+
static async decode(data, onlyExtractFirstFrame = false) {
|
|
1448
|
+
let image;
|
|
1449
|
+
data = mem.view(data);
|
|
1450
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
1451
|
+
|
|
1452
|
+
if (ImageType.isGIF(view)) { // GIF
|
|
1453
|
+
const frames = [];
|
|
1454
|
+
const decoder = new (await giflib.init()).Decoder(data);
|
|
1455
|
+
|
|
1456
|
+
const gwidth = decoder.width | 0;
|
|
1457
|
+
const gheight = decoder.height | 0;
|
|
1458
|
+
const u32 = new Uint32Array(decoder.width * decoder.height);
|
|
1459
|
+
const u8 = new Uint8Array(u32.buffer, u32.byteOffset, u32.byteLength);
|
|
1460
|
+
|
|
1461
|
+
for (const frame of decoder.frames()) {
|
|
1462
|
+
let offset8 = 0 | 0;
|
|
1463
|
+
let offset32 = 0 | 0;
|
|
1464
|
+
const fx = frame.x | 0;
|
|
1465
|
+
const fy = frame.y | 0;
|
|
1466
|
+
const f8 = frame.buffer;
|
|
1467
|
+
const mode = frame.dispose;
|
|
1468
|
+
const width = frame.width | 0;
|
|
1469
|
+
const height = frame.height | 0;
|
|
1470
|
+
const f32 = new Uint32Array(f8.buffer, f8.byteOffset, width * height);
|
|
1471
|
+
const f = frames[frames.push(new Frame(gwidth, gheight, 10 * frame.delay, 0, 0, 3)) - 1];
|
|
1472
|
+
|
|
1473
|
+
const t8 = f.bitmap;
|
|
1474
|
+
const t32 = new Uint32Array(t8.buffer);
|
|
1475
|
+
|
|
1476
|
+
t8.set(u8);
|
|
1477
|
+
|
|
1478
|
+
if (2 === mode) {
|
|
1479
|
+
for (let y = 0 | 0; y < height; y++) {
|
|
1480
|
+
const y_offset = fx + gwidth * (y + fy) | 0;
|
|
1481
|
+
|
|
1482
|
+
for (let x = 0 | 0; x < width; x++) {
|
|
1483
|
+
const x_offset = x + y_offset;
|
|
1484
|
+
|
|
1485
|
+
if (0 === f8[3 + offset8])
|
|
1486
|
+
t32[x_offset] = u32[x_offset];
|
|
1487
|
+
else t32[x_offset] = f32[offset32];
|
|
1488
|
+
|
|
1489
|
+
offset32++;
|
|
1490
|
+
offset8 += 4;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
else if (3 === mode) {
|
|
1496
|
+
for (let y = 0 | 0; y < height; y++) {
|
|
1497
|
+
const y_offset = fx + gwidth * (y + fy) | 0;
|
|
1498
|
+
|
|
1499
|
+
for (let x = 0 | 0; x < width; x++) {
|
|
1500
|
+
const x_offset = x + y_offset;
|
|
1501
|
+
|
|
1502
|
+
if (0 === f8[3 + offset8])
|
|
1503
|
+
t32[x_offset] = u32[x_offset];
|
|
1504
|
+
else t32[x_offset] = f32[offset32];
|
|
1505
|
+
|
|
1506
|
+
offset32++;
|
|
1507
|
+
offset8 += 4;
|
|
1508
|
+
u32[x_offset] = 0;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
else if (0 === mode || 1 === mode) {
|
|
1514
|
+
t8.set(u8);
|
|
1515
|
+
for (let y = 0 | 0; y < height; y++) {
|
|
1516
|
+
const y_offset = fx + gwidth * (y + fy) | 0;
|
|
1517
|
+
|
|
1518
|
+
for (let x = 0 | 0; x < width; x++) {
|
|
1519
|
+
const x_offset = x + y_offset;
|
|
1520
|
+
|
|
1521
|
+
if (0 === f8[3 + offset8])
|
|
1522
|
+
t32[x_offset] = u32[x_offset];
|
|
1523
|
+
else t32[x_offset] = f32[offset32];
|
|
1524
|
+
|
|
1525
|
+
offset32++;
|
|
1526
|
+
offset8 += 4;
|
|
1527
|
+
u32[x_offset] = t32[x_offset];
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
if (onlyExtractFirstFrame)
|
|
1533
|
+
break;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
image = new GIF(frames);
|
|
1537
|
+
} else throw new Error('Unsupported image type');
|
|
1538
|
+
|
|
1539
|
+
return image;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
resize(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) {
|
|
1543
|
+
for (const frame of this)
|
|
1544
|
+
frame.resize(width, height, mode);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
class TextLayout {
|
|
1549
|
+
/**
|
|
1550
|
+
* Layout options for {@link Image.renderText}
|
|
1551
|
+
* @param {object} [options]
|
|
1552
|
+
* @param {number} [options.maxWidth=Infinity] The texts max width
|
|
1553
|
+
* @param {number} [options.maxHeight=Infinity] The texts max height
|
|
1554
|
+
* @param {string} [options.wrapStyle='word'] The texts wrap style when reaching the max width (word, char)
|
|
1555
|
+
* @param {string} [options.verticalAlign='left'] The vertical align mode (left, center, right)
|
|
1556
|
+
* @param {string} [options.horizontalAlign='top'] The horizontal align mode (top, middle, bottom)
|
|
1557
|
+
* @param {boolean} [options.wrapHardBreaks=true] Whether to force wrap at new line characters
|
|
1558
|
+
*/
|
|
1559
|
+
constructor(options) {
|
|
1560
|
+
const {maxWidth, maxHeight, wrapStyle, verticalAlign, horizontalAlign, wrapHardBreaks} = options || {};
|
|
1561
|
+
|
|
1562
|
+
this.maxWidth = maxWidth || Infinity;
|
|
1563
|
+
if (isNaN(this.maxWidth) || this.maxWidth < 1)
|
|
1564
|
+
throw new RangeError('Invalid maxWidth');
|
|
1565
|
+
|
|
1566
|
+
this.maxHeight = maxHeight || Infinity;
|
|
1567
|
+
if (isNaN(this.maxHeight) || this.maxHeight < 1)
|
|
1568
|
+
throw new RangeError('Invalid maxHeight');
|
|
1569
|
+
|
|
1570
|
+
this.wrapStyle = wrapStyle || 'word';
|
|
1571
|
+
if (!['word', 'char'].includes(this.wrapStyle))
|
|
1572
|
+
throw new RangeError('Invalid wrapStyle');
|
|
1573
|
+
|
|
1574
|
+
this.verticalAlign = verticalAlign || 'left';
|
|
1575
|
+
if (!['left', 'center', 'right'].includes(this.verticalAlign))
|
|
1576
|
+
throw new RangeError('Invalid verticalAlign');
|
|
1577
|
+
|
|
1578
|
+
this.horizontalAlign = horizontalAlign || 'top';
|
|
1579
|
+
if (!['top', 'middle', 'bottom'].includes(this.horizontalAlign))
|
|
1580
|
+
throw new RangeError('Invalid horizontalAlign');
|
|
1581
|
+
|
|
1582
|
+
this.wrapHardBreaks = typeof wrapHardBreaks === 'undefined' ? true : wrapHardBreaks;
|
|
1583
|
+
if (typeof this.wrapHardBreaks !== 'boolean')
|
|
1584
|
+
throw new TypeError('Invalid wrapHardBreaks');
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
class ImageType {
|
|
1589
|
+
/**
|
|
1590
|
+
* Gets an images type (png, jpeg, tiff, gif)
|
|
1591
|
+
* @param {Buffer|Uint8Array} data The image binary to get the type of
|
|
1592
|
+
* @returns {string|null} The image type (png, jpeg, tiff, gif, null)
|
|
1593
|
+
*/
|
|
1594
|
+
static getType(data) {
|
|
1595
|
+
let view;
|
|
1596
|
+
if (!ArrayBuffer.isView(data)) {
|
|
1597
|
+
data = new Uint8Array(data);
|
|
1598
|
+
view = new DataView(data.buffer);
|
|
1599
|
+
} else {
|
|
1600
|
+
data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
1601
|
+
view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
if (this.isPNG(view)) return 'png';
|
|
1605
|
+
if (this.isJPEG(view)) return 'jpeg';
|
|
1606
|
+
if (this.isTIFF(view)) return 'tiff';
|
|
1607
|
+
if (this.isGIF(view)) return 'gif';
|
|
1608
|
+
return null;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
/**
|
|
1612
|
+
* @param {DataView} view
|
|
1613
|
+
* @returns {boolean}
|
|
1614
|
+
*/
|
|
1615
|
+
static isPNG(view) {
|
|
1616
|
+
return view.getUint32(0, false) === MAGIC_NUMBERS.PNG;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
/**
|
|
1620
|
+
* @param {DataView} view
|
|
1621
|
+
* @returns {boolean}
|
|
1622
|
+
*/
|
|
1623
|
+
static isJPEG(view) {
|
|
1624
|
+
return (view.getUint32(0, false) >>> 8) === MAGIC_NUMBERS.JPEG;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
/**
|
|
1628
|
+
* @param {DataView} view
|
|
1629
|
+
* @returns {boolean}
|
|
1630
|
+
*/
|
|
1631
|
+
static isTIFF(view) {
|
|
1632
|
+
return view.getUint32(0, false) === MAGIC_NUMBERS.TIFF;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
/**
|
|
1636
|
+
* @param {DataView} view
|
|
1637
|
+
* @returns {boolean}
|
|
1638
|
+
*/
|
|
1639
|
+
static isGIF(view) {
|
|
1640
|
+
return (view.getUint32(0, false) >>> 8) === MAGIC_NUMBERS.GIF;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
/**
|
|
1645
|
+
* Decodes the given image binary
|
|
1646
|
+
* @param {Uint8Array|Buffer} data The image data
|
|
1647
|
+
* @param {boolean} [onlyExtractFirstFrame] Whether to end GIF decoding after the first frame
|
|
1648
|
+
* @returns {Promise<GIF|Image>} The decoded image
|
|
1649
|
+
*/
|
|
1650
|
+
function decode(data, onlyExtractFirstFrame) {
|
|
1651
|
+
const type = ImageType.getType(data);
|
|
1652
|
+
|
|
1653
|
+
if (type === 'gif')
|
|
1654
|
+
return GIF.decode(data, onlyExtractFirstFrame);
|
|
1655
|
+
return Image.decode(data);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
module.exports = {Image, GIF, Frame, TextLayout, ImageType, decode};
|