@meonode/canvas 1.6.0-beta.1 → 1.6.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/dist/cjs/canvas/image.canvas.util.d.ts +14 -6
- package/dist/cjs/canvas/image.canvas.util.d.ts.map +1 -1
- package/dist/cjs/canvas/image.canvas.util.js +78 -71
- package/dist/cjs/canvas/image.canvas.util.js.map +1 -1
- package/dist/cjs/canvas/root.canvas.util.d.ts.map +1 -1
- package/dist/cjs/canvas/root.canvas.util.js +104 -25
- package/dist/cjs/canvas/root.canvas.util.js.map +1 -1
- package/dist/cjs/canvas/worker.types.d.ts +76 -0
- package/dist/cjs/canvas/worker.types.d.ts.map +1 -0
- package/dist/cjs/render.worker.js +58 -9
- package/dist/cjs/render.worker.js.map +1 -1
- package/dist/esm/canvas/image.canvas.util.d.ts +14 -6
- package/dist/esm/canvas/image.canvas.util.d.ts.map +1 -1
- package/dist/esm/canvas/image.canvas.util.js +78 -71
- package/dist/esm/canvas/root.canvas.util.d.ts.map +1 -1
- package/dist/esm/canvas/root.canvas.util.js +104 -25
- package/dist/esm/canvas/worker.types.d.ts +76 -0
- package/dist/esm/canvas/worker.types.d.ts.map +1 -0
- package/dist/esm/render.worker.js +58 -9
- package/package.json +1 -1
|
@@ -31,22 +31,77 @@ function configure(options) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
34
|
+
* Proxies all skia-canvas Canvas APIs to a Canvas instance living inside a worker thread.
|
|
35
|
+
* Sync methods (toBufferSync, toURLSync) return from a pre-encoded PNG buffer.
|
|
36
|
+
* Async methods (toBuffer, toURL, toFile, getters) delegate to the worker.
|
|
36
37
|
*/
|
|
37
38
|
class WorkerCanvas {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
width;
|
|
40
|
+
height;
|
|
41
|
+
_buffer; // pre-encoded PNG for sync use
|
|
42
|
+
_pool;
|
|
43
|
+
_workerIdx;
|
|
44
|
+
_canvasId;
|
|
45
|
+
constructor(opts) {
|
|
46
|
+
this._buffer = opts.buffer;
|
|
47
|
+
this.width = opts.width;
|
|
48
|
+
this.height = opts.height;
|
|
49
|
+
this._pool = opts.pool;
|
|
50
|
+
this._workerIdx = opts.workerIdx;
|
|
51
|
+
this._canvasId = opts.canvasId;
|
|
41
52
|
}
|
|
42
|
-
|
|
53
|
+
_call(method, ...args) {
|
|
54
|
+
return this._pool.callOnCanvas(this._workerIdx, this._canvasId, method, args);
|
|
55
|
+
}
|
|
56
|
+
// --- Sync methods: return from pre-encoded PNG buffer ---
|
|
57
|
+
toBufferSync(_format, _options) {
|
|
43
58
|
return this._buffer;
|
|
44
59
|
}
|
|
45
|
-
|
|
46
|
-
return
|
|
60
|
+
toURLSync(_format, _options) {
|
|
61
|
+
return `data:image/png;base64,${this._buffer.toString('base64')}`;
|
|
62
|
+
}
|
|
63
|
+
// --- Async methods: delegate to worker ---
|
|
64
|
+
toBuffer(format, options) {
|
|
65
|
+
return this._call('toBuffer', format, options);
|
|
66
|
+
}
|
|
67
|
+
toURL(format, options) {
|
|
68
|
+
return this._call('toURL', format, options);
|
|
69
|
+
}
|
|
70
|
+
toFile(filename, options) {
|
|
71
|
+
return this._call('toFile', filename, options);
|
|
72
|
+
}
|
|
73
|
+
/** Returns a Buffer (Sharp instance cannot be transferred across threads) */
|
|
74
|
+
toSharp(options) {
|
|
75
|
+
return this._call('toSharp', options);
|
|
76
|
+
}
|
|
77
|
+
toSharpSync(_options) {
|
|
78
|
+
throw new Error('[canvas] toSharpSync() is not available in worker mode — use toSharp() instead');
|
|
79
|
+
}
|
|
80
|
+
// --- Async convenience getters ---
|
|
81
|
+
get png() {
|
|
82
|
+
return this._call('toBuffer', 'png');
|
|
83
|
+
}
|
|
84
|
+
get webp() {
|
|
85
|
+
return this._call('toBuffer', 'webp');
|
|
86
|
+
}
|
|
87
|
+
get jpg() {
|
|
88
|
+
return this._call('toBuffer', 'jpg');
|
|
89
|
+
}
|
|
90
|
+
get svg() {
|
|
91
|
+
return this._call('toBuffer', 'svg');
|
|
92
|
+
}
|
|
93
|
+
get pdf() {
|
|
94
|
+
return this._call('toBuffer', 'pdf');
|
|
95
|
+
}
|
|
96
|
+
get raw() {
|
|
97
|
+
return this._call('toBuffer', 'raw');
|
|
98
|
+
}
|
|
99
|
+
/** Release the Canvas from worker memory. Call when done with this object. */
|
|
100
|
+
release() {
|
|
101
|
+
this._pool.releaseCanvas(this._workerIdx, this._canvasId);
|
|
47
102
|
}
|
|
48
103
|
}
|
|
49
|
-
/**
|
|
104
|
+
/** Worker thread pool — routes render and canvas-call messages */
|
|
50
105
|
class WorkerPool {
|
|
51
106
|
workers = [];
|
|
52
107
|
idle = [];
|
|
@@ -56,22 +111,30 @@ class WorkerPool {
|
|
|
56
111
|
constructor(size) {
|
|
57
112
|
this.init(size);
|
|
58
113
|
}
|
|
59
|
-
|
|
114
|
+
init(size) {
|
|
60
115
|
const workerFile = path.join(path.dirname(fileURLToPath(import.meta.url)), '../render.worker.js');
|
|
61
116
|
for (let i = 0; i < size; i++) {
|
|
117
|
+
const workerIdx = i;
|
|
62
118
|
const worker = new Worker(workerFile);
|
|
63
|
-
worker.on('message', (
|
|
64
|
-
const task = this.pending.get(
|
|
119
|
+
worker.on('message', (msg) => {
|
|
120
|
+
const task = this.pending.get(msg.taskId);
|
|
65
121
|
if (!task)
|
|
66
122
|
return;
|
|
67
|
-
this.pending.delete(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
123
|
+
this.pending.delete(msg.taskId);
|
|
124
|
+
if ('error' in msg) {
|
|
125
|
+
task.reject(new Error(msg.error));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if ('canvasId' in msg) {
|
|
129
|
+
// Render complete — put worker back to idle
|
|
130
|
+
this.idle.push(worker);
|
|
131
|
+
this.drain();
|
|
132
|
+
const result = { buffer: msg.buffer, canvasId: msg.canvasId, workerIdx, width: msg.width, height: msg.height };
|
|
133
|
+
task.resolve(result);
|
|
72
134
|
}
|
|
73
135
|
else {
|
|
74
|
-
|
|
136
|
+
// Canvas method call complete
|
|
137
|
+
task.resolve(msg.result);
|
|
75
138
|
}
|
|
76
139
|
});
|
|
77
140
|
this.workers.push(worker);
|
|
@@ -82,22 +145,36 @@ class WorkerPool {
|
|
|
82
145
|
while (this.queue.length > 0 && this.idle.length > 0) {
|
|
83
146
|
const task = this.queue.shift();
|
|
84
147
|
const worker = this.idle.pop();
|
|
85
|
-
|
|
148
|
+
const request = { type: 'render', taskId: task.id, props: task.props };
|
|
149
|
+
worker.postMessage(request);
|
|
86
150
|
}
|
|
87
151
|
}
|
|
88
152
|
render(props) {
|
|
89
153
|
return new Promise((resolve, reject) => {
|
|
90
154
|
const id = this.nextId++;
|
|
91
|
-
this.pending.set(id, { resolve, reject });
|
|
155
|
+
this.pending.set(id, { resolve: resolve, reject });
|
|
92
156
|
if (this.idle.length > 0) {
|
|
93
157
|
const worker = this.idle.pop();
|
|
94
|
-
|
|
158
|
+
const request = { type: 'render', taskId: id, props };
|
|
159
|
+
worker.postMessage(request);
|
|
95
160
|
}
|
|
96
161
|
else {
|
|
97
162
|
this.queue.push({ id, props });
|
|
98
163
|
}
|
|
99
164
|
});
|
|
100
165
|
}
|
|
166
|
+
callOnCanvas(workerIdx, canvasId, method, args) {
|
|
167
|
+
return new Promise((resolve, reject) => {
|
|
168
|
+
const id = this.nextId++;
|
|
169
|
+
this.pending.set(id, { resolve: resolve, reject });
|
|
170
|
+
const request = { type: 'call', taskId: id, canvasId, method, args };
|
|
171
|
+
this.workers[workerIdx].postMessage(request);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
releaseCanvas(workerIdx, canvasId) {
|
|
175
|
+
const request = { type: 'release', canvasId };
|
|
176
|
+
this.workers[workerIdx]?.postMessage(request);
|
|
177
|
+
}
|
|
101
178
|
terminate() {
|
|
102
179
|
this.workers.forEach(w => w.terminate());
|
|
103
180
|
}
|
|
@@ -211,15 +288,17 @@ class RootNode extends ColumnNode {
|
|
|
211
288
|
* @returns Promise resolving to the rendered Canvas instance
|
|
212
289
|
*/
|
|
213
290
|
async render() {
|
|
214
|
-
// Step 1: Load all images with a concurrency limit to avoid overwhelming remote sources
|
|
291
|
+
// Step 1: Load all images with a concurrency limit to avoid overwhelming remote sources.
|
|
292
|
+
// A per-render cache deduplicates identical src+color combinations within this render pass.
|
|
215
293
|
const imageNodes = this.findAllImageNodes();
|
|
216
294
|
if (imageNodes.length > 0) {
|
|
295
|
+
const imageCache = new Map();
|
|
217
296
|
const CONCURRENCY = 5;
|
|
218
297
|
const queue = [...imageNodes];
|
|
219
298
|
const workers = Array.from({ length: Math.min(CONCURRENCY, queue.length) }, async () => {
|
|
220
299
|
while (queue.length > 0) {
|
|
221
300
|
const node = queue.shift();
|
|
222
|
-
await node.load();
|
|
301
|
+
await node.load(imageCache);
|
|
223
302
|
}
|
|
224
303
|
});
|
|
225
304
|
await Promise.allSettled(workers);
|
|
@@ -259,8 +338,8 @@ const Root = async (props) => {
|
|
|
259
338
|
if (!_workerPool) {
|
|
260
339
|
_workerPool = new WorkerPool(_workerPoolSize);
|
|
261
340
|
}
|
|
262
|
-
const
|
|
263
|
-
return new WorkerCanvas(
|
|
341
|
+
const result = await _workerPool.render(props);
|
|
342
|
+
return new WorkerCanvas({ ...result, pool: _workerPool });
|
|
264
343
|
}
|
|
265
344
|
return new RootNode(props).render();
|
|
266
345
|
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ExportFormat, ExportOptions, SaveOptions, RenderOptions } from 'skia-canvas';
|
|
2
|
+
import type { RootProps } from '../canvas/canvas.type.js';
|
|
3
|
+
export interface CanvasCallMap {
|
|
4
|
+
toBuffer: {
|
|
5
|
+
args: [ExportFormat, ExportOptions?];
|
|
6
|
+
result: Buffer;
|
|
7
|
+
};
|
|
8
|
+
toURL: {
|
|
9
|
+
args: [ExportFormat, ExportOptions?];
|
|
10
|
+
result: string;
|
|
11
|
+
};
|
|
12
|
+
toFile: {
|
|
13
|
+
args: [string, SaveOptions?];
|
|
14
|
+
result: void;
|
|
15
|
+
};
|
|
16
|
+
toSharp: {
|
|
17
|
+
args: [RenderOptions?];
|
|
18
|
+
result: Buffer;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export type CanvasCallMethod = keyof CanvasCallMap;
|
|
22
|
+
export type CallArgs<M extends CanvasCallMethod> = CanvasCallMap[M]['args'];
|
|
23
|
+
export type CallResult<M extends CanvasCallMethod> = CanvasCallMap[M]['result'];
|
|
24
|
+
export interface WorkerRenderRequest {
|
|
25
|
+
type: 'render';
|
|
26
|
+
taskId: number;
|
|
27
|
+
props: RootProps;
|
|
28
|
+
}
|
|
29
|
+
/** Discriminated union — narrows args alongside method in switch statements */
|
|
30
|
+
export type WorkerCallRequest = {
|
|
31
|
+
type: 'call';
|
|
32
|
+
taskId: number;
|
|
33
|
+
canvasId: number;
|
|
34
|
+
method: 'toBuffer';
|
|
35
|
+
args: CallArgs<'toBuffer'>;
|
|
36
|
+
} | {
|
|
37
|
+
type: 'call';
|
|
38
|
+
taskId: number;
|
|
39
|
+
canvasId: number;
|
|
40
|
+
method: 'toURL';
|
|
41
|
+
args: CallArgs<'toURL'>;
|
|
42
|
+
} | {
|
|
43
|
+
type: 'call';
|
|
44
|
+
taskId: number;
|
|
45
|
+
canvasId: number;
|
|
46
|
+
method: 'toFile';
|
|
47
|
+
args: CallArgs<'toFile'>;
|
|
48
|
+
} | {
|
|
49
|
+
type: 'call';
|
|
50
|
+
taskId: number;
|
|
51
|
+
canvasId: number;
|
|
52
|
+
method: 'toSharp';
|
|
53
|
+
args: CallArgs<'toSharp'>;
|
|
54
|
+
};
|
|
55
|
+
export interface WorkerReleaseRequest {
|
|
56
|
+
type: 'release';
|
|
57
|
+
canvasId: number;
|
|
58
|
+
}
|
|
59
|
+
export type WorkerRequest = WorkerRenderRequest | WorkerCallRequest | WorkerReleaseRequest;
|
|
60
|
+
export interface WorkerRenderResponse {
|
|
61
|
+
taskId: number;
|
|
62
|
+
canvasId: number;
|
|
63
|
+
buffer: Buffer;
|
|
64
|
+
width: number;
|
|
65
|
+
height: number;
|
|
66
|
+
}
|
|
67
|
+
export interface WorkerCallResponse {
|
|
68
|
+
taskId: number;
|
|
69
|
+
result: Buffer | string | void;
|
|
70
|
+
}
|
|
71
|
+
export interface WorkerErrorResponse {
|
|
72
|
+
taskId: number;
|
|
73
|
+
error: string;
|
|
74
|
+
}
|
|
75
|
+
export type WorkerResponse = WorkerRenderResponse | WorkerCallResponse | WorkerErrorResponse;
|
|
76
|
+
//# sourceMappingURL=worker.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.types.d.ts","sourceRoot":"","sources":["../../../src/canvas/worker.types.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,19 +3,68 @@ import { RootNode } from './canvas/root.canvas.util.js';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Worker thread entry point for off-main-thread canvas rendering.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
*
|
|
7
|
+
* Message protocol (main → worker):
|
|
8
|
+
* { type: 'render', taskId, props } — render and keep Canvas alive
|
|
9
|
+
* { type: 'call', taskId, canvasId, method, args } — call a method on a live Canvas
|
|
10
|
+
* { type: 'release', canvasId } — free Canvas from memory
|
|
11
|
+
*
|
|
12
|
+
* Responses (worker → main):
|
|
13
|
+
* WorkerRenderResponse — render complete (includes pre-encoded PNG buffer)
|
|
14
|
+
* WorkerCallResponse — method call result
|
|
15
|
+
* WorkerErrorResponse — any failure
|
|
8
16
|
*/
|
|
9
17
|
if (!parentPort) {
|
|
10
18
|
throw new Error('[render.worker] Must be run as a worker thread');
|
|
11
19
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
const canvases = new Map();
|
|
21
|
+
let nextCanvasId = 0;
|
|
22
|
+
function reply(msg) {
|
|
23
|
+
parentPort.postMessage(msg);
|
|
24
|
+
}
|
|
25
|
+
parentPort.on('message', async (msg) => {
|
|
26
|
+
if (msg.type === 'render') {
|
|
27
|
+
try {
|
|
28
|
+
const canvas = await new RootNode(msg.props).render();
|
|
29
|
+
const canvasId = nextCanvasId++;
|
|
30
|
+
canvases.set(canvasId, canvas);
|
|
31
|
+
reply({ taskId: msg.taskId, canvasId, buffer: canvas.toBufferSync('png'), width: canvas.width, height: canvas.height });
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
reply({ taskId: msg.taskId, error: String(err) });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (msg.type === 'call') {
|
|
38
|
+
const canvas = canvases.get(msg.canvasId);
|
|
39
|
+
if (!canvas) {
|
|
40
|
+
reply({ taskId: msg.taskId, error: `[render.worker] Canvas ${msg.canvasId} not found` });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
let result;
|
|
45
|
+
switch (msg.method) {
|
|
46
|
+
case 'toBuffer':
|
|
47
|
+
result = await canvas.toBuffer(...msg.args);
|
|
48
|
+
break;
|
|
49
|
+
case 'toURL':
|
|
50
|
+
result = await canvas.toURL(...msg.args);
|
|
51
|
+
break;
|
|
52
|
+
case 'toFile':
|
|
53
|
+
result = await canvas.toFile(...msg.args);
|
|
54
|
+
break;
|
|
55
|
+
case 'toSharp':
|
|
56
|
+
// Sharp instances can't be transferred across threads — serialize to buffer
|
|
57
|
+
result = await canvas.toSharp(...msg.args).toBuffer();
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
reply({ taskId: msg.taskId, result });
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
reply({ taskId: msg.taskId, error: String(err) });
|
|
64
|
+
}
|
|
17
65
|
}
|
|
18
|
-
|
|
19
|
-
|
|
66
|
+
else {
|
|
67
|
+
// type === 'release'
|
|
68
|
+
canvases.delete(msg.canvasId);
|
|
20
69
|
}
|
|
21
70
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meonode/canvas",
|
|
3
|
-
"version": "1.6.0
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "A declarative, component-based library for server-side canvas image generation. Write complex visuals with simple functions, similar to the composition style of @meonode/ui.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"canvas",
|