@meonode/canvas 2.0.2 → 2.0.4

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 (119) hide show
  1. package/dist/cjs/{src/canvas → canvas}/canvas.helper.d.ts +1 -20
  2. package/dist/cjs/canvas/canvas.helper.d.ts.map +1 -0
  3. package/dist/cjs/canvas/canvas.helper.js +0 -230
  4. package/dist/cjs/canvas/canvas.helper.js.map +1 -1
  5. package/dist/{esm/src → cjs}/canvas/canvas.type.d.ts +1 -12
  6. package/dist/cjs/canvas/canvas.type.d.ts.map +1 -0
  7. package/dist/cjs/{src/canvas → canvas}/chart.canvas.d.ts +1 -1
  8. package/dist/cjs/canvas/chart.canvas.d.ts.map +1 -0
  9. package/dist/cjs/canvas/chart.canvas.js +70 -144
  10. package/dist/cjs/canvas/chart.canvas.js.map +1 -1
  11. package/dist/cjs/canvas/grid.canvas.d.ts.map +1 -0
  12. package/dist/cjs/canvas/grid.canvas.js.map +1 -1
  13. package/dist/{esm/src → cjs}/canvas/image.canvas.d.ts +1 -1
  14. package/dist/cjs/canvas/image.canvas.d.ts.map +1 -0
  15. package/dist/cjs/canvas/image.canvas.js +2 -2
  16. package/dist/cjs/canvas/image.canvas.js.map +1 -1
  17. package/dist/{esm/src → cjs}/canvas/layout.canvas.d.ts +2 -2
  18. package/dist/cjs/canvas/layout.canvas.d.ts.map +1 -0
  19. package/dist/cjs/canvas/layout.canvas.js +6 -6
  20. package/dist/cjs/canvas/layout.canvas.js.map +1 -1
  21. package/dist/{esm/src → cjs}/canvas/root.canvas.d.ts +3 -2
  22. package/dist/cjs/canvas/root.canvas.d.ts.map +1 -0
  23. package/dist/cjs/canvas/root.canvas.js +23 -117
  24. package/dist/cjs/canvas/root.canvas.js.map +1 -1
  25. package/dist/cjs/{src/canvas → canvas}/text.canvas.d.ts +1 -1
  26. package/dist/cjs/canvas/text.canvas.d.ts.map +1 -0
  27. package/dist/cjs/canvas/text.canvas.js +2 -2
  28. package/dist/cjs/canvas/text.canvas.js.map +1 -1
  29. package/dist/cjs/constant/common.const.d.ts.map +1 -0
  30. package/dist/cjs/constant/common.const.js.map +1 -1
  31. package/dist/cjs/index.d.ts.map +1 -0
  32. package/dist/cjs/util/disk.cache.d.ts.map +1 -0
  33. package/dist/cjs/util/disk.cache.js.map +1 -1
  34. package/dist/cjs/worker/comlink.pool.d.ts +30 -0
  35. package/dist/cjs/worker/comlink.pool.d.ts.map +1 -0
  36. package/dist/cjs/worker/comlink.pool.js +164 -0
  37. package/dist/cjs/worker/comlink.pool.js.map +1 -0
  38. package/dist/cjs/worker/comlink.setup.d.ts +4 -0
  39. package/dist/cjs/worker/comlink.setup.d.ts.map +1 -0
  40. package/dist/cjs/worker/comlink.setup.js +53 -0
  41. package/dist/cjs/worker/comlink.setup.js.map +1 -0
  42. package/dist/cjs/worker/render.worker.d.ts.map +1 -0
  43. package/dist/cjs/worker/render.worker.js +58 -61
  44. package/dist/cjs/worker/render.worker.js.map +1 -1
  45. package/dist/cjs/worker/worker.types.d.ts +13 -0
  46. package/dist/cjs/worker/worker.types.d.ts.map +1 -0
  47. package/dist/esm/{src/canvas → canvas}/canvas.helper.d.ts +1 -20
  48. package/dist/esm/canvas/canvas.helper.d.ts.map +1 -0
  49. package/dist/esm/canvas/canvas.helper.js +1 -230
  50. package/dist/{cjs/src → esm}/canvas/canvas.type.d.ts +1 -12
  51. package/dist/esm/canvas/canvas.type.d.ts.map +1 -0
  52. package/dist/esm/{src/canvas → canvas}/chart.canvas.d.ts +1 -1
  53. package/dist/esm/canvas/chart.canvas.d.ts.map +1 -0
  54. package/dist/esm/canvas/chart.canvas.js +71 -145
  55. package/dist/esm/canvas/grid.canvas.d.ts.map +1 -0
  56. package/dist/{cjs/src → esm}/canvas/image.canvas.d.ts +1 -1
  57. package/dist/esm/canvas/image.canvas.d.ts.map +1 -0
  58. package/dist/esm/canvas/image.canvas.js +2 -2
  59. package/dist/{cjs/src → esm}/canvas/layout.canvas.d.ts +2 -2
  60. package/dist/esm/canvas/layout.canvas.d.ts.map +1 -0
  61. package/dist/esm/canvas/layout.canvas.js +6 -6
  62. package/dist/{cjs/src → esm}/canvas/root.canvas.d.ts +3 -2
  63. package/dist/esm/canvas/root.canvas.d.ts.map +1 -0
  64. package/dist/esm/canvas/root.canvas.js +23 -116
  65. package/dist/esm/{src/canvas → canvas}/text.canvas.d.ts +1 -1
  66. package/dist/esm/canvas/text.canvas.d.ts.map +1 -0
  67. package/dist/esm/canvas/text.canvas.js +2 -2
  68. package/dist/esm/constant/common.const.d.ts.map +1 -0
  69. package/dist/esm/index.d.ts.map +1 -0
  70. package/dist/esm/util/disk.cache.d.ts.map +1 -0
  71. package/dist/esm/worker/comlink.pool.d.ts +30 -0
  72. package/dist/esm/worker/comlink.pool.d.ts.map +1 -0
  73. package/dist/esm/worker/comlink.pool.js +139 -0
  74. package/dist/esm/worker/comlink.setup.d.ts +4 -0
  75. package/dist/esm/worker/comlink.setup.d.ts.map +1 -0
  76. package/dist/esm/worker/comlink.setup.js +30 -0
  77. package/dist/esm/worker/render.worker.d.ts.map +1 -0
  78. package/dist/esm/worker/render.worker.js +38 -60
  79. package/dist/esm/worker/worker.types.d.ts +13 -0
  80. package/dist/esm/worker/worker.types.d.ts.map +1 -0
  81. package/package.json +2 -1
  82. package/dist/cjs/src/canvas/canvas.helper.d.ts.map +0 -1
  83. package/dist/cjs/src/canvas/canvas.type.d.ts.map +0 -1
  84. package/dist/cjs/src/canvas/chart.canvas.d.ts.map +0 -1
  85. package/dist/cjs/src/canvas/grid.canvas.d.ts.map +0 -1
  86. package/dist/cjs/src/canvas/image.canvas.d.ts.map +0 -1
  87. package/dist/cjs/src/canvas/layout.canvas.d.ts.map +0 -1
  88. package/dist/cjs/src/canvas/root.canvas.d.ts.map +0 -1
  89. package/dist/cjs/src/canvas/text.canvas.d.ts.map +0 -1
  90. package/dist/cjs/src/constant/common.const.d.ts.map +0 -1
  91. package/dist/cjs/src/index.d.ts.map +0 -1
  92. package/dist/cjs/src/util/disk.cache.d.ts.map +0 -1
  93. package/dist/cjs/src/worker/render.worker.d.ts.map +0 -1
  94. package/dist/cjs/src/worker/worker.types.d.ts +0 -76
  95. package/dist/cjs/src/worker/worker.types.d.ts.map +0 -1
  96. package/dist/esm/src/canvas/canvas.helper.d.ts.map +0 -1
  97. package/dist/esm/src/canvas/canvas.type.d.ts.map +0 -1
  98. package/dist/esm/src/canvas/chart.canvas.d.ts.map +0 -1
  99. package/dist/esm/src/canvas/grid.canvas.d.ts.map +0 -1
  100. package/dist/esm/src/canvas/image.canvas.d.ts.map +0 -1
  101. package/dist/esm/src/canvas/layout.canvas.d.ts.map +0 -1
  102. package/dist/esm/src/canvas/root.canvas.d.ts.map +0 -1
  103. package/dist/esm/src/canvas/text.canvas.d.ts.map +0 -1
  104. package/dist/esm/src/constant/common.const.d.ts.map +0 -1
  105. package/dist/esm/src/index.d.ts.map +0 -1
  106. package/dist/esm/src/util/disk.cache.d.ts.map +0 -1
  107. package/dist/esm/src/worker/render.worker.d.ts.map +0 -1
  108. package/dist/esm/src/worker/worker.types.d.ts +0 -76
  109. package/dist/esm/src/worker/worker.types.d.ts.map +0 -1
  110. /package/dist/cjs/{src/canvas → canvas}/grid.canvas.d.ts +0 -0
  111. /package/dist/cjs/{src/constant → constant}/common.const.d.ts +0 -0
  112. /package/dist/cjs/{src/index.d.ts → index.d.ts} +0 -0
  113. /package/dist/cjs/{src/util → util}/disk.cache.d.ts +0 -0
  114. /package/dist/cjs/{src/worker → worker}/render.worker.d.ts +0 -0
  115. /package/dist/esm/{src/canvas → canvas}/grid.canvas.d.ts +0 -0
  116. /package/dist/esm/{src/constant → constant}/common.const.d.ts +0 -0
  117. /package/dist/esm/{src/index.d.ts → index.d.ts} +0 -0
  118. /package/dist/esm/{src/util → util}/disk.cache.d.ts +0 -0
  119. /package/dist/esm/{src/worker → worker}/render.worker.d.ts +0 -0
@@ -6,12 +6,9 @@ import { TextNode } from './text.canvas.js';
6
6
  import { ChartNode } from './chart.canvas.js';
7
7
  import { GridItemNode, GridNode } from './grid.canvas.js';
8
8
  import { Style } from '../constant/common.const.js';
9
- import { WorkerPreProcessor } from './canvas.helper.js';
10
9
  import * as path from 'node:path';
11
10
  import * as fs from 'node:fs';
12
11
  import { cpus } from 'node:os';
13
- import { Worker } from 'node:worker_threads';
14
- import { fileURLToPath } from 'node:url';
15
12
 
16
13
  /** Registry to track fonts that have already been loaded */
17
14
  const registeredFonts = new Map();
@@ -20,14 +17,8 @@ const registeredFonts = new Map();
20
17
  * This is a safety net — users should still call .release() explicitly.
21
18
  */
22
19
  const canvasRegistry = new FinalizationRegistry(heldValue => {
23
- // Best-effort cleanup — worker may already be terminated
24
20
  try {
25
- // Access workers via a public method or make it accessible
26
- // For now, just try to send the message and let errors be caught
27
- if (_workerPool) {
28
- ;
29
- _workerPool.workers?.[heldValue.workerIdx]?.postMessage({ type: 'release', canvasId: heldValue.canvasId });
30
- }
21
+ _workerPool?.releaseCanvas(heldValue.workerIdx, heldValue.canvasId);
31
22
  }
32
23
  catch {
33
24
  // Worker already gone — nothing to clean up
@@ -67,7 +58,7 @@ function terminate() {
67
58
  class WorkerCanvas {
68
59
  width;
69
60
  height;
70
- _buffer; // pre-encoded PNG for sync use
61
+ _buffer;
71
62
  _pool;
72
63
  _workerIdx;
73
64
  _canvasId;
@@ -78,12 +69,8 @@ class WorkerCanvas {
78
69
  this._pool = opts.pool;
79
70
  this._workerIdx = opts.workerIdx;
80
71
  this._canvasId = opts.canvasId;
81
- // Register for finalizer-based cleanup if user forgets to call .release()
82
72
  canvasRegistry.register(this, { workerIdx: opts.workerIdx, canvasId: opts.canvasId }, this);
83
73
  }
84
- _call(method, ...args) {
85
- return this._pool.callOnCanvas(this._workerIdx, this._canvasId, method, args);
86
- }
87
74
  // --- Sync methods: return from pre-encoded PNG buffer ---
88
75
  toBufferSync(_format, _options) {
89
76
  return this._buffer;
@@ -91,128 +78,47 @@ class WorkerCanvas {
91
78
  toURLSync(_format, _options) {
92
79
  return `data:image/png;base64,${this._buffer.toString('base64')}`;
93
80
  }
94
- // --- Async methods: delegate to worker ---
81
+ // --- Async methods: delegate to worker via Comlink ---
95
82
  toBuffer(format, options) {
96
- return this._call('toBuffer', format, options);
83
+ return this._pool.callOnCanvas(this._workerIdx, this._canvasId, 'toBuffer', [format, options]);
97
84
  }
98
85
  toURL(format, options) {
99
- return this._call('toURL', format, options);
86
+ return this._pool.callOnCanvas(this._workerIdx, this._canvasId, 'toURL', [format, options]);
100
87
  }
101
88
  toFile(filename, options) {
102
- return this._call('toFile', filename, options);
89
+ return this._pool.callOnCanvas(this._workerIdx, this._canvasId, 'toFile', [filename, options]);
103
90
  }
104
- /** Returns a Buffer (Sharp instance cannot be transferred across threads) */
105
91
  toSharp(options) {
106
- return this._call('toSharp', options);
92
+ return this._pool.callOnCanvas(this._workerIdx, this._canvasId, 'toSharp', [options]);
107
93
  }
108
94
  toSharpSync(_options) {
109
95
  throw new Error('[canvas] toSharpSync() is not available in worker mode — use toSharp() instead');
110
96
  }
111
97
  // --- Async convenience getters ---
112
98
  get png() {
113
- return this._call('toBuffer', 'png');
99
+ return this.toBuffer('png');
114
100
  }
115
101
  get webp() {
116
- return this._call('toBuffer', 'webp');
102
+ return this.toBuffer('webp');
117
103
  }
118
104
  get jpg() {
119
- return this._call('toBuffer', 'jpg');
105
+ return this.toBuffer('jpg');
120
106
  }
121
107
  get svg() {
122
- return this._call('toBuffer', 'svg');
108
+ return this.toBuffer('svg');
123
109
  }
124
110
  get pdf() {
125
- return this._call('toBuffer', 'pdf');
111
+ return this.toBuffer('pdf');
126
112
  }
127
113
  get raw() {
128
- return this._call('toBuffer', 'raw');
114
+ return this.toBuffer('raw');
129
115
  }
130
116
  /** Release the Canvas from worker memory. Call when done with this object. */
131
117
  release() {
132
118
  this._pool.releaseCanvas(this._workerIdx, this._canvasId);
133
- // Unregister from finalizer since we're explicitly cleaning up
134
119
  canvasRegistry.unregister(this);
135
120
  }
136
121
  }
137
- /** Worker thread pool — routes render and canvas-call messages */
138
- class WorkerPool {
139
- workers = [];
140
- idle = [];
141
- queue = [];
142
- pending = new Map();
143
- nextId = 0;
144
- constructor(size) {
145
- this.init(size);
146
- }
147
- init(size) {
148
- const workerFile = path.join(path.dirname(fileURLToPath(import.meta.url)), '../worker/render.worker.js');
149
- for (let i = 0; i < size; i++) {
150
- const workerIdx = i;
151
- const worker = new Worker(workerFile);
152
- worker.on('message', (msg) => {
153
- const task = this.pending.get(msg.taskId);
154
- if (!task)
155
- return;
156
- this.pending.delete(msg.taskId);
157
- if ('error' in msg) {
158
- task.reject(new Error(msg.error));
159
- return;
160
- }
161
- if ('canvasId' in msg) {
162
- // Render complete — put worker back to idle
163
- this.idle.push(worker);
164
- this.drain();
165
- const result = { buffer: msg.buffer, canvasId: msg.canvasId, workerIdx, width: msg.width, height: msg.height };
166
- task.resolve(result);
167
- }
168
- else {
169
- // Canvas method call complete
170
- task.resolve(msg.result);
171
- }
172
- });
173
- this.workers.push(worker);
174
- this.idle.push(worker);
175
- }
176
- }
177
- drain() {
178
- while (this.queue.length > 0 && this.idle.length > 0) {
179
- const task = this.queue.shift();
180
- const worker = this.idle.pop();
181
- const request = { type: 'render', taskId: task.id, props: task.props };
182
- worker.postMessage(request);
183
- }
184
- }
185
- render(props) {
186
- const sanitizedProps = WorkerPreProcessor.process(props);
187
- return new Promise((resolve, reject) => {
188
- const id = this.nextId++;
189
- this.pending.set(id, { resolve: resolve, reject });
190
- if (this.idle.length > 0) {
191
- const worker = this.idle.pop();
192
- const request = { type: 'render', taskId: id, props: sanitizedProps };
193
- worker.postMessage(request);
194
- }
195
- else {
196
- this.queue.push({ id, props: sanitizedProps });
197
- }
198
- });
199
- }
200
- callOnCanvas(workerIdx, canvasId, method, args) {
201
- return new Promise((resolve, reject) => {
202
- const id = this.nextId++;
203
- this.pending.set(id, { resolve: resolve, reject });
204
- const request = { type: 'call', taskId: id, canvasId, method, args };
205
- this.workers[workerIdx].postMessage(request);
206
- });
207
- }
208
- releaseCanvas(workerIdx, canvasId) {
209
- const request = { type: 'release', canvasId };
210
- this.workers[workerIdx]?.postMessage(request);
211
- }
212
- terminate() {
213
- this.workers.forEach(w => w.terminate());
214
- }
215
- }
216
122
  /**
217
123
  * Converts a CanvasElement tree into actual BoxNode instances.
218
124
  * Used both for non-worker rendering (inline tree building) and inside
@@ -316,12 +222,12 @@ class RootNode extends ColumnNode {
316
222
  }
317
223
  return imageNodes;
318
224
  }
319
- /**
320
- * Renders the entire node tree to a canvas, handling image loading, layout calculation,
321
- * and final drawing
322
- * @returns Promise resolving to the rendered Canvas instance
323
- */
324
- async render() {
225
+ async render(ctx, offsetX = 0, offsetY = 0) {
226
+ // If ctx is provided, delegate to parent render (used when called as a child node)
227
+ if (ctx) {
228
+ await super.render(ctx, offsetX, offsetY);
229
+ return;
230
+ }
325
231
  const diskCacheKeys = this.props.useDiskCache ? new Set() : undefined;
326
232
  try {
327
233
  // Step 1: Load all images with a concurrency limit to avoid overwhelming remote sources.
@@ -355,7 +261,7 @@ class RootNode extends ColumnNode {
355
261
  this.ctx = this.canvas.getContext('2d');
356
262
  this.ctx.scale(this.scale, this.scale);
357
263
  // Step 6: Render content
358
- super.render(this.ctx, 0, 0);
264
+ await super.render(this.ctx, 0, 0);
359
265
  if (!this.canvas) {
360
266
  throw new Error('Canvas not initialized');
361
267
  }
@@ -373,9 +279,10 @@ async function Root(props) {
373
279
  const workerMode = props.workerMode ?? _defaultWorkerMode;
374
280
  const workerPoolSize = props.workers ?? _defaultWorkerPoolSize;
375
281
  if (workerMode) {
376
- // Lazy initialize worker pool
282
+ // Lazy initialize worker pool — dynamic import to avoid loading Comlink in non-worker contexts
377
283
  if (!_workerPool) {
378
- _workerPool = new WorkerPool(workerPoolSize);
284
+ const { ComlinkPool } = await import('../worker/comlink.pool.js');
285
+ _workerPool = new ComlinkPool(workerPoolSize);
379
286
  }
380
287
  const result = await _workerPool.render(props);
381
288
  return new WorkerCanvas({ ...result, pool: _workerPool });
@@ -156,7 +156,7 @@ export declare class TextNode extends BoxNode {
156
156
  * @param width Content box total width including padding
157
157
  * @param height Content box total height including padding
158
158
  */
159
- protected _renderContent(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number): void;
159
+ protected _renderContent(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number): Promise<void>;
160
160
  }
161
161
  /**
162
162
  * Creates a new TextNode instance with rich text support
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.canvas.d.ts","sourceRoot":"","sources":["../../../src/canvas/text.canvas.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAe,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACpF,OAAO,EAAU,KAAK,wBAAwB,EAA2B,MAAM,aAAa,CAAA;AAC5F,OAAO,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAGnD;;;GAGG;AACH,qBAAa,QAAS,SAAQ,OAAO;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoB;IAC7C,OAAO,CAAC,KAAK,CAAsB;IACnC,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAwC;IACzE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAU;IACxC,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,kBAAkB,CAAe;IAEjC,KAAK,EAAE,SAAS,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;gBAElC,IAAI,GAAE,MAAM,GAAG,MAAW,EAAE,KAAK,GAAE,SAAc;IAuB7D;;;;;;;;OAQG;WACW,gBAAgB,CAC5B,GAAG,EAAE,wBAAwB,EAC7B,IAAI,EAAE,MAAM,EACZ,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,KAAK,GAAE;QACL,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,SAAS,CAAC,YAAY,CAAC,CAAA;QACpC,SAAS,CAAC,EAAE,SAAS,CAAC,WAAW,CAAC,CAAA;QAClC,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,wBAAwB,CAAC,WAAW,CAAC,CAAA;QACjD,YAAY,CAAC,EAAE,wBAAwB,CAAC,cAAc,CAAC,CAAA;KACnD;cAwBW,aAAa,IAAI,IAAI;IAoDxC;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,sBAAsB;IA8B9B;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,aAAa;IA+ErB,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,gBAAgB;IAyBxB;;;;OAIG;IACH,OAAO,CAAC,qBAAqB;IAM7B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,aAAa;IAiCrB;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAQjC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,WAAW;IA+NnB;;;;;;;;;OASG;IACH,OAAO,CAAC,YAAY;IA6KpB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;IAmErB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAQzB;;;;;;;;;;;;;;;;OAgBG;cACsB,cAAc,CAAC,GAAG,EAAE,wBAAwB,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAkX3H;AAED;;GAEG;AACH,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,GAAG,MAAM,EAAE,QAAQ,SAAS,KAAG,aAI9D,CAAA"}
@@ -816,8 +816,8 @@ class TextNode extends BoxNode {
816
816
  * @param width Content box total width including padding
817
817
  * @param height Content box total height including padding
818
818
  */
819
- _renderContent(ctx, x, y, width, height) {
820
- super._renderContent(ctx, x, y, width, height);
819
+ async _renderContent(ctx, x, y, width, height) {
820
+ await super._renderContent(ctx, x, y, width, height);
821
821
  ctx.save();
822
822
  ctx.textBaseline = 'alphabetic';
823
823
  ctx.letterSpacing = this.formatSpacing(this.props.letterSpacing);
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.const.d.ts","sourceRoot":"","sources":["../../../src/constant/common.const.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,EAAE,KAAK,GAAG,MAAM,aAAa,CAAA;AAExC;;GAEG;AACH,oBAAY,MAAM;IAChB,KAAK,IAAA;IACL,MAAM,IAAA;IACN,MAAM,IAAA;CACP;AAED;;GAEG;AACH,eAAO,MAAM,KAAK,EAAE,OAAO,GAAG,GAAG;IAAE,MAAM,EAAE,OAAO,MAAM,CAAA;CAGvD,CAAA;AAED,cAAc,aAAa,CAAA;AAC3B,eAAe,IAAI,CAAA"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAA;AAC1C,cAAc,yBAAyB,CAAA;AACvC,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAC1E,OAAO,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAA;AAC9C,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AACpE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAClD,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAA;AAC9C,OAAO,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disk.cache.d.ts","sourceRoot":"","sources":["../../../src/util/disk.cache.ts"],"names":[],"mappings":"AAaA,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAOvE;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO7E;AAED,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMhE;AAED;;;GAGG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAOpD"}
@@ -0,0 +1,30 @@
1
+ import type { RenderResult } from '../worker/worker.types.js';
2
+ import type { RootProps } from '../canvas/canvas.type.js';
3
+ export interface PoolRenderResult extends RenderResult {
4
+ workerIdx: number;
5
+ }
6
+ /**
7
+ * Deeply walks an object tree, wraps any function values with Comlink.proxy(),
8
+ * and tracks wrapped proxies for deterministic cleanup.
9
+ */
10
+ export declare function wrapFunctions<T>(obj: T, proxies: Set<unknown>): T;
11
+ /**
12
+ * Pool of Comlink-wrapped worker threads.
13
+ * Manages idle/queue scheduling and proxy lifecycle.
14
+ */
15
+ export declare class ComlinkPool {
16
+ private workers;
17
+ private endpoints;
18
+ private idle;
19
+ private queue;
20
+ constructor(size: number);
21
+ private acquire;
22
+ private release;
23
+ private drain;
24
+ private executeRender;
25
+ render(props: RootProps): Promise<PoolRenderResult>;
26
+ callOnCanvas(workerIdx: number, canvasId: number, method: string, args: unknown[]): Promise<Buffer | string | void>;
27
+ releaseCanvas(workerIdx: number, canvasId: number): void;
28
+ terminate(): void;
29
+ }
30
+ //# sourceMappingURL=comlink.pool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comlink.pool.d.ts","sourceRoot":"","sources":["../../../src/worker/comlink.pool.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAa,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAExD,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IACpD,SAAS,EAAE,MAAM,CAAA;CAClB;AAQD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAuBjE;AAED;;;GAGG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,IAAI,CAAe;IAC3B,OAAO,CAAC,KAAK,CAAmB;gBAEpB,IAAI,EAAE,MAAM;IAYxB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,KAAK;YAQC,aAAa;IAWrB,MAAM,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA0CzD,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAInH,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAIxD,SAAS;CAOV"}
@@ -0,0 +1,139 @@
1
+ import { Worker } from 'node:worker_threads';
2
+ import { fileURLToPath } from 'node:url';
3
+ import * as path from 'node:path';
4
+ import './comlink.setup.js';
5
+ import * as Comlink from 'comlink';
6
+ import nodeEndpoint from 'comlink/dist/esm/node-adapter.mjs';
7
+
8
+ /**
9
+ * Deeply walks an object tree, wraps any function values with Comlink.proxy(),
10
+ * and tracks wrapped proxies for deterministic cleanup.
11
+ */
12
+ function wrapFunctions(obj, proxies) {
13
+ if (obj === null || obj === undefined)
14
+ return obj;
15
+ if (typeof obj === 'function') {
16
+ const wrapped = Comlink.proxy(obj);
17
+ proxies.add(wrapped);
18
+ return wrapped;
19
+ }
20
+ if (typeof obj !== 'object')
21
+ return obj;
22
+ // Preserve binary data types — don't walk into them
23
+ if (Buffer.isBuffer(obj))
24
+ return obj;
25
+ if (obj instanceof ArrayBuffer)
26
+ return obj;
27
+ if (ArrayBuffer.isView(obj))
28
+ return obj;
29
+ if (Array.isArray(obj)) {
30
+ return obj.map(item => wrapFunctions(item, proxies));
31
+ }
32
+ const result = {};
33
+ for (const key of Object.keys(obj)) {
34
+ result[key] = wrapFunctions(obj[key], proxies);
35
+ }
36
+ return result;
37
+ }
38
+ /**
39
+ * Pool of Comlink-wrapped worker threads.
40
+ * Manages idle/queue scheduling and proxy lifecycle.
41
+ */
42
+ class ComlinkPool {
43
+ workers = [];
44
+ endpoints = [];
45
+ idle = [];
46
+ queue = [];
47
+ constructor(size) {
48
+ const workerFile = path.join(path.dirname(fileURLToPath(import.meta.url)), '../worker/render.worker.js');
49
+ for (let i = 0; i < size; i++) {
50
+ const worker = new Worker(workerFile);
51
+ const endpoint = Comlink.wrap(nodeEndpoint(worker));
52
+ this.workers.push(worker);
53
+ this.endpoints.push(endpoint);
54
+ this.idle.push(i);
55
+ }
56
+ }
57
+ acquire() {
58
+ return this.idle.pop() ?? null;
59
+ }
60
+ release(idx) {
61
+ this.idle.push(idx);
62
+ this.drain();
63
+ }
64
+ drain() {
65
+ while (this.queue.length > 0 && this.idle.length > 0) {
66
+ const task = this.queue.shift();
67
+ const idx = this.idle.pop();
68
+ void this.executeRender(idx, task.props, task.resolve, task.reject);
69
+ }
70
+ }
71
+ async executeRender(idx, props, resolve, reject) {
72
+ try {
73
+ const result = await this.endpoints[idx].render(props);
74
+ resolve({ ...result, workerIdx: idx });
75
+ }
76
+ catch (err) {
77
+ reject(err instanceof Error ? err : new Error(String(err)));
78
+ }
79
+ finally {
80
+ this.release(idx);
81
+ }
82
+ }
83
+ async render(props) {
84
+ const proxies = new Set();
85
+ const wrapped = wrapFunctions(props, proxies);
86
+ const cleanup = () => {
87
+ for (const p of proxies) {
88
+ try {
89
+ ;
90
+ p[Comlink.releaseProxy]?.();
91
+ }
92
+ catch {
93
+ // Proxy may already be released
94
+ }
95
+ }
96
+ };
97
+ // Direct path — idle worker available
98
+ const idx = this.acquire();
99
+ if (idx !== null) {
100
+ try {
101
+ const result = await this.endpoints[idx].render(wrapped);
102
+ return { ...result, workerIdx: idx };
103
+ }
104
+ finally {
105
+ this.release(idx);
106
+ cleanup();
107
+ }
108
+ }
109
+ // Queued path — cleanup AFTER the queued task completes, not before
110
+ return new Promise((resolve, reject) => {
111
+ this.queue.push({
112
+ props: wrapped,
113
+ resolve: result => {
114
+ cleanup();
115
+ resolve(result);
116
+ },
117
+ reject: err => {
118
+ cleanup();
119
+ reject(err);
120
+ },
121
+ });
122
+ });
123
+ }
124
+ callOnCanvas(workerIdx, canvasId, method, args) {
125
+ return this.endpoints[workerIdx].callOnCanvas(canvasId, method, args);
126
+ }
127
+ releaseCanvas(workerIdx, canvasId) {
128
+ this.endpoints[workerIdx].releaseCanvas(canvasId);
129
+ }
130
+ terminate() {
131
+ this.workers.forEach(w => w.terminate());
132
+ this.workers = [];
133
+ this.endpoints = [];
134
+ this.idle = [];
135
+ this.queue = [];
136
+ }
137
+ }
138
+
139
+ export { ComlinkPool, wrapFunctions };
@@ -0,0 +1,4 @@
1
+ import * as Comlink from 'comlink';
2
+ import nodeEndpoint from 'comlink/dist/esm/node-adapter.mjs';
3
+ export { Comlink, nodeEndpoint };
4
+ //# sourceMappingURL=comlink.setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comlink.setup.d.ts","sourceRoot":"","sources":["../../../src/worker/comlink.setup.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,OAAO,MAAM,SAAS,CAAA;AAKlC,OAAO,YAAY,MAAM,mCAAmC,CAAA;AA2B5D,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,CAAA"}
@@ -0,0 +1,30 @@
1
+ import * as Comlink from 'comlink';
2
+ export { Comlink };
3
+ import { MessageChannel } from 'node:worker_threads';
4
+ import nodeEndpoint from 'comlink/dist/esm/node-adapter.mjs';
5
+ export { default as nodeEndpoint } from 'comlink/dist/esm/node-adapter.mjs';
6
+
7
+ // src/worker/comlink.setup.ts
8
+ /**
9
+ * Fix Comlink.proxy() for Node.js worker_threads (issue #313).
10
+ * The built-in proxy transfer handler uses browser MessageChannel.
11
+ * This override uses Node's MessageChannel instead.
12
+ *
13
+ * Must be called on BOTH main thread and worker before any Comlink usage.
14
+ */
15
+ function installNodeProxyHandler() {
16
+ Comlink.transferHandlers.set('proxy', {
17
+ canHandle: (obj) => typeof obj === 'object' && obj !== null && Comlink.proxyMarker in obj,
18
+ serialize: (obj) => {
19
+ const { port1, port2 } = new MessageChannel();
20
+ Comlink.expose(obj, nodeEndpoint(port1));
21
+ return [port2, [port2]];
22
+ },
23
+ deserialize: (port) => {
24
+ port.start?.();
25
+ return Comlink.wrap(nodeEndpoint(port));
26
+ },
27
+ });
28
+ }
29
+ // Install immediately on import
30
+ installNodeProxyHandler();
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.worker.d.ts","sourceRoot":"","sources":["../../../src/worker/render.worker.ts"],"names":[],"mappings":""}
@@ -1,70 +1,48 @@
1
- import { parentPort } from 'worker_threads';
1
+ import { parentPort } from 'node:worker_threads';
2
+ import './comlink.setup.js';
2
3
  import { RootNode } from '../canvas/root.canvas.js';
4
+ import * as Comlink from 'comlink';
5
+ import nodeEndpoint from 'comlink/dist/esm/node-adapter.mjs';
3
6
 
4
- /**
5
- * Worker thread entry point for off-main-thread canvas rendering.
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
16
- */
17
7
  if (!parentPort) {
18
8
  throw new Error('[render.worker] Must be run as a worker thread');
19
9
  }
20
10
  const canvases = new Map();
21
11
  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);
12
+ const api = {
13
+ async render(props) {
14
+ const canvas = await new RootNode(props).render();
15
+ const canvasId = nextCanvasId++;
16
+ canvases.set(canvasId, canvas);
17
+ const result = {
18
+ canvasId,
19
+ buffer: canvas.toBufferSync('png'),
20
+ width: canvas.width,
21
+ height: canvas.height,
22
+ };
23
+ return result;
24
+ },
25
+ async callOnCanvas(canvasId, method, args) {
26
+ const canvas = canvases.get(canvasId);
39
27
  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 });
28
+ throw new Error(`[render.worker] Canvas ${canvasId} not found`);
61
29
  }
62
- catch (err) {
63
- reply({ taskId: msg.taskId, error: String(err) });
30
+ switch (method) {
31
+ case 'toBuffer':
32
+ return canvas.toBuffer(...args);
33
+ case 'toURL':
34
+ return canvas.toURL(...args);
35
+ case 'toFile':
36
+ await canvas.toFile(...args);
37
+ return;
38
+ case 'toSharp':
39
+ return await canvas.toSharp(...args).toBuffer();
40
+ default:
41
+ throw new Error(`[render.worker] Unknown method: ${method}`);
64
42
  }
65
- }
66
- else {
67
- // type === 'release'
68
- canvases.delete(msg.canvasId);
69
- }
70
- });
43
+ },
44
+ releaseCanvas(canvasId) {
45
+ canvases.delete(canvasId);
46
+ },
47
+ };
48
+ Comlink.expose(api, nodeEndpoint(parentPort));
@@ -0,0 +1,13 @@
1
+ import type { RootProps } from '../canvas/canvas.type.js';
2
+ export interface RenderResult {
3
+ canvasId: number;
4
+ buffer: Buffer;
5
+ width: number;
6
+ height: number;
7
+ }
8
+ export interface WorkerAPI {
9
+ render(props: RootProps): Promise<RenderResult>;
10
+ callOnCanvas(canvasId: number, method: string, args: unknown[]): Promise<Buffer | string | void>;
11
+ releaseCanvas(canvasId: number): void;
12
+ }
13
+ //# sourceMappingURL=worker.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.types.d.ts","sourceRoot":"","sources":["../../../src/worker/worker.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAExD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IAC/C,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC,CAAA;IAChG,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACtC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meonode/canvas",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
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",
@@ -81,6 +81,7 @@
81
81
  },
82
82
  "packageManager": "yarn@4.11.0",
83
83
  "dependencies": {
84
+ "comlink": "^4.4.2",
84
85
  "file-type": "^22.0.0",
85
86
  "lodash-es": "^4.17.23",
86
87
  "sharp": "^0.34.5",
@@ -1 +0,0 @@
1
- {"version":3,"file":"canvas.helper.d.ts","sourceRoot":"","sources":["../../../../src/canvas/canvas.helper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA;AAC3D,OAAO,KAAK,SAAS,MAAM,aAAa,CAAA;AAExC,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAiF,MAAM,yBAAyB,CAAA;AAEjJ,eAAO,MAAM,WAAW,GAAI,sEAUzB;IACD,GAAG,EAAE,wBAAwB,CAAA;IAC7B,IAAI,EAAE,SAAS,CAAC,IAAI,CAAA;IACpB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE;QACL,OAAO,EAAE,MAAM,CAAA;QACf,QAAQ,EAAE,MAAM,CAAA;QAChB,UAAU,EAAE,MAAM,CAAA;QAClB,WAAW,EAAE,MAAM,CAAA;KACpB,CAAA;IACD,WAAW,EAAE,QAAQ,CAAC,aAAa,CAAC,CAAA;IACpC,WAAW,EAAE,QAAQ,CAAC,aAAa,CAAC,CAAA;CACrC,SA8IA,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,GAC9B,KAAK,wBAAwB,EAC7B,GAAG,MAAM,EACT,GAAG,MAAM,EACT,OAAO,MAAM,EACb,QAAQ,MAAM,EACd,OAAO;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,SAoCtF,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAC5B,YAAY,QAAQ,CAAC,cAAc,CAAC,KACnC;IACD,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;CAYnB,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAQxF;AAED;;;;GAIG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAgF;IAEpH,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS;IAc3C,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAYhC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAuHrC;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,gBAAgB;IA6C/B,OAAO,CAAC,MAAM,CAAC,aAAa;IAI5B,MAAM,CAAC,oBAAoB,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC;CAuB1C"}