imagescript 1.3.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/LICENSE +4 -1
  2. package/README.md +64 -26
  3. package/THIRD_PARTY_NOTICES.md +74 -0
  4. package/dist/codecs/crc.d.ts +9 -0
  5. package/dist/codecs/crc.js +29 -0
  6. package/dist/codecs/exif.d.ts +11 -0
  7. package/dist/codecs/exif.js +27 -0
  8. package/dist/codecs/gif.d.ts +44 -0
  9. package/dist/codecs/gif.js +401 -0
  10. package/dist/codecs/jpeg.d.ts +35 -0
  11. package/dist/codecs/jpeg.js +980 -0
  12. package/dist/codecs/png.d.ts +44 -0
  13. package/dist/codecs/png.js +442 -0
  14. package/dist/codecs/quantize.d.ts +25 -0
  15. package/dist/codecs/quantize.js +302 -0
  16. package/dist/codecs/tiff.d.ts +6 -0
  17. package/dist/codecs/tiff.js +546 -0
  18. package/dist/codecs/zlib.d.ts +22 -0
  19. package/dist/codecs/zlib.js +607 -0
  20. package/dist/frame.d.ts +57 -0
  21. package/dist/frame.js +65 -0
  22. package/dist/gif.d.ts +44 -0
  23. package/dist/gif.js +93 -0
  24. package/dist/image-type.d.ts +20 -0
  25. package/dist/image-type.js +49 -0
  26. package/dist/image.d.ts +521 -0
  27. package/dist/image.js +1118 -0
  28. package/dist/index.d.ts +19 -0
  29. package/dist/index.js +13 -0
  30. package/dist/package.json +1 -0
  31. package/dist/render/font.d.ts +63 -0
  32. package/dist/render/font.js +857 -0
  33. package/dist/render/svg.d.ts +11 -0
  34. package/dist/render/svg.js +1717 -0
  35. package/dist/render/text.d.ts +61 -0
  36. package/dist/render/text.js +317 -0
  37. package/dist/text-layout.d.ts +46 -0
  38. package/dist/text-layout.js +35 -0
  39. package/dist/util/blur.d.ts +13 -0
  40. package/dist/util/blur.js +146 -0
  41. package/dist/util/mem.d.ts +10 -0
  42. package/dist/util/mem.js +23 -0
  43. package/dist/util/resample.d.ts +12 -0
  44. package/dist/util/resample.js +99 -0
  45. package/dist/util/utf8.d.ts +5 -0
  46. package/dist/util/utf8.js +55 -0
  47. package/dist/version.d.ts +1 -0
  48. package/dist/version.js +1 -0
  49. package/licenses/Apache-2.0.txt +202 -0
  50. package/licenses/README.ijg +260 -0
  51. package/package.json +31 -7
  52. package/ImageScript.d.ts +0 -235
  53. package/ImageScript.js +0 -1669
  54. package/bench/_.mjs +0 -166
  55. package/bench/all.mjs +0 -4
  56. package/bench/blur.mjs +0 -59
  57. package/bench/flip.mjs +0 -63
  58. package/bench/overlay.mjs +0 -35
  59. package/bench/package.json +0 -7
  60. package/bench/readme.md +0 -64
  61. package/bench/resize.mjs +0 -52
  62. package/codecs/LICENSE +0 -21
  63. package/codecs/lib.d.ts +0 -50
  64. package/codecs/lib.js +0 -6
  65. package/codecs/lib.mjs +0 -1
  66. package/codecs/node/bin/arm64-darwin.node +0 -0
  67. package/codecs/node/bin/arm64-linux.node +0 -0
  68. package/codecs/node/bin/x64-darwin.node +0 -0
  69. package/codecs/node/bin/x64-linux.node +0 -0
  70. package/codecs/node/bin/x64-win32.node +0 -0
  71. package/codecs/node/index.js +0 -3
  72. package/codecs/node/index.mjs +0 -6
  73. package/codecs/package.json +0 -14
  74. package/codecs/readme.md +0 -38
  75. package/codecs/tests/gif.js +0 -0
  76. package/codecs/tests/jpeg.js +0 -0
  77. package/codecs/tests/png.js +0 -0
  78. package/codecs/tests/results/gif/1024.nis.gif +0 -0
  79. package/codecs/tests/results/gif/1024.skyra.gif +0 -0
  80. package/codecs/tests/results/gif/256.nis.gif +0 -0
  81. package/codecs/tests/results/gif/256.skyra.gif +0 -0
  82. package/codecs/tests/results/jpeg/256.jpegjs.jpeg +0 -0
  83. package/codecs/tests/results/jpeg/256.nis.jpeg +0 -0
  84. package/codecs/tests/results/jpeg/256.sharp.jpeg +0 -0
  85. package/codecs/tests/results/jpeg/512.jpegjs.jpeg +0 -0
  86. package/codecs/tests/results/jpeg/512.nis.jpeg +0 -0
  87. package/codecs/tests/results/jpeg/512.sharp.jpeg +0 -0
  88. package/codecs/tests/results/jpeg/64.jpegjs.jpeg +0 -0
  89. package/codecs/tests/results/jpeg/64.nis.jpeg +0 -0
  90. package/codecs/tests/results/jpeg/64.sharp.jpeg +0 -0
  91. package/codecs/tests/results/png/256.jis.png +0 -0
  92. package/codecs/tests/results/png/256.nis.png +0 -0
  93. package/codecs/tests/results/png/256.sharp.png +0 -0
  94. package/codecs/tests/results/png/256.upng.png +0 -0
  95. package/codecs/tests/results/png/512.jis.png +0 -0
  96. package/codecs/tests/results/png/512.nis.png +0 -0
  97. package/codecs/tests/results/png/512.sharp.png +0 -0
  98. package/codecs/tests/results/png/512.upng.png +0 -0
  99. package/codecs/tests/results/png/64.jis.png +0 -0
  100. package/codecs/tests/results/png/64.nis.png +0 -0
  101. package/codecs/tests/results/png/64.sharp.png +0 -0
  102. package/codecs/tests/results/png/64.upng.png +0 -0
  103. package/codecs/tests/webp.js +0 -0
  104. package/codecs/wasm/bin/codecs.wasm +0 -0
  105. package/codecs/wasm/bin/simd.wasm +0 -0
  106. package/codecs/wasm/bundle/browser.js +0 -1
  107. package/codecs/wasm/index.js +0 -1
  108. package/codecs/wasm/index.mjs +0 -1
  109. package/png/README.md +0 -1
  110. package/png/node.js +0 -1137
  111. package/png/src/crc.mjs +0 -49
  112. package/png/src/mem.mjs +0 -24
  113. package/png/src/png.mjs +0 -254
  114. package/png/src/zlib.mjs +0 -618
  115. package/tsconfig.json +0 -21
  116. package/utils/mem.js +0 -26
  117. package/v2/codecs/magic.js +0 -25
  118. package/v2/framebuffer.d.ts +0 -67
  119. package/v2/framebuffer.js +0 -2352
  120. package/v2/framebuffer.mjs +0 -126
  121. package/v2/framebuffer.rs +0 -1264
  122. package/v2/index.d.ts +0 -84
  123. package/v2/index.js +0 -261
  124. package/v2/ops/blur.mjs +0 -303
  125. package/v2/ops/color.mjs +0 -277
  126. package/v2/ops/crop.mjs +0 -64
  127. package/v2/ops/fill.mjs +0 -33
  128. package/v2/ops/flip.mjs +0 -33
  129. package/v2/ops/index.mjs +0 -9
  130. package/v2/ops/iterator.mjs +0 -34
  131. package/v2/ops/overlay.mjs +0 -63
  132. package/v2/ops/resize.mjs +0 -166
  133. package/v2/ops/rotate.mjs +0 -117
  134. package/v2/util/mem.mjs +0 -24
  135. package/wasm/any/font.wasm +0 -0
  136. package/wasm/any/gif.wasm +0 -0
  137. package/wasm/any/jpeg.wasm +0 -0
  138. package/wasm/any/png.wasm +0 -0
  139. package/wasm/any/svg.wasm +0 -0
  140. package/wasm/any/tiff.wasm +0 -0
  141. package/wasm/node/font.js +0 -142
  142. package/wasm/node/gif.js +0 -76
  143. package/wasm/node/jpeg.js +0 -92
  144. package/wasm/node/png.js +0 -49
  145. package/wasm/node/svg.js +0 -56
  146. package/wasm/node/tiff.js +0 -47
package/ImageScript.js DELETED
@@ -1,1669 +0,0 @@
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
- * Flips / mirrors the image horizontally or vertically
834
- * @param {'horizontal' | 'vertical'} direction The direction to flip
835
- */
836
- flip(direction) {
837
- const frame = new v2(this.width, this.height, this.bitmap).flip(direction);
838
-
839
- this.bitmap.set(frame.u8);
840
- return this;
841
- }
842
-
843
- /**
844
- * @private
845
- * @param {Image|Frame} image
846
- * @returns {Image|Frame}
847
- */
848
- __apply__(image) {
849
- this.__width__ = image.__width__;
850
- this.__height__ = image.__height__;
851
- this.__view__ = image.__view__;
852
- this.__u32__ = image.__u32__;
853
- this.bitmap = image.bitmap;
854
-
855
- if (image instanceof Frame)
856
- return Frame.from(this, image.duration, image.xOffset, image.yOffset, image.disposalMode);
857
-
858
- return this;
859
- }
860
-
861
- /**
862
- * Creates a multi-point gradient generator
863
- * @param {Object<number, number>} colors The gradient points to use (e.g. `{0: 0xff0000ff, 1: 0x00ff00ff}`)
864
- * @return {(function(number): number)} The gradient generator. The function argument is the position in the gradient (0..1).
865
- */
866
- static gradient(colors) {
867
- const entries = Object.entries(colors).sort((a, b) => a[0] - b[0]);
868
- const positions = entries.map(e => parseFloat(e[0]));
869
- const values = entries.map(e => e[1]);
870
-
871
- if (positions.length === 0) throw new RangeError('Invalid gradient point count');
872
- else if (positions.length === 1) {
873
- return () => values[0];
874
- } else if (positions.length === 2) {
875
- const gradient = this.__gradient__(values[0], values[1]);
876
- return position => {
877
- if (position <= positions[0]) return values[0];
878
- if (position >= positions[1]) return values[1];
879
- return gradient((position - positions[0]) / (positions[1] - positions[0]));
880
- };
881
- }
882
-
883
- const minDef = Math.min(...positions);
884
- const maxDef = Math.max(...positions);
885
- let gradients = [];
886
-
887
- for (let i = 0; i < positions.length; i++) {
888
- let minPos = positions[i - 1];
889
- if (minPos === undefined) continue;
890
-
891
- let maxPos = positions[i];
892
-
893
- let minVal = values[i - 1];
894
- if (minVal === undefined) minVal = values[i];
895
-
896
- const maxVal = values[i];
897
- const gradient = this.__gradient__(minVal, maxVal);
898
-
899
- gradients.push({min: minPos, max: maxPos, gradient});
900
- }
901
-
902
- return position => {
903
- if (position <= minDef) return gradients[0].gradient(0);
904
- if (position >= maxDef) return gradients[gradients.length - 1].gradient(1);
905
-
906
- for (const gradient of gradients)
907
- if (position >= gradient.min && position <= gradient.max)
908
- return gradient.gradient((position - gradient.min) / (gradient.max - gradient.min));
909
- throw new RangeError(`Invalid gradient position: ${position}`);
910
- };
911
- }
912
-
913
- /**
914
- * Rounds the images corners
915
- * @param {number} [radius=min(width,height)/4] The radius of the corners
916
- * @return {Image}
917
- */
918
- roundCorners(radius = Math.min(this.width, this.height) / 4) {
919
- const radSquared = radius ** 2;
920
- for (let x = 1; x <= radius; x++) {
921
- const xRad = (x - radius) ** 2;
922
- for (let y = 1; y <= radius; y++) {
923
- if (xRad + (y - radius) ** 2 > radSquared)
924
- this.bitmap[((y - 1) * this.width + x - 1) * 4 + 3] = 0;
925
- }
926
- }
927
-
928
- for (let x = 1; x <= radius; x++) {
929
- const xRad = (x - radius) ** 2;
930
- for (let y = this.height - radius; y <= this.height; y++) {
931
- if (xRad + ((this.height - y) - radius) ** 2 > radSquared)
932
- this.bitmap[((y - 1) * this.width + x - 1) * 4 + 3] = 0;
933
- }
934
- }
935
-
936
- for (let x = this.width - radius; x <= this.width; x++) {
937
- const xRad = ((this.width - x) - radius) ** 2;
938
- for (let y = 1; y <= radius; y++) {
939
- if (xRad + (y - radius) ** 2 > radSquared)
940
- this.bitmap[((y - 1) * this.width + x - 1) * 4 + 3] = 0;
941
- }
942
- }
943
-
944
- for (let x = this.width - radius; x <= this.width; x++) {
945
- const xRad = ((this.width - x) - radius) ** 2;
946
- for (let y = this.height - radius; y <= this.height; y++) {
947
- if (xRad + ((this.height - y) - radius) ** 2 > radSquared)
948
- this.bitmap[((y - 1) * this.width + x - 1) * 4 + 3] = 0;
949
- }
950
- }
951
-
952
- return this;
953
- }
954
-
955
- /**
956
- * @private
957
- */
958
- static __gradient__(startColor, endColor) {
959
- const sr = startColor >>> 24;
960
- const sg = startColor >> 16 & 0xff;
961
- const sb = startColor >> 8 & 0xff;
962
- const sa = startColor & 0xff;
963
- const er = (endColor >>> 24) - sr;
964
- const eg = (endColor >> 16 & 0xff) - sg;
965
- const eb = (endColor >> 8 & 0xff) - sb;
966
- const ea = (endColor & 0xff) - sa;
967
-
968
- return position => {
969
- const r = sr + position * er;
970
- const g = sg + position * eg;
971
- const b = sb + position * eb;
972
- const a = sa + position * ea;
973
- return (((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | (a & 0xff));
974
- };
975
- }
976
-
977
- fisheye(radius = 2) {
978
- const r = new Image(this.width, this.height);
979
-
980
- const w = this.width;
981
- const h = this.height;
982
- const tu32 = this.__u32__;
983
- const ru32 = r.__u32__;
984
- const iw = 1 / w;
985
- const ih = 1 / h;
986
-
987
- for (const [x, y] of this) {
988
- const xco = x * iw - .5;
989
- const yco = y * ih - .5;
990
- const dfc = Math.sqrt(xco ** 2 + yco ** 2);
991
- const dis = 2 * dfc ** radius;
992
- const nx = ((dis * xco / dfc + 0.5) * w) | 0;
993
- const ny = ((dis * yco / dfc + 0.5) * h) | 0;
994
-
995
- if (nx < 1 || nx > w || ny < 1 || ny > h || isNaN(nx) || isNaN(ny))
996
- continue;
997
-
998
- ru32[y * w + x] = tu32[w * ny + nx];
999
- }
1000
-
1001
- const cO = tu32.length * .5 + w / 2;
1002
- ru32[cO] = tu32[cO];
1003
-
1004
- return this.__apply__(r);
1005
- }
1006
-
1007
- /**
1008
- * @typedef {object} PNGMetadata
1009
- * @property {string} [title] The images title
1010
- * @property {string} [author] The images author
1011
- * @property {string} [description] The images description
1012
- * @property {string} [copyright] The images copyright info
1013
- * @property {string|number|Date} [creationTime=Date.now()] The images creation timestamp
1014
- * @property {string} [software="github.com/matmen/ImageScript vX.X.X"] The software used to create this image
1015
- * @property {string} [disclaimer] A disclaimer for the image
1016
- * @property {string} [warning] A warning for the image
1017
- * @property {string} [source] The images source
1018
- * @property {string} [comment] A comment for the image
1019
- */
1020
-
1021
- /**
1022
- * Encodes the image into a PNG
1023
- * @param {number} compression The compression level to use (0-9)
1024
- * @param {PNGMetadata} [meta={}] Image metadata
1025
- * @return {Promise<Uint8Array>} The encoded data
1026
- */
1027
- async encode(compression = 1, {
1028
- title,
1029
- author,
1030
- description,
1031
- copyright,
1032
- creationTime,
1033
- software,
1034
- disclaimer,
1035
- warning,
1036
- source,
1037
- comment
1038
- } = {}) {
1039
- return png.encode(this.bitmap, {
1040
- width: this.width,
1041
- height: this.height,
1042
- level: compression,
1043
- channels: 4,
1044
- text: {
1045
- Title: title,
1046
- Author: author,
1047
- Description: description,
1048
- Copyright: copyright,
1049
- 'Creation Time': new Date(creationTime === undefined ? Date.now() : creationTime).toUTCString(),
1050
- Software: software === undefined ? `github.com/matmen/ImageScript v${version}` : software,
1051
- Disclaimer: disclaimer,
1052
- Warning: warning,
1053
- Source: source,
1054
- Comment: comment
1055
- }
1056
- });
1057
- }
1058
-
1059
- /**
1060
- * Encodes the image into a JPEG
1061
- * @param {number} [quality=90] The JPEG quality to use (1-100)
1062
- * @return {Promise<Uint8Array>}
1063
- */
1064
- async encodeJPEG(quality = 90) {
1065
- return codecs.jpeg.encode_async(this.bitmap, {
1066
- quality,
1067
- width: this.width,
1068
- height: this.height,
1069
- });
1070
- }
1071
-
1072
- /**
1073
- * Encodes the image into a WEBP
1074
- * @param {null|number} [quality=null] The WEBP quality to use (0-100) (null is lossless)
1075
- * @return {Promise<Uint8Array>}
1076
- */
1077
- async encodeWEBP(quality = null) {
1078
- return codecs.webp.encode_async(this.bitmap, {
1079
- quality,
1080
- width: this.width,
1081
- height: this.height,
1082
- });
1083
- }
1084
-
1085
- /**
1086
- * Decodes an image (PNG, JPEG or TIFF)
1087
- * @param {Buffer|Uint8Array} data The binary data to decode
1088
- * @return {Promise<Image>} The decoded image
1089
- */
1090
- static async decode(data) {
1091
- let image;
1092
-
1093
- data = mem.view(data);
1094
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
1095
-
1096
- if (ImageType.isPNG(view)) { // PNG
1097
- const {width, height, framebuffer} = (await pnglib.init()).decode(data);
1098
- image = new Image(width, height);
1099
- image.bitmap.set(framebuffer);
1100
- } else if (ImageType.isJPEG(view)) { // JPEG
1101
- const framebuffer = (await jpeglib.init()).decode(data);
1102
-
1103
- const width = framebuffer.width;
1104
- const height = framebuffer.height;
1105
- const pixelType = framebuffer.format;
1106
-
1107
- image = new Image(width, height);
1108
- const buffer = framebuffer.buffer;
1109
-
1110
- if (pixelType === 0) {
1111
- const view = new DataView(image.bitmap.buffer);
1112
-
1113
- for (let i = 0; i < buffer.length; i++) {
1114
- const pixel = buffer[i];
1115
- view.setUint32(i * 4, pixel << 24 | pixel << 16 | pixel << 8 | 0xff, false);
1116
- }
1117
- } else if (pixelType === 1) {
1118
- image.bitmap.fill(0xff);
1119
- for (let i = 0; i < width * height; i++)
1120
- image.bitmap.set(buffer.subarray(i * 3, i * 3 + 3), i * 4);
1121
- } else if (pixelType === 2) {
1122
- for (let i = 0; i < buffer.length; i += 4) {
1123
- image.bitmap[i] = 0xff * (1 - buffer[i] / 0xff) * (1 - buffer[i + 3] / 0xff);
1124
- image.bitmap[i + 1] = 0xff * (1 - buffer[i + 1] / 0xff) * (1 - buffer[i + 3] / 0xff);
1125
- image.bitmap[i + 2] = 0xff * (1 - buffer[i + 2] / 0xff) * (1 - buffer[i + 3] / 0xff);
1126
- image.bitmap[i + 3] = 0xff;
1127
- }
1128
- }
1129
- } else if (ImageType.isTIFF(view)) { // TIFF
1130
- const framebuffer = (await tifflib.init()).decode(data);
1131
- image = new Image(framebuffer.width, framebuffer.height);
1132
-
1133
- image.bitmap.set(framebuffer.buffer);
1134
- } else throw new Error('Unsupported image type');
1135
-
1136
- return image;
1137
- }
1138
-
1139
- /**
1140
- * Scale the SVG by the given amount. For use with {@link Image.renderSVG}
1141
- * @return {number}
1142
- */
1143
- static get SVG_MODE_SCALE() {
1144
- return 1;
1145
- }
1146
-
1147
- /**
1148
- * Scale the SVG to fit the given width. For use with {@link Image.renderSVG}
1149
- * @return {number}
1150
- */
1151
- static get SVG_MODE_WIDTH() {
1152
- return 2;
1153
- }
1154
-
1155
- /**
1156
- * Scale the SVG to fit the given height. For use with {@link Image.renderSVG}
1157
- * @return {number}
1158
- */
1159
- static get SVG_MODE_HEIGHT() {
1160
- return 3;
1161
- }
1162
-
1163
- /**
1164
- * Creates a new image from the given SVG
1165
- * @param {string} svg The SVG content
1166
- * @param {number} size The size to use
1167
- * @param {number} mode The SVG resizing mode to use (one of {@link SVG_MODE_SCALE}, {@link SVG_MODE_WIDTH}, {@link SVG_MODE_HEIGHT})
1168
- * @return {Promise<Image>} The rendered SVG graphic
1169
- */
1170
- static async renderSVG(svg, size = 1, mode = this.SVG_MODE_SCALE) {
1171
- if (![this.SVG_MODE_WIDTH, this.SVG_MODE_HEIGHT, this.SVG_MODE_SCALE].includes(mode))
1172
- throw new Error('Invalid SVG scaling mode');
1173
-
1174
- if (mode === this.SVG_MODE_SCALE && size <= 0)
1175
- throw new RangeError('SVG scale must be > 0');
1176
- if (mode !== this.SVG_MODE_SCALE && size < 1)
1177
- throw new RangeError('SVG size must be >= 1')
1178
-
1179
- if (typeof svg === 'string') svg = Buffer.from(svg);
1180
- const framebuffer = (await svglib.init()).rasterize(svg, mode, size);
1181
-
1182
- const image = new Image(framebuffer.width, framebuffer.height);
1183
-
1184
- image.bitmap.set(framebuffer.buffer);
1185
-
1186
- return image;
1187
- }
1188
-
1189
- /**
1190
- * Creates a new image containing the rendered text.
1191
- * @param {Uint8Array} font TrueType (ttf/ttc) or OpenType (otf) font buffer to use
1192
- * @param {number} scale Font size to use
1193
- * @param {string} text Text to render
1194
- * @param {number} [color=0xffffffff] Text color to use
1195
- * @param {TextLayout} [layout] The text layout to use
1196
- * @return {Promise<Image>} The rendered text
1197
- */
1198
- static async renderText(font, scale, text, color = 0xffffffff, layout = new TextLayout()) {
1199
- const { Font, Layout } = await fontlib.init();
1200
-
1201
- font = new Font(scale, font);
1202
- const [r, g, b, a] = Image.colorToRGBA(color);
1203
-
1204
- const layoutOptions = new Layout();
1205
-
1206
- layoutOptions.reset({
1207
- max_width: layout.maxWidth,
1208
- max_height: layout.maxHeight,
1209
- wrap_style: layout.wrapStyle,
1210
- vertical_align: layout.verticalAlign,
1211
- horizontal_align: layout.horizontalAlign,
1212
- wrap_hard_breaks: layout.wrapHardBreaks
1213
- });
1214
-
1215
- layoutOptions.append(font, text, {scale});
1216
- const framebuffer = layoutOptions.rasterize(r, g, b);
1217
- const image = new Image(framebuffer.width, framebuffer.height);
1218
-
1219
- image.bitmap.set(framebuffer.buffer);
1220
-
1221
- if (image.height > layout.maxHeight)
1222
- image.crop(0, 0, image.width, Math.floor(layoutOptions.lines() / image.height * layout.maxHeight) * (image.height / layoutOptions.lines()));
1223
-
1224
- font.free();
1225
- layoutOptions.free();
1226
- return image.opacity(a / 0xff);
1227
- }
1228
-
1229
- }
1230
-
1231
- /**
1232
- * Represents a frame in a GIF
1233
- * @extends Image
1234
- */
1235
- class Frame extends Image {
1236
- /**
1237
- * GIF frame disposal mode KEEP. For use with {@link Frame}
1238
- * @returns {string}
1239
- */
1240
- static get DISPOSAL_KEEP() {
1241
- return 'keep';
1242
- }
1243
-
1244
- /**
1245
- * GIF frame disposal mode PREVIOUS. For use with {@link Frame}
1246
- * @returns {string}
1247
- */
1248
- static get DISPOSAL_PREVIOUS() {
1249
- return 'previous';
1250
- }
1251
-
1252
- /**
1253
- * GIF frame disposal mode BACKGROUND. For use with {@link Frame}
1254
- * @returns {string}
1255
- */
1256
- static get DISPOSAL_BACKGROUND() {
1257
- return 'background';
1258
- }
1259
-
1260
- static __convert_disposal_mode__(mode) {
1261
- if (typeof mode === 'string')
1262
- mode = ['any', 'keep', 'previous', 'background'].indexOf(mode);
1263
- if (mode < 0 || mode > 3)
1264
- throw new RangeError('Invalid disposal mode');
1265
-
1266
- return mode;
1267
- }
1268
-
1269
- /**
1270
- * Creates a new, blank frame
1271
- * @param {number} width
1272
- * @param {number} height
1273
- * @param {number} [duration = 100] The frames duration (in ms)
1274
- * @param {number} [xOffset=0] The frames offset on the x-axis
1275
- * @param {number} [yOffset=0] The frames offset on the y-axis
1276
- * @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})
1277
- * @return {Frame}
1278
- */
1279
- constructor(width, height, duration = 100, xOffset = 0, yOffset = 0, disposalMode = Frame.DISPOSAL_KEEP) {
1280
- if (isNaN(duration) || duration < 0)
1281
- throw new RangeError('Invalid frame duration');
1282
-
1283
- super(width, height);
1284
- this.duration = duration;
1285
- this.xOffset = xOffset;
1286
- this.yOffset = yOffset;
1287
- this.disposalMode = disposalMode;
1288
- }
1289
-
1290
- /**
1291
- * The Frame's disposal mode
1292
- * @returns {number}
1293
- */
1294
- get disposalMode() {
1295
- return this.__disposalMode__;
1296
- }
1297
-
1298
- /**
1299
- * Sets the frame's disposal mode, converting it to the internal numeric value.
1300
- * @param {string|number} disposalMode The frame's disposal mode
1301
- */
1302
- set disposalMode(disposalMode) {
1303
- this.__disposalMode__ = Frame.__convert_disposal_mode__(disposalMode);
1304
- }
1305
-
1306
- toString() {
1307
- return `Frame<${this.width}x${this.height}x${this.duration}ms>`;
1308
- }
1309
-
1310
- /**
1311
- * Converts an Image instance to a Frame, cloning it in the process
1312
- * @param {Image} image The image to create the frame from
1313
- * @param {number} [duration = 100] The frames duration (in ms)
1314
- * @param {number} [xOffset=0] The frames offset on the x-axis
1315
- * @param {number} [yOffset=0] The frames offset on the y-axis
1316
- * @param {string|number} [disposalMode=Frame.DISPOSAL_KEEP] The frames disposal mode ({@link Frame.DISPOSAL_KEEP}, {@link Frame.DISPOSAL_PREVIOUS} or {@link Frame.DISPOSAL_BACKGROUND})
1317
- * @return {Frame}
1318
- */
1319
- static from(image, duration, xOffset, yOffset, disposalMode = Frame.DISPOSAL_KEEP) {
1320
- if (!(image instanceof Image))
1321
- throw new TypeError('Invalid image passed');
1322
-
1323
- const frame = new Frame(image.width, image.height, duration, xOffset, yOffset, disposalMode);
1324
- frame.bitmap.set(image.bitmap);
1325
-
1326
- return frame;
1327
- }
1328
-
1329
- resize(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) {
1330
- const originalWidth = this.width;
1331
- const originalHeight = this.height;
1332
-
1333
- const result = super.resize(width, height, mode);
1334
-
1335
- this.xOffset *= result.width / originalWidth;
1336
- this.yOffset *= result.height / originalHeight;
1337
-
1338
- return result;
1339
- }
1340
- }
1341
-
1342
- /**
1343
- * Represents a GIF image as an array of frames
1344
- * @extends Array<Frame>
1345
- */
1346
- class GIF extends Array {
1347
- /**
1348
- * Creates a new GIF image.
1349
- * @param {Frame[]} frames The frames to create the GIF from
1350
- * @param {number} [loopCount=-1] How often to loop the GIF for (-1 = unlimited)
1351
- * @property {number} loopCount How often the GIF will loop for
1352
- */
1353
- constructor(frames, loopCount = -1) {
1354
- super(...frames);
1355
-
1356
- for (const frame of this)
1357
- if (!(frame instanceof Frame))
1358
- throw new TypeError(`Frame ${this.indexOf(frame)} is not an instance of Frame`);
1359
-
1360
- if (loopCount < -1 || isNaN(loopCount))
1361
- throw new RangeError('Invalid loop count');
1362
-
1363
- this.loopCount = loopCount;
1364
- }
1365
-
1366
- /**
1367
- * The GIFs width
1368
- * @returns {number}
1369
- */
1370
- get width() {
1371
- let max = 0;
1372
- for (const frame of this) {
1373
- let width = frame.width + frame.xOffset;
1374
- if (max < width)
1375
- max = width;
1376
- }
1377
-
1378
- return max;
1379
- }
1380
-
1381
- /**
1382
- * The GIFs height
1383
- * @returns {number}
1384
- */
1385
- get height() {
1386
- let max = 0;
1387
- for (const frame of this) {
1388
- let height = frame.height + frame.yOffset;
1389
- if (max < height)
1390
- max = height;
1391
- }
1392
-
1393
- return max;
1394
- }
1395
-
1396
- toString() {
1397
- return `GIF<${this.width}x${this.height}x${this.duration}ms>`;
1398
- }
1399
-
1400
- /**
1401
- * @returns {Generator<Frame, void, *>}
1402
- */
1403
- * [Symbol.iterator]() {
1404
- for (let i = 0; i < this.length; i++)
1405
- yield this[i];
1406
- }
1407
-
1408
- slice(start, end) {
1409
- if (end === Infinity)
1410
- end = this.length;
1411
- const frames = new Array(end - start);
1412
- for (let i = 0; i < frames.length; i++)
1413
- frames[i] = this[i + start];
1414
- return new GIF(frames, this.loopCount);
1415
- }
1416
-
1417
- /**
1418
- * The GIFs duration (in ms)
1419
- * @return {number}
1420
- */
1421
- get duration() {
1422
- return this.reduce((acc, frame) => acc + frame.duration, 0);
1423
- }
1424
-
1425
- /**
1426
- * Encodes the image into a GIF
1427
- * @param {number} [quality=95] GIF quality 0-100
1428
- * @return {Promise<Uint8Array>} The encoded data
1429
- */
1430
- async encode(quality = 95) {
1431
- const encoder = new codecs.gif.encoder(this.width, this.height);
1432
-
1433
- for (const frame of this) {
1434
- if (!(frame instanceof Frame)) throw new Error('GIF contains invalid frames');
1435
-
1436
- encoder.add(frame.bitmap, {
1437
- quality,
1438
- x: frame.xOffset,
1439
- y: frame.yOffset,
1440
- width: frame.width,
1441
- speed: null, // 1-10
1442
- height: frame.height,
1443
- colors: null, // 2-256
1444
- delay: ~~(frame.duration / 10),
1445
- dispose: ['any', 'keep', 'previous', 'background'][frame.disposalMode],
1446
- });
1447
- }
1448
-
1449
- return encoder.finish({ repeat: -1 === this.loopCount ? null : this.loopCount });
1450
- }
1451
-
1452
- /**
1453
- * Decodes a GIF image
1454
- * @param {Buffer|Uint8Array} data The binary data to decode
1455
- * @param {boolean} [onlyExtractFirstFrame=false] Whether to end GIF decoding after the first frame
1456
- * @return {Promise<GIF>} The decoded GIF
1457
- */
1458
- static async decode(data, onlyExtractFirstFrame = false) {
1459
- let image;
1460
- data = mem.view(data);
1461
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
1462
-
1463
- if (ImageType.isGIF(view)) { // GIF
1464
- const frames = [];
1465
- const decoder = new (await giflib.init()).Decoder(data);
1466
-
1467
- const gwidth = decoder.width | 0;
1468
- const gheight = decoder.height | 0;
1469
- const u32 = new Uint32Array(decoder.width * decoder.height);
1470
- const u8 = new Uint8Array(u32.buffer, u32.byteOffset, u32.byteLength);
1471
-
1472
- for (const frame of decoder.frames()) {
1473
- let offset8 = 0 | 0;
1474
- let offset32 = 0 | 0;
1475
- const fx = frame.x | 0;
1476
- const fy = frame.y | 0;
1477
- const f8 = frame.buffer;
1478
- const mode = frame.dispose;
1479
- const width = frame.width | 0;
1480
- const height = frame.height | 0;
1481
- const f32 = new Uint32Array(f8.buffer, f8.byteOffset, width * height);
1482
- const f = frames[frames.push(new Frame(gwidth, gheight, 10 * frame.delay, 0, 0, 3)) - 1];
1483
-
1484
- const t8 = f.bitmap;
1485
- const t32 = new Uint32Array(t8.buffer);
1486
-
1487
- t8.set(u8);
1488
-
1489
- if (2 === mode) {
1490
- for (let y = 0 | 0; y < height; y++) {
1491
- const y_offset = fx + gwidth * (y + fy) | 0;
1492
-
1493
- for (let x = 0 | 0; x < width; x++) {
1494
- const x_offset = x + y_offset;
1495
-
1496
- if (0 === f8[3 + offset8])
1497
- t32[x_offset] = u32[x_offset];
1498
- else t32[x_offset] = f32[offset32];
1499
-
1500
- offset32++;
1501
- offset8 += 4;
1502
- }
1503
- }
1504
- }
1505
-
1506
- else if (3 === mode) {
1507
- for (let y = 0 | 0; y < height; y++) {
1508
- const y_offset = fx + gwidth * (y + fy) | 0;
1509
-
1510
- for (let x = 0 | 0; x < width; x++) {
1511
- const x_offset = x + y_offset;
1512
-
1513
- if (0 === f8[3 + offset8])
1514
- t32[x_offset] = u32[x_offset];
1515
- else t32[x_offset] = f32[offset32];
1516
-
1517
- offset32++;
1518
- offset8 += 4;
1519
- u32[x_offset] = 0;
1520
- }
1521
- }
1522
- }
1523
-
1524
- else if (0 === mode || 1 === mode) {
1525
- t8.set(u8);
1526
- for (let y = 0 | 0; y < height; y++) {
1527
- const y_offset = fx + gwidth * (y + fy) | 0;
1528
-
1529
- for (let x = 0 | 0; x < width; x++) {
1530
- const x_offset = x + y_offset;
1531
-
1532
- if (0 === f8[3 + offset8])
1533
- t32[x_offset] = u32[x_offset];
1534
- else t32[x_offset] = f32[offset32];
1535
-
1536
- offset32++;
1537
- offset8 += 4;
1538
- u32[x_offset] = t32[x_offset];
1539
- }
1540
- }
1541
- }
1542
-
1543
- if (onlyExtractFirstFrame)
1544
- break;
1545
- }
1546
-
1547
- image = new GIF(frames);
1548
- } else throw new Error('Unsupported image type');
1549
-
1550
- return image;
1551
- }
1552
-
1553
- resize(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) {
1554
- for (const frame of this)
1555
- frame.resize(width, height, mode);
1556
- }
1557
- }
1558
-
1559
- class TextLayout {
1560
- /**
1561
- * Layout options for {@link Image.renderText}
1562
- * @param {object} [options]
1563
- * @param {number} [options.maxWidth=Infinity] The texts max width
1564
- * @param {number} [options.maxHeight=Infinity] The texts max height
1565
- * @param {string} [options.wrapStyle='word'] The texts wrap style when reaching the max width (word, char)
1566
- * @param {string} [options.verticalAlign='left'] The vertical align mode (left, center, right)
1567
- * @param {string} [options.horizontalAlign='top'] The horizontal align mode (top, middle, bottom)
1568
- * @param {boolean} [options.wrapHardBreaks=true] Whether to force wrap at new line characters
1569
- */
1570
- constructor(options) {
1571
- const {maxWidth, maxHeight, wrapStyle, verticalAlign, horizontalAlign, wrapHardBreaks} = options || {};
1572
-
1573
- this.maxWidth = maxWidth || Infinity;
1574
- if (isNaN(this.maxWidth) || this.maxWidth < 1)
1575
- throw new RangeError('Invalid maxWidth');
1576
-
1577
- this.maxHeight = maxHeight || Infinity;
1578
- if (isNaN(this.maxHeight) || this.maxHeight < 1)
1579
- throw new RangeError('Invalid maxHeight');
1580
-
1581
- this.wrapStyle = wrapStyle || 'word';
1582
- if (!['word', 'char'].includes(this.wrapStyle))
1583
- throw new RangeError('Invalid wrapStyle');
1584
-
1585
- this.verticalAlign = verticalAlign || 'left';
1586
- if (!['left', 'center', 'right'].includes(this.verticalAlign))
1587
- throw new RangeError('Invalid verticalAlign');
1588
-
1589
- this.horizontalAlign = horizontalAlign || 'top';
1590
- if (!['top', 'middle', 'bottom'].includes(this.horizontalAlign))
1591
- throw new RangeError('Invalid horizontalAlign');
1592
-
1593
- this.wrapHardBreaks = typeof wrapHardBreaks === 'undefined' ? true : wrapHardBreaks;
1594
- if (typeof this.wrapHardBreaks !== 'boolean')
1595
- throw new TypeError('Invalid wrapHardBreaks');
1596
- }
1597
- }
1598
-
1599
- class ImageType {
1600
- /**
1601
- * Gets an images type (png, jpeg, tiff, gif)
1602
- * @param {Buffer|Uint8Array} data The image binary to get the type of
1603
- * @returns {string|null} The image type (png, jpeg, tiff, gif, null)
1604
- */
1605
- static getType(data) {
1606
- let view;
1607
- if (!ArrayBuffer.isView(data)) {
1608
- data = new Uint8Array(data);
1609
- view = new DataView(data.buffer);
1610
- } else {
1611
- data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
1612
- view = new DataView(data.buffer, data.byteOffset, data.byteLength);
1613
- }
1614
-
1615
- if (this.isPNG(view)) return 'png';
1616
- if (this.isJPEG(view)) return 'jpeg';
1617
- if (this.isTIFF(view)) return 'tiff';
1618
- if (this.isGIF(view)) return 'gif';
1619
- return null;
1620
- }
1621
-
1622
- /**
1623
- * @param {DataView} view
1624
- * @returns {boolean}
1625
- */
1626
- static isPNG(view) {
1627
- return view.getUint32(0, false) === MAGIC_NUMBERS.PNG;
1628
- }
1629
-
1630
- /**
1631
- * @param {DataView} view
1632
- * @returns {boolean}
1633
- */
1634
- static isJPEG(view) {
1635
- return (view.getUint32(0, false) >>> 8) === MAGIC_NUMBERS.JPEG;
1636
- }
1637
-
1638
- /**
1639
- * @param {DataView} view
1640
- * @returns {boolean}
1641
- */
1642
- static isTIFF(view) {
1643
- return view.getUint32(0, false) === MAGIC_NUMBERS.TIFF;
1644
- }
1645
-
1646
- /**
1647
- * @param {DataView} view
1648
- * @returns {boolean}
1649
- */
1650
- static isGIF(view) {
1651
- return (view.getUint32(0, false) >>> 8) === MAGIC_NUMBERS.GIF;
1652
- }
1653
- }
1654
-
1655
- /**
1656
- * Decodes the given image binary
1657
- * @param {Uint8Array|Buffer} data The image data
1658
- * @param {boolean} [onlyExtractFirstFrame] Whether to end GIF decoding after the first frame
1659
- * @returns {Promise<GIF|Image>} The decoded image
1660
- */
1661
- function decode(data, onlyExtractFirstFrame) {
1662
- const type = ImageType.getType(data);
1663
-
1664
- if (type === 'gif')
1665
- return GIF.decode(data, onlyExtractFirstFrame);
1666
- return Image.decode(data);
1667
- }
1668
-
1669
- module.exports = {Image, GIF, Frame, TextLayout, ImageType, decode};