imagescript 3.2.0 → 3.3.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 +8 -3
  2. package/THIRD_PARTY_NOTICES.md +131 -0
  3. package/dist/animation.d.ts +7 -0
  4. package/dist/animation.js +22 -12
  5. package/dist/codecs/crc.js +5 -0
  6. package/dist/codecs/gif.js +30 -52
  7. package/dist/codecs/jpeg.js +9 -27
  8. package/dist/codecs/png.js +22 -10
  9. package/dist/codecs/tga.js +3 -1
  10. package/dist/codecs/tiff.js +7 -2
  11. package/dist/codecs/vp8.js +3 -1
  12. package/dist/codecs/vp8l.js +126 -58
  13. package/dist/codecs/webp.js +11 -9
  14. package/dist/codecs/zlib.js +81 -69
  15. package/dist/frame.d.ts +1 -0
  16. package/dist/frame.js +4 -2
  17. package/dist/gif.d.ts +1 -0
  18. package/dist/gif.js +2 -2
  19. package/dist/image.d.ts +42 -7
  20. package/dist/image.js +224 -113
  21. package/dist/index.js +4 -4
  22. package/dist/render/bidi.js +350 -0
  23. package/dist/render/buffer.js +362 -0
  24. package/dist/render/color.js +164 -0
  25. package/dist/render/colr.js +523 -0
  26. package/dist/render/font.js +173 -241
  27. package/dist/render/hinting.js +12 -4
  28. package/dist/render/layout.js +1553 -0
  29. package/dist/render/linebreak.js +210 -0
  30. package/dist/render/segment.js +106 -0
  31. package/dist/render/shape.js +984 -0
  32. package/dist/render/shapers/arabic.js +284 -0
  33. package/dist/render/shapers/hangul.js +142 -0
  34. package/dist/render/shapers/hebrew.js +49 -0
  35. package/dist/render/shapers/index.js +52 -0
  36. package/dist/render/shapers/indic.js +579 -0
  37. package/dist/render/shapers/khmer.js +104 -0
  38. package/dist/render/shapers/machine.js +129 -0
  39. package/dist/render/shapers/myanmar.js +121 -0
  40. package/dist/render/shapers/syllabic.js +100 -0
  41. package/dist/render/shapers/thai.js +125 -0
  42. package/dist/render/shapers/use.js +235 -0
  43. package/dist/render/svg.js +101 -22
  44. package/dist/render/text.js +373 -103
  45. package/dist/render/unicode-data.js +63 -0
  46. package/dist/render/unicode.js +58 -0
  47. package/dist/text-layout.d.ts +4 -1
  48. package/dist/text-layout.js +2 -2
  49. package/dist/util/alpha.js +43 -0
  50. package/dist/util/blur.js +80 -5
  51. package/dist/util/bytes.js +31 -0
  52. package/dist/util/limits.js +34 -0
  53. package/dist/util/options.js +7 -0
  54. package/dist/util/resample.js +359 -79
  55. package/dist/util/rotate.js +43 -0
  56. package/dist/version.js +1 -1
  57. package/licenses/HarfBuzz.txt +42 -0
  58. package/licenses/Unicode-3.0.txt +39 -0
  59. package/package.json +1 -1
package/README.md CHANGED
@@ -28,12 +28,15 @@ deno add jsr:@matmen/imagescript # Deno (npm:imagescript works too)
28
28
  - WebPs (lossy, lossless and with alpha; bit-exact with libwebp)
29
29
  - TGAs (color mapped, truecolor and greyscale; uncompressed and run length encoded)
30
30
  - JPEGs, TIFFs, PNGs and WebPs are rotated as their Exif (or, for PNGs, XMP) orientation says
31
+ - images larger than 268 megapixels (`maxPixels`) are rejected before decoding, so small files declaring huge images
32
+ can't exhaust the memory
31
33
  - [Decoding animations](https://imagescript.matmen.dev/classes/Animation.html#decode) (GIF, APNG and WebP) with all
32
34
  frames, durations and disposal modes
33
35
  - [Rendering SVGs](https://imagescript.matmen.dev/classes/Image.html#rendersvg) (shapes, paths, gradients, strokes, clip paths,
34
36
  masks, CSS; no text, images or filters)
35
- - [Rendering vector fonts](https://imagescript.matmen.dev/classes/Image.html#rendertext) (TrueType and OpenType, with kerning,
36
- outlines and TrueType hinting)
37
+ - [Rendering vector fonts](https://imagescript.matmen.dev/classes/Image.html#rendertext) (TrueType and OpenType, with
38
+ complex text shaping in every script, right to left and bidirectional text, outlines, TrueType hinting, fallback
39
+ fonts and color emoji)
37
40
  - [Resizing](https://imagescript.matmen.dev/classes/Image.html#resize) (bilinear, bicubic, area averaging and nearest neighbor)
38
41
  - Image manipulation functions ([crop](https://imagescript.matmen.dev/classes/Image.html#crop)
39
42
  , [rotate](https://imagescript.matmen.dev/classes/Image.html#rotate)
@@ -101,7 +104,9 @@ The JPEG codec is based in part on the work of the Independent JPEG Group (libjp
101
104
  [pdf.js](https://github.com/mozilla/pdf.js), the glyph rasterizer is translated from
102
105
  [font-rs](https://github.com/raphlinus/font-rs). The WebP codecs follow [RFC 9649](https://www.rfc-editor.org/rfc/rfc9649) and [RFC 6386](https://www.rfc-editor.org/rfc/rfc6386)
103
106
  and reproduce [libwebp](https://chromium.googlesource.com/webm/libwebp)'s color conversion, the TrueType hinting follows
104
- [FreeType](https://freetype.org)'s interpreter (portions of this software are copyright © 2026 The FreeType Project).
107
+ [FreeType](https://freetype.org)'s interpreter (portions of this software are copyright © 2026 The FreeType Project),
108
+ the text shaping is ported from [HarfBuzz](https://github.com/harfbuzz/harfbuzz) and uses data of the
109
+ [Unicode Character Database](https://www.unicode.org/ucd/).
105
110
  See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for their licenses.
106
111
 
107
112
  ---
@@ -14,6 +14,10 @@ following projects, under their own licenses. Each derived file says so in its h
14
14
  | [RFC 6386](https://www.rfc-editor.org/rfc/rfc6386) (VP8 Data Format and Decoding Guide, with the libvpx reference decoder) | `src/codecs/vp8-tables.ts` (probability and quantizer tables), `src/codecs/vp8.ts` (decoding procedures) | BSD-3-Clause |
15
15
  | [libwebp](https://chromium.googlesource.com/webm/libwebp) | `src/codecs/vp8.ts` (YUV to RGB conversion, "fancy" chroma upsampling), `src/codecs/webp.ts` (animation frame blending) | BSD-3-Clause |
16
16
  | [FreeType](https://freetype.org) | `src/render/hinting.ts` (behavior and parts of the logic of the TrueType interpreter) | FreeType License |
17
+ | [Pillow](https://github.com/python-pillow/Pillow) | `src/util/resample.ts` (filter coefficients, box, bilinear and bicubic filters) | MIT-CMU |
18
+ | [HarfBuzz](https://github.com/harfbuzz/harfbuzz) | `src/render/buffer.ts`, `src/render/layout.ts`, `src/render/shape.ts`, `src/render/shapers/*` (OpenType shaping: buffer, GSUB/GPOS, normalization, fallback positioning, the script shapers and syllable grammars), `scripts/unicode.js` (shaping category tables) | Old MIT |
19
+ | [Universal Shaping Engine data](https://github.com/harfbuzz/harfbuzz/tree/main/src/ms-use) by Microsoft | `scripts/ms-use` (category overrides, invalid vowel sequences), the tables generated from them in `src/render/unicode-data.ts` | MIT |
20
+ | [Unicode Character Database](https://www.unicode.org/ucd/) | `src/render/unicode-data.ts` (character properties, generated by `scripts/unicode.js`) | Unicode License v3 |
17
21
 
18
22
  ## fflate, Fast CRC32 in JavaScript
19
23
 
@@ -154,3 +158,130 @@ Where the specification leaves details open, its behavior and parts of its logic
154
158
  interpolation of untouched points (IUP), the handling of near-perpendicular freedom and projection vectors, and the
155
159
  tolerance of invalid references. FreeType is used under the FreeType License, included as
156
160
  [licenses/FTL.txt](licenses/FTL.txt).
161
+
162
+ ## Pillow
163
+
164
+ ```
165
+ The Python Imaging Library (PIL) is
166
+
167
+ Copyright © 1997-2011 by Secret Labs AB
168
+ Copyright © 1995-2011 by Fredrik Lundh and contributors
169
+
170
+ Pillow is the friendly PIL fork. It is
171
+
172
+ Copyright © 2010 by Jeffrey 'Alex' Clark and contributors
173
+
174
+ Like PIL, Pillow is licensed under the open source MIT-CMU License:
175
+
176
+ By obtaining, using, and/or copying this software and/or its associated
177
+ documentation, you agree that you have read, understood, and will comply
178
+ with the following terms and conditions:
179
+
180
+ Permission to use, copy, modify and distribute this software and its
181
+ documentation for any purpose and without fee is hereby granted,
182
+ provided that the above copyright notice appears in all copies, and that
183
+ both that copyright notice and this permission notice appear in supporting
184
+ documentation, and that the name of Secret Labs AB or the author not be
185
+ used in advertising or publicity pertaining to distribution of the software
186
+ without specific, written prior permission.
187
+
188
+ SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
189
+ SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
190
+ IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL,
191
+ INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
192
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
193
+ OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
194
+ PERFORMANCE OF THIS SOFTWARE.
195
+ ```
196
+
197
+ The resampling in `src/util/resample.ts` computes its filter coefficients as Pillow's `precompute_coeffs` does (the
198
+ filter stretched by the downscaling factor, the source range rounded around each output pixel's center, the weights
199
+ normalized) and uses Pillow's box, bilinear and bicubic (a = -0.5) filter functions. The integer passes, the fast
200
+ paths and the premultiplied alpha handling are ImageScript's own.
201
+
202
+ ## Unicode Character Database
203
+
204
+ Copyright © 1991-2026 Unicode, Inc.
205
+
206
+ `src/render/unicode-data.ts` is generated by `scripts/unicode.js` from data files of the Unicode Character Database
207
+ (character properties for the bidirectional algorithm, line breaking, text segmentation and shaping). They are used
208
+ under the Unicode License v3, included as [licenses/Unicode-3.0.txt](licenses/Unicode-3.0.txt). The bidirectional
209
+ algorithm (`src/render/bidi.ts`) and the line breaking (`src/render/linebreak.ts`) are written after UAX #9 and
210
+ UAX #14.
211
+
212
+ ## HarfBuzz
213
+
214
+ The shaping engine is ported from HarfBuzz 14.6.0; its license is included as
215
+ [licenses/HarfBuzz.txt](licenses/HarfBuzz.txt). The copyright holders of the ported files are listed in the headers
216
+ of the files derived from them.
217
+
218
+ ```
219
+ HarfBuzz is licensed under the so-called "Old MIT" license. Details follow.
220
+ For parts of HarfBuzz that are licensed under different licenses see individual
221
+ files names COPYING in subdirectories where applicable.
222
+
223
+ Copyright © 2010-2022 Google, Inc.
224
+ Copyright © 2015-2020 Ebrahim Byagowi
225
+ Copyright © 2019,2020 Facebook, Inc.
226
+ Copyright © 2012,2015 Mozilla Foundation
227
+ Copyright © 2011 Codethink Limited
228
+ Copyright © 2008,2010 Nokia Corporation and/or its subsidiary(-ies)
229
+ Copyright © 2009 Keith Stribley
230
+ Copyright © 2011 Martin Hosken and SIL International
231
+ Copyright © 2007 Chris Wilson
232
+ Copyright © 2005,2006,2020,2021,2022,2023 Behdad Esfahbod
233
+ Copyright © 2004,2007,2008,2009,2010,2013,2021,2022,2023 Red Hat, Inc.
234
+ Copyright © 1998-2005 David Turner and Werner Lemberg
235
+ Copyright © 2016 Igalia S.L.
236
+ Copyright © 2022 Matthias Clasen
237
+ Copyright © 2018,2021 Khaled Hosny
238
+ Copyright © 2018,2019,2020 Adobe, Inc
239
+ Copyright © 2013-2015 Alexei Podtelezhnikov
240
+
241
+ For full copyright notices consult the individual files in the package.
242
+
243
+
244
+ Permission is hereby granted, without written agreement and without
245
+ license or royalty fees, to use, copy, modify, and distribute this
246
+ software and its documentation for any purpose, provided that the
247
+ above copyright notice and the following two paragraphs appear in
248
+ all copies of this software.
249
+
250
+ IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
251
+ DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
252
+ ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
253
+ IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
254
+ DAMAGE.
255
+
256
+ THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
257
+ BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
258
+ FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
259
+ ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
260
+ PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
261
+ ```
262
+
263
+ ## Universal Shaping Engine data
264
+
265
+ ```
266
+ MIT License
267
+
268
+ Copyright (c) Microsoft Corporation.
269
+
270
+ Permission is hereby granted, free of charge, to any person obtaining a copy
271
+ of this software and associated documentation files (the "Software"), to deal
272
+ in the Software without restriction, including without limitation the rights
273
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
274
+ copies of the Software, and to permit persons to whom the Software is
275
+ furnished to do so, subject to the following conditions:
276
+
277
+ The above copyright notice and this permission notice shall be included in all
278
+ copies or substantial portions of the Software.
279
+
280
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
281
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
282
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
283
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
284
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
285
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
286
+ SOFTWARE
287
+ ```
@@ -11,6 +11,12 @@ export interface AnimationOptions {
11
11
  export interface AnimationDecodeOptions {
12
12
  /** Whether to stop decoding after the first frame. Default: `false` */
13
13
  onlyFirstFrame?: boolean;
14
+ /**
15
+ * The most pixels to decode, of all frames together (every frame is a full canvas): larger animations throw a
16
+ * RangeError before anything that size is allocated, so small files declaring huge animations (decompression
17
+ * bombs) can't exhaust the memory. Default: `268402689` (16383², like sharp), `Infinity` for no limit
18
+ */
19
+ maxPixels?: number;
14
20
  }
15
21
  /** Options of {@link Animation.encodeWebP}, and those all animation encoders have */
16
22
  export interface AnimationEncodeOptions {
@@ -68,6 +74,7 @@ export declare class Animation extends Array<Frame> {
68
74
  get height(): number;
69
75
  /** The animation's duration (in ms) */
70
76
  get duration(): number;
77
+ /** A short description for debugging, e.g. `Animation<64x64x1000ms>` */
71
78
  toString(): string;
72
79
  /**
73
80
  * Returns a new animation of the same class containing a range of this one's frames
package/dist/animation.js CHANGED
@@ -7,13 +7,14 @@ import { Frame } from "./frame.js";
7
7
  import { ImageType } from "./image-type.js";
8
8
  import { optimize } from "./util/optimize.js";
9
9
  import { resolveMetadata, pngText, exifData, gifComments } from "./metadata.js";
10
- import { isOptions } from "./util/options.js";
10
+ import { isOptions, finite } from "./util/options.js";
11
+ import { MAX_PIXELS, decoding } from "./util/limits.js";
11
12
  const toPlays = (loopCount) => loopCount < 0 ? 0 : loopCount + 1;
12
13
  const fromPlays = (plays) => plays === 0 ? -1 : plays - 1;
13
14
  function canvasFrames(width, height, frames) {
14
15
  return frames.map(({ pixels, duration }) => {
15
16
  const frame = new Frame(width, height, duration, 0, 0, Frame.DISPOSAL_BACKGROUND);
16
- frame.bitmap.set(pixels);
17
+ frame.bitmap = pixels;
17
18
  return frame;
18
19
  });
19
20
  }
@@ -27,7 +28,7 @@ export class Animation extends Array {
27
28
  for (let i = 0; i < this.length; i++)
28
29
  if (!(this[i] instanceof Frame))
29
30
  throw new TypeError(`Frame ${i} is not an instance of Frame`);
30
- if (loopCount < -1 || isNaN(loopCount))
31
+ if (finite(loopCount, 'loop count') < -1)
31
32
  throw new RangeError('Invalid loop count');
32
33
  this.loopCount = loopCount;
33
34
  }
@@ -73,7 +74,6 @@ export class Animation extends Array {
73
74
  return new Class(Array.prototype.slice.call(this, start, end), this.loopCount);
74
75
  }
75
76
  resize(width, height, modeOrOptions) {
76
- const mode = (isOptions(modeOrOptions) ? modeOrOptions.mode : modeOrOptions) ?? Image.RESIZE_BILINEAR;
77
77
  const originalWidth = this.width, originalHeight = this.height;
78
78
  if (width === Image.RESIZE_AUTO && height === Image.RESIZE_AUTO)
79
79
  throw new Error('RESIZE_AUTO can only be used for either width or height, not for both');
@@ -85,7 +85,7 @@ export class Animation extends Array {
85
85
  for (const frame of this) {
86
86
  const left = Math.round(frame.xOffset * sx), top = Math.round(frame.yOffset * sy);
87
87
  const right = Math.round((frame.xOffset + frame.width) * sx), bottom = Math.round((frame.yOffset + frame.height) * sy);
88
- Image.prototype.resize.call(frame, Math.max(1, right - left), Math.max(1, bottom - top), mode);
88
+ Image.prototype.resize.call(frame, Math.max(1, right - left), Math.max(1, bottom - top), modeOrOptions);
89
89
  frame.xOffset = left;
90
90
  frame.yOffset = top;
91
91
  }
@@ -170,27 +170,37 @@ export class Animation extends Array {
170
170
  }));
171
171
  }
172
172
  static async decode(input, onlyFirstFrameOrOptions = false) {
173
- const onlyExtractFirstFrame = isOptions(onlyFirstFrameOrOptions) ? onlyFirstFrameOrOptions.onlyFirstFrame ?? false : onlyFirstFrameOrOptions;
173
+ return decoding(() => Animation.__decode__(input, onlyFirstFrameOrOptions));
174
+ }
175
+ static async __decode__(input, onlyFirstFrameOrOptions) {
176
+ const { onlyFirstFrame: onlyExtractFirstFrame = false, maxPixels = MAX_PIXELS } = isOptions(onlyFirstFrameOrOptions) ? onlyFirstFrameOrOptions : { onlyFirstFrame: onlyFirstFrameOrOptions };
174
177
  const data = view(input);
175
178
  const type = ImageType.getType(data);
176
179
  if (type === 'gif')
177
- return Animation.__fromGIF__(data, onlyExtractFirstFrame, Animation);
180
+ return Animation.__fromGIF__(data, onlyExtractFirstFrame, Animation, maxPixels);
178
181
  if (type === 'webp') {
179
- const decoded = webp.decodeAnimation(data, onlyExtractFirstFrame);
182
+ const decoded = webp.decodeAnimation(data, onlyExtractFirstFrame, maxPixels);
180
183
  const frames = canvasFrames(decoded.width, decoded.height, webp.composite(decoded));
181
184
  return new Animation(frames, fromPlays(decoded.loopCount));
182
185
  }
186
+ if (type === 'png' && !png.isAnimated(data)) {
187
+ const image = await Image.decode(data, { maxPixels, applyOrientation: false });
188
+ return new Animation(canvasFrames(image.width, image.height, [{ pixels: image.bitmap, duration: 0 }]), -1);
189
+ }
183
190
  if (type === 'png') {
184
- const decoded = await png.decodeAnimation(data, onlyExtractFirstFrame);
191
+ const decoded = await png.decodeAnimation(data, onlyExtractFirstFrame, maxPixels);
185
192
  const frames = canvasFrames(decoded.width, decoded.height, png.compositeAnimation(decoded));
186
193
  return new Animation(frames, decoded.frames.length > 1 ? fromPlays(decoded.plays) : -1);
187
194
  }
188
195
  if (type)
189
- return new Animation([Frame.from(await Image.decode(data), 0)]);
196
+ return new Animation([Frame.from(await Image.decode(data, { maxPixels }), 0)]);
190
197
  throw new Error('Unsupported image type');
191
198
  }
192
- static __fromGIF__(data, onlyExtractFirstFrame, Class) {
193
- const decoded = gifCodec.decode(data, onlyExtractFirstFrame);
199
+ static __fromGIF__(data, onlyExtractFirstFrame, Class, maxPixels = MAX_PIXELS) {
200
+ return decoding(() => Animation.__gifFrames__(data, onlyExtractFirstFrame, Class, maxPixels));
201
+ }
202
+ static __gifFrames__(data, onlyExtractFirstFrame, Class, maxPixels) {
203
+ const decoded = gifCodec.decode(data, onlyExtractFirstFrame, maxPixels);
194
204
  const frames = gifCodec.composite(decoded).map(({ pixels, delay }) => ({ pixels, duration: 10 * delay }));
195
205
  return new Class(canvasFrames(decoded.width, decoded.height, frames), decoded.loopCount);
196
206
  }
@@ -2,6 +2,7 @@
2
2
  * Slice-by-16 CRC-32, derived from "Fast CRC32 in JavaScript" (https://gist.github.com/101arrowz/e58695f7ccfdf74f60ba22018093edea)
3
3
  * Copyright (c) 2021 Arjun Barrett, MIT License (see THIRD_PARTY_NOTICES.md)
4
4
  */
5
+ import { nodeZlib } from "./zlib.js";
5
6
  const TABLE = new Int32Array(16 * 256);
6
7
  for (let n = 0; n < 256; n++) {
7
8
  let c = n;
@@ -14,7 +15,11 @@ for (let n = 0; n < 256; n++) {
14
15
  for (let k = 256; k < TABLE.length; k += 256)
15
16
  c = TABLE[k + n] = TABLE[c & 0xff] ^ c >>> 8;
16
17
  }
18
+ const native = nodeZlib && nodeZlib.crc32 ? nodeZlib.crc32.bind(nodeZlib) : null;
19
+ const NATIVE_MIN = 64;
17
20
  export function crc32(buffer) {
21
+ if (native && buffer.length >= NATIVE_MIN)
22
+ return native(buffer) >>> 0;
18
23
  let crc = -1, i = 0;
19
24
  for (const end = buffer.length - 16; i <= end; i += 16) {
20
25
  crc = TABLE[3840 + ((buffer[i] ^ crc) & 0xff)] ^ TABLE[3584 + ((buffer[i + 1] ^ crc >> 8) & 0xff)]
@@ -1,10 +1,12 @@
1
+ import { ByteWriter } from "../util/bytes.js";
1
2
  import { quantize } from "./quantize.js";
3
+ import { MAX_PIXELS, PixelBudget, checkPixels } from "../util/limits.js";
2
4
  const DISPOSAL_TO_GIF = [0, 1, 3, 2];
3
5
  const DISPOSAL_FROM_GIF = [0, 1, 3, 2, 0, 0, 0, 0];
4
6
  const MAX_CODE = 4095;
5
7
  const UNEXPECTED_END = new Error('GIF: unexpected end of data');
6
8
  export function encode({ width, height, loopCount = -1, frames }, quality = 95, comments = []) {
7
- const out = new ByteWriter();
9
+ const out = new GIFWriter();
8
10
  out.bytes(0x47, 0x49, 0x46, 0x38, 0x39, 0x61);
9
11
  out.u16(width);
10
12
  out.u16(height);
@@ -47,7 +49,7 @@ export function encode({ width, height, loopCount = -1, frames }, quality = 95,
47
49
  return out.finish();
48
50
  }
49
51
  function lzwEncode(indices, minCodeSize) {
50
- const out = new ByteWriter(indices.length);
52
+ const out = new GIFWriter(indices.length);
51
53
  const clear = 1 << minCodeSize;
52
54
  const eoi = clear + 1;
53
55
  const dictionary = new Int16Array(4096 * 256);
@@ -110,7 +112,7 @@ function writeSubBlocks(out, data) {
110
112
  }
111
113
  out.byte(0x00);
112
114
  }
113
- export function decode(data, onlyFirstFrame = false) {
115
+ export function decode(data, onlyFirstFrame = false, maxPixels = MAX_PIXELS) {
114
116
  const reader = new ByteReader(data);
115
117
  const signature = reader.string(6);
116
118
  if (signature !== 'GIF87a' && signature !== 'GIF89a')
@@ -123,6 +125,7 @@ export function decode(data, onlyFirstFrame = false) {
123
125
  const frames = [];
124
126
  let loopCount = 0;
125
127
  let control = null;
128
+ const budget = new PixelBudget(maxPixels);
126
129
  try {
127
130
  parse();
128
131
  }
@@ -130,6 +133,7 @@ export function decode(data, onlyFirstFrame = false) {
130
133
  if (err !== UNEXPECTED_END)
131
134
  throw err;
132
135
  }
136
+ checkPixels(width, height, maxPixels, Math.max(1, frames.length));
133
137
  return { width, height, loopCount, frames };
134
138
  function parse() {
135
139
  while (reader.offset < data.length) {
@@ -183,28 +187,26 @@ export function decode(data, onlyFirstFrame = false) {
183
187
  const table = localTable || globalTable;
184
188
  const minCodeSize = reader.u8();
185
189
  const compressed = readSubBlocks(reader);
190
+ budget.take(fw, fh);
186
191
  const indices = lzwDecode(compressed, minCodeSize, fw * fh);
187
192
  const { dispose = 0, delay = 0, transparentIndex = -1 } = control || {};
188
193
  control = null;
189
194
  if (!fw || !fh)
190
195
  continue;
191
- const pixels = new Uint8Array(fw * fh * 4);
192
- const rows = interlaced ? interlacedRows(fh) : null;
193
- const colors = new Uint8Array(256 * 4);
196
+ const colors = new Uint32Array(256), bytes = new Uint8Array(colors.buffer);
194
197
  for (let index = 0; index < 256; index++) {
195
- if (index === transparentIndex)
196
- continue;
197
198
  if (table && index * 3 + 2 < table.length)
198
- colors.set(table.subarray(index * 3, index * 3 + 3), index * 4);
199
- colors[index * 4 + 3] = 0xff;
199
+ bytes.set(table.subarray(index * 3, index * 3 + 3), index * 4);
200
+ bytes[index * 4 + 3] = 0xff;
200
201
  }
201
- const lookup = new Uint32Array(colors.buffer), out = new Uint32Array(pixels.buffer);
202
- for (let row = 0; row < fh; row++) {
203
- const target = (rows ? rows[row] : row) * fw, source = row * fw;
204
- for (let column = 0; column < fw; column++)
205
- out[target + column] = lookup[indices[source + column]];
202
+ let ordered = indices;
203
+ if (interlaced) {
204
+ const rows = interlacedRows(fh);
205
+ ordered = new Uint8Array(indices.length);
206
+ for (let row = 0; row < fh; row++)
207
+ ordered.set(indices.subarray(row * fw, (row + 1) * fw), rows[row] * fw);
206
208
  }
207
- frames.push({ x, y, width: fw, height: fh, delay, dispose, pixels });
209
+ frames.push({ x, y, width: fw, height: fh, delay, dispose, indices: ordered, colors, transparentIndex });
208
210
  if (onlyFirstFrame)
209
211
  break;
210
212
  }
@@ -215,15 +217,21 @@ export function composite({ width, height, frames }) {
215
217
  const output = [];
216
218
  for (const frame of frames) {
217
219
  const previous = frame.dispose === 2 ? canvas.slice() : null;
218
- const src = new Uint32Array(frame.pixels.buffer, frame.pixels.byteOffset, frame.width * frame.height);
220
+ const { indices, colors, transparentIndex } = frame;
219
221
  const x0 = Math.min(frame.x, width), x1 = Math.min(frame.x + frame.width, width);
220
222
  const y0 = Math.min(frame.y, height), y1 = Math.min(frame.y + frame.height, height);
221
223
  for (let y = y0; y < y1; y++) {
222
224
  const srcRow = (y - frame.y) * frame.width - frame.x;
223
225
  const dstRow = y * width;
224
- for (let x = x0; x < x1; x++)
225
- if (frame.pixels[(srcRow + x) * 4 + 3] !== 0)
226
- canvas[dstRow + x] = src[srcRow + x];
226
+ if (transparentIndex < 0)
227
+ for (let x = x0; x < x1; x++)
228
+ canvas[dstRow + x] = colors[indices[srcRow + x]];
229
+ else
230
+ for (let x = x0; x < x1; x++) {
231
+ const index = indices[srcRow + x];
232
+ if (index !== transparentIndex)
233
+ canvas[dstRow + x] = colors[index];
234
+ }
227
235
  }
228
236
  output.push({ pixels: new Uint8Array(canvas.slice().buffer), delay: frame.delay });
229
237
  if (frame.dispose === 3) {
@@ -373,42 +381,12 @@ class ByteReader {
373
381
  return String.fromCharCode(...this.slice(length));
374
382
  }
375
383
  }
376
- class ByteWriter {
377
- buffer;
378
- length;
379
- constructor(capacity = 1024) {
380
- this.buffer = new Uint8Array(Math.max(16, capacity));
381
- this.length = 0;
382
- }
383
- reserve(size) {
384
- if (this.length + size <= this.buffer.length)
385
- return;
386
- const buffer = new Uint8Array(Math.max(this.buffer.length * 2, this.length + size));
387
- buffer.set(this.buffer.subarray(0, this.length));
388
- this.buffer = buffer;
389
- }
390
- byte(value) {
391
- this.reserve(1);
392
- this.buffer[this.length++] = value;
393
- }
394
- bytes(...values) {
395
- for (const value of values)
396
- this.byte(value);
397
- }
384
+ class GIFWriter extends ByteWriter {
398
385
  u16(value) {
399
- this.byte(value & 0xff);
400
- this.byte(value >> 8 & 0xff);
386
+ this.bytes(value & 0xff, value >> 8 & 0xff);
401
387
  }
402
388
  string(value) {
403
389
  for (let i = 0; i < value.length; i++)
404
390
  this.byte(value.charCodeAt(i));
405
391
  }
406
- array(values) {
407
- this.reserve(values.length);
408
- this.buffer.set(values, this.length);
409
- this.length += values.length;
410
- }
411
- finish() {
412
- return this.buffer.slice(0, this.length);
413
- }
414
392
  }
@@ -4,13 +4,15 @@
4
4
  * (IJG license, see licenses/README.ijg). The scan decoder follows the structure of pdf.js' jpg.js
5
5
  * (Copyright 2014 Mozilla Foundation, Apache License 2.0). See THIRD_PARTY_NOTICES.md
6
6
  */
7
+ import { ByteWriter } from "../util/bytes.js";
8
+ import { MAX_PIXELS, checkPixels } from "../util/limits.js";
7
9
  import { orientation as exifOrientation } from "./exif.js";
8
10
  import { concat } from "../util/mem.js";
9
11
  const ZIGZAG = Int32Array.of(0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63);
10
12
  function clamp8(v) {
11
13
  return v < 0 ? 0 : v > 255 ? 255 : v;
12
14
  }
13
- export function decode(data, { transform } = {}) {
15
+ export function decode(data, { transform, maxPixels = MAX_PIXELS } = {}) {
14
16
  if (data[0] !== 0xff || data[1] !== 0xd8)
15
17
  throw new Error('JPEG: invalid signature');
16
18
  const quantizationTables = [];
@@ -84,6 +86,7 @@ export function decode(data, { transform } = {}) {
84
86
  throw new Error('JPEG: multiple frames are not supported');
85
87
  if (data[start] !== 8)
86
88
  throw new Error(`JPEG: ${data[start]}-bit precision is not supported`);
89
+ checkPixels(u16(start + 3), u16(start + 1), maxPixels);
87
90
  frame = createFrame(marker === 0xc2, u16(start + 1), u16(start + 3), data.subarray(start + 5, end));
88
91
  break;
89
92
  }
@@ -158,6 +161,8 @@ function createFrame(progressive, height, width, componentData) {
158
161
  }
159
162
  if (!width || !sampling.length)
160
163
  throw new Error('JPEG: invalid dimensions');
164
+ if (sampling.length > 4)
165
+ throw new Error(`JPEG: ${sampling.length} components are not supported`);
161
166
  const hmax = Math.max(...sampling.map(c => c.h));
162
167
  const vmax = Math.max(...sampling.map(c => c.v));
163
168
  const mcusPerLine = Math.ceil(width / (8 * hmax));
@@ -946,29 +951,9 @@ function fdctPass(d, o, s) {
946
951
  d[o + s] = z11 + z4;
947
952
  d[o + 7 * s] = z11 - z4;
948
953
  }
949
- class BitWriter {
950
- buffer;
951
- length;
952
- bits;
953
- count;
954
- constructor(capacity) {
955
- this.buffer = new Uint8Array(Math.max(1024, capacity));
956
- this.length = 0;
957
- this.bits = 0;
958
- this.count = 0;
959
- }
960
- reserve(size) {
961
- if (this.length + size <= this.buffer.length)
962
- return;
963
- const buffer = new Uint8Array(Math.max(this.buffer.length * 2, this.length + size));
964
- buffer.set(this.buffer.subarray(0, this.length));
965
- this.buffer = buffer;
966
- }
967
- bytes(...values) {
968
- this.reserve(values.length);
969
- for (const value of values)
970
- this.buffer[this.length++] = value;
971
- }
954
+ class BitWriter extends ByteWriter {
955
+ bits = 0;
956
+ count = 0;
972
957
  write(value, size) {
973
958
  this.bits = this.bits << size | value & (1 << size) - 1;
974
959
  this.count += size;
@@ -986,7 +971,4 @@ class BitWriter {
986
971
  if (this.count)
987
972
  this.write((1 << 8 - this.count) - 1, 8 - this.count);
988
973
  }
989
- finish() {
990
- return this.buffer.slice(0, this.length);
991
- }
992
974
  }
@@ -1,4 +1,5 @@
1
1
  import { crc32 } from "./crc.js";
2
+ import { MAX_PIXELS, checkPixels } from "../util/limits.js";
2
3
  import { concat } from "../util/mem.js";
3
4
  import { encode as utf8, decode as utf8Decode } from "../util/utf8.js";
4
5
  import { orientation as exifOrientation, xmpOrientation } from "./exif.js";
@@ -437,15 +438,16 @@ async function decodeImageData(p, chunks, width, height) {
437
438
  continue;
438
439
  const rowBytes = Math.ceil(pw * bitsPerPixel / 8);
439
440
  const size = ph * rowBytes;
440
- const pixels = unfilter(data, dataOffset, ph, rowBytes, bpp);
441
+ const pixels = unfilter(data, dataOffset, ph, rowBytes, bpp, !interlace);
441
442
  dataOffset += size + ph;
442
443
  toRGBA(pixels, rowBytes, pw, ph, rgba, { row: y => ((ys + y * dy) * width + xs) * 4, step: dx * 4 });
443
444
  }
444
445
  return rgba;
445
446
  }
446
- export async function decode(array) {
447
+ export async function decode(array, maxPixels = MAX_PIXELS) {
447
448
  const p = parse(array);
448
449
  const { width, height } = p.header;
450
+ checkPixels(width, height, maxPixels);
449
451
  const pixels = await decodeImageData(p, p.idat, width, height);
450
452
  return { width, height, pixels, orientation: p.exif ? exifOrientation(p.exif) : await textOrientation(p.text) };
451
453
  }
@@ -458,9 +460,10 @@ export function isAnimated(array) {
458
460
  return false;
459
461
  }
460
462
  }
461
- export async function decodeAnimation(array, onlyFirstFrame = false) {
463
+ export async function decodeAnimation(array, onlyFirstFrame = false, maxPixels = MAX_PIXELS) {
462
464
  const p = parse(array);
463
465
  const { width, height } = p.header;
466
+ checkPixels(width, height, maxPixels, !p.animation || p.animationError || onlyFirstFrame ? 1 : Math.max(1, p.frames.length));
464
467
  if (!p.animation || p.animationError) {
465
468
  const pixels = await decodeImageData(p, p.idat, width, height);
466
469
  return { width, height, plays: 0, frames: [{ x: 0, y: 0, width, height, duration: 0, dispose: 0, blend: 0, pixels }] };
@@ -485,13 +488,21 @@ export function compositeAnimation({ width, height, frames }) {
485
488
  const output = [];
486
489
  for (const f of frames) {
487
490
  const previous = f.dispose === 2 ? canvas.slice() : null;
488
- for (let y = 0; y < f.height; y++)
491
+ for (let y = 0; y < f.height; y++) {
492
+ if (f.blend === 0) {
493
+ canvas.set(f.pixels.subarray(y * f.width * 4, (y + 1) * f.width * 4), ((f.y + y) * width + f.x) * 4);
494
+ continue;
495
+ }
489
496
  for (let x = 0; x < f.width; x++) {
490
497
  const s = (y * f.width + x) * 4, d = ((f.y + y) * width + f.x + x) * 4;
491
498
  const sa = f.pixels[s + 3], da = canvas[d + 3];
492
- if (f.blend === 0 || sa === 255 || da === 0) {
493
- if (f.blend === 0 || sa !== 0)
494
- canvas.set(f.pixels.subarray(s, s + 4), d);
499
+ if (sa === 255 || da === 0) {
500
+ if (sa !== 0) {
501
+ canvas[d] = f.pixels[s];
502
+ canvas[d + 1] = f.pixels[s + 1];
503
+ canvas[d + 2] = f.pixels[s + 2];
504
+ canvas[d + 3] = sa;
505
+ }
495
506
  }
496
507
  else if (sa !== 0) {
497
508
  const u = sa * 255, v = (255 - sa) * da, al = u + v;
@@ -500,6 +511,7 @@ export function compositeAnimation({ width, height, frames }) {
500
511
  canvas[d + 3] = al / 255 | 0;
501
512
  }
502
513
  }
514
+ }
503
515
  output.push({ pixels: canvas.slice(), duration: f.duration });
504
516
  if (f.dispose === 1)
505
517
  for (let y = 0; y < f.height; y++)
@@ -550,11 +562,11 @@ function hexProfile(text) {
550
562
  const digits = match[2].replace(/\s+/g, '');
551
563
  const out = new Uint8Array(Math.min(+match[1], digits.length >> 1));
552
564
  for (let i = 0; i < out.length; i++)
553
- out[i] = parseInt(digits.substr(i * 2, 2), 16);
565
+ out[i] = parseInt(digits.slice(i * 2, i * 2 + 2), 16);
554
566
  return out;
555
567
  }
556
- function unfilter(data, offset, height, rowBytes, bpp) {
557
- const out = new Uint8Array(height * rowBytes);
568
+ function unfilter(data, offset, height, rowBytes, bpp, inPlace = false) {
569
+ const out = inPlace ? data.subarray(offset, offset + height * rowBytes) : new Uint8Array(height * rowBytes);
558
570
  const first = Math.min(bpp, rowBytes);
559
571
  for (let y = 0; y < height; y++) {
560
572
  const filter = data[offset++];
@@ -1,4 +1,5 @@
1
1
  import { encode as utf8 } from "../util/utf8.js";
2
+ import { MAX_PIXELS, checkPixels } from "../util/limits.js";
2
3
  const SIGNATURE = 'TRUEVISION-XFILE.\0';
3
4
  function readHeader(d) {
4
5
  if (d.length < 18)
@@ -69,11 +70,12 @@ function readColor(d, o, bits, alphaBits, out, p) {
69
70
  out[p + 3] = bits === 32 && alphaBits ? d[o + 3] : 255;
70
71
  }
71
72
  }
72
- export function decode(d) {
73
+ export function decode(d, maxPixels = MAX_PIXELS) {
73
74
  const h = readHeader(d);
74
75
  if (!h || !isTGA(d))
75
76
  throw new Error('TGA: invalid header');
76
77
  const { width, height, depth } = h;
78
+ checkPixels(width, height, maxPixels);
77
79
  const base = h.imageType & ~8, rle = (h.imageType & 8) !== 0;
78
80
  const bytesPerPixel = Math.ceil(depth / 8);
79
81
  let offset = 18 + h.idLength;