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.
Files changed (53) hide show
  1. package/ImageScript.d.ts +233 -223
  2. package/ImageScript.js +1658 -1646
  3. package/LICENSE +12 -619
  4. package/LICENSE.AGPL-3.0 +619 -0
  5. package/LICENSE.MIT +19 -0
  6. package/README.md +51 -46
  7. package/bench/_.mjs +165 -165
  8. package/bench/all.mjs +3 -3
  9. package/bench/blur.mjs +58 -58
  10. package/bench/flip.mjs +62 -62
  11. package/bench/overlay.mjs +34 -34
  12. package/bench/package.json +6 -6
  13. package/bench/readme.md +63 -63
  14. package/bench/resize.mjs +51 -51
  15. package/codecs/LICENSE +20 -20
  16. package/codecs/lib.d.ts +49 -49
  17. package/codecs/lib.js +5 -5
  18. package/codecs/node/index.js +2 -2
  19. package/codecs/node/index.mjs +5 -5
  20. package/codecs/package.json +13 -13
  21. package/codecs/readme.md +37 -37
  22. package/package.json +41 -41
  23. package/png/node.js +1137 -1137
  24. package/png/src/crc.mjs +48 -48
  25. package/png/src/mem.mjs +23 -23
  26. package/png/src/png.mjs +254 -254
  27. package/png/src/zlib.mjs +617 -617
  28. package/tsconfig.json +21 -21
  29. package/utils/mem.js +25 -25
  30. package/v2/codecs/magic.js +24 -24
  31. package/v2/framebuffer.d.ts +66 -66
  32. package/v2/framebuffer.js +2352 -2352
  33. package/v2/framebuffer.mjs +125 -125
  34. package/v2/framebuffer.rs +1263 -1263
  35. package/v2/index.d.ts +83 -83
  36. package/v2/index.js +260 -260
  37. package/v2/ops/blur.mjs +302 -302
  38. package/v2/ops/color.mjs +276 -276
  39. package/v2/ops/crop.mjs +63 -63
  40. package/v2/ops/fill.mjs +32 -32
  41. package/v2/ops/flip.mjs +32 -32
  42. package/v2/ops/index.mjs +8 -8
  43. package/v2/ops/iterator.mjs +33 -33
  44. package/v2/ops/overlay.mjs +62 -62
  45. package/v2/ops/resize.mjs +165 -165
  46. package/v2/ops/rotate.mjs +116 -116
  47. package/v2/util/mem.mjs +23 -23
  48. package/wasm/node/font.js +141 -141
  49. package/wasm/node/gif.js +76 -76
  50. package/wasm/node/jpeg.js +91 -91
  51. package/wasm/node/png.js +49 -49
  52. package/wasm/node/svg.js +55 -55
  53. 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
- disposalMode = Frame.__convert_disposal_mode__(disposalMode);
1273
-
1274
- super(width, height);
1275
- this.duration = duration;
1276
- this.xOffset = xOffset;
1277
- this.yOffset = yOffset;
1278
- this.disposalMode = disposalMode;
1279
- }
1280
-
1281
- toString() {
1282
- return `Frame<${this.width}x${this.height}x${this.duration}ms>`;
1283
- }
1284
-
1285
- /**
1286
- * Converts an Image instance to a Frame, cloning it in the process
1287
- * @param {Image} image The image to create the frame from
1288
- * @param {number} [duration = 100] The frames duration (in ms)
1289
- * @param {number} [xOffset=0] The frames offset on the x-axis
1290
- * @param {number} [yOffset=0] The frames offset on the y-axis
1291
- * @param {string|number} [disposalMode=Frame.DISPOSAL_KEEP] The frames disposal mode ({@link Frame.DISPOSAL_KEEP}, {@link Frame.DISPOSAL_PREVIOUS} or {@link Frame.DISPOSAL_BACKGROUND})
1292
- * @return {Frame}
1293
- */
1294
- static from(image, duration, xOffset, yOffset, disposalMode = Frame.DISPOSAL_KEEP) {
1295
- if (!(image instanceof Image))
1296
- throw new TypeError('Invalid image passed');
1297
-
1298
- disposalMode = Frame.__convert_disposal_mode__(disposalMode);
1299
-
1300
- const frame = new Frame(image.width, image.height, duration, xOffset, yOffset, disposalMode);
1301
- frame.bitmap.set(image.bitmap);
1302
-
1303
- return frame;
1304
- }
1305
-
1306
- resize(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) {
1307
- const originalWidth = this.width;
1308
- const originalHeight = this.height;
1309
-
1310
- const result = super.resize(width, height, mode);
1311
-
1312
- this.xOffset *= result.width / originalWidth;
1313
- this.yOffset *= result.height / originalHeight;
1314
-
1315
- return result;
1316
- }
1317
- }
1318
-
1319
- /**
1320
- * Represents a GIF image as an array of frames
1321
- * @extends Array<Frame>
1322
- */
1323
- class GIF extends Array {
1324
- /**
1325
- * Creates a new GIF image.
1326
- * @param {Frame[]} frames The frames to create the GIF from
1327
- * @param {number} [loopCount=0] How often to loop the GIF for (-1 = unlimited)
1328
- * @property {number} loopCount How often the GIF will loop for
1329
- */
1330
- constructor(frames, loopCount = -1) {
1331
- super(...frames);
1332
-
1333
- for (const frame of this)
1334
- if (!(frame instanceof Frame))
1335
- throw new TypeError(`Frame ${this.indexOf(frame)} is not an instance of Frame`);
1336
-
1337
- if (loopCount < -1 || isNaN(loopCount))
1338
- throw new RangeError('Invalid loop count');
1339
-
1340
- this.loopCount = loopCount;
1341
- }
1342
-
1343
- /**
1344
- * The GIFs width
1345
- * @returns {number}
1346
- */
1347
- get width() {
1348
- let max = 0;
1349
- for (const frame of this) {
1350
- let width = frame.width + frame.xOffset;
1351
- if (max < width)
1352
- max = width;
1353
- }
1354
-
1355
- return max;
1356
- }
1357
-
1358
- /**
1359
- * The GIFs height
1360
- * @returns {number}
1361
- */
1362
- get height() {
1363
- let max = 0;
1364
- for (const frame of this) {
1365
- let height = frame.height + frame.yOffset;
1366
- if (max < height)
1367
- max = height;
1368
- }
1369
-
1370
- return max;
1371
- }
1372
-
1373
- toString() {
1374
- return `GIF<${this.width}x${this.height}x${this.duration}ms>`;
1375
- }
1376
-
1377
- /**
1378
- * @returns {Generator<Frame, void, *>}
1379
- */
1380
- * [Symbol.iterator]() {
1381
- for (let i = 0; i < this.length; i++)
1382
- yield this[i];
1383
- }
1384
-
1385
- slice(start, end) {
1386
- if (end === Infinity)
1387
- end = this.length;
1388
- const frames = new Array(end - start);
1389
- for (let i = 0; i < frames.length; i++)
1390
- frames[i] = this[i + start];
1391
- return new GIF(frames, this.loopCount);
1392
- }
1393
-
1394
- /**
1395
- * The GIFs duration (in ms)
1396
- * @return {number}
1397
- */
1398
- get duration() {
1399
- return this.reduce((acc, frame) => acc + frame.duration, 0);
1400
- }
1401
-
1402
- /**
1403
- * Encodes the image into a GIF
1404
- * @param {number} [quality=95] GIF quality 0-100
1405
- * @return {Promise<Uint8Array>} The encoded data
1406
- */
1407
- async encode(quality = 95) {
1408
- const encoder = new codecs.gif.encoder(this.width, this.height);
1409
-
1410
- for (const frame of this) {
1411
- if (!(frame instanceof Frame)) throw new Error('GIF contains invalid frames');
1412
-
1413
- encoder.add(frame.bitmap, {
1414
- quality,
1415
- x: frame.xOffset,
1416
- y: frame.yOffset,
1417
- width: frame.width,
1418
- speed: null, // 1-10
1419
- height: frame.height,
1420
- colors: null, // 2-256
1421
- delay: ~~(frame.duration / 10),
1422
- dispose: ['any', 'keep', 'previous', 'background'][frame.disposalMode],
1423
- });
1424
- }
1425
-
1426
- return encoder.finish({ repeat: -1 === this.loopCount ? null : this.loopCount });
1427
- }
1428
-
1429
- /**
1430
- * Decodes a GIF image
1431
- * @param {Buffer|Uint8Array} data The binary data to decode
1432
- * @param {boolean} [onlyExtractFirstFrame=false] Whether to end GIF decoding after the first frame
1433
- * @return {Promise<GIF>} The decoded GIF
1434
- */
1435
- static async decode(data, onlyExtractFirstFrame = false) {
1436
- let image;
1437
- data = mem.view(data);
1438
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
1439
-
1440
- if (ImageType.isGIF(view)) { // GIF
1441
- const frames = [];
1442
- const decoder = new (await giflib.init()).Decoder(data);
1443
-
1444
- const gwidth = decoder.width | 0;
1445
- const gheight = decoder.height | 0;
1446
- const u32 = new Uint32Array(decoder.width * decoder.height);
1447
- const u8 = new Uint8Array(u32.buffer, u32.byteOffset, u32.byteLength);
1448
-
1449
- for (const frame of decoder.frames()) {
1450
- let offset8 = 0 | 0;
1451
- let offset32 = 0 | 0;
1452
- const fx = frame.x | 0;
1453
- const fy = frame.y | 0;
1454
- const f8 = frame.buffer;
1455
- const mode = frame.dispose;
1456
- const width = frame.width | 0;
1457
- const height = frame.height | 0;
1458
- const f32 = new Uint32Array(f8.buffer, f8.byteOffset, width * height);
1459
- const f = frames[frames.push(new Frame(gwidth, gheight, 10 * frame.delay, 0, 0, 3)) - 1];
1460
-
1461
- const t8 = f.bitmap;
1462
- const t32 = new Uint32Array(t8.buffer);
1463
-
1464
- t8.set(u8);
1465
-
1466
- if (2 === mode) {
1467
- for (let y = 0 | 0; y < height; y++) {
1468
- const y_offset = fx + gwidth * (y + fy) | 0;
1469
-
1470
- for (let x = 0 | 0; x < width; x++) {
1471
- const x_offset = x + y_offset;
1472
-
1473
- if (0 === f8[3 + offset8])
1474
- t32[x_offset] = u32[x_offset];
1475
- else t32[x_offset] = f32[offset32];
1476
-
1477
- offset32++;
1478
- offset8 += 4;
1479
- }
1480
- }
1481
- }
1482
-
1483
- else if (3 === mode) {
1484
- for (let y = 0 | 0; y < height; y++) {
1485
- const y_offset = fx + gwidth * (y + fy) | 0;
1486
-
1487
- for (let x = 0 | 0; x < width; x++) {
1488
- const x_offset = x + y_offset;
1489
-
1490
- if (0 === f8[3 + offset8])
1491
- t32[x_offset] = u32[x_offset];
1492
- else t32[x_offset] = f32[offset32];
1493
-
1494
- offset32++;
1495
- offset8 += 4;
1496
- u32[x_offset] = 0;
1497
- }
1498
- }
1499
- }
1500
-
1501
- else if (0 === mode || 1 === mode) {
1502
- t8.set(u8);
1503
- for (let y = 0 | 0; y < height; y++) {
1504
- const y_offset = fx + gwidth * (y + fy) | 0;
1505
-
1506
- for (let x = 0 | 0; x < width; x++) {
1507
- const x_offset = x + y_offset;
1508
-
1509
- if (0 === f8[3 + offset8])
1510
- t32[x_offset] = u32[x_offset];
1511
- else t32[x_offset] = f32[offset32];
1512
-
1513
- offset32++;
1514
- offset8 += 4;
1515
- u32[x_offset] = t32[x_offset];
1516
- }
1517
- }
1518
- }
1519
-
1520
- if (onlyExtractFirstFrame)
1521
- break;
1522
- }
1523
-
1524
- image = new GIF(frames);
1525
- } else throw new Error('Unsupported image type');
1526
-
1527
- return image;
1528
- }
1529
-
1530
- resize(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) {
1531
- for (const frame of this)
1532
- frame.resize(width, height, mode);
1533
- }
1534
- }
1535
-
1536
- class TextLayout {
1537
- /**
1538
- * Layout options for {@link Image.renderText}
1539
- * @param {object} [options]
1540
- * @param {number} [options.maxWidth=Infinity] The texts max width
1541
- * @param {number} [options.maxHeight=Infinity] The texts max height
1542
- * @param {string} [options.wrapStyle='word'] The texts wrap style when reaching the max width (word, char)
1543
- * @param {string} [options.verticalAlign='left'] The vertical align mode (left, center, right)
1544
- * @param {string} [options.horizontalAlign='top'] The horizontal align mode (top, middle, bottom)
1545
- * @param {boolean} [options.wrapHardBreaks=true] Whether to force wrap at new line characters
1546
- */
1547
- constructor(options) {
1548
- const {maxWidth, maxHeight, wrapStyle, verticalAlign, horizontalAlign, wrapHardBreaks} = options || {};
1549
-
1550
- this.maxWidth = maxWidth || Infinity;
1551
- if (isNaN(this.maxWidth) || this.maxWidth < 1)
1552
- throw new RangeError('Invalid maxWidth');
1553
-
1554
- this.maxHeight = maxHeight || Infinity;
1555
- if (isNaN(this.maxHeight) || this.maxHeight < 1)
1556
- throw new RangeError('Invalid maxHeight');
1557
-
1558
- this.wrapStyle = wrapStyle || 'word';
1559
- if (!['word', 'char'].includes(this.wrapStyle))
1560
- throw new RangeError('Invalid wrapStyle');
1561
-
1562
- this.verticalAlign = verticalAlign || 'left';
1563
- if (!['left', 'center', 'right'].includes(this.verticalAlign))
1564
- throw new RangeError('Invalid verticalAlign');
1565
-
1566
- this.horizontalAlign = horizontalAlign || 'top';
1567
- if (!['top', 'middle', 'bottom'].includes(this.horizontalAlign))
1568
- throw new RangeError('Invalid horizontalAlign');
1569
-
1570
- this.wrapHardBreaks = wrapHardBreaks || true;
1571
- if (typeof this.wrapHardBreaks !== 'boolean')
1572
- throw new TypeError('Invalid wrapHardBreaks');
1573
- }
1574
- }
1575
-
1576
- class ImageType {
1577
- /**
1578
- * Gets an images type (png, jpeg, tiff, gif)
1579
- * @param {Buffer|Uint8Array} data The image binary to get the type of
1580
- * @returns {string|null} The image type (png, jpeg, tiff, gif, null)
1581
- */
1582
- static getType(data) {
1583
- let view;
1584
- if (!ArrayBuffer.isView(data)) {
1585
- data = new Uint8Array(data);
1586
- view = new DataView(data.buffer);
1587
- } else {
1588
- data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
1589
- view = new DataView(data.buffer, data.byteOffset, data.byteLength);
1590
- }
1591
-
1592
- if (this.isPNG(view)) return 'png';
1593
- if (this.isJPEG(view)) return 'jpeg';
1594
- if (this.isTIFF(view)) return 'tiff';
1595
- if (this.isGIF(view)) return 'gif';
1596
- return null;
1597
- }
1598
-
1599
- /**
1600
- * @param {DataView} view
1601
- * @returns {boolean}
1602
- */
1603
- static isPNG(view) {
1604
- return view.getUint32(0, false) === MAGIC_NUMBERS.PNG;
1605
- }
1606
-
1607
- /**
1608
- * @param {DataView} view
1609
- * @returns {boolean}
1610
- */
1611
- static isJPEG(view) {
1612
- return (view.getUint32(0, false) >>> 8) === MAGIC_NUMBERS.JPEG;
1613
- }
1614
-
1615
- /**
1616
- * @param {DataView} view
1617
- * @returns {boolean}
1618
- */
1619
- static isTIFF(view) {
1620
- return view.getUint32(0, false) === MAGIC_NUMBERS.TIFF;
1621
- }
1622
-
1623
- /**
1624
- * @param {DataView} view
1625
- * @returns {boolean}
1626
- */
1627
- static isGIF(view) {
1628
- return (view.getUint32(0, false) >>> 8) === MAGIC_NUMBERS.GIF;
1629
- }
1630
- }
1631
-
1632
- /**
1633
- * Decodes the given image binary
1634
- * @param {Uint8Array|Buffer} data The image data
1635
- * @param {boolean} [onlyExtractFirstFrame] Whether to end GIF decoding after the first frame
1636
- * @returns {Promise<GIF|Image>} The decoded image
1637
- */
1638
- function decode(data, onlyExtractFirstFrame) {
1639
- const type = ImageType.getType(data);
1640
-
1641
- if (type === 'gif')
1642
- return GIF.decode(data, onlyExtractFirstFrame);
1643
- return Image.decode(data);
1644
- }
1645
-
1646
- module.exports = {Image, GIF, Frame, TextLayout, ImageType, decode};
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};