imagescript 3.1.0 → 3.2.1
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 +12 -4
- package/dist/animation.d.ts +92 -7
- package/dist/animation.js +50 -20
- package/dist/codecs/exif.js +62 -0
- package/dist/codecs/gif.js +43 -27
- package/dist/codecs/jpeg.js +13 -1
- package/dist/codecs/png.js +85 -21
- package/dist/codecs/quantize.js +2 -2
- package/dist/codecs/tga.js +47 -4
- package/dist/codecs/vp8l.js +27 -30
- package/dist/codecs/webp.js +23 -10
- package/dist/frame.d.ts +30 -2
- package/dist/frame.js +20 -9
- package/dist/gif.d.ts +10 -7
- package/dist/gif.js +7 -5
- package/dist/image-type.d.ts +1 -0
- package/dist/image-type.js +1 -0
- package/dist/image.d.ts +231 -31
- package/dist/image.js +118 -53
- package/dist/index.d.ts +14 -4
- package/dist/index.js +6 -4
- package/dist/metadata.d.ts +35 -0
- package/dist/metadata.js +43 -0
- package/dist/render/svg.js +39 -21
- package/dist/util/blur.js +88 -0
- package/dist/util/optimize.js +264 -0
- package/dist/util/options.js +3 -0
- package/dist/util/resample.js +244 -63
- package/dist/util/utf8.js +4 -51
- package/dist/version.js +1 -1
- package/package.json +9 -2
- package/dist/codecs/crc.d.ts +0 -9
- package/dist/codecs/exif.d.ts +0 -11
- package/dist/codecs/gif.d.ts +0 -44
- package/dist/codecs/jpeg.d.ts +0 -35
- package/dist/codecs/png.d.ts +0 -97
- package/dist/codecs/quantize.d.ts +0 -25
- package/dist/codecs/tga.d.ts +0 -16
- package/dist/codecs/tiff.d.ts +0 -6
- package/dist/codecs/vp8-tables.d.ts +0 -10
- package/dist/codecs/vp8.d.ts +0 -8
- package/dist/codecs/vp8l.d.ts +0 -62
- package/dist/codecs/webp.d.ts +0 -67
- package/dist/codecs/zlib.d.ts +0 -22
- package/dist/render/font.d.ts +0 -92
- package/dist/render/hinting.d.ts +0 -110
- package/dist/render/svg.d.ts +0 -11
- package/dist/render/text.d.ts +0 -63
- package/dist/util/blur.d.ts +0 -13
- package/dist/util/resample.d.ts +0 -12
- package/dist/util/utf8.d.ts +0 -5
- package/dist/version.d.ts +0 -1
package/dist/image.js
CHANGED
|
@@ -9,9 +9,11 @@ import * as svgRenderer from "./render/svg.js";
|
|
|
9
9
|
import { view } from "./util/mem.js";
|
|
10
10
|
import { ImageType } from "./image-type.js";
|
|
11
11
|
import { TextLayout } from "./text-layout.js";
|
|
12
|
-
import {
|
|
12
|
+
import { resolveMetadata, pngText, exifData } from "./metadata.js";
|
|
13
|
+
import { encode as utf8 } from "./util/utf8.js";
|
|
13
14
|
import { resample, BOX, TRIANGLE, CUBIC } from "./util/resample.js";
|
|
14
|
-
import { gaussianBlur, premultiply, unpremultiply } from "./util/blur.js";
|
|
15
|
+
import { gaussianBlur, boxBlurRGBA, premultiply, unpremultiply } from "./util/blur.js";
|
|
16
|
+
import { isOptions } from "./util/options.js";
|
|
15
17
|
export class Image {
|
|
16
18
|
__width__;
|
|
17
19
|
__height__;
|
|
@@ -187,8 +189,10 @@ export class Image {
|
|
|
187
189
|
return this;
|
|
188
190
|
}
|
|
189
191
|
clone() {
|
|
190
|
-
const image = new Image(
|
|
191
|
-
image.
|
|
192
|
+
const image = new Image(1, 1);
|
|
193
|
+
image.__width__ = this.width;
|
|
194
|
+
image.__height__ = this.height;
|
|
195
|
+
image.__set_bitmap__(new Uint8ClampedArray(this.bitmap));
|
|
192
196
|
return image;
|
|
193
197
|
}
|
|
194
198
|
static get RESIZE_NEAREST_NEIGHBOR() {
|
|
@@ -206,8 +210,8 @@ export class Image {
|
|
|
206
210
|
static get RESIZE_AUTO() {
|
|
207
211
|
return -1;
|
|
208
212
|
}
|
|
209
|
-
scale(factor,
|
|
210
|
-
const image = this.__scale__(factor,
|
|
213
|
+
scale(factor, modeOrOptions) {
|
|
214
|
+
const image = this.__scale__(factor, resizeMode(modeOrOptions));
|
|
211
215
|
return this.__apply__(image);
|
|
212
216
|
}
|
|
213
217
|
__scale__(factor, mode = Image.RESIZE_BILINEAR) {
|
|
@@ -215,23 +219,27 @@ export class Image {
|
|
|
215
219
|
return this;
|
|
216
220
|
return this.__resize__(this.width * factor, this.height * factor, mode);
|
|
217
221
|
}
|
|
218
|
-
resize(width, height,
|
|
222
|
+
resize(width, height, modeOrOptions) {
|
|
223
|
+
const mode = resizeMode(modeOrOptions);
|
|
219
224
|
const image = this.__resize__(width, height, mode);
|
|
220
225
|
return this.__apply__(image);
|
|
221
226
|
}
|
|
222
|
-
contain(width, height,
|
|
227
|
+
contain(width, height, modeOrOptions) {
|
|
228
|
+
const mode = resizeMode(modeOrOptions);
|
|
223
229
|
width = Math.floor(width);
|
|
224
230
|
height = Math.floor(height);
|
|
225
231
|
const scale = Math.min(width / this.width, height / this.height);
|
|
226
232
|
return this.resize(Math.max(1, Math.min(width, Math.round(this.width * scale))), Math.max(1, Math.min(height, Math.round(this.height * scale))), mode);
|
|
227
233
|
}
|
|
228
|
-
fit(width, height,
|
|
234
|
+
fit(width, height, modeOrOptions) {
|
|
235
|
+
const mode = resizeMode(modeOrOptions);
|
|
229
236
|
const result = new Image(width, height);
|
|
230
237
|
this.contain(width, height, mode);
|
|
231
238
|
result.composite(this, (width - this.width) / 2, (height - this.height) / 2);
|
|
232
239
|
return this.__apply__(result);
|
|
233
240
|
}
|
|
234
|
-
cover(width, height,
|
|
241
|
+
cover(width, height, modeOrOptions) {
|
|
242
|
+
const mode = resizeMode(modeOrOptions);
|
|
235
243
|
width = Math.floor(width);
|
|
236
244
|
height = Math.floor(height);
|
|
237
245
|
const scale = Math.max(width / this.width, height / this.height);
|
|
@@ -282,6 +290,51 @@ export class Image {
|
|
|
282
290
|
throw new RangeError('Crop region does not overlap the image');
|
|
283
291
|
return this.__apply__(this.__crop__(left, top, right - left, bottom - top));
|
|
284
292
|
}
|
|
293
|
+
trim(color, options = {}) {
|
|
294
|
+
const bounds = this.__trim_bounds__(color, options);
|
|
295
|
+
return bounds ? this.__apply__(this.__crop__(...bounds)) : this;
|
|
296
|
+
}
|
|
297
|
+
__trim_bounds__(color, { tolerance = 0 }) {
|
|
298
|
+
if (!(tolerance >= 0 && tolerance <= 1))
|
|
299
|
+
throw new RangeError('Invalid tolerance, it has to be between 0 and 1');
|
|
300
|
+
const bitmap = this.bitmap;
|
|
301
|
+
const { width, height } = this;
|
|
302
|
+
const c = color === undefined ? [bitmap[0], bitmap[1], bitmap[2], bitmap[3]] : [color >>> 24, color >> 16 & 255, color >> 8 & 255, color & 255];
|
|
303
|
+
const limit = tolerance * 255;
|
|
304
|
+
const matches = (x, y) => {
|
|
305
|
+
const i = (y * width + x) * 4;
|
|
306
|
+
if (Math.abs(bitmap[i + 3] - c[3]) > limit)
|
|
307
|
+
return false;
|
|
308
|
+
if (bitmap[i + 3] === 0 && c[3] === 0)
|
|
309
|
+
return true;
|
|
310
|
+
return Math.abs(bitmap[i] - c[0]) <= limit && Math.abs(bitmap[i + 1] - c[1]) <= limit && Math.abs(bitmap[i + 2] - c[2]) <= limit;
|
|
311
|
+
};
|
|
312
|
+
const rowMatches = (y) => {
|
|
313
|
+
for (let x = 0; x < width; x++)
|
|
314
|
+
if (!matches(x, y))
|
|
315
|
+
return false;
|
|
316
|
+
return true;
|
|
317
|
+
};
|
|
318
|
+
const columnMatches = (x, top, bottom) => {
|
|
319
|
+
for (let y = top; y < bottom; y++)
|
|
320
|
+
if (!matches(x, y))
|
|
321
|
+
return false;
|
|
322
|
+
return true;
|
|
323
|
+
};
|
|
324
|
+
let top = 0, bottom = height;
|
|
325
|
+
while (top < height && rowMatches(top))
|
|
326
|
+
top++;
|
|
327
|
+
if (top === height)
|
|
328
|
+
return null;
|
|
329
|
+
while (rowMatches(bottom - 1))
|
|
330
|
+
bottom--;
|
|
331
|
+
let left = 0, right = width;
|
|
332
|
+
while (columnMatches(left, top, bottom))
|
|
333
|
+
left++;
|
|
334
|
+
while (columnMatches(right - 1, top, bottom))
|
|
335
|
+
right--;
|
|
336
|
+
return [left, top, right - left, bottom - top];
|
|
337
|
+
}
|
|
285
338
|
__crop__(x, y, width, height) {
|
|
286
339
|
x = ~~x;
|
|
287
340
|
y = ~~y;
|
|
@@ -369,25 +422,29 @@ export class Image {
|
|
|
369
422
|
}
|
|
370
423
|
return this;
|
|
371
424
|
}
|
|
372
|
-
opacity(opacity,
|
|
425
|
+
opacity(opacity, absoluteOrOptions = false) {
|
|
426
|
+
const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
|
|
373
427
|
if (isNaN(opacity) || opacity < 0)
|
|
374
428
|
throw new RangeError('Invalid opacity value');
|
|
375
429
|
this.__set_channel_value__(opacity, absolute, 3);
|
|
376
430
|
return this;
|
|
377
431
|
}
|
|
378
|
-
red(factor,
|
|
432
|
+
red(factor, absoluteOrOptions = false) {
|
|
433
|
+
const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
|
|
379
434
|
if (isNaN(factor) || factor < 0)
|
|
380
435
|
throw new RangeError('Invalid red factor');
|
|
381
436
|
this.__set_channel_value__(factor, absolute, 0);
|
|
382
437
|
return this;
|
|
383
438
|
}
|
|
384
|
-
green(factor,
|
|
439
|
+
green(factor, absoluteOrOptions = false) {
|
|
440
|
+
const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
|
|
385
441
|
if (isNaN(factor) || factor < 0)
|
|
386
442
|
throw new RangeError('Invalid green factor');
|
|
387
443
|
this.__set_channel_value__(factor, absolute, 1);
|
|
388
444
|
return this;
|
|
389
445
|
}
|
|
390
|
-
blue(factor,
|
|
446
|
+
blue(factor, absoluteOrOptions = false) {
|
|
447
|
+
const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
|
|
391
448
|
if (isNaN(factor) || factor < 0)
|
|
392
449
|
throw new RangeError('Invalid blue factor');
|
|
393
450
|
this.__set_channel_value__(factor, absolute, 2);
|
|
@@ -397,18 +454,22 @@ export class Image {
|
|
|
397
454
|
for (let i = offset; i < this.bitmap.length; i += 4)
|
|
398
455
|
this.bitmap[i] = value * (absolute ? 255 : this.bitmap[i]);
|
|
399
456
|
}
|
|
400
|
-
lightness(value,
|
|
457
|
+
lightness(value, absoluteOrOptions = false) {
|
|
458
|
+
const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
|
|
401
459
|
if (isNaN(value) || value < 0)
|
|
402
460
|
throw new RangeError('Invalid lightness value');
|
|
403
461
|
return this.__map_hsla__(c => { c.l = value * (absolute ? 1 : c.l); });
|
|
404
462
|
}
|
|
405
|
-
saturation(value,
|
|
463
|
+
saturation(value, absoluteOrOptions = false) {
|
|
464
|
+
const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
|
|
406
465
|
if (isNaN(value) || value < 0)
|
|
407
466
|
throw new RangeError('Invalid saturation value');
|
|
408
467
|
return this.__map_hsla__(c => { c.s = value * (absolute ? 1 : c.s); });
|
|
409
468
|
}
|
|
410
|
-
composite(source,
|
|
469
|
+
composite(source, xOrOptions = 0, yArgument = 0, optionsArgument = {}) {
|
|
470
|
+
const options = isOptions(xOrOptions) ? xOrOptions : optionsArgument;
|
|
411
471
|
const { opacity = 1, mode = 'normal' } = options;
|
|
472
|
+
let x = isOptions(xOrOptions) ? xOrOptions.x ?? 0 : xOrOptions, y = isOptions(xOrOptions) ? xOrOptions.y ?? 0 : yArgument;
|
|
412
473
|
x |= 0;
|
|
413
474
|
y |= 0;
|
|
414
475
|
if (isNaN(opacity) || opacity < 0 || opacity > 1)
|
|
@@ -553,12 +614,15 @@ export class Image {
|
|
|
553
614
|
blur(radius) {
|
|
554
615
|
if (isNaN(radius) || radius < 0)
|
|
555
616
|
throw new RangeError('Invalid blur radius');
|
|
617
|
+
if (boxBlurRGBA(this.bitmap, this.width, this.height, radius))
|
|
618
|
+
return this;
|
|
556
619
|
const pixels = premultiply(this.bitmap);
|
|
557
620
|
gaussianBlur(pixels, this.width, this.height, radius);
|
|
558
621
|
unpremultiply(pixels, this.bitmap);
|
|
559
622
|
return this;
|
|
560
623
|
}
|
|
561
|
-
sharpen(
|
|
624
|
+
sharpen(amountOrOptions = 1, radiusArgument = 1) {
|
|
625
|
+
const { amount = 1, radius = 1 } = isOptions(amountOrOptions) ? amountOrOptions : { amount: amountOrOptions, radius: radiusArgument };
|
|
562
626
|
if (isNaN(amount) || amount < 0)
|
|
563
627
|
throw new RangeError('Invalid sharpen amount');
|
|
564
628
|
if (isNaN(radius) || radius <= 0)
|
|
@@ -820,7 +884,8 @@ export class Image {
|
|
|
820
884
|
throw new RangeError(`Invalid gradient position: ${position}`);
|
|
821
885
|
};
|
|
822
886
|
}
|
|
823
|
-
roundCorners(
|
|
887
|
+
roundCorners(radiusOrOptions) {
|
|
888
|
+
let radius = (isOptions(radiusOrOptions) ? radiusOrOptions.radius : radiusOrOptions) ?? Math.min(this.width, this.height) / 4;
|
|
824
889
|
radius = Math.max(0, Math.min(radius, this.width / 2, this.height / 2));
|
|
825
890
|
const size = Math.ceil(radius);
|
|
826
891
|
for (let y = 0; y < size; y++) {
|
|
@@ -852,7 +917,8 @@ export class Image {
|
|
|
852
917
|
return (((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | (a & 0xff)) >>> 0;
|
|
853
918
|
};
|
|
854
919
|
}
|
|
855
|
-
fisheye(
|
|
920
|
+
fisheye(strengthOrOptions) {
|
|
921
|
+
const strength = (isOptions(strengthOrOptions) ? strengthOrOptions.strength : strengthOrOptions) ?? 2;
|
|
856
922
|
const r = new Image(this.width, this.height);
|
|
857
923
|
const w = this.width;
|
|
858
924
|
const h = this.height;
|
|
@@ -877,37 +943,31 @@ export class Image {
|
|
|
877
943
|
}
|
|
878
944
|
return this.__apply__(r);
|
|
879
945
|
}
|
|
880
|
-
async encode(
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
compression =
|
|
884
|
-
|
|
946
|
+
async encode(compressionOrOptions = 1, metadataArgument = {}) {
|
|
947
|
+
let compression = 1, metadata = metadataArgument;
|
|
948
|
+
if (!isOptions(compressionOrOptions))
|
|
949
|
+
compression = compressionOrOptions;
|
|
950
|
+
else if ('compression' in compressionOrOptions || 'metadata' in compressionOrOptions)
|
|
951
|
+
({ compression = 1, metadata = {} } = compressionOrOptions);
|
|
952
|
+
else
|
|
953
|
+
metadata = compressionOrOptions;
|
|
885
954
|
const level = Math.min(9, Math.max(0, ~~compression));
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
return
|
|
902
|
-
}
|
|
903
|
-
async encodeJPEG(quality = 90) {
|
|
904
|
-
return jpeg.encode(this.__bytes__(), this.width, this.height, quality);
|
|
905
|
-
}
|
|
906
|
-
async encodeWebP() {
|
|
907
|
-
return webp.encode(this.__bytes__(), this.width, this.height);
|
|
908
|
-
}
|
|
909
|
-
async encodeTGA() {
|
|
910
|
-
return tga.encode(this.__bytes__(), this.width, this.height);
|
|
955
|
+
return png.encodeRGBA(this.__bytes__(), this.width, this.height, level, pngText(resolveMetadata(metadata)));
|
|
956
|
+
}
|
|
957
|
+
encodePNG(compressionOrOptions, metadata) {
|
|
958
|
+
return this.encode(compressionOrOptions, metadata);
|
|
959
|
+
}
|
|
960
|
+
async encodeJPEG(qualityOrOptions = 90) {
|
|
961
|
+
const { quality = 90, metadata = {} } = isOptions(qualityOrOptions) ? qualityOrOptions : { quality: qualityOrOptions };
|
|
962
|
+
const m = resolveMetadata(metadata);
|
|
963
|
+
return jpeg.encode(this.__bytes__(), this.width, this.height, quality, { exif: exifData(m), comment: m.comment ? utf8(m.comment) : undefined });
|
|
964
|
+
}
|
|
965
|
+
async encodeWebP({ metadata = {} } = {}) {
|
|
966
|
+
return webp.encode(this.__bytes__(), this.width, this.height, exifData(resolveMetadata(metadata)));
|
|
967
|
+
}
|
|
968
|
+
async encodeTGA({ metadata = {} } = {}) {
|
|
969
|
+
const { author, comment, creationTime: time, software } = resolveMetadata(metadata);
|
|
970
|
+
return tga.encode(this.__bytes__(), this.width, this.height, { author, comment, time, software });
|
|
911
971
|
}
|
|
912
972
|
static async decode(input, options = {}) {
|
|
913
973
|
const { applyOrientation = true } = options;
|
|
@@ -988,7 +1048,8 @@ export class Image {
|
|
|
988
1048
|
static get SVG_MODE_HEIGHT() {
|
|
989
1049
|
return 3;
|
|
990
1050
|
}
|
|
991
|
-
static async renderSVG(svg,
|
|
1051
|
+
static async renderSVG(svg, sizeOrOptions = 1, modeArgument = Image.SVG_MODE_SCALE) {
|
|
1052
|
+
const { size = 1, mode = Image.SVG_MODE_SCALE } = isOptions(sizeOrOptions) ? sizeOrOptions : { size: sizeOrOptions, mode: modeArgument };
|
|
992
1053
|
if (![1, 2, 3].includes(mode))
|
|
993
1054
|
throw new Error('Invalid SVG scaling mode');
|
|
994
1055
|
if (mode === Image.SVG_MODE_SCALE && size <= 0)
|
|
@@ -1000,8 +1061,9 @@ export class Image {
|
|
|
1000
1061
|
image.bitmap.set(pixels);
|
|
1001
1062
|
return image;
|
|
1002
1063
|
}
|
|
1003
|
-
static async renderText(font, size, text,
|
|
1004
|
-
const
|
|
1064
|
+
static async renderText(font, size, text, colorOrOptions = 0xffffffff, layoutArgument, styleArgument = {}) {
|
|
1065
|
+
const options = isOptions(colorOrOptions) ? colorOrOptions : { ...styleArgument, color: colorOrOptions, layout: layoutArgument };
|
|
1066
|
+
const { color = 0xffffffff, layout = new TextLayout(), strokeWidth = 0, strokeColor = 0x000000ff, hinting = true } = options;
|
|
1005
1067
|
if (isNaN(strokeWidth) || strokeWidth < 0)
|
|
1006
1068
|
throw new RangeError('Invalid stroke width');
|
|
1007
1069
|
const rendered = textRenderer.render(view(font), size, text, layout, hinting);
|
|
@@ -1076,6 +1138,9 @@ function hue2rgb(p, q, t) {
|
|
|
1076
1138
|
return p + (q - p) * (2 / 3 - t) * 6;
|
|
1077
1139
|
return p;
|
|
1078
1140
|
}
|
|
1141
|
+
function resizeMode(modeOrOptions) {
|
|
1142
|
+
return (isOptions(modeOrOptions) ? modeOrOptions.mode : modeOrOptions) ?? Image.RESIZE_BILINEAR;
|
|
1143
|
+
}
|
|
1079
1144
|
const hardLight = (b, s) => s <= 0.5 ? b * 2 * s : 1 - (1 - b) * (1 - (2 * s - 1));
|
|
1080
1145
|
const BLEND_MODES = {
|
|
1081
1146
|
'normal': (_b, s) => s,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
import { BinaryData } from './util/mem.js';
|
|
2
|
-
import { Image } from './image.js';
|
|
2
|
+
import { Image, DecodeOptions } from './image.js';
|
|
3
3
|
import { GIF } from './gif.js';
|
|
4
|
-
import { Animation } from './animation.js';
|
|
4
|
+
import { Animation, AnimationDecodeOptions } from './animation.js';
|
|
5
5
|
export { Image } from './image.js';
|
|
6
|
-
export type { Color, ColorFunction, ResizeMode, SVGScaleMode, PNGMetadata, TextStyle, DecodeOptions, CompositeOptions, BlendMode, CropCircleOptions, DominantColorOptions, RotateOptions, ColorToAlphaOptions } from './image.js';
|
|
6
|
+
export type { Color, ColorFunction, ResizeMode, SVGScaleMode, PNGMetadata, TextStyle, DecodeOptions, CompositeOptions, BlendMode, CropCircleOptions, DominantColorOptions, RotateOptions, ColorToAlphaOptions, ResizeOptions, AdjustOptions, SharpenOptions, PNGEncodeOptions, JPEGEncodeOptions, EncodeOptions, SVGOptions, TextOptions, RoundCornersOptions, FisheyeOptions, TrimOptions } from './image.js';
|
|
7
7
|
export { Frame } from './frame.js';
|
|
8
|
-
export type { FrameDisposalModeName, FrameDisposalModeId } from './frame.js';
|
|
8
|
+
export type { FrameDisposalModeName, FrameDisposalModeId, FrameOptions } from './frame.js';
|
|
9
9
|
export { GIF } from './gif.js';
|
|
10
10
|
export { Animation } from './animation.js';
|
|
11
|
+
export type { AnimationOptions, AnimationDecodeOptions, AnimationEncodeOptions, GIFEncodeOptions, APNGEncodeOptions } from './animation.js';
|
|
11
12
|
export { TextLayout } from './text-layout.js';
|
|
12
13
|
export type { TextLayoutOptions, WrapStyle, HorizontalAlign, VerticalAlign } from './text-layout.js';
|
|
13
14
|
export { ImageType } from './image-type.js';
|
|
14
15
|
export type { ImageTypeName } from './image-type.js';
|
|
15
16
|
export type { BinaryData } from './util/mem.js';
|
|
17
|
+
export type { Metadata } from './metadata.js';
|
|
18
|
+
/**
|
|
19
|
+
* Decodes any supported image: GIFs are returned as {@link GIF} (an {@link Animation}), animated WebPs as
|
|
20
|
+
* {@link Animation}, everything else (including APNGs, as their default image) as {@link Image}.
|
|
21
|
+
* {@link Animation.decode} decodes the frames of GIFs, APNGs and WebPs
|
|
22
|
+
* @param data The image data
|
|
23
|
+
* @param options See {@link AnimationDecodeOptions} (for animations) and {@link DecodeOptions} (for images)
|
|
24
|
+
*/
|
|
25
|
+
export declare function decode(data: BinaryData, options?: AnimationDecodeOptions & DecodeOptions): Promise<GIF | Animation | Image>;
|
|
16
26
|
/**
|
|
17
27
|
* Decodes any supported image: GIFs are returned as {@link GIF} (an {@link Animation}), animated WebPs as
|
|
18
28
|
* {@link Animation}, everything else (including APNGs, as their default image) as {@link Image}.
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Image } from "./image.js";
|
|
2
2
|
import { GIF } from "./gif.js";
|
|
3
3
|
import { Animation } from "./animation.js";
|
|
4
|
+
import { isOptions } from "./util/options.js";
|
|
4
5
|
import * as webp from "./codecs/webp.js";
|
|
5
6
|
import { view } from "./util/mem.js";
|
|
6
7
|
import { ImageType } from "./image-type.js";
|
|
@@ -10,11 +11,12 @@ export { GIF } from "./gif.js";
|
|
|
10
11
|
export { Animation } from "./animation.js";
|
|
11
12
|
export { TextLayout } from "./text-layout.js";
|
|
12
13
|
export { ImageType } from "./image-type.js";
|
|
13
|
-
export function decode(data,
|
|
14
|
+
export function decode(data, onlyFirstFrameOrOptions = false) {
|
|
15
|
+
const { onlyFirstFrame = false, applyOrientation } = isOptions(onlyFirstFrameOrOptions) ? onlyFirstFrameOrOptions : { onlyFirstFrame: onlyFirstFrameOrOptions };
|
|
14
16
|
const type = ImageType.getType(data);
|
|
15
17
|
if (type === 'gif')
|
|
16
|
-
return GIF.decode(data,
|
|
18
|
+
return GIF.decode(data, { onlyFirstFrame });
|
|
17
19
|
if (type === 'webp' && webp.isAnimated(view(data)))
|
|
18
|
-
return Animation.decode(data,
|
|
19
|
-
return Image.decode(data);
|
|
20
|
+
return Animation.decode(data, { onlyFirstFrame });
|
|
21
|
+
return Image.decode(data, { applyOrientation });
|
|
20
22
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata written by the encoders. Each format stores the fields it has a place for:
|
|
3
|
+
*
|
|
4
|
+
* | Format | Fields |
|
|
5
|
+
* |---|---|
|
|
6
|
+
* | PNG, APNG | all (as text chunks) |
|
|
7
|
+
* | JPEG | author, description, copyright, creation time, software (Exif); comment (comment marker) |
|
|
8
|
+
* | WebP | author, description, copyright, creation time, software (Exif) |
|
|
9
|
+
* | GIF | software, comment (comment extensions) |
|
|
10
|
+
* | TGA | author, comment, creation time, software (extension area) |
|
|
11
|
+
*
|
|
12
|
+
* Empty fields are left out
|
|
13
|
+
*/
|
|
14
|
+
export interface Metadata {
|
|
15
|
+
/** The image's title */
|
|
16
|
+
title?: string;
|
|
17
|
+
/** The image's author */
|
|
18
|
+
author?: string;
|
|
19
|
+
/** The image's description */
|
|
20
|
+
description?: string;
|
|
21
|
+
/** The image's copyright info */
|
|
22
|
+
copyright?: string;
|
|
23
|
+
/** The image's creation time. Default: now */
|
|
24
|
+
creationTime?: string | number | Date;
|
|
25
|
+
/** The software used to create the image. Default: `'github.com/matmen/ImageScript vX.X.X'`, `''` leaves it out */
|
|
26
|
+
software?: string;
|
|
27
|
+
/** A disclaimer for the image */
|
|
28
|
+
disclaimer?: string;
|
|
29
|
+
/** A warning for the image */
|
|
30
|
+
warning?: string;
|
|
31
|
+
/** The image's source */
|
|
32
|
+
source?: string;
|
|
33
|
+
/** A comment for the image */
|
|
34
|
+
comment?: string;
|
|
35
|
+
}
|
package/dist/metadata.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { VERSION } from "./version.js";
|
|
2
|
+
import { encode as utf8 } from "./util/utf8.js";
|
|
3
|
+
import * as exif from "./codecs/exif.js";
|
|
4
|
+
const TEXT_FIELDS = ['title', 'author', 'description', 'copyright', 'software', 'disclaimer', 'warning', 'source', 'comment'];
|
|
5
|
+
export function resolveMetadata(metadata = {}) {
|
|
6
|
+
const { creationTime, software } = metadata || {};
|
|
7
|
+
const time = new Date(creationTime === undefined ? Date.now() : creationTime);
|
|
8
|
+
if (isNaN(time.getTime()))
|
|
9
|
+
throw new RangeError('Invalid creation time');
|
|
10
|
+
const resolved = { creationTime: time };
|
|
11
|
+
for (const key of TEXT_FIELDS) {
|
|
12
|
+
const value = key === 'software' && software === undefined ? `github.com/matmen/ImageScript v${VERSION}` : metadata?.[key];
|
|
13
|
+
if (value)
|
|
14
|
+
resolved[key] = String(value);
|
|
15
|
+
}
|
|
16
|
+
return resolved;
|
|
17
|
+
}
|
|
18
|
+
export function pngText(m) {
|
|
19
|
+
return {
|
|
20
|
+
Title: m.title,
|
|
21
|
+
Author: m.author,
|
|
22
|
+
Description: m.description,
|
|
23
|
+
Copyright: m.copyright,
|
|
24
|
+
'Creation Time': m.creationTime.toUTCString(),
|
|
25
|
+
Software: m.software,
|
|
26
|
+
Disclaimer: m.disclaimer,
|
|
27
|
+
Warning: m.warning,
|
|
28
|
+
Source: m.source,
|
|
29
|
+
Comment: m.comment
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function exifData(m) {
|
|
33
|
+
return exif.encode({
|
|
34
|
+
description: m.description,
|
|
35
|
+
software: m.software,
|
|
36
|
+
dateTime: m.creationTime,
|
|
37
|
+
artist: m.author,
|
|
38
|
+
copyright: m.copyright
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export function gifComments(m) {
|
|
42
|
+
return [m.software, m.comment].filter((c) => !!c).map(utf8);
|
|
43
|
+
}
|
package/dist/render/svg.js
CHANGED
|
@@ -875,14 +875,25 @@ class Layer {
|
|
|
875
875
|
width;
|
|
876
876
|
height;
|
|
877
877
|
data;
|
|
878
|
+
x0 = Infinity;
|
|
879
|
+
y0 = Infinity;
|
|
880
|
+
x1 = -Infinity;
|
|
881
|
+
y1 = -Infinity;
|
|
878
882
|
constructor(width, height) {
|
|
879
883
|
this.width = width;
|
|
880
884
|
this.height = height;
|
|
881
885
|
this.data = new Float32Array(width * height * 4);
|
|
882
886
|
}
|
|
887
|
+
extend(x0, y0, x1, y1) {
|
|
888
|
+
this.x0 = Math.min(this.x0, x0);
|
|
889
|
+
this.y0 = Math.min(this.y0, y0);
|
|
890
|
+
this.x1 = Math.max(this.x1, x1);
|
|
891
|
+
this.y1 = Math.max(this.y1, y1);
|
|
892
|
+
}
|
|
883
893
|
fill({ x0, y0, w, h, data }, paint) {
|
|
884
894
|
const d = this.data;
|
|
885
895
|
const solid = paint.solid;
|
|
896
|
+
this.extend(x0, y0, x0 + w, y0 + h);
|
|
886
897
|
for (let y = 0; y < h; y++) {
|
|
887
898
|
let i = ((y0 + y) * this.width + x0) * 4;
|
|
888
899
|
for (let x = 0, o = y * w; x < w; x++, o++, i += 4) {
|
|
@@ -921,33 +932,40 @@ class Layer {
|
|
|
921
932
|
}
|
|
922
933
|
}
|
|
923
934
|
composite(layer, opacity, mask) {
|
|
935
|
+
if (layer.x1 <= layer.x0)
|
|
936
|
+
return;
|
|
937
|
+
this.extend(layer.x0, layer.y0, layer.x1, layer.y1);
|
|
924
938
|
const s = layer.data, d = this.data;
|
|
925
|
-
for (let
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
939
|
+
for (let y = layer.y0; y < layer.y1; y++) {
|
|
940
|
+
for (let p = y * this.width + layer.x0, end = y * this.width + layer.x1, i = p * 4; p < end; p++, i += 4) {
|
|
941
|
+
if (s[i + 3] <= 0)
|
|
942
|
+
continue;
|
|
943
|
+
const f = mask ? opacity * mask[p] : opacity;
|
|
944
|
+
if (f <= 0)
|
|
945
|
+
continue;
|
|
946
|
+
const a = s[i + 3] * f;
|
|
947
|
+
const inv = 1 - a;
|
|
948
|
+
d[i] = s[i] * f + d[i] * inv;
|
|
949
|
+
d[i + 1] = s[i + 1] * f + d[i + 1] * inv;
|
|
950
|
+
d[i + 2] = s[i + 2] * f + d[i + 2] * inv;
|
|
951
|
+
d[i + 3] = a + d[i + 3] * inv;
|
|
952
|
+
}
|
|
937
953
|
}
|
|
938
954
|
}
|
|
939
955
|
toRGBA() {
|
|
940
956
|
const out = new Uint8ClampedArray(this.width * this.height * 4);
|
|
941
957
|
const d = this.data;
|
|
942
|
-
for (let
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
958
|
+
for (let y = this.y0; y < this.y1; y++) {
|
|
959
|
+
for (let i = (y * this.width + this.x0) * 4, end = (y * this.width + this.x1) * 4; i < end; i += 4) {
|
|
960
|
+
const a = d[i + 3];
|
|
961
|
+
if (a <= 0)
|
|
962
|
+
continue;
|
|
963
|
+
const f = a >= 1 ? 255 : 255 / a;
|
|
964
|
+
out[i] = d[i] * f;
|
|
965
|
+
out[i + 1] = d[i + 1] * f;
|
|
966
|
+
out[i + 2] = d[i + 2] * f;
|
|
967
|
+
out[i + 3] = a * 255;
|
|
968
|
+
}
|
|
951
969
|
}
|
|
952
970
|
return new Uint8Array(out.buffer);
|
|
953
971
|
}
|
package/dist/util/blur.js
CHANGED
|
@@ -119,6 +119,94 @@ export function gaussianBlur(pixels, width, height, sigma) {
|
|
|
119
119
|
boxPassRows(tmp, pixels, width, height, radius);
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
+
function boxRowWords(src, dst, s, length, radius, mult) {
|
|
123
|
+
const last = length - 1;
|
|
124
|
+
const first = src[s], end = src[s + last];
|
|
125
|
+
let rb = (radius + 1) * (first & 0xff00ff), ga = (radius + 1) * (first >>> 8 & 0xff00ff);
|
|
126
|
+
for (let k = 1; k <= radius; k++) {
|
|
127
|
+
const p = src[s + (k < last ? k : last)];
|
|
128
|
+
rb += p & 0xff00ff;
|
|
129
|
+
ga += p >>> 8 & 0xff00ff;
|
|
130
|
+
}
|
|
131
|
+
for (let x = 0; x < length; x++) {
|
|
132
|
+
dst[s + x] = (Math.imul(rb & 0xffff, mult) + 32768 >>> 16) | (Math.imul(ga & 0xffff, mult) + 32768 >>> 16) << 8
|
|
133
|
+
| (Math.imul(rb >>> 16, mult) + 32768 >>> 16) << 16 | (Math.imul(ga >>> 16, mult) + 32768 >>> 16) << 24;
|
|
134
|
+
const add = x + radius + 1, remove = x - radius;
|
|
135
|
+
const entering = add < last ? src[s + add] : end, leaving = remove > 0 ? src[s + remove] : first;
|
|
136
|
+
rb += (entering & 0xff00ff) - (leaving & 0xff00ff);
|
|
137
|
+
ga += (entering >>> 8 & 0xff00ff) - (leaving >>> 8 & 0xff00ff);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function boxColumnsWords(src, dst, width, height, radius, mult, rbs, gas) {
|
|
141
|
+
for (let x = 0; x < width; x++) {
|
|
142
|
+
const p = src[x];
|
|
143
|
+
rbs[x] = (radius + 1) * (p & 0xff00ff);
|
|
144
|
+
gas[x] = (radius + 1) * (p >>> 8 & 0xff00ff);
|
|
145
|
+
}
|
|
146
|
+
for (let k = 1; k <= radius; k++) {
|
|
147
|
+
const o = Math.min(k, height - 1) * width;
|
|
148
|
+
for (let x = 0; x < width; x++) {
|
|
149
|
+
const p = src[o + x];
|
|
150
|
+
rbs[x] += p & 0xff00ff;
|
|
151
|
+
gas[x] += p >>> 8 & 0xff00ff;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (let y = 0; y < height; y++) {
|
|
155
|
+
const o = y * width, add = Math.min(height - 1, y + radius + 1) * width, remove = Math.max(0, y - radius) * width;
|
|
156
|
+
for (let x = 0; x < width; x++) {
|
|
157
|
+
const rb = rbs[x], ga = gas[x];
|
|
158
|
+
dst[o + x] = (Math.imul(rb & 0xffff, mult) + 32768 >>> 16) | (Math.imul(ga & 0xffff, mult) + 32768 >>> 16) << 8
|
|
159
|
+
| (Math.imul(rb >>> 16, mult) + 32768 >>> 16) << 16 | (Math.imul(ga >>> 16, mult) + 32768 >>> 16) << 24;
|
|
160
|
+
const entering = src[add + x], leaving = src[remove + x];
|
|
161
|
+
rbs[x] = rb + (entering & 0xff00ff) - (leaving & 0xff00ff);
|
|
162
|
+
gas[x] = ga + (entering >>> 8 & 0xff00ff) - (leaving >>> 8 & 0xff00ff);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export function boxBlurRGBA(bitmap, width, height, sigma) {
|
|
167
|
+
const sizes = boxSizes(sigma);
|
|
168
|
+
if (!(sigma >= 2) || Math.max(...sizes) > 257)
|
|
169
|
+
return false;
|
|
170
|
+
let opaque = true;
|
|
171
|
+
for (let i = 3; i < bitmap.length; i += 4)
|
|
172
|
+
if (bitmap[i] !== 255) {
|
|
173
|
+
opaque = false;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
if (!opaque) {
|
|
177
|
+
for (let i = 0; i < bitmap.length; i += 4) {
|
|
178
|
+
const a = bitmap[i + 3];
|
|
179
|
+
bitmap[i] = (bitmap[i] * a + 128) * 257 >> 16;
|
|
180
|
+
bitmap[i + 1] = (bitmap[i + 1] * a + 128) * 257 >> 16;
|
|
181
|
+
bitmap[i + 2] = (bitmap[i + 2] * a + 128) * 257 >> 16;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const pixels = new Int32Array(bitmap.buffer, bitmap.byteOffset, width * height);
|
|
185
|
+
const tmp = new Int32Array(pixels.length), rbs = new Int32Array(width), gas = new Int32Array(width);
|
|
186
|
+
for (const size of sizes) {
|
|
187
|
+
const radius = (size - 1) / 2;
|
|
188
|
+
if (radius < 1)
|
|
189
|
+
continue;
|
|
190
|
+
const mult = Math.round(65536 / size);
|
|
191
|
+
for (let y = 0; y < height; y++)
|
|
192
|
+
boxRowWords(pixels, tmp, y * width, width, radius, mult);
|
|
193
|
+
boxColumnsWords(tmp, pixels, width, height, radius, mult, rbs, gas);
|
|
194
|
+
}
|
|
195
|
+
if (!opaque) {
|
|
196
|
+
for (let i = 0; i < bitmap.length; i += 4) {
|
|
197
|
+
const a = bitmap[i + 3];
|
|
198
|
+
if (a === 0) {
|
|
199
|
+
bitmap[i] = bitmap[i + 1] = bitmap[i + 2] = 0;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const f = 255 / a;
|
|
203
|
+
bitmap[i] *= f;
|
|
204
|
+
bitmap[i + 1] *= f;
|
|
205
|
+
bitmap[i + 2] *= f;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
122
210
|
export function premultiply(rgba) {
|
|
123
211
|
const out = new Float32Array(rgba.length);
|
|
124
212
|
for (let i = 0; i < rgba.length; i += 4) {
|