skia-canvas 2.0.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,15 +4,17 @@
4
4
 
5
5
  "use strict"
6
6
 
7
- const {RustClass, core, inspect, REPR} = require('./neon'),
8
- {Image, ImageData, pixelSize} = require('./imagery'),
7
+ const {fileURLToPath} = require('url'),
8
+ {RustClass, core, inspect, argc, REPR} = require('./neon'),
9
+ {Image, ImageData, pixelSize, getSharp} = require('./imagery'),
10
+ {Path2D} = require('./path'),
9
11
  {toSkMatrix} = require('./geometry')
10
12
 
11
13
  class Canvas extends RustClass{
12
14
  #contexts
13
15
 
14
- constructor(width, height){
15
- super(Canvas).alloc()
16
+ constructor(width, height, {textContrast=0, textGamma=1.4, gpu=true}={}){
17
+ super(Canvas).alloc({textContrast, textGamma, gpu:!!gpu})
16
18
  this.#contexts = []
17
19
  Object.assign(this, {width, height})
18
20
  }
@@ -28,13 +30,13 @@ class Canvas extends RustClass{
28
30
 
29
31
  get width(){ return this.prop('width') }
30
32
  set width(w){
31
- this.prop('width', (typeof w=='number' && !Number.isNaN(w) && w>=0) ? w : 300)
33
+ this.prop('width', !Number.isNaN(+w) && +w>=0 ? w : 300)
32
34
  if (this.#contexts[0]) this.getContext("2d").ƒ('resetSize', core(this))
33
35
  }
34
36
 
35
37
  get height(){ return this.prop('height') }
36
38
  set height(h){
37
- this.prop('height', h = (typeof h=='number' && !Number.isNaN(h) && h>=0) ? h : 150)
39
+ this.prop('height', !Number.isNaN(+h) && +h>=0 ? h : 150)
38
40
  if (this.#contexts[0]) this.getContext("2d").ƒ('resetSize', core(this))
39
41
  }
40
42
 
@@ -52,52 +54,70 @@ class Canvas extends RustClass{
52
54
  return this.#contexts.slice().reverse()
53
55
  }
54
56
 
57
+ get raw(){ return this.toBuffer("raw") }
55
58
  get png(){ return this.toBuffer("png") }
56
59
  get jpg(){ return this.toBuffer("jpg") }
57
60
  get pdf(){ return this.toBuffer("pdf") }
58
61
  get svg(){ return this.toBuffer("svg") }
59
62
  get webp(){ return this.toBuffer("webp") }
60
63
 
61
- saveAs(filename, opts={}){
62
- opts = typeof opts=='number' ? {quality:opts} : opts
63
- let {format, quality, pages, padding, pattern, density, outline, matte, msaa, colorType} = exportOptions(this.pages, {filename, ...opts}),
64
- args = [pages.map(core), pattern, padding, {format, quality, density, outline, matte, msaa, colorType}]
64
+ // Warn about renamed methods but map them to the new names (for now)
65
+ saveAs(){ _deprecated('Canvas.saveAs()'); this.toFile(...arguments) }
66
+ saveAsSync(){ _deprecated('Canvas.saveAsSync()'); this.toFileSync(...arguments) }
67
+ toDataURLSync(){ _deprecated('Canvas.toDataURLSync()'); this.toURLSync(...arguments) }
68
+
69
+ toFile(filename, opts={}){
70
+ let {pages, padding, pattern, ...rest} = exportOptions(this, {filename}, opts),
71
+ args = [pages.map(core), pattern, padding, rest]
65
72
  return this.ƒ("save", ...args)
66
73
  }
67
74
 
68
- saveAsSync(filename, opts={}){
69
- opts = typeof opts=='number' ? {quality:opts} : opts
70
- let {format, quality, pages, padding, pattern, density, outline, matte, msaa, colorType} = exportOptions(this.pages, {filename, ...opts})
71
- this.ƒ("saveSync", pages.map(core), pattern, padding, {format, quality, density, outline, matte, msaa, colorType})
75
+ toFileSync(filename, opts={}){
76
+ let {pages, padding, pattern, ...rest} = exportOptions(this, {filename}, opts)
77
+ this.ƒ("saveSync", pages.map(core), pattern, padding, rest)
72
78
  }
73
79
 
74
80
  toBuffer(extension="png", opts={}){
75
- opts = typeof opts=='number' ? {quality:opts} : opts
76
- let {format, quality, pages, density, outline, matte, msaa, colorType} = exportOptions(this.pages, {extension, ...opts}),
77
- args = [pages.map(core), {format, quality, density, outline, matte, msaa, colorType}];
78
- return this.ƒ("toBuffer", ...args)
81
+ let {pages, ...rest} = exportOptions(this, {extension}, opts)
82
+ return this.ƒ("toBuffer", pages.map(core), rest)
79
83
  }
80
84
 
81
85
  toBufferSync(extension="png", opts={}){
82
- opts = typeof opts=='number' ? {quality:opts} : opts
83
- let {format, quality, pages, density, outline, matte, msaa, colorType} = exportOptions(this.pages, {extension, ...opts})
84
- return this.ƒ("toBufferSync", pages.map(core), {format, quality, density, outline, matte, msaa, colorType})
86
+ let {pages, ...rest} = exportOptions(this, {extension}, opts)
87
+ return this.ƒ("toBufferSync", pages.map(core), rest)
85
88
  }
86
89
 
87
- toDataURL(extension="png", opts={}){
88
- opts = typeof opts=='number' ? {quality:opts} : opts
89
- let {mime} = exportOptions(this.pages, {extension, ...opts}),
90
+ toURL(extension="png", opts={}){
91
+ let {mime} = exportOptions(this, {extension}, opts),
90
92
  buffer = this.toBuffer(extension, opts);
91
93
  return buffer.then(data => `data:${mime};base64,${data.toString('base64')}`)
92
94
  }
93
95
 
94
- toDataURLSync(extension="png", opts={}){
95
- opts = typeof opts=='number' ? {quality:opts} : opts
96
- let {mime} = exportOptions(this.pages, {extension, ...opts}),
96
+ toURLSync(extension="png", opts={}){
97
+ let {mime} = exportOptions(this, {extension}, opts),
97
98
  buffer = this.toBufferSync(extension, opts);
98
99
  return `data:${mime};base64,${buffer.toString('base64')}`
99
100
  }
100
101
 
102
+ // Match the browser API in only accepting a single optional quality argument
103
+ toDataURL(extension="png", quality){
104
+ if (quality!==undefined && typeof quality!=='number'){
105
+ throw TypeError("Expected a number in the range 0–1 for `quality` (use toURL() for additional rendering options)")
106
+ }
107
+ return this.toURLSync(extension, {quality})
108
+ }
109
+
110
+ toSharp({page, matte, msaa, density=1}={}){
111
+ const {Readable} = require('node:stream'),
112
+ sharp = getSharp(),
113
+ buffer = this.toBuffer("raw", {page, matte, density, msaa})
114
+
115
+ return Readable.from(
116
+ (async function * (){ yield buffer })()
117
+ ).pipe(sharp({
118
+ raw: {width:this.width*density, height:this.height*density, channels:4}
119
+ }).withMetadata({density:density * 72}))
120
+ }
101
121
 
102
122
  [REPR](depth, options) {
103
123
  let {width, height, gpu, engine, pages} = this
@@ -114,8 +134,7 @@ class CanvasGradient extends RustClass{
114
134
  }
115
135
 
116
136
  addColorStop(offset, color){
117
- if (offset>=0 && offset<=1) this.ƒ('addColorStop', offset, color)
118
- else throw new Error("Color stop offsets must be between 0.0 and 1.0")
137
+ this.ƒ('addColorStop', ...arguments)
119
138
  }
120
139
 
121
140
  [REPR](depth, options) {
@@ -125,15 +144,16 @@ class CanvasGradient extends RustClass{
125
144
 
126
145
  class CanvasPattern extends RustClass{
127
146
  constructor(canvas, src, repeat){
147
+ repeat = [...arguments].slice(2)
128
148
  super(CanvasPattern)
129
149
  if (src instanceof Image){
130
150
  let {width, height} = canvas
131
- this.init('from_image', core(src), width, height, repeat)
151
+ this.init('from_image', core(src), width, height, ...repeat)
132
152
  }else if (src instanceof ImageData){
133
- this.init('from_image_data', src, repeat)
153
+ this.init('from_image_data', src, ...repeat)
134
154
  }else if (src instanceof Canvas){
135
155
  let ctx = src.getContext('2d')
136
- this.init('from_canvas', core(ctx), repeat)
156
+ this.init('from_canvas', core(ctx), ...repeat)
137
157
  }else{
138
158
  throw new Error("CanvasPatterns require a source Image or a Canvas")
139
159
  }
@@ -147,14 +167,18 @@ class CanvasPattern extends RustClass{
147
167
  }
148
168
 
149
169
  class CanvasTexture extends RustClass{
150
- constructor(spacing, {path, line, color, angle, offset=0}={}){
170
+ constructor(spacing, {path, color, angle, line, cap="butt", outline=false, offset=0}={}){
151
171
  super(CanvasTexture)
152
- let [x, y] = typeof offset=='number' ? [offset, offset] : offset.slice(0, 2)
153
- let [h, v] = typeof spacing=='number' ? [spacing, spacing] : spacing.slice(0, 2)
172
+ argc(arguments, 1)
173
+ let [x, y] = Array.isArray(offset) ? offset.concat(offset).slice(0, 2) : [offset, offset]
174
+ let [h, v] = Array.isArray(spacing) ? spacing.concat(spacing).slice(0, 2) : [spacing, spacing]
175
+ if (path!==undefined && !(path instanceof Path2D)){
176
+ throw TypeError("Expected a Path2D object for `path`")
177
+ }
154
178
  path = core(path)
155
179
  line = line != null ? line : (path ? 0 : 1)
156
180
  angle = angle != null ? angle : (path ? 0 : -Math.PI / 4)
157
- this.alloc(path, color, line, angle, h, v, x, y)
181
+ this.alloc(path, color, line, cap, angle, !!outline, h, v, x, y)
158
182
  }
159
183
 
160
184
  [REPR](depth, options) {
@@ -201,16 +225,33 @@ class Format{
201
225
 
202
226
  const {basename, extname} = require('path')
203
227
 
204
- function exportOptions(pages, {filename='', extension='', format, page, quality, matte, density, outline, msaa, colorType}={}){
228
+ function exportOptions(canvas, {filename='', extension=''}, opts){
229
+ // a single number will be interpreted as a quality setting
230
+ if (typeof opts=='number') opts = {quality:opts}
231
+
232
+ // unpack common export options
233
+ let {page, quality, matte, density, msaa, outline, downsample, colorType} = opts
234
+
235
+ // only allow format overrides in toFile()
236
+ let imageFormat = !!filename ? opts.format : undefined
237
+
238
+ if (filename instanceof URL){
239
+ if (filename.protocol=='file:') filename = fileURLToPath(filename)
240
+ else throw Error(`URLs must use 'file' protocol (got '${filename.protocol.replace(':', '')}')`)
241
+ }
242
+
243
+ // ensure the canvas has a context (so it can at least generate an empty image)
244
+ if (!canvas.pages.length) canvas.getContext("2d")
245
+
205
246
  var {fromMime, toMime, expected} = new Format(),
206
- ext = format || extension.replace(/@\d+x$/i,'') || extname(filename),
247
+ ext = imageFormat || extension.replace(/@\d+x$/i,'') || extname(filename),
207
248
  format = fromMime(toMime(ext) || ext),
208
249
  mime = toMime(format),
250
+ pages = canvas.pages,
209
251
  pp = pages.length
210
252
 
211
253
  if (!ext) throw new Error(`Cannot determine image format (use a filename extension or 'format' argument)`)
212
254
  if (!format) throw new Error(`Unsupported file format "${ext}" (expected ${expected})`)
213
- if (!pp) throw new RangeError(`Canvas has no associated contexts (try calling getContext or newPage first)`)
214
255
 
215
256
  let padding, isSequence, pattern = filename.replace(/{(\d*)}/g, (_, width) => {
216
257
  isSequence = true
@@ -233,11 +274,14 @@ function exportOptions(pages, {filename='', extension='', format, page, quality,
233
274
  : isSequence || format=='pdf' ? pages
234
275
  : pages.slice(-1) // default to the 'current' context
235
276
 
277
+ // inherit text settings from the canvas (since they can't be changed on a per-render basis due to glyph caching)
278
+ const {textContrast, textGamma} = canvas.engine
279
+
236
280
  if (quality===undefined){
237
281
  quality = 0.92
238
282
  }else{
239
283
  if (typeof quality!='number' || !isFinite(quality) || quality<0 || quality>1){
240
- throw new TypeError("The quality option must be an number in the 0.0–1.0 range")
284
+ throw new TypeError("Expected a number between 0.0–1.0 for `quality`")
241
285
  }
242
286
  }
243
287
 
@@ -245,20 +289,12 @@ function exportOptions(pages, {filename='', extension='', format, page, quality,
245
289
  let m = (extension || basename(filename, ext)).match(/@(\d+)x$/i)
246
290
  density = m ? parseInt(m[1], 10) : 1
247
291
  }else if (typeof density!='number' || !Number.isInteger(density) || density<1){
248
- throw new TypeError("The density option must be a non-negative integer")
249
- }
250
-
251
- if (outline===undefined){
252
- outline = true
253
- }else if (format == 'svg'){
254
- outline = !!outline
292
+ throw new TypeError("Expected a non-negative integer for `density`")
255
293
  }
256
294
 
257
- if (msaa===undefined) {
258
- msaa = undefined // leave as-is
259
- }else if (!msaa){
260
- msaa = 0 // null, false, etc. should all disable it
261
- }else if (typeof msaa!='number' || !isFinite(msaa) || msaa<0){
295
+ if (msaa===undefined || msaa===true) {
296
+ msaa = undefined // use the default 4x msaa
297
+ }else if (!isFinite(+msaa) || +msaa<0){
262
298
  throw new TypeError("The number of MSAA samples must be an integer ≥0")
263
299
  }
264
300
 
@@ -266,7 +302,26 @@ function exportOptions(pages, {filename='', extension='', format, page, quality,
266
302
  pixelSize(colorType) // throw an error if invalid
267
303
  }
268
304
 
269
- return {filename, pattern, format, mime, pages, padding, quality, matte, density, outline, msaa, colorType}
305
+ // default to false, otherwise detect truthy
306
+ downsample = !!downsample
307
+ outline = !!outline
308
+
309
+ return {
310
+ filename, pattern, format, mime, pages, padding, quality, matte,
311
+ density, msaa, outline, textContrast, textGamma, downsample, colorType
312
+ }
313
+ }
314
+
315
+ // emit a deprecation warning, once per API per process
316
+ let _warnings = {
317
+ "Canvas.saveAs()": "Canvas.toFile()",
318
+ "Canvas.saveAsSync()": "Canvas.toFileSync()",
319
+ "Canvas.toDataURLSync()": "Canvas.toURLSync() (see also Canvas.toDataURL() which is now synchronous)",
320
+ }
321
+ function _deprecated(oldAPI){
322
+ let newAPI = _warnings[oldAPI]
323
+ if (newAPI) console.error(`Deprecation warning: ${oldAPI} has been renamed to ${newAPI} and will stop working in a future release.`)
324
+ delete _warnings[oldAPI]
270
325
  }
271
326
 
272
- module.exports = {Canvas, CanvasGradient, CanvasPattern, CanvasTexture}
327
+ module.exports = {Canvas, CanvasGradient, CanvasPattern, CanvasTexture, getSharp}
@@ -4,7 +4,7 @@
4
4
 
5
5
  "use strict"
6
6
 
7
- const {RustClass, core, wrap, inspect, REPR} = require('./neon'),
7
+ const {RustClass, core, wrap, inspect, argc, REPR} = require('./neon'),
8
8
  {Canvas, CanvasGradient, CanvasPattern, CanvasTexture} = require('./canvas'),
9
9
  {fromSkMatrix, toSkMatrix} = require('./geometry'),
10
10
  {Image, ImageData} = require('./imagery'),
@@ -63,7 +63,8 @@ class CanvasRenderingContext2D extends RustClass{
63
63
  quadraticCurveTo(cpx, cpy, x, y){ this.ƒ('quadraticCurveTo', ...arguments) }
64
64
  conicCurveTo(cpx, cpy, x, y, weight){ this.ƒ("conicCurveTo", ...arguments) }
65
65
  closePath(){ this.ƒ('closePath') }
66
- roundRect(x, y, w, h, r){
66
+ roundRect(x, y, w, h, r=0){
67
+ argc(arguments, 4, 5)
67
68
  let radii = css.radii(r)
68
69
  if (radii){
69
70
  if (w < 0) radii = [radii[1], radii[0], radii[3], radii[2]]
@@ -75,32 +76,32 @@ class CanvasRenderingContext2D extends RustClass{
75
76
 
76
77
  // -- using paths -----------------------------------------------------------
77
78
  fill(path, rule){
78
- if (path instanceof Path2D) this.ƒ('fill', core(path), rule)
79
- else this.ƒ('fill', path) // 'path' is the optional winding-rule
79
+ if (path instanceof Path2D) arguments[0] = core(path)
80
+ return this.ƒ('fill', ...arguments)
80
81
  }
81
82
 
82
- stroke(path, rule){
83
- if (path instanceof Path2D) this.ƒ('stroke', core(path), rule)
84
- else this.ƒ('stroke', path) // 'path' is the optional winding-rule
83
+ stroke(path){
84
+ if (path instanceof Path2D) arguments[0] = core(path)
85
+ return this.ƒ('stroke', ...arguments)
85
86
  }
86
87
 
87
88
  clip(path, rule){
88
- if (path instanceof Path2D) this.ƒ('clip', core(path), rule)
89
- else this.ƒ('clip', path) // 'path' is the optional winding-rule
89
+ if (path instanceof Path2D) arguments[0] = core(path)
90
+ return this.ƒ('clip', ...arguments)
90
91
  }
91
92
 
92
- isPointInPath(path, x, y){
93
- let args = path instanceof Path2D ? [core(path), x, y] : arguments
94
- return this.ƒ('isPointInPath', ...args)
93
+ isPointInPath(path, x, y, rule){
94
+ if (path instanceof Path2D) arguments[0] = core(path)
95
+ return this.ƒ('isPointInPath', ...arguments)
95
96
  }
96
97
  isPointInStroke(path, x, y){
97
- let args = path instanceof Path2D ? [core(path), x, y] : arguments
98
- return this.ƒ('isPointInStroke', ...args)
98
+ if (path instanceof Path2D) arguments[0] = core(path)
99
+ return this.ƒ('isPointInStroke', ...arguments)
99
100
  }
100
101
 
101
102
 
102
103
  // -- shaders ---------------------------------------------------------------
103
- createPattern(image, repetition){ return new CanvasPattern(this.canvas, image, repetition) }
104
+ createPattern(image, repetition){ return new CanvasPattern(this.canvas, ...arguments) }
104
105
  createLinearGradient(x0, y0, x1, y1){
105
106
  return new CanvasGradient("Linear", ...arguments)
106
107
  }
@@ -112,7 +113,7 @@ class CanvasRenderingContext2D extends RustClass{
112
113
  }
113
114
 
114
115
  createTexture(spacing, options){
115
- return new CanvasTexture(spacing, options)
116
+ return new CanvasTexture(...arguments)
116
117
  }
117
118
 
118
119
  // -- fill & stroke ---------------------------------------------------------
@@ -146,7 +147,7 @@ class CanvasRenderingContext2D extends RustClass{
146
147
 
147
148
  // -- line style ------------------------------------------------------------
148
149
  getLineDash(){ return this.ƒ("getLineDash") }
149
- setLineDash(segments){ this.ƒ("setLineDash", segments) }
150
+ setLineDash(segments){ this.ƒ("setLineDash", ...arguments) }
150
151
  get lineCap(){ return this.prop("lineCap") }
151
152
  set lineCap(style){ this.prop("lineCap", style) }
152
153
  get lineDashFit(){ return this.prop("lineDashFit") }
@@ -167,14 +168,34 @@ class CanvasRenderingContext2D extends RustClass{
167
168
  set imageSmoothingEnabled(flag){ this.prop("imageSmoothingEnabled", !!flag)}
168
169
  get imageSmoothingQuality(){ return this.prop("imageSmoothingQuality")}
169
170
  set imageSmoothingQuality(level){ this.prop("imageSmoothingQuality", level)}
170
- putImageData(imageData, ...coords){ this.ƒ('putImageData', imageData, ...coords) }
171
- createImageData(width, height, settings){ return new ImageData(width, height, settings) }
172
-
173
- getImageData(x, y, width, height, {colorType='rgba', colorSpace='srgb'}={}){
174
- let w = Math.floor(width),
175
- h = Math.floor(height),
176
- buffer = this.ƒ('getImageData', core(this.canvas), x, y, w, h, {colorType, colorSpace});
177
- return new ImageData(buffer, w, h, {colorType, colorSpace})
171
+
172
+ createImageData(width, height, settings){
173
+ argc(arguments, 2, 3)
174
+ return new ImageData(width, height, settings)
175
+ }
176
+
177
+ getImageData(x, y, width, height, {colorType='rgba', colorSpace='srgb', density=1, matte, msaa}={}){
178
+ argc(arguments, 4, 5)
179
+
180
+ if (typeof density!='number' || !Number.isInteger(density) || density<1){
181
+ throw new TypeError("Expected a non-negative integer for `density`")
182
+ }
183
+
184
+ if (msaa===undefined || msaa===true) {
185
+ msaa = undefined // use the default 4x msaa
186
+ }else if (!isFinite(+msaa) || +msaa<0){
187
+ throw new TypeError("The number of MSAA samples must be an integer ≥0")
188
+ }
189
+
190
+ let opts = {colorType, colorSpace, density, matte, msaa},
191
+ buffer = this.ƒ('getImageData', x, y, width, height, opts, core(this.canvas));
192
+ return new ImageData(buffer, width*density, height*density, {colorType, colorSpace})
193
+ }
194
+
195
+ putImageData(imageData, ...coords){
196
+ argc(arguments, 3, 7)
197
+ if (!(imageData instanceof ImageData)) throw TypeError("Expected an ImageData as 1st arg")
198
+ this.ƒ('putImageData', imageData, ...coords)
178
199
  }
179
200
 
180
201
  drawImage(image, ...coords){
@@ -182,13 +203,14 @@ class CanvasRenderingContext2D extends RustClass{
182
203
  this.ƒ('drawImage', core(image.getContext('2d')), ...coords)
183
204
  }else if (image instanceof Image){
184
205
  if (image.complete) this.ƒ('drawImage', core(image), ...coords)
185
- else throw new Error("Image has not completed loading: listen for `load` event or await `decode()` first")
206
+ else throw Error("Image has not completed loading: listen for `load` event or await `decode()` first")
186
207
  }else if (image instanceof ImageData){
187
208
  this.ƒ('drawImage', image, ...coords)
188
209
  }else if (image instanceof Promise) {
189
- throw new Error("Promise has not yet resolved: `await` image loading before drawing")
210
+ throw Error("Promise has not yet resolved: `await` image loading before drawing")
190
211
  }else{
191
- throw new Error(`Expected an Image or a Canvas argument (got: ${inspect(image, {depth:1})})`)
212
+ let nonimage = inspect(image, {depth:1})
213
+ throw Error(`Expected an Image or a Canvas argument (got: ${nonimage})`)
192
214
  }
193
215
  }
194
216
 
@@ -217,21 +239,20 @@ class CanvasRenderingContext2D extends RustClass{
217
239
  set wordSpacing(str){ this.prop('wordSpacing', css.spacing(str)) }
218
240
 
219
241
  measureText(text, maxWidth){
220
- text = this.textWrap ? text : text + '\u200b' // include trailing whitespace by default
221
- let [metrics, ...lines] = this.ƒ('measureText', toString(text), maxWidth)
222
- return new TextMetrics(metrics, lines)
242
+ let metrics = JSON.parse(this.ƒ('measureText', toString(text), maxWidth))
243
+ return new TextMetrics(metrics)
223
244
  }
224
245
 
225
- fillText(text, x, y, maxWidth){
226
- this.ƒ('fillText', toString(text), x, y, maxWidth)
246
+ fillText(text, ...geom){
247
+ this.ƒ('fillText', toString(text), ...geom)
227
248
  }
228
249
 
229
- strokeText(text, x, y, maxWidth){
230
- this.ƒ('strokeText', toString(text), x, y, maxWidth)
250
+ strokeText(text, ...geom){
251
+ this.ƒ('strokeText', toString(text), ...geom)
231
252
  }
232
253
 
233
- outlineText(text, width){
234
- let path = this.ƒ('outlineText', toString(text), width)
254
+ outlineText(text, ...geom){
255
+ let path = this.ƒ('outlineText', toString(text), ...geom)
235
256
  return path ? wrap(Path2D, path) : null
236
257
  }
237
258
 
@@ -9,7 +9,6 @@
9
9
  // https://www.w3.org/TR/css-fonts-3/#font-size-prop
10
10
 
11
11
  var splitBy = require('string-split-by'),
12
- {DOMPoint} = require('./geometry'),
13
12
  m, cache = {font:{}, variant:{}};
14
13
 
15
14
  const styleRE = /^(normal|italic|oblique)$/,
@@ -225,20 +224,20 @@ function parseFit(mode){
225
224
 
226
225
  function parseCornerRadii(r){
227
226
  r = [r].flat()
228
- .map(n => n instanceof DOMPoint ? n : new DOMPoint(n, n))
229
227
  .slice(0, 4)
228
+ .map(n => n && Object.hasOwn(n, 'x') && Object.hasOwn(n, 'y') ? n : {x:n, y:n})
230
229
 
231
230
  if (r.some(pt => !Number.isFinite(pt.x) || !Number.isFinite(pt.y))){
232
231
  return null // silently abort
233
232
  }else if (r.some(pt => pt.x < 0 || pt.y < 0)){
234
- throw new Error("Corner radius cannot be negative")
233
+ throw RangeError("Corner radius cannot be negative")
235
234
  }
236
235
 
237
236
  return r.length == 1 ? [r[0], r[0], r[0], r[0]]
238
237
  : r.length == 2 ? [r[0], r[1], r[0], r[1]]
239
238
  : r.length == 3 ? [r[0], r[1], r[2], r[1]]
240
239
  : r.length == 4 ? [r[0], r[1], r[2], r[3]]
241
- : [0, 0, 0, 0].map(n => new DOMPoint(n, n))
240
+ : [0, 0, 0, 0].map(n => ({x:n, y:n}))
242
241
  }
243
242
 
244
243
  // -- Image Filters -----------------------------------------------------------------------
@@ -6,6 +6,14 @@
6
6
 
7
7
  const {inspect} = require('util')
8
8
 
9
+ const isPlainObject = o => (
10
+ o !== null &&
11
+ typeof o === "object" &&
12
+ !(o instanceof DOMMatrix) &&
13
+ !Array.isArray(o) &&
14
+ !ArrayBuffer.isView(o)
15
+ )
16
+
9
17
  /*
10
18
  * vendored in order to fix its dependence on the window global [@samizdatco 2020/08/04]
11
19
  * removed SVGMatrix references that were guaranteed to be undefined on node [@mpaperno 2024/10/20]
@@ -331,8 +339,10 @@ class DOMMatrix {
331
339
  init.m31, init.m32, init.m33, init.m34,
332
340
  init.m41, init.m42, init.m43, init.m44,
333
341
  ]);
334
- if (DOMMatrix.isMatrix3(init))
335
- return new DOMMatrix([init.a, init.b, init.c, init.d, init.e, init.f]);
342
+ if (DOMMatrix.isMatrix3(init) || isPlainObject(init)){
343
+ let {a=1, b=0, c=0, d=1, e=0, f=0} = init
344
+ return new DOMMatrix([a, b, c, d, e, f]);
345
+ }
336
346
  throw new TypeError(`Expected DOMMatrix, got: '${init}'`);
337
347
  }
338
348
 
@@ -378,7 +388,7 @@ class DOMMatrix {
378
388
  // (Float64Array) => void
379
389
  constructor(init) {
380
390
 
381
- if (typeof init === "object" && !Array.isArray(init) && !ArrayBuffer.isView(init))
391
+ if (init instanceof DOMMatrix || isPlainObject(init))
382
392
  return DOMMatrix.fromMatrix(init);
383
393
 
384
394
  if (arguments.length > 1)
@@ -437,7 +447,7 @@ class DOMMatrix {
437
447
  setNumber3D(this, M44, init[i]);
438
448
  }
439
449
  else if (init !== undefined) {
440
- throw new TypeError("Expected string or array.");
450
+ throw new TypeError("Expected string, array, or matrix object.");
441
451
  }
442
452
  }
443
453
 
@@ -861,6 +871,9 @@ for (let propertyName of [
861
871
  //
862
872
 
863
873
  function toSkMatrix() {
874
+ if (arguments.length != 1 && arguments.length < 6){
875
+ throw new TypeError("not enough arguments")
876
+ }
864
877
  try {
865
878
  const m = new DOMMatrix(...arguments);
866
879
  return [m.a, m.c, m.e, m.b, m.d, m.f, m.m14, m.m24, m.m44];