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