pts 0.12.8 → 1.0.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.
package/src/Image.ts ADDED
@@ -0,0 +1,722 @@
1
+ import { CanvasForm, type CanvasSpace } from "./Canvas";
2
+ import { type Bound, Pt } from "./Pt";
3
+ import { Mat } from "./LinearAlgebra";
4
+ import { type PtLike, type CanvasPatternRepetition } from "./Types";
5
+ import { Util } from "./Util";
6
+ import { type RenderingContext2D } from "./Types";
7
+
8
+ /**
9
+ * Options for creating an [`Img`](#link).
10
+ */
11
+ export type ImgOptions = {
12
+ /** Specify if you want to manipulate pixels of this image. Default is `false`. */
13
+ editable?: boolean;
14
+ /** Set the `CanvasSpace` reference so the image's pixelScale matches the canvas. */
15
+ space?: CanvasSpace;
16
+ /** Enable loading cross-domain images. The image server must also allow it. */
17
+ crossOrigin?: boolean;
18
+ /** Set a specific pixel scale, overriding the space's. */
19
+ pixelScale?: number;
20
+ };
21
+
22
+ /**
23
+ * Img provides convenient functions to support image operations on HTML Canvas and [`CanvasSpace`](#link). Combine this with other Pts functions to experiment with visual forms that integrate bitmaps and vector graphics.
24
+ */
25
+ export class Img {
26
+ protected _img!: HTMLImageElement;
27
+ protected _data!: ImageData;
28
+ protected _cv!: HTMLCanvasElement;
29
+ protected _ctx!: RenderingContext2D;
30
+ protected _scale: number = 1;
31
+
32
+ protected _loaded: boolean = false;
33
+ protected _editable: boolean;
34
+
35
+ protected _space: CanvasSpace | undefined;
36
+ protected _patternCtx!: RenderingContext2D; // lazy fallback when no space is set
37
+ protected _objectUrl!: string | null; // tracked for revocation on dispose
38
+ private _pendingLoadReject: ((err: Error) => void) | null = null; // newer loads supersede pending ones
39
+ private _disposed = false;
40
+ protected _dataDirty: boolean = false; // ImageData refreshes lazily on first read
41
+
42
+ /**
43
+ * Create an Img
44
+ * @param editable either an [`ImgOptions`](#link) object, or a boolean specifying if you want to manipulate pixels of this image. Default is `false`.
45
+ * @param space Set the `CanvasSpace` reference. This is optional but will make sure the image's pixelScale match the canvas and set the context for creating pattern.
46
+ * @param crossOrigin an optional parameter to enable loading cross-domain images if set to true. The image server's configuration must also be set correctly. For more, see [this documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image).
47
+ * @example `new Img(true, space)`, `new Img({ editable: true, pixelScale: 2 })`
48
+ */
49
+ constructor(
50
+ editable: boolean | ImgOptions = false,
51
+ space?: CanvasSpace,
52
+ crossOrigin?: boolean,
53
+ ) {
54
+ const opts: ImgOptions =
55
+ typeof editable === "object"
56
+ ? editable
57
+ : { editable, space, crossOrigin };
58
+ this._editable = !!opts.editable;
59
+ this._space = opts.space;
60
+ this._scale = opts.pixelScale ?? (this._space ? this._space.pixelScale : 1);
61
+ this._img = new Image();
62
+ if (opts.crossOrigin) this._img.crossOrigin = "Anonymous";
63
+ }
64
+
65
+ /**
66
+ * A static function to load an image, returning a Promise that resolves to the loaded Img.
67
+ * A load failure rejects the Promise.
68
+ * @param src an url of the image in same domain. Alternatively you can use a base64 string. To load from Blob, use `Img.fromBlob`.
69
+ * @param editable either an [`ImgOptions`](#link) object, or a boolean specifying if you want to manipulate pixels of this image. Default is `false`.
70
+ * @param space Set the `CanvasSpace` reference. This is optional but will make sure the image's pixelScale match the canvas and set the context for creating pattern.
71
+ * @param ready An optional callback, invoked with the Img when loading succeeds
72
+ * @example `const img = await Img.load("photo.jpg", true)`
73
+ */
74
+ static load(
75
+ src: string,
76
+ editable: boolean | ImgOptions = false,
77
+ space?: CanvasSpace,
78
+ ready?: (img: Img) => void,
79
+ ): Promise<Img> {
80
+ return new Img(editable, space).load(src).then((res) => {
81
+ if (ready) ready(res);
82
+ return res;
83
+ });
84
+ }
85
+
86
+ /**
87
+ * A static method to load an image using async/await.
88
+ * @deprecated Use [`Img.load`](#link), which now returns a Promise.
89
+ * @param src an url of the image in same domain. Alternatively you can use a base64 string. To load from Blob, use `Img.fromBlob`.
90
+ * @param editable Specify if you want to manipulate pixels of this image. Default is `false`.
91
+ * @param space Set the `CanvasSpace` reference. This is optional but will make sure the image's pixelScale match the canvas and set the context for creating pattern.
92
+ */
93
+ static async loadAsync(
94
+ src: string,
95
+ editable: boolean | ImgOptions = false,
96
+ space?: CanvasSpace,
97
+ ): Promise<Img> {
98
+ return Img.load(src, editable, space);
99
+ }
100
+
101
+ /**
102
+ * A static method to load an image pattern using async/await.
103
+ * @param src an url of the image in same domain. Alternatively you can use a base64 string. To load from Blob, use `Img.fromBlob`.
104
+ * @param space Set the `CanvasSpace` reference. This is optional but will make sure the image's pixelScale match the canvas and set the context for creating pattern.
105
+ * @param repeat set how the pattern will repeat fills
106
+ * @param editable Specify if you want to manipulate pixels of this image. Default is `false`.
107
+ * @returns a `CanvasPattern` instance for use in `fill()`
108
+ */
109
+ static async loadPattern(
110
+ src: string,
111
+ space: CanvasSpace,
112
+ repeat: CanvasPatternRepetition = "repeat",
113
+ editable: boolean = false,
114
+ ) {
115
+ const img = await Img.loadAsync(src, editable, space);
116
+ return img.pattern(repeat);
117
+ }
118
+
119
+ /**
120
+ * Create an editable blank image
121
+ * @param size of image
122
+ * @param space Optionally set the `CanvasSpace` reference. This is optional but will make sure the image's pixelScale match the canvas and set the context for creating pattern.
123
+ * @param scale Optionally set a specific pixel scale (density) of the image canvas.
124
+ */
125
+ static blank(size: PtLike, space?: CanvasSpace, scale?: number): Img {
126
+ let img = new Img(true, space);
127
+ const s = scale ? scale : space ? space.pixelScale : 1;
128
+ img.initCanvas(size[0], size[1], s);
129
+ return img;
130
+ }
131
+
132
+ /**
133
+ * Load an image.
134
+ * @param src an url of the image in same domain. Alternatively you can use a base64 string. To load from Blob, use `Img.fromBlob`.
135
+ * @returns a Promise that resolves to an Img
136
+ */
137
+ load(src: string): Promise<Img> {
138
+ if (this._editable && typeof document === "undefined") {
139
+ return Promise.reject(
140
+ new Error("Cannot create html canvas element. document not found."),
141
+ );
142
+ }
143
+
144
+ return this._loadImageSrc(src).then(() => {
145
+ if (this._disposed) throw new Error("Img has been disposed");
146
+ if (this._editable) {
147
+ if (!this._cv)
148
+ this._cv = document.createElement("canvas") as HTMLCanvasElement;
149
+ this._drawToScale(this._scale, this._img);
150
+ this._dataDirty = true;
151
+ }
152
+ this._loaded = true;
153
+ return this;
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Swap the underlying image's source and await its load — without the editable
159
+ * pipeline. Shared by `load()` and `sync()` so both respect the supersede rule.
160
+ */
161
+ protected _loadImageSrc(src: string): Promise<void> {
162
+ if (this._disposed)
163
+ return Promise.reject(new Error("Img has been disposed"));
164
+ return new Promise<void>((resolve, reject) => {
165
+ // a newer load replaces this one's handlers on the shared <img>, so a
166
+ // pending previous promise must be rejected proactively
167
+ if (this._pendingLoadReject) {
168
+ this._pendingLoadReject(
169
+ new Error("Img loading superseded by a newer load"),
170
+ );
171
+ }
172
+ this._pendingLoadReject = reject;
173
+
174
+ this._img.onload = () => {
175
+ this._pendingLoadReject = null;
176
+ resolve();
177
+ };
178
+
179
+ this._img.onerror = () => {
180
+ this._pendingLoadReject = null;
181
+ reject(new Error(`Img cannot load ${src}`));
182
+ };
183
+
184
+ this._img.src = src;
185
+ });
186
+ }
187
+
188
+ /** Refresh the cached `ImageData` from the current canvas. */
189
+ protected _refreshData(): void {
190
+ this._data = this._ctx.getImageData(0, 0, this._cv.width, this._cv.height);
191
+ this._dataDirty = false;
192
+ }
193
+
194
+ /** Materialize the cached `ImageData` lazily, on first read after a change. */
195
+ protected _ensureData(): void {
196
+ if ((this._dataDirty || !this._data) && this._ctx && this._cv) {
197
+ this._refreshData();
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Rescale the canvas and draw an image-source on it.
203
+ * @param canvasScale rescale factor for the canvas
204
+ * @param img an image source like Image, Canvas, or ImageBitmap.
205
+ */
206
+ protected _drawToScale(
207
+ canvasScale: number | PtLike,
208
+ img:
209
+ | HTMLImageElement
210
+ | HTMLCanvasElement
211
+ | ImageBitmap
212
+ | OffscreenCanvas
213
+ | HTMLVideoElement,
214
+ ) {
215
+ const nw = img.width as number;
216
+ const nh = img.height as number;
217
+ this._initCanvas(nw, nh, canvasScale);
218
+ if (img)
219
+ this._ctx.drawImage(
220
+ img,
221
+ 0,
222
+ 0,
223
+ nw,
224
+ nh,
225
+ 0,
226
+ 0,
227
+ this._cv.width,
228
+ this._cv.height,
229
+ );
230
+ }
231
+
232
+ /**
233
+ * Initiate an editable canvas
234
+ * @param width width of canvas
235
+ * @param height height of canvas
236
+ * @param canvasScale pixel scale
237
+ */
238
+ initCanvas(width: number, height: number, canvasScale: number | PtLike = 1) {
239
+ this._initCanvas(width, height, canvasScale);
240
+ }
241
+
242
+ /**
243
+ * Internal canvas setup without the pixel-data refresh — callers that draw
244
+ * immediately afterwards refresh once after their draw instead.
245
+ */
246
+ protected _initCanvas(
247
+ width: number,
248
+ height: number,
249
+ canvasScale: number | PtLike = 1,
250
+ ) {
251
+ if (!this._editable) {
252
+ Util.warn(
253
+ "Cannot initiate canvas because this Img is not set to be editable",
254
+ );
255
+ return;
256
+ }
257
+
258
+ if (!this._cv)
259
+ this._cv = document.createElement("canvas") as HTMLCanvasElement;
260
+
261
+ const cms =
262
+ typeof canvasScale === "number"
263
+ ? [canvasScale, canvasScale]
264
+ : canvasScale;
265
+ this._cv.width = width * cms[0];
266
+ this._cv.height = height * cms[1];
267
+ // the whole point of an editable Img is repeated readback
268
+ this._ctx = this._cv.getContext("2d", { willReadFrequently: true })!;
269
+ // resizing resets the context state; forget any cached style values so a
270
+ // CanvasForm from getForm() re-applies its styles
271
+ CanvasForm.resetStyleCache(this._ctx);
272
+ // keep the pixel-density field coherent with the actual canvas scaling,
273
+ // which `pixel( p, true )` depends on
274
+ if (typeof canvasScale === "number") this._scale = canvasScale;
275
+ this._dataDirty = true; // pixel reads materialize lazily
276
+ this._loaded = true;
277
+ }
278
+
279
+ /**
280
+ * Get an efficient, readonly bitmap of the current canvas.
281
+ * @param size Optional size to crop
282
+ * @returns a Promise that resolves to an ImageBitmap
283
+ */
284
+ bitmap(size?: PtLike): Promise<ImageBitmap> {
285
+ const w = size ? size[0] : this._cv.width;
286
+ const h = size ? size[1] : this._cv.height;
287
+ return createImageBitmap(this._cv, 0, 0, w, h);
288
+ }
289
+
290
+ /**
291
+ * Create a canvas pattern for `fill()`
292
+ * @param reptition set how the pattern should repeat-fill
293
+ * @param dynamic If true, use this Img's internal canvas content as pattern fill. This enables the pattern to update dynamically.
294
+ * @returns a `CanvasPattern` instance for use in `fill()`
295
+ */
296
+ pattern(
297
+ reptition: CanvasPatternRepetition = "repeat",
298
+ dynamic: boolean = false,
299
+ ): CanvasPattern {
300
+ // any 2D context can create a pattern; fall back to an internal one so a
301
+ // CanvasSpace reference is optional
302
+ let ctx: RenderingContext2D | undefined = this._space
303
+ ? this._space.ctx
304
+ : undefined;
305
+ if (!ctx) {
306
+ if (!this._patternCtx) {
307
+ this._patternCtx = document.createElement("canvas").getContext("2d")!;
308
+ }
309
+ ctx = this._patternCtx;
310
+ }
311
+ return ctx.createPattern(dynamic ? this._cv : this._img, reptition)!;
312
+ }
313
+
314
+ /**
315
+ * Replace the image with the current canvas data. For example, you can use CanvasForm's static functions to draw on `this.ctx` and then update the current image.
316
+ * To display the internal canvas, use `form.image( [0, 0], img.current )`.
317
+ */
318
+ async sync(): Promise<Img> {
319
+ if (this._disposed) throw new Error("Img has been disposed");
320
+ // Blob-blit instead of a base64 round-trip: encode asynchronously, load
321
+ // the result into the image, and leave the working canvas untouched (the
322
+ // canvas is already the source of truth, so no redraw or readback is
323
+ // needed — and the retina canvas is no longer squashed through a lossy
324
+ // reload; a temporary canvas produces the logical-size image instead).
325
+ let source: HTMLCanvasElement = this._cv;
326
+ if (this._scale !== 1) {
327
+ source = document.createElement("canvas");
328
+ source.width = this._cv.width / this._scale;
329
+ source.height = this._cv.height / this._scale;
330
+ source
331
+ .getContext("2d")!
332
+ .drawImage(
333
+ this._cv,
334
+ 0,
335
+ 0,
336
+ this._cv.width,
337
+ this._cv.height,
338
+ 0,
339
+ 0,
340
+ source.width,
341
+ source.height,
342
+ );
343
+ }
344
+
345
+ const blob = await new Promise<Blob>((resolve, reject) => {
346
+ source.toBlob((b) =>
347
+ b ? resolve(b) : reject(new Error("Img cannot export canvas to blob")),
348
+ );
349
+ });
350
+
351
+ const url = URL.createObjectURL(blob);
352
+ this._objectUrl = url;
353
+ try {
354
+ await this._loadImageSrc(url);
355
+ if (this._disposed) throw new Error("Img has been disposed");
356
+ this._loaded = true;
357
+ } finally {
358
+ URL.revokeObjectURL(url);
359
+ this._objectUrl = null;
360
+ }
361
+ return this;
362
+ }
363
+
364
+ /**
365
+ * Get the RGBA values of a pixel in the image
366
+ * @param p position of the pixel
367
+ * @param rescale Specify if the pixel position should be scaled. Usually use rescale when tracking image and don't rescale when tracking canvas. You may also set a custom scale value.
368
+ * @returns [R,G,B,A] values of the pixel at the specific position
369
+ */
370
+ pixel(p: PtLike, rescale: boolean | number = true): Pt {
371
+ this._ensureData();
372
+ if (!this._data) {
373
+ Util.warn(
374
+ "Img has no pixel data — create it as editable and wait for load",
375
+ );
376
+ return new Pt(0, 0, 0, 0);
377
+ }
378
+ const s = typeof rescale == "number" ? rescale : rescale ? this._scale : 1;
379
+ return Img.getPixel(this._data, [p[0] * s, p[1] * s]);
380
+ }
381
+
382
+ /**
383
+ * Set the RGBA values of a pixel in the cached `ImageData`. Call [`Img.updatePixels`](#link)
384
+ * to write the changes onto the canvas.
385
+ * @param p position of the pixel
386
+ * @param rgba [R,G,B,A] values, 0-255
387
+ * @param rescale Specify if the pixel position should be scaled, matching [`Img.pixel`](#link)
388
+ */
389
+ setPixel(p: PtLike, rgba: PtLike, rescale: boolean | number = true): this {
390
+ this._ensureData();
391
+ if (!this._data) {
392
+ Util.warn("Img has no pixel data — create it as editable");
393
+ return this;
394
+ }
395
+ const s = typeof rescale == "number" ? rescale : rescale ? this._scale : 1;
396
+ const x = Math.floor(p[0] * s);
397
+ const y = Math.floor(p[1] * s);
398
+ if (x < 0 || y < 0 || x >= this._data.width || y >= this._data.height) {
399
+ return this;
400
+ }
401
+ const i = y * this._data.width * 4 + x * 4;
402
+ this._data.data[i] = rgba[0];
403
+ this._data.data[i + 1] = rgba[1];
404
+ this._data.data[i + 2] = rgba[2];
405
+ this._data.data[i + 3] = rgba[3] !== undefined ? rgba[3] : 255;
406
+ return this;
407
+ }
408
+
409
+ /**
410
+ * Refresh the cached `ImageData` from the canvas — call this after drawing on the
411
+ * canvas (eg, via [`Img.getForm`](#link)) before reading pixels.
412
+ */
413
+ loadPixels(): this {
414
+ if (!this._ctx) {
415
+ Util.warn("Img has no canvas — create it as editable");
416
+ return this;
417
+ }
418
+ this._refreshData();
419
+ return this;
420
+ }
421
+
422
+ /**
423
+ * Write the cached `ImageData` (eg, after [`Img.setPixel`](#link) calls) back onto
424
+ * the canvas.
425
+ */
426
+ updatePixels(): this {
427
+ if (!this._ctx || !this._data) {
428
+ Util.warn("Img has no canvas — create it as editable");
429
+ return this;
430
+ }
431
+ this._ctx.putImageData(this._data, 0, 0);
432
+ this._dataDirty = false; // canvas now equals the cached data
433
+ return this;
434
+ }
435
+
436
+ /**
437
+ * Given an ImaegData object and a position, return the RGBA pixel value at that position.
438
+ * @param imgData an ImageData object
439
+ * @param p a position on the image
440
+ * @returns [R,G,B,A] values of the pixel at the specific position
441
+ */
442
+ static getPixel(imgData: ImageData, p: PtLike): Pt {
443
+ // `new Pt(4)` + element stores is ~8x faster than the 4-argument
444
+ // constructor path, and out-of-bound reads return the zeroed Pt as before
445
+ const out = new Pt(4);
446
+ if (
447
+ p[0] < 0 ||
448
+ p[1] < 0 ||
449
+ p[0] >= imgData.width ||
450
+ p[1] >= imgData.height
451
+ ) {
452
+ return out;
453
+ }
454
+
455
+ const i = Math.floor(p[1]) * (imgData.width * 4) + Math.floor(p[0]) * 4;
456
+ const d = imgData.data;
457
+ if (i > d.length - 4) return out;
458
+
459
+ out[0] = d[i];
460
+ out[1] = d[i + 1];
461
+ out[2] = d[i + 2];
462
+ out[3] = d[i + 3];
463
+ return out;
464
+ }
465
+
466
+ /**
467
+ * Resize the canvas image. The original image is unchanged until `sync()`.
468
+ * @param sizeOrScale A PtLike array specifying either [x, y] scales or [x, y] sizes.
469
+ * @param asScale If true, treat the first parameter as scales. Otherwise, treat it as specific sizes.
470
+ */
471
+ resize(sizeOrScale: PtLike, asScale: boolean = false): this {
472
+ const hasImage = this._img.naturalWidth > 0;
473
+ // canvas-only images (eg, from `Img.blank`) scale relative to the canvas
474
+ // size, and need a snapshot since `_drawToScale` clears the canvas first
475
+ const refW = hasImage ? this._img.naturalWidth : this._cv.width;
476
+ const refH = hasImage ? this._img.naturalHeight : this._cv.height;
477
+ if (!refW || !refH) {
478
+ Util.warn("Img cannot resize before an image or canvas exists");
479
+ return this;
480
+ }
481
+ const s = asScale
482
+ ? sizeOrScale
483
+ : [sizeOrScale[0] / refW, sizeOrScale[1] / refH];
484
+
485
+ let source: HTMLImageElement | HTMLCanvasElement = this._img;
486
+ if (!hasImage) {
487
+ const snap = document.createElement("canvas");
488
+ snap.width = this._cv.width;
489
+ snap.height = this._cv.height;
490
+ snap.getContext("2d")!.drawImage(this._cv, 0, 0);
491
+ source = snap;
492
+ }
493
+ this._drawToScale(s, source);
494
+ this._dataDirty = true;
495
+ return this;
496
+ }
497
+
498
+ /**
499
+ * Crop an area of the image.
500
+ * @param box bounding box
501
+ */
502
+ crop(box: Bound): ImageData {
503
+ const s = this._scale;
504
+ return this._ctx.getImageData(
505
+ box[0][0] * s,
506
+ box[0][1] * s,
507
+ box.width * s,
508
+ box.height * s,
509
+ );
510
+ }
511
+
512
+ /**
513
+ * Apply filters such as blur and grayscale to the canvas image. The original image is unchanged until `sync()`.
514
+ * @param css a css filter string such as "blur(10px) contrast(200%)". See [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter#browser_compatibility) for a list of filter functions.
515
+ */
516
+ filter(css: string): this {
517
+ // "copy" replaces the canvas with the filtered result (the source is
518
+ // snapshotted before compositing) — plain source-over would blend the
519
+ // filtered copy with the original wherever the filter introduces alpha
520
+ const op = this._ctx.globalCompositeOperation;
521
+ this._ctx.globalCompositeOperation = "copy";
522
+ this._ctx.filter = css;
523
+ this._ctx.drawImage(this._cv, 0, 0);
524
+ this._ctx.filter = "none";
525
+ this._ctx.globalCompositeOperation = op;
526
+ this._dataDirty = true;
527
+ return this;
528
+ }
529
+
530
+ /**
531
+ * Dispose of the elements, data, and any object URL associated with this Img. Pending loads reject; the instance should not be reused.
532
+ */
533
+ dispose(): this {
534
+ this._disposed = true;
535
+ this._pendingLoadReject?.(new Error("Img has been disposed"));
536
+ this._pendingLoadReject = null;
537
+ if (this._img) {
538
+ this._img.onload = null;
539
+ this._img.onerror = null;
540
+ this._img.removeAttribute("src");
541
+ }
542
+ if (this._objectUrl) {
543
+ URL.revokeObjectURL(this._objectUrl);
544
+ this._objectUrl = null;
545
+ }
546
+ if (this._cv) this._cv.remove();
547
+ if (this._img) this._img.remove();
548
+ this._cv = null!;
549
+ this._ctx = null!;
550
+ this._patternCtx = null!;
551
+ this._img = null!;
552
+ this._data = null!;
553
+ this._loaded = false;
554
+ return this;
555
+ }
556
+
557
+ /**
558
+ * Remove the elements and data associated with this Img.
559
+ * @deprecated Use [`Img.dispose`](#link).
560
+ */
561
+ cleanup() {
562
+ this.dispose();
563
+ }
564
+
565
+ /**
566
+ * Create a blob url that can be passed to `Img.load`
567
+ * @param blob an image blob such as `new Blob([my_Uint8Array], {type: 'image/png'})`
568
+ * @param editable Specify if you want to manipulate pixels of this image. Default is `false`.
569
+ */
570
+ static fromBlob(
571
+ blob: Blob,
572
+ editable: boolean | ImgOptions = false,
573
+ space?: CanvasSpace,
574
+ ): Promise<Img> {
575
+ const url = URL.createObjectURL(blob);
576
+ const img = new Img(editable, space);
577
+ img._objectUrl = url;
578
+ // the decoded image no longer needs the URL once the load settles;
579
+ // dispose() also guards this
580
+ const done = () => {
581
+ URL.revokeObjectURL(url);
582
+ img._objectUrl = null;
583
+ };
584
+ return img.load(url).then(
585
+ (res) => {
586
+ done();
587
+ return res;
588
+ },
589
+ (err) => {
590
+ done();
591
+ throw err;
592
+ },
593
+ );
594
+ }
595
+
596
+ /**
597
+ * Convert ImageData object to a Blob, which you can then create an Img instance via [`Img.fromBlob`](#link). Note that the resulting image's dimensions will not account for pixel density.
598
+ * @param data
599
+ */
600
+ static imageDataToBlob(data: ImageData): Promise<Blob> {
601
+ return new Promise(function (resolve, reject) {
602
+ if (typeof document === "undefined") {
603
+ reject(
604
+ new Error("Cannot create html canvas element. document not found."),
605
+ );
606
+ return;
607
+ }
608
+ let cv = document.createElement("canvas") as HTMLCanvasElement;
609
+ cv.width = data.width;
610
+ cv.height = data.height;
611
+ cv.getContext("2d")!.putImageData(data, 0, 0);
612
+ cv.toBlob((blob) => {
613
+ resolve(blob!);
614
+ cv.remove();
615
+ });
616
+ });
617
+ }
618
+
619
+ /**
620
+ * Export current canvas image as base64 string
621
+ */
622
+ toBase64(): string {
623
+ return this._cv.toDataURL();
624
+ }
625
+
626
+ /**
627
+ * Export current canvas image as a blob
628
+ */
629
+ toBlob(): Promise<Blob> {
630
+ return new Promise((resolve) => {
631
+ this._cv.toBlob((blob) => resolve(blob!));
632
+ });
633
+ }
634
+
635
+ /**
636
+ * Get a CanvasForm for drawing on the internal canvas if this Img is editable
637
+ */
638
+ getForm(): CanvasForm | undefined {
639
+ if (!this._editable) {
640
+ Util.warn("Cannot get a CanvasForm because this Img is not editable");
641
+ }
642
+ return this._ctx ? new CanvasForm(this._ctx) : undefined;
643
+ }
644
+
645
+ /**
646
+ * Get current image source. If editable, this will return the canvas, otherwise it will return the original image.
647
+ */
648
+ get current(): CanvasImageSource {
649
+ return this._editable ? this._cv : this._img;
650
+ }
651
+
652
+ /**
653
+ * Get the original image
654
+ */
655
+ get image(): HTMLImageElement {
656
+ return this._img;
657
+ }
658
+
659
+ /**
660
+ * Get the internal canvas
661
+ */
662
+ get canvas(): HTMLCanvasElement {
663
+ return this._cv;
664
+ }
665
+
666
+ /**
667
+ * Get the internal canvas' ImageData
668
+ */
669
+ get data(): ImageData {
670
+ this._ensureData();
671
+ return this._data;
672
+ }
673
+
674
+ /**
675
+ * Get the internal canvas' context. You can use this to draw directly on canvas, or create a new [CanvasForm](#link) instance with it.
676
+ */
677
+ get ctx(): RenderingContext2D {
678
+ return this._ctx;
679
+ }
680
+
681
+ /**
682
+ * Get whether the image is loaded
683
+ */
684
+ get loaded(): boolean {
685
+ return this._loaded;
686
+ }
687
+
688
+ /**
689
+ * Get pixel density scale
690
+ */
691
+ get pixelScale(): number {
692
+ return this._scale;
693
+ }
694
+
695
+ /**
696
+ * Get size of the original image
697
+ */
698
+ get imageSize(): Pt {
699
+ if (!this._img || !this._img.width || !this._img.height) {
700
+ return this._cv ? this.canvasSize.$divide(this._scale) : new Pt(0, 0);
701
+ } else {
702
+ return new Pt(this._img.width, this._img.height);
703
+ }
704
+ }
705
+
706
+ /**
707
+ * Get size of the canvas
708
+ */
709
+ get canvasSize(): Pt {
710
+ return new Pt(this._cv.width, this._cv.height);
711
+ }
712
+
713
+ /**
714
+ * Get a Mat instance with a scale transform based on current `pixelScale`.
715
+ * This can be useful for generating a domMatrix for transforming patterns consistently across different pixel-density screens.
716
+ * @example `img.scaledMatrix.translate2d(...).rotate2D(...).domMatrix`
717
+ */
718
+ get scaledMatrix(): Mat {
719
+ const s = 1 / this._scale;
720
+ return new Mat().scale2D([s, s]);
721
+ }
722
+ }