imagescript 3.2.1 → 3.4.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 (101) hide show
  1. package/README.md +35 -10
  2. package/THIRD_PARTY_NOTICES.md +298 -2
  3. package/dist/animation.d.ts +25 -8
  4. package/dist/animation.js +44 -13
  5. package/dist/codecs/av1/cdef.js +197 -0
  6. package/dist/codecs/av1/data.js +141 -0
  7. package/dist/codecs/av1/decode.js +1769 -0
  8. package/dist/codecs/av1/filmgrain.js +219 -0
  9. package/dist/codecs/av1/frame.js +114 -0
  10. package/dist/codecs/av1/ipred.js +491 -0
  11. package/dist/codecs/av1/itx.js +950 -0
  12. package/dist/codecs/av1/loopfilter.js +149 -0
  13. package/dist/codecs/av1/msac.js +161 -0
  14. package/dist/codecs/av1/obu.js +516 -0
  15. package/dist/codecs/av1/postfilter.js +28 -0
  16. package/dist/codecs/av1/restoration.js +193 -0
  17. package/dist/codecs/av1/tables.js +160 -0
  18. package/dist/codecs/avif.js +581 -0
  19. package/dist/codecs/bmp.js +317 -0
  20. package/dist/codecs/crc.js +5 -0
  21. package/dist/codecs/gif.js +10 -35
  22. package/dist/codecs/hdr.d.ts +25 -0
  23. package/dist/codecs/hdr.js +201 -0
  24. package/dist/codecs/ico.js +48 -0
  25. package/dist/codecs/jpeg.js +11 -28
  26. package/dist/codecs/jxl/bits.js +126 -0
  27. package/dist/codecs/jxl/blend.js +117 -0
  28. package/dist/codecs/jxl/color.js +116 -0
  29. package/dist/codecs/jxl/dct.js +404 -0
  30. package/dist/codecs/jxl/entropy.js +556 -0
  31. package/dist/codecs/jxl/features.js +325 -0
  32. package/dist/codecs/jxl/filters.js +119 -0
  33. package/dist/codecs/jxl/frame-header.js +195 -0
  34. package/dist/codecs/jxl/frame.js +319 -0
  35. package/dist/codecs/jxl/headers.js +249 -0
  36. package/dist/codecs/jxl/idct.js +484 -0
  37. package/dist/codecs/jxl/modular.js +688 -0
  38. package/dist/codecs/jxl/quant.js +219 -0
  39. package/dist/codecs/jxl/upsample.js +52 -0
  40. package/dist/codecs/jxl/vardct.js +472 -0
  41. package/dist/codecs/jxl.js +89 -0
  42. package/dist/codecs/netpbm.js +239 -0
  43. package/dist/codecs/png.js +22 -10
  44. package/dist/codecs/qoi.js +150 -0
  45. package/dist/codecs/tga.js +3 -1
  46. package/dist/codecs/tiff.js +7 -2
  47. package/dist/codecs/vp8-enc-dsp.js +932 -0
  48. package/dist/codecs/vp8-enc-tables.js +78 -0
  49. package/dist/codecs/vp8-enc.js +1930 -0
  50. package/dist/codecs/vp8.js +3 -1
  51. package/dist/codecs/vp8l.js +126 -58
  52. package/dist/codecs/webp.js +47 -11
  53. package/dist/codecs/zlib.js +81 -69
  54. package/dist/frame.d.ts +1 -0
  55. package/dist/frame.js +4 -2
  56. package/dist/gif.d.ts +1 -0
  57. package/dist/gif.js +2 -2
  58. package/dist/image-type.d.ts +15 -1
  59. package/dist/image-type.js +45 -2
  60. package/dist/image.d.ts +89 -13
  61. package/dist/image.js +266 -113
  62. package/dist/index.d.ts +3 -2
  63. package/dist/index.js +4 -4
  64. package/dist/render/bidi.js +350 -0
  65. package/dist/render/buffer.js +362 -0
  66. package/dist/render/color.js +164 -0
  67. package/dist/render/colr.js +523 -0
  68. package/dist/render/font.js +173 -241
  69. package/dist/render/hinting.js +38 -22
  70. package/dist/render/layout.js +1553 -0
  71. package/dist/render/linebreak.js +210 -0
  72. package/dist/render/segment.js +106 -0
  73. package/dist/render/shape.js +984 -0
  74. package/dist/render/shapers/arabic.js +284 -0
  75. package/dist/render/shapers/hangul.js +142 -0
  76. package/dist/render/shapers/hebrew.js +49 -0
  77. package/dist/render/shapers/index.js +52 -0
  78. package/dist/render/shapers/indic.js +579 -0
  79. package/dist/render/shapers/khmer.js +104 -0
  80. package/dist/render/shapers/machine.js +129 -0
  81. package/dist/render/shapers/myanmar.js +121 -0
  82. package/dist/render/shapers/syllabic.js +100 -0
  83. package/dist/render/shapers/thai.js +125 -0
  84. package/dist/render/shapers/use.js +235 -0
  85. package/dist/render/svg.js +102 -23
  86. package/dist/render/text.js +373 -103
  87. package/dist/render/unicode-data.js +63 -0
  88. package/dist/render/unicode.js +50 -0
  89. package/dist/text-layout.d.ts +4 -1
  90. package/dist/text-layout.js +2 -2
  91. package/dist/util/alpha.js +43 -0
  92. package/dist/util/blur.js +20 -33
  93. package/dist/util/bytes.js +40 -0
  94. package/dist/util/limits.js +34 -0
  95. package/dist/util/options.js +7 -0
  96. package/dist/util/resample.js +126 -7
  97. package/dist/util/rotate.js +43 -0
  98. package/dist/version.js +1 -1
  99. package/licenses/HarfBuzz.txt +42 -0
  100. package/licenses/Unicode-3.0.txt +39 -0
  101. package/package.json +25 -2
package/dist/image.js CHANGED
@@ -3,6 +3,13 @@ import * as jpeg from "./codecs/jpeg.js";
3
3
  import * as tiff from "./codecs/tiff.js";
4
4
  import * as webp from "./codecs/webp.js";
5
5
  import * as tga from "./codecs/tga.js";
6
+ import * as bmp from "./codecs/bmp.js";
7
+ import * as ico from "./codecs/ico.js";
8
+ import * as qoi from "./codecs/qoi.js";
9
+ import * as netpbm from "./codecs/netpbm.js";
10
+ import * as hdr from "./codecs/hdr.js";
11
+ import * as avif from "./codecs/avif.js";
12
+ import * as jxl from "./codecs/jxl.js";
6
13
  import * as quantize from "./codecs/quantize.js";
7
14
  import * as textRenderer from "./render/text.js";
8
15
  import * as svgRenderer from "./render/svg.js";
@@ -12,8 +19,10 @@ import { TextLayout } from "./text-layout.js";
12
19
  import { resolveMetadata, pngText, exifData } from "./metadata.js";
13
20
  import { encode as utf8 } from "./util/utf8.js";
14
21
  import { resample, BOX, TRIANGLE, CUBIC } from "./util/resample.js";
22
+ import { rotateBilinear } from "./util/rotate.js";
15
23
  import { gaussianBlur, boxBlurRGBA, premultiply, unpremultiply } from "./util/blur.js";
16
- import { isOptions } from "./util/options.js";
24
+ import { isOptions, finite, color as checkColor } from "./util/options.js";
25
+ import { MAX_PIXELS, decoding } from "./util/limits.js";
17
26
  export class Image {
18
27
  __width__;
19
28
  __height__;
@@ -70,13 +79,20 @@ export class Image {
70
79
  yield [x, y, view.getUint32(offset, false)];
71
80
  }
72
81
  static rgbaToColor(r, g, b, a) {
82
+ r = finite(r, 'red');
83
+ g = finite(g, 'green');
84
+ b = finite(b, 'blue');
85
+ a = finite(a, 'alpha');
73
86
  return (channel(r) << 24 | channel(g) << 16 | channel(b) << 8 | channel(a)) >>> 0;
74
87
  }
75
88
  static rgbToColor(r, g, b) {
76
89
  return Image.rgbaToColor(r, g, b, 0xff);
77
90
  }
78
91
  static hslaToColor(h, s, l, a) {
79
- h = wrapHue(h);
92
+ h = wrapHue(finite(h, 'hue'));
93
+ s = finite(s, 'saturation');
94
+ l = finite(l, 'lightness');
95
+ a = finite(a, 'alpha');
80
96
  s = Math.min(1, Math.max(0, s));
81
97
  l = Math.min(1, Math.max(0, l));
82
98
  a = Math.min(1, Math.max(0, a));
@@ -97,9 +113,10 @@ export class Image {
97
113
  return Image.hslaToColor(h, s, l, 1);
98
114
  }
99
115
  static rgbaToHSLA(r, g, b, a) {
100
- r /= 255;
101
- g /= 255;
102
- b /= 255;
116
+ r = finite(r, 'red') / 255;
117
+ g = finite(g, 'green') / 255;
118
+ b = finite(b, 'blue') / 255;
119
+ a = finite(a, 'alpha');
103
120
  const max = Math.max(r, g, b), min = Math.min(r, g, b);
104
121
  let h = 0, s;
105
122
  const l = (max + min) / 2;
@@ -125,6 +142,7 @@ export class Image {
125
142
  return [h, s, l, a / 255];
126
143
  }
127
144
  static colorToRGBA(color) {
145
+ color = checkColor(color);
128
146
  return [(color >> 24) & 0xff, (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff];
129
147
  }
130
148
  static colorToRGB(color) {
@@ -132,23 +150,23 @@ export class Image {
132
150
  return [r, g, b];
133
151
  }
134
152
  getPixelAt(x, y) {
135
- x = ~~x;
136
- y = ~~y;
153
+ x = ~~finite(x, 'x');
154
+ y = ~~finite(y, 'y');
137
155
  this.__check_boundaries__(x, y);
138
156
  return this.__view__.getUint32((y * this.width + x) * 4, false);
139
157
  }
140
158
  getRGBAAt(x, y) {
141
- x = ~~x;
142
- y = ~~y;
159
+ x = ~~finite(x, 'x');
160
+ y = ~~finite(y, 'y');
143
161
  this.__check_boundaries__(x, y);
144
162
  const idx = (y * this.width + x) * 4;
145
163
  return this.bitmap.subarray(idx, idx + 4);
146
164
  }
147
165
  setPixelAt(x, y, pixelColor) {
148
- x = ~~x;
149
- y = ~~y;
166
+ x = ~~finite(x, 'x');
167
+ y = ~~finite(y, 'y');
150
168
  this.__check_boundaries__(x, y);
151
- this.__set_pixel__(x, y, pixelColor);
169
+ this.__set_pixel__(x, y, checkColor(pixelColor));
152
170
  return this;
153
171
  }
154
172
  __set_pixel__(x, y, pixelColor) {
@@ -180,6 +198,7 @@ export class Image {
180
198
  view.setUint32(offset, color(x, y), false);
181
199
  }
182
200
  else {
201
+ color = checkColor(color);
183
202
  if (!this.bitmap.length)
184
203
  return this;
185
204
  view.setUint32(0, color, false);
@@ -211,42 +230,43 @@ export class Image {
211
230
  return -1;
212
231
  }
213
232
  scale(factor, modeOrOptions) {
214
- const image = this.__scale__(factor, resizeMode(modeOrOptions));
233
+ const { mode, fast } = resizeOptions(modeOrOptions);
234
+ const image = this.__scale__(factor, mode, fast);
215
235
  return this.__apply__(image);
216
236
  }
217
- __scale__(factor, mode = Image.RESIZE_BILINEAR) {
237
+ __scale__(factor, mode = Image.RESIZE_BILINEAR, fast = true) {
218
238
  if (factor === 1)
219
239
  return this;
220
- return this.__resize__(this.width * factor, this.height * factor, mode);
240
+ return this.__resize__(this.width * factor, this.height * factor, mode, fast);
221
241
  }
222
242
  resize(width, height, modeOrOptions) {
223
- const mode = resizeMode(modeOrOptions);
224
- const image = this.__resize__(width, height, mode);
243
+ const { mode, fast } = resizeOptions(modeOrOptions);
244
+ const image = this.__resize__(width, height, mode, fast);
225
245
  return this.__apply__(image);
226
246
  }
227
247
  contain(width, height, modeOrOptions) {
228
- const mode = resizeMode(modeOrOptions);
229
- width = Math.floor(width);
230
- height = Math.floor(height);
248
+ const options = resizeOptions(modeOrOptions);
249
+ width = Math.floor(finite(width, 'width'));
250
+ height = Math.floor(finite(height, 'height'));
231
251
  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);
252
+ 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
253
  }
234
254
  fit(width, height, modeOrOptions) {
235
- const mode = resizeMode(modeOrOptions);
255
+ const options = resizeOptions(modeOrOptions);
236
256
  const result = new Image(width, height);
237
- this.contain(width, height, mode);
257
+ this.contain(width, height, options);
238
258
  result.composite(this, (width - this.width) / 2, (height - this.height) / 2);
239
259
  return this.__apply__(result);
240
260
  }
241
261
  cover(width, height, modeOrOptions) {
242
- const mode = resizeMode(modeOrOptions);
262
+ const options = resizeOptions(modeOrOptions);
243
263
  width = Math.floor(width);
244
264
  height = Math.floor(height);
245
265
  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);
266
+ this.resize(Math.max(width, Math.round(this.width * scale)), Math.max(height, Math.round(this.height * scale)), options);
247
267
  return this.crop((this.width - width) / 2, (this.height - height) / 2, width, height);
248
268
  }
249
- __resize__(width, height, mode = Image.RESIZE_BILINEAR) {
269
+ __resize__(width, height, mode = Image.RESIZE_BILINEAR, fast = true) {
250
270
  if (width === Image.RESIZE_AUTO && height === Image.RESIZE_AUTO)
251
271
  throw new Error('RESIZE_AUTO can only be used for either width or height, not for both');
252
272
  else if (width === Image.RESIZE_AUTO)
@@ -265,7 +285,7 @@ export class Image {
265
285
  if (!filter)
266
286
  throw new Error('Invalid resize mode');
267
287
  const image = new Image(width, height);
268
- resample(this.bitmap, this.width, this.height, width, height, filter, image.bitmap);
288
+ resample(this.bitmap, this.width, this.height, width, height, filter, image.bitmap, fast);
269
289
  return image;
270
290
  }
271
291
  __resize_nearest_neighbor__(width, height) {
@@ -280,10 +300,10 @@ export class Image {
280
300
  return image;
281
301
  }
282
302
  crop(x, y, width, height) {
283
- x = ~~x;
284
- y = ~~y;
285
- width = ~~width;
286
- height = ~~height;
303
+ x = ~~finite(x, 'x');
304
+ y = ~~finite(y, 'y');
305
+ width = ~~finite(width, 'width');
306
+ height = ~~finite(height, 'height');
287
307
  const left = Math.max(0, x), top = Math.max(0, y);
288
308
  const right = Math.min(this.width, x + width), bottom = Math.min(this.height, y + height);
289
309
  if (right <= left || bottom <= top)
@@ -299,6 +319,8 @@ export class Image {
299
319
  throw new RangeError('Invalid tolerance, it has to be between 0 and 1');
300
320
  const bitmap = this.bitmap;
301
321
  const { width, height } = this;
322
+ if (color !== undefined)
323
+ color = checkColor(color);
302
324
  const c = color === undefined ? [bitmap[0], bitmap[1], bitmap[2], bitmap[3]] : [color >>> 24, color >> 16 & 255, color >> 8 & 255, color & 255];
303
325
  const limit = tolerance * 255;
304
326
  const matches = (x, y) => {
@@ -346,12 +368,12 @@ export class Image {
346
368
  return image;
347
369
  }
348
370
  drawBox(x, y, width, height, color) {
349
- x = ~~x;
350
- y = ~~y;
351
- width = ~~width;
352
- height = ~~height;
371
+ x = ~~finite(x, 'x');
372
+ y = ~~finite(y, 'y');
373
+ width = ~~finite(width, 'width');
374
+ height = ~~finite(height, 'height');
353
375
  if (typeof color !== 'function')
354
- return this.__fast_box__(x, y, width, height, color);
376
+ return this.__fast_box__(x, y, width, height, checkColor(color));
355
377
  for (let tY = Math.max(0, -y); tY < height && tY + y < this.height; tY++)
356
378
  for (let tX = Math.max(0, -x); tX < width && tX + x < this.width; tX++)
357
379
  this.__set_pixel__(tX + x, tY + y, color(tX, tY));
@@ -371,8 +393,11 @@ export class Image {
371
393
  return this;
372
394
  }
373
395
  drawCircle(x, y, radius, color) {
374
- x = ~~x;
375
- y = ~~y;
396
+ x = ~~finite(x, 'x');
397
+ y = ~~finite(y, 'y');
398
+ radius = finite(radius, 'radius');
399
+ if (typeof color !== 'function')
400
+ color = checkColor(color);
376
401
  const cx = x + 0.5, cy = y + 0.5;
377
402
  for (let py = Math.max(0, Math.floor(cy - radius)); py < Math.min(this.height, Math.ceil(cy + radius)); py++) {
378
403
  for (let px = Math.max(0, Math.floor(cx - radius)); px < Math.min(this.width, Math.ceil(cx + radius)); px++) {
@@ -404,6 +429,7 @@ export class Image {
404
429
  cropCircle(maxOrOptions = false, featheringArgument = 0) {
405
430
  const { max = false, feathering = 0 } = typeof maxOrOptions === 'object' && maxOrOptions !== null
406
431
  ? maxOrOptions : { max: maxOrOptions, feathering: featheringArgument };
432
+ finite(feathering, 'feathering');
407
433
  const radius = (max ? Math.max : Math.min)(this.width, this.height) / 2;
408
434
  const cx = this.width / 2, cy = this.height / 2;
409
435
  const band = Math.min(1, Math.max(0, feathering)) * radius;
@@ -424,28 +450,32 @@ export class Image {
424
450
  }
425
451
  opacity(opacity, absoluteOrOptions = false) {
426
452
  const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
427
- if (isNaN(opacity) || opacity < 0)
453
+ opacity = finite(opacity, 'opacity value');
454
+ if (opacity < 0)
428
455
  throw new RangeError('Invalid opacity value');
429
456
  this.__set_channel_value__(opacity, absolute, 3);
430
457
  return this;
431
458
  }
432
459
  red(factor, absoluteOrOptions = false) {
433
460
  const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
434
- if (isNaN(factor) || factor < 0)
461
+ factor = finite(factor, 'red factor');
462
+ if (factor < 0)
435
463
  throw new RangeError('Invalid red factor');
436
464
  this.__set_channel_value__(factor, absolute, 0);
437
465
  return this;
438
466
  }
439
467
  green(factor, absoluteOrOptions = false) {
440
468
  const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
441
- if (isNaN(factor) || factor < 0)
469
+ factor = finite(factor, 'green factor');
470
+ if (factor < 0)
442
471
  throw new RangeError('Invalid green factor');
443
472
  this.__set_channel_value__(factor, absolute, 1);
444
473
  return this;
445
474
  }
446
475
  blue(factor, absoluteOrOptions = false) {
447
476
  const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
448
- if (isNaN(factor) || factor < 0)
477
+ factor = finite(factor, 'blue factor');
478
+ if (factor < 0)
449
479
  throw new RangeError('Invalid blue factor');
450
480
  this.__set_channel_value__(factor, absolute, 2);
451
481
  return this;
@@ -456,13 +486,15 @@ export class Image {
456
486
  }
457
487
  lightness(value, absoluteOrOptions = false) {
458
488
  const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
459
- if (isNaN(value) || value < 0)
489
+ value = finite(value, 'lightness value');
490
+ if (value < 0)
460
491
  throw new RangeError('Invalid lightness value');
461
492
  return this.__map_hsla__(c => { c.l = value * (absolute ? 1 : c.l); });
462
493
  }
463
494
  saturation(value, absoluteOrOptions = false) {
464
495
  const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
465
- if (isNaN(value) || value < 0)
496
+ value = finite(value, 'saturation value');
497
+ if (value < 0)
466
498
  throw new RangeError('Invalid saturation value');
467
499
  return this.__map_hsla__(c => { c.s = value * (absolute ? 1 : c.s); });
468
500
  }
@@ -470,8 +502,8 @@ export class Image {
470
502
  const options = isOptions(xOrOptions) ? xOrOptions : optionsArgument;
471
503
  const { opacity = 1, mode = 'normal' } = options;
472
504
  let x = isOptions(xOrOptions) ? xOrOptions.x ?? 0 : xOrOptions, y = isOptions(xOrOptions) ? xOrOptions.y ?? 0 : yArgument;
473
- x |= 0;
474
- y |= 0;
505
+ x = finite(x, 'x') | 0;
506
+ y = finite(y, 'y') | 0;
475
507
  if (isNaN(opacity) || opacity < 0 || opacity > 1)
476
508
  throw new RangeError('Invalid composite opacity');
477
509
  const blend = BLEND_MODES[mode];
@@ -557,6 +589,7 @@ export class Image {
557
589
  return this;
558
590
  }
559
591
  hueShift(degrees) {
592
+ degrees = finite(degrees, 'hue shift');
560
593
  return this.__map_hsla__(c => { c.h += degrees / 360; });
561
594
  }
562
595
  __map_hsla__(map) {
@@ -612,7 +645,7 @@ export class Image {
612
645
  return this;
613
646
  }
614
647
  blur(radius) {
615
- if (isNaN(radius) || radius < 0)
648
+ if (!Number.isFinite(radius) || radius < 0)
616
649
  throw new RangeError('Invalid blur radius');
617
650
  if (boxBlurRGBA(this.bitmap, this.width, this.height, radius))
618
651
  return this;
@@ -623,9 +656,9 @@ export class Image {
623
656
  }
624
657
  sharpen(amountOrOptions = 1, radiusArgument = 1) {
625
658
  const { amount = 1, radius = 1 } = isOptions(amountOrOptions) ? amountOrOptions : { amount: amountOrOptions, radius: radiusArgument };
626
- if (isNaN(amount) || amount < 0)
659
+ if (!Number.isFinite(amount) || amount < 0)
627
660
  throw new RangeError('Invalid sharpen amount');
628
- if (isNaN(radius) || radius <= 0)
661
+ if (!Number.isFinite(radius) || radius <= 0)
629
662
  throw new RangeError('Invalid sharpen radius');
630
663
  const blurred = premultiply(this.bitmap);
631
664
  gaussianBlur(blurred, this.width, this.height, radius);
@@ -642,17 +675,20 @@ export class Image {
642
675
  return this;
643
676
  }
644
677
  brightness(factor) {
645
- if (isNaN(factor) || factor < 0)
678
+ factor = finite(factor, 'brightness factor');
679
+ if (factor < 0)
646
680
  throw new RangeError('Invalid brightness factor');
647
681
  return this.__map_channels__(v => v * factor);
648
682
  }
649
683
  contrast(factor) {
650
- if (isNaN(factor) || factor < 0)
684
+ factor = finite(factor, 'contrast factor');
685
+ if (factor < 0)
651
686
  throw new RangeError('Invalid contrast factor');
652
687
  return this.__map_channels__(v => (v - 127.5) * factor + 127.5);
653
688
  }
654
689
  gamma(gamma) {
655
- if (isNaN(gamma) || gamma <= 0)
690
+ gamma = finite(gamma, 'gamma');
691
+ if (gamma <= 0)
656
692
  throw new RangeError('Invalid gamma');
657
693
  return this.__map_channels__(v => 255 * (v / 255) ** (1 / gamma));
658
694
  }
@@ -677,6 +713,8 @@ export class Image {
677
713
  if (!(transparencyThreshold >= 0 && opacityThreshold <= 1 && transparencyThreshold < opacityThreshold))
678
714
  throw new RangeError('Invalid thresholds, they have to satisfy 0 <= transparencyThreshold < opacityThreshold <= 1');
679
715
  const bitmap = this.bitmap;
716
+ if (color !== undefined)
717
+ color = checkColor(color);
680
718
  const c = color === undefined ? [bitmap[0], bitmap[1], bitmap[2]] : [color >>> 24, color >> 16 & 255, color >> 8 & 255];
681
719
  for (let i = 0; i < bitmap.length; i += 4) {
682
720
  let distance = 0;
@@ -730,8 +768,8 @@ export class Image {
730
768
  return color ? Image.rgbToColor(...color) : Image.rgbaToColor(0, 0, 0, 0xff);
731
769
  }
732
770
  rotate(angle, resizeOrOptions = true) {
733
- const { resize = true } = typeof resizeOrOptions === 'object' && resizeOrOptions !== null ? resizeOrOptions : { resize: resizeOrOptions };
734
- angle = ((angle % 360) + 360) % 360;
771
+ const { resize = true, fast = true } = typeof resizeOrOptions === 'object' && resizeOrOptions !== null ? resizeOrOptions : { resize: resizeOrOptions };
772
+ angle = ((finite(angle, 'angle') % 360) + 360) % 360;
735
773
  if (angle === 0)
736
774
  return this;
737
775
  const { width, height } = this;
@@ -756,6 +794,8 @@ export class Image {
756
794
  const outWidth = resize ? Math.round(Math.abs(width * cos) + Math.abs(height * sin)) : width;
757
795
  const outHeight = resize ? Math.round(Math.abs(width * sin) + Math.abs(height * cos)) : height;
758
796
  const out = new Image(outWidth, outHeight);
797
+ if (fast && rotateBilinear(this.bitmap, width, height, out.bitmap, outWidth, outHeight, sin, cos))
798
+ return this.__apply__(out);
759
799
  const s8 = this.bitmap, d8 = out.bitmap;
760
800
  for (let oy = 0; oy < outHeight; oy++) {
761
801
  for (let ox = 0; ox < outWidth; ox++) {
@@ -841,8 +881,8 @@ export class Image {
841
881
  }
842
882
  static gradient(colors) {
843
883
  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]);
884
+ const positions = entries.map(e => finite(parseFloat(e[0]), 'gradient position'));
885
+ const values = entries.map(e => checkColor(e[1]));
846
886
  if (positions.length === 0)
847
887
  throw new RangeError('Invalid gradient point count');
848
888
  else if (positions.length === 1) {
@@ -862,10 +902,10 @@ export class Image {
862
902
  const maxDef = Math.max(...positions);
863
903
  const gradients = [];
864
904
  for (let i = 0; i < positions.length; i++) {
865
- let minPos = positions[i - 1];
905
+ const minPos = positions[i - 1];
866
906
  if (minPos === undefined)
867
907
  continue;
868
- let maxPos = positions[i];
908
+ const maxPos = positions[i];
869
909
  let minVal = values[i - 1];
870
910
  if (minVal === undefined)
871
911
  minVal = values[i];
@@ -886,6 +926,7 @@ export class Image {
886
926
  }
887
927
  roundCorners(radiusOrOptions) {
888
928
  let radius = (isOptions(radiusOrOptions) ? radiusOrOptions.radius : radiusOrOptions) ?? Math.min(this.width, this.height) / 4;
929
+ radius = finite(radius, 'radius');
889
930
  radius = Math.max(0, Math.min(radius, this.width / 2, this.height / 2));
890
931
  const size = Math.ceil(radius);
891
932
  for (let y = 0; y < size; y++) {
@@ -918,7 +959,7 @@ export class Image {
918
959
  };
919
960
  }
920
961
  fisheye(strengthOrOptions) {
921
- const strength = (isOptions(strengthOrOptions) ? strengthOrOptions.strength : strengthOrOptions) ?? 2;
962
+ const strength = finite((isOptions(strengthOrOptions) ? strengthOrOptions.strength : strengthOrOptions) ?? 2, 'strength');
922
963
  const r = new Image(this.width, this.height);
923
964
  const w = this.width;
924
965
  const h = this.height;
@@ -951,7 +992,7 @@ export class Image {
951
992
  ({ compression = 1, metadata = {} } = compressionOrOptions);
952
993
  else
953
994
  metadata = compressionOrOptions;
954
- const level = Math.min(9, Math.max(0, ~~compression));
995
+ const level = Math.min(9, Math.max(0, ~~finite(compression, 'compression level')));
955
996
  return png.encodeRGBA(this.__bytes__(), this.width, this.height, level, pngText(resolveMetadata(metadata)));
956
997
  }
957
998
  encodePNG(compressionOrOptions, metadata) {
@@ -960,37 +1001,76 @@ export class Image {
960
1001
  async encodeJPEG(qualityOrOptions = 90) {
961
1002
  const { quality = 90, metadata = {} } = isOptions(qualityOrOptions) ? qualityOrOptions : { quality: qualityOrOptions };
962
1003
  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 });
964
- }
965
- async encodeWebP({ metadata = {} } = {}) {
966
- return webp.encode(this.__bytes__(), this.width, this.height, exifData(resolveMetadata(metadata)));
1004
+ return jpeg.encode(this.__bytes__(), this.width, this.height, finite(quality, 'quality'), { exif: exifData(m), comment: m.comment ? utf8(m.comment) : undefined });
1005
+ }
1006
+ async encodeWebP({ quality, effort = 4, metadata = {} } = {}) {
1007
+ const exif = exifData(resolveMetadata(metadata));
1008
+ if (quality === undefined)
1009
+ return webp.encode(this.__bytes__(), this.width, this.height, exif);
1010
+ const q = finite(quality, 'quality');
1011
+ if (q < 0 || q > 100)
1012
+ throw new RangeError(`quality must be 0-100, not ${q}`);
1013
+ if (!Number.isInteger(effort) || effort < 0 || effort > 6)
1014
+ throw new RangeError(`effort must be an integer 0-6, not ${effort}`);
1015
+ return webp.encodeLossy(this.__bytes__(), this.width, this.height, q, exif, effort);
967
1016
  }
968
1017
  async encodeTGA({ metadata = {} } = {}) {
969
1018
  const { author, comment, creationTime: time, software } = resolveMetadata(metadata);
970
1019
  return tga.encode(this.__bytes__(), this.width, this.height, { author, comment, time, software });
971
1020
  }
1021
+ async encodeBMP() {
1022
+ return bmp.encode(this.__bytes__(), this.width, this.height);
1023
+ }
1024
+ async encodeQOI() {
1025
+ return qoi.encode(this.__bytes__(), this.width, this.height);
1026
+ }
1027
+ async encodePNM() {
1028
+ return netpbm.encode(this.__bytes__(), this.width, this.height);
1029
+ }
972
1030
  static async decode(input, options = {}) {
973
- const { applyOrientation = true } = options;
1031
+ const { applyOrientation = true, maxPixels = MAX_PIXELS, toneMapping = 'reinhard' } = options;
1032
+ if (toneMapping !== 'reinhard' && toneMapping !== 'clamp')
1033
+ throw new TypeError(`toneMapping must be 'reinhard' or 'clamp', not ${String(toneMapping)}`);
974
1034
  const data = view(input);
975
1035
  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
1036
+ const decoded = await decoding(() => {
1037
+ if (ImageType.isPNG(dataView))
1038
+ return png.decode(data, maxPixels);
1039
+ if (ImageType.isJPEG(dataView))
1040
+ return jpeg.decode(data, { maxPixels });
1041
+ if (ImageType.isTIFF(dataView))
1042
+ return tiff.decode(data, maxPixels);
1043
+ if (ImageType.isWebP(dataView)) {
1044
+ if (webp.isAnimated(data))
1045
+ throw new Error('Animated WebPs have to be decoded with Animation.decode()');
1046
+ return webp.decode(data, maxPixels);
1047
+ }
1048
+ const bitmap = async (decoded) => {
1049
+ if (!('embedded' in decoded))
1050
+ return decoded;
1051
+ const { width, height, pixels } = decoded.embedded === 'png' ? await png.decode(decoded.data, maxPixels) : jpeg.decode(decoded.data, { maxPixels });
1052
+ return { width, height, pixels };
1053
+ };
1054
+ if (ImageType.isAVIF(dataView))
1055
+ return avif.decode(data, maxPixels, toneMapping);
1056
+ if (ImageType.isJXL(dataView))
1057
+ return jxl.decode(data, maxPixels, toneMapping);
1058
+ if (ImageType.isBMP(dataView))
1059
+ return bitmap(bmp.decode(data, maxPixels));
1060
+ if (ImageType.isICO(dataView))
1061
+ return bitmap(ico.decode(data, maxPixels));
1062
+ if (ImageType.isQOI(dataView))
1063
+ return qoi.decode(data, maxPixels);
1064
+ if (ImageType.isPNM(dataView))
1065
+ return netpbm.decode(data, maxPixels, toneMapping);
1066
+ if (ImageType.isHDR(dataView))
1067
+ return hdr.decode(data, maxPixels, toneMapping);
1068
+ if (ImageType.isTGA(dataView))
1069
+ return tga.decode(data, maxPixels);
991
1070
  throw new Error('Unsupported image type');
1071
+ });
992
1072
  const image = new Image(decoded.width, decoded.height);
993
- image.bitmap.set(decoded.pixels);
1073
+ image.bitmap = decoded.pixels;
994
1074
  return applyOrientation && decoded.orientation ? image.__orient__(decoded.orientation) : image;
995
1075
  }
996
1076
  __orient__(orientation) {
@@ -1064,39 +1144,67 @@ export class Image {
1064
1144
  static async renderText(font, size, text, colorOrOptions = 0xffffffff, layoutArgument, styleArgument = {}) {
1065
1145
  const options = isOptions(colorOrOptions) ? colorOrOptions : { ...styleArgument, color: colorOrOptions, layout: layoutArgument };
1066
1146
  const { color = 0xffffffff, layout = new TextLayout(), strokeWidth = 0, strokeColor = 0x000000ff, hinting = true } = options;
1067
- if (isNaN(strokeWidth) || strokeWidth < 0)
1147
+ size = finite(size, 'font size');
1148
+ if (finite(strokeWidth, 'stroke width') < 0)
1068
1149
  throw new RangeError('Invalid stroke width');
1069
- const rendered = textRenderer.render(view(font), size, text, layout, hinting);
1150
+ const fonts = Array.isArray(font) ? font : [font];
1151
+ if (!fonts.length)
1152
+ throw new RangeError('No font given');
1070
1153
  const [r, g, b, a] = Image.colorToRGBA(color);
1154
+ const rendered = await textRenderer.render(fonts.map(view), size, text, layout, hinting, (r << 24 | g << 16 | b << 8 | 255) >>> 0);
1071
1155
  const pad = Math.ceil(strokeWidth);
1072
1156
  const width = rendered.width + 2 * pad, height = rendered.height + 2 * pad;
1073
1157
  const coverage = new Uint8Array(width * height);
1074
1158
  for (let y = 0; y < rendered.height; y++)
1075
1159
  coverage.set(rendered.coverage.subarray(y * rendered.width, (y + 1) * rendered.width), (y + pad) * width + pad);
1160
+ let colored = null;
1161
+ if (rendered.color) {
1162
+ colored = new Uint8ClampedArray(width * height * 4);
1163
+ for (let y = 0; y < rendered.height; y++)
1164
+ colored.set(rendered.color.subarray(y * rendered.width * 4, (y + 1) * rendered.width * 4), ((y + pad) * width + pad) * 4);
1165
+ }
1076
1166
  const image = new Image(width, height);
1077
1167
  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
- }
1168
+ if (!strokeWidth && !colored) {
1169
+ const pixels = new Uint32Array(bitmap.buffer, bitmap.byteOffset, bitmap.length >> 2), lut = new Uint32Array(256);
1170
+ const bytes = new Uint8ClampedArray(lut.buffer);
1171
+ for (let v = 1; v < 256; v++)
1172
+ bytes.set([r, g, b, v * a / 255], v * 4);
1173
+ for (let i = 0; i < coverage.length; i++)
1174
+ if (coverage[i])
1175
+ pixels[i] = lut[coverage[i]];
1087
1176
  return image;
1088
1177
  }
1089
- const stroke = outline(coverage, width, height, strokeWidth);
1178
+ let shape = coverage;
1179
+ if (colored) {
1180
+ shape = coverage.slice();
1181
+ for (let i = 0; i < shape.length; i++)
1182
+ shape[i] = Math.max(shape[i], colored[i * 4 + 3]);
1183
+ }
1184
+ const stroke = strokeWidth ? outline(shape, width, height, strokeWidth) : null;
1090
1185
  const [sr, sg, sb, sa] = Image.colorToRGBA(strokeColor);
1091
1186
  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)
1187
+ let af = coverage[i] / 255 * a / 255, pr = r * af, pg = g * af, pb = b * af;
1188
+ if (colored && colored[i * 4 + 3]) {
1189
+ const ca = colored[i * 4 + 3] / 255 * a / 255, keep = 1 - ca, f = a / 255;
1190
+ pr = colored[i * 4] * f + pr * keep;
1191
+ pg = colored[i * 4 + 1] * f + pg * keep;
1192
+ pb = colored[i * 4 + 2] * f + pb * keep;
1193
+ af = ca + af * keep;
1194
+ }
1195
+ if (stroke) {
1196
+ const as = stroke[i] * sa / 255 * (1 - shape[i] / 255);
1197
+ pr += sr * as;
1198
+ pg += sg * as;
1199
+ pb += sb * as;
1200
+ af += as;
1201
+ }
1202
+ if (af <= 0)
1095
1203
  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;
1204
+ bitmap[i * 4] = pr / af;
1205
+ bitmap[i * 4 + 1] = pg / af;
1206
+ bitmap[i * 4 + 2] = pb / af;
1207
+ bitmap[i * 4 + 3] = af * 255;
1100
1208
  }
1101
1209
  return image;
1102
1210
  }
@@ -1138,8 +1246,9 @@ function hue2rgb(p, q, t) {
1138
1246
  return p + (q - p) * (2 / 3 - t) * 6;
1139
1247
  return p;
1140
1248
  }
1141
- function resizeMode(modeOrOptions) {
1142
- return (isOptions(modeOrOptions) ? modeOrOptions.mode : modeOrOptions) ?? Image.RESIZE_BILINEAR;
1249
+ function resizeOptions(modeOrOptions) {
1250
+ const options = isOptions(modeOrOptions) ? modeOrOptions : { mode: modeOrOptions };
1251
+ return { mode: options.mode ?? Image.RESIZE_BILINEAR, fast: options.fast ?? true };
1143
1252
  }
1144
1253
  const hardLight = (b, s) => s <= 0.5 ? b * 2 * s : 1 - (1 - b) * (1 - (2 * s - 1));
1145
1254
  const BLEND_MODES = {
@@ -1162,37 +1271,80 @@ const BLEND_MODES = {
1162
1271
  'exclusion': (b, s) => b + s - 2 * b * s,
1163
1272
  'add': (b, s) => Math.min(1, b + s)
1164
1273
  };
1274
+ const OUTLINE_WINDOW = 1;
1165
1275
  function outline(coverage, width, height, radius) {
1166
1276
  const count = width * height;
1167
1277
  const distance = new Float64Array(count);
1168
1278
  for (let i = 0; i < count; i++)
1169
1279
  distance[i] = coverage[i] >= 128 ? 0 : Infinity;
1280
+ const nearestY = new Int32Array(count), nearestX = new Int32Array(count);
1170
1281
  const size = Math.max(width, height);
1171
- const line = new Float64Array(size), result = new Float64Array(size);
1282
+ const line = new Float64Array(size), result = new Float64Array(size), nearest = new Int32Array(size);
1172
1283
  const parabolas = new Int32Array(size), bounds = new Float64Array(size + 1);
1173
1284
  for (let x = 0; x < width; x++) {
1174
1285
  for (let y = 0; y < height; y++)
1175
1286
  line[y] = distance[y * width + x];
1176
- distanceTransform(line, height, result, parabolas, bounds);
1177
- for (let y = 0; y < height; y++)
1287
+ distanceTransform(line, height, result, parabolas, bounds, nearest);
1288
+ for (let y = 0; y < height; y++) {
1178
1289
  distance[y * width + x] = result[y];
1290
+ nearestY[y * width + x] = nearest[y];
1291
+ }
1179
1292
  }
1293
+ const far = (radius + 2) ** 2;
1180
1294
  for (let y = 0; y < height; y++) {
1181
1295
  const row = y * width;
1182
1296
  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++)
1297
+ line[x] = distance[row + x] < far ? distance[row + x] : Infinity;
1298
+ distanceTransform(line, width, result, parabolas, bounds, nearest);
1299
+ for (let x = 0; x < width; x++) {
1186
1300
  distance[row + x] = result[x];
1301
+ nearestX[row + x] = nearest[x];
1302
+ }
1187
1303
  }
1304
+ const pointX = new Float32Array(count), pointY = new Float32Array(count), isEdge = new Uint8Array(count);
1305
+ for (let y = 1; y < height - 1; y++)
1306
+ for (let x = 1, e = y * width + 1; x < width - 1; x++, e++) {
1307
+ const c = coverage[e];
1308
+ if (!c || c === 255 && coverage[e - 1] >= 128 && coverage[e + 1] >= 128 && coverage[e - width] >= 128 && coverage[e + width] >= 128)
1309
+ continue;
1310
+ const above = e - width, below = e + width;
1311
+ const gx = coverage[above + 1] + 2 * coverage[e + 1] + coverage[below + 1] - coverage[above - 1] - 2 * coverage[e - 1] - coverage[below - 1];
1312
+ const gy = coverage[below - 1] + 2 * coverage[below] + coverage[below + 1] - coverage[above - 1] - 2 * coverage[above] - coverage[above + 1];
1313
+ const length = Math.sqrt(gx * gx + gy * gy);
1314
+ if (!length)
1315
+ continue;
1316
+ const outside = 0.5 - c / 255;
1317
+ pointX[e] = x + outside * gx / length;
1318
+ pointY[e] = y + outside * gy / length;
1319
+ isEdge[e] = 1;
1320
+ }
1188
1321
  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
- }
1322
+ const inner = radius - 1.5, near = inner > 0 ? inner * inner : -1;
1323
+ for (let y = 0, i = 0; y < height; y++)
1324
+ for (let x = 0; x < width; x++, i++) {
1325
+ const d2 = distance[i];
1326
+ if (d2 <= near)
1327
+ out[i] = 1;
1328
+ if (d2 <= near || d2 >= far) {
1329
+ if (d2 >= far)
1330
+ out[i] = coverage[i] / 255;
1331
+ continue;
1332
+ }
1333
+ const qx = nearestX[i], qy = nearestY[y * width + qx];
1334
+ let closest = Infinity;
1335
+ for (let wy = Math.max(0, qy - OUTLINE_WINDOW); wy <= Math.min(height - 1, qy + OUTLINE_WINDOW); wy++)
1336
+ for (let wx = Math.max(0, qx - OUTLINE_WINDOW), e = wy * width + wx; wx <= Math.min(width - 1, qx + OUTLINE_WINDOW); wx++, e++) {
1337
+ if (!isEdge[e])
1338
+ continue;
1339
+ const ex = x - pointX[e], ey = y - pointY[e];
1340
+ closest = Math.min(closest, ex * ex + ey * ey);
1341
+ }
1342
+ closest = closest === Infinity ? Math.sqrt(d2) - 0.5 : Math.sqrt(closest);
1343
+ out[i] = Math.max(coverage[i] / 255, Math.min(1, Math.max(0, radius + 0.5 - closest)));
1344
+ }
1193
1345
  return out;
1194
1346
  }
1195
- function distanceTransform(f, n, d, v, z) {
1347
+ function distanceTransform(f, n, d, v, z, nearest) {
1196
1348
  let k = -1;
1197
1349
  for (let q = 0; q < n; q++) {
1198
1350
  if (f[q] === Infinity)
@@ -1217,5 +1369,6 @@ function distanceTransform(f, n, d, v, z) {
1217
1369
  while (z[j + 1] < q)
1218
1370
  j++;
1219
1371
  d[q] = (q - v[j]) ** 2 + f[v[j]];
1372
+ nearest[q] = v[j];
1220
1373
  }
1221
1374
  }