@happy-dom/node-canvas-adapter 20.10.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/README.md +48 -0
- package/lib/CanvasAdapter.d.ts +69 -0
- package/lib/CanvasAdapter.js +216 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/package.json +41 -0
- package/src/CanvasAdapter.ts +275 -0
- package/src/index.ts +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
Pluggable canvas adapter for [Happy DOM](https://github.com/capricorn86/happy-dom) using [node-canvas](https://github.com/Automattic/node-canvas).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install canvas @happy-dom/node-canvas-adapter
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Documentation
|
|
12
|
+
|
|
13
|
+
You will find the documentation in the [Happy DOM Wiki](https://github.com/capricorn86/happy-dom/wiki) under [Node Canvas Adapter](https://github.com/capricorn86/happy-dom/wiki/Node-Canvas-Adapter).
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { Window } from 'happy-dom';
|
|
19
|
+
import { CanvasAdapter } from '@happy-dom/node-canvas-adapter';
|
|
20
|
+
|
|
21
|
+
const window = new Window({
|
|
22
|
+
settings: {
|
|
23
|
+
canvasAdapter: new CanvasAdapter(),
|
|
24
|
+
// Optionally, enable image file loading (e.g. for <img> elements)
|
|
25
|
+
enableImageFileLoading: true
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const canvas = window.document.createElement('canvas');
|
|
30
|
+
const context = canvas.getContext('2d');
|
|
31
|
+
|
|
32
|
+
canvas.width = 200;
|
|
33
|
+
canvas.height = 200;
|
|
34
|
+
|
|
35
|
+
// Now you can use canvas context
|
|
36
|
+
context.fillStyle = 'red';
|
|
37
|
+
context.fillRect(0, 0, 100, 100);
|
|
38
|
+
|
|
39
|
+
// Get data URL
|
|
40
|
+
const dataUrl = canvas.toDataURL();
|
|
41
|
+
|
|
42
|
+
// Output the data URL
|
|
43
|
+
console.log(dataUrl);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Happy DOM
|
|
47
|
+
|
|
48
|
+
[Documentation](https://github.com/capricorn86/happy-dom/wiki/) | [Getting Started](https://github.com/capricorn86/happy-dom/wiki/Getting-started) | [Setup as Test Environment](https://github.com/capricorn86/happy-dom/wiki/Setup-as-Test-Environment) | [GitHub](https://github.com/capricorn86/happy-dom/)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type ICanvasAdapter, type ICanvasRenderingContext2D, type ICanvasAdapterCaller, type Blob } from 'happy-dom';
|
|
2
|
+
/**
|
|
3
|
+
* Canvas adapter that delegates rendering to the `canvas` npm package.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```typescript
|
|
7
|
+
* import { Window } from 'happy-dom';
|
|
8
|
+
* import { CanvasAdapter } from '@happy-dom/node-canvas-adapter';
|
|
9
|
+
*
|
|
10
|
+
* const window = new Window({
|
|
11
|
+
* settings: { canvasAdapter: new CanvasAdapter() }
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* const canvas = window.document.createElement('canvas');
|
|
15
|
+
* const ctx = canvas.getContext('2d');
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export default class CanvasAdapter implements ICanvasAdapter {
|
|
19
|
+
#private;
|
|
20
|
+
/**
|
|
21
|
+
* Creates a rendering context for the given canvas element.
|
|
22
|
+
*
|
|
23
|
+
* @param canvas.canvas
|
|
24
|
+
* @param canvas The canvas element.
|
|
25
|
+
* @param canvas.window Window.
|
|
26
|
+
* @param contextType The context identifier ('2d', 'webgl', etc.).
|
|
27
|
+
* @param contextAttributes Optional context creation attributes.
|
|
28
|
+
* @param canvas.canvasType
|
|
29
|
+
* @returns The rendering context, or null if the type is unsupported.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* const ctx = adapter.getContext(canvas, '2d');
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
getContext({ canvas, window }: ICanvasAdapterCaller, contextType: string, contextAttributes?: Record<string, unknown>): ICanvasRenderingContext2D | null;
|
|
37
|
+
/**
|
|
38
|
+
* Serialize the canvas content as a data URL.
|
|
39
|
+
*
|
|
40
|
+
* @param caller Information about the caller, including the canvas element and its associated window and browser frame.
|
|
41
|
+
* @param caller.canvas Canvas.
|
|
42
|
+
* @param type MIME type of the output image.
|
|
43
|
+
* @param quality Encoder quality for lossy formats, in the range 0–1.
|
|
44
|
+
* @returns A data URL string.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* const url = adapter.toDataURL(canvas, 'image/png');
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
toDataURL({ canvas }: ICanvasAdapterCaller, type?: string, quality?: unknown): string;
|
|
52
|
+
/**
|
|
53
|
+
* Creates a Blob from the canvas content and passes it to the callback.
|
|
54
|
+
*
|
|
55
|
+
* @param caller Information about the caller, including the canvas element and its associated window and browser frame.
|
|
56
|
+
* @param caller.canvas Canvas.
|
|
57
|
+
* @param caller.window Window.
|
|
58
|
+
* @param canvas The canvas element.
|
|
59
|
+
* @param callback Receives the resulting Blob, or null on failure.
|
|
60
|
+
* @param type MIME type of the output image.
|
|
61
|
+
* @param quality Encoder quality for lossy formats, in the range 0–1.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* adapter.toBlob(canvas, (blob) => console.log(blob?.size));
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
toBlob({ canvas, window }: ICanvasAdapterCaller, callback: (blob: Blob | null) => void, type?: string, quality?: unknown): void;
|
|
69
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { createCanvas, Image as CanvasImage } from 'canvas';
|
|
2
|
+
import { HTMLCanvasElement, HTMLImageElement, OffscreenCanvas, ImageData, PropertySymbol, HTMLVideoElement, ImageBitmap } from 'happy-dom';
|
|
3
|
+
const EXTENDED_SYMBOL = Symbol('extended');
|
|
4
|
+
/**
|
|
5
|
+
* Canvas adapter that delegates rendering to the `canvas` npm package.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { Window } from 'happy-dom';
|
|
10
|
+
* import { CanvasAdapter } from '@happy-dom/node-canvas-adapter';
|
|
11
|
+
*
|
|
12
|
+
* const window = new Window({
|
|
13
|
+
* settings: { canvasAdapter: new CanvasAdapter() }
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* const canvas = window.document.createElement('canvas');
|
|
17
|
+
* const ctx = canvas.getContext('2d');
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export default class CanvasAdapter {
|
|
21
|
+
#canvases = new WeakMap();
|
|
22
|
+
/**
|
|
23
|
+
* Returns or creates a node-canvas instance bound to the given canvas element.
|
|
24
|
+
*
|
|
25
|
+
* @param canvas The canvas element.
|
|
26
|
+
* @returns The bound node-canvas instance.
|
|
27
|
+
*/
|
|
28
|
+
#getNodeCanvas(canvas) {
|
|
29
|
+
const existing = this.#canvases.get(canvas);
|
|
30
|
+
if (existing !== undefined) {
|
|
31
|
+
return existing;
|
|
32
|
+
}
|
|
33
|
+
const nodeCanvas = createCanvas(canvas.width, canvas.height);
|
|
34
|
+
this.#canvases.set(canvas, nodeCanvas);
|
|
35
|
+
return nodeCanvas;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Creates a rendering context for the given canvas element.
|
|
39
|
+
*
|
|
40
|
+
* @param canvas.canvas
|
|
41
|
+
* @param canvas The canvas element.
|
|
42
|
+
* @param canvas.window Window.
|
|
43
|
+
* @param contextType The context identifier ('2d', 'webgl', etc.).
|
|
44
|
+
* @param contextAttributes Optional context creation attributes.
|
|
45
|
+
* @param canvas.canvasType
|
|
46
|
+
* @returns The rendering context, or null if the type is unsupported.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* const ctx = adapter.getContext(canvas, '2d');
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
getContext({ canvas, window }, contextType, contextAttributes) {
|
|
54
|
+
if (contextType !== '2d') {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const context = this.#getNodeCanvas(canvas).getContext('2d', contextAttributes);
|
|
58
|
+
if (context === null) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (context[EXTENDED_SYMBOL]) {
|
|
62
|
+
return context;
|
|
63
|
+
}
|
|
64
|
+
context[EXTENDED_SYMBOL] = true;
|
|
65
|
+
// drawImage()
|
|
66
|
+
const originalDrawImage = context.drawImage;
|
|
67
|
+
context.drawImage = (...args) => {
|
|
68
|
+
const shape = args[0];
|
|
69
|
+
const drawImageBuffer = (buffer) => {
|
|
70
|
+
const canvasImage = new CanvasImage();
|
|
71
|
+
canvasImage.src = buffer;
|
|
72
|
+
canvasImage.width = shape.width;
|
|
73
|
+
canvasImage.height = shape.height;
|
|
74
|
+
args[0] = canvasImage;
|
|
75
|
+
originalDrawImage.apply(context, args);
|
|
76
|
+
};
|
|
77
|
+
if (shape instanceof HTMLImageElement) {
|
|
78
|
+
if (shape[PropertySymbol.buffer]) {
|
|
79
|
+
drawImageBuffer(shape[PropertySymbol.buffer]);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!shape.complete) {
|
|
83
|
+
shape.addEventListener('load', () => {
|
|
84
|
+
if (shape[PropertySymbol.buffer]) {
|
|
85
|
+
drawImageBuffer(shape[PropertySymbol.buffer]);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (shape instanceof HTMLVideoElement) {
|
|
92
|
+
// Not supported yet
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (shape instanceof ImageBitmap ||
|
|
96
|
+
shape instanceof HTMLCanvasElement ||
|
|
97
|
+
shape instanceof OffscreenCanvas) {
|
|
98
|
+
const nodeCanvas = this.#getNodeCanvas(shape[PropertySymbol.canvas] || shape);
|
|
99
|
+
if (nodeCanvas) {
|
|
100
|
+
args[0] = nodeCanvas;
|
|
101
|
+
originalDrawImage.apply(context, args);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
// createImageData()
|
|
107
|
+
const originalCreateImageData = context.createImageData;
|
|
108
|
+
context.createImageData = (...args) => {
|
|
109
|
+
const imageData = originalCreateImageData.apply(context, args);
|
|
110
|
+
return new ImageData(imageData.data, imageData.width, imageData.height);
|
|
111
|
+
};
|
|
112
|
+
// putImageData()
|
|
113
|
+
const originalPutImageData = context.putImageData;
|
|
114
|
+
context.putImageData = (...args) => {
|
|
115
|
+
const imageData = args[0];
|
|
116
|
+
if (!(imageData instanceof ImageData)) {
|
|
117
|
+
throw new window.TypeError(`Failed to execute 'putImageData' on 'CanvasRenderingContext2D': parameter 1 is not of type 'ImageData'`);
|
|
118
|
+
}
|
|
119
|
+
const canvasImageData = originalCreateImageData.call(context, imageData.width, imageData.height);
|
|
120
|
+
canvasImageData.data.set(imageData.data);
|
|
121
|
+
args[0] = canvasImageData;
|
|
122
|
+
originalPutImageData.apply(context, args);
|
|
123
|
+
};
|
|
124
|
+
// getImageData()
|
|
125
|
+
const originalGetImageData = context.getImageData;
|
|
126
|
+
context.getImageData = (...args) => {
|
|
127
|
+
const imageData = originalGetImageData.apply(context, args);
|
|
128
|
+
return new ImageData(imageData.data, imageData.width, imageData.height);
|
|
129
|
+
};
|
|
130
|
+
// createPattern()
|
|
131
|
+
const originalCreatePattern = context.createPattern;
|
|
132
|
+
context.createPattern = (shape, repetition) => {
|
|
133
|
+
if (shape instanceof HTMLImageElement) {
|
|
134
|
+
if (!shape[PropertySymbol.buffer]) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const canvasImage = new CanvasImage();
|
|
138
|
+
canvasImage.src = shape[PropertySymbol.buffer];
|
|
139
|
+
canvasImage.width = shape.width;
|
|
140
|
+
canvasImage.height = shape.height;
|
|
141
|
+
return originalCreatePattern.call(context, canvasImage, repetition);
|
|
142
|
+
}
|
|
143
|
+
if (shape instanceof HTMLVideoElement) {
|
|
144
|
+
// Not supported yet
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (shape instanceof ImageBitmap ||
|
|
148
|
+
shape instanceof HTMLCanvasElement ||
|
|
149
|
+
shape instanceof OffscreenCanvas) {
|
|
150
|
+
const nodeCanvas = this.#getNodeCanvas(shape[PropertySymbol.canvas] || shape);
|
|
151
|
+
if (nodeCanvas) {
|
|
152
|
+
return originalCreatePattern.call(context, nodeCanvas, repetition);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
};
|
|
157
|
+
return context;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Serialize the canvas content as a data URL.
|
|
161
|
+
*
|
|
162
|
+
* @param caller Information about the caller, including the canvas element and its associated window and browser frame.
|
|
163
|
+
* @param caller.canvas Canvas.
|
|
164
|
+
* @param type MIME type of the output image.
|
|
165
|
+
* @param quality Encoder quality for lossy formats, in the range 0–1.
|
|
166
|
+
* @returns A data URL string.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```typescript
|
|
170
|
+
* const url = adapter.toDataURL(canvas, 'image/png');
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
toDataURL({ canvas }, type, quality) {
|
|
174
|
+
const nodeCanvas = this.#getNodeCanvas(canvas);
|
|
175
|
+
if (type === 'image/jpeg') {
|
|
176
|
+
return nodeCanvas.toDataURL('image/jpeg', quality);
|
|
177
|
+
}
|
|
178
|
+
return nodeCanvas.toDataURL('image/png');
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Creates a Blob from the canvas content and passes it to the callback.
|
|
182
|
+
*
|
|
183
|
+
* @param caller Information about the caller, including the canvas element and its associated window and browser frame.
|
|
184
|
+
* @param caller.canvas Canvas.
|
|
185
|
+
* @param caller.window Window.
|
|
186
|
+
* @param canvas The canvas element.
|
|
187
|
+
* @param callback Receives the resulting Blob, or null on failure.
|
|
188
|
+
* @param type MIME type of the output image.
|
|
189
|
+
* @param quality Encoder quality for lossy formats, in the range 0–1.
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```typescript
|
|
193
|
+
* adapter.toBlob(canvas, (blob) => console.log(blob?.size));
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
toBlob({ canvas, window }, callback, type, quality) {
|
|
197
|
+
const nodeCanvas = this.#getNodeCanvas(canvas);
|
|
198
|
+
if (type === 'image/jpeg') {
|
|
199
|
+
nodeCanvas.toBuffer((error, buffer) => {
|
|
200
|
+
if (error !== null) {
|
|
201
|
+
callback(null);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
callback(new window.Blob([new Uint8Array(buffer)], { type: 'image/jpeg' }));
|
|
205
|
+
}, 'image/jpeg', { quality: quality });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
nodeCanvas.toBuffer((error, buffer) => {
|
|
209
|
+
if (error !== null) {
|
|
210
|
+
callback(null);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
callback(new window.Blob([new Uint8Array(buffer)], { type: type ?? 'image/png' }));
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as CanvasAdapter } from './CanvasAdapter.js';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as CanvasAdapter } from './CanvasAdapter.js';
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@happy-dom/node-canvas-adapter",
|
|
3
|
+
"version": "20.10.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"homepage": "https://github.com/capricorn86/happy-dom/tree/master/packages/@happy-dom/node-canvas-adapter",
|
|
6
|
+
"repository": "https://github.com/capricorn86/happy-dom",
|
|
7
|
+
"author": "David Ortner",
|
|
8
|
+
"description": "Pluggable canvas adapter for happy-dom using node-canvas.",
|
|
9
|
+
"main": "lib/index.js",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"canvas",
|
|
13
|
+
"happy-dom",
|
|
14
|
+
"adapter",
|
|
15
|
+
"node-canvas",
|
|
16
|
+
"rendering"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"compile": "tsc",
|
|
23
|
+
"watch": "tsc -w --preserveWatchOutput",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:debug": "vitest run --inspect-brk --no-file-parallelism"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"canvas": "^3.2.3",
|
|
29
|
+
"happy-dom": ">=20.9.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@vitest/ui": "^4.0.16",
|
|
33
|
+
"canvas": "^3.2.1",
|
|
34
|
+
"happy-dom": "^20.10.0",
|
|
35
|
+
"typescript": "^5.8.3",
|
|
36
|
+
"vitest": "^4.0.16"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=20.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { createCanvas, Image as CanvasImage } from 'canvas';
|
|
2
|
+
import {
|
|
3
|
+
HTMLCanvasElement,
|
|
4
|
+
HTMLImageElement,
|
|
5
|
+
OffscreenCanvas,
|
|
6
|
+
ImageData,
|
|
7
|
+
PropertySymbol,
|
|
8
|
+
type ICanvasAdapter,
|
|
9
|
+
type ICanvasRenderingContext2D,
|
|
10
|
+
type ICanvasAdapterCaller,
|
|
11
|
+
type ICanvasShape,
|
|
12
|
+
type Blob,
|
|
13
|
+
HTMLVideoElement,
|
|
14
|
+
ImageBitmap
|
|
15
|
+
} from 'happy-dom';
|
|
16
|
+
|
|
17
|
+
const EXTENDED_SYMBOL = Symbol('extended');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Canvas adapter that delegates rendering to the `canvas` npm package.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* import { Window } from 'happy-dom';
|
|
25
|
+
* import { CanvasAdapter } from '@happy-dom/node-canvas-adapter';
|
|
26
|
+
*
|
|
27
|
+
* const window = new Window({
|
|
28
|
+
* settings: { canvasAdapter: new CanvasAdapter() }
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* const canvas = window.document.createElement('canvas');
|
|
32
|
+
* const ctx = canvas.getContext('2d');
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export default class CanvasAdapter implements ICanvasAdapter {
|
|
36
|
+
readonly #canvases = new WeakMap<ICanvasShape, ReturnType<typeof createCanvas>>();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Returns or creates a node-canvas instance bound to the given canvas element.
|
|
40
|
+
*
|
|
41
|
+
* @param canvas The canvas element.
|
|
42
|
+
* @returns The bound node-canvas instance.
|
|
43
|
+
*/
|
|
44
|
+
#getNodeCanvas(canvas: ICanvasShape): ReturnType<typeof createCanvas> {
|
|
45
|
+
const existing = this.#canvases.get(canvas);
|
|
46
|
+
if (existing !== undefined) {
|
|
47
|
+
return existing;
|
|
48
|
+
}
|
|
49
|
+
const nodeCanvas = createCanvas(canvas.width, canvas.height);
|
|
50
|
+
this.#canvases.set(canvas, nodeCanvas);
|
|
51
|
+
return nodeCanvas;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Creates a rendering context for the given canvas element.
|
|
56
|
+
*
|
|
57
|
+
* @param canvas.canvas
|
|
58
|
+
* @param canvas The canvas element.
|
|
59
|
+
* @param canvas.window Window.
|
|
60
|
+
* @param contextType The context identifier ('2d', 'webgl', etc.).
|
|
61
|
+
* @param contextAttributes Optional context creation attributes.
|
|
62
|
+
* @param canvas.canvasType
|
|
63
|
+
* @returns The rendering context, or null if the type is unsupported.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* const ctx = adapter.getContext(canvas, '2d');
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
public getContext(
|
|
71
|
+
{ canvas, window }: ICanvasAdapterCaller,
|
|
72
|
+
contextType: string,
|
|
73
|
+
contextAttributes?: Record<string, unknown>
|
|
74
|
+
): ICanvasRenderingContext2D | null {
|
|
75
|
+
if (contextType !== '2d') {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const context = this.#getNodeCanvas(canvas).getContext('2d', contextAttributes);
|
|
79
|
+
|
|
80
|
+
if (context === null) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if ((<any>context)[EXTENDED_SYMBOL]) {
|
|
85
|
+
return <ICanvasRenderingContext2D>(<unknown>context);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
(<any>context)[EXTENDED_SYMBOL] = true;
|
|
89
|
+
|
|
90
|
+
// drawImage()
|
|
91
|
+
const originalDrawImage = context.drawImage;
|
|
92
|
+
context.drawImage = (...args: any[]) => {
|
|
93
|
+
const shape = <ICanvasShape>args[0];
|
|
94
|
+
const drawImageBuffer = (buffer: Buffer): void => {
|
|
95
|
+
const canvasImage = new CanvasImage();
|
|
96
|
+
canvasImage.src = buffer;
|
|
97
|
+
canvasImage.width = shape.width;
|
|
98
|
+
canvasImage.height = shape.height;
|
|
99
|
+
args[0] = canvasImage;
|
|
100
|
+
(<any>originalDrawImage).apply(context, args);
|
|
101
|
+
};
|
|
102
|
+
if (shape instanceof HTMLImageElement) {
|
|
103
|
+
if (shape[PropertySymbol.buffer]) {
|
|
104
|
+
drawImageBuffer(shape[PropertySymbol.buffer]);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!shape.complete) {
|
|
108
|
+
shape.addEventListener('load', () => {
|
|
109
|
+
if (shape[PropertySymbol.buffer]) {
|
|
110
|
+
drawImageBuffer(shape[PropertySymbol.buffer]);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (shape instanceof HTMLVideoElement) {
|
|
117
|
+
// Not supported yet
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (
|
|
121
|
+
shape instanceof ImageBitmap ||
|
|
122
|
+
shape instanceof HTMLCanvasElement ||
|
|
123
|
+
shape instanceof OffscreenCanvas
|
|
124
|
+
) {
|
|
125
|
+
const nodeCanvas = this.#getNodeCanvas(
|
|
126
|
+
(<ImageBitmap>shape)[PropertySymbol.canvas] || shape
|
|
127
|
+
);
|
|
128
|
+
if (nodeCanvas) {
|
|
129
|
+
args[0] = nodeCanvas;
|
|
130
|
+
(<any>originalDrawImage).apply(context, args);
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// createImageData()
|
|
137
|
+
const originalCreateImageData = context.createImageData;
|
|
138
|
+
context.createImageData = (...args: any[]) => {
|
|
139
|
+
const imageData = originalCreateImageData.apply(context, <any>args);
|
|
140
|
+
return new ImageData(imageData.data, imageData.width, imageData.height);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// putImageData()
|
|
144
|
+
const originalPutImageData = context.putImageData;
|
|
145
|
+
context.putImageData = (...args: any[]) => {
|
|
146
|
+
const imageData = args[0];
|
|
147
|
+
if (!(imageData instanceof ImageData)) {
|
|
148
|
+
throw new window.TypeError(
|
|
149
|
+
`Failed to execute 'putImageData' on 'CanvasRenderingContext2D': parameter 1 is not of type 'ImageData'`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const canvasImageData = (<any>originalCreateImageData).call(
|
|
153
|
+
context,
|
|
154
|
+
imageData.width,
|
|
155
|
+
imageData.height
|
|
156
|
+
);
|
|
157
|
+
canvasImageData.data.set(imageData.data);
|
|
158
|
+
args[0] = canvasImageData;
|
|
159
|
+
(<any>originalPutImageData).apply(context, args);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// getImageData()
|
|
163
|
+
const originalGetImageData = context.getImageData;
|
|
164
|
+
context.getImageData = (...args: any[]) => {
|
|
165
|
+
const imageData = originalGetImageData.apply(context, <any>args);
|
|
166
|
+
return new ImageData(imageData.data, imageData.width, imageData.height);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// createPattern()
|
|
170
|
+
const originalCreatePattern = context.createPattern;
|
|
171
|
+
context.createPattern = (
|
|
172
|
+
shape: any,
|
|
173
|
+
repetition: '' | 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat'
|
|
174
|
+
): any => {
|
|
175
|
+
if (shape instanceof HTMLImageElement) {
|
|
176
|
+
if (!shape[PropertySymbol.buffer]) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const canvasImage = new CanvasImage();
|
|
181
|
+
canvasImage.src = shape[PropertySymbol.buffer];
|
|
182
|
+
canvasImage.width = shape.width;
|
|
183
|
+
canvasImage.height = shape.height;
|
|
184
|
+
return originalCreatePattern.call(context, canvasImage, repetition);
|
|
185
|
+
}
|
|
186
|
+
if (shape instanceof HTMLVideoElement) {
|
|
187
|
+
// Not supported yet
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
if (
|
|
191
|
+
shape instanceof ImageBitmap ||
|
|
192
|
+
shape instanceof HTMLCanvasElement ||
|
|
193
|
+
shape instanceof OffscreenCanvas
|
|
194
|
+
) {
|
|
195
|
+
const nodeCanvas = this.#getNodeCanvas(
|
|
196
|
+
(<ImageBitmap>shape)[PropertySymbol.canvas] || shape
|
|
197
|
+
);
|
|
198
|
+
if (nodeCanvas) {
|
|
199
|
+
return originalCreatePattern.call(context, nodeCanvas, repetition);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
return <ICanvasRenderingContext2D>(<unknown>context);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Serialize the canvas content as a data URL.
|
|
210
|
+
*
|
|
211
|
+
* @param caller Information about the caller, including the canvas element and its associated window and browser frame.
|
|
212
|
+
* @param caller.canvas Canvas.
|
|
213
|
+
* @param type MIME type of the output image.
|
|
214
|
+
* @param quality Encoder quality for lossy formats, in the range 0–1.
|
|
215
|
+
* @returns A data URL string.
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```typescript
|
|
219
|
+
* const url = adapter.toDataURL(canvas, 'image/png');
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
public toDataURL({ canvas }: ICanvasAdapterCaller, type?: string, quality?: unknown): string {
|
|
223
|
+
const nodeCanvas = this.#getNodeCanvas(canvas);
|
|
224
|
+
if (type === 'image/jpeg') {
|
|
225
|
+
return nodeCanvas.toDataURL('image/jpeg', <number>quality);
|
|
226
|
+
}
|
|
227
|
+
return nodeCanvas.toDataURL('image/png');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Creates a Blob from the canvas content and passes it to the callback.
|
|
232
|
+
*
|
|
233
|
+
* @param caller Information about the caller, including the canvas element and its associated window and browser frame.
|
|
234
|
+
* @param caller.canvas Canvas.
|
|
235
|
+
* @param caller.window Window.
|
|
236
|
+
* @param canvas The canvas element.
|
|
237
|
+
* @param callback Receives the resulting Blob, or null on failure.
|
|
238
|
+
* @param type MIME type of the output image.
|
|
239
|
+
* @param quality Encoder quality for lossy formats, in the range 0–1.
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```typescript
|
|
243
|
+
* adapter.toBlob(canvas, (blob) => console.log(blob?.size));
|
|
244
|
+
* ```
|
|
245
|
+
*/
|
|
246
|
+
public toBlob(
|
|
247
|
+
{ canvas, window }: ICanvasAdapterCaller,
|
|
248
|
+
callback: (blob: Blob | null) => void,
|
|
249
|
+
type?: string,
|
|
250
|
+
quality?: unknown
|
|
251
|
+
): void {
|
|
252
|
+
const nodeCanvas = this.#getNodeCanvas(canvas);
|
|
253
|
+
if (type === 'image/jpeg') {
|
|
254
|
+
nodeCanvas.toBuffer(
|
|
255
|
+
(error, buffer) => {
|
|
256
|
+
if (error !== null) {
|
|
257
|
+
callback(null);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
callback(new window.Blob([new Uint8Array(buffer)], { type: 'image/jpeg' }));
|
|
261
|
+
},
|
|
262
|
+
'image/jpeg',
|
|
263
|
+
{ quality: <number>quality }
|
|
264
|
+
);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
nodeCanvas.toBuffer((error, buffer) => {
|
|
268
|
+
if (error !== null) {
|
|
269
|
+
callback(null);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
callback(new window.Blob([new Uint8Array(buffer)], { type: type ?? 'image/png' }));
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as CanvasAdapter } from './CanvasAdapter.js';
|