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.
- package/README.md +8 -3
- package/THIRD_PARTY_NOTICES.md +131 -0
- package/dist/animation.d.ts +7 -0
- package/dist/animation.js +21 -11
- package/dist/codecs/crc.js +5 -0
- package/dist/codecs/gif.js +10 -35
- package/dist/codecs/jpeg.js +9 -27
- package/dist/codecs/png.js +22 -10
- package/dist/codecs/tga.js +3 -1
- package/dist/codecs/tiff.js +7 -2
- package/dist/codecs/vp8.js +3 -1
- package/dist/codecs/vp8l.js +126 -58
- package/dist/codecs/webp.js +11 -9
- package/dist/codecs/zlib.js +81 -69
- package/dist/frame.d.ts +1 -0
- package/dist/frame.js +4 -2
- package/dist/gif.d.ts +1 -0
- package/dist/gif.js +2 -2
- package/dist/image.d.ts +42 -7
- package/dist/image.js +217 -110
- package/dist/index.js +4 -4
- package/dist/render/bidi.js +350 -0
- package/dist/render/buffer.js +362 -0
- package/dist/render/color.js +164 -0
- package/dist/render/colr.js +523 -0
- package/dist/render/font.js +173 -241
- package/dist/render/hinting.js +12 -4
- package/dist/render/layout.js +1553 -0
- package/dist/render/linebreak.js +210 -0
- package/dist/render/segment.js +106 -0
- package/dist/render/shape.js +984 -0
- package/dist/render/shapers/arabic.js +284 -0
- package/dist/render/shapers/hangul.js +142 -0
- package/dist/render/shapers/hebrew.js +49 -0
- package/dist/render/shapers/index.js +52 -0
- package/dist/render/shapers/indic.js +579 -0
- package/dist/render/shapers/khmer.js +104 -0
- package/dist/render/shapers/machine.js +129 -0
- package/dist/render/shapers/myanmar.js +121 -0
- package/dist/render/shapers/syllabic.js +100 -0
- package/dist/render/shapers/thai.js +125 -0
- package/dist/render/shapers/use.js +235 -0
- package/dist/render/svg.js +101 -22
- package/dist/render/text.js +373 -103
- package/dist/render/unicode-data.js +63 -0
- package/dist/render/unicode.js +58 -0
- package/dist/text-layout.d.ts +4 -1
- package/dist/text-layout.js +2 -2
- package/dist/util/alpha.js +43 -0
- package/dist/util/blur.js +20 -33
- package/dist/util/bytes.js +31 -0
- package/dist/util/limits.js +34 -0
- package/dist/util/options.js +7 -0
- package/dist/util/resample.js +122 -5
- package/dist/util/rotate.js +43 -0
- package/dist/version.js +1 -1
- package/licenses/HarfBuzz.txt +42 -0
- package/licenses/Unicode-3.0.txt +39 -0
- 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
|
|
101
|
-
g
|
|
102
|
-
b
|
|
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
|
|
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 =
|
|
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
|
|
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))),
|
|
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
|
|
248
|
+
const options = resizeOptions(modeOrOptions);
|
|
236
249
|
const result = new Image(width, height);
|
|
237
|
-
this.contain(width, height,
|
|
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
|
|
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)),
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
474
|
-
y
|
|
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 (
|
|
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 (
|
|
652
|
+
if (!Number.isFinite(amount) || amount < 0)
|
|
627
653
|
throw new RangeError('Invalid sharpen amount');
|
|
628
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
898
|
+
const minPos = positions[i - 1];
|
|
866
899
|
if (minPos === undefined)
|
|
867
900
|
continue;
|
|
868
|
-
|
|
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
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
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
|
|
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
|
-
|
|
1101
|
+
size = finite(size, 'font size');
|
|
1102
|
+
if (finite(strokeWidth, 'stroke width') < 0)
|
|
1068
1103
|
throw new RangeError('Invalid stroke width');
|
|
1069
|
-
const
|
|
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
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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] =
|
|
1097
|
-
bitmap[i * 4 + 1] =
|
|
1098
|
-
bitmap[i * 4 + 2] =
|
|
1099
|
-
bitmap[i * 4 + 3] =
|
|
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
|
|
1142
|
-
|
|
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
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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
|
}
|