@napi-rs/canvas 1.0.11 → 1.0.13

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