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/dist/image.js ADDED
@@ -0,0 +1,1118 @@
1
+ import * as png from "./codecs/png.js";
2
+ import * as jpeg from "./codecs/jpeg.js";
3
+ import * as tiff from "./codecs/tiff.js";
4
+ import * as quantize from "./codecs/quantize.js";
5
+ import * as textRenderer from "./render/text.js";
6
+ import * as svgRenderer from "./render/svg.js";
7
+ import { view } from "./util/mem.js";
8
+ import { ImageType } from "./image-type.js";
9
+ import { TextLayout } from "./text-layout.js";
10
+ import { VERSION } from "./version.js";
11
+ import { resample, BOX, TRIANGLE, CUBIC } from "./util/resample.js";
12
+ import { gaussianBlur, premultiply, unpremultiply } from "./util/blur.js";
13
+ export class Image {
14
+ __width__;
15
+ __height__;
16
+ __view__;
17
+ __u32__;
18
+ __bitmap__;
19
+ constructor(width, height) {
20
+ width = ~~width;
21
+ height = ~~height;
22
+ if (width < 1)
23
+ throw new RangeError('Image has to be at least 1 pixel wide');
24
+ if (height < 1)
25
+ throw new RangeError('Image has to be at least 1 pixel high');
26
+ this.__width__ = width;
27
+ this.__height__ = height;
28
+ this.__set_bitmap__(new Uint8ClampedArray(width * height * 4));
29
+ }
30
+ get bitmap() {
31
+ return this.__bitmap__;
32
+ }
33
+ set bitmap(bitmap) {
34
+ if (!(bitmap instanceof Uint8ClampedArray || bitmap instanceof Uint8Array))
35
+ throw new TypeError('The bitmap has to be a Uint8ClampedArray or Uint8Array');
36
+ if (bitmap.length !== this.width * this.height * 4)
37
+ throw new RangeError(`The bitmap has to have ${this.width * this.height * 4} bytes (width * height * 4), got ${bitmap.length}`);
38
+ this.__set_bitmap__(bitmap instanceof Uint8ClampedArray ? bitmap : new Uint8ClampedArray(bitmap.buffer, bitmap.byteOffset, bitmap.length));
39
+ }
40
+ __set_bitmap__(bitmap) {
41
+ if (bitmap.byteOffset % 4)
42
+ bitmap = bitmap.slice();
43
+ this.__bitmap__ = bitmap;
44
+ this.__view__ = new DataView(bitmap.buffer, bitmap.byteOffset, bitmap.byteLength);
45
+ this.__u32__ = new Uint32Array(bitmap.buffer, bitmap.byteOffset, bitmap.length >> 2);
46
+ }
47
+ toString() {
48
+ return `Image<${this.width}x${this.height}>`;
49
+ }
50
+ get width() {
51
+ return this.__width__;
52
+ }
53
+ get height() {
54
+ return this.__height__;
55
+ }
56
+ *[Symbol.iterator]() {
57
+ for (let y = 0; y < this.height; y++)
58
+ for (let x = 0; x < this.width; x++)
59
+ yield [x, y];
60
+ }
61
+ *iterateWithColors() {
62
+ const view = new DataView(this.bitmap.buffer, this.bitmap.byteOffset, this.bitmap.byteLength);
63
+ let offset = 0;
64
+ for (let y = 0; y < this.height; y++)
65
+ for (let x = 0; x < this.width; x++, offset += 4)
66
+ yield [x, y, view.getUint32(offset, false)];
67
+ }
68
+ static rgbaToColor(r, g, b, a) {
69
+ return (channel(r) << 24 | channel(g) << 16 | channel(b) << 8 | channel(a)) >>> 0;
70
+ }
71
+ static rgbToColor(r, g, b) {
72
+ return Image.rgbaToColor(r, g, b, 0xff);
73
+ }
74
+ static hslaToColor(h, s, l, a) {
75
+ h = wrapHue(h);
76
+ s = Math.min(1, Math.max(0, s));
77
+ l = Math.min(1, Math.max(0, l));
78
+ a = Math.min(1, Math.max(0, a));
79
+ let r, g, b;
80
+ if (s === 0) {
81
+ r = g = b = l;
82
+ }
83
+ else {
84
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
85
+ const p = 2 * l - q;
86
+ r = hue2rgb(p, q, h + 1 / 3);
87
+ g = hue2rgb(p, q, h);
88
+ b = hue2rgb(p, q, h - 1 / 3);
89
+ }
90
+ return Image.rgbaToColor(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255), Math.round(a * 255));
91
+ }
92
+ static hslToColor(h, s, l) {
93
+ return Image.hslaToColor(h, s, l, 1);
94
+ }
95
+ static rgbaToHSLA(r, g, b, a) {
96
+ r /= 255;
97
+ g /= 255;
98
+ b /= 255;
99
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
100
+ let h = 0, s;
101
+ const l = (max + min) / 2;
102
+ if (max === min) {
103
+ h = s = 0;
104
+ }
105
+ else {
106
+ const d = max - min;
107
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
108
+ switch (max) {
109
+ case r:
110
+ h = (g - b) / d + (g < b ? 6 : 0);
111
+ break;
112
+ case g:
113
+ h = (b - r) / d + 2;
114
+ break;
115
+ case b:
116
+ h = (r - g) / d + 4;
117
+ break;
118
+ }
119
+ h /= 6;
120
+ }
121
+ return [h, s, l, a / 255];
122
+ }
123
+ static colorToRGBA(color) {
124
+ return [(color >> 24) & 0xff, (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff];
125
+ }
126
+ static colorToRGB(color) {
127
+ const [r, g, b] = Image.colorToRGBA(color);
128
+ return [r, g, b];
129
+ }
130
+ getPixelAt(x, y) {
131
+ x = ~~x;
132
+ y = ~~y;
133
+ this.__check_boundaries__(x, y);
134
+ return this.__view__.getUint32((y * this.width + x) * 4, false);
135
+ }
136
+ getRGBAAt(x, y) {
137
+ x = ~~x;
138
+ y = ~~y;
139
+ this.__check_boundaries__(x, y);
140
+ const idx = (y * this.width + x) * 4;
141
+ return this.bitmap.subarray(idx, idx + 4);
142
+ }
143
+ setPixelAt(x, y, pixelColor) {
144
+ x = ~~x;
145
+ y = ~~y;
146
+ this.__check_boundaries__(x, y);
147
+ this.__set_pixel__(x, y, pixelColor);
148
+ return this;
149
+ }
150
+ __set_pixel__(x, y, pixelColor) {
151
+ this.__view__.setUint32((y * this.width + x) * 4, pixelColor, false);
152
+ }
153
+ __check_boundaries__(x, y) {
154
+ if (isNaN(x))
155
+ throw new TypeError(`Invalid pixel coordinates (x=${x})`);
156
+ if (isNaN(y))
157
+ throw new TypeError(`Invalid pixel coordinates (y=${y})`);
158
+ if (x < 0)
159
+ throw new RangeError(`${Image.__out_of_bounds__} (x=${x})<0`);
160
+ if (x >= this.width)
161
+ throw new RangeError(`${Image.__out_of_bounds__} (x=${x})>=(width=${this.width})`);
162
+ if (y < 0)
163
+ throw new RangeError(`${Image.__out_of_bounds__} (y=${y})<0`);
164
+ if (y >= this.height)
165
+ throw new RangeError(`${Image.__out_of_bounds__} (y=${y})>=(height=${this.height})`);
166
+ }
167
+ static get __out_of_bounds__() {
168
+ return 'Tried referencing a pixel outside of the images boundaries:';
169
+ }
170
+ fill(color) {
171
+ const view = new DataView(this.bitmap.buffer, this.bitmap.byteOffset, this.bitmap.byteLength);
172
+ if (typeof color === 'function') {
173
+ let offset = 0;
174
+ for (let y = 0; y < this.height; y++)
175
+ for (let x = 0; x < this.width; x++, offset += 4)
176
+ view.setUint32(offset, color(x, y), false);
177
+ }
178
+ else {
179
+ if (!this.bitmap.length)
180
+ return this;
181
+ view.setUint32(0, color, false);
182
+ const u32 = new Uint32Array(this.bitmap.buffer, this.bitmap.byteOffset, this.bitmap.byteLength / 4);
183
+ u32.fill(u32[0]);
184
+ }
185
+ return this;
186
+ }
187
+ clone() {
188
+ const image = new Image(this.width, this.height);
189
+ image.bitmap.set(this.bitmap);
190
+ return image;
191
+ }
192
+ static get RESIZE_NEAREST_NEIGHBOR() {
193
+ return 'RESIZE_NEAREST_NEIGHBOR';
194
+ }
195
+ static get RESIZE_BILINEAR() {
196
+ return 'RESIZE_BILINEAR';
197
+ }
198
+ static get RESIZE_BICUBIC() {
199
+ return 'RESIZE_BICUBIC';
200
+ }
201
+ static get RESIZE_AREA() {
202
+ return 'RESIZE_AREA';
203
+ }
204
+ static get RESIZE_AUTO() {
205
+ return -1;
206
+ }
207
+ scale(factor, mode = Image.RESIZE_BILINEAR) {
208
+ const image = this.__scale__(factor, mode);
209
+ return this.__apply__(image);
210
+ }
211
+ __scale__(factor, mode = Image.RESIZE_BILINEAR) {
212
+ if (factor === 1)
213
+ return this;
214
+ return this.__resize__(this.width * factor, this.height * factor, mode);
215
+ }
216
+ resize(width, height, mode = Image.RESIZE_BILINEAR) {
217
+ const image = this.__resize__(width, height, mode);
218
+ return this.__apply__(image);
219
+ }
220
+ contain(width, height, mode = Image.RESIZE_BILINEAR) {
221
+ width = Math.floor(width);
222
+ height = Math.floor(height);
223
+ const scale = Math.min(width / this.width, height / this.height);
224
+ return this.resize(Math.max(1, Math.min(width, Math.round(this.width * scale))), Math.max(1, Math.min(height, Math.round(this.height * scale))), mode);
225
+ }
226
+ fit(width, height, mode = Image.RESIZE_BILINEAR) {
227
+ const result = new Image(width, height);
228
+ this.contain(width, height, mode);
229
+ result.composite(this, (width - this.width) / 2, (height - this.height) / 2);
230
+ return this.__apply__(result);
231
+ }
232
+ cover(width, height, mode = Image.RESIZE_BILINEAR) {
233
+ width = Math.floor(width);
234
+ height = Math.floor(height);
235
+ const scale = Math.max(width / this.width, height / this.height);
236
+ this.resize(Math.max(width, Math.round(this.width * scale)), Math.max(height, Math.round(this.height * scale)), mode);
237
+ return this.crop((this.width - width) / 2, (this.height - height) / 2, width, height);
238
+ }
239
+ __resize__(width, height, mode = Image.RESIZE_BILINEAR) {
240
+ if (width === Image.RESIZE_AUTO && height === Image.RESIZE_AUTO)
241
+ throw new Error('RESIZE_AUTO can only be used for either width or height, not for both');
242
+ else if (width === Image.RESIZE_AUTO)
243
+ width = Math.max(1, Math.round(this.width / this.height * Math.floor(height)));
244
+ else if (height === Image.RESIZE_AUTO)
245
+ height = Math.max(1, Math.round(this.height / this.width * Math.floor(width)));
246
+ width = Math.floor(width);
247
+ height = Math.floor(height);
248
+ if (width < 1)
249
+ throw new RangeError('Image has to be at least 1 pixel wide');
250
+ if (height < 1)
251
+ throw new RangeError('Image has to be at least 1 pixel high');
252
+ if (mode === Image.RESIZE_NEAREST_NEIGHBOR)
253
+ return this.__resize_nearest_neighbor__(width, height);
254
+ const filter = mode === Image.RESIZE_BILINEAR ? TRIANGLE : mode === Image.RESIZE_BICUBIC ? CUBIC : mode === Image.RESIZE_AREA ? BOX : null;
255
+ if (!filter)
256
+ throw new Error('Invalid resize mode');
257
+ const image = new Image(width, height);
258
+ resample(this.bitmap, this.width, this.height, width, height, filter, image.bitmap);
259
+ return image;
260
+ }
261
+ __resize_nearest_neighbor__(width, height) {
262
+ const image = new Image(width, height);
263
+ const src = this.__u32__;
264
+ const dst = image.__u32__;
265
+ for (let y = 0; y < height; y++) {
266
+ const row = this.width * ((y + 0.5) * (this.height / height) | 0);
267
+ for (let x = 0; x < width; x++)
268
+ dst[y * width + x] = src[row + ((x + 0.5) * (this.width / width) | 0)];
269
+ }
270
+ return image;
271
+ }
272
+ crop(x, y, width, height) {
273
+ x = ~~x;
274
+ y = ~~y;
275
+ width = ~~width;
276
+ height = ~~height;
277
+ const left = Math.max(0, x), top = Math.max(0, y);
278
+ const right = Math.min(this.width, x + width), bottom = Math.min(this.height, y + height);
279
+ if (right <= left || bottom <= top)
280
+ throw new RangeError('Crop region does not overlap the image');
281
+ return this.__apply__(this.__crop__(left, top, right - left, bottom - top));
282
+ }
283
+ __crop__(x, y, width, height) {
284
+ x = ~~x;
285
+ y = ~~y;
286
+ const image = new Image(width, height);
287
+ for (let tY = 0; tY < height; tY++) {
288
+ const idx = (tY + y) * this.width + x;
289
+ image.__u32__.set(this.__u32__.subarray(idx, idx + width), tY * width);
290
+ }
291
+ return image;
292
+ }
293
+ drawBox(x, y, width, height, color) {
294
+ x = ~~x;
295
+ y = ~~y;
296
+ width = ~~width;
297
+ height = ~~height;
298
+ if (typeof color !== 'function')
299
+ return this.__fast_box__(x, y, width, height, color);
300
+ for (let tY = Math.max(0, -y); tY < height && tY + y < this.height; tY++)
301
+ for (let tX = Math.max(0, -x); tX < width && tX + x < this.width; tX++)
302
+ this.__set_pixel__(tX + x, tY + y, color(tX, tY));
303
+ return this;
304
+ }
305
+ __fast_box__(x, y, width, height, color) {
306
+ const left = Math.max(0, x), top = Math.max(0, y);
307
+ const right = Math.min(this.width, x + width), bottom = Math.min(this.height, y + height);
308
+ if (right <= left || bottom <= top)
309
+ return this;
310
+ for (let xPos = left; xPos < right; xPos++)
311
+ this.__view__.setUint32(4 * (xPos + top * this.width), color);
312
+ const start = 4 * (left + top * this.width);
313
+ const end = 4 * (right + top * this.width);
314
+ for (let row = top + 1; row < bottom; row++)
315
+ this.bitmap.copyWithin(4 * (left + row * this.width), start, end);
316
+ return this;
317
+ }
318
+ drawCircle(x, y, radius, color) {
319
+ x = ~~x;
320
+ y = ~~y;
321
+ const cx = x + 0.5, cy = y + 0.5;
322
+ for (let py = Math.max(0, Math.floor(cy - radius)); py < Math.min(this.height, Math.ceil(cy + radius)); py++) {
323
+ for (let px = Math.max(0, Math.floor(cx - radius)); px < Math.min(this.width, Math.ceil(cx + radius)); px++) {
324
+ const coverage = circleCoverage(px, py, cx, cy, radius);
325
+ if (coverage <= 0)
326
+ continue;
327
+ const value = typeof color === 'function' ? color(px - x + radius, py - y + radius) : color;
328
+ if (coverage >= 1)
329
+ this.__set_pixel__(px, py, value);
330
+ else
331
+ this.__blend_pixel__(px, py, value, coverage);
332
+ }
333
+ }
334
+ return this;
335
+ }
336
+ __blend_pixel__(x, y, color, amount) {
337
+ const i = (y * this.width + x) * 4, b = this.bitmap;
338
+ const sa = (color & 0xff) / 255 * amount, da = b[i + 3] / 255 * (1 - amount);
339
+ const a = sa + da;
340
+ if (a <= 0) {
341
+ b[i] = b[i + 1] = b[i + 2] = b[i + 3] = 0;
342
+ return;
343
+ }
344
+ b[i] = ((color >>> 24) * sa + b[i] * da) / a;
345
+ b[i + 1] = ((color >> 16 & 0xff) * sa + b[i + 1] * da) / a;
346
+ b[i + 2] = ((color >> 8 & 0xff) * sa + b[i + 2] * da) / a;
347
+ b[i + 3] = a * 255;
348
+ }
349
+ cropCircle(maxOrOptions = false, featheringArgument = 0) {
350
+ const { max = false, feathering = 0 } = typeof maxOrOptions === 'object' && maxOrOptions !== null
351
+ ? maxOrOptions : { max: maxOrOptions, feathering: featheringArgument };
352
+ const radius = (max ? Math.max : Math.min)(this.width, this.height) / 2;
353
+ const cx = this.width / 2, cy = this.height / 2;
354
+ const band = Math.min(1, Math.max(0, feathering)) * radius;
355
+ for (let y = 0; y < this.height; y++) {
356
+ for (let x = 0; x < this.width; x++) {
357
+ const alpha = 4 * (y * this.width + x) + 3;
358
+ const dx = x + 0.5 - cx, dy = y + 0.5 - cy;
359
+ if (dx * dx + dy * dy <= (radius - Math.max(band, 1)) ** 2)
360
+ continue;
361
+ const factor = band > 1
362
+ ? Math.min(1, Math.max(0, (radius - Math.hypot(dx, dy)) / band))
363
+ : circleCoverage(x, y, cx, cy, radius);
364
+ if (factor < 1)
365
+ this.bitmap[alpha] *= factor;
366
+ }
367
+ }
368
+ return this;
369
+ }
370
+ opacity(opacity, absolute = false) {
371
+ if (isNaN(opacity) || opacity < 0)
372
+ throw new RangeError('Invalid opacity value');
373
+ this.__set_channel_value__(opacity, absolute, 3);
374
+ return this;
375
+ }
376
+ red(factor, absolute = false) {
377
+ if (isNaN(factor) || factor < 0)
378
+ throw new RangeError('Invalid red factor');
379
+ this.__set_channel_value__(factor, absolute, 0);
380
+ return this;
381
+ }
382
+ green(factor, absolute = false) {
383
+ if (isNaN(factor) || factor < 0)
384
+ throw new RangeError('Invalid green factor');
385
+ this.__set_channel_value__(factor, absolute, 1);
386
+ return this;
387
+ }
388
+ blue(factor, absolute = false) {
389
+ if (isNaN(factor) || factor < 0)
390
+ throw new RangeError('Invalid blue factor');
391
+ this.__set_channel_value__(factor, absolute, 2);
392
+ return this;
393
+ }
394
+ __set_channel_value__(value, absolute, offset) {
395
+ for (let i = offset; i < this.bitmap.length; i += 4)
396
+ this.bitmap[i] = value * (absolute ? 255 : this.bitmap[i]);
397
+ }
398
+ lightness(value, absolute = false) {
399
+ if (isNaN(value) || value < 0)
400
+ throw new RangeError('Invalid lightness value');
401
+ return this.__map_hsla__(c => { c.l = value * (absolute ? 1 : c.l); });
402
+ }
403
+ saturation(value, absolute = false) {
404
+ if (isNaN(value) || value < 0)
405
+ throw new RangeError('Invalid saturation value');
406
+ return this.__map_hsla__(c => { c.s = value * (absolute ? 1 : c.s); });
407
+ }
408
+ composite(source, x = 0, y = 0, options = {}) {
409
+ const { opacity = 1, mode = 'normal' } = options;
410
+ x |= 0;
411
+ y |= 0;
412
+ if (isNaN(opacity) || opacity < 0 || opacity > 1)
413
+ throw new RangeError('Invalid composite opacity');
414
+ const blend = BLEND_MODES[mode];
415
+ if (!blend || !Object.prototype.hasOwnProperty.call(BLEND_MODES, mode))
416
+ throw new RangeError(`Invalid blend mode: ${mode}`);
417
+ if (opacity !== 1 || mode !== 'normal')
418
+ return this.__blend__(source, x, y, opacity, blend);
419
+ const left = Math.max(0, x), top = Math.max(0, y);
420
+ const right = Math.min(this.width, x + source.width), bottom = Math.min(this.height, y + source.height);
421
+ const dst = this.bitmap, src = source.bitmap;
422
+ const dst32 = new Uint32Array(dst.buffer, dst.byteOffset, dst.length >> 2);
423
+ const src32 = new Uint32Array(src.buffer, src.byteOffset, src.length >> 2);
424
+ for (let row = top; row < bottom; row++) {
425
+ for (let column = left; column < right; column++) {
426
+ const sp = (row - y) * source.width + column - x, s = 4 * sp;
427
+ const sa = src[s + 3];
428
+ if (sa === 0)
429
+ continue;
430
+ const dp = row * this.width + column, d = 4 * dp;
431
+ const da = dst[d + 3];
432
+ if (sa === 255 || da === 0) {
433
+ dst32[dp] = src32[sp];
434
+ continue;
435
+ }
436
+ const fa = sa / 255, ba = da / 255 * (1 - fa);
437
+ const a = fa + ba;
438
+ dst[d] = (src[s] * fa + dst[d] * ba) / a;
439
+ dst[d + 1] = (src[s + 1] * fa + dst[d + 1] * ba) / a;
440
+ dst[d + 2] = (src[s + 2] * fa + dst[d + 2] * ba) / a;
441
+ dst[d + 3] = a * 255;
442
+ }
443
+ }
444
+ return this;
445
+ }
446
+ __blend__(source, x, y, opacity, blend) {
447
+ const left = Math.max(0, x), top = Math.max(0, y);
448
+ const right = Math.min(this.width, x + source.width), bottom = Math.min(this.height, y + source.height);
449
+ const dst = this.bitmap, src = source.bitmap;
450
+ for (let row = top; row < bottom; row++) {
451
+ for (let column = left; column < right; column++) {
452
+ const s = 4 * ((row - y) * source.width + column - x);
453
+ const as = src[s + 3] / 255 * opacity;
454
+ if (as <= 0)
455
+ continue;
456
+ const d = 4 * (row * this.width + column);
457
+ const ab = dst[d + 3] / 255;
458
+ const ao = as + ab * (1 - as);
459
+ for (let c = 0; c < 3; c++) {
460
+ const cs = src[s + c] / 255, cb = dst[d + c] / 255;
461
+ const cr = (1 - ab) * cs + ab * blend(cb, cs);
462
+ dst[d + c] = (as * cr + ab * (1 - as) * cb) / ao * 255;
463
+ }
464
+ dst[d + 3] = ao * 255;
465
+ }
466
+ }
467
+ return this;
468
+ }
469
+ invert() {
470
+ const bitmap = this.bitmap;
471
+ for (let i = 0; i < bitmap.length; i += 4) {
472
+ bitmap[i] = 255 - bitmap[i];
473
+ bitmap[i + 1] = 255 - bitmap[i + 1];
474
+ bitmap[i + 2] = 255 - bitmap[i + 2];
475
+ }
476
+ return this;
477
+ }
478
+ invertLightness() {
479
+ return this.__map_hsla__(c => { c.l = 1 - c.l; });
480
+ }
481
+ invertValue() {
482
+ const bitmap = this.bitmap;
483
+ for (let i = 0; i < bitmap.length; i += 4) {
484
+ const max = Math.max(bitmap[i], bitmap[i + 1], bitmap[i + 2]);
485
+ if (max === 0) {
486
+ bitmap[i] = bitmap[i + 1] = bitmap[i + 2] = 255;
487
+ continue;
488
+ }
489
+ const factor = (255 - max) / max;
490
+ bitmap[i] *= factor;
491
+ bitmap[i + 1] *= factor;
492
+ bitmap[i + 2] *= factor;
493
+ }
494
+ return this;
495
+ }
496
+ hueShift(degrees) {
497
+ return this.__map_hsla__(c => { c.h += degrees / 360; });
498
+ }
499
+ __map_hsla__(map) {
500
+ const bitmap = this.bitmap;
501
+ const hsla = { h: 0, s: 0, l: 0, a: 0 };
502
+ const pixels = new Uint32Array(bitmap.buffer, bitmap.byteOffset, bitmap.length >> 2);
503
+ const keys = new Uint32Array(4096), values = new Uint32Array(4096);
504
+ keys[0] = 1;
505
+ for (let i = 0; i < bitmap.length; i += 4) {
506
+ const color = pixels[i >> 2];
507
+ const slot = Math.imul(color, 0x9e3779b1) >>> 20;
508
+ if (keys[slot] === color) {
509
+ pixels[i >> 2] = values[slot];
510
+ continue;
511
+ }
512
+ const r = bitmap[i] / 255, g = bitmap[i + 1] / 255, b = bitmap[i + 2] / 255;
513
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
514
+ const l = (max + min) / 2;
515
+ let h = 0, s = 0;
516
+ if (max !== min) {
517
+ const d = max - min;
518
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
519
+ if (max === r)
520
+ h = (g - b) / d + (g < b ? 6 : 0);
521
+ else if (max === g)
522
+ h = (b - r) / d + 2;
523
+ else
524
+ h = (r - g) / d + 4;
525
+ h /= 6;
526
+ }
527
+ hsla.h = h;
528
+ hsla.s = s;
529
+ hsla.l = l;
530
+ hsla.a = bitmap[i + 3] / 255;
531
+ map(hsla);
532
+ const nh = wrapHue(hsla.h);
533
+ const ns = Math.min(1, Math.max(0, hsla.s));
534
+ const nl = Math.min(1, Math.max(0, hsla.l));
535
+ const na = Math.min(1, Math.max(0, hsla.a));
536
+ if (ns === 0)
537
+ bitmap[i] = bitmap[i + 1] = bitmap[i + 2] = Math.round(nl * 255);
538
+ else {
539
+ const q = nl < 0.5 ? nl * (1 + ns) : nl + ns - nl * ns;
540
+ const p = 2 * nl - q;
541
+ bitmap[i] = Math.round(hue2rgb(p, q, nh + 1 / 3) * 255);
542
+ bitmap[i + 1] = Math.round(hue2rgb(p, q, nh) * 255);
543
+ bitmap[i + 2] = Math.round(hue2rgb(p, q, nh - 1 / 3) * 255);
544
+ }
545
+ bitmap[i + 3] = Math.round(na * 255);
546
+ keys[slot] = color;
547
+ values[slot] = pixels[i >> 2];
548
+ }
549
+ return this;
550
+ }
551
+ blur(radius) {
552
+ if (isNaN(radius) || radius < 0)
553
+ throw new RangeError('Invalid blur radius');
554
+ const pixels = premultiply(this.bitmap);
555
+ gaussianBlur(pixels, this.width, this.height, radius);
556
+ unpremultiply(pixels, this.bitmap);
557
+ return this;
558
+ }
559
+ sharpen(amount = 1, radius = 1) {
560
+ if (isNaN(amount) || amount < 0)
561
+ throw new RangeError('Invalid sharpen amount');
562
+ if (isNaN(radius) || radius <= 0)
563
+ throw new RangeError('Invalid sharpen radius');
564
+ const blurred = premultiply(this.bitmap);
565
+ gaussianBlur(blurred, this.width, this.height, radius);
566
+ const bitmap = this.bitmap;
567
+ for (let i = 0; i < bitmap.length; i += 4) {
568
+ const a = blurred[i + 3];
569
+ if (a < 0.5 || bitmap[i + 3] === 0)
570
+ continue;
571
+ const f = 255 / a;
572
+ bitmap[i] += amount * (bitmap[i] - blurred[i] * f);
573
+ bitmap[i + 1] += amount * (bitmap[i + 1] - blurred[i + 1] * f);
574
+ bitmap[i + 2] += amount * (bitmap[i + 2] - blurred[i + 2] * f);
575
+ }
576
+ return this;
577
+ }
578
+ brightness(factor) {
579
+ if (isNaN(factor) || factor < 0)
580
+ throw new RangeError('Invalid brightness factor');
581
+ return this.__map_channels__(v => v * factor);
582
+ }
583
+ contrast(factor) {
584
+ if (isNaN(factor) || factor < 0)
585
+ throw new RangeError('Invalid contrast factor');
586
+ return this.__map_channels__(v => (v - 127.5) * factor + 127.5);
587
+ }
588
+ gamma(gamma) {
589
+ if (isNaN(gamma) || gamma <= 0)
590
+ throw new RangeError('Invalid gamma');
591
+ return this.__map_channels__(v => 255 * (v / 255) ** (1 / gamma));
592
+ }
593
+ greyscale() {
594
+ const bitmap = this.bitmap;
595
+ for (let i = 0; i < bitmap.length; i += 4)
596
+ bitmap[i] = bitmap[i + 1] = bitmap[i + 2] = bitmap[i] * 19595 + bitmap[i + 1] * 38470 + bitmap[i + 2] * 7471 + 0x8000 >> 16;
597
+ return this;
598
+ }
599
+ sepia() {
600
+ const bitmap = this.bitmap;
601
+ for (let i = 0; i < bitmap.length; i += 4) {
602
+ const r = bitmap[i], g = bitmap[i + 1], b = bitmap[i + 2];
603
+ bitmap[i] = 0.393 * r + 0.769 * g + 0.189 * b;
604
+ bitmap[i + 1] = 0.349 * r + 0.686 * g + 0.168 * b;
605
+ bitmap[i + 2] = 0.272 * r + 0.534 * g + 0.131 * b;
606
+ }
607
+ return this;
608
+ }
609
+ __map_channels__(map) {
610
+ const lut = new Uint8ClampedArray(256);
611
+ for (let v = 0; v < 256; v++)
612
+ lut[v] = map(v);
613
+ const bitmap = this.bitmap;
614
+ for (let i = 0; i < bitmap.length; i += 4) {
615
+ bitmap[i] = lut[bitmap[i]];
616
+ bitmap[i + 1] = lut[bitmap[i + 1]];
617
+ bitmap[i + 2] = lut[bitmap[i + 2]];
618
+ }
619
+ return this;
620
+ }
621
+ averageColor() {
622
+ let r = 0, g = 0, b = 0, weight = 0;
623
+ for (let i = 0; i < this.bitmap.length; i += 4) {
624
+ const a = this.bitmap[i + 3];
625
+ r += this.bitmap[i] * a;
626
+ g += this.bitmap[i + 1] * a;
627
+ b += this.bitmap[i + 2] * a;
628
+ weight += a;
629
+ }
630
+ if (!weight)
631
+ return Image.rgbaToColor(0, 0, 0, 0xff);
632
+ return Image.rgbaToColor(Math.round(r / weight), Math.round(g / weight), Math.round(b / weight), 0xff);
633
+ }
634
+ dominantColor(ignoreBlackOrOptions = true, ignoreWhiteArgument = true, thresholdArgument = 0xf) {
635
+ const { ignoreBlack = true, ignoreWhite = true, threshold: bwThreshold = 0xf } = typeof ignoreBlackOrOptions === 'object' && ignoreBlackOrOptions !== null
636
+ ? ignoreBlackOrOptions : { ignoreBlack: ignoreBlackOrOptions, ignoreWhite: ignoreWhiteArgument, threshold: thresholdArgument };
637
+ const threshold = Math.min(64, Math.max(0, bwThreshold)) * 4;
638
+ const pixels = this.__bytes__();
639
+ const color = quantize.dominantColor(pixels, (r, g, b) => !(ignoreBlack && Math.max(r, g, b) < threshold) && !(ignoreWhite && Math.min(r, g, b) > 255 - threshold))
640
+ || quantize.dominantColor(pixels, () => true);
641
+ return color ? Image.rgbToColor(...color) : Image.rgbaToColor(0, 0, 0, 0xff);
642
+ }
643
+ rotate(angle, resizeOrOptions = true) {
644
+ const { resize = true } = typeof resizeOrOptions === 'object' && resizeOrOptions !== null ? resizeOrOptions : { resize: resizeOrOptions };
645
+ angle = ((angle % 360) + 360) % 360;
646
+ if (angle === 0)
647
+ return this;
648
+ const { width, height } = this;
649
+ const src = this.__u32__;
650
+ if (angle === 90 || angle === 180 || angle === 270) {
651
+ const out = new Image(angle === 180 ? width : height, angle === 180 ? height : width);
652
+ const dst = out.__u32__;
653
+ for (let y = 0; y < height; y++)
654
+ for (let x = 0; x < width; x++) {
655
+ const i = src[y * width + x];
656
+ if (angle === 90)
657
+ dst[(width - 1 - x) * height + y] = i;
658
+ else if (angle === 180)
659
+ dst[(height - 1 - y) * width + width - 1 - x] = i;
660
+ else
661
+ dst[x * height + height - 1 - y] = i;
662
+ }
663
+ return this.__apply__(out);
664
+ }
665
+ const rad = angle * Math.PI / 180;
666
+ const sin = Math.sin(rad), cos = Math.cos(rad);
667
+ const outWidth = resize ? Math.round(Math.abs(width * cos) + Math.abs(height * sin)) : width;
668
+ const outHeight = resize ? Math.round(Math.abs(width * sin) + Math.abs(height * cos)) : height;
669
+ const out = new Image(outWidth, outHeight);
670
+ const s8 = this.bitmap, d8 = out.bitmap;
671
+ for (let oy = 0; oy < outHeight; oy++) {
672
+ for (let ox = 0; ox < outWidth; ox++) {
673
+ const dx = ox + 0.5 - outWidth / 2, dy = oy + 0.5 - outHeight / 2;
674
+ const sx = dx * cos - dy * sin + width / 2 - 0.5;
675
+ const sy = dx * sin + dy * cos + height / 2 - 0.5;
676
+ const x0 = Math.floor(sx), y0 = Math.floor(sy);
677
+ if (x0 < -1 || y0 < -1 || x0 >= width || y0 >= height)
678
+ continue;
679
+ const fx = sx - x0, fy = sy - y0;
680
+ const w00 = (1 - fx) * (1 - fy), w10 = fx * (1 - fy), w01 = (1 - fx) * fy, w11 = fx * fy;
681
+ let r = 0, g = 0, b = 0, a = 0;
682
+ const left = x0 >= 0, right = x0 + 1 < width, top = y0 >= 0, bottom = y0 + 1 < height;
683
+ let i = 4 * (y0 * width + x0);
684
+ if (top && left) {
685
+ const wa = w00 * s8[i + 3];
686
+ r += wa * s8[i];
687
+ g += wa * s8[i + 1];
688
+ b += wa * s8[i + 2];
689
+ a += wa;
690
+ }
691
+ if (top && right) {
692
+ const wa = w10 * s8[i + 7];
693
+ r += wa * s8[i + 4];
694
+ g += wa * s8[i + 5];
695
+ b += wa * s8[i + 6];
696
+ a += wa;
697
+ }
698
+ i += 4 * width;
699
+ if (bottom && left) {
700
+ const wa = w01 * s8[i + 3];
701
+ r += wa * s8[i];
702
+ g += wa * s8[i + 1];
703
+ b += wa * s8[i + 2];
704
+ a += wa;
705
+ }
706
+ if (bottom && right) {
707
+ const wa = w11 * s8[i + 7];
708
+ r += wa * s8[i + 4];
709
+ g += wa * s8[i + 5];
710
+ b += wa * s8[i + 6];
711
+ a += wa;
712
+ }
713
+ if (a <= 0)
714
+ continue;
715
+ const o = 4 * (oy * outWidth + ox);
716
+ d8[o] = r / a;
717
+ d8[o + 1] = g / a;
718
+ d8[o + 2] = b / a;
719
+ d8[o + 3] = a;
720
+ }
721
+ }
722
+ return this.__apply__(out);
723
+ }
724
+ flip(direction) {
725
+ const u32 = this.__u32__;
726
+ const { width, height } = this;
727
+ if (direction === 'horizontal') {
728
+ for (let y = 0; y < height; y++)
729
+ u32.subarray(y * width, (y + 1) * width).reverse();
730
+ }
731
+ else if (direction === 'vertical') {
732
+ for (let y = 0; y < height >> 1; y++) {
733
+ const top = u32.slice(y * width, (y + 1) * width);
734
+ u32.copyWithin(y * width, (height - 1 - y) * width, (height - y) * width);
735
+ u32.set(top, (height - 1 - y) * width);
736
+ }
737
+ }
738
+ else
739
+ throw new RangeError('Invalid flip direction');
740
+ return this;
741
+ }
742
+ __bytes__() {
743
+ return new Uint8Array(this.bitmap.buffer, this.bitmap.byteOffset, this.bitmap.byteLength);
744
+ }
745
+ __apply__(image) {
746
+ this.__width__ = image.__width__;
747
+ this.__height__ = image.__height__;
748
+ this.__bitmap__ = image.__bitmap__;
749
+ this.__view__ = image.__view__;
750
+ this.__u32__ = image.__u32__;
751
+ return this;
752
+ }
753
+ static gradient(colors) {
754
+ const entries = Object.entries(colors).sort((a, b) => Number(a[0]) - Number(b[0]));
755
+ const positions = entries.map(e => parseFloat(e[0]));
756
+ const values = entries.map(e => e[1]);
757
+ if (positions.length === 0)
758
+ throw new RangeError('Invalid gradient point count');
759
+ else if (positions.length === 1) {
760
+ return () => values[0];
761
+ }
762
+ else if (positions.length === 2) {
763
+ const gradient = this.__gradient__(values[0], values[1]);
764
+ return position => {
765
+ if (position <= positions[0])
766
+ return values[0];
767
+ if (position >= positions[1])
768
+ return values[1];
769
+ return gradient((position - positions[0]) / (positions[1] - positions[0]));
770
+ };
771
+ }
772
+ const minDef = Math.min(...positions);
773
+ const maxDef = Math.max(...positions);
774
+ const gradients = [];
775
+ for (let i = 0; i < positions.length; i++) {
776
+ let minPos = positions[i - 1];
777
+ if (minPos === undefined)
778
+ continue;
779
+ let maxPos = positions[i];
780
+ let minVal = values[i - 1];
781
+ if (minVal === undefined)
782
+ minVal = values[i];
783
+ const maxVal = values[i];
784
+ const gradient = this.__gradient__(minVal, maxVal);
785
+ gradients.push({ min: minPos, max: maxPos, gradient });
786
+ }
787
+ return position => {
788
+ if (position <= minDef)
789
+ return gradients[0].gradient(0);
790
+ if (position >= maxDef)
791
+ return gradients[gradients.length - 1].gradient(1);
792
+ for (const gradient of gradients)
793
+ if (position >= gradient.min && position <= gradient.max)
794
+ return gradient.gradient((position - gradient.min) / (gradient.max - gradient.min));
795
+ throw new RangeError(`Invalid gradient position: ${position}`);
796
+ };
797
+ }
798
+ roundCorners(radius = Math.min(this.width, this.height) / 4) {
799
+ radius = Math.max(0, Math.min(radius, this.width / 2, this.height / 2));
800
+ const size = Math.ceil(radius);
801
+ for (let y = 0; y < size; y++) {
802
+ for (let x = 0; x < size; x++) {
803
+ const coverage = pixelCoverage(x, y, (sx, sy) => sx >= radius || sy >= radius || (sx - radius) ** 2 + (sy - radius) ** 2 <= radius * radius);
804
+ if (coverage >= 1)
805
+ continue;
806
+ const corners = new Set([[x, y], [this.width - 1 - x, y], [x, this.height - 1 - y], [this.width - 1 - x, this.height - 1 - y]].map(([px, py]) => py * this.width + px));
807
+ for (const pixel of corners)
808
+ this.bitmap[pixel * 4 + 3] *= coverage;
809
+ }
810
+ }
811
+ return this;
812
+ }
813
+ static __gradient__(startColor, endColor) {
814
+ const sr = startColor >>> 24;
815
+ const sg = startColor >> 16 & 0xff;
816
+ const sb = startColor >> 8 & 0xff;
817
+ const sa = startColor & 0xff;
818
+ const er = (endColor >>> 24) - sr;
819
+ const eg = (endColor >> 16 & 0xff) - sg;
820
+ const eb = (endColor >> 8 & 0xff) - sb;
821
+ const ea = (endColor & 0xff) - sa;
822
+ return position => {
823
+ const r = sr + position * er;
824
+ const g = sg + position * eg;
825
+ const b = sb + position * eb;
826
+ const a = sa + position * ea;
827
+ return (((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | (a & 0xff)) >>> 0;
828
+ };
829
+ }
830
+ fisheye(strength = 2) {
831
+ const r = new Image(this.width, this.height);
832
+ const w = this.width;
833
+ const h = this.height;
834
+ const tu32 = this.__u32__;
835
+ const ru32 = r.__u32__;
836
+ for (let y = 0; y < h; y++) {
837
+ for (let x = 0; x < w; x++) {
838
+ const xco = (x + 0.5) / w - 0.5;
839
+ const yco = (y + 0.5) / h - 0.5;
840
+ const dfc = Math.sqrt(xco ** 2 + yco ** 2);
841
+ if (dfc === 0) {
842
+ ru32[y * w + x] = tu32[y * w + x];
843
+ continue;
844
+ }
845
+ const dis = 2 * dfc ** strength;
846
+ const nx = Math.floor((dis * xco / dfc + 0.5) * w);
847
+ const ny = Math.floor((dis * yco / dfc + 0.5) * h);
848
+ if (nx < 0 || nx >= w || ny < 0 || ny >= h)
849
+ continue;
850
+ ru32[y * w + x] = tu32[ny * w + nx];
851
+ }
852
+ }
853
+ return this.__apply__(r);
854
+ }
855
+ async encode(compression = 1, metadata = {}) {
856
+ if (typeof compression === 'object' && compression !== null) {
857
+ metadata = compression;
858
+ compression = 1;
859
+ }
860
+ const level = Math.min(9, Math.max(0, ~~compression));
861
+ const { title, author, description, copyright, creationTime, software, disclaimer, warning, source, comment } = metadata || {};
862
+ return png.encodeRGBA(this.__bytes__(), this.width, this.height, level, {
863
+ Title: title,
864
+ Author: author,
865
+ Description: description,
866
+ Copyright: copyright,
867
+ 'Creation Time': new Date(creationTime === undefined ? Date.now() : creationTime).toUTCString(),
868
+ Software: software === undefined ? `github.com/matmen/ImageScript v${VERSION}` : software,
869
+ Disclaimer: disclaimer,
870
+ Warning: warning,
871
+ Source: source,
872
+ Comment: comment
873
+ });
874
+ }
875
+ encodePNG(compression, metadata) {
876
+ return this.encode(compression, metadata);
877
+ }
878
+ async encodeJPEG(quality = 90) {
879
+ return jpeg.encode(this.__bytes__(), this.width, this.height, quality);
880
+ }
881
+ static async decode(input, options = {}) {
882
+ const { applyOrientation = true } = options;
883
+ const data = view(input);
884
+ const dataView = new DataView(data.buffer, data.byteOffset, data.byteLength);
885
+ let decoded;
886
+ if (ImageType.isPNG(dataView))
887
+ decoded = await png.decode(data);
888
+ else if (ImageType.isJPEG(dataView))
889
+ decoded = jpeg.decode(data);
890
+ else if (ImageType.isTIFF(dataView))
891
+ decoded = await tiff.decode(data);
892
+ else
893
+ throw new Error('Unsupported image type');
894
+ const image = new Image(decoded.width, decoded.height);
895
+ image.bitmap.set(decoded.pixels);
896
+ return applyOrientation && decoded.orientation ? image.__orient__(decoded.orientation) : image;
897
+ }
898
+ __orient__(orientation) {
899
+ if (orientation <= 1 || orientation > 8)
900
+ return this;
901
+ const w = this.width, h = this.height;
902
+ const swap = orientation >= 5;
903
+ const out = new Image(swap ? h : w, swap ? w : h);
904
+ const src = this.__u32__, dst = out.__u32__;
905
+ const ow = out.width, oh = out.height;
906
+ for (let Y = 0; Y < oh; Y++) {
907
+ for (let X = 0; X < ow; X++) {
908
+ let x, y;
909
+ switch (orientation) {
910
+ case 2:
911
+ x = w - 1 - X;
912
+ y = Y;
913
+ break;
914
+ case 3:
915
+ x = w - 1 - X;
916
+ y = h - 1 - Y;
917
+ break;
918
+ case 4:
919
+ x = X;
920
+ y = h - 1 - Y;
921
+ break;
922
+ case 5:
923
+ x = Y;
924
+ y = X;
925
+ break;
926
+ case 6:
927
+ x = Y;
928
+ y = h - 1 - X;
929
+ break;
930
+ case 7:
931
+ x = w - 1 - Y;
932
+ y = h - 1 - X;
933
+ break;
934
+ default:
935
+ x = w - 1 - Y;
936
+ y = X;
937
+ break;
938
+ }
939
+ dst[Y * ow + X] = src[y * w + x];
940
+ }
941
+ }
942
+ return this.__apply__(out);
943
+ }
944
+ static get SVG_MODE_SCALE() {
945
+ return 1;
946
+ }
947
+ static get SVG_MODE_WIDTH() {
948
+ return 2;
949
+ }
950
+ static get SVG_MODE_HEIGHT() {
951
+ return 3;
952
+ }
953
+ static async renderSVG(svg, size = 1, mode = Image.SVG_MODE_SCALE) {
954
+ if (![1, 2, 3].includes(mode))
955
+ throw new Error('Invalid SVG scaling mode');
956
+ if (mode === Image.SVG_MODE_SCALE && size <= 0)
957
+ throw new RangeError('SVG scale must be > 0');
958
+ if (mode !== Image.SVG_MODE_SCALE && size < 1)
959
+ throw new RangeError('SVG size must be >= 1');
960
+ const { width, height, pixels } = svgRenderer.render(typeof svg === 'string' ? svg : view(svg), mode, size);
961
+ const image = new Image(width, height);
962
+ image.bitmap.set(pixels);
963
+ return image;
964
+ }
965
+ static async renderText(font, size, text, color = 0xffffffff, layout = new TextLayout(), style = {}) {
966
+ const { strokeWidth = 0, strokeColor = 0x000000ff } = style;
967
+ if (isNaN(strokeWidth) || strokeWidth < 0)
968
+ throw new RangeError('Invalid stroke width');
969
+ const rendered = textRenderer.render(view(font), size, text, layout);
970
+ const [r, g, b, a] = Image.colorToRGBA(color);
971
+ const pad = Math.ceil(strokeWidth);
972
+ const width = rendered.width + 2 * pad, height = rendered.height + 2 * pad;
973
+ const coverage = new Uint8Array(width * height);
974
+ for (let y = 0; y < rendered.height; y++)
975
+ coverage.set(rendered.coverage.subarray(y * rendered.width, (y + 1) * rendered.width), (y + pad) * width + pad);
976
+ const image = new Image(width, height);
977
+ const bitmap = image.bitmap;
978
+ if (!strokeWidth) {
979
+ for (let i = 0; i < coverage.length; i++) {
980
+ if (!coverage[i])
981
+ continue;
982
+ bitmap[i * 4] = r;
983
+ bitmap[i * 4 + 1] = g;
984
+ bitmap[i * 4 + 2] = b;
985
+ bitmap[i * 4 + 3] = coverage[i] * a / 255;
986
+ }
987
+ return image;
988
+ }
989
+ const stroke = outline(coverage, width, height, strokeWidth);
990
+ const [sr, sg, sb, sa] = Image.colorToRGBA(strokeColor);
991
+ for (let i = 0; i < coverage.length; i++) {
992
+ const af = coverage[i] / 255 * a / 255, as = stroke[i] * sa / 255;
993
+ const ao = af + as * (1 - af);
994
+ if (ao <= 0)
995
+ continue;
996
+ bitmap[i * 4] = (r * af + sr * as * (1 - af)) / ao;
997
+ bitmap[i * 4 + 1] = (g * af + sg * as * (1 - af)) / ao;
998
+ bitmap[i * 4 + 2] = (b * af + sb * as * (1 - af)) / ao;
999
+ bitmap[i * 4 + 3] = ao * 255;
1000
+ }
1001
+ return image;
1002
+ }
1003
+ }
1004
+ const SUBSAMPLES = 4;
1005
+ function pixelCoverage(px, py, inside) {
1006
+ let count = 0;
1007
+ for (let j = 0; j < SUBSAMPLES; j++)
1008
+ for (let i = 0; i < SUBSAMPLES; i++)
1009
+ if (inside(px + (i + 0.5) / SUBSAMPLES, py + (j + 0.5) / SUBSAMPLES))
1010
+ count++;
1011
+ return count / (SUBSAMPLES * SUBSAMPLES);
1012
+ }
1013
+ function circleCoverage(px, py, cx, cy, radius) {
1014
+ const d = Math.hypot(px + 0.5 - cx, py + 0.5 - cy);
1015
+ if (d <= radius - Math.SQRT1_2)
1016
+ return 1;
1017
+ if (d >= radius + Math.SQRT1_2)
1018
+ return 0;
1019
+ return pixelCoverage(px, py, (x, y) => (x - cx) ** 2 + (y - cy) ** 2 <= radius * radius);
1020
+ }
1021
+ function channel(value) {
1022
+ return value > 255 ? 255 : value > 0 ? Math.round(value) : 0;
1023
+ }
1024
+ function wrapHue(h) {
1025
+ const t = h - Math.trunc(h) + 1;
1026
+ return t < 1 ? t : t < 2 ? t - 1 : 0;
1027
+ }
1028
+ function hue2rgb(p, q, t) {
1029
+ if (t < 0)
1030
+ t += 1;
1031
+ if (t > 1)
1032
+ t -= 1;
1033
+ if (t < 1 / 6)
1034
+ return p + (q - p) * 6 * t;
1035
+ if (t < 1 / 2)
1036
+ return q;
1037
+ if (t < 2 / 3)
1038
+ return p + (q - p) * (2 / 3 - t) * 6;
1039
+ return p;
1040
+ }
1041
+ const hardLight = (b, s) => s <= 0.5 ? b * 2 * s : 1 - (1 - b) * (1 - (2 * s - 1));
1042
+ const BLEND_MODES = {
1043
+ 'normal': (_b, s) => s,
1044
+ 'multiply': (b, s) => b * s,
1045
+ 'screen': (b, s) => b + s - b * s,
1046
+ 'overlay': (b, s) => hardLight(s, b),
1047
+ 'darken': (b, s) => Math.min(b, s),
1048
+ 'lighten': (b, s) => Math.max(b, s),
1049
+ 'color-dodge': (b, s) => b === 0 ? 0 : s === 1 ? 1 : Math.min(1, b / (1 - s)),
1050
+ 'color-burn': (b, s) => b === 1 ? 1 : s === 0 ? 0 : 1 - Math.min(1, (1 - b) / s),
1051
+ 'hard-light': hardLight,
1052
+ 'soft-light': (b, s) => {
1053
+ if (s <= 0.5)
1054
+ return b - (1 - 2 * s) * b * (1 - b);
1055
+ const d = b <= 0.25 ? ((16 * b - 12) * b + 4) * b : Math.sqrt(b);
1056
+ return b + (2 * s - 1) * (d - b);
1057
+ },
1058
+ 'difference': (b, s) => Math.abs(b - s),
1059
+ 'exclusion': (b, s) => b + s - 2 * b * s,
1060
+ 'add': (b, s) => Math.min(1, b + s)
1061
+ };
1062
+ function outline(coverage, width, height, radius) {
1063
+ const count = width * height;
1064
+ const distance = new Float64Array(count);
1065
+ for (let i = 0; i < count; i++)
1066
+ distance[i] = coverage[i] >= 128 ? 0 : Infinity;
1067
+ const size = Math.max(width, height);
1068
+ const line = new Float64Array(size), result = new Float64Array(size);
1069
+ const parabolas = new Int32Array(size), bounds = new Float64Array(size + 1);
1070
+ for (let x = 0; x < width; x++) {
1071
+ for (let y = 0; y < height; y++)
1072
+ line[y] = distance[y * width + x];
1073
+ distanceTransform(line, height, result, parabolas, bounds);
1074
+ for (let y = 0; y < height; y++)
1075
+ distance[y * width + x] = result[y];
1076
+ }
1077
+ for (let y = 0; y < height; y++) {
1078
+ const row = y * width;
1079
+ for (let x = 0; x < width; x++)
1080
+ line[x] = distance[row + x];
1081
+ distanceTransform(line, width, result, parabolas, bounds);
1082
+ for (let x = 0; x < width; x++)
1083
+ distance[row + x] = result[x];
1084
+ }
1085
+ const out = new Float32Array(count);
1086
+ for (let i = 0; i < count; i++) {
1087
+ const d = Math.sqrt(distance[i]);
1088
+ out[i] = Math.max(coverage[i] / 255, Math.min(1, Math.max(0, radius + 0.5 - d)));
1089
+ }
1090
+ return out;
1091
+ }
1092
+ function distanceTransform(f, n, d, v, z) {
1093
+ let k = -1;
1094
+ for (let q = 0; q < n; q++) {
1095
+ if (f[q] === Infinity)
1096
+ continue;
1097
+ let s = 0;
1098
+ while (k >= 0) {
1099
+ s = (f[q] + q * q - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]);
1100
+ if (s > z[k])
1101
+ break;
1102
+ k--;
1103
+ }
1104
+ k++;
1105
+ v[k] = q;
1106
+ z[k] = k === 0 ? -Infinity : s;
1107
+ z[k + 1] = Infinity;
1108
+ }
1109
+ if (k < 0) {
1110
+ d.fill(Infinity, 0, n);
1111
+ return;
1112
+ }
1113
+ for (let q = 0, j = 0; q < n; q++) {
1114
+ while (z[j + 1] < q)
1115
+ j++;
1116
+ d[q] = (q - v[j]) ** 2 + f[v[j]];
1117
+ }
1118
+ }