imagescript 3.2.1 → 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 +21 -11
  5. package/dist/codecs/crc.js +5 -0
  6. package/dist/codecs/gif.js +10 -35
  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 +217 -110
  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 +20 -33
  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 +122 -5
  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 { rotateBilinear } from "./util/rotate.js";
15
16
  import { gaussianBlur, boxBlurRGBA, premultiply, unpremultiply } from "./util/blur.js";
16
- import { isOptions } from "./util/options.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);
@@ -211,42 +223,43 @@ export class Image {
211
223
  return -1;
212
224
  }
213
225
  scale(factor, modeOrOptions) {
214
- const image = this.__scale__(factor, resizeMode(modeOrOptions));
226
+ const { mode, fast } = resizeOptions(modeOrOptions);
227
+ const image = this.__scale__(factor, mode, fast);
215
228
  return this.__apply__(image);
216
229
  }
217
- __scale__(factor, mode = Image.RESIZE_BILINEAR) {
230
+ __scale__(factor, mode = Image.RESIZE_BILINEAR, fast = true) {
218
231
  if (factor === 1)
219
232
  return this;
220
- return this.__resize__(this.width * factor, this.height * factor, mode);
233
+ return this.__resize__(this.width * factor, this.height * factor, mode, fast);
221
234
  }
222
235
  resize(width, height, modeOrOptions) {
223
- const mode = resizeMode(modeOrOptions);
224
- const image = this.__resize__(width, height, mode);
236
+ const { mode, fast } = resizeOptions(modeOrOptions);
237
+ const image = this.__resize__(width, height, mode, fast);
225
238
  return this.__apply__(image);
226
239
  }
227
240
  contain(width, height, modeOrOptions) {
228
- const mode = resizeMode(modeOrOptions);
229
- width = Math.floor(width);
230
- height = Math.floor(height);
241
+ const options = resizeOptions(modeOrOptions);
242
+ width = Math.floor(finite(width, 'width'));
243
+ height = Math.floor(finite(height, 'height'));
231
244
  const scale = Math.min(width / this.width, height / this.height);
232
- 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);
233
246
  }
234
247
  fit(width, height, modeOrOptions) {
235
- const mode = resizeMode(modeOrOptions);
248
+ const options = resizeOptions(modeOrOptions);
236
249
  const result = new Image(width, height);
237
- this.contain(width, height, mode);
250
+ this.contain(width, height, options);
238
251
  result.composite(this, (width - this.width) / 2, (height - this.height) / 2);
239
252
  return this.__apply__(result);
240
253
  }
241
254
  cover(width, height, modeOrOptions) {
242
- const mode = resizeMode(modeOrOptions);
255
+ const options = resizeOptions(modeOrOptions);
243
256
  width = Math.floor(width);
244
257
  height = Math.floor(height);
245
258
  const scale = Math.max(width / this.width, height / this.height);
246
- 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);
247
260
  return this.crop((this.width - width) / 2, (this.height - height) / 2, width, height);
248
261
  }
249
- __resize__(width, height, mode = Image.RESIZE_BILINEAR) {
262
+ __resize__(width, height, mode = Image.RESIZE_BILINEAR, fast = true) {
250
263
  if (width === Image.RESIZE_AUTO && height === Image.RESIZE_AUTO)
251
264
  throw new Error('RESIZE_AUTO can only be used for either width or height, not for both');
252
265
  else if (width === Image.RESIZE_AUTO)
@@ -265,7 +278,7 @@ export class Image {
265
278
  if (!filter)
266
279
  throw new Error('Invalid resize mode');
267
280
  const image = new Image(width, height);
268
- 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);
269
282
  return image;
270
283
  }
271
284
  __resize_nearest_neighbor__(width, height) {
@@ -280,10 +293,10 @@ export class Image {
280
293
  return image;
281
294
  }
282
295
  crop(x, y, width, height) {
283
- x = ~~x;
284
- y = ~~y;
285
- width = ~~width;
286
- height = ~~height;
296
+ x = ~~finite(x, 'x');
297
+ y = ~~finite(y, 'y');
298
+ width = ~~finite(width, 'width');
299
+ height = ~~finite(height, 'height');
287
300
  const left = Math.max(0, x), top = Math.max(0, y);
288
301
  const right = Math.min(this.width, x + width), bottom = Math.min(this.height, y + height);
289
302
  if (right <= left || bottom <= top)
@@ -299,6 +312,8 @@ export class Image {
299
312
  throw new RangeError('Invalid tolerance, it has to be between 0 and 1');
300
313
  const bitmap = this.bitmap;
301
314
  const { width, height } = this;
315
+ if (color !== undefined)
316
+ color = checkColor(color);
302
317
  const c = color === undefined ? [bitmap[0], bitmap[1], bitmap[2], bitmap[3]] : [color >>> 24, color >> 16 & 255, color >> 8 & 255, color & 255];
303
318
  const limit = tolerance * 255;
304
319
  const matches = (x, y) => {
@@ -346,12 +361,12 @@ export class Image {
346
361
  return image;
347
362
  }
348
363
  drawBox(x, y, width, height, color) {
349
- x = ~~x;
350
- y = ~~y;
351
- width = ~~width;
352
- height = ~~height;
364
+ x = ~~finite(x, 'x');
365
+ y = ~~finite(y, 'y');
366
+ width = ~~finite(width, 'width');
367
+ height = ~~finite(height, 'height');
353
368
  if (typeof color !== 'function')
354
- return this.__fast_box__(x, y, width, height, color);
369
+ return this.__fast_box__(x, y, width, height, checkColor(color));
355
370
  for (let tY = Math.max(0, -y); tY < height && tY + y < this.height; tY++)
356
371
  for (let tX = Math.max(0, -x); tX < width && tX + x < this.width; tX++)
357
372
  this.__set_pixel__(tX + x, tY + y, color(tX, tY));
@@ -371,8 +386,11 @@ export class Image {
371
386
  return this;
372
387
  }
373
388
  drawCircle(x, y, radius, color) {
374
- x = ~~x;
375
- 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);
376
394
  const cx = x + 0.5, cy = y + 0.5;
377
395
  for (let py = Math.max(0, Math.floor(cy - radius)); py < Math.min(this.height, Math.ceil(cy + radius)); py++) {
378
396
  for (let px = Math.max(0, Math.floor(cx - radius)); px < Math.min(this.width, Math.ceil(cx + radius)); px++) {
@@ -404,6 +422,7 @@ export class Image {
404
422
  cropCircle(maxOrOptions = false, featheringArgument = 0) {
405
423
  const { max = false, feathering = 0 } = typeof maxOrOptions === 'object' && maxOrOptions !== null
406
424
  ? maxOrOptions : { max: maxOrOptions, feathering: featheringArgument };
425
+ finite(feathering, 'feathering');
407
426
  const radius = (max ? Math.max : Math.min)(this.width, this.height) / 2;
408
427
  const cx = this.width / 2, cy = this.height / 2;
409
428
  const band = Math.min(1, Math.max(0, feathering)) * radius;
@@ -424,28 +443,32 @@ export class Image {
424
443
  }
425
444
  opacity(opacity, absoluteOrOptions = false) {
426
445
  const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
427
- if (isNaN(opacity) || opacity < 0)
446
+ opacity = finite(opacity, 'opacity value');
447
+ if (opacity < 0)
428
448
  throw new RangeError('Invalid opacity value');
429
449
  this.__set_channel_value__(opacity, absolute, 3);
430
450
  return this;
431
451
  }
432
452
  red(factor, absoluteOrOptions = false) {
433
453
  const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
434
- if (isNaN(factor) || factor < 0)
454
+ factor = finite(factor, 'red factor');
455
+ if (factor < 0)
435
456
  throw new RangeError('Invalid red factor');
436
457
  this.__set_channel_value__(factor, absolute, 0);
437
458
  return this;
438
459
  }
439
460
  green(factor, absoluteOrOptions = false) {
440
461
  const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
441
- if (isNaN(factor) || factor < 0)
462
+ factor = finite(factor, 'green factor');
463
+ if (factor < 0)
442
464
  throw new RangeError('Invalid green factor');
443
465
  this.__set_channel_value__(factor, absolute, 1);
444
466
  return this;
445
467
  }
446
468
  blue(factor, absoluteOrOptions = false) {
447
469
  const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
448
- if (isNaN(factor) || factor < 0)
470
+ factor = finite(factor, 'blue factor');
471
+ if (factor < 0)
449
472
  throw new RangeError('Invalid blue factor');
450
473
  this.__set_channel_value__(factor, absolute, 2);
451
474
  return this;
@@ -456,13 +479,15 @@ export class Image {
456
479
  }
457
480
  lightness(value, absoluteOrOptions = false) {
458
481
  const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
459
- if (isNaN(value) || value < 0)
482
+ value = finite(value, 'lightness value');
483
+ if (value < 0)
460
484
  throw new RangeError('Invalid lightness value');
461
485
  return this.__map_hsla__(c => { c.l = value * (absolute ? 1 : c.l); });
462
486
  }
463
487
  saturation(value, absoluteOrOptions = false) {
464
488
  const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
465
- if (isNaN(value) || value < 0)
489
+ value = finite(value, 'saturation value');
490
+ if (value < 0)
466
491
  throw new RangeError('Invalid saturation value');
467
492
  return this.__map_hsla__(c => { c.s = value * (absolute ? 1 : c.s); });
468
493
  }
@@ -470,8 +495,8 @@ export class Image {
470
495
  const options = isOptions(xOrOptions) ? xOrOptions : optionsArgument;
471
496
  const { opacity = 1, mode = 'normal' } = options;
472
497
  let x = isOptions(xOrOptions) ? xOrOptions.x ?? 0 : xOrOptions, y = isOptions(xOrOptions) ? xOrOptions.y ?? 0 : yArgument;
473
- x |= 0;
474
- y |= 0;
498
+ x = finite(x, 'x') | 0;
499
+ y = finite(y, 'y') | 0;
475
500
  if (isNaN(opacity) || opacity < 0 || opacity > 1)
476
501
  throw new RangeError('Invalid composite opacity');
477
502
  const blend = BLEND_MODES[mode];
@@ -557,6 +582,7 @@ export class Image {
557
582
  return this;
558
583
  }
559
584
  hueShift(degrees) {
585
+ degrees = finite(degrees, 'hue shift');
560
586
  return this.__map_hsla__(c => { c.h += degrees / 360; });
561
587
  }
562
588
  __map_hsla__(map) {
@@ -612,7 +638,7 @@ export class Image {
612
638
  return this;
613
639
  }
614
640
  blur(radius) {
615
- if (isNaN(radius) || radius < 0)
641
+ if (!Number.isFinite(radius) || radius < 0)
616
642
  throw new RangeError('Invalid blur radius');
617
643
  if (boxBlurRGBA(this.bitmap, this.width, this.height, radius))
618
644
  return this;
@@ -623,9 +649,9 @@ export class Image {
623
649
  }
624
650
  sharpen(amountOrOptions = 1, radiusArgument = 1) {
625
651
  const { amount = 1, radius = 1 } = isOptions(amountOrOptions) ? amountOrOptions : { amount: amountOrOptions, radius: radiusArgument };
626
- if (isNaN(amount) || amount < 0)
652
+ if (!Number.isFinite(amount) || amount < 0)
627
653
  throw new RangeError('Invalid sharpen amount');
628
- if (isNaN(radius) || radius <= 0)
654
+ if (!Number.isFinite(radius) || radius <= 0)
629
655
  throw new RangeError('Invalid sharpen radius');
630
656
  const blurred = premultiply(this.bitmap);
631
657
  gaussianBlur(blurred, this.width, this.height, radius);
@@ -642,17 +668,20 @@ export class Image {
642
668
  return this;
643
669
  }
644
670
  brightness(factor) {
645
- if (isNaN(factor) || factor < 0)
671
+ factor = finite(factor, 'brightness factor');
672
+ if (factor < 0)
646
673
  throw new RangeError('Invalid brightness factor');
647
674
  return this.__map_channels__(v => v * factor);
648
675
  }
649
676
  contrast(factor) {
650
- if (isNaN(factor) || factor < 0)
677
+ factor = finite(factor, 'contrast factor');
678
+ if (factor < 0)
651
679
  throw new RangeError('Invalid contrast factor');
652
680
  return this.__map_channels__(v => (v - 127.5) * factor + 127.5);
653
681
  }
654
682
  gamma(gamma) {
655
- if (isNaN(gamma) || gamma <= 0)
683
+ gamma = finite(gamma, 'gamma');
684
+ if (gamma <= 0)
656
685
  throw new RangeError('Invalid gamma');
657
686
  return this.__map_channels__(v => 255 * (v / 255) ** (1 / gamma));
658
687
  }
@@ -677,6 +706,8 @@ export class Image {
677
706
  if (!(transparencyThreshold >= 0 && opacityThreshold <= 1 && transparencyThreshold < opacityThreshold))
678
707
  throw new RangeError('Invalid thresholds, they have to satisfy 0 <= transparencyThreshold < opacityThreshold <= 1');
679
708
  const bitmap = this.bitmap;
709
+ if (color !== undefined)
710
+ color = checkColor(color);
680
711
  const c = color === undefined ? [bitmap[0], bitmap[1], bitmap[2]] : [color >>> 24, color >> 16 & 255, color >> 8 & 255];
681
712
  for (let i = 0; i < bitmap.length; i += 4) {
682
713
  let distance = 0;
@@ -730,8 +761,8 @@ export class Image {
730
761
  return color ? Image.rgbToColor(...color) : Image.rgbaToColor(0, 0, 0, 0xff);
731
762
  }
732
763
  rotate(angle, resizeOrOptions = true) {
733
- const { resize = true } = typeof resizeOrOptions === 'object' && resizeOrOptions !== null ? resizeOrOptions : { resize: resizeOrOptions };
734
- 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;
735
766
  if (angle === 0)
736
767
  return this;
737
768
  const { width, height } = this;
@@ -756,6 +787,8 @@ export class Image {
756
787
  const outWidth = resize ? Math.round(Math.abs(width * cos) + Math.abs(height * sin)) : width;
757
788
  const outHeight = resize ? Math.round(Math.abs(width * sin) + Math.abs(height * cos)) : height;
758
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);
759
792
  const s8 = this.bitmap, d8 = out.bitmap;
760
793
  for (let oy = 0; oy < outHeight; oy++) {
761
794
  for (let ox = 0; ox < outWidth; ox++) {
@@ -841,8 +874,8 @@ export class Image {
841
874
  }
842
875
  static gradient(colors) {
843
876
  const entries = Object.entries(colors).sort((a, b) => Number(a[0]) - Number(b[0]));
844
- const positions = entries.map(e => parseFloat(e[0]));
845
- 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]));
846
879
  if (positions.length === 0)
847
880
  throw new RangeError('Invalid gradient point count');
848
881
  else if (positions.length === 1) {
@@ -862,10 +895,10 @@ export class Image {
862
895
  const maxDef = Math.max(...positions);
863
896
  const gradients = [];
864
897
  for (let i = 0; i < positions.length; i++) {
865
- let minPos = positions[i - 1];
898
+ const minPos = positions[i - 1];
866
899
  if (minPos === undefined)
867
900
  continue;
868
- let maxPos = positions[i];
901
+ const maxPos = positions[i];
869
902
  let minVal = values[i - 1];
870
903
  if (minVal === undefined)
871
904
  minVal = values[i];
@@ -886,6 +919,7 @@ export class Image {
886
919
  }
887
920
  roundCorners(radiusOrOptions) {
888
921
  let radius = (isOptions(radiusOrOptions) ? radiusOrOptions.radius : radiusOrOptions) ?? Math.min(this.width, this.height) / 4;
922
+ radius = finite(radius, 'radius');
889
923
  radius = Math.max(0, Math.min(radius, this.width / 2, this.height / 2));
890
924
  const size = Math.ceil(radius);
891
925
  for (let y = 0; y < size; y++) {
@@ -918,7 +952,7 @@ export class Image {
918
952
  };
919
953
  }
920
954
  fisheye(strengthOrOptions) {
921
- const strength = (isOptions(strengthOrOptions) ? strengthOrOptions.strength : strengthOrOptions) ?? 2;
955
+ const strength = finite((isOptions(strengthOrOptions) ? strengthOrOptions.strength : strengthOrOptions) ?? 2, 'strength');
922
956
  const r = new Image(this.width, this.height);
923
957
  const w = this.width;
924
958
  const h = this.height;
@@ -951,7 +985,7 @@ export class Image {
951
985
  ({ compression = 1, metadata = {} } = compressionOrOptions);
952
986
  else
953
987
  metadata = compressionOrOptions;
954
- const level = Math.min(9, Math.max(0, ~~compression));
988
+ const level = Math.min(9, Math.max(0, ~~finite(compression, 'compression level')));
955
989
  return png.encodeRGBA(this.__bytes__(), this.width, this.height, level, pngText(resolveMetadata(metadata)));
956
990
  }
957
991
  encodePNG(compressionOrOptions, metadata) {
@@ -960,7 +994,7 @@ export class Image {
960
994
  async encodeJPEG(qualityOrOptions = 90) {
961
995
  const { quality = 90, metadata = {} } = isOptions(qualityOrOptions) ? qualityOrOptions : { quality: qualityOrOptions };
962
996
  const m = resolveMetadata(metadata);
963
- 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 });
964
998
  }
965
999
  async encodeWebP({ metadata = {} } = {}) {
966
1000
  return webp.encode(this.__bytes__(), this.width, this.height, exifData(resolveMetadata(metadata)));
@@ -970,27 +1004,27 @@ export class Image {
970
1004
  return tga.encode(this.__bytes__(), this.width, this.height, { author, comment, time, software });
971
1005
  }
972
1006
  static async decode(input, options = {}) {
973
- const { applyOrientation = true } = options;
1007
+ const { applyOrientation = true, maxPixels = MAX_PIXELS } = options;
974
1008
  const data = view(input);
975
1009
  const dataView = new DataView(data.buffer, data.byteOffset, data.byteLength);
976
- let decoded;
977
- if (ImageType.isPNG(dataView))
978
- decoded = await png.decode(data);
979
- else if (ImageType.isJPEG(dataView))
980
- decoded = jpeg.decode(data);
981
- else if (ImageType.isTIFF(dataView))
982
- decoded = await tiff.decode(data);
983
- else if (ImageType.isWebP(dataView)) {
984
- if (webp.isAnimated(data))
985
- throw new Error('Animated WebPs have to be decoded with Animation.decode()');
986
- decoded = webp.decode(data);
987
- }
988
- else if (ImageType.isTGA(dataView))
989
- decoded = tga.decode(data);
990
- 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);
991
1024
  throw new Error('Unsupported image type');
1025
+ });
992
1026
  const image = new Image(decoded.width, decoded.height);
993
- image.bitmap.set(decoded.pixels);
1027
+ image.bitmap = decoded.pixels;
994
1028
  return applyOrientation && decoded.orientation ? image.__orient__(decoded.orientation) : image;
995
1029
  }
996
1030
  __orient__(orientation) {
@@ -1064,39 +1098,67 @@ export class Image {
1064
1098
  static async renderText(font, size, text, colorOrOptions = 0xffffffff, layoutArgument, styleArgument = {}) {
1065
1099
  const options = isOptions(colorOrOptions) ? colorOrOptions : { ...styleArgument, color: colorOrOptions, layout: layoutArgument };
1066
1100
  const { color = 0xffffffff, layout = new TextLayout(), strokeWidth = 0, strokeColor = 0x000000ff, hinting = true } = options;
1067
- if (isNaN(strokeWidth) || strokeWidth < 0)
1101
+ size = finite(size, 'font size');
1102
+ if (finite(strokeWidth, 'stroke width') < 0)
1068
1103
  throw new RangeError('Invalid stroke width');
1069
- 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');
1070
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);
1071
1109
  const pad = Math.ceil(strokeWidth);
1072
1110
  const width = rendered.width + 2 * pad, height = rendered.height + 2 * pad;
1073
1111
  const coverage = new Uint8Array(width * height);
1074
1112
  for (let y = 0; y < rendered.height; y++)
1075
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
+ }
1076
1120
  const image = new Image(width, height);
1077
1121
  const bitmap = image.bitmap;
1078
- if (!strokeWidth) {
1079
- for (let i = 0; i < coverage.length; i++) {
1080
- if (!coverage[i])
1081
- continue;
1082
- bitmap[i * 4] = r;
1083
- bitmap[i * 4 + 1] = g;
1084
- bitmap[i * 4 + 2] = b;
1085
- bitmap[i * 4 + 3] = coverage[i] * a / 255;
1086
- }
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]];
1087
1130
  return image;
1088
1131
  }
1089
- 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;
1090
1139
  const [sr, sg, sb, sa] = Image.colorToRGBA(strokeColor);
1091
1140
  for (let i = 0; i < coverage.length; i++) {
1092
- const af = coverage[i] / 255 * a / 255, as = stroke[i] * sa / 255;
1093
- const ao = af + as * (1 - af);
1094
- 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)
1095
1157
  continue;
1096
- bitmap[i * 4] = (r * af + sr * as * (1 - af)) / ao;
1097
- bitmap[i * 4 + 1] = (g * af + sg * as * (1 - af)) / ao;
1098
- bitmap[i * 4 + 2] = (b * af + sb * as * (1 - af)) / ao;
1099
- 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;
1100
1162
  }
1101
1163
  return image;
1102
1164
  }
@@ -1138,8 +1200,9 @@ function hue2rgb(p, q, t) {
1138
1200
  return p + (q - p) * (2 / 3 - t) * 6;
1139
1201
  return p;
1140
1202
  }
1141
- function resizeMode(modeOrOptions) {
1142
- 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 };
1143
1206
  }
1144
1207
  const hardLight = (b, s) => s <= 0.5 ? b * 2 * s : 1 - (1 - b) * (1 - (2 * s - 1));
1145
1208
  const BLEND_MODES = {
@@ -1162,37 +1225,80 @@ const BLEND_MODES = {
1162
1225
  'exclusion': (b, s) => b + s - 2 * b * s,
1163
1226
  'add': (b, s) => Math.min(1, b + s)
1164
1227
  };
1228
+ const OUTLINE_WINDOW = 1;
1165
1229
  function outline(coverage, width, height, radius) {
1166
1230
  const count = width * height;
1167
1231
  const distance = new Float64Array(count);
1168
1232
  for (let i = 0; i < count; i++)
1169
1233
  distance[i] = coverage[i] >= 128 ? 0 : Infinity;
1234
+ const nearestY = new Int32Array(count), nearestX = new Int32Array(count);
1170
1235
  const size = Math.max(width, height);
1171
- const line = new Float64Array(size), result = new Float64Array(size);
1236
+ const line = new Float64Array(size), result = new Float64Array(size), nearest = new Int32Array(size);
1172
1237
  const parabolas = new Int32Array(size), bounds = new Float64Array(size + 1);
1173
1238
  for (let x = 0; x < width; x++) {
1174
1239
  for (let y = 0; y < height; y++)
1175
1240
  line[y] = distance[y * width + x];
1176
- distanceTransform(line, height, result, parabolas, bounds);
1177
- for (let y = 0; y < height; y++)
1241
+ distanceTransform(line, height, result, parabolas, bounds, nearest);
1242
+ for (let y = 0; y < height; y++) {
1178
1243
  distance[y * width + x] = result[y];
1244
+ nearestY[y * width + x] = nearest[y];
1245
+ }
1179
1246
  }
1247
+ const far = (radius + 2) ** 2;
1180
1248
  for (let y = 0; y < height; y++) {
1181
1249
  const row = y * width;
1182
1250
  for (let x = 0; x < width; x++)
1183
- line[x] = distance[row + x];
1184
- distanceTransform(line, width, result, parabolas, bounds);
1185
- 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++) {
1186
1254
  distance[row + x] = result[x];
1255
+ nearestX[row + x] = nearest[x];
1256
+ }
1187
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
+ }
1188
1275
  const out = new Float32Array(count);
1189
- for (let i = 0; i < count; i++) {
1190
- const d = Math.sqrt(distance[i]);
1191
- out[i] = Math.max(coverage[i] / 255, Math.min(1, Math.max(0, radius + 0.5 - d)));
1192
- }
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
+ }
1193
1299
  return out;
1194
1300
  }
1195
- function distanceTransform(f, n, d, v, z) {
1301
+ function distanceTransform(f, n, d, v, z, nearest) {
1196
1302
  let k = -1;
1197
1303
  for (let q = 0; q < n; q++) {
1198
1304
  if (f[q] === Infinity)
@@ -1217,5 +1323,6 @@ function distanceTransform(f, n, d, v, z) {
1217
1323
  while (z[j + 1] < q)
1218
1324
  j++;
1219
1325
  d[q] = (q - v[j]) ** 2 + f[v[j]];
1326
+ nearest[q] = v[j];
1220
1327
  }
1221
1328
  }