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.
Files changed (52) hide show
  1. package/README.md +12 -4
  2. package/dist/animation.d.ts +92 -7
  3. package/dist/animation.js +50 -20
  4. package/dist/codecs/exif.js +62 -0
  5. package/dist/codecs/gif.js +43 -27
  6. package/dist/codecs/jpeg.js +13 -1
  7. package/dist/codecs/png.js +85 -21
  8. package/dist/codecs/quantize.js +2 -2
  9. package/dist/codecs/tga.js +47 -4
  10. package/dist/codecs/vp8l.js +27 -30
  11. package/dist/codecs/webp.js +23 -10
  12. package/dist/frame.d.ts +30 -2
  13. package/dist/frame.js +20 -9
  14. package/dist/gif.d.ts +10 -7
  15. package/dist/gif.js +7 -5
  16. package/dist/image-type.d.ts +1 -0
  17. package/dist/image-type.js +1 -0
  18. package/dist/image.d.ts +231 -31
  19. package/dist/image.js +118 -53
  20. package/dist/index.d.ts +14 -4
  21. package/dist/index.js +6 -4
  22. package/dist/metadata.d.ts +35 -0
  23. package/dist/metadata.js +43 -0
  24. package/dist/render/svg.js +39 -21
  25. package/dist/util/blur.js +88 -0
  26. package/dist/util/optimize.js +264 -0
  27. package/dist/util/options.js +3 -0
  28. package/dist/util/resample.js +244 -63
  29. package/dist/util/utf8.js +4 -51
  30. package/dist/version.js +1 -1
  31. package/package.json +9 -2
  32. package/dist/codecs/crc.d.ts +0 -9
  33. package/dist/codecs/exif.d.ts +0 -11
  34. package/dist/codecs/gif.d.ts +0 -44
  35. package/dist/codecs/jpeg.d.ts +0 -35
  36. package/dist/codecs/png.d.ts +0 -97
  37. package/dist/codecs/quantize.d.ts +0 -25
  38. package/dist/codecs/tga.d.ts +0 -16
  39. package/dist/codecs/tiff.d.ts +0 -6
  40. package/dist/codecs/vp8-tables.d.ts +0 -10
  41. package/dist/codecs/vp8.d.ts +0 -8
  42. package/dist/codecs/vp8l.d.ts +0 -62
  43. package/dist/codecs/webp.d.ts +0 -67
  44. package/dist/codecs/zlib.d.ts +0 -22
  45. package/dist/render/font.d.ts +0 -92
  46. package/dist/render/hinting.d.ts +0 -110
  47. package/dist/render/svg.d.ts +0 -11
  48. package/dist/render/text.d.ts +0 -63
  49. package/dist/util/blur.d.ts +0 -13
  50. package/dist/util/resample.d.ts +0 -12
  51. package/dist/util/utf8.d.ts +0 -5
  52. 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 { VERSION } from "./version.js";
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(this.width, this.height);
191
- image.bitmap.set(this.bitmap);
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, mode = Image.RESIZE_BILINEAR) {
210
- const image = this.__scale__(factor, mode);
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, mode = Image.RESIZE_BILINEAR) {
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, mode = Image.RESIZE_BILINEAR) {
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, mode = Image.RESIZE_BILINEAR) {
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, mode = Image.RESIZE_BILINEAR) {
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, absolute = false) {
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, absolute = false) {
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, absolute = false) {
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, absolute = false) {
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, absolute = false) {
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, absolute = false) {
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, x = 0, y = 0, options = {}) {
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(amount = 1, radius = 1) {
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(radius = Math.min(this.width, this.height) / 4) {
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(strength = 2) {
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(compression = 1, metadata = {}) {
881
- if (typeof compression === 'object' && compression !== null) {
882
- metadata = compression;
883
- compression = 1;
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
- const { title, author, description, copyright, creationTime, software, disclaimer, warning, source, comment } = metadata || {};
887
- return png.encodeRGBA(this.__bytes__(), this.width, this.height, level, {
888
- Title: title,
889
- Author: author,
890
- Description: description,
891
- Copyright: copyright,
892
- 'Creation Time': new Date(creationTime === undefined ? Date.now() : creationTime).toUTCString(),
893
- Software: software === undefined ? `github.com/matmen/ImageScript v${VERSION}` : software,
894
- Disclaimer: disclaimer,
895
- Warning: warning,
896
- Source: source,
897
- Comment: comment
898
- });
899
- }
900
- encodePNG(compression, metadata) {
901
- return this.encode(compression, metadata);
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, size = 1, mode = Image.SVG_MODE_SCALE) {
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, color = 0xffffffff, layout = new TextLayout(), style = {}) {
1004
- const { strokeWidth = 0, strokeColor = 0x000000ff, hinting = true } = style;
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, onlyExtractFirstFrame = false) {
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, onlyExtractFirstFrame);
18
+ return GIF.decode(data, { onlyFirstFrame });
17
19
  if (type === 'webp' && webp.isAnimated(view(data)))
18
- return Animation.decode(data, onlyExtractFirstFrame);
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
+ }
@@ -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
+ }
@@ -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 p = 0, i = 0; p < this.width * this.height; p++, i += 4) {
926
- if (s[i + 3] <= 0)
927
- continue;
928
- const f = mask ? opacity * mask[p] : opacity;
929
- if (f <= 0)
930
- continue;
931
- const a = s[i + 3] * f;
932
- const inv = 1 - a;
933
- d[i] = s[i] * f + d[i] * inv;
934
- d[i + 1] = s[i + 1] * f + d[i + 1] * inv;
935
- d[i + 2] = s[i + 2] * f + d[i + 2] * inv;
936
- d[i + 3] = a + d[i + 3] * inv;
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 i = 0; i < d.length; i += 4) {
943
- const a = d[i + 3];
944
- if (a <= 0)
945
- continue;
946
- const f = a >= 1 ? 255 : 255 / a;
947
- out[i] = d[i] * f;
948
- out[i + 1] = d[i + 1] * f;
949
- out[i + 2] = d[i + 2] * f;
950
- out[i + 3] = a * 255;
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) {