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.
- package/README.md +35 -10
- package/THIRD_PARTY_NOTICES.md +298 -2
- package/dist/animation.d.ts +25 -8
- package/dist/animation.js +44 -13
- package/dist/codecs/av1/cdef.js +197 -0
- package/dist/codecs/av1/data.js +141 -0
- package/dist/codecs/av1/decode.js +1769 -0
- package/dist/codecs/av1/filmgrain.js +219 -0
- package/dist/codecs/av1/frame.js +114 -0
- package/dist/codecs/av1/ipred.js +491 -0
- package/dist/codecs/av1/itx.js +950 -0
- package/dist/codecs/av1/loopfilter.js +149 -0
- package/dist/codecs/av1/msac.js +161 -0
- package/dist/codecs/av1/obu.js +516 -0
- package/dist/codecs/av1/postfilter.js +28 -0
- package/dist/codecs/av1/restoration.js +193 -0
- package/dist/codecs/av1/tables.js +160 -0
- package/dist/codecs/avif.js +581 -0
- package/dist/codecs/bmp.js +317 -0
- package/dist/codecs/crc.js +5 -0
- package/dist/codecs/gif.js +10 -35
- package/dist/codecs/hdr.d.ts +25 -0
- package/dist/codecs/hdr.js +201 -0
- package/dist/codecs/ico.js +48 -0
- package/dist/codecs/jpeg.js +11 -28
- package/dist/codecs/jxl/bits.js +126 -0
- package/dist/codecs/jxl/blend.js +117 -0
- package/dist/codecs/jxl/color.js +116 -0
- package/dist/codecs/jxl/dct.js +404 -0
- package/dist/codecs/jxl/entropy.js +556 -0
- package/dist/codecs/jxl/features.js +325 -0
- package/dist/codecs/jxl/filters.js +119 -0
- package/dist/codecs/jxl/frame-header.js +195 -0
- package/dist/codecs/jxl/frame.js +319 -0
- package/dist/codecs/jxl/headers.js +249 -0
- package/dist/codecs/jxl/idct.js +484 -0
- package/dist/codecs/jxl/modular.js +688 -0
- package/dist/codecs/jxl/quant.js +219 -0
- package/dist/codecs/jxl/upsample.js +52 -0
- package/dist/codecs/jxl/vardct.js +472 -0
- package/dist/codecs/jxl.js +89 -0
- package/dist/codecs/netpbm.js +239 -0
- package/dist/codecs/png.js +22 -10
- package/dist/codecs/qoi.js +150 -0
- package/dist/codecs/tga.js +3 -1
- package/dist/codecs/tiff.js +7 -2
- package/dist/codecs/vp8-enc-dsp.js +932 -0
- package/dist/codecs/vp8-enc-tables.js +78 -0
- package/dist/codecs/vp8-enc.js +1930 -0
- package/dist/codecs/vp8.js +3 -1
- package/dist/codecs/vp8l.js +126 -58
- package/dist/codecs/webp.js +47 -11
- 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-type.d.ts +15 -1
- package/dist/image-type.js +45 -2
- package/dist/image.d.ts +89 -13
- package/dist/image.js +266 -113
- package/dist/index.d.ts +3 -2
- 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 +38 -22
- 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 +102 -23
- package/dist/render/text.js +373 -103
- package/dist/render/unicode-data.js +63 -0
- package/dist/render/unicode.js +50 -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 +40 -0
- package/dist/util/limits.js +34 -0
- package/dist/util/options.js +7 -0
- package/dist/util/resample.js +126 -7
- 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 +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
|
|
101
|
-
g
|
|
102
|
-
b
|
|
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
|
|
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 =
|
|
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
|
|
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))),
|
|
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
|
|
255
|
+
const options = resizeOptions(modeOrOptions);
|
|
236
256
|
const result = new Image(width, height);
|
|
237
|
-
this.contain(width, height,
|
|
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
|
|
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)),
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
474
|
-
y
|
|
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 (
|
|
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 (
|
|
659
|
+
if (!Number.isFinite(amount) || amount < 0)
|
|
627
660
|
throw new RangeError('Invalid sharpen amount');
|
|
628
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
905
|
+
const minPos = positions[i - 1];
|
|
866
906
|
if (minPos === undefined)
|
|
867
907
|
continue;
|
|
868
|
-
|
|
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
|
-
|
|
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
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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
|
|
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
|
-
|
|
1147
|
+
size = finite(size, 'font size');
|
|
1148
|
+
if (finite(strokeWidth, 'stroke width') < 0)
|
|
1068
1149
|
throw new RangeError('Invalid stroke width');
|
|
1069
|
-
const
|
|
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
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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] =
|
|
1097
|
-
bitmap[i * 4 + 1] =
|
|
1098
|
-
bitmap[i * 4 + 2] =
|
|
1099
|
-
bitmap[i * 4 + 3] =
|
|
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
|
|
1142
|
-
|
|
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
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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
|
}
|