imagescript 3.2.1 → 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 (101) hide show
  1. package/README.md +35 -10
  2. package/THIRD_PARTY_NOTICES.md +298 -2
  3. package/dist/animation.d.ts +25 -8
  4. package/dist/animation.js +44 -13
  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/crc.js +5 -0
  21. package/dist/codecs/gif.js +10 -35
  22. package/dist/codecs/hdr.d.ts +25 -0
  23. package/dist/codecs/hdr.js +201 -0
  24. package/dist/codecs/ico.js +48 -0
  25. package/dist/codecs/jpeg.js +11 -28
  26. package/dist/codecs/jxl/bits.js +126 -0
  27. package/dist/codecs/jxl/blend.js +117 -0
  28. package/dist/codecs/jxl/color.js +116 -0
  29. package/dist/codecs/jxl/dct.js +404 -0
  30. package/dist/codecs/jxl/entropy.js +556 -0
  31. package/dist/codecs/jxl/features.js +325 -0
  32. package/dist/codecs/jxl/filters.js +119 -0
  33. package/dist/codecs/jxl/frame-header.js +195 -0
  34. package/dist/codecs/jxl/frame.js +319 -0
  35. package/dist/codecs/jxl/headers.js +249 -0
  36. package/dist/codecs/jxl/idct.js +484 -0
  37. package/dist/codecs/jxl/modular.js +688 -0
  38. package/dist/codecs/jxl/quant.js +219 -0
  39. package/dist/codecs/jxl/upsample.js +52 -0
  40. package/dist/codecs/jxl/vardct.js +472 -0
  41. package/dist/codecs/jxl.js +89 -0
  42. package/dist/codecs/netpbm.js +239 -0
  43. package/dist/codecs/png.js +22 -10
  44. package/dist/codecs/qoi.js +150 -0
  45. package/dist/codecs/tga.js +3 -1
  46. package/dist/codecs/tiff.js +7 -2
  47. package/dist/codecs/vp8-enc-dsp.js +932 -0
  48. package/dist/codecs/vp8-enc-tables.js +78 -0
  49. package/dist/codecs/vp8-enc.js +1930 -0
  50. package/dist/codecs/vp8.js +3 -1
  51. package/dist/codecs/vp8l.js +126 -58
  52. package/dist/codecs/webp.js +47 -11
  53. package/dist/codecs/zlib.js +81 -69
  54. package/dist/frame.d.ts +1 -0
  55. package/dist/frame.js +4 -2
  56. package/dist/gif.d.ts +1 -0
  57. package/dist/gif.js +2 -2
  58. package/dist/image-type.d.ts +15 -1
  59. package/dist/image-type.js +45 -2
  60. package/dist/image.d.ts +89 -13
  61. package/dist/image.js +266 -113
  62. package/dist/index.d.ts +3 -2
  63. package/dist/index.js +4 -4
  64. package/dist/render/bidi.js +350 -0
  65. package/dist/render/buffer.js +362 -0
  66. package/dist/render/color.js +164 -0
  67. package/dist/render/colr.js +523 -0
  68. package/dist/render/font.js +173 -241
  69. package/dist/render/hinting.js +38 -22
  70. package/dist/render/layout.js +1553 -0
  71. package/dist/render/linebreak.js +210 -0
  72. package/dist/render/segment.js +106 -0
  73. package/dist/render/shape.js +984 -0
  74. package/dist/render/shapers/arabic.js +284 -0
  75. package/dist/render/shapers/hangul.js +142 -0
  76. package/dist/render/shapers/hebrew.js +49 -0
  77. package/dist/render/shapers/index.js +52 -0
  78. package/dist/render/shapers/indic.js +579 -0
  79. package/dist/render/shapers/khmer.js +104 -0
  80. package/dist/render/shapers/machine.js +129 -0
  81. package/dist/render/shapers/myanmar.js +121 -0
  82. package/dist/render/shapers/syllabic.js +100 -0
  83. package/dist/render/shapers/thai.js +125 -0
  84. package/dist/render/shapers/use.js +235 -0
  85. package/dist/render/svg.js +102 -23
  86. package/dist/render/text.js +373 -103
  87. package/dist/render/unicode-data.js +63 -0
  88. package/dist/render/unicode.js +50 -0
  89. package/dist/text-layout.d.ts +4 -1
  90. package/dist/text-layout.js +2 -2
  91. package/dist/util/alpha.js +43 -0
  92. package/dist/util/blur.js +20 -33
  93. package/dist/util/bytes.js +40 -0
  94. package/dist/util/limits.js +34 -0
  95. package/dist/util/options.js +7 -0
  96. package/dist/util/resample.js +126 -7
  97. package/dist/util/rotate.js +43 -0
  98. package/dist/version.js +1 -1
  99. package/licenses/HarfBuzz.txt +42 -0
  100. package/licenses/Unicode-3.0.txt +39 -0
  101. package/package.json +25 -2
@@ -0,0 +1,317 @@
1
+ import { MAX_PIXELS, checkPixels } from "../util/limits.js";
2
+ const BI_RGB = 0, BI_RLE8 = 1, BI_RLE4 = 2, BI_BITFIELDS = 3, BI_JPEG = 4, BI_PNG = 5, BI_ALPHABITFIELDS = 6;
3
+ const u16 = (d, o) => d[o] | d[o + 1] << 8;
4
+ const u32 = (d, o) => (d[o] | d[o + 1] << 8 | d[o + 2] << 16 | d[o + 3] << 24) >>> 0;
5
+ const i32 = (d, o) => d[o] | d[o + 1] << 8 | d[o + 2] << 16 | d[o + 3] << 24;
6
+ const WINDOWS_HEADERS = [40, 52, 56, 108, 124];
7
+ const isHeaderSize = (size) => size === 12 || WINDOWS_HEADERS.includes(size) || (size >= 16 && size <= 64 && size % 4 === 0);
8
+ export function isBMP(d) {
9
+ return d.length >= 26 && d[0] === 0x42 && d[1] === 0x4d && isHeaderSize(u32(d, 14));
10
+ }
11
+ function channel(mask) {
12
+ mask >>>= 0;
13
+ let shift = 0;
14
+ if (mask)
15
+ while (!(mask >>> shift & 1))
16
+ shift++;
17
+ const max = mask >>> shift;
18
+ let table = null;
19
+ if (max && max < 65536) {
20
+ table = new Uint8Array(max + 1);
21
+ for (let v = 0; v <= max; v++)
22
+ table[v] = Math.round(v * 255 / max);
23
+ }
24
+ return { mask, shift, max, table };
25
+ }
26
+ function sample(c, v) {
27
+ if (!c.max)
28
+ return 0;
29
+ const x = (v & c.mask) >>> c.shift;
30
+ return c.table ? c.table[x] : Math.round(x * 255 / c.max);
31
+ }
32
+ function readInfo(d, o) {
33
+ const size = u32(d, o);
34
+ if (!isHeaderSize(size))
35
+ throw new Error(`BMP: unknown header size ${size}`);
36
+ if (o + size > d.length)
37
+ throw new Error('BMP: truncated header');
38
+ if (size === 12) {
39
+ return { width: u16(d, o + 4), height: u16(d, o + 6), topDown: false, bits: u16(d, o + 10), compression: BI_RGB, headerEnd: o + 12, entryBytes: 3, colors: 0, masks: null };
40
+ }
41
+ const os2 = !WINDOWS_HEADERS.includes(size);
42
+ const width = i32(d, o + 4), rawHeight = i32(d, o + 8);
43
+ const bits = u16(d, o + 14);
44
+ let compression = size >= 20 ? u32(d, o + 16) : BI_RGB;
45
+ if (os2 && (compression === 3 || compression === 4))
46
+ throw new Error(`BMP: unsupported OS/2 compression (${compression === 3 ? 'Huffman 1D' : 'RLE24'})`);
47
+ const colors = size >= 36 ? u32(d, o + 32) : 0;
48
+ let masks = null;
49
+ let headerEnd = o + size;
50
+ if (compression === BI_BITFIELDS || compression === BI_ALPHABITFIELDS) {
51
+ if (size >= 52)
52
+ masks = [u32(d, o + 40), u32(d, o + 44), u32(d, o + 48), size >= 56 ? u32(d, o + 52) : 0];
53
+ else {
54
+ const count = compression === BI_ALPHABITFIELDS ? 4 : 3;
55
+ if (headerEnd + count * 4 > d.length)
56
+ throw new Error('BMP: truncated bit fields');
57
+ masks = [u32(d, headerEnd), u32(d, headerEnd + 4), u32(d, headerEnd + 8), count === 4 ? u32(d, headerEnd + 12) : 0];
58
+ headerEnd += count * 4;
59
+ }
60
+ compression = BI_BITFIELDS;
61
+ }
62
+ if (width <= 0 || rawHeight === 0 || rawHeight === -0x80000000)
63
+ throw new Error(`BMP: invalid dimensions ${width}x${rawHeight}`);
64
+ return { width, height: Math.abs(rawHeight), topDown: rawHeight < 0, bits, compression, headerEnd, entryBytes: 4, colors, masks };
65
+ }
66
+ function readPalette(d, info, end) {
67
+ const { bits, entryBytes, headerEnd } = info;
68
+ if (bits > 8)
69
+ return null;
70
+ let count = info.colors || 1 << bits;
71
+ count = Math.max(0, Math.min(count, Math.floor((Math.min(end, d.length) - headerEnd) / entryBytes)));
72
+ const palette = new Uint32Array(Math.max(count, 1 << bits));
73
+ palette.fill(0xff000000);
74
+ for (let i = 0; i < count; i++) {
75
+ const p = headerEnd + i * entryBytes;
76
+ palette[i] = (0xff000000 | d[p] << 16 | d[p + 1] << 8 | d[p + 2]) >>> 0;
77
+ }
78
+ return palette;
79
+ }
80
+ export function decodeDIB(d, o, pixelOffset, maxPixels, ico = false) {
81
+ const info = readInfo(d, o);
82
+ const { width, bits, compression, topDown } = info;
83
+ const height = ico ? info.height >> 1 : info.height;
84
+ if (!height)
85
+ throw new Error('BMP: invalid dimensions');
86
+ if (compression === BI_JPEG || compression === BI_PNG) {
87
+ const start = pixelOffset ?? info.headerEnd;
88
+ return { embedded: compression === BI_JPEG ? 'jpeg' : 'png', data: d.subarray(start) };
89
+ }
90
+ if (![1, 2, 4, 8, 16, 24, 32, 64].includes(bits))
91
+ throw new Error(`BMP: unsupported bit depth ${bits}`);
92
+ if (compression === BI_RLE8 && bits !== 8 || compression === BI_RLE4 && bits !== 4)
93
+ throw new Error('BMP: RLE compression of the wrong bit depth');
94
+ if (compression === BI_BITFIELDS && bits !== 16 && bits !== 32)
95
+ throw new Error('BMP: bit fields need 16 or 32 bit pixels');
96
+ if (compression > BI_PNG)
97
+ throw new Error(`BMP: unsupported compression ${compression}`);
98
+ checkPixels(width, height, maxPixels);
99
+ const palette = readPalette(d, info, pixelOffset ?? d.length);
100
+ const start = pixelOffset ?? info.headerEnd + (bits <= 8 ? info.colors || 1 << bits : info.colors) * info.entryBytes;
101
+ const pixels = new Uint8Array(width * height * 4);
102
+ const out = new Uint32Array(pixels.buffer);
103
+ const row = (y) => (topDown ? y : height - 1 - y) * width;
104
+ if (compression === BI_RLE8 || compression === BI_RLE4) {
105
+ decodeRLE(d, start, width, height, compression === BI_RLE4, palette, out, row);
106
+ return { width, height, pixels };
107
+ }
108
+ const stride = (width * bits + 31 >>> 5) * 4;
109
+ if (start + stride * (height - 1) + Math.ceil(width * bits / 8) > d.length)
110
+ throw new Error('BMP: truncated pixel data');
111
+ if (palette) {
112
+ const mask = (1 << bits) - 1;
113
+ for (let y = 0; y < height; y++) {
114
+ const r = start + y * stride, o = row(y);
115
+ for (let x = 0; x < width; x++) {
116
+ const bit = x * bits;
117
+ out[o + x] = palette[d[r + (bit >> 3)] >> (8 - bits - (bit & 7)) & mask];
118
+ }
119
+ }
120
+ }
121
+ else if (bits === 24) {
122
+ for (let y = 0; y < height; y++) {
123
+ const r = start + y * stride, o = row(y);
124
+ for (let x = 0; x < width; x++) {
125
+ const p = r + x * 3;
126
+ out[o + x] = (0xff000000 | d[p] << 16 | d[p + 1] << 8 | d[p + 2]) >>> 0;
127
+ }
128
+ }
129
+ }
130
+ else if (bits === 64) {
131
+ const srgb = (v) => {
132
+ const c = Math.min(1, Math.max(0, v / 8192));
133
+ return Math.round(255 * (c <= 0.0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055));
134
+ };
135
+ const s16 = (p) => d[p] | d[p + 1] << 8 << 16 >> 16;
136
+ for (let y = 0; y < height; y++) {
137
+ const r = start + y * stride, o = row(y) * 4;
138
+ for (let x = 0; x < width; x++) {
139
+ const p = r + x * 8, q = o + x * 4;
140
+ pixels[q] = srgb(s16(p + 4));
141
+ pixels[q + 1] = srgb(s16(p + 2));
142
+ pixels[q + 2] = srgb(s16(p));
143
+ pixels[q + 3] = Math.round(Math.min(1, Math.max(0, s16(p + 6) / 8192)) * 255);
144
+ }
145
+ }
146
+ }
147
+ else {
148
+ const masks = info.masks ?? (bits === 16 ? [0x7c00, 0x3e0, 0x1f, 0] : [0xff0000, 0xff00, 0xff, ico && bits === 32 ? 0xff000000 : 0]);
149
+ const [cr, cg, cb, ca] = masks.map(channel);
150
+ const fast = bits === 32 && cr.mask === 0xff0000 && cg.mask === 0xff00 && cb.mask === 0xff;
151
+ let alphaSeen = false;
152
+ for (let y = 0; y < height; y++) {
153
+ const r = start + y * stride, o = row(y);
154
+ for (let x = 0; x < width; x++) {
155
+ const v = bits === 16 ? u16(d, r + x * 2) : u32(d, r + x * 4);
156
+ const a = ca.max ? sample(ca, v) : 255;
157
+ if (ca.max && a)
158
+ alphaSeen = true;
159
+ out[o + x] = fast
160
+ ? (a << 24 | (v & 0xff) << 16 | v & 0xff00 | v >> 16 & 0xff) >>> 0
161
+ : (a << 24 | sample(cb, v) << 16 | sample(cg, v) << 8 | sample(cr, v)) >>> 0;
162
+ }
163
+ }
164
+ if (ca.max && !alphaSeen)
165
+ for (let i = 0; i < out.length; i++)
166
+ out[i] |= 0xff000000;
167
+ if (ico && bits === 32 && alphaSeen)
168
+ return { width, height, pixels };
169
+ }
170
+ if (ico) {
171
+ const maskStart = start + stride * height, maskStride = (width + 31 >>> 5) * 4;
172
+ if (maskStart + maskStride * (height - 1) + Math.ceil(width / 8) <= d.length) {
173
+ for (let y = 0; y < height; y++) {
174
+ const r = maskStart + y * maskStride, o = row(y);
175
+ for (let x = 0; x < width; x++)
176
+ if (d[r + (x >> 3)] >> (7 - (x & 7)) & 1)
177
+ out[o + x] = 0;
178
+ }
179
+ }
180
+ }
181
+ return { width, height, pixels };
182
+ }
183
+ function decodeRLE(d, o, width, height, four, palette, out, row) {
184
+ let x = 0, y = 0;
185
+ const put = (index) => {
186
+ if (x < width && y < height)
187
+ out[row(y) + x] = palette[index];
188
+ x++;
189
+ };
190
+ while (y < height) {
191
+ if (o + 1 >= d.length)
192
+ throw new Error('BMP: truncated RLE data');
193
+ const n = d[o], v = d[o + 1];
194
+ o += 2;
195
+ if (n) {
196
+ for (let i = 0; i < n; i++)
197
+ put(four ? (i & 1 ? v & 15 : v >> 4) : v);
198
+ }
199
+ else if (v === 0) {
200
+ x = 0;
201
+ y++;
202
+ }
203
+ else if (v === 1) {
204
+ break;
205
+ }
206
+ else if (v === 2) {
207
+ if (o + 1 >= d.length)
208
+ throw new Error('BMP: truncated RLE data');
209
+ x += d[o];
210
+ y += d[o + 1];
211
+ o += 2;
212
+ }
213
+ else {
214
+ const bytes = four ? v + 1 >> 1 : v;
215
+ if (o + bytes > d.length)
216
+ throw new Error('BMP: truncated RLE data');
217
+ for (let i = 0; i < v; i++)
218
+ put(four ? (i & 1 ? d[o + (i >> 1)] & 15 : d[o + (i >> 1)] >> 4) : d[o + i]);
219
+ o += bytes + (bytes & 1);
220
+ }
221
+ }
222
+ }
223
+ export function decode(d, maxPixels = MAX_PIXELS) {
224
+ if (!isBMP(d))
225
+ throw new Error('BMP: invalid header');
226
+ const offset = u32(d, 10);
227
+ return decodeDIB(d, 14, offset >= 26 && offset < d.length ? offset : null, maxPixels);
228
+ }
229
+ export function encode(rgba, width, height) {
230
+ if (width < 1 || height < 1)
231
+ throw new RangeError('BMP: dimensions must be at least 1');
232
+ const count = width * height;
233
+ if (rgba.byteOffset % 4)
234
+ rgba = rgba.slice(0, count * 4);
235
+ const pixels = new Uint32Array(rgba.buffer, rgba.byteOffset, count);
236
+ let opaque = true;
237
+ for (let i = 3; i < count * 4; i += 4)
238
+ if (rgba[i] !== 255) {
239
+ opaque = false;
240
+ break;
241
+ }
242
+ let colors = null;
243
+ if (opaque) {
244
+ colors = new Map();
245
+ for (let i = 0; i < count && colors; i++) {
246
+ if (!colors.has(pixels[i])) {
247
+ if (colors.size === 256)
248
+ colors = null;
249
+ else
250
+ colors.set(pixels[i], colors.size);
251
+ }
252
+ }
253
+ }
254
+ const bits = colors ? colors.size <= 2 ? 1 : colors.size <= 16 ? 4 : 8 : opaque ? 24 : 32;
255
+ const headerSize = bits === 32 ? 108 : 40;
256
+ const paletteSize = colors ? (1 << bits) * 4 : 0;
257
+ const stride = (width * bits + 31 >>> 5) * 4;
258
+ const offset = 14 + headerSize + paletteSize;
259
+ const size = offset + stride * height;
260
+ if (size > 0xffffffff)
261
+ throw new RangeError('BMP: image too large');
262
+ const out = new Uint8Array(size);
263
+ const view = new DataView(out.buffer);
264
+ out[0] = 0x42;
265
+ out[1] = 0x4d;
266
+ view.setUint32(2, size, true);
267
+ view.setUint32(10, offset, true);
268
+ view.setUint32(14, headerSize, true);
269
+ view.setInt32(18, width, true);
270
+ view.setInt32(22, height, true);
271
+ view.setUint16(26, 1, true);
272
+ view.setUint16(28, bits, true);
273
+ view.setUint32(30, bits === 32 ? BI_BITFIELDS : BI_RGB, true);
274
+ view.setUint32(34, stride * height, true);
275
+ view.setUint32(38, 2835, true);
276
+ view.setUint32(42, 2835, true);
277
+ if (bits === 32) {
278
+ view.setUint32(54, 0x00ff0000, true);
279
+ view.setUint32(58, 0x0000ff00, true);
280
+ view.setUint32(62, 0x000000ff, true);
281
+ view.setUint32(66, 0xff000000, true);
282
+ view.setUint32(70, 0x73524742, true);
283
+ }
284
+ if (colors)
285
+ for (const [color, index] of colors) {
286
+ const p = 14 + headerSize + index * 4;
287
+ out[p] = color >> 16 & 255;
288
+ out[p + 1] = color >> 8 & 255;
289
+ out[p + 2] = color & 255;
290
+ }
291
+ for (let y = 0; y < height; y++) {
292
+ const r = offset + (height - 1 - y) * stride, s = y * width * 4;
293
+ if (colors) {
294
+ const map = colors;
295
+ for (let x = 0; x < width; x++)
296
+ out[r + (x * bits >> 3)] |= map.get(pixels[y * width + x]) << (8 - bits - (x * bits & 7));
297
+ }
298
+ else if (bits === 24) {
299
+ for (let x = 0; x < width; x++) {
300
+ const p = r + x * 3, q = s + x * 4;
301
+ out[p] = rgba[q + 2];
302
+ out[p + 1] = rgba[q + 1];
303
+ out[p + 2] = rgba[q];
304
+ }
305
+ }
306
+ else {
307
+ for (let x = 0; x < width; x++) {
308
+ const p = r + x * 4, q = s + x * 4;
309
+ out[p] = rgba[q + 2];
310
+ out[p + 1] = rgba[q + 1];
311
+ out[p + 2] = rgba[q];
312
+ out[p + 3] = rgba[q + 3];
313
+ }
314
+ }
315
+ }
316
+ return out;
317
+ }
@@ -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,6 +187,7 @@ 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;
@@ -376,42 +381,12 @@ class ByteReader {
376
381
  return String.fromCharCode(...this.slice(length));
377
382
  }
378
383
  }
379
- class ByteWriter {
380
- buffer;
381
- length;
382
- constructor(capacity = 1024) {
383
- this.buffer = new Uint8Array(Math.max(16, capacity));
384
- this.length = 0;
385
- }
386
- reserve(size) {
387
- if (this.length + size <= this.buffer.length)
388
- return;
389
- const buffer = new Uint8Array(Math.max(this.buffer.length * 2, this.length + size));
390
- buffer.set(this.buffer.subarray(0, this.length));
391
- this.buffer = buffer;
392
- }
393
- byte(value) {
394
- this.reserve(1);
395
- this.buffer[this.length++] = value;
396
- }
397
- bytes(...values) {
398
- for (const value of values)
399
- this.byte(value);
400
- }
384
+ class GIFWriter extends ByteWriter {
401
385
  u16(value) {
402
- this.byte(value & 0xff);
403
- this.byte(value >> 8 & 0xff);
386
+ this.bytes(value & 0xff, value >> 8 & 0xff);
404
387
  }
405
388
  string(value) {
406
389
  for (let i = 0; i < value.length; i++)
407
390
  this.byte(value.charCodeAt(i));
408
391
  }
409
- array(values) {
410
- this.reserve(values.length);
411
- this.buffer.set(values, this.length);
412
- this.length += values.length;
413
- }
414
- finish() {
415
- return this.buffer.slice(0, this.length);
416
- }
417
392
  }
@@ -0,0 +1,25 @@
1
+ /** How linear light brighter than white becomes 8 bit sRGB: tone mapped by Reinhard's operator, or clamped */
2
+ export type ToneMapping = 'reinhard' | 'clamp';
3
+ /**
4
+ * Linear light RGB (3 values per pixel) as sRGB RGBA. Reinhard's extended operator ("Photographic Tone Reproduction
5
+ * for Digital Images", 2002, equation 4) without its automatic exposure, which makes dark scenes like nights grey:
6
+ * the luminance L becomes L (1 + L / Lw²) / (1 + L), the brightest pixel's (at least 1) Lw white, and the channels are
7
+ * scaled by that, keeping their hue; images no brighter than white stay as they are
8
+ */
9
+ export declare function toSRGB(rgb: Float32Array, toneMapping: ToneMapping): Uint8Array;
10
+ /** the white of SDR content in HDR (BT.2408, cd/m²): linear light 1 */
11
+ export declare const REFERENCE_WHITE = 203;
12
+ /**
13
+ * Non-linear PQ (SMPTE ST 2084, transfer 16) or HLG (BT.2100, 18) RGB (3 values per pixel, primaries as CICP codes) as
14
+ * linear light (in place) with BT.709 primaries, 1 being the reference white (203 cd/m²): HLG is displayed as on a
15
+ * 1000 cd/m² display (BT.2100's OOTF, gamma 1.2)
16
+ */
17
+ export declare function hdrToLinear(rgb: Float32Array, transfer: number, primaries: number): Float32Array;
18
+ /** Whether the data starts like a Radiance HDR */
19
+ export declare function isHDR(d: Uint8Array): boolean;
20
+ /** Decodes a Radiance HDR into RGBA */
21
+ export declare function decode(d: Uint8Array, maxPixels?: number, toneMapping?: ToneMapping): {
22
+ width: number;
23
+ height: number;
24
+ pixels: Uint8Array;
25
+ };
@@ -0,0 +1,201 @@
1
+ import { MAX_PIXELS, checkPixels } from "../util/limits.js";
2
+ function linearToSRGB(v) {
3
+ if (!(v > 0))
4
+ return 0;
5
+ if (v >= 1)
6
+ return 255;
7
+ return Math.round(255 * (v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055));
8
+ }
9
+ export function toSRGB(rgb, toneMapping) {
10
+ const count = rgb.length / 3;
11
+ const luminance = (i) => 0.2126 * rgb[i] + 0.7152 * rgb[i + 1] + 0.0722 * rgb[i + 2];
12
+ let white = 1;
13
+ if (toneMapping === 'reinhard')
14
+ for (let i = 0; i < rgb.length; i += 3)
15
+ white = Math.max(white, luminance(i));
16
+ const white2 = white * white;
17
+ const pixels = new Uint8Array(count * 4);
18
+ for (let i = 0, p = 0; i < rgb.length; i += 3, p += 4) {
19
+ let scale = 1;
20
+ if (white > 1) {
21
+ const l = Math.max(0, luminance(i));
22
+ scale = (1 + l / white2) / (1 + l);
23
+ }
24
+ pixels[p] = linearToSRGB(rgb[i] * scale);
25
+ pixels[p + 1] = linearToSRGB(rgb[i + 1] * scale);
26
+ pixels[p + 2] = linearToSRGB(rgb[i + 2] * scale);
27
+ pixels[p + 3] = 255;
28
+ }
29
+ return pixels;
30
+ }
31
+ const PRIMARIES_BT2020 = 9, PRIMARIES_P3 = 12;
32
+ export const REFERENCE_WHITE = 203;
33
+ export function hdrToLinear(rgb, transfer, primaries) {
34
+ const m1 = 2610 / 16384, m2 = 2523 / 4096 * 128, c1 = 3424 / 4096, c2 = 2413 / 4096 * 32, c3 = 2392 / 4096 * 32;
35
+ const pq = (v) => {
36
+ const e = (v <= 0 ? 0 : v >= 1 ? 1 : v) ** (1 / m2);
37
+ return 10000 / REFERENCE_WHITE * (Math.max(e - c1, 0) / (c2 - c3 * e)) ** (1 / m1);
38
+ };
39
+ const a = 0.17883277, b = 1 - 4 * a, c = 0.5 - a * Math.log(4 * a);
40
+ const hlg = (v) => {
41
+ const e = v <= 0 ? 0 : v >= 1 ? 1 : v;
42
+ return e <= 0.5 ? e * e / 3 : (Math.exp((e - c) / a) + b) / 12;
43
+ };
44
+ const matrix = primaries === PRIMARIES_BT2020 ? [1.660491, -0.587641, -0.072850, -0.124550, 1.132900, -0.008349, -0.018151, -0.100579, 1.118730] :
45
+ primaries === PRIMARIES_P3 ? [1.224940, -0.224940, 0, -0.042057, 1.042057, 0, -0.019638, -0.078636, 1.098274] : null;
46
+ for (let i = 0; i < rgb.length; i += 3) {
47
+ let r, g, bl;
48
+ if (transfer === 16) {
49
+ r = pq(rgb[i]);
50
+ g = pq(rgb[i + 1]);
51
+ bl = pq(rgb[i + 2]);
52
+ }
53
+ else {
54
+ r = hlg(rgb[i]);
55
+ g = hlg(rgb[i + 1]);
56
+ bl = hlg(rgb[i + 2]);
57
+ const ys = 0.2627 * r + 0.6780 * g + 0.0593 * bl;
58
+ const gain = ys > 0 ? 1000 / REFERENCE_WHITE * ys ** 0.2 : 0;
59
+ r *= gain;
60
+ g *= gain;
61
+ bl *= gain;
62
+ }
63
+ if (matrix) {
64
+ rgb[i] = matrix[0] * r + matrix[1] * g + matrix[2] * bl;
65
+ rgb[i + 1] = matrix[3] * r + matrix[4] * g + matrix[5] * bl;
66
+ rgb[i + 2] = matrix[6] * r + matrix[7] * g + matrix[8] * bl;
67
+ }
68
+ else {
69
+ rgb[i] = r;
70
+ rgb[i + 1] = g;
71
+ rgb[i + 2] = bl;
72
+ }
73
+ }
74
+ return rgb;
75
+ }
76
+ const ascii = (d, start, end) => String.fromCharCode(...d.subarray(start, end));
77
+ export function isHDR(d) {
78
+ const start = ascii(d, 0, 10);
79
+ return start === '#?RADIANCE' || start.startsWith('#?RGBE');
80
+ }
81
+ const XYZ_TO_RGB = (() => {
82
+ const inverse = ([a, b, c, d, e, f, g, h, i]) => {
83
+ const det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
84
+ return [e * i - f * h, c * h - b * i, b * f - c * e, f * g - d * i, a * i - c * g, c * d - a * f, d * h - e * g, b * g - a * h, a * e - b * d].map(v => v / det);
85
+ };
86
+ const [r, g, b] = [[0.640, 0.330], [0.290, 0.600], [0.150, 0.060]].map(([x, y]) => [x / y, 1, (1 - x - y) / y]);
87
+ const primaries = [r[0], g[0], b[0], r[1], g[1], b[1], r[2], g[2], b[2]];
88
+ const inv = inverse(primaries);
89
+ const scale = [0, 1, 2].map(row => inv[row * 3] + inv[row * 3 + 1] + inv[row * 3 + 2]);
90
+ return inverse(primaries.map((v, i) => v * scale[i % 3]));
91
+ })();
92
+ export function decode(d, maxPixels = MAX_PIXELS, toneMapping = 'reinhard') {
93
+ if (!isHDR(d))
94
+ throw new Error('HDR: invalid header');
95
+ let o = 0, xyz = false;
96
+ for (;;) {
97
+ let end = o;
98
+ while (end < d.length && d[end] !== 0x0a)
99
+ end++;
100
+ if (end >= d.length)
101
+ throw new Error('HDR: truncated header');
102
+ const line = ascii(d, o, end);
103
+ o = end + 1;
104
+ if (!line)
105
+ break;
106
+ if (line.startsWith('FORMAT=')) {
107
+ const format = line.slice(7).trim();
108
+ if (format !== '32-bit_rle_rgbe' && format !== '32-bit_rle_xyze')
109
+ throw new Error(`HDR: unsupported format ${format}`);
110
+ xyz = format === '32-bit_rle_xyze';
111
+ }
112
+ }
113
+ let end = o;
114
+ while (end < d.length && d[end] !== 0x0a)
115
+ end++;
116
+ const resolution = /^([-+])([XY]) (\d+) ([-+])([XY]) (\d+)$/.exec(ascii(d, o, Math.min(end, o + 64)).trim());
117
+ if (!resolution || resolution[2] === resolution[5])
118
+ throw new Error('HDR: invalid resolution');
119
+ o = end + 1;
120
+ const [, sign1, axis1, n1, sign2, , n2] = resolution;
121
+ const lines = +n1, length = +n2;
122
+ const columns = axis1 === 'X';
123
+ const width = columns ? lines : length, height = columns ? length : lines;
124
+ if (!width || !height)
125
+ throw new Error(`HDR: invalid dimensions ${width}x${height}`);
126
+ checkPixels(width, height, maxPixels);
127
+ const rgb = new Float32Array(width * height * 3);
128
+ const line = new Uint8Array(length * 4);
129
+ for (let s = 0; s < lines; s++) {
130
+ o = readLine(d, o, line, length);
131
+ for (let i = 0; i < length; i++) {
132
+ const x = columns ? (sign1 === '+' ? s : width - 1 - s) : sign2 === '+' ? i : width - 1 - i;
133
+ const y = columns ? (sign2 === '-' ? i : height - 1 - i) : sign1 === '-' ? s : height - 1 - s;
134
+ const p = (y * width + x) * 3, e = line[i * 4 + 3];
135
+ if (e) {
136
+ const f = 2 ** (e - 136);
137
+ let r = line[i * 4] * f, g = line[i * 4 + 1] * f, b = line[i * 4 + 2] * f;
138
+ if (xyz) {
139
+ const m = XYZ_TO_RGB;
140
+ [r, g, b] = [m[0] * r + m[1] * g + m[2] * b, m[3] * r + m[4] * g + m[5] * b, m[6] * r + m[7] * g + m[8] * b];
141
+ }
142
+ rgb[p] = r;
143
+ rgb[p + 1] = g;
144
+ rgb[p + 2] = b;
145
+ }
146
+ }
147
+ }
148
+ return { width, height, pixels: toSRGB(rgb, toneMapping) };
149
+ }
150
+ function readLine(d, o, line, length) {
151
+ if (o + 4 > d.length)
152
+ throw new Error('HDR: truncated image data');
153
+ if (length >= 8 && length < 0x8000 && d[o] === 2 && d[o + 1] === 2 && !(d[o + 2] & 0x80)) {
154
+ if ((d[o + 2] << 8 | d[o + 3]) !== length)
155
+ throw new Error('HDR: scan line of the wrong length');
156
+ o += 4;
157
+ for (let c = 0; c < 4; c++) {
158
+ for (let i = 0; i < length;) {
159
+ if (o >= d.length)
160
+ throw new Error('HDR: truncated image data');
161
+ let n = d[o++];
162
+ if (n > 128) {
163
+ n -= 128;
164
+ if (i + n > length || o >= d.length)
165
+ throw new Error('HDR: invalid run');
166
+ const v = d[o++];
167
+ for (; n; n--)
168
+ line[i++ * 4 + c] = v;
169
+ }
170
+ else {
171
+ if (!n || i + n > length || o + n > d.length)
172
+ throw new Error('HDR: invalid run');
173
+ for (; n; n--)
174
+ line[i++ * 4 + c] = d[o++];
175
+ }
176
+ }
177
+ }
178
+ return o;
179
+ }
180
+ for (let i = 0, shift = 0; i < length;) {
181
+ if (o + 4 > d.length)
182
+ throw new Error('HDR: truncated image data');
183
+ if (d[o] === 1 && d[o + 1] === 1 && d[o + 2] === 1) {
184
+ if (!i)
185
+ throw new Error('HDR: repetition without a pixel');
186
+ const n = d[o + 3] * 2 ** shift;
187
+ if (i + n > length)
188
+ throw new Error('HDR: invalid run');
189
+ for (let k = 0; k < n; k++, i++)
190
+ line.copyWithin(i * 4, i * 4 - 4, i * 4);
191
+ shift += 8;
192
+ }
193
+ else {
194
+ line.set(d.subarray(o, o + 4), i * 4);
195
+ i++;
196
+ shift = 0;
197
+ }
198
+ o += 4;
199
+ }
200
+ return o;
201
+ }