imagescript 3.2.0 → 3.3.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 (59) hide show
  1. package/README.md +8 -3
  2. package/THIRD_PARTY_NOTICES.md +131 -0
  3. package/dist/animation.d.ts +7 -0
  4. package/dist/animation.js +22 -12
  5. package/dist/codecs/crc.js +5 -0
  6. package/dist/codecs/gif.js +30 -52
  7. package/dist/codecs/jpeg.js +9 -27
  8. package/dist/codecs/png.js +22 -10
  9. package/dist/codecs/tga.js +3 -1
  10. package/dist/codecs/tiff.js +7 -2
  11. package/dist/codecs/vp8.js +3 -1
  12. package/dist/codecs/vp8l.js +126 -58
  13. package/dist/codecs/webp.js +11 -9
  14. package/dist/codecs/zlib.js +81 -69
  15. package/dist/frame.d.ts +1 -0
  16. package/dist/frame.js +4 -2
  17. package/dist/gif.d.ts +1 -0
  18. package/dist/gif.js +2 -2
  19. package/dist/image.d.ts +42 -7
  20. package/dist/image.js +224 -113
  21. package/dist/index.js +4 -4
  22. package/dist/render/bidi.js +350 -0
  23. package/dist/render/buffer.js +362 -0
  24. package/dist/render/color.js +164 -0
  25. package/dist/render/colr.js +523 -0
  26. package/dist/render/font.js +173 -241
  27. package/dist/render/hinting.js +12 -4
  28. package/dist/render/layout.js +1553 -0
  29. package/dist/render/linebreak.js +210 -0
  30. package/dist/render/segment.js +106 -0
  31. package/dist/render/shape.js +984 -0
  32. package/dist/render/shapers/arabic.js +284 -0
  33. package/dist/render/shapers/hangul.js +142 -0
  34. package/dist/render/shapers/hebrew.js +49 -0
  35. package/dist/render/shapers/index.js +52 -0
  36. package/dist/render/shapers/indic.js +579 -0
  37. package/dist/render/shapers/khmer.js +104 -0
  38. package/dist/render/shapers/machine.js +129 -0
  39. package/dist/render/shapers/myanmar.js +121 -0
  40. package/dist/render/shapers/syllabic.js +100 -0
  41. package/dist/render/shapers/thai.js +125 -0
  42. package/dist/render/shapers/use.js +235 -0
  43. package/dist/render/svg.js +101 -22
  44. package/dist/render/text.js +373 -103
  45. package/dist/render/unicode-data.js +63 -0
  46. package/dist/render/unicode.js +58 -0
  47. package/dist/text-layout.d.ts +4 -1
  48. package/dist/text-layout.js +2 -2
  49. package/dist/util/alpha.js +43 -0
  50. package/dist/util/blur.js +80 -5
  51. package/dist/util/bytes.js +31 -0
  52. package/dist/util/limits.js +34 -0
  53. package/dist/util/options.js +7 -0
  54. package/dist/util/resample.js +359 -79
  55. package/dist/util/rotate.js +43 -0
  56. package/dist/version.js +1 -1
  57. package/licenses/HarfBuzz.txt +42 -0
  58. package/licenses/Unicode-3.0.txt +39 -0
  59. package/package.json +1 -1
package/dist/image.js CHANGED
@@ -12,8 +12,10 @@ import { TextLayout } from "./text-layout.js";
12
12
  import { resolveMetadata, pngText, exifData } from "./metadata.js";
13
13
  import { encode as utf8 } from "./util/utf8.js";
14
14
  import { resample, BOX, TRIANGLE, CUBIC } from "./util/resample.js";
15
- import { gaussianBlur, premultiply, unpremultiply } from "./util/blur.js";
16
- import { isOptions } from "./util/options.js";
15
+ import { rotateBilinear } from "./util/rotate.js";
16
+ import { gaussianBlur, boxBlurRGBA, premultiply, unpremultiply } from "./util/blur.js";
17
+ import { isOptions, finite, color as checkColor } from "./util/options.js";
18
+ import { MAX_PIXELS, decoding } from "./util/limits.js";
17
19
  export class Image {
18
20
  __width__;
19
21
  __height__;
@@ -70,13 +72,20 @@ export class Image {
70
72
  yield [x, y, view.getUint32(offset, false)];
71
73
  }
72
74
  static rgbaToColor(r, g, b, a) {
75
+ r = finite(r, 'red');
76
+ g = finite(g, 'green');
77
+ b = finite(b, 'blue');
78
+ a = finite(a, 'alpha');
73
79
  return (channel(r) << 24 | channel(g) << 16 | channel(b) << 8 | channel(a)) >>> 0;
74
80
  }
75
81
  static rgbToColor(r, g, b) {
76
82
  return Image.rgbaToColor(r, g, b, 0xff);
77
83
  }
78
84
  static hslaToColor(h, s, l, a) {
79
- h = wrapHue(h);
85
+ h = wrapHue(finite(h, 'hue'));
86
+ s = finite(s, 'saturation');
87
+ l = finite(l, 'lightness');
88
+ a = finite(a, 'alpha');
80
89
  s = Math.min(1, Math.max(0, s));
81
90
  l = Math.min(1, Math.max(0, l));
82
91
  a = Math.min(1, Math.max(0, a));
@@ -97,9 +106,10 @@ export class Image {
97
106
  return Image.hslaToColor(h, s, l, 1);
98
107
  }
99
108
  static rgbaToHSLA(r, g, b, a) {
100
- r /= 255;
101
- g /= 255;
102
- b /= 255;
109
+ r = finite(r, 'red') / 255;
110
+ g = finite(g, 'green') / 255;
111
+ b = finite(b, 'blue') / 255;
112
+ a = finite(a, 'alpha');
103
113
  const max = Math.max(r, g, b), min = Math.min(r, g, b);
104
114
  let h = 0, s;
105
115
  const l = (max + min) / 2;
@@ -125,6 +135,7 @@ export class Image {
125
135
  return [h, s, l, a / 255];
126
136
  }
127
137
  static colorToRGBA(color) {
138
+ color = checkColor(color);
128
139
  return [(color >> 24) & 0xff, (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff];
129
140
  }
130
141
  static colorToRGB(color) {
@@ -132,23 +143,23 @@ export class Image {
132
143
  return [r, g, b];
133
144
  }
134
145
  getPixelAt(x, y) {
135
- x = ~~x;
136
- y = ~~y;
146
+ x = ~~finite(x, 'x');
147
+ y = ~~finite(y, 'y');
137
148
  this.__check_boundaries__(x, y);
138
149
  return this.__view__.getUint32((y * this.width + x) * 4, false);
139
150
  }
140
151
  getRGBAAt(x, y) {
141
- x = ~~x;
142
- y = ~~y;
152
+ x = ~~finite(x, 'x');
153
+ y = ~~finite(y, 'y');
143
154
  this.__check_boundaries__(x, y);
144
155
  const idx = (y * this.width + x) * 4;
145
156
  return this.bitmap.subarray(idx, idx + 4);
146
157
  }
147
158
  setPixelAt(x, y, pixelColor) {
148
- x = ~~x;
149
- y = ~~y;
159
+ x = ~~finite(x, 'x');
160
+ y = ~~finite(y, 'y');
150
161
  this.__check_boundaries__(x, y);
151
- this.__set_pixel__(x, y, pixelColor);
162
+ this.__set_pixel__(x, y, checkColor(pixelColor));
152
163
  return this;
153
164
  }
154
165
  __set_pixel__(x, y, pixelColor) {
@@ -180,6 +191,7 @@ export class Image {
180
191
  view.setUint32(offset, color(x, y), false);
181
192
  }
182
193
  else {
194
+ color = checkColor(color);
183
195
  if (!this.bitmap.length)
184
196
  return this;
185
197
  view.setUint32(0, color, false);
@@ -189,8 +201,10 @@ export class Image {
189
201
  return this;
190
202
  }
191
203
  clone() {
192
- const image = new Image(this.width, this.height);
193
- image.bitmap.set(this.bitmap);
204
+ const image = new Image(1, 1);
205
+ image.__width__ = this.width;
206
+ image.__height__ = this.height;
207
+ image.__set_bitmap__(new Uint8ClampedArray(this.bitmap));
194
208
  return image;
195
209
  }
196
210
  static get RESIZE_NEAREST_NEIGHBOR() {
@@ -209,42 +223,43 @@ export class Image {
209
223
  return -1;
210
224
  }
211
225
  scale(factor, modeOrOptions) {
212
- const image = this.__scale__(factor, resizeMode(modeOrOptions));
226
+ const { mode, fast } = resizeOptions(modeOrOptions);
227
+ const image = this.__scale__(factor, mode, fast);
213
228
  return this.__apply__(image);
214
229
  }
215
- __scale__(factor, mode = Image.RESIZE_BILINEAR) {
230
+ __scale__(factor, mode = Image.RESIZE_BILINEAR, fast = true) {
216
231
  if (factor === 1)
217
232
  return this;
218
- return this.__resize__(this.width * factor, this.height * factor, mode);
233
+ return this.__resize__(this.width * factor, this.height * factor, mode, fast);
219
234
  }
220
235
  resize(width, height, modeOrOptions) {
221
- const mode = resizeMode(modeOrOptions);
222
- const image = this.__resize__(width, height, mode);
236
+ const { mode, fast } = resizeOptions(modeOrOptions);
237
+ const image = this.__resize__(width, height, mode, fast);
223
238
  return this.__apply__(image);
224
239
  }
225
240
  contain(width, height, modeOrOptions) {
226
- const mode = resizeMode(modeOrOptions);
227
- width = Math.floor(width);
228
- height = Math.floor(height);
241
+ const options = resizeOptions(modeOrOptions);
242
+ width = Math.floor(finite(width, 'width'));
243
+ height = Math.floor(finite(height, 'height'));
229
244
  const scale = Math.min(width / this.width, height / this.height);
230
- 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);
245
+ 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))), options);
231
246
  }
232
247
  fit(width, height, modeOrOptions) {
233
- const mode = resizeMode(modeOrOptions);
248
+ const options = resizeOptions(modeOrOptions);
234
249
  const result = new Image(width, height);
235
- this.contain(width, height, mode);
250
+ this.contain(width, height, options);
236
251
  result.composite(this, (width - this.width) / 2, (height - this.height) / 2);
237
252
  return this.__apply__(result);
238
253
  }
239
254
  cover(width, height, modeOrOptions) {
240
- const mode = resizeMode(modeOrOptions);
255
+ const options = resizeOptions(modeOrOptions);
241
256
  width = Math.floor(width);
242
257
  height = Math.floor(height);
243
258
  const scale = Math.max(width / this.width, height / this.height);
244
- this.resize(Math.max(width, Math.round(this.width * scale)), Math.max(height, Math.round(this.height * scale)), mode);
259
+ this.resize(Math.max(width, Math.round(this.width * scale)), Math.max(height, Math.round(this.height * scale)), options);
245
260
  return this.crop((this.width - width) / 2, (this.height - height) / 2, width, height);
246
261
  }
247
- __resize__(width, height, mode = Image.RESIZE_BILINEAR) {
262
+ __resize__(width, height, mode = Image.RESIZE_BILINEAR, fast = true) {
248
263
  if (width === Image.RESIZE_AUTO && height === Image.RESIZE_AUTO)
249
264
  throw new Error('RESIZE_AUTO can only be used for either width or height, not for both');
250
265
  else if (width === Image.RESIZE_AUTO)
@@ -263,7 +278,7 @@ export class Image {
263
278
  if (!filter)
264
279
  throw new Error('Invalid resize mode');
265
280
  const image = new Image(width, height);
266
- resample(this.bitmap, this.width, this.height, width, height, filter, image.bitmap);
281
+ resample(this.bitmap, this.width, this.height, width, height, filter, image.bitmap, fast);
267
282
  return image;
268
283
  }
269
284
  __resize_nearest_neighbor__(width, height) {
@@ -278,10 +293,10 @@ export class Image {
278
293
  return image;
279
294
  }
280
295
  crop(x, y, width, height) {
281
- x = ~~x;
282
- y = ~~y;
283
- width = ~~width;
284
- height = ~~height;
296
+ x = ~~finite(x, 'x');
297
+ y = ~~finite(y, 'y');
298
+ width = ~~finite(width, 'width');
299
+ height = ~~finite(height, 'height');
285
300
  const left = Math.max(0, x), top = Math.max(0, y);
286
301
  const right = Math.min(this.width, x + width), bottom = Math.min(this.height, y + height);
287
302
  if (right <= left || bottom <= top)
@@ -297,6 +312,8 @@ export class Image {
297
312
  throw new RangeError('Invalid tolerance, it has to be between 0 and 1');
298
313
  const bitmap = this.bitmap;
299
314
  const { width, height } = this;
315
+ if (color !== undefined)
316
+ color = checkColor(color);
300
317
  const c = color === undefined ? [bitmap[0], bitmap[1], bitmap[2], bitmap[3]] : [color >>> 24, color >> 16 & 255, color >> 8 & 255, color & 255];
301
318
  const limit = tolerance * 255;
302
319
  const matches = (x, y) => {
@@ -344,12 +361,12 @@ export class Image {
344
361
  return image;
345
362
  }
346
363
  drawBox(x, y, width, height, color) {
347
- x = ~~x;
348
- y = ~~y;
349
- width = ~~width;
350
- height = ~~height;
364
+ x = ~~finite(x, 'x');
365
+ y = ~~finite(y, 'y');
366
+ width = ~~finite(width, 'width');
367
+ height = ~~finite(height, 'height');
351
368
  if (typeof color !== 'function')
352
- return this.__fast_box__(x, y, width, height, color);
369
+ return this.__fast_box__(x, y, width, height, checkColor(color));
353
370
  for (let tY = Math.max(0, -y); tY < height && tY + y < this.height; tY++)
354
371
  for (let tX = Math.max(0, -x); tX < width && tX + x < this.width; tX++)
355
372
  this.__set_pixel__(tX + x, tY + y, color(tX, tY));
@@ -369,8 +386,11 @@ export class Image {
369
386
  return this;
370
387
  }
371
388
  drawCircle(x, y, radius, color) {
372
- x = ~~x;
373
- y = ~~y;
389
+ x = ~~finite(x, 'x');
390
+ y = ~~finite(y, 'y');
391
+ radius = finite(radius, 'radius');
392
+ if (typeof color !== 'function')
393
+ color = checkColor(color);
374
394
  const cx = x + 0.5, cy = y + 0.5;
375
395
  for (let py = Math.max(0, Math.floor(cy - radius)); py < Math.min(this.height, Math.ceil(cy + radius)); py++) {
376
396
  for (let px = Math.max(0, Math.floor(cx - radius)); px < Math.min(this.width, Math.ceil(cx + radius)); px++) {
@@ -402,6 +422,7 @@ export class Image {
402
422
  cropCircle(maxOrOptions = false, featheringArgument = 0) {
403
423
  const { max = false, feathering = 0 } = typeof maxOrOptions === 'object' && maxOrOptions !== null
404
424
  ? maxOrOptions : { max: maxOrOptions, feathering: featheringArgument };
425
+ finite(feathering, 'feathering');
405
426
  const radius = (max ? Math.max : Math.min)(this.width, this.height) / 2;
406
427
  const cx = this.width / 2, cy = this.height / 2;
407
428
  const band = Math.min(1, Math.max(0, feathering)) * radius;
@@ -422,28 +443,32 @@ export class Image {
422
443
  }
423
444
  opacity(opacity, absoluteOrOptions = false) {
424
445
  const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
425
- if (isNaN(opacity) || opacity < 0)
446
+ opacity = finite(opacity, 'opacity value');
447
+ if (opacity < 0)
426
448
  throw new RangeError('Invalid opacity value');
427
449
  this.__set_channel_value__(opacity, absolute, 3);
428
450
  return this;
429
451
  }
430
452
  red(factor, absoluteOrOptions = false) {
431
453
  const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
432
- if (isNaN(factor) || factor < 0)
454
+ factor = finite(factor, 'red factor');
455
+ if (factor < 0)
433
456
  throw new RangeError('Invalid red factor');
434
457
  this.__set_channel_value__(factor, absolute, 0);
435
458
  return this;
436
459
  }
437
460
  green(factor, absoluteOrOptions = false) {
438
461
  const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
439
- if (isNaN(factor) || factor < 0)
462
+ factor = finite(factor, 'green factor');
463
+ if (factor < 0)
440
464
  throw new RangeError('Invalid green factor');
441
465
  this.__set_channel_value__(factor, absolute, 1);
442
466
  return this;
443
467
  }
444
468
  blue(factor, absoluteOrOptions = false) {
445
469
  const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
446
- if (isNaN(factor) || factor < 0)
470
+ factor = finite(factor, 'blue factor');
471
+ if (factor < 0)
447
472
  throw new RangeError('Invalid blue factor');
448
473
  this.__set_channel_value__(factor, absolute, 2);
449
474
  return this;
@@ -454,13 +479,15 @@ export class Image {
454
479
  }
455
480
  lightness(value, absoluteOrOptions = false) {
456
481
  const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
457
- if (isNaN(value) || value < 0)
482
+ value = finite(value, 'lightness value');
483
+ if (value < 0)
458
484
  throw new RangeError('Invalid lightness value');
459
485
  return this.__map_hsla__(c => { c.l = value * (absolute ? 1 : c.l); });
460
486
  }
461
487
  saturation(value, absoluteOrOptions = false) {
462
488
  const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
463
- if (isNaN(value) || value < 0)
489
+ value = finite(value, 'saturation value');
490
+ if (value < 0)
464
491
  throw new RangeError('Invalid saturation value');
465
492
  return this.__map_hsla__(c => { c.s = value * (absolute ? 1 : c.s); });
466
493
  }
@@ -468,8 +495,8 @@ export class Image {
468
495
  const options = isOptions(xOrOptions) ? xOrOptions : optionsArgument;
469
496
  const { opacity = 1, mode = 'normal' } = options;
470
497
  let x = isOptions(xOrOptions) ? xOrOptions.x ?? 0 : xOrOptions, y = isOptions(xOrOptions) ? xOrOptions.y ?? 0 : yArgument;
471
- x |= 0;
472
- y |= 0;
498
+ x = finite(x, 'x') | 0;
499
+ y = finite(y, 'y') | 0;
473
500
  if (isNaN(opacity) || opacity < 0 || opacity > 1)
474
501
  throw new RangeError('Invalid composite opacity');
475
502
  const blend = BLEND_MODES[mode];
@@ -555,6 +582,7 @@ export class Image {
555
582
  return this;
556
583
  }
557
584
  hueShift(degrees) {
585
+ degrees = finite(degrees, 'hue shift');
558
586
  return this.__map_hsla__(c => { c.h += degrees / 360; });
559
587
  }
560
588
  __map_hsla__(map) {
@@ -610,8 +638,10 @@ export class Image {
610
638
  return this;
611
639
  }
612
640
  blur(radius) {
613
- if (isNaN(radius) || radius < 0)
641
+ if (!Number.isFinite(radius) || radius < 0)
614
642
  throw new RangeError('Invalid blur radius');
643
+ if (boxBlurRGBA(this.bitmap, this.width, this.height, radius))
644
+ return this;
615
645
  const pixels = premultiply(this.bitmap);
616
646
  gaussianBlur(pixels, this.width, this.height, radius);
617
647
  unpremultiply(pixels, this.bitmap);
@@ -619,9 +649,9 @@ export class Image {
619
649
  }
620
650
  sharpen(amountOrOptions = 1, radiusArgument = 1) {
621
651
  const { amount = 1, radius = 1 } = isOptions(amountOrOptions) ? amountOrOptions : { amount: amountOrOptions, radius: radiusArgument };
622
- if (isNaN(amount) || amount < 0)
652
+ if (!Number.isFinite(amount) || amount < 0)
623
653
  throw new RangeError('Invalid sharpen amount');
624
- if (isNaN(radius) || radius <= 0)
654
+ if (!Number.isFinite(radius) || radius <= 0)
625
655
  throw new RangeError('Invalid sharpen radius');
626
656
  const blurred = premultiply(this.bitmap);
627
657
  gaussianBlur(blurred, this.width, this.height, radius);
@@ -638,17 +668,20 @@ export class Image {
638
668
  return this;
639
669
  }
640
670
  brightness(factor) {
641
- if (isNaN(factor) || factor < 0)
671
+ factor = finite(factor, 'brightness factor');
672
+ if (factor < 0)
642
673
  throw new RangeError('Invalid brightness factor');
643
674
  return this.__map_channels__(v => v * factor);
644
675
  }
645
676
  contrast(factor) {
646
- if (isNaN(factor) || factor < 0)
677
+ factor = finite(factor, 'contrast factor');
678
+ if (factor < 0)
647
679
  throw new RangeError('Invalid contrast factor');
648
680
  return this.__map_channels__(v => (v - 127.5) * factor + 127.5);
649
681
  }
650
682
  gamma(gamma) {
651
- if (isNaN(gamma) || gamma <= 0)
683
+ gamma = finite(gamma, 'gamma');
684
+ if (gamma <= 0)
652
685
  throw new RangeError('Invalid gamma');
653
686
  return this.__map_channels__(v => 255 * (v / 255) ** (1 / gamma));
654
687
  }
@@ -673,6 +706,8 @@ export class Image {
673
706
  if (!(transparencyThreshold >= 0 && opacityThreshold <= 1 && transparencyThreshold < opacityThreshold))
674
707
  throw new RangeError('Invalid thresholds, they have to satisfy 0 <= transparencyThreshold < opacityThreshold <= 1');
675
708
  const bitmap = this.bitmap;
709
+ if (color !== undefined)
710
+ color = checkColor(color);
676
711
  const c = color === undefined ? [bitmap[0], bitmap[1], bitmap[2]] : [color >>> 24, color >> 16 & 255, color >> 8 & 255];
677
712
  for (let i = 0; i < bitmap.length; i += 4) {
678
713
  let distance = 0;
@@ -726,8 +761,8 @@ export class Image {
726
761
  return color ? Image.rgbToColor(...color) : Image.rgbaToColor(0, 0, 0, 0xff);
727
762
  }
728
763
  rotate(angle, resizeOrOptions = true) {
729
- const { resize = true } = typeof resizeOrOptions === 'object' && resizeOrOptions !== null ? resizeOrOptions : { resize: resizeOrOptions };
730
- angle = ((angle % 360) + 360) % 360;
764
+ const { resize = true, fast = true } = typeof resizeOrOptions === 'object' && resizeOrOptions !== null ? resizeOrOptions : { resize: resizeOrOptions };
765
+ angle = ((finite(angle, 'angle') % 360) + 360) % 360;
731
766
  if (angle === 0)
732
767
  return this;
733
768
  const { width, height } = this;
@@ -752,6 +787,8 @@ export class Image {
752
787
  const outWidth = resize ? Math.round(Math.abs(width * cos) + Math.abs(height * sin)) : width;
753
788
  const outHeight = resize ? Math.round(Math.abs(width * sin) + Math.abs(height * cos)) : height;
754
789
  const out = new Image(outWidth, outHeight);
790
+ if (fast && rotateBilinear(this.bitmap, width, height, out.bitmap, outWidth, outHeight, sin, cos))
791
+ return this.__apply__(out);
755
792
  const s8 = this.bitmap, d8 = out.bitmap;
756
793
  for (let oy = 0; oy < outHeight; oy++) {
757
794
  for (let ox = 0; ox < outWidth; ox++) {
@@ -837,8 +874,8 @@ export class Image {
837
874
  }
838
875
  static gradient(colors) {
839
876
  const entries = Object.entries(colors).sort((a, b) => Number(a[0]) - Number(b[0]));
840
- const positions = entries.map(e => parseFloat(e[0]));
841
- const values = entries.map(e => e[1]);
877
+ const positions = entries.map(e => finite(parseFloat(e[0]), 'gradient position'));
878
+ const values = entries.map(e => checkColor(e[1]));
842
879
  if (positions.length === 0)
843
880
  throw new RangeError('Invalid gradient point count');
844
881
  else if (positions.length === 1) {
@@ -858,10 +895,10 @@ export class Image {
858
895
  const maxDef = Math.max(...positions);
859
896
  const gradients = [];
860
897
  for (let i = 0; i < positions.length; i++) {
861
- let minPos = positions[i - 1];
898
+ const minPos = positions[i - 1];
862
899
  if (minPos === undefined)
863
900
  continue;
864
- let maxPos = positions[i];
901
+ const maxPos = positions[i];
865
902
  let minVal = values[i - 1];
866
903
  if (minVal === undefined)
867
904
  minVal = values[i];
@@ -882,6 +919,7 @@ export class Image {
882
919
  }
883
920
  roundCorners(radiusOrOptions) {
884
921
  let radius = (isOptions(radiusOrOptions) ? radiusOrOptions.radius : radiusOrOptions) ?? Math.min(this.width, this.height) / 4;
922
+ radius = finite(radius, 'radius');
885
923
  radius = Math.max(0, Math.min(radius, this.width / 2, this.height / 2));
886
924
  const size = Math.ceil(radius);
887
925
  for (let y = 0; y < size; y++) {
@@ -914,7 +952,7 @@ export class Image {
914
952
  };
915
953
  }
916
954
  fisheye(strengthOrOptions) {
917
- const strength = (isOptions(strengthOrOptions) ? strengthOrOptions.strength : strengthOrOptions) ?? 2;
955
+ const strength = finite((isOptions(strengthOrOptions) ? strengthOrOptions.strength : strengthOrOptions) ?? 2, 'strength');
918
956
  const r = new Image(this.width, this.height);
919
957
  const w = this.width;
920
958
  const h = this.height;
@@ -947,7 +985,7 @@ export class Image {
947
985
  ({ compression = 1, metadata = {} } = compressionOrOptions);
948
986
  else
949
987
  metadata = compressionOrOptions;
950
- const level = Math.min(9, Math.max(0, ~~compression));
988
+ const level = Math.min(9, Math.max(0, ~~finite(compression, 'compression level')));
951
989
  return png.encodeRGBA(this.__bytes__(), this.width, this.height, level, pngText(resolveMetadata(metadata)));
952
990
  }
953
991
  encodePNG(compressionOrOptions, metadata) {
@@ -956,7 +994,7 @@ export class Image {
956
994
  async encodeJPEG(qualityOrOptions = 90) {
957
995
  const { quality = 90, metadata = {} } = isOptions(qualityOrOptions) ? qualityOrOptions : { quality: qualityOrOptions };
958
996
  const m = resolveMetadata(metadata);
959
- return jpeg.encode(this.__bytes__(), this.width, this.height, quality, { exif: exifData(m), comment: m.comment ? utf8(m.comment) : undefined });
997
+ return jpeg.encode(this.__bytes__(), this.width, this.height, finite(quality, 'quality'), { exif: exifData(m), comment: m.comment ? utf8(m.comment) : undefined });
960
998
  }
961
999
  async encodeWebP({ metadata = {} } = {}) {
962
1000
  return webp.encode(this.__bytes__(), this.width, this.height, exifData(resolveMetadata(metadata)));
@@ -966,27 +1004,27 @@ export class Image {
966
1004
  return tga.encode(this.__bytes__(), this.width, this.height, { author, comment, time, software });
967
1005
  }
968
1006
  static async decode(input, options = {}) {
969
- const { applyOrientation = true } = options;
1007
+ const { applyOrientation = true, maxPixels = MAX_PIXELS } = options;
970
1008
  const data = view(input);
971
1009
  const dataView = new DataView(data.buffer, data.byteOffset, data.byteLength);
972
- let decoded;
973
- if (ImageType.isPNG(dataView))
974
- decoded = await png.decode(data);
975
- else if (ImageType.isJPEG(dataView))
976
- decoded = jpeg.decode(data);
977
- else if (ImageType.isTIFF(dataView))
978
- decoded = await tiff.decode(data);
979
- else if (ImageType.isWebP(dataView)) {
980
- if (webp.isAnimated(data))
981
- throw new Error('Animated WebPs have to be decoded with Animation.decode()');
982
- decoded = webp.decode(data);
983
- }
984
- else if (ImageType.isTGA(dataView))
985
- decoded = tga.decode(data);
986
- else
1010
+ const decoded = await decoding(() => {
1011
+ if (ImageType.isPNG(dataView))
1012
+ return png.decode(data, maxPixels);
1013
+ if (ImageType.isJPEG(dataView))
1014
+ return jpeg.decode(data, { maxPixels });
1015
+ if (ImageType.isTIFF(dataView))
1016
+ return tiff.decode(data, maxPixels);
1017
+ if (ImageType.isWebP(dataView)) {
1018
+ if (webp.isAnimated(data))
1019
+ throw new Error('Animated WebPs have to be decoded with Animation.decode()');
1020
+ return webp.decode(data, maxPixels);
1021
+ }
1022
+ if (ImageType.isTGA(dataView))
1023
+ return tga.decode(data, maxPixels);
987
1024
  throw new Error('Unsupported image type');
1025
+ });
988
1026
  const image = new Image(decoded.width, decoded.height);
989
- image.bitmap.set(decoded.pixels);
1027
+ image.bitmap = decoded.pixels;
990
1028
  return applyOrientation && decoded.orientation ? image.__orient__(decoded.orientation) : image;
991
1029
  }
992
1030
  __orient__(orientation) {
@@ -1060,39 +1098,67 @@ export class Image {
1060
1098
  static async renderText(font, size, text, colorOrOptions = 0xffffffff, layoutArgument, styleArgument = {}) {
1061
1099
  const options = isOptions(colorOrOptions) ? colorOrOptions : { ...styleArgument, color: colorOrOptions, layout: layoutArgument };
1062
1100
  const { color = 0xffffffff, layout = new TextLayout(), strokeWidth = 0, strokeColor = 0x000000ff, hinting = true } = options;
1063
- if (isNaN(strokeWidth) || strokeWidth < 0)
1101
+ size = finite(size, 'font size');
1102
+ if (finite(strokeWidth, 'stroke width') < 0)
1064
1103
  throw new RangeError('Invalid stroke width');
1065
- const rendered = textRenderer.render(view(font), size, text, layout, hinting);
1104
+ const fonts = Array.isArray(font) ? font : [font];
1105
+ if (!fonts.length)
1106
+ throw new RangeError('No font given');
1066
1107
  const [r, g, b, a] = Image.colorToRGBA(color);
1108
+ const rendered = await textRenderer.render(fonts.map(view), size, text, layout, hinting, (r << 24 | g << 16 | b << 8 | 255) >>> 0);
1067
1109
  const pad = Math.ceil(strokeWidth);
1068
1110
  const width = rendered.width + 2 * pad, height = rendered.height + 2 * pad;
1069
1111
  const coverage = new Uint8Array(width * height);
1070
1112
  for (let y = 0; y < rendered.height; y++)
1071
1113
  coverage.set(rendered.coverage.subarray(y * rendered.width, (y + 1) * rendered.width), (y + pad) * width + pad);
1114
+ let colored = null;
1115
+ if (rendered.color) {
1116
+ colored = new Uint8ClampedArray(width * height * 4);
1117
+ for (let y = 0; y < rendered.height; y++)
1118
+ colored.set(rendered.color.subarray(y * rendered.width * 4, (y + 1) * rendered.width * 4), ((y + pad) * width + pad) * 4);
1119
+ }
1072
1120
  const image = new Image(width, height);
1073
1121
  const bitmap = image.bitmap;
1074
- if (!strokeWidth) {
1075
- for (let i = 0; i < coverage.length; i++) {
1076
- if (!coverage[i])
1077
- continue;
1078
- bitmap[i * 4] = r;
1079
- bitmap[i * 4 + 1] = g;
1080
- bitmap[i * 4 + 2] = b;
1081
- bitmap[i * 4 + 3] = coverage[i] * a / 255;
1082
- }
1122
+ if (!strokeWidth && !colored) {
1123
+ const pixels = new Uint32Array(bitmap.buffer, bitmap.byteOffset, bitmap.length >> 2), lut = new Uint32Array(256);
1124
+ const bytes = new Uint8ClampedArray(lut.buffer);
1125
+ for (let v = 1; v < 256; v++)
1126
+ bytes.set([r, g, b, v * a / 255], v * 4);
1127
+ for (let i = 0; i < coverage.length; i++)
1128
+ if (coverage[i])
1129
+ pixels[i] = lut[coverage[i]];
1083
1130
  return image;
1084
1131
  }
1085
- const stroke = outline(coverage, width, height, strokeWidth);
1132
+ let shape = coverage;
1133
+ if (colored) {
1134
+ shape = coverage.slice();
1135
+ for (let i = 0; i < shape.length; i++)
1136
+ shape[i] = Math.max(shape[i], colored[i * 4 + 3]);
1137
+ }
1138
+ const stroke = strokeWidth ? outline(shape, width, height, strokeWidth) : null;
1086
1139
  const [sr, sg, sb, sa] = Image.colorToRGBA(strokeColor);
1087
1140
  for (let i = 0; i < coverage.length; i++) {
1088
- const af = coverage[i] / 255 * a / 255, as = stroke[i] * sa / 255;
1089
- const ao = af + as * (1 - af);
1090
- if (ao <= 0)
1141
+ let af = coverage[i] / 255 * a / 255, pr = r * af, pg = g * af, pb = b * af;
1142
+ if (colored && colored[i * 4 + 3]) {
1143
+ const ca = colored[i * 4 + 3] / 255 * a / 255, keep = 1 - ca, f = a / 255;
1144
+ pr = colored[i * 4] * f + pr * keep;
1145
+ pg = colored[i * 4 + 1] * f + pg * keep;
1146
+ pb = colored[i * 4 + 2] * f + pb * keep;
1147
+ af = ca + af * keep;
1148
+ }
1149
+ if (stroke) {
1150
+ const as = stroke[i] * sa / 255 * (1 - shape[i] / 255);
1151
+ pr += sr * as;
1152
+ pg += sg * as;
1153
+ pb += sb * as;
1154
+ af += as;
1155
+ }
1156
+ if (af <= 0)
1091
1157
  continue;
1092
- bitmap[i * 4] = (r * af + sr * as * (1 - af)) / ao;
1093
- bitmap[i * 4 + 1] = (g * af + sg * as * (1 - af)) / ao;
1094
- bitmap[i * 4 + 2] = (b * af + sb * as * (1 - af)) / ao;
1095
- bitmap[i * 4 + 3] = ao * 255;
1158
+ bitmap[i * 4] = pr / af;
1159
+ bitmap[i * 4 + 1] = pg / af;
1160
+ bitmap[i * 4 + 2] = pb / af;
1161
+ bitmap[i * 4 + 3] = af * 255;
1096
1162
  }
1097
1163
  return image;
1098
1164
  }
@@ -1134,8 +1200,9 @@ function hue2rgb(p, q, t) {
1134
1200
  return p + (q - p) * (2 / 3 - t) * 6;
1135
1201
  return p;
1136
1202
  }
1137
- function resizeMode(modeOrOptions) {
1138
- return (isOptions(modeOrOptions) ? modeOrOptions.mode : modeOrOptions) ?? Image.RESIZE_BILINEAR;
1203
+ function resizeOptions(modeOrOptions) {
1204
+ const options = isOptions(modeOrOptions) ? modeOrOptions : { mode: modeOrOptions };
1205
+ return { mode: options.mode ?? Image.RESIZE_BILINEAR, fast: options.fast ?? true };
1139
1206
  }
1140
1207
  const hardLight = (b, s) => s <= 0.5 ? b * 2 * s : 1 - (1 - b) * (1 - (2 * s - 1));
1141
1208
  const BLEND_MODES = {
@@ -1158,37 +1225,80 @@ const BLEND_MODES = {
1158
1225
  'exclusion': (b, s) => b + s - 2 * b * s,
1159
1226
  'add': (b, s) => Math.min(1, b + s)
1160
1227
  };
1228
+ const OUTLINE_WINDOW = 1;
1161
1229
  function outline(coverage, width, height, radius) {
1162
1230
  const count = width * height;
1163
1231
  const distance = new Float64Array(count);
1164
1232
  for (let i = 0; i < count; i++)
1165
1233
  distance[i] = coverage[i] >= 128 ? 0 : Infinity;
1234
+ const nearestY = new Int32Array(count), nearestX = new Int32Array(count);
1166
1235
  const size = Math.max(width, height);
1167
- const line = new Float64Array(size), result = new Float64Array(size);
1236
+ const line = new Float64Array(size), result = new Float64Array(size), nearest = new Int32Array(size);
1168
1237
  const parabolas = new Int32Array(size), bounds = new Float64Array(size + 1);
1169
1238
  for (let x = 0; x < width; x++) {
1170
1239
  for (let y = 0; y < height; y++)
1171
1240
  line[y] = distance[y * width + x];
1172
- distanceTransform(line, height, result, parabolas, bounds);
1173
- for (let y = 0; y < height; y++)
1241
+ distanceTransform(line, height, result, parabolas, bounds, nearest);
1242
+ for (let y = 0; y < height; y++) {
1174
1243
  distance[y * width + x] = result[y];
1244
+ nearestY[y * width + x] = nearest[y];
1245
+ }
1175
1246
  }
1247
+ const far = (radius + 2) ** 2;
1176
1248
  for (let y = 0; y < height; y++) {
1177
1249
  const row = y * width;
1178
1250
  for (let x = 0; x < width; x++)
1179
- line[x] = distance[row + x];
1180
- distanceTransform(line, width, result, parabolas, bounds);
1181
- for (let x = 0; x < width; x++)
1251
+ line[x] = distance[row + x] < far ? distance[row + x] : Infinity;
1252
+ distanceTransform(line, width, result, parabolas, bounds, nearest);
1253
+ for (let x = 0; x < width; x++) {
1182
1254
  distance[row + x] = result[x];
1255
+ nearestX[row + x] = nearest[x];
1256
+ }
1183
1257
  }
1258
+ const pointX = new Float32Array(count), pointY = new Float32Array(count), isEdge = new Uint8Array(count);
1259
+ for (let y = 1; y < height - 1; y++)
1260
+ for (let x = 1, e = y * width + 1; x < width - 1; x++, e++) {
1261
+ const c = coverage[e];
1262
+ if (!c || c === 255 && coverage[e - 1] >= 128 && coverage[e + 1] >= 128 && coverage[e - width] >= 128 && coverage[e + width] >= 128)
1263
+ continue;
1264
+ const above = e - width, below = e + width;
1265
+ const gx = coverage[above + 1] + 2 * coverage[e + 1] + coverage[below + 1] - coverage[above - 1] - 2 * coverage[e - 1] - coverage[below - 1];
1266
+ const gy = coverage[below - 1] + 2 * coverage[below] + coverage[below + 1] - coverage[above - 1] - 2 * coverage[above] - coverage[above + 1];
1267
+ const length = Math.sqrt(gx * gx + gy * gy);
1268
+ if (!length)
1269
+ continue;
1270
+ const outside = 0.5 - c / 255;
1271
+ pointX[e] = x + outside * gx / length;
1272
+ pointY[e] = y + outside * gy / length;
1273
+ isEdge[e] = 1;
1274
+ }
1184
1275
  const out = new Float32Array(count);
1185
- for (let i = 0; i < count; i++) {
1186
- const d = Math.sqrt(distance[i]);
1187
- out[i] = Math.max(coverage[i] / 255, Math.min(1, Math.max(0, radius + 0.5 - d)));
1188
- }
1276
+ const inner = radius - 1.5, near = inner > 0 ? inner * inner : -1;
1277
+ for (let y = 0, i = 0; y < height; y++)
1278
+ for (let x = 0; x < width; x++, i++) {
1279
+ const d2 = distance[i];
1280
+ if (d2 <= near)
1281
+ out[i] = 1;
1282
+ if (d2 <= near || d2 >= far) {
1283
+ if (d2 >= far)
1284
+ out[i] = coverage[i] / 255;
1285
+ continue;
1286
+ }
1287
+ const qx = nearestX[i], qy = nearestY[y * width + qx];
1288
+ let closest = Infinity;
1289
+ for (let wy = Math.max(0, qy - OUTLINE_WINDOW); wy <= Math.min(height - 1, qy + OUTLINE_WINDOW); wy++)
1290
+ for (let wx = Math.max(0, qx - OUTLINE_WINDOW), e = wy * width + wx; wx <= Math.min(width - 1, qx + OUTLINE_WINDOW); wx++, e++) {
1291
+ if (!isEdge[e])
1292
+ continue;
1293
+ const ex = x - pointX[e], ey = y - pointY[e];
1294
+ closest = Math.min(closest, ex * ex + ey * ey);
1295
+ }
1296
+ closest = closest === Infinity ? Math.sqrt(d2) - 0.5 : Math.sqrt(closest);
1297
+ out[i] = Math.max(coverage[i] / 255, Math.min(1, Math.max(0, radius + 0.5 - closest)));
1298
+ }
1189
1299
  return out;
1190
1300
  }
1191
- function distanceTransform(f, n, d, v, z) {
1301
+ function distanceTransform(f, n, d, v, z, nearest) {
1192
1302
  let k = -1;
1193
1303
  for (let q = 0; q < n; q++) {
1194
1304
  if (f[q] === Infinity)
@@ -1213,5 +1323,6 @@ function distanceTransform(f, n, d, v, z) {
1213
1323
  while (z[j + 1] < q)
1214
1324
  j++;
1215
1325
  d[q] = (q - v[j]) ** 2 + f[v[j]];
1326
+ nearest[q] = v[j];
1216
1327
  }
1217
1328
  }