imagescript 3.3.0 → 3.4.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 (59) hide show
  1. package/README.md +28 -8
  2. package/THIRD_PARTY_NOTICES.md +167 -2
  3. package/dist/animation.d.ts +18 -8
  4. package/dist/animation.js +23 -2
  5. package/dist/codecs/av1/cdef.js +197 -0
  6. package/dist/codecs/av1/data.js +141 -0
  7. package/dist/codecs/av1/decode.js +1769 -0
  8. package/dist/codecs/av1/filmgrain.js +219 -0
  9. package/dist/codecs/av1/frame.js +114 -0
  10. package/dist/codecs/av1/ipred.js +491 -0
  11. package/dist/codecs/av1/itx.js +950 -0
  12. package/dist/codecs/av1/loopfilter.js +149 -0
  13. package/dist/codecs/av1/msac.js +161 -0
  14. package/dist/codecs/av1/obu.js +516 -0
  15. package/dist/codecs/av1/postfilter.js +28 -0
  16. package/dist/codecs/av1/restoration.js +193 -0
  17. package/dist/codecs/av1/tables.js +160 -0
  18. package/dist/codecs/avif.js +581 -0
  19. package/dist/codecs/bmp.js +317 -0
  20. package/dist/codecs/hdr.d.ts +25 -0
  21. package/dist/codecs/hdr.js +201 -0
  22. package/dist/codecs/ico.js +48 -0
  23. package/dist/codecs/jpeg.js +2 -1
  24. package/dist/codecs/jxl/bits.js +126 -0
  25. package/dist/codecs/jxl/blend.js +117 -0
  26. package/dist/codecs/jxl/color.js +116 -0
  27. package/dist/codecs/jxl/dct.js +404 -0
  28. package/dist/codecs/jxl/entropy.js +556 -0
  29. package/dist/codecs/jxl/features.js +325 -0
  30. package/dist/codecs/jxl/filters.js +119 -0
  31. package/dist/codecs/jxl/frame-header.js +195 -0
  32. package/dist/codecs/jxl/frame.js +319 -0
  33. package/dist/codecs/jxl/headers.js +249 -0
  34. package/dist/codecs/jxl/idct.js +484 -0
  35. package/dist/codecs/jxl/modular.js +688 -0
  36. package/dist/codecs/jxl/quant.js +219 -0
  37. package/dist/codecs/jxl/upsample.js +52 -0
  38. package/dist/codecs/jxl/vardct.js +472 -0
  39. package/dist/codecs/jxl.js +89 -0
  40. package/dist/codecs/netpbm.js +239 -0
  41. package/dist/codecs/qoi.js +150 -0
  42. package/dist/codecs/vp8-enc-dsp.js +932 -0
  43. package/dist/codecs/vp8-enc-tables.js +78 -0
  44. package/dist/codecs/vp8-enc.js +1930 -0
  45. package/dist/codecs/webp.js +36 -2
  46. package/dist/image-type.d.ts +15 -1
  47. package/dist/image-type.js +45 -2
  48. package/dist/image.d.ts +47 -6
  49. package/dist/image.js +49 -3
  50. package/dist/index.d.ts +3 -2
  51. package/dist/index.js +2 -2
  52. package/dist/render/hinting.js +26 -18
  53. package/dist/render/layout.js +1 -1
  54. package/dist/render/svg.js +1 -1
  55. package/dist/render/unicode.js +1 -9
  56. package/dist/util/bytes.js +9 -0
  57. package/dist/util/resample.js +4 -2
  58. package/dist/version.js +1 -1
  59. package/package.json +25 -2
@@ -4,6 +4,8 @@
4
4
  */
5
5
  import * as vp8l from "./vp8l.js";
6
6
  import * as vp8 from "./vp8.js";
7
+ import { encodeVP8, encodeAlpha } from "./vp8-enc.js";
8
+ import { rgbaToYUV } from "./vp8-enc-dsp.js";
7
9
  import { orientation as exifOrientation } from "./exif.js";
8
10
  import { MAX_PIXELS, checkPixels } from "../util/limits.js";
9
11
  const fourCC = (s) => s.charCodeAt(0) | s.charCodeAt(1) << 8 | s.charCodeAt(2) << 16 | s.charCodeAt(3) << 24;
@@ -218,6 +220,15 @@ function riffChunk(type, body) {
218
220
  out.set(body, 8);
219
221
  return out;
220
222
  }
223
+ function concat(parts) {
224
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
225
+ let offset = 0;
226
+ for (const p of parts) {
227
+ out.set(p, offset);
228
+ offset += p.length;
229
+ }
230
+ return out;
231
+ }
221
232
  function riff(chunks) {
222
233
  let size = 4;
223
234
  for (const c of chunks)
@@ -257,7 +268,29 @@ export function encode(rgba, width, height, exif) {
257
268
  set24(vp8x, 7, height - 1);
258
269
  return riff([riffChunk('VP8X', vp8x), bitstream, riffChunk('EXIF', exif)]);
259
270
  }
260
- export function encodeAnimation(frames, width, height, plays, exif) {
271
+ export function encodeLossy(rgba, width, height, quality, exif, method = 4) {
272
+ const { alpha, chunks } = lossyChunks(rgba, width, height, quality, method);
273
+ if (!alpha && !exif)
274
+ return riff(chunks);
275
+ const vp8x = new Uint8Array(10);
276
+ vp8x[0] = (exif ? 8 : 0) | (alpha ? 16 : 0);
277
+ set24(vp8x, 4, width - 1);
278
+ set24(vp8x, 7, height - 1);
279
+ return riff([riffChunk('VP8X', vp8x), ...chunks, ...exif ? [riffChunk('EXIF', exif)] : []]);
280
+ }
281
+ function lossyChunks(rgba, width, height, quality, method) {
282
+ if (width > 16383 || height > 16383)
283
+ throw new RangeError('WebP: lossy images can be at most 16383 pixels wide and high');
284
+ const picture = rgbaToYUV(rgba, width, height);
285
+ const frame = encodeVP8(picture, quality, method);
286
+ const padded = new Uint8Array(frame.length + (frame.length & 1));
287
+ padded.set(frame);
288
+ const chunks = [riffChunk('VP8 ', padded)];
289
+ if (picture.a)
290
+ chunks.unshift(riffChunk('ALPH', encodeAlpha(picture.a, width, height, method)));
291
+ return { alpha: !!picture.a, chunks };
292
+ }
293
+ export function encodeAnimation(frames, width, height, plays, exif, quality, method = 4) {
261
294
  if (!frames.length)
262
295
  throw new Error('WebP: an animation needs at least one frame');
263
296
  if (width > 1 << 24 || height > 1 << 24)
@@ -276,7 +309,8 @@ export function encodeAnimation(frames, width, height, plays, exif) {
276
309
  for (const f of frames) {
277
310
  if (f.x & 1 || f.y & 1)
278
311
  throw new RangeError('WebP: frame offsets have to be even');
279
- const bitstream = riffChunk('VP8L', vp8l.encode(toARGB(f.pixels, f.width * f.height), f.width, f.height));
312
+ const bitstream = quality === undefined ? riffChunk('VP8L', vp8l.encode(toARGB(f.pixels, f.width * f.height), f.width, f.height))
313
+ : concat(lossyChunks(f.pixels, f.width, f.height, quality, method).chunks);
280
314
  const header = new Uint8Array(16);
281
315
  set24(header, 0, f.x >> 1);
282
316
  set24(header, 3, f.y >> 1);
@@ -1,6 +1,6 @@
1
1
  import { BinaryData } from './util/mem.js';
2
2
  /** Image formats recognized by {@link ImageType.getType} */
3
- export type ImageTypeName = 'png' | 'jpeg' | 'tiff' | 'gif' | 'webp' | 'tga';
3
+ export type ImageTypeName = 'png' | 'jpeg' | 'tiff' | 'gif' | 'webp' | 'avif' | 'jxl' | 'bmp' | 'ico' | 'cur' | 'qoi' | 'pnm' | 'hdr' | 'tga';
4
4
  /** Detects image formats by their magic numbers */
5
5
  export declare class ImageType {
6
6
  private constructor();
@@ -20,6 +20,20 @@ export declare class ImageType {
20
20
  static isGIF(data: BinaryData): boolean;
21
21
  /** Whether the data starts like a WebP */
22
22
  static isWebP(data: BinaryData): boolean;
23
+ /** Whether the data starts like an AVIF (the avif or avis brand) */
24
+ static isAVIF(data: BinaryData): boolean;
25
+ /** Whether the data starts like a JPEG XL (a codestream or the container) */
26
+ static isJXL(data: BinaryData): boolean;
27
+ /** Whether the data starts like a BMP */
28
+ static isBMP(data: BinaryData): boolean;
29
+ /** Whether the data is a Windows icon or cursor (ICO or CUR) */
30
+ static isICO(data: BinaryData): boolean;
31
+ /** Whether the data starts like a QOI */
32
+ static isQOI(data: BinaryData): boolean;
33
+ /** Whether the data starts like a Netpbm image (PBM, PGM, PPM, PAM or PFM) */
34
+ static isPNM(data: BinaryData): boolean;
35
+ /** Whether the data starts like a Radiance HDR */
36
+ static isHDR(data: BinaryData): boolean;
23
37
  /** Whether the data looks like a TGA (which has no magic number: its header has to be consistent) */
24
38
  static isTGA(data: BinaryData): boolean;
25
39
  }
@@ -1,5 +1,12 @@
1
1
  import { view as bytes } from "./util/mem.js";
2
2
  import { isTGA } from "./codecs/tga.js";
3
+ import { isBMP } from "./codecs/bmp.js";
4
+ import { iconType } from "./codecs/ico.js";
5
+ import { isQOI } from "./codecs/qoi.js";
6
+ import { isNetpbm } from "./codecs/netpbm.js";
7
+ import { isHDR } from "./codecs/hdr.js";
8
+ import { isAVIF } from "./codecs/avif.js";
9
+ import { isJXL } from "./codecs/jxl.js";
3
10
  const MAGIC_NUMBERS = {
4
11
  PNG: 0x89504e47,
5
12
  JPEG: 0xffd8ff,
@@ -17,6 +24,7 @@ function dataView(data) {
17
24
  const u8 = bytes(data);
18
25
  return new DataView(u8.buffer, u8.byteOffset, u8.byteLength);
19
26
  }
27
+ const u8 = (view) => new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
20
28
  export class ImageType {
21
29
  constructor() { }
22
30
  static getType(data) {
@@ -31,6 +39,21 @@ export class ImageType {
31
39
  return 'gif';
32
40
  if (this.isWebP(view))
33
41
  return 'webp';
42
+ if (this.isAVIF(view))
43
+ return 'avif';
44
+ if (this.isJXL(view))
45
+ return 'jxl';
46
+ if (this.isBMP(view))
47
+ return 'bmp';
48
+ const icon = iconType(u8(view));
49
+ if (icon)
50
+ return icon;
51
+ if (this.isQOI(view))
52
+ return 'qoi';
53
+ if (this.isPNM(view))
54
+ return 'pnm';
55
+ if (this.isHDR(view))
56
+ return 'hdr';
34
57
  if (this.isTGA(view))
35
58
  return 'tga';
36
59
  return null;
@@ -58,8 +81,28 @@ export class ImageType {
58
81
  const view = dataView(data);
59
82
  return view.byteLength >= 12 && view.getUint32(0, false) === MAGIC_NUMBERS.RIFF && view.getUint32(8, false) === MAGIC_NUMBERS.WEBP;
60
83
  }
84
+ static isAVIF(data) {
85
+ return isAVIF(u8(dataView(data)));
86
+ }
87
+ static isJXL(data) {
88
+ return isJXL(u8(dataView(data)));
89
+ }
90
+ static isBMP(data) {
91
+ return isBMP(u8(dataView(data)));
92
+ }
93
+ static isICO(data) {
94
+ return iconType(u8(dataView(data))) !== null;
95
+ }
96
+ static isQOI(data) {
97
+ return isQOI(u8(dataView(data)));
98
+ }
99
+ static isPNM(data) {
100
+ return isNetpbm(u8(dataView(data)));
101
+ }
102
+ static isHDR(data) {
103
+ return isHDR(u8(dataView(data)));
104
+ }
61
105
  static isTGA(data) {
62
- const view = dataView(data);
63
- return isTGA(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
106
+ return isTGA(u8(dataView(data)));
64
107
  }
65
108
  }
package/dist/image.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ToneMapping } from './codecs/hdr.js';
1
2
  import { BinaryData } from './util/mem.js';
2
3
  import { TextLayout } from './text-layout.js';
3
4
  import { Metadata } from './metadata.js';
@@ -65,7 +66,23 @@ export interface JPEGEncodeOptions {
65
66
  /** Metadata to include, see {@link Metadata} */
66
67
  metadata?: Metadata;
67
68
  }
68
- /** Options of {@link Image.encodeWebP} and {@link Image.encodeTGA} */
69
+ /** Options of {@link Image.encodeWebP} */
70
+ export interface WebPEncodeOptions {
71
+ /**
72
+ * Lossy compression (0-100, like libwebp's quality: 75 is its default): the image is encoded like cwebp encodes it
73
+ * with its default settings, much smaller than lossless. Without it the image is encoded losslessly
74
+ */
75
+ quality?: number;
76
+ /**
77
+ * How much effort lossy compression takes (0-6, libwebp's method): higher is smaller but slower. 0-2 pick the
78
+ * predictions by how close they are, 3 and up also by how many bits they take, 5 and 6 also optimize the
79
+ * quantized values. Default: `4` (like cwebp)
80
+ */
81
+ effort?: number;
82
+ /** Metadata to include, see {@link Metadata} */
83
+ metadata?: Metadata;
84
+ }
85
+ /** Options of {@link Image.encodeTGA} */
69
86
  export interface EncodeOptions {
70
87
  /** Metadata to include, see {@link Metadata} */
71
88
  metadata?: Metadata;
@@ -107,6 +124,13 @@ export interface DecodeOptions {
107
124
  * sharp), `Infinity` for no limit
108
125
  */
109
126
  maxPixels?: number;
127
+ /**
128
+ * How Radiance HDRs, PFMs and HDR (PQ or HLG) AVIFs and JPEG XLs, whose light can be brighter than (SDR) white,
129
+ * become 8 bit sRGB: `'reinhard'` compresses the brightest parts like Reinhard's tone mapping operator does (the
130
+ * brightest pixel becomes white, images no brighter than white stay as they are), `'clamp'` clamps everything
131
+ * brighter than white. Default: `'reinhard'`
132
+ */
133
+ toneMapping?: ToneMapping;
110
134
  }
111
135
  /** Text style options of {@link Image.renderText} */
112
136
  export interface TextStyle {
@@ -711,18 +735,35 @@ export declare class Image {
711
735
  */
712
736
  encodeJPEG(quality?: number): Promise<Uint8Array>;
713
737
  /**
714
- * Encodes the image as lossless WebP (usually 20-30% smaller than PNG, but slower to encode)
715
- * @param options See {@link EncodeOptions}
738
+ * Encodes the image as WebP: lossless (usually 20-30% smaller than PNG, but slower to encode), or lossy with a
739
+ * quality
740
+ * @param options See {@link WebPEncodeOptions}
716
741
  */
717
- encodeWebP({ metadata }?: EncodeOptions): Promise<Uint8Array>;
742
+ encodeWebP({ quality, effort, metadata }?: WebPEncodeOptions): Promise<Uint8Array>;
718
743
  /**
719
744
  * Encodes the image as run length encoded TGA (32 bit, or 24 bit if the image is opaque)
720
745
  * @param options See {@link EncodeOptions}
721
746
  */
722
747
  encodeTGA({ metadata }?: EncodeOptions): Promise<Uint8Array>;
723
748
  /**
724
- * Decodes an image (PNG, JPEG, TIFF, WebP or TGA). For APNGs this is the default image; animated WebPs and GIFs
725
- * have no single image, decode those with {@link Animation.decode}
749
+ * Encodes the image as BMP: with a palette if it is opaque and has at most 256 colors, else with 24 bit pixels, or
750
+ * 32 bit ones if it has transparency
751
+ */
752
+ encodeBMP(): Promise<Uint8Array>;
753
+ /**
754
+ * Encodes the image as QOI (3 channels if the image is opaque)
755
+ */
756
+ encodeQOI(): Promise<Uint8Array>;
757
+ /**
758
+ * Encodes the image as the smallest fitting Netpbm format: PBM for black and white images, PGM for greyscale ones,
759
+ * PPM for others, PAM if the image has transparency
760
+ */
761
+ encodePNM(): Promise<Uint8Array>;
762
+ /**
763
+ * Decodes an image (PNG, JPEG, TIFF, WebP, BMP, ICO, CUR, QOI, PBM, PGM, PPM, PAM, PFM, Radiance HDR, TGA, AVIF or
764
+ * JPEG XL). For APNGs this is the default image; animated WebPs and GIFs have no single image, decode those with
765
+ * {@link Animation.decode}; for icons and cursors it's their largest image, for AVIF image sequences and JPEG XL
766
+ * animations their first frame
726
767
  * @param input The binary data to decode
727
768
  * @param options See {@link DecodeOptions}. JPEGs, TIFFs and PNGs are rotated/mirrored as their orientation tag
728
769
  * (Exif, for PNGs also XMP) says by default: photos taken with a rotated camera are stored sideways
package/dist/image.js CHANGED
@@ -3,6 +3,13 @@ import * as jpeg from "./codecs/jpeg.js";
3
3
  import * as tiff from "./codecs/tiff.js";
4
4
  import * as webp from "./codecs/webp.js";
5
5
  import * as tga from "./codecs/tga.js";
6
+ import * as bmp from "./codecs/bmp.js";
7
+ import * as ico from "./codecs/ico.js";
8
+ import * as qoi from "./codecs/qoi.js";
9
+ import * as netpbm from "./codecs/netpbm.js";
10
+ import * as hdr from "./codecs/hdr.js";
11
+ import * as avif from "./codecs/avif.js";
12
+ import * as jxl from "./codecs/jxl.js";
6
13
  import * as quantize from "./codecs/quantize.js";
7
14
  import * as textRenderer from "./render/text.js";
8
15
  import * as svgRenderer from "./render/svg.js";
@@ -996,15 +1003,34 @@ export class Image {
996
1003
  const m = resolveMetadata(metadata);
997
1004
  return jpeg.encode(this.__bytes__(), this.width, this.height, finite(quality, 'quality'), { exif: exifData(m), comment: m.comment ? utf8(m.comment) : undefined });
998
1005
  }
999
- async encodeWebP({ metadata = {} } = {}) {
1000
- return webp.encode(this.__bytes__(), this.width, this.height, exifData(resolveMetadata(metadata)));
1006
+ async encodeWebP({ quality, effort = 4, metadata = {} } = {}) {
1007
+ const exif = exifData(resolveMetadata(metadata));
1008
+ if (quality === undefined)
1009
+ return webp.encode(this.__bytes__(), this.width, this.height, exif);
1010
+ const q = finite(quality, 'quality');
1011
+ if (q < 0 || q > 100)
1012
+ throw new RangeError(`quality must be 0-100, not ${q}`);
1013
+ if (!Number.isInteger(effort) || effort < 0 || effort > 6)
1014
+ throw new RangeError(`effort must be an integer 0-6, not ${effort}`);
1015
+ return webp.encodeLossy(this.__bytes__(), this.width, this.height, q, exif, effort);
1001
1016
  }
1002
1017
  async encodeTGA({ metadata = {} } = {}) {
1003
1018
  const { author, comment, creationTime: time, software } = resolveMetadata(metadata);
1004
1019
  return tga.encode(this.__bytes__(), this.width, this.height, { author, comment, time, software });
1005
1020
  }
1021
+ async encodeBMP() {
1022
+ return bmp.encode(this.__bytes__(), this.width, this.height);
1023
+ }
1024
+ async encodeQOI() {
1025
+ return qoi.encode(this.__bytes__(), this.width, this.height);
1026
+ }
1027
+ async encodePNM() {
1028
+ return netpbm.encode(this.__bytes__(), this.width, this.height);
1029
+ }
1006
1030
  static async decode(input, options = {}) {
1007
- const { applyOrientation = true, maxPixels = MAX_PIXELS } = options;
1031
+ const { applyOrientation = true, maxPixels = MAX_PIXELS, toneMapping = 'reinhard' } = options;
1032
+ if (toneMapping !== 'reinhard' && toneMapping !== 'clamp')
1033
+ throw new TypeError(`toneMapping must be 'reinhard' or 'clamp', not ${String(toneMapping)}`);
1008
1034
  const data = view(input);
1009
1035
  const dataView = new DataView(data.buffer, data.byteOffset, data.byteLength);
1010
1036
  const decoded = await decoding(() => {
@@ -1019,6 +1045,26 @@ export class Image {
1019
1045
  throw new Error('Animated WebPs have to be decoded with Animation.decode()');
1020
1046
  return webp.decode(data, maxPixels);
1021
1047
  }
1048
+ const bitmap = async (decoded) => {
1049
+ if (!('embedded' in decoded))
1050
+ return decoded;
1051
+ const { width, height, pixels } = decoded.embedded === 'png' ? await png.decode(decoded.data, maxPixels) : jpeg.decode(decoded.data, { maxPixels });
1052
+ return { width, height, pixels };
1053
+ };
1054
+ if (ImageType.isAVIF(dataView))
1055
+ return avif.decode(data, maxPixels, toneMapping);
1056
+ if (ImageType.isJXL(dataView))
1057
+ return jxl.decode(data, maxPixels, toneMapping);
1058
+ if (ImageType.isBMP(dataView))
1059
+ return bitmap(bmp.decode(data, maxPixels));
1060
+ if (ImageType.isICO(dataView))
1061
+ return bitmap(ico.decode(data, maxPixels));
1062
+ if (ImageType.isQOI(dataView))
1063
+ return qoi.decode(data, maxPixels);
1064
+ if (ImageType.isPNM(dataView))
1065
+ return netpbm.decode(data, maxPixels, toneMapping);
1066
+ if (ImageType.isHDR(dataView))
1067
+ return hdr.decode(data, maxPixels, toneMapping);
1022
1068
  if (ImageType.isTGA(dataView))
1023
1069
  return tga.decode(data, maxPixels);
1024
1070
  throw new Error('Unsupported image type');
package/dist/index.d.ts CHANGED
@@ -3,16 +3,17 @@ import { Image, DecodeOptions } from './image.js';
3
3
  import { GIF } from './gif.js';
4
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, ResizeOptions, AdjustOptions, SharpenOptions, PNGEncodeOptions, JPEGEncodeOptions, EncodeOptions, SVGOptions, TextOptions, RoundCornersOptions, FisheyeOptions, TrimOptions } from './image.js';
6
+ export type { Color, ColorFunction, ResizeMode, SVGScaleMode, PNGMetadata, TextStyle, DecodeOptions, CompositeOptions, BlendMode, CropCircleOptions, DominantColorOptions, RotateOptions, ColorToAlphaOptions, ResizeOptions, AdjustOptions, SharpenOptions, PNGEncodeOptions, JPEGEncodeOptions, WebPEncodeOptions, EncodeOptions, SVGOptions, TextOptions, RoundCornersOptions, FisheyeOptions, TrimOptions } from './image.js';
7
7
  export { Frame } from './frame.js';
8
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
+ export type { AnimationOptions, AnimationDecodeOptions, AnimationEncodeOptions, WebPAnimationEncodeOptions, GIFEncodeOptions, APNGEncodeOptions } from './animation.js';
12
12
  export { TextLayout } from './text-layout.js';
13
13
  export type { TextLayoutOptions, WrapStyle, HorizontalAlign, VerticalAlign } from './text-layout.js';
14
14
  export { ImageType } from './image-type.js';
15
15
  export type { ImageTypeName } from './image-type.js';
16
+ export type { ToneMapping } from './codecs/hdr.js';
16
17
  export type { BinaryData } from './util/mem.js';
17
18
  export type { Metadata } from './metadata.js';
18
19
  /**
package/dist/index.js CHANGED
@@ -12,11 +12,11 @@ export { Animation } from "./animation.js";
12
12
  export { TextLayout } from "./text-layout.js";
13
13
  export { ImageType } from "./image-type.js";
14
14
  export function decode(data, onlyFirstFrameOrOptions = false) {
15
- const { onlyFirstFrame = false, applyOrientation, maxPixels } = isOptions(onlyFirstFrameOrOptions) ? onlyFirstFrameOrOptions : { onlyFirstFrame: onlyFirstFrameOrOptions };
15
+ const { onlyFirstFrame = false, applyOrientation, maxPixels, toneMapping } = isOptions(onlyFirstFrameOrOptions) ? onlyFirstFrameOrOptions : { onlyFirstFrame: onlyFirstFrameOrOptions };
16
16
  const type = ImageType.getType(data);
17
17
  if (type === 'gif')
18
18
  return GIF.decode(data, { onlyFirstFrame, maxPixels });
19
19
  if (type === 'webp' && webp.isAnimated(view(data)))
20
20
  return Animation.decode(data, { onlyFirstFrame, maxPixels });
21
- return Image.decode(data, { applyOrientation, maxPixels });
21
+ return Image.decode(data, { applyOrientation, maxPixels, toneMapping });
22
22
  }
@@ -12,6 +12,12 @@ class SkipInstruction extends Error {
12
12
  }
13
13
  const roundHalfAway = (v) => v < 0 ? -Math.round(-v) : Math.round(v);
14
14
  const mulFix = (a, b) => roundHalfAway(a * b / 65536);
15
+ const mulDiv = (a, b, c) => {
16
+ if (!c)
17
+ return a * b < 0 ? -0x7fffffff : 0x7fffffff;
18
+ const d = Math.floor((Math.abs(a * b) + Math.abs(c) / 2) / Math.abs(c));
19
+ return a * b * c < 0 ? -d : d;
20
+ };
15
21
  const divFix = (a, b) => b === 0 ? 0 : roundHalfAway(a * 65536 / b);
16
22
  const ROUND_GRID = { period: 64, phase: 0, threshold: 32 };
17
23
  const defaultState = () => ({
@@ -347,10 +353,10 @@ class Execution {
347
353
  }
348
354
  lineVector(p1, p2, perpendicular, original) {
349
355
  const z1 = this.zone(this.gs.zp1), z2 = this.zone(this.gs.zp2);
350
- this.point(z1, p2);
351
- this.point(z2, p1);
352
- let x = (original ? z1.orgX[p2] - z2.orgX[p1] : z1.curX[p2] - z2.curX[p1]);
353
- let y = (original ? z1.orgY[p2] - z2.orgY[p1] : z1.curY[p2] - z2.curY[p1]);
356
+ this.point(z1, p1);
357
+ this.point(z2, p2);
358
+ let x = (original ? z1.orgX[p1] - z2.orgX[p2] : z1.curX[p1] - z2.curX[p2]);
359
+ let y = (original ? z1.orgY[p1] - z2.orgY[p2] : z1.curY[p1] - z2.curY[p2]);
354
360
  if (x === 0 && y === 0) {
355
361
  x = 1;
356
362
  y = 0;
@@ -475,18 +481,19 @@ class Execution {
475
481
  [a0, a1].forEach(i => this.point(za, i));
476
482
  [b0, b1].forEach(i => this.point(zb, i));
477
483
  this.point(z, p);
478
- const ax0 = za.curX[a0], ay0 = za.curY[a0], ax1 = za.curX[a1], ay1 = za.curY[a1];
479
- const bx0 = zb.curX[b0], by0 = zb.curY[b0], bx1 = zb.curX[b1], by1 = zb.curY[b1];
484
+ const ax0 = Math.round(za.curX[a0]), ay0 = Math.round(za.curY[a0]), ax1 = Math.round(za.curX[a1]), ay1 = Math.round(za.curY[a1]);
485
+ const bx0 = Math.round(zb.curX[b0]), by0 = Math.round(zb.curY[b0]), bx1 = Math.round(zb.curX[b1]), by1 = Math.round(zb.curY[b1]);
480
486
  const dax = ax1 - ax0, day = ay1 - ay0, dbx = bx1 - bx0, dby = by1 - by0;
481
- const det = dax * dby - day * dbx;
482
- if (Math.abs(det) < 1e-9) {
483
- z.curX[p] = (ax0 + ax1 + bx0 + bx1) / 4;
484
- z.curY[p] = (ay0 + ay1 + by0 + by1) / 4;
487
+ const discriminant = mulDiv(dax, -dby, 64) + mulDiv(day, dbx, 64);
488
+ const dotProduct = mulDiv(dax, dbx, 64) + mulDiv(day, dby, 64);
489
+ if (19 * Math.abs(discriminant) > Math.abs(dotProduct)) {
490
+ const v = mulDiv(bx0 - ax0, -dby, 64) + mulDiv(by0 - ay0, dbx, 64);
491
+ z.curX[p] = ax0 + mulDiv(v, dax, discriminant);
492
+ z.curY[p] = ay0 + mulDiv(v, day, discriminant);
485
493
  }
486
494
  else {
487
- const t = ((bx0 - ax0) * dby - (by0 - ay0) * dbx) / det;
488
- z.curX[p] = ax0 + t * dax;
489
- z.curY[p] = ay0 + t * day;
495
+ z.curX[p] = Math.trunc((ax0 + ax1 + bx0 + bx1) / 4);
496
+ z.curY[p] = Math.trunc((ay0 + ay1 + by0 + by1) / 4);
490
497
  }
491
498
  z.touchX[p] = z.touchY[p] = 1;
492
499
  break;
@@ -540,11 +547,12 @@ class Execution {
540
547
  case 0x76:
541
548
  case 0x77: {
542
549
  const n = this.pop();
543
- const grid = op === 0x76 ? 64 : 64 / Math.SQRT2;
544
- const period = [grid / 2, grid, grid * 2, grid][n >> 6 & 3];
545
- const phase = [0, period / 4, period / 2, period * 3 / 4][n >> 4 & 3];
550
+ const grid = op === 0x76 ? 0x4000 : 0x2d41;
551
+ const period = [grid >> 1, grid, grid * 2, grid][n >> 6 & 3];
552
+ const phase = [0, period >> 2, period >> 1, Math.trunc(period * 3 / 4)][n >> 4 & 3];
546
553
  const t = n & 15;
547
- gs.round = { period, phase, threshold: t === 0 ? period - 1 : (t - 4) * period / 8 };
554
+ const threshold = t === 0 ? period - 1 : Math.trunc((t - 4) * period / 8);
555
+ gs.round = { period: period >> 8, phase: phase >> 8, threshold: threshold >> 8 };
548
556
  break;
549
557
  }
550
558
  case 0x1a:
@@ -1024,7 +1032,7 @@ class Execution {
1024
1032
  const z0 = this.zone(gs.zp0), z1 = this.zone(gs.zp1);
1025
1033
  this.point(z0, p2);
1026
1034
  this.point(z1, p1);
1027
- const d = this.project(z0.curX[p2] - z1.curX[p1], z0.curY[p2] - z1.curY[p1]) / 2;
1035
+ const d = Math.trunc(Math.round(this.project(z0.curX[p2] - z1.curX[p1], z0.curY[p2] - z1.curY[p1])) / 2);
1028
1036
  this.move(z1, p1, d);
1029
1037
  this.move(z0, p2, -d);
1030
1038
  break;
@@ -207,7 +207,7 @@ export class ApplyContext {
207
207
  this.initIters();
208
208
  }
209
209
  randomNumber() {
210
- this.randomState = this.randomState * 48271 % 2147483647;
210
+ this.randomState = (Math.imul(this.randomState, 48271) >>> 0) % 2147483647;
211
211
  return this.randomState;
212
212
  }
213
213
  recurse(lookupIndex) {
@@ -421,7 +421,7 @@ class Document {
421
421
  const presentation = {};
422
422
  for (const name in node.attrs)
423
423
  if (PROPERTIES.has(name))
424
- presentation[name] = node.attrs[name];
424
+ presentation[name] = node.attrs[name].trim();
425
425
  apply(presentation);
426
426
  const rules = this.rules.filter(rule => rule.match(node));
427
427
  const inline = node.attrs.style ? parseDeclarations(node.attrs.style) : null;
@@ -1,13 +1,5 @@
1
+ import { base64 } from "../util/bytes.js";
1
2
  const decoded = new WeakMap();
2
- const fromBase64 = Uint8Array.fromBase64;
3
- function base64(text) {
4
- if (fromBase64)
5
- return fromBase64(text);
6
- const binary = atob(text), bytes = new Uint8Array(binary.length);
7
- for (let i = 0; i < binary.length; i++)
8
- bytes[i] = binary.charCodeAt(i);
9
- return bytes;
10
- }
11
3
  function decode(property) {
12
4
  let ranges = decoded.get(property);
13
5
  if (ranges)
@@ -1,3 +1,12 @@
1
+ const fromBase64 = Uint8Array.fromBase64;
2
+ export function base64(text) {
3
+ if (fromBase64)
4
+ return fromBase64(text);
5
+ const binary = atob(text), bytes = new Uint8Array(binary.length);
6
+ for (let i = 0; i < binary.length; i++)
7
+ bytes[i] = binary.charCodeAt(i);
8
+ return bytes;
9
+ }
1
10
  export class ByteWriter {
2
11
  buffer;
3
12
  length = 0;
@@ -192,7 +192,8 @@ function resampleOpaque(src, width, height, outWidth, outHeight, filter, out) {
192
192
  }
193
193
  else {
194
194
  for (let x = 0; x < outWidth; x++, o++) {
195
- let r = 0, g = 0, b = 0, k = 0, s = row + start[x] * 2, w = x * taps;
195
+ let r = 0, g = 0, b = 0, k = 0, s = row + start[x] * 2;
196
+ const w = x * taps;
196
197
  for (; k + 1 < taps; k += 2, s += 4) {
197
198
  const a0 = lanes[s], a1 = lanes[s + 2], w0 = weights[w + k], w1 = weights[w + k + 1];
198
199
  r += (a0 & 0xffff) * w0 + (a1 & 0xffff) * w1;
@@ -367,7 +368,8 @@ function resamplePremultiplied(src, width, height, outWidth, outHeight, filter,
367
368
  a = plane[s + 3] * ONE;
368
369
  }
369
370
  else {
370
- let k = 0, s = row + start[x] * 4, w = x * taps;
371
+ let k = 0, s = row + start[x] * 4;
372
+ const w = x * taps;
371
373
  for (; k + 1 < taps; k += 2, s += 8) {
372
374
  const w0 = weights[w + k], w1 = weights[w + k + 1];
373
375
  r += plane[s] * w0 + plane[s + 4] * w1;
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '3.3.0';
1
+ export const VERSION = '3.4.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imagescript",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "zero-dependency javascript image manipulation",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -24,8 +24,10 @@
24
24
  "scripts": {
25
25
  "build": "node scripts/build.js",
26
26
  "typecheck": "tsc -p tsconfig.json --noEmit",
27
+ "lint": "eslint",
27
28
  "docs": "node scripts/docs.js",
28
29
  "test": "npm run build && node ./tests/run.js",
30
+ "coverage": "node scripts/build.js --source-maps && c8 node ./tests/run.js && node scripts/coverage-report.js",
29
31
  "prepack": "npm run build",
30
32
  "version": "node scripts/version.js && git add src/version.ts jsr.json"
31
33
  },
@@ -49,9 +51,25 @@
49
51
  "picture",
50
52
  "thumbnail",
51
53
  "webp",
54
+ "avif",
55
+ "av1",
56
+ "jxl",
57
+ "jpeg xl",
52
58
  "apng",
53
59
  "tiff",
54
60
  "tga",
61
+ "bmp",
62
+ "ico",
63
+ "cur",
64
+ "qoi",
65
+ "netpbm",
66
+ "ppm",
67
+ "pgm",
68
+ "pbm",
69
+ "pam",
70
+ "pfm",
71
+ "hdr",
72
+ "radiance",
55
73
  "animation",
56
74
  "text rendering",
57
75
  "font"
@@ -66,7 +84,12 @@
66
84
  "node": ">=22.13.0"
67
85
  },
68
86
  "devDependencies": {
87
+ "@eslint/js": "9.39.5",
88
+ "c8": "12.0.0",
89
+ "eslint": "9.39.5",
90
+ "eslint-plugin-sonarjs": "4.2.2",
69
91
  "typedoc": "0.27.9",
70
- "typescript": "5.7.3"
92
+ "typescript": "5.7.3",
93
+ "typescript-eslint": "8.72.0"
71
94
  }
72
95
  }