imagescript 3.1.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.
Files changed (51) hide show
  1. package/README.md +12 -4
  2. package/dist/animation.d.ts +92 -7
  3. package/dist/animation.js +49 -19
  4. package/dist/codecs/exif.js +62 -0
  5. package/dist/codecs/gif.js +32 -19
  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 +111 -50
  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/optimize.js +264 -0
  26. package/dist/util/options.js +3 -0
  27. package/dist/util/resample.js +44 -26
  28. package/dist/util/utf8.js +4 -51
  29. package/dist/version.js +1 -1
  30. package/package.json +9 -2
  31. package/dist/codecs/crc.d.ts +0 -9
  32. package/dist/codecs/exif.d.ts +0 -11
  33. package/dist/codecs/gif.d.ts +0 -44
  34. package/dist/codecs/jpeg.d.ts +0 -35
  35. package/dist/codecs/png.d.ts +0 -97
  36. package/dist/codecs/quantize.d.ts +0 -25
  37. package/dist/codecs/tga.d.ts +0 -16
  38. package/dist/codecs/tiff.d.ts +0 -6
  39. package/dist/codecs/vp8-tables.d.ts +0 -10
  40. package/dist/codecs/vp8.d.ts +0 -8
  41. package/dist/codecs/vp8l.d.ts +0 -62
  42. package/dist/codecs/webp.d.ts +0 -67
  43. package/dist/codecs/zlib.d.ts +0 -22
  44. package/dist/render/font.d.ts +0 -92
  45. package/dist/render/hinting.d.ts +0 -110
  46. package/dist/render/svg.d.ts +0 -11
  47. package/dist/render/text.d.ts +0 -63
  48. package/dist/util/blur.d.ts +0 -13
  49. package/dist/util/resample.d.ts +0 -12
  50. package/dist/util/utf8.d.ts +0 -5
  51. 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
15
  import { gaussianBlur, premultiply, unpremultiply } from "./util/blur.js";
16
+ import { isOptions } from "./util/options.js";
15
17
  export class Image {
16
18
  __width__;
17
19
  __height__;
@@ -206,8 +208,8 @@ export class Image {
206
208
  static get RESIZE_AUTO() {
207
209
  return -1;
208
210
  }
209
- scale(factor, mode = Image.RESIZE_BILINEAR) {
210
- const image = this.__scale__(factor, mode);
211
+ scale(factor, modeOrOptions) {
212
+ const image = this.__scale__(factor, resizeMode(modeOrOptions));
211
213
  return this.__apply__(image);
212
214
  }
213
215
  __scale__(factor, mode = Image.RESIZE_BILINEAR) {
@@ -215,23 +217,27 @@ export class Image {
215
217
  return this;
216
218
  return this.__resize__(this.width * factor, this.height * factor, mode);
217
219
  }
218
- resize(width, height, mode = Image.RESIZE_BILINEAR) {
220
+ resize(width, height, modeOrOptions) {
221
+ const mode = resizeMode(modeOrOptions);
219
222
  const image = this.__resize__(width, height, mode);
220
223
  return this.__apply__(image);
221
224
  }
222
- contain(width, height, mode = Image.RESIZE_BILINEAR) {
225
+ contain(width, height, modeOrOptions) {
226
+ const mode = resizeMode(modeOrOptions);
223
227
  width = Math.floor(width);
224
228
  height = Math.floor(height);
225
229
  const scale = Math.min(width / this.width, height / this.height);
226
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);
227
231
  }
228
- fit(width, height, mode = Image.RESIZE_BILINEAR) {
232
+ fit(width, height, modeOrOptions) {
233
+ const mode = resizeMode(modeOrOptions);
229
234
  const result = new Image(width, height);
230
235
  this.contain(width, height, mode);
231
236
  result.composite(this, (width - this.width) / 2, (height - this.height) / 2);
232
237
  return this.__apply__(result);
233
238
  }
234
- cover(width, height, mode = Image.RESIZE_BILINEAR) {
239
+ cover(width, height, modeOrOptions) {
240
+ const mode = resizeMode(modeOrOptions);
235
241
  width = Math.floor(width);
236
242
  height = Math.floor(height);
237
243
  const scale = Math.max(width / this.width, height / this.height);
@@ -282,6 +288,51 @@ export class Image {
282
288
  throw new RangeError('Crop region does not overlap the image');
283
289
  return this.__apply__(this.__crop__(left, top, right - left, bottom - top));
284
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
+ }
285
336
  __crop__(x, y, width, height) {
286
337
  x = ~~x;
287
338
  y = ~~y;
@@ -369,25 +420,29 @@ export class Image {
369
420
  }
370
421
  return this;
371
422
  }
372
- opacity(opacity, absolute = false) {
423
+ opacity(opacity, absoluteOrOptions = false) {
424
+ const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
373
425
  if (isNaN(opacity) || opacity < 0)
374
426
  throw new RangeError('Invalid opacity value');
375
427
  this.__set_channel_value__(opacity, absolute, 3);
376
428
  return this;
377
429
  }
378
- red(factor, absolute = false) {
430
+ red(factor, absoluteOrOptions = false) {
431
+ const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
379
432
  if (isNaN(factor) || factor < 0)
380
433
  throw new RangeError('Invalid red factor');
381
434
  this.__set_channel_value__(factor, absolute, 0);
382
435
  return this;
383
436
  }
384
- green(factor, absolute = false) {
437
+ green(factor, absoluteOrOptions = false) {
438
+ const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
385
439
  if (isNaN(factor) || factor < 0)
386
440
  throw new RangeError('Invalid green factor');
387
441
  this.__set_channel_value__(factor, absolute, 1);
388
442
  return this;
389
443
  }
390
- blue(factor, absolute = false) {
444
+ blue(factor, absoluteOrOptions = false) {
445
+ const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
391
446
  if (isNaN(factor) || factor < 0)
392
447
  throw new RangeError('Invalid blue factor');
393
448
  this.__set_channel_value__(factor, absolute, 2);
@@ -397,18 +452,22 @@ export class Image {
397
452
  for (let i = offset; i < this.bitmap.length; i += 4)
398
453
  this.bitmap[i] = value * (absolute ? 255 : this.bitmap[i]);
399
454
  }
400
- lightness(value, absolute = false) {
455
+ lightness(value, absoluteOrOptions = false) {
456
+ const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
401
457
  if (isNaN(value) || value < 0)
402
458
  throw new RangeError('Invalid lightness value');
403
459
  return this.__map_hsla__(c => { c.l = value * (absolute ? 1 : c.l); });
404
460
  }
405
- saturation(value, absolute = false) {
461
+ saturation(value, absoluteOrOptions = false) {
462
+ const absolute = isOptions(absoluteOrOptions) ? absoluteOrOptions.absolute ?? false : absoluteOrOptions;
406
463
  if (isNaN(value) || value < 0)
407
464
  throw new RangeError('Invalid saturation value');
408
465
  return this.__map_hsla__(c => { c.s = value * (absolute ? 1 : c.s); });
409
466
  }
410
- composite(source, x = 0, y = 0, options = {}) {
467
+ composite(source, xOrOptions = 0, yArgument = 0, optionsArgument = {}) {
468
+ const options = isOptions(xOrOptions) ? xOrOptions : optionsArgument;
411
469
  const { opacity = 1, mode = 'normal' } = options;
470
+ let x = isOptions(xOrOptions) ? xOrOptions.x ?? 0 : xOrOptions, y = isOptions(xOrOptions) ? xOrOptions.y ?? 0 : yArgument;
412
471
  x |= 0;
413
472
  y |= 0;
414
473
  if (isNaN(opacity) || opacity < 0 || opacity > 1)
@@ -558,7 +617,8 @@ export class Image {
558
617
  unpremultiply(pixels, this.bitmap);
559
618
  return this;
560
619
  }
561
- sharpen(amount = 1, radius = 1) {
620
+ sharpen(amountOrOptions = 1, radiusArgument = 1) {
621
+ const { amount = 1, radius = 1 } = isOptions(amountOrOptions) ? amountOrOptions : { amount: amountOrOptions, radius: radiusArgument };
562
622
  if (isNaN(amount) || amount < 0)
563
623
  throw new RangeError('Invalid sharpen amount');
564
624
  if (isNaN(radius) || radius <= 0)
@@ -820,7 +880,8 @@ export class Image {
820
880
  throw new RangeError(`Invalid gradient position: ${position}`);
821
881
  };
822
882
  }
823
- roundCorners(radius = Math.min(this.width, this.height) / 4) {
883
+ roundCorners(radiusOrOptions) {
884
+ let radius = (isOptions(radiusOrOptions) ? radiusOrOptions.radius : radiusOrOptions) ?? Math.min(this.width, this.height) / 4;
824
885
  radius = Math.max(0, Math.min(radius, this.width / 2, this.height / 2));
825
886
  const size = Math.ceil(radius);
826
887
  for (let y = 0; y < size; y++) {
@@ -852,7 +913,8 @@ export class Image {
852
913
  return (((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | (a & 0xff)) >>> 0;
853
914
  };
854
915
  }
855
- fisheye(strength = 2) {
916
+ fisheye(strengthOrOptions) {
917
+ const strength = (isOptions(strengthOrOptions) ? strengthOrOptions.strength : strengthOrOptions) ?? 2;
856
918
  const r = new Image(this.width, this.height);
857
919
  const w = this.width;
858
920
  const h = this.height;
@@ -877,37 +939,31 @@ export class Image {
877
939
  }
878
940
  return this.__apply__(r);
879
941
  }
880
- async encode(compression = 1, metadata = {}) {
881
- if (typeof compression === 'object' && compression !== null) {
882
- metadata = compression;
883
- compression = 1;
884
- }
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;
885
950
  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);
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 });
911
967
  }
912
968
  static async decode(input, options = {}) {
913
969
  const { applyOrientation = true } = options;
@@ -988,7 +1044,8 @@ export class Image {
988
1044
  static get SVG_MODE_HEIGHT() {
989
1045
  return 3;
990
1046
  }
991
- static async renderSVG(svg, size = 1, mode = Image.SVG_MODE_SCALE) {
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 };
992
1049
  if (![1, 2, 3].includes(mode))
993
1050
  throw new Error('Invalid SVG scaling mode');
994
1051
  if (mode === Image.SVG_MODE_SCALE && size <= 0)
@@ -1000,8 +1057,9 @@ export class Image {
1000
1057
  image.bitmap.set(pixels);
1001
1058
  return image;
1002
1059
  }
1003
- static async renderText(font, size, text, color = 0xffffffff, layout = new TextLayout(), style = {}) {
1004
- const { strokeWidth = 0, strokeColor = 0x000000ff, hinting = true } = style;
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;
1005
1063
  if (isNaN(strokeWidth) || strokeWidth < 0)
1006
1064
  throw new RangeError('Invalid stroke width');
1007
1065
  const rendered = textRenderer.render(view(font), size, text, layout, hinting);
@@ -1076,6 +1134,9 @@ function hue2rgb(p, q, t) {
1076
1134
  return p + (q - p) * (2 / 3 - t) * 6;
1077
1135
  return p;
1078
1136
  }
1137
+ function resizeMode(modeOrOptions) {
1138
+ return (isOptions(modeOrOptions) ? modeOrOptions.mode : modeOrOptions) ?? Image.RESIZE_BILINEAR;
1139
+ }
1079
1140
  const hardLight = (b, s) => s <= 0.5 ? b * 2 * s : 1 - (1 - b) * (1 - (2 * s - 1));
1080
1141
  const BLEND_MODES = {
1081
1142
  '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
  }