@huggingface/transformers 3.0.0-alpha.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 (96) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +376 -0
  3. package/dist/ort-wasm-simd-threaded.jsep.wasm +0 -0
  4. package/dist/transformers.cjs +30741 -0
  5. package/dist/transformers.cjs.map +1 -0
  6. package/dist/transformers.js +33858 -0
  7. package/dist/transformers.js.map +1 -0
  8. package/dist/transformers.min.cjs +173 -0
  9. package/dist/transformers.min.cjs.map +1 -0
  10. package/dist/transformers.min.js +231 -0
  11. package/dist/transformers.min.js.map +1 -0
  12. package/package.json +92 -0
  13. package/src/backends/onnx.js +151 -0
  14. package/src/configs.js +360 -0
  15. package/src/env.js +152 -0
  16. package/src/generation/configuration_utils.js +381 -0
  17. package/src/generation/logits_process.js +716 -0
  18. package/src/generation/logits_sampler.js +204 -0
  19. package/src/generation/parameters.js +35 -0
  20. package/src/generation/stopping_criteria.js +156 -0
  21. package/src/generation/streamers.js +212 -0
  22. package/src/models/whisper/common_whisper.js +151 -0
  23. package/src/models/whisper/generation_whisper.js +89 -0
  24. package/src/models.js +7028 -0
  25. package/src/ops/registry.js +92 -0
  26. package/src/pipelines.js +3341 -0
  27. package/src/processors.js +2614 -0
  28. package/src/tokenizers.js +4395 -0
  29. package/src/transformers.js +28 -0
  30. package/src/utils/audio.js +704 -0
  31. package/src/utils/constants.js +2 -0
  32. package/src/utils/core.js +149 -0
  33. package/src/utils/data-structures.js +445 -0
  34. package/src/utils/devices.js +11 -0
  35. package/src/utils/dtypes.js +62 -0
  36. package/src/utils/generic.js +35 -0
  37. package/src/utils/hub.js +671 -0
  38. package/src/utils/image.js +745 -0
  39. package/src/utils/maths.js +1050 -0
  40. package/src/utils/tensor.js +1378 -0
  41. package/types/backends/onnx.d.ts +26 -0
  42. package/types/backends/onnx.d.ts.map +1 -0
  43. package/types/configs.d.ts +59 -0
  44. package/types/configs.d.ts.map +1 -0
  45. package/types/env.d.ts +106 -0
  46. package/types/env.d.ts.map +1 -0
  47. package/types/generation/configuration_utils.d.ts +320 -0
  48. package/types/generation/configuration_utils.d.ts.map +1 -0
  49. package/types/generation/logits_process.d.ts +354 -0
  50. package/types/generation/logits_process.d.ts.map +1 -0
  51. package/types/generation/logits_sampler.d.ts +51 -0
  52. package/types/generation/logits_sampler.d.ts.map +1 -0
  53. package/types/generation/parameters.d.ts +47 -0
  54. package/types/generation/parameters.d.ts.map +1 -0
  55. package/types/generation/stopping_criteria.d.ts +81 -0
  56. package/types/generation/stopping_criteria.d.ts.map +1 -0
  57. package/types/generation/streamers.d.ts +81 -0
  58. package/types/generation/streamers.d.ts.map +1 -0
  59. package/types/models/whisper/common_whisper.d.ts +8 -0
  60. package/types/models/whisper/common_whisper.d.ts.map +1 -0
  61. package/types/models/whisper/generation_whisper.d.ts +76 -0
  62. package/types/models/whisper/generation_whisper.d.ts.map +1 -0
  63. package/types/models.d.ts +3845 -0
  64. package/types/models.d.ts.map +1 -0
  65. package/types/ops/registry.d.ts +11 -0
  66. package/types/ops/registry.d.ts.map +1 -0
  67. package/types/pipelines.d.ts +2403 -0
  68. package/types/pipelines.d.ts.map +1 -0
  69. package/types/processors.d.ts +917 -0
  70. package/types/processors.d.ts.map +1 -0
  71. package/types/tokenizers.d.ts +999 -0
  72. package/types/tokenizers.d.ts.map +1 -0
  73. package/types/transformers.d.ts +13 -0
  74. package/types/transformers.d.ts.map +1 -0
  75. package/types/utils/audio.d.ts +130 -0
  76. package/types/utils/audio.d.ts.map +1 -0
  77. package/types/utils/constants.d.ts +2 -0
  78. package/types/utils/constants.d.ts.map +1 -0
  79. package/types/utils/core.d.ts +91 -0
  80. package/types/utils/core.d.ts.map +1 -0
  81. package/types/utils/data-structures.d.ts +236 -0
  82. package/types/utils/data-structures.d.ts.map +1 -0
  83. package/types/utils/devices.d.ts +8 -0
  84. package/types/utils/devices.d.ts.map +1 -0
  85. package/types/utils/dtypes.d.ts +22 -0
  86. package/types/utils/dtypes.d.ts.map +1 -0
  87. package/types/utils/generic.d.ts +11 -0
  88. package/types/utils/generic.d.ts.map +1 -0
  89. package/types/utils/hub.d.ts +191 -0
  90. package/types/utils/hub.d.ts.map +1 -0
  91. package/types/utils/image.d.ts +119 -0
  92. package/types/utils/image.d.ts.map +1 -0
  93. package/types/utils/maths.d.ts +280 -0
  94. package/types/utils/maths.d.ts.map +1 -0
  95. package/types/utils/tensor.d.ts +392 -0
  96. package/types/utils/tensor.d.ts.map +1 -0
@@ -0,0 +1,745 @@
1
+
2
+ /**
3
+ * @file Helper module for image processing.
4
+ *
5
+ * These functions and classes are only used internally,
6
+ * meaning an end-user shouldn't need to access anything here.
7
+ *
8
+ * @module utils/image
9
+ */
10
+
11
+ import { getFile } from './hub.js';
12
+ import { env } from '../env.js';
13
+ import { Tensor } from './tensor.js';
14
+
15
+ // Will be empty (or not used) if running in browser or web-worker
16
+ import sharp from 'sharp';
17
+
18
+ const BROWSER_ENV = typeof self !== 'undefined';
19
+ const WEBWORKER_ENV = BROWSER_ENV && self.constructor.name === 'DedicatedWorkerGlobalScope';
20
+
21
+ let createCanvasFunction;
22
+ let ImageDataClass;
23
+ let loadImageFunction;
24
+ if (BROWSER_ENV) {
25
+ // Running in browser or web-worker
26
+ createCanvasFunction = (/** @type {number} */ width, /** @type {number} */ height) => {
27
+ if (!self.OffscreenCanvas) {
28
+ throw new Error('OffscreenCanvas not supported by this browser.');
29
+ }
30
+ return new self.OffscreenCanvas(width, height)
31
+ };
32
+ loadImageFunction = self.createImageBitmap;
33
+ ImageDataClass = self.ImageData;
34
+
35
+ } else if (sharp) {
36
+ // Running in Node.js, electron, or other non-browser environment
37
+
38
+ loadImageFunction = async (/**@type {sharp.Sharp}*/img) => {
39
+ const metadata = await img.metadata();
40
+ const rawChannels = metadata.channels;
41
+
42
+ const { data, info } = await img.rotate().raw().toBuffer({ resolveWithObject: true });
43
+
44
+ const newImage = new RawImage(new Uint8ClampedArray(data), info.width, info.height, info.channels);
45
+ if (rawChannels !== undefined && rawChannels !== info.channels) {
46
+ // Make sure the new image has the same number of channels as the input image.
47
+ // This is necessary for grayscale images.
48
+ newImage.convert(rawChannels);
49
+ }
50
+ return newImage;
51
+ }
52
+
53
+ } else {
54
+ throw new Error('Unable to load image processing library.');
55
+ }
56
+
57
+
58
+ // Defined here: https://github.com/python-pillow/Pillow/blob/a405e8406b83f8bfb8916e93971edc7407b8b1ff/src/libImaging/Imaging.h#L262-L268
59
+ const RESAMPLING_MAPPING = {
60
+ 0: 'nearest',
61
+ 1: 'lanczos',
62
+ 2: 'bilinear',
63
+ 3: 'bicubic',
64
+ 4: 'box',
65
+ 5: 'hamming',
66
+ }
67
+
68
+ /**
69
+ * Mapping from file extensions to MIME types.
70
+ */
71
+ const CONTENT_TYPE_MAP = new Map([
72
+ ['png', 'image/png'],
73
+ ['jpg', 'image/jpeg'],
74
+ ['jpeg', 'image/jpeg'],
75
+ ['gif', 'image/gif'],
76
+ ]);
77
+
78
+ export class RawImage {
79
+
80
+ /**
81
+ * Create a new `RawImage` object.
82
+ * @param {Uint8ClampedArray|Uint8Array} data The pixel data.
83
+ * @param {number} width The width of the image.
84
+ * @param {number} height The height of the image.
85
+ * @param {1|2|3|4} channels The number of channels.
86
+ */
87
+ constructor(data, width, height, channels) {
88
+ this.data = data;
89
+ this.width = width;
90
+ this.height = height;
91
+ this.channels = channels;
92
+ }
93
+
94
+ /**
95
+ * Returns the size of the image (width, height).
96
+ * @returns {[number, number]} The size of the image (width, height).
97
+ */
98
+ get size() {
99
+ return [this.width, this.height];
100
+ }
101
+
102
+ /**
103
+ * Helper method for reading an image from a variety of input types.
104
+ * @param {RawImage|string|URL} input
105
+ * @returns The image object.
106
+ *
107
+ * **Example:** Read image from a URL.
108
+ * ```javascript
109
+ * let image = await RawImage.read('https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/football-match.jpg');
110
+ * // RawImage {
111
+ * // "data": Uint8ClampedArray [ 25, 25, 25, 19, 19, 19, ... ],
112
+ * // "width": 800,
113
+ * // "height": 533,
114
+ * // "channels": 3
115
+ * // }
116
+ * ```
117
+ */
118
+ static async read(input) {
119
+ if (input instanceof RawImage) {
120
+ return input;
121
+ } else if (typeof input === 'string' || input instanceof URL) {
122
+ return await this.fromURL(input);
123
+ } else {
124
+ throw new Error(`Unsupported input type: ${typeof input}`);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Read an image from a canvas.
130
+ * @param {HTMLCanvasElement|OffscreenCanvas} canvas The canvas to read the image from.
131
+ * @returns {RawImage} The image object.
132
+ */
133
+ static fromCanvas(canvas) {
134
+ if (!BROWSER_ENV) {
135
+ throw new Error('fromCanvas() is only supported in browser environments.')
136
+ }
137
+
138
+ const ctx = canvas.getContext('2d');
139
+ const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
140
+ return new RawImage(data, canvas.width, canvas.height, 4);
141
+ }
142
+
143
+ /**
144
+ * Read an image from a URL or file path.
145
+ * @param {string|URL} url The URL or file path to read the image from.
146
+ * @returns {Promise<RawImage>} The image object.
147
+ */
148
+ static async fromURL(url) {
149
+ const response = await getFile(url);
150
+ if (response.status !== 200) {
151
+ throw new Error(`Unable to read image from "${url}" (${response.status} ${response.statusText})`);
152
+ }
153
+ const blob = await response.blob();
154
+ return this.fromBlob(blob);
155
+ }
156
+
157
+ /**
158
+ * Helper method to create a new Image from a blob.
159
+ * @param {Blob} blob The blob to read the image from.
160
+ * @returns {Promise<RawImage>} The image object.
161
+ */
162
+ static async fromBlob(blob) {
163
+ if (BROWSER_ENV) {
164
+ // Running in environment with canvas
165
+ const img = await loadImageFunction(blob);
166
+
167
+ const ctx = createCanvasFunction(img.width, img.height).getContext('2d');
168
+
169
+ // Draw image to context
170
+ ctx.drawImage(img, 0, 0);
171
+
172
+ return new this(ctx.getImageData(0, 0, img.width, img.height).data, img.width, img.height, 4);
173
+
174
+ } else {
175
+ // Use sharp.js to read (and possible resize) the image.
176
+ const img = sharp(await blob.arrayBuffer());
177
+
178
+ return await loadImageFunction(img);
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Helper method to create a new Image from a tensor
184
+ * @param {Tensor} tensor
185
+ */
186
+ static fromTensor(tensor, channel_format = 'CHW') {
187
+ if (tensor.dims.length !== 3) {
188
+ throw new Error(`Tensor should have 3 dimensions, but has ${tensor.dims.length} dimensions.`);
189
+ }
190
+
191
+ if (channel_format === 'CHW') {
192
+ tensor = tensor.transpose(1, 2, 0);
193
+ } else if (channel_format === 'HWC') {
194
+ // Do nothing
195
+ } else {
196
+ throw new Error(`Unsupported channel format: ${channel_format}`);
197
+ }
198
+ if (!(tensor.data instanceof Uint8ClampedArray || tensor.data instanceof Uint8Array)) {
199
+ throw new Error(`Unsupported tensor type: ${tensor.type}`);
200
+ }
201
+ switch (tensor.dims[2]) {
202
+ case 1:
203
+ case 2:
204
+ case 3:
205
+ case 4:
206
+ return new RawImage(tensor.data, tensor.dims[1], tensor.dims[0], tensor.dims[2]);
207
+ default:
208
+ throw new Error(`Unsupported number of channels: ${tensor.dims[2]}`);
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Convert the image to grayscale format.
214
+ * @returns {RawImage} `this` to support chaining.
215
+ */
216
+ grayscale() {
217
+ if (this.channels === 1) {
218
+ return this;
219
+ }
220
+
221
+ const newData = new Uint8ClampedArray(this.width * this.height * 1);
222
+ switch (this.channels) {
223
+ case 3: // rgb to grayscale
224
+ case 4: // rgba to grayscale
225
+ for (let i = 0, offset = 0; i < this.data.length; i += this.channels) {
226
+ const red = this.data[i];
227
+ const green = this.data[i + 1];
228
+ const blue = this.data[i + 2];
229
+
230
+ newData[offset++] = Math.round(0.2989 * red + 0.5870 * green + 0.1140 * blue);
231
+ }
232
+ break;
233
+ default:
234
+ throw new Error(`Conversion failed due to unsupported number of channels: ${this.channels}`);
235
+ }
236
+ return this._update(newData, this.width, this.height, 1);
237
+ }
238
+
239
+ /**
240
+ * Convert the image to RGB format.
241
+ * @returns {RawImage} `this` to support chaining.
242
+ */
243
+ rgb() {
244
+ if (this.channels === 3) {
245
+ return this;
246
+ }
247
+
248
+ const newData = new Uint8ClampedArray(this.width * this.height * 3);
249
+
250
+ switch (this.channels) {
251
+ case 1: // grayscale to rgb
252
+ for (let i = 0, offset = 0; i < this.data.length; ++i) {
253
+ newData[offset++] = this.data[i];
254
+ newData[offset++] = this.data[i];
255
+ newData[offset++] = this.data[i];
256
+ }
257
+ break;
258
+ case 4: // rgba to rgb
259
+ for (let i = 0, offset = 0; i < this.data.length; i += 4) {
260
+ newData[offset++] = this.data[i];
261
+ newData[offset++] = this.data[i + 1];
262
+ newData[offset++] = this.data[i + 2];
263
+ }
264
+ break;
265
+ default:
266
+ throw new Error(`Conversion failed due to unsupported number of channels: ${this.channels}`);
267
+ }
268
+ return this._update(newData, this.width, this.height, 3);
269
+
270
+ }
271
+
272
+ /**
273
+ * Convert the image to RGBA format.
274
+ * @returns {RawImage} `this` to support chaining.
275
+ */
276
+ rgba() {
277
+ if (this.channels === 4) {
278
+ return this;
279
+ }
280
+
281
+ const newData = new Uint8ClampedArray(this.width * this.height * 4);
282
+
283
+ switch (this.channels) {
284
+ case 1: // grayscale to rgba
285
+ for (let i = 0, offset = 0; i < this.data.length; ++i) {
286
+ newData[offset++] = this.data[i];
287
+ newData[offset++] = this.data[i];
288
+ newData[offset++] = this.data[i];
289
+ newData[offset++] = 255;
290
+ }
291
+ break;
292
+ case 3: // rgb to rgba
293
+ for (let i = 0, offset = 0; i < this.data.length; i += 3) {
294
+ newData[offset++] = this.data[i];
295
+ newData[offset++] = this.data[i + 1];
296
+ newData[offset++] = this.data[i + 2];
297
+ newData[offset++] = 255;
298
+ }
299
+ break;
300
+ default:
301
+ throw new Error(`Conversion failed due to unsupported number of channels: ${this.channels}`);
302
+ }
303
+
304
+ return this._update(newData, this.width, this.height, 4);
305
+ }
306
+
307
+ /**
308
+ * Resize the image to the given dimensions. This method uses the canvas API to perform the resizing.
309
+ * @param {number} width The width of the new image.
310
+ * @param {number} height The height of the new image.
311
+ * @param {Object} options Additional options for resizing.
312
+ * @param {0|1|2|3|4|5|string} [options.resample] The resampling method to use.
313
+ * @returns {Promise<RawImage>} `this` to support chaining.
314
+ */
315
+ async resize(width, height, {
316
+ resample = 2,
317
+ } = {}) {
318
+
319
+ // Ensure resample method is a string
320
+ let resampleMethod = RESAMPLING_MAPPING[resample] ?? resample;
321
+
322
+ if (BROWSER_ENV) {
323
+ // TODO use `resample` in browser environment
324
+
325
+ // Store number of channels before resizing
326
+ const numChannels = this.channels;
327
+
328
+ // Create canvas object for this image
329
+ const canvas = this.toCanvas();
330
+
331
+ // Actually perform resizing using the canvas API
332
+ const ctx = createCanvasFunction(width, height).getContext('2d');
333
+
334
+ // Draw image to context, resizing in the process
335
+ ctx.drawImage(canvas, 0, 0, width, height);
336
+
337
+ // Create image from the resized data
338
+ const resizedImage = new RawImage(ctx.getImageData(0, 0, width, height).data, width, height, 4);
339
+
340
+ // Convert back so that image has the same number of channels as before
341
+ return resizedImage.convert(numChannels);
342
+
343
+ } else {
344
+ // Create sharp image from raw data, and resize
345
+ let img = this.toSharp();
346
+
347
+ switch (resampleMethod) {
348
+ case 'box':
349
+ case 'hamming':
350
+ if (resampleMethod === 'box' || resampleMethod === 'hamming') {
351
+ console.warn(`Resampling method ${resampleMethod} is not yet supported. Using bilinear instead.`);
352
+ resampleMethod = 'bilinear';
353
+ }
354
+
355
+ case 'nearest':
356
+ case 'bilinear':
357
+ case 'bicubic':
358
+ // Perform resizing using affine transform.
359
+ // This matches how the python Pillow library does it.
360
+ img = img.affine([width / this.width, 0, 0, height / this.height], {
361
+ interpolator: resampleMethod
362
+ });
363
+ break;
364
+
365
+ case 'lanczos':
366
+ // https://github.com/python-pillow/Pillow/discussions/5519
367
+ // https://github.com/lovell/sharp/blob/main/docs/api-resize.md
368
+ img = img.resize({
369
+ width, height,
370
+ fit: 'fill',
371
+ kernel: 'lanczos3', // PIL Lanczos uses a kernel size of 3
372
+ });
373
+ break;
374
+
375
+ default:
376
+ throw new Error(`Resampling method ${resampleMethod} is not supported.`);
377
+ }
378
+
379
+ return await loadImageFunction(img);
380
+ }
381
+
382
+ }
383
+
384
+ async pad([left, right, top, bottom]) {
385
+ left = Math.max(left, 0);
386
+ right = Math.max(right, 0);
387
+ top = Math.max(top, 0);
388
+ bottom = Math.max(bottom, 0);
389
+
390
+ if (left === 0 && right === 0 && top === 0 && bottom === 0) {
391
+ // No padding needed
392
+ return this;
393
+ }
394
+
395
+ if (BROWSER_ENV) {
396
+ // Store number of channels before padding
397
+ const numChannels = this.channels;
398
+
399
+ // Create canvas object for this image
400
+ const canvas = this.toCanvas();
401
+
402
+ const newWidth = this.width + left + right;
403
+ const newHeight = this.height + top + bottom;
404
+
405
+ // Create a new canvas of the desired size.
406
+ const ctx = createCanvasFunction(newWidth, newHeight).getContext('2d');
407
+
408
+ // Draw image to context, padding in the process
409
+ ctx.drawImage(canvas,
410
+ 0, 0, this.width, this.height,
411
+ left, top, newWidth, newHeight
412
+ );
413
+
414
+ // Create image from the padded data
415
+ const paddedImage = new RawImage(
416
+ ctx.getImageData(0, 0, newWidth, newHeight).data,
417
+ newWidth, newHeight, 4);
418
+
419
+ // Convert back so that image has the same number of channels as before
420
+ return paddedImage.convert(numChannels);
421
+
422
+ } else {
423
+ const img = this.toSharp().extend({ left, right, top, bottom });
424
+ return await loadImageFunction(img);
425
+ }
426
+ }
427
+
428
+ async crop([x_min, y_min, x_max, y_max]) {
429
+ // Ensure crop bounds are within the image
430
+ x_min = Math.max(x_min, 0);
431
+ y_min = Math.max(y_min, 0);
432
+ x_max = Math.min(x_max, this.width - 1);
433
+ y_max = Math.min(y_max, this.height - 1);
434
+
435
+ // Do nothing if the crop is the entire image
436
+ if (x_min === 0 && y_min === 0 && x_max === this.width - 1 && y_max === this.height - 1) {
437
+ return this;
438
+ }
439
+
440
+ const crop_width = x_max - x_min + 1;
441
+ const crop_height = y_max - y_min + 1;
442
+
443
+ if (BROWSER_ENV) {
444
+ // Store number of channels before resizing
445
+ const numChannels = this.channels;
446
+
447
+ // Create canvas object for this image
448
+ const canvas = this.toCanvas();
449
+
450
+ // Create a new canvas of the desired size. This is needed since if the
451
+ // image is too small, we need to pad it with black pixels.
452
+ const ctx = createCanvasFunction(crop_width, crop_height).getContext('2d');
453
+
454
+ // Draw image to context, cropping in the process
455
+ ctx.drawImage(canvas,
456
+ x_min, y_min, crop_width, crop_height,
457
+ 0, 0, crop_width, crop_height
458
+ );
459
+
460
+ // Create image from the resized data
461
+ const resizedImage = new RawImage(ctx.getImageData(0, 0, crop_width, crop_height).data, crop_width, crop_height, 4);
462
+
463
+ // Convert back so that image has the same number of channels as before
464
+ return resizedImage.convert(numChannels);
465
+
466
+ } else {
467
+ // Create sharp image from raw data
468
+ const img = this.toSharp().extract({
469
+ left: x_min,
470
+ top: y_min,
471
+ width: crop_width,
472
+ height: crop_height,
473
+ });
474
+
475
+ return await loadImageFunction(img);
476
+ }
477
+
478
+ }
479
+
480
+ async center_crop(crop_width, crop_height) {
481
+ // If the image is already the desired size, return it
482
+ if (this.width === crop_width && this.height === crop_height) {
483
+ return this;
484
+ }
485
+
486
+ // Determine bounds of the image in the new canvas
487
+ const width_offset = (this.width - crop_width) / 2;
488
+ const height_offset = (this.height - crop_height) / 2;
489
+
490
+
491
+ if (BROWSER_ENV) {
492
+ // Store number of channels before resizing
493
+ const numChannels = this.channels;
494
+
495
+ // Create canvas object for this image
496
+ const canvas = this.toCanvas();
497
+
498
+ // Create a new canvas of the desired size. This is needed since if the
499
+ // image is too small, we need to pad it with black pixels.
500
+ const ctx = createCanvasFunction(crop_width, crop_height).getContext('2d');
501
+
502
+ let sourceX = 0;
503
+ let sourceY = 0;
504
+ let destX = 0;
505
+ let destY = 0;
506
+
507
+ if (width_offset >= 0) {
508
+ sourceX = width_offset;
509
+ } else {
510
+ destX = -width_offset;
511
+ }
512
+
513
+ if (height_offset >= 0) {
514
+ sourceY = height_offset;
515
+ } else {
516
+ destY = -height_offset;
517
+ }
518
+
519
+ // Draw image to context, cropping in the process
520
+ ctx.drawImage(canvas,
521
+ sourceX, sourceY, crop_width, crop_height,
522
+ destX, destY, crop_width, crop_height
523
+ );
524
+
525
+ // Create image from the resized data
526
+ const resizedImage = new RawImage(ctx.getImageData(0, 0, crop_width, crop_height).data, crop_width, crop_height, 4);
527
+
528
+ // Convert back so that image has the same number of channels as before
529
+ return resizedImage.convert(numChannels);
530
+
531
+ } else {
532
+ // Create sharp image from raw data
533
+ let img = this.toSharp();
534
+
535
+ if (width_offset >= 0 && height_offset >= 0) {
536
+ // Cropped image lies entirely within the original image
537
+ img = img.extract({
538
+ left: Math.floor(width_offset),
539
+ top: Math.floor(height_offset),
540
+ width: crop_width,
541
+ height: crop_height,
542
+ })
543
+ } else if (width_offset <= 0 && height_offset <= 0) {
544
+ // Cropped image lies entirely outside the original image,
545
+ // so we add padding
546
+ const top = Math.floor(-height_offset);
547
+ const left = Math.floor(-width_offset);
548
+ img = img.extend({
549
+ top: top,
550
+ left: left,
551
+
552
+ // Ensures the resulting image has the desired dimensions
553
+ right: crop_width - this.width - left,
554
+ bottom: crop_height - this.height - top,
555
+ });
556
+ } else {
557
+ // Cropped image lies partially outside the original image.
558
+ // We first pad, then crop.
559
+
560
+ let y_padding = [0, 0];
561
+ let y_extract = 0;
562
+ if (height_offset < 0) {
563
+ y_padding[0] = Math.floor(-height_offset);
564
+ y_padding[1] = crop_height - this.height - y_padding[0];
565
+ } else {
566
+ y_extract = Math.floor(height_offset);
567
+ }
568
+
569
+ let x_padding = [0, 0];
570
+ let x_extract = 0;
571
+ if (width_offset < 0) {
572
+ x_padding[0] = Math.floor(-width_offset);
573
+ x_padding[1] = crop_width - this.width - x_padding[0];
574
+ } else {
575
+ x_extract = Math.floor(width_offset);
576
+ }
577
+
578
+ img = img.extend({
579
+ top: y_padding[0],
580
+ bottom: y_padding[1],
581
+ left: x_padding[0],
582
+ right: x_padding[1],
583
+ }).extract({
584
+ left: x_extract,
585
+ top: y_extract,
586
+ width: crop_width,
587
+ height: crop_height,
588
+ })
589
+ }
590
+
591
+ return await loadImageFunction(img);
592
+ }
593
+ }
594
+
595
+ async toBlob(type = 'image/png', quality = 1) {
596
+ if (!BROWSER_ENV) {
597
+ throw new Error('toBlob() is only supported in browser environments.')
598
+ }
599
+
600
+ const canvas = this.toCanvas();
601
+ return await canvas.convertToBlob({ type, quality });
602
+ }
603
+
604
+ toTensor(channel_format = 'CHW') {
605
+ let tensor = new Tensor(
606
+ 'uint8',
607
+ new Uint8Array(this.data),
608
+ [this.height, this.width, this.channels]
609
+ );
610
+
611
+ if (channel_format === 'HWC') {
612
+ // Do nothing
613
+ } else if (channel_format === 'CHW') { // hwc -> chw
614
+ tensor = tensor.permute(2, 0, 1);
615
+ } else {
616
+ throw new Error(`Unsupported channel format: ${channel_format}`);
617
+ }
618
+ return tensor;
619
+ }
620
+
621
+ toCanvas() {
622
+ if (!BROWSER_ENV) {
623
+ throw new Error('toCanvas() is only supported in browser environments.')
624
+ }
625
+
626
+ // Clone, and convert data to RGBA before drawing to canvas.
627
+ // This is because the canvas API only supports RGBA
628
+ const cloned = this.clone().rgba();
629
+
630
+ // Create canvas object for the cloned image
631
+ const clonedCanvas = createCanvasFunction(cloned.width, cloned.height);
632
+
633
+ // Draw image to context
634
+ const data = new ImageDataClass(cloned.data, cloned.width, cloned.height);
635
+ clonedCanvas.getContext('2d').putImageData(data, 0, 0);
636
+
637
+ return clonedCanvas;
638
+ }
639
+
640
+ /**
641
+ * Helper method to update the image data.
642
+ * @param {Uint8ClampedArray} data The new image data.
643
+ * @param {number} width The new width of the image.
644
+ * @param {number} height The new height of the image.
645
+ * @param {1|2|3|4|null} [channels] The new number of channels of the image.
646
+ * @private
647
+ */
648
+ _update(data, width, height, channels = null) {
649
+ this.data = data;
650
+ this.width = width;
651
+ this.height = height;
652
+ if (channels !== null) {
653
+ this.channels = channels;
654
+ }
655
+ return this;
656
+ }
657
+
658
+ /**
659
+ * Clone the image
660
+ * @returns {RawImage} The cloned image
661
+ */
662
+ clone() {
663
+ return new RawImage(this.data.slice(), this.width, this.height, this.channels);
664
+ }
665
+
666
+ /**
667
+ * Helper method for converting image to have a certain number of channels
668
+ * @param {number} numChannels The number of channels. Must be 1, 3, or 4.
669
+ * @returns {RawImage} `this` to support chaining.
670
+ */
671
+ convert(numChannels) {
672
+ if (this.channels === numChannels) return this; // Already correct number of channels
673
+
674
+ switch (numChannels) {
675
+ case 1:
676
+ this.grayscale();
677
+ break;
678
+ case 3:
679
+ this.rgb();
680
+ break;
681
+ case 4:
682
+ this.rgba();
683
+ break;
684
+ default:
685
+ throw new Error(`Conversion failed due to unsupported number of channels: ${this.channels}`);
686
+ }
687
+ return this;
688
+ }
689
+
690
+ /**
691
+ * Save the image to the given path.
692
+ * @param {string} path The path to save the image to.
693
+ */
694
+ async save(path) {
695
+
696
+ if (BROWSER_ENV) {
697
+ if (WEBWORKER_ENV) {
698
+ throw new Error('Unable to save an image from a Web Worker.')
699
+ }
700
+
701
+ const extension = path.split('.').pop().toLowerCase();
702
+ const mime = CONTENT_TYPE_MAP.get(extension) ?? 'image/png';
703
+
704
+ // Convert image to Blob
705
+ const blob = await this.toBlob(mime);
706
+
707
+ // Convert the canvas content to a data URL
708
+ const dataURL = URL.createObjectURL(blob);
709
+
710
+ // Create an anchor element with the data URL as the href attribute
711
+ const downloadLink = document.createElement('a');
712
+ downloadLink.href = dataURL;
713
+
714
+ // Set the download attribute to specify the desired filename for the downloaded image
715
+ downloadLink.download = path;
716
+
717
+ // Trigger the download
718
+ downloadLink.click();
719
+
720
+ // Clean up: remove the anchor element from the DOM
721
+ downloadLink.remove();
722
+
723
+ } else if (!env.useFS) {
724
+ throw new Error('Unable to save the image because filesystem is disabled in this environment.')
725
+
726
+ } else {
727
+ const img = this.toSharp();
728
+ return await img.toFile(path);
729
+ }
730
+ }
731
+
732
+ toSharp() {
733
+ if (BROWSER_ENV) {
734
+ throw new Error('toSharp() is only supported in server-side environments.')
735
+ }
736
+
737
+ return sharp(this.data, {
738
+ raw: {
739
+ width: this.width,
740
+ height: this.height,
741
+ channels: this.channels
742
+ }
743
+ });
744
+ }
745
+ }