imagescript 3.0.0 → 3.2.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 +29 -9
- package/THIRD_PARTY_NOTICES.md +82 -0
- package/dist/animation.d.ts +140 -0
- package/dist/animation.js +197 -0
- package/dist/codecs/exif.js +62 -0
- package/dist/codecs/gif.js +32 -19
- package/dist/codecs/jpeg.js +13 -1
- package/dist/codecs/png.js +348 -62
- package/dist/codecs/quantize.js +2 -2
- package/dist/codecs/tga.js +246 -0
- package/dist/codecs/vp8-tables.js +17 -0
- package/dist/codecs/vp8.js +711 -0
- package/dist/codecs/vp8l.js +906 -0
- package/dist/codecs/webp.js +293 -0
- package/dist/frame.d.ts +40 -12
- package/dist/frame.js +20 -9
- package/dist/gif.d.ts +16 -26
- package/dist/gif.js +10 -81
- package/dist/image-type.d.ts +6 -1
- package/dist/image-type.js +17 -1
- package/dist/image.d.ts +262 -30
- package/dist/image.js +144 -45
- package/dist/index.d.ts +20 -6
- package/dist/index.js +13 -4
- package/dist/metadata.d.ts +35 -0
- package/dist/metadata.js +43 -0
- package/dist/render/font.js +99 -2
- package/dist/render/hinting.js +1236 -0
- package/dist/render/svg.js +39 -21
- package/dist/render/text.js +32 -7
- package/dist/util/optimize.js +264 -0
- package/dist/util/options.js +3 -0
- package/dist/util/resample.js +44 -26
- package/dist/util/utf8.js +4 -51
- package/dist/version.js +1 -1
- package/licenses/FTL.txt +169 -0
- package/package.json +10 -3
- 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 -44
- package/dist/codecs/quantize.d.ts +0 -25
- package/dist/codecs/tiff.d.ts +0 -6
- package/dist/codecs/zlib.d.ts +0 -22
- package/dist/render/font.d.ts +0 -63
- package/dist/render/svg.d.ts +0 -11
- package/dist/render/text.d.ts +0 -61
- 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
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import * as png from "./codecs/png.js";
|
|
2
2
|
import * as jpeg from "./codecs/jpeg.js";
|
|
3
3
|
import * as tiff from "./codecs/tiff.js";
|
|
4
|
+
import * as webp from "./codecs/webp.js";
|
|
5
|
+
import * as tga from "./codecs/tga.js";
|
|
4
6
|
import * as quantize from "./codecs/quantize.js";
|
|
5
7
|
import * as textRenderer from "./render/text.js";
|
|
6
8
|
import * as svgRenderer from "./render/svg.js";
|
|
7
9
|
import { view } from "./util/mem.js";
|
|
8
10
|
import { ImageType } from "./image-type.js";
|
|
9
11
|
import { TextLayout } from "./text-layout.js";
|
|
10
|
-
import {
|
|
12
|
+
import { resolveMetadata, pngText, exifData } from "./metadata.js";
|
|
13
|
+
import { encode as utf8 } from "./util/utf8.js";
|
|
11
14
|
import { resample, BOX, TRIANGLE, CUBIC } from "./util/resample.js";
|
|
12
15
|
import { gaussianBlur, premultiply, unpremultiply } from "./util/blur.js";
|
|
16
|
+
import { isOptions } from "./util/options.js";
|
|
13
17
|
export class Image {
|
|
14
18
|
__width__;
|
|
15
19
|
__height__;
|
|
@@ -204,8 +208,8 @@ export class Image {
|
|
|
204
208
|
static get RESIZE_AUTO() {
|
|
205
209
|
return -1;
|
|
206
210
|
}
|
|
207
|
-
scale(factor,
|
|
208
|
-
const image = this.__scale__(factor,
|
|
211
|
+
scale(factor, modeOrOptions) {
|
|
212
|
+
const image = this.__scale__(factor, resizeMode(modeOrOptions));
|
|
209
213
|
return this.__apply__(image);
|
|
210
214
|
}
|
|
211
215
|
__scale__(factor, mode = Image.RESIZE_BILINEAR) {
|
|
@@ -213,23 +217,27 @@ export class Image {
|
|
|
213
217
|
return this;
|
|
214
218
|
return this.__resize__(this.width * factor, this.height * factor, mode);
|
|
215
219
|
}
|
|
216
|
-
resize(width, height,
|
|
220
|
+
resize(width, height, modeOrOptions) {
|
|
221
|
+
const mode = resizeMode(modeOrOptions);
|
|
217
222
|
const image = this.__resize__(width, height, mode);
|
|
218
223
|
return this.__apply__(image);
|
|
219
224
|
}
|
|
220
|
-
contain(width, height,
|
|
225
|
+
contain(width, height, modeOrOptions) {
|
|
226
|
+
const mode = resizeMode(modeOrOptions);
|
|
221
227
|
width = Math.floor(width);
|
|
222
228
|
height = Math.floor(height);
|
|
223
229
|
const scale = Math.min(width / this.width, height / this.height);
|
|
224
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))), mode);
|
|
225
231
|
}
|
|
226
|
-
fit(width, height,
|
|
232
|
+
fit(width, height, modeOrOptions) {
|
|
233
|
+
const mode = resizeMode(modeOrOptions);
|
|
227
234
|
const result = new Image(width, height);
|
|
228
235
|
this.contain(width, height, mode);
|
|
229
236
|
result.composite(this, (width - this.width) / 2, (height - this.height) / 2);
|
|
230
237
|
return this.__apply__(result);
|
|
231
238
|
}
|
|
232
|
-
cover(width, height,
|
|
239
|
+
cover(width, height, modeOrOptions) {
|
|
240
|
+
const mode = resizeMode(modeOrOptions);
|
|
233
241
|
width = Math.floor(width);
|
|
234
242
|
height = Math.floor(height);
|
|
235
243
|
const scale = Math.max(width / this.width, height / this.height);
|
|
@@ -280,6 +288,51 @@ export class Image {
|
|
|
280
288
|
throw new RangeError('Crop region does not overlap the image');
|
|
281
289
|
return this.__apply__(this.__crop__(left, top, right - left, bottom - top));
|
|
282
290
|
}
|
|
291
|
+
trim(color, options = {}) {
|
|
292
|
+
const bounds = this.__trim_bounds__(color, options);
|
|
293
|
+
return bounds ? this.__apply__(this.__crop__(...bounds)) : this;
|
|
294
|
+
}
|
|
295
|
+
__trim_bounds__(color, { tolerance = 0 }) {
|
|
296
|
+
if (!(tolerance >= 0 && tolerance <= 1))
|
|
297
|
+
throw new RangeError('Invalid tolerance, it has to be between 0 and 1');
|
|
298
|
+
const bitmap = this.bitmap;
|
|
299
|
+
const { width, height } = this;
|
|
300
|
+
const c = color === undefined ? [bitmap[0], bitmap[1], bitmap[2], bitmap[3]] : [color >>> 24, color >> 16 & 255, color >> 8 & 255, color & 255];
|
|
301
|
+
const limit = tolerance * 255;
|
|
302
|
+
const matches = (x, y) => {
|
|
303
|
+
const i = (y * width + x) * 4;
|
|
304
|
+
if (Math.abs(bitmap[i + 3] - c[3]) > limit)
|
|
305
|
+
return false;
|
|
306
|
+
if (bitmap[i + 3] === 0 && c[3] === 0)
|
|
307
|
+
return true;
|
|
308
|
+
return Math.abs(bitmap[i] - c[0]) <= limit && Math.abs(bitmap[i + 1] - c[1]) <= limit && Math.abs(bitmap[i + 2] - c[2]) <= limit;
|
|
309
|
+
};
|
|
310
|
+
const rowMatches = (y) => {
|
|
311
|
+
for (let x = 0; x < width; x++)
|
|
312
|
+
if (!matches(x, y))
|
|
313
|
+
return false;
|
|
314
|
+
return true;
|
|
315
|
+
};
|
|
316
|
+
const columnMatches = (x, top, bottom) => {
|
|
317
|
+
for (let y = top; y < bottom; y++)
|
|
318
|
+
if (!matches(x, y))
|
|
319
|
+
return false;
|
|
320
|
+
return true;
|
|
321
|
+
};
|
|
322
|
+
let top = 0, bottom = height;
|
|
323
|
+
while (top < height && rowMatches(top))
|
|
324
|
+
top++;
|
|
325
|
+
if (top === height)
|
|
326
|
+
return null;
|
|
327
|
+
while (rowMatches(bottom - 1))
|
|
328
|
+
bottom--;
|
|
329
|
+
let left = 0, right = width;
|
|
330
|
+
while (columnMatches(left, top, bottom))
|
|
331
|
+
left++;
|
|
332
|
+
while (columnMatches(right - 1, top, bottom))
|
|
333
|
+
right--;
|
|
334
|
+
return [left, top, right - left, bottom - top];
|
|
335
|
+
}
|
|
283
336
|
__crop__(x, y, width, height) {
|
|
284
337
|
x = ~~x;
|
|
285
338
|
y = ~~y;
|
|
@@ -367,25 +420,29 @@ export class Image {
|
|
|
367
420
|
}
|
|
368
421
|
return this;
|
|
369
422
|
}
|
|
370
|
-
opacity(opacity,
|
|
423
|
+
opacity(opacity, absoluteOrOptions = false) {
|
|
424
|
+
const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
|
|
371
425
|
if (isNaN(opacity) || opacity < 0)
|
|
372
426
|
throw new RangeError('Invalid opacity value');
|
|
373
427
|
this.__set_channel_value__(opacity, absolute, 3);
|
|
374
428
|
return this;
|
|
375
429
|
}
|
|
376
|
-
red(factor,
|
|
430
|
+
red(factor, absoluteOrOptions = false) {
|
|
431
|
+
const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
|
|
377
432
|
if (isNaN(factor) || factor < 0)
|
|
378
433
|
throw new RangeError('Invalid red factor');
|
|
379
434
|
this.__set_channel_value__(factor, absolute, 0);
|
|
380
435
|
return this;
|
|
381
436
|
}
|
|
382
|
-
green(factor,
|
|
437
|
+
green(factor, absoluteOrOptions = false) {
|
|
438
|
+
const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
|
|
383
439
|
if (isNaN(factor) || factor < 0)
|
|
384
440
|
throw new RangeError('Invalid green factor');
|
|
385
441
|
this.__set_channel_value__(factor, absolute, 1);
|
|
386
442
|
return this;
|
|
387
443
|
}
|
|
388
|
-
blue(factor,
|
|
444
|
+
blue(factor, absoluteOrOptions = false) {
|
|
445
|
+
const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
|
|
389
446
|
if (isNaN(factor) || factor < 0)
|
|
390
447
|
throw new RangeError('Invalid blue factor');
|
|
391
448
|
this.__set_channel_value__(factor, absolute, 2);
|
|
@@ -395,18 +452,22 @@ export class Image {
|
|
|
395
452
|
for (let i = offset; i < this.bitmap.length; i += 4)
|
|
396
453
|
this.bitmap[i] = value * (absolute ? 255 : this.bitmap[i]);
|
|
397
454
|
}
|
|
398
|
-
lightness(value,
|
|
455
|
+
lightness(value, absoluteOrOptions = false) {
|
|
456
|
+
const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
|
|
399
457
|
if (isNaN(value) || value < 0)
|
|
400
458
|
throw new RangeError('Invalid lightness value');
|
|
401
459
|
return this.__map_hsla__(c => { c.l = value * (absolute ? 1 : c.l); });
|
|
402
460
|
}
|
|
403
|
-
saturation(value,
|
|
461
|
+
saturation(value, absoluteOrOptions = false) {
|
|
462
|
+
const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
|
|
404
463
|
if (isNaN(value) || value < 0)
|
|
405
464
|
throw new RangeError('Invalid saturation value');
|
|
406
465
|
return this.__map_hsla__(c => { c.s = value * (absolute ? 1 : c.s); });
|
|
407
466
|
}
|
|
408
|
-
composite(source,
|
|
467
|
+
composite(source, xOrOptions = 0, yArgument = 0, optionsArgument = {}) {
|
|
468
|
+
const options = isOptions(xOrOptions) ? xOrOptions : optionsArgument;
|
|
409
469
|
const { opacity = 1, mode = 'normal' } = options;
|
|
470
|
+
let x = isOptions(xOrOptions) ? xOrOptions.x ?? 0 : xOrOptions, y = isOptions(xOrOptions) ? xOrOptions.y ?? 0 : yArgument;
|
|
410
471
|
x |= 0;
|
|
411
472
|
y |= 0;
|
|
412
473
|
if (isNaN(opacity) || opacity < 0 || opacity > 1)
|
|
@@ -556,7 +617,8 @@ export class Image {
|
|
|
556
617
|
unpremultiply(pixels, this.bitmap);
|
|
557
618
|
return this;
|
|
558
619
|
}
|
|
559
|
-
sharpen(
|
|
620
|
+
sharpen(amountOrOptions = 1, radiusArgument = 1) {
|
|
621
|
+
const { amount = 1, radius = 1 } = isOptions(amountOrOptions) ? amountOrOptions : { amount: amountOrOptions, radius: radiusArgument };
|
|
560
622
|
if (isNaN(amount) || amount < 0)
|
|
561
623
|
throw new RangeError('Invalid sharpen amount');
|
|
562
624
|
if (isNaN(radius) || radius <= 0)
|
|
@@ -606,6 +668,29 @@ export class Image {
|
|
|
606
668
|
}
|
|
607
669
|
return this;
|
|
608
670
|
}
|
|
671
|
+
colorToAlpha(color, options = {}) {
|
|
672
|
+
const { transparencyThreshold = 0, opacityThreshold = 1 } = options;
|
|
673
|
+
if (!(transparencyThreshold >= 0 && opacityThreshold <= 1 && transparencyThreshold < opacityThreshold))
|
|
674
|
+
throw new RangeError('Invalid thresholds, they have to satisfy 0 <= transparencyThreshold < opacityThreshold <= 1');
|
|
675
|
+
const bitmap = this.bitmap;
|
|
676
|
+
const c = color === undefined ? [bitmap[0], bitmap[1], bitmap[2]] : [color >>> 24, color >> 16 & 255, color >> 8 & 255];
|
|
677
|
+
for (let i = 0; i < bitmap.length; i += 4) {
|
|
678
|
+
let distance = 0;
|
|
679
|
+
for (let k = 0; k < 3; k++) {
|
|
680
|
+
const d = bitmap[i + k] - c[k];
|
|
681
|
+
const a = d > 0 ? d / (255 - c[k]) : d < 0 ? -d / c[k] : 0;
|
|
682
|
+
if (a > distance)
|
|
683
|
+
distance = a;
|
|
684
|
+
}
|
|
685
|
+
const alpha = distance <= transparencyThreshold ? 0 : distance >= opacityThreshold ? 1
|
|
686
|
+
: (distance - transparencyThreshold) / (opacityThreshold - transparencyThreshold);
|
|
687
|
+
if (alpha > 0)
|
|
688
|
+
for (let k = 0; k < 3; k++)
|
|
689
|
+
bitmap[i + k] = Math.round((bitmap[i + k] - c[k]) / alpha + c[k]);
|
|
690
|
+
bitmap[i + 3] = Math.round(bitmap[i + 3] * alpha);
|
|
691
|
+
}
|
|
692
|
+
return this;
|
|
693
|
+
}
|
|
609
694
|
__map_channels__(map) {
|
|
610
695
|
const lut = new Uint8ClampedArray(256);
|
|
611
696
|
for (let v = 0; v < 256; v++)
|
|
@@ -795,7 +880,8 @@ export class Image {
|
|
|
795
880
|
throw new RangeError(`Invalid gradient position: ${position}`);
|
|
796
881
|
};
|
|
797
882
|
}
|
|
798
|
-
roundCorners(
|
|
883
|
+
roundCorners(radiusOrOptions) {
|
|
884
|
+
let radius = (isOptions(radiusOrOptions) ? radiusOrOptions.radius : radiusOrOptions) ?? Math.min(this.width, this.height) / 4;
|
|
799
885
|
radius = Math.max(0, Math.min(radius, this.width / 2, this.height / 2));
|
|
800
886
|
const size = Math.ceil(radius);
|
|
801
887
|
for (let y = 0; y < size; y++) {
|
|
@@ -827,7 +913,8 @@ export class Image {
|
|
|
827
913
|
return (((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | (a & 0xff)) >>> 0;
|
|
828
914
|
};
|
|
829
915
|
}
|
|
830
|
-
fisheye(
|
|
916
|
+
fisheye(strengthOrOptions) {
|
|
917
|
+
const strength = (isOptions(strengthOrOptions) ? strengthOrOptions.strength : strengthOrOptions) ?? 2;
|
|
831
918
|
const r = new Image(this.width, this.height);
|
|
832
919
|
const w = this.width;
|
|
833
920
|
const h = this.height;
|
|
@@ -852,31 +939,31 @@ export class Image {
|
|
|
852
939
|
}
|
|
853
940
|
return this.__apply__(r);
|
|
854
941
|
}
|
|
855
|
-
async encode(
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
compression =
|
|
859
|
-
|
|
942
|
+
async encode(compressionOrOptions = 1, metadataArgument = {}) {
|
|
943
|
+
let compression = 1, metadata = metadataArgument;
|
|
944
|
+
if (!isOptions(compressionOrOptions))
|
|
945
|
+
compression = compressionOrOptions;
|
|
946
|
+
else if ('compression' in compressionOrOptions || 'metadata' in compressionOrOptions)
|
|
947
|
+
({ compression = 1, metadata = {} } = compressionOrOptions);
|
|
948
|
+
else
|
|
949
|
+
metadata = compressionOrOptions;
|
|
860
950
|
const level = Math.min(9, Math.max(0, ~~compression));
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
return
|
|
877
|
-
}
|
|
878
|
-
async encodeJPEG(quality = 90) {
|
|
879
|
-
return jpeg.encode(this.__bytes__(), this.width, this.height, quality);
|
|
951
|
+
return png.encodeRGBA(this.__bytes__(), this.width, this.height, level, pngText(resolveMetadata(metadata)));
|
|
952
|
+
}
|
|
953
|
+
encodePNG(compressionOrOptions, metadata) {
|
|
954
|
+
return this.encode(compressionOrOptions, metadata);
|
|
955
|
+
}
|
|
956
|
+
async encodeJPEG(qualityOrOptions = 90) {
|
|
957
|
+
const { quality = 90, metadata = {} } = isOptions(qualityOrOptions) ? qualityOrOptions : { quality: qualityOrOptions };
|
|
958
|
+
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 });
|
|
960
|
+
}
|
|
961
|
+
async encodeWebP({ metadata = {} } = {}) {
|
|
962
|
+
return webp.encode(this.__bytes__(), this.width, this.height, exifData(resolveMetadata(metadata)));
|
|
963
|
+
}
|
|
964
|
+
async encodeTGA({ metadata = {} } = {}) {
|
|
965
|
+
const { author, comment, creationTime: time, software } = resolveMetadata(metadata);
|
|
966
|
+
return tga.encode(this.__bytes__(), this.width, this.height, { author, comment, time, software });
|
|
880
967
|
}
|
|
881
968
|
static async decode(input, options = {}) {
|
|
882
969
|
const { applyOrientation = true } = options;
|
|
@@ -889,6 +976,13 @@ export class Image {
|
|
|
889
976
|
decoded = jpeg.decode(data);
|
|
890
977
|
else if (ImageType.isTIFF(dataView))
|
|
891
978
|
decoded = await tiff.decode(data);
|
|
979
|
+
else if (ImageType.isWebP(dataView)) {
|
|
980
|
+
if (webp.isAnimated(data))
|
|
981
|
+
throw new Error('Animated WebPs have to be decoded with Animation.decode()');
|
|
982
|
+
decoded = webp.decode(data);
|
|
983
|
+
}
|
|
984
|
+
else if (ImageType.isTGA(dataView))
|
|
985
|
+
decoded = tga.decode(data);
|
|
892
986
|
else
|
|
893
987
|
throw new Error('Unsupported image type');
|
|
894
988
|
const image = new Image(decoded.width, decoded.height);
|
|
@@ -950,7 +1044,8 @@ export class Image {
|
|
|
950
1044
|
static get SVG_MODE_HEIGHT() {
|
|
951
1045
|
return 3;
|
|
952
1046
|
}
|
|
953
|
-
static async renderSVG(svg,
|
|
1047
|
+
static async renderSVG(svg, sizeOrOptions = 1, modeArgument = Image.SVG_MODE_SCALE) {
|
|
1048
|
+
const { size = 1, mode = Image.SVG_MODE_SCALE } = isOptions(sizeOrOptions) ? sizeOrOptions : { size: sizeOrOptions, mode: modeArgument };
|
|
954
1049
|
if (![1, 2, 3].includes(mode))
|
|
955
1050
|
throw new Error('Invalid SVG scaling mode');
|
|
956
1051
|
if (mode === Image.SVG_MODE_SCALE && size <= 0)
|
|
@@ -962,11 +1057,12 @@ export class Image {
|
|
|
962
1057
|
image.bitmap.set(pixels);
|
|
963
1058
|
return image;
|
|
964
1059
|
}
|
|
965
|
-
static async renderText(font, size, text,
|
|
966
|
-
const {
|
|
1060
|
+
static async renderText(font, size, text, colorOrOptions = 0xffffffff, layoutArgument, styleArgument = {}) {
|
|
1061
|
+
const options = isOptions(colorOrOptions) ? colorOrOptions : { ...styleArgument, color: colorOrOptions, layout: layoutArgument };
|
|
1062
|
+
const { color = 0xffffffff, layout = new TextLayout(), strokeWidth = 0, strokeColor = 0x000000ff, hinting = true } = options;
|
|
967
1063
|
if (isNaN(strokeWidth) || strokeWidth < 0)
|
|
968
1064
|
throw new RangeError('Invalid stroke width');
|
|
969
|
-
const rendered = textRenderer.render(view(font), size, text, layout);
|
|
1065
|
+
const rendered = textRenderer.render(view(font), size, text, layout, hinting);
|
|
970
1066
|
const [r, g, b, a] = Image.colorToRGBA(color);
|
|
971
1067
|
const pad = Math.ceil(strokeWidth);
|
|
972
1068
|
const width = rendered.width + 2 * pad, height = rendered.height + 2 * pad;
|
|
@@ -1038,6 +1134,9 @@ function hue2rgb(p, q, t) {
|
|
|
1038
1134
|
return p + (q - p) * (2 / 3 - t) * 6;
|
|
1039
1135
|
return p;
|
|
1040
1136
|
}
|
|
1137
|
+
function resizeMode(modeOrOptions) {
|
|
1138
|
+
return (isOptions(modeOrOptions) ? modeOrOptions.mode : modeOrOptions) ?? Image.RESIZE_BILINEAR;
|
|
1139
|
+
}
|
|
1041
1140
|
const hardLight = (b, s) => s <= 0.5 ? b * 2 * s : 1 - (1 - b) * (1 - (2 * s - 1));
|
|
1042
1141
|
const BLEND_MODES = {
|
|
1043
1142
|
'normal': (_b, s) => s,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,19 +1,33 @@
|
|
|
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, AnimationDecodeOptions } from './animation.js';
|
|
4
5
|
export { Image } from './image.js';
|
|
5
|
-
export type { Color, ColorFunction, ResizeMode, SVGScaleMode, PNGMetadata, TextStyle, DecodeOptions, CompositeOptions, BlendMode, CropCircleOptions, DominantColorOptions, RotateOptions } 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';
|
|
6
7
|
export { Frame } from './frame.js';
|
|
7
|
-
export type { FrameDisposalModeName, FrameDisposalModeId } from './frame.js';
|
|
8
|
+
export type { FrameDisposalModeName, FrameDisposalModeId, FrameOptions } from './frame.js';
|
|
8
9
|
export { GIF } from './gif.js';
|
|
10
|
+
export { Animation } from './animation.js';
|
|
11
|
+
export type { AnimationOptions, AnimationDecodeOptions, AnimationEncodeOptions, GIFEncodeOptions, APNGEncodeOptions } from './animation.js';
|
|
9
12
|
export { TextLayout } from './text-layout.js';
|
|
10
13
|
export type { TextLayoutOptions, WrapStyle, HorizontalAlign, VerticalAlign } from './text-layout.js';
|
|
11
14
|
export { ImageType } from './image-type.js';
|
|
12
15
|
export type { ImageTypeName } from './image-type.js';
|
|
13
16
|
export type { BinaryData } from './util/mem.js';
|
|
17
|
+
export type { Metadata } from './metadata.js';
|
|
14
18
|
/**
|
|
15
|
-
* Decodes any supported image
|
|
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
|
|
16
22
|
* @param data The image data
|
|
17
|
-
* @param
|
|
23
|
+
* @param options See {@link AnimationDecodeOptions} (for animations) and {@link DecodeOptions} (for images)
|
|
18
24
|
*/
|
|
19
|
-
export declare function decode(data: BinaryData,
|
|
25
|
+
export declare function decode(data: BinaryData, options?: AnimationDecodeOptions & DecodeOptions): Promise<GIF | Animation | Image>;
|
|
26
|
+
/**
|
|
27
|
+
* Decodes any supported image: GIFs are returned as {@link GIF} (an {@link Animation}), animated WebPs as
|
|
28
|
+
* {@link Animation}, everything else (including APNGs, as their default image) as {@link Image}.
|
|
29
|
+
* {@link Animation.decode} decodes the frames of GIFs, APNGs and WebPs
|
|
30
|
+
* @param data The image data
|
|
31
|
+
* @param onlyExtractFirstFrame Whether to end animation decoding after the first frame
|
|
32
|
+
*/
|
|
33
|
+
export declare function decode(data: BinaryData, onlyExtractFirstFrame?: boolean): Promise<GIF | Animation | Image>;
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { Image } from "./image.js";
|
|
2
2
|
import { GIF } from "./gif.js";
|
|
3
|
+
import { Animation } from "./animation.js";
|
|
4
|
+
import { isOptions } from "./util/options.js";
|
|
5
|
+
import * as webp from "./codecs/webp.js";
|
|
6
|
+
import { view } from "./util/mem.js";
|
|
3
7
|
import { ImageType } from "./image-type.js";
|
|
4
8
|
export { Image } from "./image.js";
|
|
5
9
|
export { Frame } from "./frame.js";
|
|
6
10
|
export { GIF } from "./gif.js";
|
|
11
|
+
export { Animation } from "./animation.js";
|
|
7
12
|
export { TextLayout } from "./text-layout.js";
|
|
8
13
|
export { ImageType } from "./image-type.js";
|
|
9
|
-
export function decode(data,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
export function decode(data, onlyFirstFrameOrOptions = false) {
|
|
15
|
+
const { onlyFirstFrame = false, applyOrientation } = isOptions(onlyFirstFrameOrOptions) ? onlyFirstFrameOrOptions : { onlyFirstFrame: onlyFirstFrameOrOptions };
|
|
16
|
+
const type = ImageType.getType(data);
|
|
17
|
+
if (type === 'gif')
|
|
18
|
+
return GIF.decode(data, { onlyFirstFrame });
|
|
19
|
+
if (type === 'webp' && webp.isAnimated(view(data)))
|
|
20
|
+
return Animation.decode(data, { onlyFirstFrame });
|
|
21
|
+
return Image.decode(data, { applyOrientation });
|
|
13
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/font.js
CHANGED
|
@@ -73,6 +73,103 @@ export class Font {
|
|
|
73
73
|
const index = Math.min(glyph, this.numberOfHMetrics - 1);
|
|
74
74
|
return this.u16(hmtx + index * 4);
|
|
75
75
|
}
|
|
76
|
+
leftSideBearing(glyph) {
|
|
77
|
+
const hmtx = this.tables.hmtx.offset;
|
|
78
|
+
if (glyph < this.numberOfHMetrics)
|
|
79
|
+
return this.i16(hmtx + glyph * 4 + 2);
|
|
80
|
+
return this.i16(hmtx + this.numberOfHMetrics * 4 + (glyph - this.numberOfHMetrics) * 2);
|
|
81
|
+
}
|
|
82
|
+
glyfPoints(glyph) {
|
|
83
|
+
if (this.outlines !== 'glyf' || glyph >= this.numGlyphs)
|
|
84
|
+
return null;
|
|
85
|
+
const loca = this.tables.loca.offset;
|
|
86
|
+
const start = this.indexToLocFormat ? this.u32(loca + glyph * 4) : this.u16(loca + glyph * 2) * 2;
|
|
87
|
+
const end = this.indexToLocFormat ? this.u32(loca + glyph * 4 + 4) : this.u16(loca + glyph * 2 + 2) * 2;
|
|
88
|
+
if (end <= start)
|
|
89
|
+
return null;
|
|
90
|
+
const offset = this.tables.glyf.offset + start;
|
|
91
|
+
const contourCount = this.i16(offset);
|
|
92
|
+
const xMin = this.i16(offset + 2);
|
|
93
|
+
if (contourCount < 0) {
|
|
94
|
+
const components = [];
|
|
95
|
+
let p = offset + 10, flags = 0;
|
|
96
|
+
do {
|
|
97
|
+
flags = this.u16(p);
|
|
98
|
+
const component = this.u16(p + 2);
|
|
99
|
+
p += 4;
|
|
100
|
+
let arg1, arg2;
|
|
101
|
+
if (flags & 1) {
|
|
102
|
+
arg1 = flags & 2 ? this.i16(p) : this.u16(p);
|
|
103
|
+
arg2 = flags & 2 ? this.i16(p + 2) : this.u16(p + 2);
|
|
104
|
+
p += 4;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
arg1 = flags & 2 ? this.i8(p) : this.u8(p);
|
|
108
|
+
arg2 = flags & 2 ? this.i8(p + 1) : this.u8(p + 1);
|
|
109
|
+
p += 2;
|
|
110
|
+
}
|
|
111
|
+
let matrix = [1, 0, 0, 1];
|
|
112
|
+
const f2dot14 = (o) => this.i16(o) / 16384;
|
|
113
|
+
if (flags & 8) {
|
|
114
|
+
const s = f2dot14(p);
|
|
115
|
+
matrix = [s, 0, 0, s];
|
|
116
|
+
p += 2;
|
|
117
|
+
}
|
|
118
|
+
else if (flags & 0x40) {
|
|
119
|
+
matrix = [f2dot14(p), 0, 0, f2dot14(p + 2)];
|
|
120
|
+
p += 4;
|
|
121
|
+
}
|
|
122
|
+
else if (flags & 0x80) {
|
|
123
|
+
matrix = [f2dot14(p), f2dot14(p + 2), f2dot14(p + 4), f2dot14(p + 6)];
|
|
124
|
+
p += 8;
|
|
125
|
+
}
|
|
126
|
+
components.push({
|
|
127
|
+
glyph: component,
|
|
128
|
+
dx: flags & 2 ? arg1 : 0,
|
|
129
|
+
dy: flags & 2 ? arg2 : 0,
|
|
130
|
+
matrix,
|
|
131
|
+
round: (flags & 4) !== 0,
|
|
132
|
+
match: flags & 2 ? null : [arg1, arg2],
|
|
133
|
+
useMetrics: (flags & 0x200) !== 0
|
|
134
|
+
});
|
|
135
|
+
} while (flags & 0x20);
|
|
136
|
+
const instructions = flags & 0x100 ? this.data.subarray(p + 2, p + 2 + this.u16(p)) : new Uint8Array(0);
|
|
137
|
+
return { xs: [], ys: [], on: [], ends: [], instructions, xMin, components };
|
|
138
|
+
}
|
|
139
|
+
const ends = [];
|
|
140
|
+
for (let i = 0; i < contourCount; i++)
|
|
141
|
+
ends.push(this.u16(offset + 10 + i * 2));
|
|
142
|
+
const pointCount = contourCount ? ends[contourCount - 1] + 1 : 0;
|
|
143
|
+
let p = offset + 10 + contourCount * 2;
|
|
144
|
+
const instructionLength = this.u16(p);
|
|
145
|
+
const instructions = this.data.subarray(p + 2, p + 2 + instructionLength);
|
|
146
|
+
p += 2 + instructionLength;
|
|
147
|
+
const flags = new Uint8Array(pointCount);
|
|
148
|
+
for (let i = 0; i < pointCount;) {
|
|
149
|
+
const flag = this.u8(p++);
|
|
150
|
+
flags[i++] = flag;
|
|
151
|
+
if (flag & 8)
|
|
152
|
+
for (let repeat = this.u8(p++); repeat > 0 && i < pointCount; repeat--)
|
|
153
|
+
flags[i++] = flag;
|
|
154
|
+
}
|
|
155
|
+
const xs = [], ys = [];
|
|
156
|
+
for (const [coords, short, same] of [[xs, 2, 16], [ys, 4, 32]]) {
|
|
157
|
+
let value = 0;
|
|
158
|
+
for (let i = 0; i < pointCount; i++) {
|
|
159
|
+
const flag = flags[i];
|
|
160
|
+
if (flag & short) {
|
|
161
|
+
const delta = this.u8(p++);
|
|
162
|
+
value += flag & same ? delta : -delta;
|
|
163
|
+
}
|
|
164
|
+
else if (!(flag & same)) {
|
|
165
|
+
value += this.i16(p);
|
|
166
|
+
p += 2;
|
|
167
|
+
}
|
|
168
|
+
coords.push(value);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { xs, ys, on: Array.from(flags, f => (f & 1) === 1), ends, instructions, xMin, components: null };
|
|
172
|
+
}
|
|
76
173
|
outline(glyph) {
|
|
77
174
|
if (glyph >= this.numGlyphs)
|
|
78
175
|
glyph = 0;
|
|
@@ -190,11 +287,11 @@ export class Font {
|
|
|
190
287
|
return cffCharString(this.cff, glyph);
|
|
191
288
|
}
|
|
192
289
|
}
|
|
193
|
-
function contourToPath(path, xs, ys, flags, first, last) {
|
|
290
|
+
export function contourToPath(path, xs, ys, flags, first, last) {
|
|
194
291
|
if (last < first)
|
|
195
292
|
return;
|
|
196
293
|
const count = last - first + 1;
|
|
197
|
-
const on = (i) => flags[first + (i % count)] & 1;
|
|
294
|
+
const on = (i) => flags[first + (i % count)] === true || (flags[first + (i % count)] & 1) === 1;
|
|
198
295
|
const x = (i) => xs[first + (i % count)];
|
|
199
296
|
const y = (i) => ys[first + (i % count)];
|
|
200
297
|
let start = -1;
|