@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/LICENSE CHANGED
@@ -19,3 +19,37 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
+
23
+ ---
24
+
25
+ src/svg_doc/broken_image.png is Chromium's broken-image icon
26
+ (third_party/blink/public/default_100_percent/blink/broken_image.png),
27
+ used under the Chromium license:
28
+
29
+ Copyright 2015 The Chromium Authors
30
+
31
+ Redistribution and use in source and binary forms, with or without
32
+ modification, are permitted provided that the following conditions are
33
+ met:
34
+
35
+ * Redistributions of source code must retain the above copyright
36
+ notice, this list of conditions and the following disclaimer.
37
+ * Redistributions in binary form must reproduce the above
38
+ copyright notice, this list of conditions and the following disclaimer
39
+ in the documentation and/or other materials provided with the
40
+ distribution.
41
+ * Neither the name of Google LLC nor the names of its
42
+ contributors may be used to endorse or promote products derived from
43
+ this software without specific prior written permission.
44
+
45
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
46
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
47
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
48
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
49
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
50
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
51
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
52
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
53
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
54
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
55
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # `skr canvas`
2
2
 
3
3
  [![CI](https://github.com/Brooooooklyn/canvas/actions/workflows/CI.yaml/badge.svg)](https://github.com/Brooooooklyn/canvas/actions/workflows/CI.yaml)
4
- ![Skia Version](https://img.shields.io/badge/Skia-chrome%2Fm156-hotpink)
4
+ ![Skia Version](https://img.shields.io/badge/Skia-chrome%2Fm157-hotpink)
5
5
  [![install size](https://packagephobia.com/badge?p=@napi-rs/canvas)](https://packagephobia.com/result?p=@napi-rs/canvas)
6
6
  [![Downloads](https://img.shields.io/npm/dm/@napi-rs/canvas.svg?sanitize=true)](https://npmcharts.com/compare/@napi-rs/canvas?minimal=true)
7
7
 
@@ -92,6 +92,34 @@ main()
92
92
 
93
93
  ![](./example/simple.png)
94
94
 
95
+ ## Image metadata
96
+
97
+ `toBuffer`, `encode` and `encodeSync` can write the resolution (DPI) and a few text fields into PNG and JPEG output. Put them in the options object:
98
+
99
+ ```js
100
+ const png = canvas.toBuffer('image/png', {
101
+ resolution: 300, // pixels per inch, the same option as node-canvas
102
+ title: 'Floor plan',
103
+ author: 'Jane Doe',
104
+ description: 'Ground floor, 1:100',
105
+ })
106
+
107
+ // JPEG: use encode / encodeSync to set the quality as well
108
+ const jpeg = await canvas.encode('jpeg', { quality: 90, resolution: 300, copyright: '© 2026 Jane Doe' })
109
+ ```
110
+
111
+ | Option | PNG | JPEG |
112
+ | ------------- | ------------------ | ---------------------------- |
113
+ | `resolution` | `pHYs` chunk | JFIF density (dots per inch) |
114
+ | `title` | `Title` text chunk | XMP `dc:title` |
115
+ | `author` | `Author` | XMP `dc:creator` |
116
+ | `description` | `Description` | XMP `dc:description` |
117
+ | `copyright` | `Copyright` | XMP `dc:rights` |
118
+ | `software` | `Software` | XMP `xmp:CreatorTool` |
119
+ | `comment` | `Comment` | `COM` segment |
120
+
121
+ WebP, AVIF and GIF output ignore these options. `toDataURL`, `toDataURLAsync`, `toBlob`, `convertToBlob`, `encodeStream` and `savePng` never write metadata. To save a file with metadata, write the Buffer from `toBuffer` or `encode`. With an options object, `toBuffer('image/jpeg', ...)` encodes at quality 92; use `encode` or `encodeSync` for another quality.
122
+
95
123
  ## Emoji text
96
124
 
97
125
  ```js
@@ -121,53 +149,6 @@ writeFileSync(join(__dirname, 'draw-emoji.png'), b)
121
149
 
122
150
  ![](./example/draw-emoji.png)
123
151
 
124
- # Performance
125
-
126
- See [benchmark](./benchmark) for benchmark code.
127
-
128
- Hardware info:
129
-
130
- ```
131
- ,MMMM. Host - xxxxxxxxxxxxxxxxxxxxxxx
132
- .MMMMMM Machine - Mac15,9
133
- MMMMM, Kernel - 24.0.0
134
- .;MMMMM:' MMMMMMMMMM;. OS - macOS 15.0.1 Sequoia
135
- MMMMMMMMMMMMNWMMMMMMMMMMM: DE - Aqua
136
- .MMMMMMMMMMMMMMMMMMMMMMMMWM. WM - Quartz Compositor
137
- MMMMMMMMMMMMMMMMMMMMMMMMM. Packages - 194 (Homebrew), 32 (cargo)
138
- ;MMMMMMMMMMMMMMMMMMMMMMMM: Shell - zsh
139
- :MMMMMMMMMMMMMMMMMMMMMMMM: Terminal - warpterminal (Version v0.2024.10.23.14.49.stable_00)
140
- .MMMMMMMMMMMMMMMMMMMMMMMMM. Resolution - 5120x2880@160fps (as 2560x1440)
141
- MMMMMMMMMMMMMMMMMMMMMMMMMMM. 2992x1934@120fps (as 1496x967)
142
- .MMMMMMMMMMMMMMMMMMMMMMMMMM. 2232x1512@60fps (as 1116x756)
143
- MMMMMMMMMMMMMMMMMMMMMMMM Uptime - 1d 2h 32m
144
- ;MMMMMMMMMMMMMMMMMMMM. CPU - Apple M3 Max (16)
145
- .MMMM,. .MMMM,. CPU Load - 16%
146
- Memory - 50.1 GB / 134.2 GB
147
- Battery - 78% & Discharging
148
- Disk Space - 624.0 GB / 994.7 GB
149
- ```
150
-
151
- ```
152
- ❯ yarn bench
153
- Draw a House and export to PNG
154
- ┌─────────┬─────────────────┬───────────────────────┬──────────────────────────┬────────────────────────────┬───────────────────────────┬─────────┐
155
- │ (index) │ Task name │ Latency average (ns) │ Latency median (ns) │ Throughput average (ops/s) │ Throughput median (ops/s) │ Samples │
156
- ├─────────┼─────────────────┼───────────────────────┼──────────────────────────┼────────────────────────────┼───────────────────────────┼─────────┤
157
- │ 0 │ '@napi-rs/skia' │ '14676992.14 ± 0.68%' │ '14602333.00' │ '68 ± 0.59%' │ '68' │ 69 │
158
- │ 1 │ 'skia-canvas' │ '21167809.17 ± 2.05%' │ '20960021.00 ± 13646.00' │ '47 ± 1.31%' │ '48' │ 64 │
159
- │ 2 │ 'node-canvas' │ '16552027.42 ± 0.70%' │ '16451291.50 ± 2208.50' │ '60 ± 0.62%' │ '61' │ 64 │
160
- └─────────┴─────────────────┴───────────────────────┴──────────────────────────┴────────────────────────────┴───────────────────────────┴─────────┘
161
- Draw Gradient and export to PNG
162
- ┌─────────┬─────────────────┬───────────────────────┬─────────────────────────┬────────────────────────────┬───────────────────────────┬─────────┐
163
- │ (index) │ Task name │ Latency average (ns) │ Latency median (ns) │ Throughput average (ops/s) │ Throughput median (ops/s) │ Samples │
164
- ├─────────┼─────────────────┼───────────────────────┼─────────────────────────┼────────────────────────────┼───────────────────────────┼─────────┤
165
- │ 0 │ '@napi-rs/skia' │ '15228495.58 ± 0.53%' │ '15146312.50 ± 1187.50' │ '66 ± 0.48%' │ '66' │ 66 │
166
- │ 1 │ 'skia-canvas' │ '21725564.41 ± 2.20%' │ '21412520.50 ± 2104.50' │ '46 ± 1.39%' │ '47' │ 64 │
167
- │ 2 │ 'node-canvas' │ '17976022.14 ± 1.53%' │ '17563479.50 ± 5104.50' │ '56 ± 1.38%' │ '57' │ 64 │
168
- └─────────┴─────────────────┴───────────────────────┴─────────────────────────┴────────────────────────────┴───────────────────────────┴─────────┘
169
- ```
170
-
171
152
  # Features
172
153
 
173
154
  ## Path2D
@@ -411,7 +392,7 @@ $ node scripts/build-skia.js
411
392
 
412
393
  # Install NPM packages, build the Node.js addon:
413
394
  $ npm install -g yarn
414
- $ yarn install --mode=skip-build # Here are modules that are used for benchmarking and are hard to install, you can skip it by specifying `--mode=skip-build`
395
+ $ yarn install --mode=skip-build # `canvas` (node-canvas) is a native module that only the node-canvas compat tests use and is hard to install, you can skip building it by specifying `--mode=skip-build`
415
396
  $ sudo dnf install clang # https://fedora.pkgs.org/34/fedora-x86_64/clang-12.0.0-0.3.rc1.fc34.x86_64.rpm.html
416
397
  $ yarn build
417
398
 
package/geometry.js CHANGED
@@ -26,20 +26,17 @@ class DOMPoint {
26
26
  )
27
27
  }
28
28
 
29
+ // matrixTransform(optional DOMMatrixInit matrix = {})
29
30
  matrixTransform(matrix) {
30
- if ((matrix.is2D || matrix instanceof SVGMatrix) && this.z === 0 && this.w === 1) {
31
- return new DOMPoint(
32
- this.x * matrix.a + this.y * matrix.c + matrix.e,
33
- this.x * matrix.b + this.y * matrix.d + matrix.f,
34
- 0,
35
- 1,
36
- )
31
+ const { values: m, is2D } = matrixFromInit(matrix, MATRIX_TRANSFORM)
32
+ if (is2D && this.z === 0 && this.w === 1) {
33
+ return new DOMPoint(this.x * m[M11] + this.y * m[M21] + m[M41], this.x * m[M12] + this.y * m[M22] + m[M42], 0, 1)
37
34
  } else {
38
35
  return new DOMPoint(
39
- this.x * matrix.m11 + this.y * matrix.m21 + this.z * matrix.m31 + this.w * matrix.m41,
40
- this.x * matrix.m12 + this.y * matrix.m22 + this.z * matrix.m32 + this.w * matrix.m42,
41
- this.x * matrix.m13 + this.y * matrix.m23 + this.z * matrix.m33 + this.w * matrix.m43,
42
- this.x * matrix.m14 + this.y * matrix.m24 + this.z * matrix.m34 + this.w * matrix.m44,
36
+ this.x * m[M11] + this.y * m[M21] + this.z * m[M31] + this.w * m[M41],
37
+ this.x * m[M12] + this.y * m[M22] + this.z * m[M32] + this.w * m[M42],
38
+ this.x * m[M13] + this.y * m[M23] + this.z * m[M33] + this.w * m[M43],
39
+ this.x * m[M14] + this.y * m[M24] + this.z * m[M34] + this.w * m[M44],
43
40
  )
44
41
  }
45
42
  }
@@ -211,6 +208,152 @@ const newInstance = (values) => {
211
208
  return instance
212
209
  }
213
210
 
211
+ // DOMMatrixInit dictionaries (Geometry Interfaces 1: "validate and fixup",
212
+ // "create a DOMMatrix from the dictionary"), with Chrome's errors.
213
+ //
214
+ // matrixFromInit(init, prefix)
215
+ // DOMMatrix -> its values and is2D
216
+ // undefined / null -> {}
217
+ // other non-object -> TypeError "not of type 'DOMMatrixInit'"
218
+ // object -> read members (toMatrixInit), validate and fix up
219
+
220
+ const FROM_MATRIX = "Failed to execute 'fromMatrix' on 'DOMMatrix': "
221
+ const MULTIPLY = "Failed to execute 'multiply' on 'DOMMatrixReadOnly': "
222
+ const MULTIPLY_SELF = "Failed to execute 'multiplySelf' on 'DOMMatrix': "
223
+ const PRE_MULTIPLY_SELF = "Failed to execute 'preMultiplySelf' on 'DOMMatrix': "
224
+ const MATRIX_TRANSFORM = "Failed to execute 'matrixTransform' on 'DOMPointReadOnly': "
225
+
226
+ // The members in the order Chrome reads them: the inherited dictionary's
227
+ // first, each dictionary's in lexicographic order.
228
+ const MATRIX_2D_INIT_MEMBERS = ['a', 'b', 'c', 'd', 'e', 'f', 'm11', 'm12', 'm21', 'm22', 'm41', 'm42']
229
+ const MATRIX_INIT_MEMBERS = ['m13', 'm14', 'm23', 'm24', 'm31', 'm32', 'm33', 'm34', 'm43', 'm44']
230
+ // [alias, member, default]: validate and fixup (2D).
231
+ const MATRIX_2D_ALIASES = [
232
+ ['a', 'm11', 1],
233
+ ['b', 'm12', 0],
234
+ ['c', 'm21', 0],
235
+ ['d', 'm22', 1],
236
+ ['e', 'm41', 0],
237
+ ['f', 'm42', 0],
238
+ ]
239
+
240
+ const isObject = (value) => value !== null && (typeof value === 'object' || typeof value === 'function')
241
+
242
+ // ECMAScript ToNumber, written out so that the errors it throws itself can
243
+ // get Chrome's context (`fail`), while the errors thrown by user code
244
+ // (valueOf, toString, @@toPrimitive) pass through unchanged, as in Chrome.
245
+ // V8's messages; an object in a message is shown as #<Object>.
246
+ function toNumber(value, fail) {
247
+ if (!isObject(value)) {
248
+ if (typeof value === 'bigint') throw fail('Cannot convert a BigInt value to a number')
249
+ if (typeof value === 'symbol') throw fail('Cannot convert a Symbol value to a number')
250
+ return +value
251
+ }
252
+ const toPrimitive = value[Symbol.toPrimitive]
253
+ if (toPrimitive !== undefined && toPrimitive !== null) {
254
+ if (typeof toPrimitive !== 'function') {
255
+ const shown = isObject(toPrimitive) ? '#<Object>' : String(toPrimitive)
256
+ throw fail(
257
+ `'${shown}' returned for property 'Symbol(Symbol.toPrimitive)' of object '#<Object>' is not a function`,
258
+ )
259
+ }
260
+ const primitive = toPrimitive.call(value, 'number')
261
+ if (isObject(primitive)) throw fail('Cannot convert object to primitive value')
262
+ return toNumber(primitive, fail)
263
+ }
264
+ for (const name of ['valueOf', 'toString']) {
265
+ const method = value[name]
266
+ if (typeof method === 'function') {
267
+ const primitive = method.call(value)
268
+ if (!isObject(primitive)) return toNumber(primitive, fail)
269
+ }
270
+ }
271
+ throw fail('Cannot convert object to primitive value')
272
+ }
273
+
274
+ // WebIDL conversion to a DOMMatrixInit: the present members only.
275
+ function toMatrixInit(init, prefix) {
276
+ const dict = Object.create(null)
277
+ if (init === undefined || init === null) return dict
278
+ if (!isObject(init)) throw new TypeError(`${prefix}The provided value is not of type 'DOMMatrixInit'.`)
279
+ const read = (key, dictionary) => {
280
+ const value = init[key]
281
+ if (value !== undefined) {
282
+ dict[key] = toNumber(
283
+ value,
284
+ (msg) => new TypeError(`${prefix}Failed to read the '${key}' property from '${dictionary}': ${msg}`),
285
+ )
286
+ }
287
+ }
288
+ for (const key of MATRIX_2D_INIT_MEMBERS) read(key, 'DOMMatrix2DInit')
289
+ const is2D = init.is2D
290
+ if (is2D !== undefined) dict.is2D = Boolean(is2D)
291
+ for (const key of MATRIX_INIT_MEMBERS) read(key, 'DOMMatrixInit')
292
+ return dict
293
+ }
294
+
295
+ const sameValueZero = (x, y) => x === y || (x !== x && y !== y)
296
+
297
+ // { values, is2D } of the matrix `init` describes. `values` may be the
298
+ // DOMMatrix's own array: read it, do not write it.
299
+ function matrixFromInit(init, prefix) {
300
+ if (init instanceof DOMMatrix && init[VALUES] instanceof Float64Array) {
301
+ return { values: init[VALUES], is2D: init[IS_2D] }
302
+ }
303
+ const dict = toMatrixInit(init, prefix)
304
+ // validate and fixup (2D)
305
+ for (const [alias, member] of MATRIX_2D_ALIASES) {
306
+ if (alias in dict && member in dict && !sameValueZero(dict[alias], dict[member])) {
307
+ throw new TypeError(`${prefix}Property mismatch on matrix initialization.`)
308
+ }
309
+ }
310
+ for (const [alias, member, defaultValue] of MATRIX_2D_ALIASES) {
311
+ if (!(member in dict)) dict[member] = alias in dict ? dict[alias] : defaultValue
312
+ }
313
+ // validate and fixup: the members default to the identity.
314
+ const value = (key) => (key in dict ? dict[key] : key === 'm33' || key === 'm44' ? 1 : 0)
315
+ const is3D =
316
+ ['m13', 'm14', 'm23', 'm24', 'm31', 'm32', 'm34', 'm43'].some((key) => value(key) !== 0) ||
317
+ value('m33') !== 1 ||
318
+ value('m44') !== 1
319
+ if (dict.is2D === true && is3D) {
320
+ throw new TypeError(`${prefix}The is2D member is set to true but the input matrix is a 3d matrix.`)
321
+ }
322
+ const is2D = dict.is2D === undefined ? !is3D : dict.is2D
323
+ const { m11, m12, m21, m22, m41, m42 } = dict
324
+ const values = is2D
325
+ ? [m11, m12, 0, 0, m21, m22, 0, 0, 0, 0, 1, 0, m41, m42, 0, 1]
326
+ : [
327
+ m11,
328
+ m12,
329
+ value('m13'),
330
+ value('m14'),
331
+ m21,
332
+ m22,
333
+ value('m23'),
334
+ value('m24'),
335
+ value('m31'),
336
+ value('m32'),
337
+ value('m33'),
338
+ value('m34'),
339
+ m41,
340
+ m42,
341
+ value('m43'),
342
+ value('m44'),
343
+ ]
344
+ return { values: new Float64Array(values), is2D }
345
+ }
346
+
347
+ // `target` times (post) or after (pre) the matrix `other` describes.
348
+ function multiplyByInit(target, other, prefix, post) {
349
+ const { values, is2D } = matrixFromInit(other, prefix)
350
+ target[VALUES] = post ? multiply(values, target[VALUES]) : multiply(target[VALUES], values)
351
+ if (!is2D) {
352
+ target[IS_2D] = false
353
+ }
354
+ return target
355
+ }
356
+
214
357
  const multiply = (first, second) => {
215
358
  const dest = new Float64Array(16)
216
359
 
@@ -391,14 +534,13 @@ class DOMMatrix {
391
534
  )
392
535
  }
393
536
 
537
+ // fromMatrix(optional DOMMatrixInit other = {})
394
538
  static fromMatrix(init) {
395
- if (init instanceof DOMMatrix) {
396
- return new DOMMatrix(init[VALUES])
397
- } else if (init instanceof SVGMatrix) {
398
- return new DOMMatrix([init.a, init.b, init.c, init.d, init.e, init.f])
399
- } else {
400
- throw new TypeError('Expected DOMMatrix')
401
- }
539
+ const { values, is2D } = matrixFromInit(init, FROM_MATRIX)
540
+ const matrix = new DOMMatrix()
541
+ matrix[VALUES] = new Float64Array(values)
542
+ matrix[IS_2D] = is2D
543
+ return matrix
402
544
  }
403
545
 
404
546
  static fromFloat32Array(init) {
@@ -513,28 +655,19 @@ class DOMMatrix {
513
655
  }
514
656
  }
515
657
 
658
+ // multiply, multiplySelf, preMultiplySelf(optional DOMMatrixInit other = {})
516
659
  multiply(other) {
517
- return newInstance(this[VALUES]).multiplySelf(other)
660
+ const result = newInstance(this[VALUES])
661
+ result[IS_2D] = this[IS_2D]
662
+ return multiplyByInit(result, other, MULTIPLY, true)
518
663
  }
519
664
 
520
665
  multiplySelf(other) {
521
- this[VALUES] = multiply(other[VALUES], this[VALUES])
522
-
523
- if (!other.is2D) {
524
- this[IS_2D] = false
525
- }
526
-
527
- return this
666
+ return multiplyByInit(this, other, MULTIPLY_SELF, true)
528
667
  }
529
668
 
530
669
  preMultiplySelf(other) {
531
- this[VALUES] = multiply(this[VALUES], other[VALUES])
532
-
533
- if (!other.is2D) {
534
- this[IS_2D] = false
535
- }
536
-
537
- return this
670
+ return multiplyByInit(this, other, PRE_MULTIPLY_SELF, false)
538
671
  }
539
672
 
540
673
  translate(tx, ty, tz) {