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.
Files changed (52) hide show
  1. package/README.md +29 -9
  2. package/THIRD_PARTY_NOTICES.md +82 -0
  3. package/dist/animation.d.ts +140 -0
  4. package/dist/animation.js +197 -0
  5. package/dist/codecs/exif.js +62 -0
  6. package/dist/codecs/gif.js +32 -19
  7. package/dist/codecs/jpeg.js +13 -1
  8. package/dist/codecs/png.js +348 -62
  9. package/dist/codecs/quantize.js +2 -2
  10. package/dist/codecs/tga.js +246 -0
  11. package/dist/codecs/vp8-tables.js +17 -0
  12. package/dist/codecs/vp8.js +711 -0
  13. package/dist/codecs/vp8l.js +906 -0
  14. package/dist/codecs/webp.js +293 -0
  15. package/dist/frame.d.ts +40 -12
  16. package/dist/frame.js +20 -9
  17. package/dist/gif.d.ts +16 -26
  18. package/dist/gif.js +10 -81
  19. package/dist/image-type.d.ts +6 -1
  20. package/dist/image-type.js +17 -1
  21. package/dist/image.d.ts +262 -30
  22. package/dist/image.js +144 -45
  23. package/dist/index.d.ts +20 -6
  24. package/dist/index.js +13 -4
  25. package/dist/metadata.d.ts +35 -0
  26. package/dist/metadata.js +43 -0
  27. package/dist/render/font.js +99 -2
  28. package/dist/render/hinting.js +1236 -0
  29. package/dist/render/svg.js +39 -21
  30. package/dist/render/text.js +32 -7
  31. package/dist/util/optimize.js +264 -0
  32. package/dist/util/options.js +3 -0
  33. package/dist/util/resample.js +44 -26
  34. package/dist/util/utf8.js +4 -51
  35. package/dist/version.js +1 -1
  36. package/licenses/FTL.txt +169 -0
  37. package/package.json +10 -3
  38. package/dist/codecs/crc.d.ts +0 -9
  39. package/dist/codecs/exif.d.ts +0 -11
  40. package/dist/codecs/gif.d.ts +0 -44
  41. package/dist/codecs/jpeg.d.ts +0 -35
  42. package/dist/codecs/png.d.ts +0 -44
  43. package/dist/codecs/quantize.d.ts +0 -25
  44. package/dist/codecs/tiff.d.ts +0 -6
  45. package/dist/codecs/zlib.d.ts +0 -22
  46. package/dist/render/font.d.ts +0 -63
  47. package/dist/render/svg.d.ts +0 -11
  48. package/dist/render/text.d.ts +0 -61
  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
@@ -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 { VERSION } from "./version.js";
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, mode = Image.RESIZE_BILINEAR) {
208
- const image = this.__scale__(factor, mode);
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, mode = Image.RESIZE_BILINEAR) {
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, mode = Image.RESIZE_BILINEAR) {
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, mode = Image.RESIZE_BILINEAR) {
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, mode = Image.RESIZE_BILINEAR) {
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, absolute = false) {
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, absolute = false) {
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, absolute = false) {
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, absolute = false) {
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, absolute = false) {
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, absolute = false) {
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, x = 0, y = 0, options = {}) {
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(amount = 1, radius = 1) {
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(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;
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(strength = 2) {
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(compression = 1, metadata = {}) {
856
- if (typeof compression === 'object' && compression !== null) {
857
- metadata = compression;
858
- compression = 1;
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
- const { title, author, description, copyright, creationTime, software, disclaimer, warning, source, comment } = metadata || {};
862
- return png.encodeRGBA(this.__bytes__(), this.width, this.height, level, {
863
- Title: title,
864
- Author: author,
865
- Description: description,
866
- Copyright: copyright,
867
- 'Creation Time': new Date(creationTime === undefined ? Date.now() : creationTime).toUTCString(),
868
- Software: software === undefined ? `github.com/matmen/ImageScript v${VERSION}` : software,
869
- Disclaimer: disclaimer,
870
- Warning: warning,
871
- Source: source,
872
- Comment: comment
873
- });
874
- }
875
- encodePNG(compression, metadata) {
876
- return this.encode(compression, metadata);
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, 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 };
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, color = 0xffffffff, layout = new TextLayout(), style = {}) {
966
- const { strokeWidth = 0, strokeColor = 0x000000ff } = 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;
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, GIFs are returned as {@link GIF}
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 onlyExtractFirstFrame Whether to end GIF decoding after the first frame
23
+ * @param options See {@link AnimationDecodeOptions} (for animations) and {@link DecodeOptions} (for images)
18
24
  */
19
- export declare function decode(data: BinaryData, onlyExtractFirstFrame?: boolean): Promise<GIF | Image>;
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, onlyExtractFirstFrame = false) {
10
- if (ImageType.getType(data) === 'gif')
11
- return GIF.decode(data, onlyExtractFirstFrame);
12
- return Image.decode(data);
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
+ }
@@ -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
+ }
@@ -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;