@napi-rs/canvas 1.0.10 → 1.0.12

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/index.js CHANGED
@@ -5,9 +5,11 @@ const {
5
5
  clearAllCache,
6
6
  CanvasRenderingContext2D,
7
7
  CanvasElement,
8
+ CanvasGradient,
9
+ CanvasPattern,
8
10
  SVGCanvas,
9
11
  Path: Path2D,
10
- ImageData,
12
+ ImageData: NativeImageData,
11
13
  Image,
12
14
  FontKey,
13
15
  GlobalFonts,
@@ -20,6 +22,10 @@ const {
20
22
  GifEncoder,
21
23
  GifDisposal,
22
24
  LottieAnimation,
25
+ TextMetrics,
26
+ _argumentCounts,
27
+ _imageDataConstructor,
28
+ _toNumber: toNumber,
23
29
  } = require('./js-binding')
24
30
 
25
31
  const { DOMPoint, DOMMatrix, DOMRect } = require('./geometry')
@@ -33,6 +39,45 @@ if (GifEncoder && typeof Symbol.dispose !== 'undefined') {
33
39
  }
34
40
  }
35
41
 
42
+ // napi-rs defines a class's accessors in name order. Chrome's TextMetrics
43
+ // prototype has them in IDL order (text_metrics.idl), which for...in shows,
44
+ // then the constructor: define them again in that order.
45
+ for (const name of [
46
+ 'width',
47
+ 'actualBoundingBoxLeft',
48
+ 'actualBoundingBoxRight',
49
+ 'fontBoundingBoxAscent',
50
+ 'fontBoundingBoxDescent',
51
+ 'actualBoundingBoxAscent',
52
+ 'actualBoundingBoxDescent',
53
+ 'hangingBaseline',
54
+ 'alphabeticBaseline',
55
+ 'ideographicBaseline',
56
+ 'emHeightAscent',
57
+ 'emHeightDescent',
58
+ 'constructor',
59
+ ]) {
60
+ const descriptor = Object.getOwnPropertyDescriptor(TextMetrics.prototype, name)
61
+ delete TextMetrics.prototype[name]
62
+ Object.defineProperty(TextMetrics.prototype, name, descriptor)
63
+ }
64
+
65
+ // Chrome's Symbol.toStringTag, an own property of each prototype. The
66
+ // canvas and Image classes take their HTML element names; SVGCanvas has no
67
+ // counterpart and no tag.
68
+ for (const [Class, tag] of [
69
+ [CanvasRenderingContext2D, 'CanvasRenderingContext2D'],
70
+ [CanvasGradient, 'CanvasGradient'],
71
+ [CanvasPattern, 'CanvasPattern'],
72
+ [Path2D, 'Path2D'],
73
+ [NativeImageData, 'ImageData'],
74
+ [TextMetrics, 'TextMetrics'],
75
+ [CanvasElement, 'HTMLCanvasElement'],
76
+ [Image, 'HTMLImageElement'],
77
+ ]) {
78
+ Object.defineProperty(Class.prototype, Symbol.toStringTag, { value: tag, configurable: true })
79
+ }
80
+
36
81
  const SvgExportFlag = {
37
82
  ConvertTextToPaths: 0x01,
38
83
  NoPrettyXML: 0x02,
@@ -60,23 +105,44 @@ if (!('has' in GlobalFonts)) {
60
105
 
61
106
  const _toBlob = CanvasElement.prototype.toBlob
62
107
  const _convertToBlob = CanvasElement.prototype.convertToBlob
108
+ // A native method of CanvasElement for the receiver check: with no arguments
109
+ // it has no side effects.
110
+ const canvasBrand = CanvasElement.prototype.getContext
63
111
  if ('Blob' in globalThis) {
64
- CanvasElement.prototype.toBlob = function toBlob(callback, mimeType, quality) {
65
- _toBlob.call(
66
- this,
67
- function (/** @type {Uint8Array} */ imageBuffer) {
68
- const blob = new Blob([imageBuffer.buffer], { type: mimeType })
69
- callback(blob)
112
+ // toBlob(callback, type, quality), as Chrome: the receiver, the argument
113
+ // count and the callback are checked first, then the native method converts
114
+ // the type and encodes. The callback gets the Blob, or null without pixels
115
+ // or when the encoder fails, from a task. A throw in it is an uncaught
116
+ // exception, as in Chrome (window.onerror).
117
+ CanvasElement.prototype.toBlob = function toBlob(callback) {
118
+ if (arguments.length < 1) {
119
+ argumentCountError(canvasBrand, this, 'toBlob', 'HTMLCanvasElement', 1, 0)
120
+ }
121
+ if (typeof callback !== 'function') {
122
+ bindingTypeError(canvasBrand, this, 'toBlob', 'HTMLCanvasElement', "parameter 1 is not of type 'Function'.")
123
+ }
124
+ // Only the arguments that were passed: past arguments.length,
125
+ // arguments[i] reads Object.prototype[i], which Chrome never reads.
126
+ const type = arguments.length > 1 ? arguments[1] : undefined
127
+ const quality = arguments.length > 2 ? arguments[2] : undefined
128
+ _toBlob.call(this, type, quality).then(
129
+ (result) => {
130
+ const blob = result === null ? null : new Blob([result.data], { type: result.type })
131
+ queueMicrotask(() => callback(blob))
70
132
  },
71
- mimeType,
72
- quality,
133
+ () => queueMicrotask(() => callback(null)),
73
134
  )
74
135
  }
75
- CanvasElement.prototype.convertToBlob = function convertToBlob(options) {
76
- return _convertToBlob.call(this, options).then((/** @type {Uint8Array} */ imageBuffer) => {
77
- const blob = new Blob([imageBuffer.buffer], { type: options?.mime || 'image/png' })
78
- return blob
79
- })
136
+ // convertToBlob(options): Chrome's OffscreenCanvas method. An options error
137
+ // rejects the promise, as a WebIDL conversion error of a promise method.
138
+ CanvasElement.prototype.convertToBlob = function convertToBlob() {
139
+ let pending
140
+ try {
141
+ pending = _convertToBlob.call(this, arguments.length > 0 ? arguments[0] : undefined)
142
+ } catch (error) {
143
+ return Promise.reject(error)
144
+ }
145
+ return pending.then((result) => new Blob([result.data], { type: result.type }))
80
146
  }
81
147
  } else {
82
148
  // oxlint-disable-next-line no-unused-vars
@@ -101,12 +167,466 @@ CanvasRenderingContext2D.prototype.getTransform = function getTransform() {
101
167
  return new DOMMatrix([a, b, c, d, e, f])
102
168
  }
103
169
 
170
+ // Chrome checks the receiver ('Illegal invocation'), then the argument count,
171
+ // before it converts any argument. `brand` is a native method of the class: a
172
+ // call on another receiver throws 'Illegal invocation'. As Blink's binding
173
+ // errors, the stack keeps the bare message and `message` gets the prefix, as
174
+ // an enumerable own property.
175
+ function argumentCountError(brand, self, method, iface, required, present) {
176
+ brand.call(self)
177
+ const message = `${required} argument${required === 1 ? '' : 's'} required, but only ${present} present.`
178
+ const error = new TypeError(message)
179
+ void error.stack
180
+ Object.defineProperty(error, 'message', {
181
+ value: `Failed to execute '${method}' on '${iface}': ${message}`,
182
+ writable: true,
183
+ enumerable: true,
184
+ configurable: true,
185
+ })
186
+ throw error
187
+ }
188
+
189
+ // A TypeError as Blink's bindings throw it after the receiver check, with the
190
+ // message formatted as argumentCountError does.
191
+ function bindingTypeError(brand, self, method, iface, message) {
192
+ brand.call(self)
193
+ const error = new TypeError(message)
194
+ void error.stack
195
+ Object.defineProperty(error, 'message', {
196
+ value: `Failed to execute '${method}' on '${iface}': ${message}`,
197
+ writable: true,
198
+ enumerable: true,
199
+ configurable: true,
200
+ })
201
+ throw error
202
+ }
203
+
204
+ // Wraps `proto[method]` with Chrome's argument count check; the wrapper's
205
+ // `length` is the required argument count, as Chrome's Function.length.
206
+ // napi pads missing arguments with undefined, so the native method cannot
207
+ // tell f(1) from f(1, undefined); an explicit undefined counts in Chrome.
208
+ function requireArguments(proto, brand, iface, method, required) {
209
+ const native = proto[method]
210
+ const wrapper = {
211
+ [method]() {
212
+ if (arguments.length < required) {
213
+ argumentCountError(brand, this, method, iface, required, arguments.length)
214
+ }
215
+ return native.apply(this, arguments)
216
+ },
217
+ }[method]
218
+ Object.defineProperty(wrapper, 'length', { value: required })
219
+ proto[method] = wrapper
220
+ }
221
+
222
+ const ctxBrand = CanvasRenderingContext2D.prototype.getContextAttributes
223
+ for (const [method, required] of [
224
+ ['arc', 5],
225
+ ['arcTo', 5],
226
+ ['bezierCurveTo', 6],
227
+ ['clearRect', 4],
228
+ ['createConicGradient', 3],
229
+ ['createLinearGradient', 4],
230
+ ['createRadialGradient', 6],
231
+ ['ellipse', 7],
232
+ ['fillRect', 4],
233
+ ['fillText', 3],
234
+ ['getImageData', 4],
235
+ ['lineTo', 2],
236
+ ['measureText', 1],
237
+ ['moveTo', 2],
238
+ ['quadraticCurveTo', 4],
239
+ ['rect', 4],
240
+ ['rotate', 1],
241
+ ['roundRect', 4],
242
+ ['scale', 2],
243
+ ['setLineDash', 1],
244
+ ['strokeRect', 4],
245
+ ['strokeText', 3],
246
+ ['transform', 6],
247
+ ['translate', 2],
248
+ ]) {
249
+ requireArguments(CanvasRenderingContext2D.prototype, ctxBrand, 'CanvasRenderingContext2D', method, required)
250
+ }
251
+
252
+ // getContext(contextId, options): Chrome needs the id. The native method is
253
+ // its own brand: with no arguments it converts undefined to 'undefined' and
254
+ // returns null, without side effects.
255
+ for (const [Class, iface] of [
256
+ [CanvasElement, 'HTMLCanvasElement'],
257
+ [SVGCanvas, 'SVGCanvas'],
258
+ ]) {
259
+ requireArguments(Class.prototype, Class.prototype.getContext, iface, 'getContext', 1)
260
+ }
261
+
262
+ const pathBrand = Path2D.prototype.getFillType
263
+ for (const [method, required] of [
264
+ ['arc', 5],
265
+ ['arcTo', 5],
266
+ ['bezierCurveTo', 6],
267
+ ['ellipse', 7],
268
+ ['lineTo', 2],
269
+ ['moveTo', 2],
270
+ ['quadraticCurveTo', 4],
271
+ ['rect', 4],
272
+ ['roundRect', 4],
273
+ ]) {
274
+ requireArguments(Path2D.prototype, pathBrand, 'Path2D', method, required)
275
+ }
276
+
277
+ // The argument-count markers for 0 to 9 arguments. napi pads missing
278
+ // arguments with undefined, so the wrappers below pass the real count after
279
+ // the positional arguments, as one of these markers: the native methods read
280
+ // a count only from a marker, and any other value there (from a raw
281
+ // js-binding call) is an extra argument.
282
+ const ARGUMENT_COUNTS = _argumentCounts()
283
+
284
+ // new ImageData(): Chrome checks `new` and the real argument count before it
285
+ // picks an overload (image_data.idl). The class napi-rs makes sees neither,
286
+ // so a native function checks them and builds the instance itself, from
287
+ // new.target.prototype. Instances share the class prototype, so instanceof
288
+ // works for those that createImageData and getImageData return.
289
+ const ImageData = _imageDataConstructor()
290
+ Object.defineProperty(ImageData, 'length', { value: 2 })
291
+ Object.defineProperty(ImageData, 'prototype', { value: NativeImageData.prototype, writable: false })
292
+ Object.defineProperty(NativeImageData.prototype, 'constructor', {
293
+ value: ImageData,
294
+ writable: true,
295
+ enumerable: false,
296
+ configurable: true,
297
+ })
298
+
299
+ // What the wrappers below pass after their `n` positional arguments, for a
300
+ // call with `count` (1 to `n`) of them, the last one `last`. Without a marker
301
+ // the native method counts up to the last argument that is not undefined:
302
+ // that is `count` unless `last` is an explicit undefined, and only then is
303
+ // the marker needed.
304
+ function argumentCount(count, last) {
305
+ return last === undefined ? ARGUMENT_COUNTS[count] : undefined
306
+ }
307
+
308
+ // fill(), clip(), stroke(), isPointInPath() and isPointInStroke(): WebIDL
309
+ // picks the overload from the argument count (canvas_2d_recorder_context.idl),
310
+ // and an explicit undefined counts: fill(path, undefined) is the nonzero
311
+ // fill of path, fill(undefined, 'evenodd') a TypeError.
312
+ // These wrappers and drawImage's read only the arguments the call has: past
313
+ // arguments.length, arguments[i] would read Object.prototype[i], which
314
+ // Chrome never reads.
315
+ const _fill = CanvasRenderingContext2D.prototype.fill
316
+ CanvasRenderingContext2D.prototype.fill = function fill() {
317
+ const count = Math.min(arguments.length, 2)
318
+ return _fill.call(
319
+ this,
320
+ count > 0 ? arguments[0] : undefined,
321
+ count > 1 ? arguments[1] : undefined,
322
+ count === 0 ? undefined : argumentCount(count, arguments[count - 1]),
323
+ )
324
+ }
325
+
326
+ const _clip = CanvasRenderingContext2D.prototype.clip
327
+ CanvasRenderingContext2D.prototype.clip = function clip() {
328
+ const count = Math.min(arguments.length, 2)
329
+ return _clip.call(
330
+ this,
331
+ count > 0 ? arguments[0] : undefined,
332
+ count > 1 ? arguments[1] : undefined,
333
+ count === 0 ? undefined : argumentCount(count, arguments[count - 1]),
334
+ )
335
+ }
336
+
337
+ const _stroke = CanvasRenderingContext2D.prototype.stroke
338
+ CanvasRenderingContext2D.prototype.stroke = function stroke() {
339
+ if (arguments.length === 0) {
340
+ return _stroke.call(this, undefined, undefined)
341
+ }
342
+ const path = arguments[0]
343
+ return _stroke.call(this, path, argumentCount(1, path))
344
+ }
345
+
346
+ const _isPointInPath = CanvasRenderingContext2D.prototype.isPointInPath
347
+ CanvasRenderingContext2D.prototype.isPointInPath = function isPointInPath() {
348
+ if (arguments.length < 2) {
349
+ argumentCountError(ctxBrand, this, 'isPointInPath', 'CanvasRenderingContext2D', 2, arguments.length)
350
+ }
351
+ const count = Math.min(arguments.length, 4)
352
+ return _isPointInPath.call(
353
+ this,
354
+ arguments[0],
355
+ arguments[1],
356
+ count > 2 ? arguments[2] : undefined,
357
+ count > 3 ? arguments[3] : undefined,
358
+ // (x, y, undefined) converts as (x, y) when x is not an object (only an
359
+ // object can be a Path2D), so the count is not needed.
360
+ count === 3 && typeof arguments[0] !== 'object' ? undefined : argumentCount(count, arguments[count - 1]),
361
+ )
362
+ }
363
+ Object.defineProperty(CanvasRenderingContext2D.prototype.isPointInPath, 'length', { value: 2 })
364
+
365
+ const _isPointInStroke = CanvasRenderingContext2D.prototype.isPointInStroke
366
+ CanvasRenderingContext2D.prototype.isPointInStroke = function isPointInStroke() {
367
+ if (arguments.length < 2) {
368
+ argumentCountError(ctxBrand, this, 'isPointInStroke', 'CanvasRenderingContext2D', 2, arguments.length)
369
+ }
370
+ const count = Math.min(arguments.length, 3)
371
+ return _isPointInStroke.call(
372
+ this,
373
+ arguments[0],
374
+ arguments[1],
375
+ count > 2 ? arguments[2] : undefined,
376
+ argumentCount(count, arguments[count - 1]),
377
+ )
378
+ }
379
+ Object.defineProperty(CanvasRenderingContext2D.prototype.isPointInStroke, 'length', { value: 2 })
380
+
381
+ // A TypeError as Chrome's bindings throw it, as in argumentCountError: the
382
+ // stack keeps the bare message, and `message` gets the prefix.
383
+ function contextTypeError(prefix, message) {
384
+ const error = new TypeError(message)
385
+ void error.stack
386
+ Object.defineProperty(error, 'message', {
387
+ value: prefix + message,
388
+ writable: true,
389
+ enumerable: true,
390
+ configurable: true,
391
+ })
392
+ return error
393
+ }
394
+
395
+ // An `unrestricted double` member of a matrix dictionary; undefined when it
396
+ // is absent. The native ToNumber keeps the engine's texts, and the error
397
+ // names the member, as in Chrome.
398
+ function matrixMember(value, key, dictionary, prefix) {
399
+ if (typeof value === 'number' || value === undefined) return value
400
+ return toNumber(value, `${prefix}Failed to read the '${key}' property from '${dictionary}': `)
401
+ }
402
+
403
+ // a and m11 (and so on) disagree: both given and not equal. Two NaNs agree.
404
+ const mismatch = (x, y) => x !== undefined && y !== undefined && x !== y && (x === x || y === y)
405
+ // A 3D member against its identity value; absent is the identity. As C++
406
+ // truthiness, NaN counts as 3D and -0 as 0.
407
+ const notZero = (v) => v !== undefined && v !== 0
408
+ const notOne = (v) => v !== undefined && v !== 1
409
+
410
+ // The matrix of a DOMMatrix2DInit, or of a DOMMatrixInit when `full`, as
411
+ // [m11, m12, m21, m22, m41, m42]; undefined and null are {}. As Blink's
412
+ // bindings, every member is read with [[Get]], in order (a..f, m11, m12,
413
+ // m21, m22, m41, m42, then is2D and m13..m44), then checked as
414
+ // DOMMatrixReadOnly::ValidateAndFixup (dom_matrix_read_only.cc). A DOMMatrix
415
+ // is read the same way: an own property, a patched getter or a Proxy may
416
+ // make its members disagree. The result is one shared array: read it before
417
+ // any other user code runs.
418
+ const MATRIX = [1, 0, 0, 1, 0, 0]
419
+ function matrix2DFromInit(init, prefix, full) {
420
+ if (init === undefined || init === null) {
421
+ MATRIX[0] = 1
422
+ MATRIX[1] = 0
423
+ MATRIX[2] = 0
424
+ MATRIX[3] = 1
425
+ MATRIX[4] = 0
426
+ MATRIX[5] = 0
427
+ return MATRIX
428
+ }
429
+ if (typeof init !== 'object' && typeof init !== 'function') {
430
+ throw contextTypeError(prefix, `The provided value is not of type '${full ? 'DOMMatrixInit' : 'DOMMatrix2DInit'}'.`)
431
+ }
432
+ const D2 = 'DOMMatrix2DInit'
433
+ const a = matrixMember(init.a, 'a', D2, prefix)
434
+ const b = matrixMember(init.b, 'b', D2, prefix)
435
+ const c = matrixMember(init.c, 'c', D2, prefix)
436
+ const d = matrixMember(init.d, 'd', D2, prefix)
437
+ const e = matrixMember(init.e, 'e', D2, prefix)
438
+ const f = matrixMember(init.f, 'f', D2, prefix)
439
+ const m11 = matrixMember(init.m11, 'm11', D2, prefix)
440
+ const m12 = matrixMember(init.m12, 'm12', D2, prefix)
441
+ const m21 = matrixMember(init.m21, 'm21', D2, prefix)
442
+ const m22 = matrixMember(init.m22, 'm22', D2, prefix)
443
+ const m41 = matrixMember(init.m41, 'm41', D2, prefix)
444
+ const m42 = matrixMember(init.m42, 'm42', D2, prefix)
445
+ let is3DIn2D = false
446
+ if (full) {
447
+ const D = 'DOMMatrixInit'
448
+ const is2D = init.is2D
449
+ const m13 = matrixMember(init.m13, 'm13', D, prefix)
450
+ const m14 = matrixMember(init.m14, 'm14', D, prefix)
451
+ const m23 = matrixMember(init.m23, 'm23', D, prefix)
452
+ const m24 = matrixMember(init.m24, 'm24', D, prefix)
453
+ const m31 = matrixMember(init.m31, 'm31', D, prefix)
454
+ const m32 = matrixMember(init.m32, 'm32', D, prefix)
455
+ const m33 = matrixMember(init.m33, 'm33', D, prefix)
456
+ const m34 = matrixMember(init.m34, 'm34', D, prefix)
457
+ const m43 = matrixMember(init.m43, 'm43', D, prefix)
458
+ const m44 = matrixMember(init.m44, 'm44', D, prefix)
459
+ // is2D is a boolean member: ToBoolean when present.
460
+ is3DIn2D =
461
+ is2D !== undefined &&
462
+ !!is2D &&
463
+ (notZero(m31) ||
464
+ notZero(m32) ||
465
+ notZero(m13) ||
466
+ notZero(m23) ||
467
+ notZero(m43) ||
468
+ notZero(m14) ||
469
+ notZero(m24) ||
470
+ notZero(m34) ||
471
+ notOne(m33) ||
472
+ notOne(m44))
473
+ }
474
+ if (
475
+ mismatch(a, m11) ||
476
+ mismatch(b, m12) ||
477
+ mismatch(c, m21) ||
478
+ mismatch(d, m22) ||
479
+ mismatch(e, m41) ||
480
+ mismatch(f, m42)
481
+ ) {
482
+ throw contextTypeError(prefix, 'Property mismatch on matrix initialization.')
483
+ }
484
+ if (is3DIn2D) {
485
+ throw contextTypeError(prefix, 'The is2D member is set to true but the input matrix is a 3d matrix.')
486
+ }
487
+ MATRIX[0] = m11 !== undefined ? m11 : a !== undefined ? a : 1
488
+ MATRIX[1] = m12 !== undefined ? m12 : b !== undefined ? b : 0
489
+ MATRIX[2] = m21 !== undefined ? m21 : c !== undefined ? c : 0
490
+ MATRIX[3] = m22 !== undefined ? m22 : d !== undefined ? d : 1
491
+ MATRIX[4] = m41 !== undefined ? m41 : e !== undefined ? e : 0
492
+ MATRIX[5] = m42 !== undefined ? m42 : f !== undefined ? f : 0
493
+ return MATRIX
494
+ }
495
+
496
+ // setTransform(): six or more arguments are six numbers, fewer a DOMMatrixInit
497
+ // (canvas_2d_recorder_context.idl), and an explicit undefined counts. The
498
+ // DOMMatrixInit is read above; the native method takes six numbers. It reads
499
+ // a first argument that is not a number as a DOMMatrixInit, the form a raw
500
+ // js-binding call may use, so ToNumber runs here for such an `a`.
501
+ const _setTransform = CanvasRenderingContext2D.prototype.setTransform
502
+ // A cheap native method: on another receiver it throws 'Illegal invocation',
503
+ // which Chrome checks before it converts any argument.
504
+ const ctxReceiver = CanvasRenderingContext2D.prototype.isContextLost
505
+ const SET_TRANSFORM = "Failed to execute 'setTransform' on 'CanvasRenderingContext2D': "
506
+ CanvasRenderingContext2D.prototype.setTransform = function setTransform() {
507
+ if (arguments.length >= 6) {
508
+ let a = arguments[0]
509
+ if (typeof a !== 'number') {
510
+ ctxReceiver.call(this)
511
+ a = toNumber(a, SET_TRANSFORM)
512
+ }
513
+ return _setTransform.call(this, a, arguments[1], arguments[2], arguments[3], arguments[4], arguments[5])
514
+ }
515
+ // Past arguments.length, arguments[0] would read Object.prototype[0].
516
+ const init = arguments.length > 0 ? arguments[0] : undefined
517
+ if (init !== undefined && init !== null) ctxReceiver.call(this)
518
+ const m = matrix2DFromInit(init, SET_TRANSFORM, true)
519
+ return _setTransform.call(this, m[0], m[1], m[2], m[3], m[4], m[5])
520
+ }
521
+
522
+ // addPath(path, transform = {}): Chrome checks the receiver, the argument
523
+ // count and the path, then reads the DOMMatrix2DInit, read above. The native
524
+ // method takes six numbers after the path; anything else there is a
525
+ // DOMMatrix2DInit, which only a raw js-binding call passes. Without a
526
+ // transform (undefined or null is `{}`), a native method that takes only the
527
+ // path adds it as it is; Chrome's Path2D has no such method, so it comes off
528
+ // the prototype.
529
+ const _addPath = Path2D.prototype.addPath
530
+ const _addPathWithoutTransform = Path2D.prototype._addPathWithoutTransform
531
+ delete Path2D.prototype._addPathWithoutTransform
532
+ const ADD_PATH = "Failed to execute 'addPath' on 'Path2D': "
533
+ Path2D.prototype.addPath = function addPath() {
534
+ if (arguments.length < 1) {
535
+ argumentCountError(pathBrand, this, 'addPath', 'Path2D', 1, arguments.length)
536
+ }
537
+ const path = arguments[0]
538
+ // Past arguments.length, arguments[1] would read Object.prototype[1].
539
+ const transform = arguments.length > 1 ? arguments[1] : undefined
540
+ if (transform === undefined || transform === null) {
541
+ return _addPathWithoutTransform.call(this, path)
542
+ }
543
+ pathBrand.call(this)
544
+ try {
545
+ pathBrand.call(path)
546
+ } catch {
547
+ throw contextTypeError(ADD_PATH, "parameter 1 is not of type 'Path2D'.")
548
+ }
549
+ const m = matrix2DFromInit(transform, ADD_PATH, false)
550
+ return _addPath.call(this, path, m[0], m[1], m[2], m[3], m[4], m[5])
551
+ }
552
+ Object.defineProperty(Path2D.prototype.addPath, 'length', { value: 1 })
553
+
554
+ // createImageData(): WebIDL picks the overload from the argument count, and an
555
+ // explicit undefined counts: createImageData(imagedata, undefined) is the
556
+ // (sw, sh) form.
557
+ const _createImageData = CanvasRenderingContext2D.prototype.createImageData
558
+
559
+ CanvasRenderingContext2D.prototype.createImageData = function createImageData() {
560
+ const count = arguments.length
561
+ // Past arguments.length, arguments[i] would read Object.prototype[i]: pass
562
+ // undefined there, as napi pads missing arguments.
563
+ return _createImageData.call(
564
+ this,
565
+ count > 0 ? arguments[0] : undefined,
566
+ count > 1 ? arguments[1] : undefined,
567
+ count > 2 ? arguments[2] : undefined,
568
+ count > 3 ? arguments[3] : undefined,
569
+ ARGUMENT_COUNTS[Math.min(count, 4)],
570
+ )
571
+ }
572
+ Object.defineProperty(CanvasRenderingContext2D.prototype.createImageData, 'length', { value: 1 })
573
+
574
+ // putImageData(imagedata, dx, dy) or (imagedata, dx, dy, dirtyX, dirtyY,
575
+ // dirtyWidth, dirtyHeight): as in Chrome, it needs 3 arguments; the native
576
+ // method picks the overload from the argument count (3 or 7).
577
+ const _putImageData = CanvasRenderingContext2D.prototype.putImageData
578
+
579
+ CanvasRenderingContext2D.prototype.putImageData = function putImageData() {
580
+ const count = arguments.length
581
+ if (count < 3) {
582
+ argumentCountError(ctxBrand, this, 'putImageData', 'CanvasRenderingContext2D', 3, count)
583
+ }
584
+ // Past arguments.length, arguments[i] would read Object.prototype[i]: pass
585
+ // undefined there, as napi pads missing arguments.
586
+ return _putImageData.call(
587
+ this,
588
+ arguments[0],
589
+ arguments[1],
590
+ arguments[2],
591
+ count > 3 ? arguments[3] : undefined,
592
+ count > 4 ? arguments[4] : undefined,
593
+ count > 5 ? arguments[5] : undefined,
594
+ count > 6 ? arguments[6] : undefined,
595
+ ARGUMENT_COUNTS[Math.min(count, 7)],
596
+ )
597
+ }
598
+ Object.defineProperty(CanvasRenderingContext2D.prototype.putImageData, 'length', { value: 3 })
599
+
600
+ // createPattern(image, repetitionType): as in Chrome, both arguments are
601
+ // required, and the image is converted before the repetition, a
602
+ // [LegacyNullToEmptyString] DOMString (canvas_2d_recorder_context.idl), so
603
+ // null is '' and undefined is 'undefined'.
604
+ const _createPattern = CanvasRenderingContext2D.prototype.createPattern
605
+
606
+ CanvasRenderingContext2D.prototype.createPattern = function createPattern(image, repetition) {
607
+ return _createPattern.call(this, image, repetition, ARGUMENT_COUNTS[Math.min(arguments.length, 4)])
608
+ }
609
+
610
+ // addColorStop(offset, color): both arguments are required. CanvasGradient
611
+ // has no other native method to check the receiver first, so the native
612
+ // method itself reads the real count from the marker after them.
613
+ const _addColorStop = CanvasGradient.prototype.addColorStop
614
+ CanvasGradient.prototype.addColorStop = function addColorStop(offset, color) {
615
+ return _addColorStop.call(this, offset, color, ARGUMENT_COUNTS[Math.min(arguments.length, 2)])
616
+ }
617
+
104
618
  // Workaround for webpack bundling issue with drawImage
105
619
  // Store the original drawImage method
106
620
  const _drawImage = CanvasRenderingContext2D.prototype.drawImage
107
621
 
108
- // Override drawImage to ensure proper type recognition in bundled environments
109
- CanvasRenderingContext2D.prototype.drawImage = function drawImage(image, ...args) {
622
+ // Override drawImage to ensure proper type recognition in bundled environments.
623
+ // As in Chrome, it needs 3 arguments; the native method picks the overload
624
+ // from the argument count (3, 5 or 9).
625
+ CanvasRenderingContext2D.prototype.drawImage = function drawImage() {
626
+ if (arguments.length < 3) {
627
+ argumentCountError(ctxBrand, this, 'drawImage', 'CanvasRenderingContext2D', 3, arguments.length)
628
+ }
629
+ let image = arguments[0]
110
630
  // If the image is a Canvas-like object but not recognized due to bundling,
111
631
  // we need to ensure it's properly identified
112
632
  if (image && typeof image === 'object') {
@@ -129,8 +649,22 @@ CanvasRenderingContext2D.prototype.drawImage = function drawImage(image, ...args
129
649
  }
130
650
 
131
651
  // Call the original drawImage with the potentially corrected image
132
- return _drawImage.apply(this, [image, ...args])
652
+ const count = Math.min(arguments.length, 9)
653
+ return _drawImage.call(
654
+ this,
655
+ image,
656
+ arguments[1],
657
+ arguments[2],
658
+ count > 3 ? arguments[3] : undefined,
659
+ count > 4 ? arguments[4] : undefined,
660
+ count > 5 ? arguments[5] : undefined,
661
+ count > 6 ? arguments[6] : undefined,
662
+ count > 7 ? arguments[7] : undefined,
663
+ count > 8 ? arguments[8] : undefined,
664
+ argumentCount(count, arguments[count - 1]),
665
+ )
133
666
  }
667
+ Object.defineProperty(CanvasRenderingContext2D.prototype.drawImage, 'length', { value: 3 })
134
668
 
135
669
  function createCanvas(width, height, flag) {
136
670
  const isSvgBackend = typeof flag !== 'undefined'
@@ -148,16 +682,15 @@ class Canvas {
148
682
  }
149
683
 
150
684
  if (!process.env.DISABLE_SYSTEM_FONTS_LOAD) {
151
- GlobalFonts.loadSystemFonts()
685
+ // macOS: CoreText's installed fonts, user fonts included, listed at the
686
+ // first font lookup.
687
+ GlobalFonts.enableSystemFonts()
152
688
  const platformName = platform()
153
689
  const homedirPath = homedir()
154
690
  switch (platformName) {
155
691
  case 'win32':
156
692
  GlobalFonts.loadFontsFromDir(join(homedirPath, 'AppData', 'Local', 'Microsoft', 'Windows', 'Fonts'))
157
693
  break
158
- case 'darwin':
159
- GlobalFonts.loadFontsFromDir(join(homedirPath, 'Library', 'Fonts'))
160
- break
161
694
  case 'linux':
162
695
  GlobalFonts.loadFontsFromDir(join('usr', 'local', 'share', 'fonts'))
163
696
  GlobalFonts.loadFontsFromDir(join(homedirPath, '.fonts'))