skia-canvas 3.0.8 → 4.0.0-rc2

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/README.md CHANGED
@@ -18,200 +18,58 @@
18
18
 
19
19
  <div align="center">
20
20
 
21
- ### [Version 3.0 now available](https://github.com/samizdatco/skia-canvas/discussions/255)
21
+ <!--### [Version 3.0 now available](https://github.com/samizdatco/skia-canvas/discussions/255)-->
22
22
 
23
23
  </div>
24
24
 
25
25
  ---
26
26
 
27
- Skia Canvas is a Node.js implementation of the HTML Canvas drawing [API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) for both on- and off-screen rendering. Since it uses Google’s [Skia](https://skia.org) graphics engine, its output is very similar to Chrome’s [`<canvas>`](https://html.spec.whatwg.org/multipage/canvas.html) element — though it's also capable of things the browser’s Canvas still can't achieve.
27
+ Skia Canvas is an implementation of the [HTML Canvas](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) drawing API that runs in [Node.js](https://nodejs.org/en) on Mac, Linux, and Windows systems. Depending on your needs, you can use it as:
28
+ 1. **A spec-compliant offscreen canvas:** it accepts the same drawing code you'd write for a browser but can run on servers and in other ‘headless’ contexts to generate image files and buffers.
29
+ 2. **A windowing toolkit:** it can open native [windows][window] on macOS, Windows, and Linux with [display-synced][win_animation] drawing and browser-inspired [event handling][win_events].
30
+ 3. **A JavaScript interface for the Skia graphics library:** it uses familiar web APIs as a front-end to Google’s sophisticated [imaging engine](https://skia.org), rendering with high-performance native code (and optional GPU acceleration).
28
31
 
29
- In particular, Skia Canvas:
30
32
 
31
- - generates images in vector (PDF & SVG) as well as bitmap (JPEG, PNG, & WEBP) formats
32
- - can draw to interactive GUI [windows][window] and provides a browser-like [event][win_bind] framework
33
- - can save images to [files][toFile], encode to [dataURL][toURL] strings, and return [Buffers][toBuffer] or [Sharp][sharp] objects
34
- - uses native threads in a [user-configurable][multithreading] worker pool for asynchronous rendering and file I/O
35
- - can create [multiple ‘pages’][newPage] on a given canvas and then [output][toFile] them as a single, multi-page PDF or an image-sequence saved to multiple files
36
- - can [simplify][p2d_simplify], [blunt][p2d_round], [combine][bool-ops], [excerpt][p2d_trim], and [atomize][p2d_points] Bézier paths using [efficient](https://www.youtube.com/watch?v=OmfliNQsk88) boolean operations or point-by-point [interpolation][p2d_interpolate]
37
- - provides [3D perspective][createProjection()] transformations in addition to [scaling][scale()], [rotation][rotate()], and [translation][translate()]
38
- - can fill shapes with vector-based [Textures][createTexture()] in addition to bitmap-based [Patterns][createPattern()] and supports line-drawing with custom [markers][lineDashMarker]
39
- - supports the full set of [CSS filter][filter] image processing operators
40
- - offers rich typographic control including:
33
+
34
+ ### A More Capable Canvas
35
+
36
+ In addition to being a faithful emulation of the [canvas standard](https://html.spec.whatwg.org/multipage/canvas.html), Skia Canvas includes a raft of extensions, adding 2D capabilities that reach well beyond what the browser’s `<canvas>` can do.
37
+
38
+ In particular, Skia Canvas can:
39
+
40
+ - generate images in vector (PDF & SVG) as well as bitmap (JPEG, PNG, WEBP, & RAW) formats
41
+ - save images to [files][toFile], encode to [dataURL][toURL] strings, and return [Buffers][toBuffer] or [Sharp][sharp] objects
42
+ - create [multiple ‘pages’][newPage] on a given canvas and [output][toFile] them as a multi-page PDF or an image-sequence saved to multiple files
43
+ - load [PDFs & SVGs][loadimage] as scalable vector images or open a [multi-page PDF][loadcanvas] as an editable canvas
44
+ - render in wide-gamut Display P3 color with [CSS Color 4][ctx_colors] syntax support
45
+ - [slice][p2d_slice] & [sample][p2d_points] Path2D objects, combine them with [boolean operators][bool-ops], and decompose them into [contours][p2d_contours], [verbs][edges], or [points][p2d_positionAt]
46
+ - transform coordinates using [3D perspective][createProjection()] in addition to [scaling][scale()], [rotation][rotate()], and [translation][translate()]
47
+ - fill paths with vector-based [Textures][createTexture()] or bitmap [Patterns][createPattern()] and draw strokes with custom [markers][lineDashMarker]
48
+ - apply the full set of [CSS filter][filter] image processing operators
49
+ - provide rich typographic control including:
41
50
  - multi-line, [word-wrapped][textwrap] text
42
51
  - line-by-line [text metrics][c2d_measuretext]
43
- - small-caps, ligatures, and other opentype features accessible using standard [font-variant][fontvariant] syntax
52
+ - small-caps, ligatures, and other [opentype features][fontfeatures] accessible using standard [font-variant][fontvariant] syntax
44
53
  - proportional [letter-spacing][letterSpacing], [word-spacing][wordSpacing], and [leading][c2d_font]
45
- - support for [variable fonts][VariableFonts] and transparent mapping of weight values
54
+ - support for [variable fonts][VariableFonts] and automatic use of weight, width, and optical-sizing axes
46
55
  - use of non-system fonts [loaded][fontlibrary-use] from local files
47
- - can be used for server-side image rendering on standard Linux hosts and ‘serverless’ platforms like Vercel and AWS Lambda
56
+ - use native threads in a [user-configurable][multithreading] worker pool for asynchronous rendering and file I/O
57
+ - render images server-side on standard Linux hosts and ‘serverless’ platforms like Vercel, Cloudflare Containers, and AWS Lambda
48
58
 
49
- ## Installation
59
+ ## Installing Skia Canvas
50
60
 
51
61
  If you’re running on a supported platform, installation should be as simple as:
52
- ```bash
53
- npm install skia-canvas
54
- ```
55
-
56
- This will download a pre-compiled library from the project’s most recent [release](https://github.com/samizdatco/skia-canvas/releases).
57
-
58
-
59
- ### `pnpm`
60
- If you use the `pnpm` package manager, it will not download `skia-canvas`'s platform-native binary unless you explicitly allow it. You can do this interactively via the ‘approve builds’ command (note that you need to press `<space>` to toggle the selection and then `<enter>` to proceed):
61
-
62
- ```bash
63
- pnpm install skia-canvas
64
- pnpm approve-builds
65
- ```
66
- In non-interactive scenarios (like building via CI), you can approve the build step when you add `skia-canvas` to your project:
67
-
68
- ```bash
69
- pnpm install skia-canvas --allow-build=skia-canvas
70
- ```
71
-
72
- Alternatively, you can add a [`pnpm.onlyBuiltDependencies`](https://pnpm.io/9.x/package_json#pnpmonlybuiltdependencies) entry to your `package.json` file to mark the build-step as allowed:
73
- ```json
74
- {
75
- "pnpm": {
76
- "onlyBuiltDependencies": ["skia-canvas"]
77
- }
78
- }
79
- ```
80
-
81
-
82
-
83
- ## Platform Support
84
-
85
- Skia Canvas runs on Linux, macOS, or Windows as well as serverless platforms like Vercel and AWS Lambda. Precompiled versions of the library’s native code will be automatically downloaded in the appropriate architecture (`arm64` or `x64`) when you install it via npm.
86
-
87
- The underlying Rust library uses [N-API][node_napi] v8 which allows it to run on all [currently supported](https://nodejs.org/en/about/previous-releases) Node.js releases, and it is backward compatible with versions going back to v12.22+, v14.17+, v15.12+, and v16+.
88
-
89
- ### Linux
90
-
91
- The library is compatible with Linux systems using [glibc](https://www.gnu.org/software/libc/) 2.28 or later as well as Alpine Linux and the [musl](https://musl.libc.org) C library it favors. It will make use of the system’s `fontconfig` settings in `/etc/fonts` if they exist but will otherwise fall back to using a [placeholder configuration](https://github.com/samizdatco/skia-canvas/blob/main/lib/fonts/fonts.conf), looking for installed fonts at commonly used Linux paths.
92
-
93
- ### Docker
94
-
95
- If you are setting up a [Dockerfile](https://nodejs.org/en/docs/guides/nodejs-docker-webapp/) that uses [`node`](https://hub.docker.com/_/node) as its basis, the simplest approach is to set your `FROM` image to one of the (Debian-derived) defaults like `node:lts`, `node:22`, `node:24-bookworm`, or simply:
96
- ```dockerfile
97
- FROM node
98
- ```
99
-
100
- If you wish to use Alpine as the underlying distribution, you can start with something along the lines of:
101
-
102
- ```dockerfile
103
- FROM node:alpine
104
- ```
105
-
106
- ### AWS Lambda
107
-
108
- Skia Canvas depends on libraries that aren't present in the standard Lambda [runtime](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html). You can add these to your function by uploading a ‘[layer](https://docs.aws.amazon.com/lambda/latest/dg/chapter-layers.html)’ (a zip file containing the required libraries and `node_modules` directory) and configuring your function to use it.
109
-
110
-
111
- <details><summary>
112
-
113
- **Detailed AWS instructions**
114
-
115
- </summary>
116
-
117
- #### Adding the Skia Canvas layer to your AWS account
118
-
119
- 1. Look in the **Assets** section of Skia Canvas’s [current release](https://github.com/samizdatco/skia-canvas/releases/latest) and download the `aws-lambda-x64.zip` or `aws-lambda-arm64.zip` file (depending on your architecture) but don’t decompress it
120
- 2. Go to the AWS Lambda [Layers console](https://console.aws.amazon.com/lambda/home/#/layers) and click the **Create Layer** button, then fill in the fields:
121
- - **Name**: `skia-canvas` (or whatever you want)
122
- - **Description**: you might want to note the Skia Canvas version here
123
- - **Compatible architectures**: select **x86_64** or **arm64** depending on which zip you chose
124
- - **Compatible runtimes**: select **Node.js 22.x** (and/or 20.x)
125
- 3. Click the **Choose file** button and select the zip file you downloaded in Step 1, then click **Create**
126
-
127
- Alternatively, you can use the [`aws` command line tool](https://github.com/aws/aws-cli) to create the layer. This bash script will fetch the skia-canvas version of your choice and make it available to your Lambda functions.
128
- ```sh
129
- #!/usr/bin/env bash
130
- VERSION=3.0 # the skia-canvas version to include
131
- PLATFORM=arm64 # arm64 or x64
132
-
133
- curl -sLO https://github.com/samizdatco/skia-canvas/releases/download/v${VERSION}/aws-lambda-${PLATFORM}.zip
134
- aws lambda publish-layer-version \
135
- --layer-name "skia-canvas" \
136
- --description "Skia Canvas ${VERSION} layer" \
137
- --zip-file "fileb://aws-lambda-${PLATFORM}.zip" \
138
- --compatible-runtimes "nodejs20.x" "nodejs22.x" \
139
- --compatible-architectures "${X/#x/x86_}"
140
- ```
141
-
142
- #### Using the layer in a Lambda function
143
62
 
144
- You can now use this layer in any function you create in the [Functions console](https://console.aws.amazon.com/lambda/home/#/functions). After creating a new function, click the **Add a Layer** button and you can select your newly created Skia Canvas layer from the **Custom Layers** layer source.
145
-
146
- Note that the layer only includes Skia Canvas and its dependencies—any other npm modules you want to use will need to be bundled into your function. To prevent the `skia-canvas` module from being doubly-included, make sure you add it to the `devDependencies` section (**not** the regular `dependencies` section) of your package.json file.
147
-
148
- </details>
149
-
150
-
151
- ### Next.js / Webpack
152
-
153
- If you are using a framework like Next.js that bundles your server-side code with Webpack, you'll need to mark `skia-canvas` as an ‘external’, otherwise its platform-native binary file will be excluded from the final build. Try adding these options to your `next.config.ts` file:
154
-
155
- ```js
156
- const nextConfig: NextConfig = {
157
- serverExternalPackages: ['skia-canvas'],
158
- webpack: (config, options) => {
159
- if (options.isServer){
160
- config.externals = [
161
- ...config.externals,
162
- {'skia-canvas': 'commonjs skia-canvas'},
163
- ]
164
- }
165
- return config
166
- }
167
- };
168
- ```
169
-
170
-
171
- ## Compiling from Source
172
-
173
- If prebuilt binaries aren’t available for your system you’ll need to compile the portions of this library that directly interface with Skia.
174
-
175
- Start by installing:
176
-
177
- 1. A recent version of `git` (older versions have difficulties with Skia's submodules)
178
- 2. The [Rust compiler](https://www.rust-lang.org/tools/install) and cargo package manager using [`rustup`](https://rust-lang.github.io/rustup/)
179
- 3. A C compiler toolchain (either LLVM/Clang or MSVC)
180
- 4. Python 3 (used by Skia's [build process](https://skia.org/docs/user/build/))
181
- 5. The [Ninja](https://ninja-build.org) build system
182
- 6. On Linux: Fontconfig and OpenSSL
183
-
184
- [Detailed instructions](https://github.com/rust-skia/rust-skia#building) for setting up these dependencies on different operating systems can be found in the ‘Building’ section of the Rust Skia documentation. The Dockerfiles in the [containers](https://github.com/samizdatco/skia-canvas/tree/main/containers) directory may also be useful for identifying needed dependencies. Once all the necessary compilers and libraries are present, running `npm run build` will give you a usable library (after a fairly lengthy compilation process).
185
-
186
- ## Global Settings
187
-
188
- > There are a handful of settings that can only be configured at launch and will apply to all the canvases you create in your script. The sections below describe the different [environment variables][node_env] you can set to make global changes. You can either set them as part of your command line invocation, or place them in a `.env` file in your project directory and use Node 20's [`--env-file` argument][node_env_arg] to load them all at once.
189
-
190
- ### Multithreading
191
-
192
- When rendering canvases in the background (e.g., by using the asynchronous [toFile][toFile] or [toBuffer][toBuffer] methods), tasks are spawned in a thread pool managed by the [rayon][rayon] library. By default it will create up to as many threads as your CPU has cores. You can see this default value by inspecting any [Canvas][canvas] object's [`engine.threads`][engine] property. If you wish to override this default, you can set the `SKIA_CANVAS_THREADS` environment variable to your preferred value.
193
-
194
- For example, you can limit your asynchronous processing to two simultaneous tasks by running your script with:
195
63
  ```bash
196
- SKIA_CANVAS_THREADS=2 node my-canvas-script.js
197
- ```
198
-
199
- ### Argument Validation
200
-
201
- There are a number of situations where the browser API will react to invalid arguments by silently ignoring the method call rather than throwing an error. For example, these lines will simply have no effect:
202
-
203
- ```js
204
- ctx.fillRect(0, 0, 100, "october")
205
- ctx.lineTo(NaN, 0)
64
+ npm install skia-canvas
206
65
  ```
207
66
 
208
-
209
- Skia Canvas does its best to emulate these quirks, but allows you to opt into a stricter mode in which it will throw TypeErrors in these situations (which can be useful for debugging).
210
-
211
- Set the `SKIA_CANVAS_STRICT` environment variable to `1` or `true` to enable this mode.
67
+ For detailed [installation][installation] instructions and runtime [configuration][global_settings] options, take a look at the [Getting Started][getting_started] page.
212
68
 
213
69
  ## Example Usage
214
70
 
71
+ Skia Canvas's classes and extensions to the standard are extensively covered in the [API Documentation][api_docs]. But to give you a sense of some of things you can achieve with it, here are some real-world examples:
72
+
215
73
  ### Generating image files
216
74
 
217
75
  ```js
@@ -221,6 +79,7 @@ let canvas = new Canvas(400, 400),
221
79
  ctx = canvas.getContext("2d"),
222
80
  {width, height} = canvas;
223
81
 
82
+ // draw an empty box with a gradient at its edges
224
83
  let sweep = ctx.createConicGradient(Math.PI * 1.2, width/2, height/2)
225
84
  sweep.addColorStop(0, "red")
226
85
  sweep.addColorStop(0.25, "orange")
@@ -231,32 +90,27 @@ ctx.strokeStyle = sweep
231
90
  ctx.lineWidth = 100
232
91
  ctx.strokeRect(100,100, 200,200)
233
92
 
234
- // render to multiple destinations using a background thread
235
- async function render(){
236
- // save a ‘retina’ image...
237
- await canvas.saveAs("rainbox.png", {density:2})
238
- // ...or use a shorthand for canvas.toBuffer("png")
239
- let pngData = await canvas.png
240
- // ...or embed it in a string
241
- let pngEmbed = `<img src="${await canvas.toDataURL("png")}">`
242
- }
243
- render()
93
+ // render to multiple destinations using a background thread...
94
+ await canvas.toFile("rainbox.png", {density:2}) // save a ‘retina’ image
95
+ let pngData = await canvas.png // use a shorthand for canvas.toBuffer("png")
96
+ let pngEmbed = `<img src="${await canvas.toURL("png")}">` // embed it in a string
244
97
 
245
98
  // ...or save the file synchronously from the main thread
246
- canvas.saveAsSync("rainbox.pdf")
99
+ canvas.toFileSync("rainbox.pdf")
247
100
  ```
101
+ &nbsp;&nbsp;[*rainbox.png* 👁️](docs/assets/examples/generating-image-files@2x.png)
248
102
 
249
103
  ### Multi-page sequences
250
104
 
251
105
  ```js
252
- import {Canvas} from 'skia-canvas'
106
+ import {Canvas, loadCanvas} from 'skia-canvas'
253
107
 
254
108
  let canvas = new Canvas(400, 400),
255
- ctx = canvas.getContext("2d"),
109
+ ctx = canvas.getContext("2d"), // leave first page blank
256
110
  {width, height} = canvas
257
111
 
258
112
  for (const color of ['orange', 'yellow', 'green', 'skyblue', 'purple']){
259
- ctx = canvas.newPage()
113
+ ctx = canvas.newPage() // add pages 2–6
260
114
  ctx.fillStyle = color
261
115
  ctx.fillRect(0,0, width, height)
262
116
  ctx.fillStyle = 'white'
@@ -264,15 +118,20 @@ for (const color of ['orange', 'yellow', 'green', 'skyblue', 'purple']){
264
118
  ctx.fill()
265
119
  }
266
120
 
267
- async function render(){
268
- // save to a multi-page PDF file
269
- await canvas.saveAs("all-pages.pdf")
121
+ await canvas.toFile("page-{2}.png") // save to files named `page-01.png`, `page-02.png`, etc.
122
+ await canvas.toFile("all-pages.pdf") // save to a single multi-page PDF file
270
123
 
271
- // save to files named `page-01.png`, `page-02.png`, etc.
272
- await canvas.saveAs("page-{2}.png")
124
+ // the multi-page PDF can be read back in and even drawn upon
125
+ let multipage = await loadCanvas("all-pages.pdf")
126
+ for (let [i, pg] of multipage.pages.entries()){
127
+ pg.font = 'italic 12px serif'
128
+ pg.textAlign = 'center'
129
+ pg.textBaseline = 'middle'
130
+ pg.fillText(`p. ${i+1}`, multipage.width/2, multipage.height/2)
273
131
  }
274
- render()
132
+ await multipage.toFile("all-pages-labeled.pdf")
275
133
  ```
134
+ &nbsp;&nbsp;[*all-pages-labeled.pdf* 👁️](docs/assets/examples/multi-page-sequences.pdf)
276
135
 
277
136
  ### Rendering to a window
278
137
 
@@ -294,6 +153,51 @@ win.on("draw", e => {
294
153
  ctx.fill()
295
154
  })
296
155
  ```
156
+ &nbsp;&nbsp;[*screenshot* 👁️](docs/assets/examples/rendering-to-a-window@2x.png)
157
+
158
+
159
+ ### Wide-gamut colors
160
+
161
+ ```js
162
+ import {Canvas} from 'skia-canvas'
163
+
164
+ let pad = 16, size = 64, width = 4*size + 3*pad,
165
+ canvas = new Canvas(336, 240),
166
+ ctx = canvas.getContext("2d", {colorSpace:"display-p3"})
167
+
168
+ // CSS Color 4 syntax is supported everywhere (and colors can exceed the sRGB gamut)
169
+ for (let [p3, srgb] of [
170
+ ["color(display-p3 1 0 0)", "#ff0000"], ["lch(75% 100 150)", "#00dc51"],
171
+ ["lch(85% 80 170)", "#00f8b6"], ["color(display-p3 0 1 1)", "#00ffff"]
172
+ ]){
173
+ ctx.fillStyle = p3 // wide gamut color
174
+ ctx.fillRect(pad, pad, size, size/2)
175
+ ctx.fillStyle = srgb // nearest sRGB equivalent
176
+ ctx.fillRect(pad, pad + size/2, size, size/2)
177
+ ctx.translate(size + pad, 0)
178
+ }
179
+ ctx.translate(-width, pad + size)
180
+
181
+ // gradients can select the color space used for interpolation
182
+ for (let {space, from, to, hue} of [
183
+ {space:"srgb", from:"navy", to:"gold"}, // perceptual midpoint is off-center
184
+ {space:"oklab", from:"navy", to:"gold"}, // Oklab stays perceptually uniform
185
+ {space:"oklch", from:"red", to:"red", hue:"longer"}, // full 360° from a single hue
186
+ ]){
187
+ let ramp = ctx.createLinearGradient(0, pad, width, pad)
188
+ if (hue) ramp.hueInterpolationMethod = hue // only applies to angle-based spaces
189
+ ramp.colorInterpolationMethod = space
190
+ ramp.addColorStop(0, from)
191
+ ramp.addColorStop(1, to)
192
+ ctx.fillStyle = ramp
193
+ ctx.fillRect(0, pad, width, size/2)
194
+ ctx.translate(0, pad + size/2)
195
+ }
196
+
197
+ await canvas.toFile("test-pattern.png")
198
+ ```
199
+
200
+ &nbsp;&nbsp;[*test-pattern.png* 👁️](docs/assets/examples/wide-gamut-colors@2x.png)
297
201
 
298
202
  ### Integrating with [Sharp.js][sharp]
299
203
 
@@ -322,58 +226,59 @@ await imgData.toSharp().grayscale().png().toFile("black-and-white.png")
322
226
  let sharpImage = sharp({create:{ width:x, height:y, channels:4, background:"skyblue" }})
323
227
  let canvasImage = await loadImage(sharpImage)
324
228
  ctx.drawImage(canvasImage, x, 0)
325
- await canvas.saveAs('mosaic.png')
229
+ await canvas.toFile('mosaic.png')
326
230
  ```
327
231
 
232
+ &nbsp;&nbsp;[*sharp exports* 👁️](docs/assets/examples/integrating-with-sharp@2x.png)
233
+
328
234
  ## Benchmarks
329
235
  In these benchmarks, Skia Canvas is tested running in two modes: serial and async. When running serially, each rendering operation is awaited before continuing to the next test iteration. When running asynchronously, all the test iterations are begun at once and are executed in parallel using the library’s multi-threading support.
330
236
 
331
- [See full results here…](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/index.md)
237
+ [See full results here…](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/index.md)
332
238
 
333
239
  ### [Startup latency](https://github.com/samizdatco/canvas-benchmarks/tree/main/tests/cold-start.js)
334
240
  | Library | Per Run | Total Time (100 iterations) |
335
241
  | -------------------- | --------- | --------------------------------------------- |
336
- | *canvaskit-wasm*    | `  25 ms` | ` 2.46 s` ![ ](./docs/assets/benchmarks.svg#cold-start_wasm) |
337
- | *canvas*    | `  88 ms` | ` 8.76 s` ![ ](./docs/assets/benchmarks.svg#cold-start_canvas) |
338
- | *@napi-rs/canvas*    | `  73 ms` | ` 7.30 s` ![ ](./docs/assets/benchmarks.svg#cold-start_napi) |
242
+ | *canvaskit-wasm*    | `  25 ms` | ` 2.47 s` ![ ](./docs/assets/benchmarks.svg#cold-start_wasm) |
243
+ | *canvas*    | `  88 ms` | ` 8.77 s` ![ ](./docs/assets/benchmarks.svg#cold-start_canvas) |
244
+ | *@napi-rs/canvas*    | `  69 ms` | ` 6.87 s` ![ ](./docs/assets/benchmarks.svg#cold-start_napi) |
339
245
  | *skia-canvas*    | `  <1 ms` | `  33 ms` ![ ](./docs/assets/benchmarks.svg#cold-start_skia-sync) |
340
246
 
341
247
  ### [Bezier curves](https://github.com/samizdatco/canvas-benchmarks/tree/main/tests/beziers.js)
342
248
  | Library | Per Run | Total Time (20 iterations) |
343
249
  | ------------------------------------------------------------- | --------- | ------------------------------------------- |
344
- | *canvaskit-wasm* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/snapshots/beziers_wasm.png) | ` 789 ms` | `15.77 s` ![ ](./docs/assets/benchmarks.svg#beziers_wasm) |
345
- | *canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/snapshots/beziers_canvas.png) | ` 488 ms` | ` 9.76 s` ![ ](./docs/assets/benchmarks.svg#beziers_canvas) |
346
- | *@napi-rs/canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/snapshots/beziers_napi.png) | ` 233 ms` | ` 4.65 s` ![ ](./docs/assets/benchmarks.svg#beziers_napi) |
347
- | *skia-canvas (serial)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/snapshots/beziers_skia-sync.png) | ` 137 ms` | ` 2.74 s` ![ ](./docs/assets/benchmarks.svg#beziers_skia-sync) |
348
- | *skia-canvas (async)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/snapshots/beziers_skia-async.png) | `  28 ms` | ` 558 ms` ![ ](./docs/assets/benchmarks.svg#beziers_skia-async) |
250
+ | *canvaskit-wasm* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/beziers_wasm.png) | ` 790 ms` | `15.81 s` ![ ](./docs/assets/benchmarks.svg#beziers_wasm) |
251
+ | *canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/beziers_canvas.png) | ` 486 ms` | ` 9.72 s` ![ ](./docs/assets/benchmarks.svg#beziers_canvas) |
252
+ | *@napi-rs/canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/beziers_napi.png) | ` 230 ms` | ` 4.60 s` ![ ](./docs/assets/benchmarks.svg#beziers_napi) |
253
+ | *skia-canvas (serial)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/beziers_skia-sync.png) | ` 137 ms` | ` 2.74 s` ![ ](./docs/assets/benchmarks.svg#beziers_skia-sync) |
254
+ | *skia-canvas (async)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/beziers_skia-async.png) | `  28 ms` | ` 558 ms` ![ ](./docs/assets/benchmarks.svg#beziers_skia-async) |
349
255
 
350
256
  ### [SVG to PNG](https://github.com/samizdatco/canvas-benchmarks/tree/main/tests/from-svg.js)
351
257
  | Library | Per Run | Total Time (100 iterations) |
352
258
  | -------------------------------------------------------------- | --------- | -------------------------------------------- |
353
259
  | canvaskit-wasm | ` ————— ` | ` ————— `   *not supported* |
354
- | *canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/snapshots/from-svg_canvas.png) | ` 122 ms` | `12.20 s` ![ ](./docs/assets/benchmarks.svg#from-svg_canvas) |
355
- | *@napi-rs/canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/snapshots/from-svg_napi.png) | `  98 ms` | ` 9.76 s` ![ ](./docs/assets/benchmarks.svg#from-svg_napi) |
356
- | *skia-canvas (serial)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/snapshots/from-svg_skia-sync.png) | `  59 ms` | ` 5.91 s` ![ ](./docs/assets/benchmarks.svg#from-svg_skia-sync) |
357
- | *skia-canvas (async)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/snapshots/from-svg_skia-async.png) | `  11 ms` | ` 1.06 s` ![ ](./docs/assets/benchmarks.svg#from-svg_skia-async) |
260
+ | *canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/from-svg_canvas.png) | ` 122 ms` | `12.16 s` ![ ](./docs/assets/benchmarks.svg#from-svg_canvas) |
261
+ | *@napi-rs/canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/from-svg_napi.png) | `  84 ms` | ` 8.42 s` ![ ](./docs/assets/benchmarks.svg#from-svg_napi) |
262
+ | *skia-canvas (serial)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/from-svg_skia-sync.png) | `  58 ms` | ` 5.83 s` ![ ](./docs/assets/benchmarks.svg#from-svg_skia-sync) |
263
+ | *skia-canvas (async)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/from-svg_skia-async.png) | `  11 ms` | ` 1.08 s` ![ ](./docs/assets/benchmarks.svg#from-svg_skia-async) |
358
264
 
359
265
  ### [Scale/rotate images](https://github.com/samizdatco/canvas-benchmarks/tree/main/tests/image-blit.js)
360
266
  | Library | Per Run | Total Time (50 iterations) |
361
267
  | ---------------------------------------------------------------- | --------- | ---------------------------------------------- |
362
- | *canvaskit-wasm* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/snapshots/image-blit_wasm.png) | ` 279 ms` | `13.95 s` ![ ](./docs/assets/benchmarks.svg#image-blit_wasm) |
363
- | *canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/snapshots/image-blit_canvas.png) | ` 284 ms` | `14.21 s` ![ ](./docs/assets/benchmarks.svg#image-blit_canvas) |
364
- | *@napi-rs/canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/snapshots/image-blit_napi.png) | ` 116 ms` | ` 5.78 s` ![ ](./docs/assets/benchmarks.svg#image-blit_napi) |
365
- | *skia-canvas (serial)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/snapshots/image-blit_skia-sync.png) | ` 100 ms` | ` 5.01 s` ![ ](./docs/assets/benchmarks.svg#image-blit_skia-sync) |
366
- | *skia-canvas (async)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/snapshots/image-blit_skia-async.png) | `  19 ms` | ` 937 ms` ![ ](./docs/assets/benchmarks.svg#image-blit_skia-async) |
268
+ | *canvaskit-wasm* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/image-blit_wasm.png) | ` 274 ms` | `13.72 s` ![ ](./docs/assets/benchmarks.svg#image-blit_wasm) |
269
+ | *canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/image-blit_canvas.png) | ` 283 ms` | `14.13 s` ![ ](./docs/assets/benchmarks.svg#image-blit_canvas) |
270
+ | *@napi-rs/canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/image-blit_napi.png) | ` 112 ms` | ` 5.60 s` ![ ](./docs/assets/benchmarks.svg#image-blit_napi) |
271
+ | *skia-canvas (serial)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/image-blit_skia-sync.png) | ` 100 ms` | ` 5.00 s` ![ ](./docs/assets/benchmarks.svg#image-blit_skia-sync) |
272
+ | *skia-canvas (async)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/image-blit_skia-async.png) | `  19 ms` | ` 935 ms` ![ ](./docs/assets/benchmarks.svg#image-blit_skia-async) |
367
273
 
368
274
  ### [Basic text](https://github.com/samizdatco/canvas-benchmarks/tree/main/tests/text.js)
369
275
  | Library | Per Run | Total Time (200 iterations) |
370
276
  | ---------------------------------------------------------- | --------- | ---------------------------------------- |
371
- | *canvaskit-wasm* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/snapshots/text_wasm.png) | `  24 ms` | ` 4.74 s` ![ ](./docs/assets/benchmarks.svg#text_wasm) |
372
- | *canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/snapshots/text_canvas.png) | `  24 ms` | ` 4.86 s` ![ ](./docs/assets/benchmarks.svg#text_canvas) |
373
- | *@napi-rs/canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/snapshots/text_napi.png) | `  19 ms` | ` 3.82 s` ![ ](./docs/assets/benchmarks.svg#text_napi) |
374
- | *skia-canvas (serial)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/snapshots/text_skia-sync.png) | `  21 ms` | ` 4.24 s` ![ ](./docs/assets/benchmarks.svg#text_skia-sync) |
375
- | *skia-canvas (async)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/snapshots/text_skia-async.png) | `   4 ms` | ` 781 ms` ![ ](./docs/assets/benchmarks.svg#text_skia-async) |
376
-
277
+ | *canvaskit-wasm* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/text_wasm.png) | `  24 ms` | ` 4.75 s` ![ ](./docs/assets/benchmarks.svg#text_wasm) |
278
+ | *canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/text_canvas.png) | `  24 ms` | ` 4.88 s` ![ ](./docs/assets/benchmarks.svg#text_canvas) |
279
+ | *@napi-rs/canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/text_napi.png) | `  19 ms` | ` 3.83 s` ![ ](./docs/assets/benchmarks.svg#text_napi) |
280
+ | *skia-canvas (serial)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/text_skia-sync.png) | `  21 ms` | ` 4.26 s` ![ ](./docs/assets/benchmarks.svg#text_skia-sync) |
281
+ | *skia-canvas (async)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/text_skia-async.png) | `   4 ms` | ` 819 ms` ![ ](./docs/assets/benchmarks.svg#text_skia-async) |
377
282
 
378
283
  ## Acknowledgements
379
284
 
@@ -389,35 +294,38 @@ This project is deeply indebted to the work of the [Rust Skia project](https://g
389
294
  - [@meihuanyu](https://github.com/meihuanyu) contributed filter & path rendering fixes
390
295
 
391
296
  ## Copyright
392
- © 2020–2025 [Samizdat Drafting Co.](https://samizdat.co)
297
+ © 2020–2026 [Samizdat Drafting Co.](https://samizdat.co)
393
298
 
394
- [bool-ops]: https://skia-canvas.org/api/path2d#complement-difference-intersect-union-and-xor
299
+ [bool-ops]: https://skia-canvas.org/api/path2d#complement-difference-intersect-union-xor
395
300
  [c2d_font]: https://skia-canvas.org/api/context#font
396
301
  [c2d_measuretext]: https://skia-canvas.org/api/context#measuretext
397
- [canvas]: https://skia-canvas.org/api/canvas
398
302
  [createProjection()]: https://skia-canvas.org/api/context#createprojection
399
303
  [createTexture()]: https://skia-canvas.org/api/context#createtexture
400
- [engine]: https://skia-canvas.org/api/canvas#engine
304
+ [edges]: https://skia-canvas.org/api/path2d#edges
401
305
  [fontlibrary-use]: https://skia-canvas.org/api/font-library#use
402
306
  [fontvariant]: https://skia-canvas.org/api/context#fontvariant
307
+ [fontfeatures]: https://skia-canvas.org/api/context#fontfeaturesettings
403
308
  [lineDashMarker]: https://skia-canvas.org/api/context#linedashmarker
309
+ [loadimage]: https://skia-canvas.org/api/image#loadimage
404
310
  [newPage]: https://skia-canvas.org/api/canvas#newpage
405
- [p2d_interpolate]: https://skia-canvas.org/api/path2d#interpolate
311
+ [p2d_contours]: https://skia-canvas.org/api/path2d#contours
406
312
  [p2d_points]: https://skia-canvas.org/api/path2d#points
407
- [p2d_round]: https://skia-canvas.org/api/path2d#round
408
- [p2d_simplify]: https://skia-canvas.org/api/path2d#simplify
409
- [p2d_trim]: https://skia-canvas.org/api/path2d#trim
313
+ [p2d_positionAt]: https://skia-canvas.org/api/path2d#positionat
314
+ [p2d_slice]: https://skia-canvas.org/api/path2d#slice
410
315
  [toFile]: https://skia-canvas.org/api/canvas#tofile
411
316
  [textwrap]: https://skia-canvas.org/api/context#textwrap
412
317
  [toBuffer]: https://skia-canvas.org/api/canvas#tobuffer
413
318
  [toURL]: https://skia-canvas.org/api/canvas#tourl
414
- [win_bind]: https://skia-canvas.org/api/window#on--off--once
319
+ [win_events]: https://skia-canvas.org/api/window#events
320
+ [win_animation]: https://skia-canvas.org/api/window#events-for-animation
415
321
  [window]: https://skia-canvas.org/api/window
322
+ [loadcanvas]: https://skia-canvas.org/api/canvas#loadcanvas
323
+ [ctx_colors]: https://skia-canvas.org/api/context#choosing-colors
324
+ [api_docs]: https://skia-canvas.org/api
325
+ [getting_started]: https://skia-canvas.org/getting-started
326
+ [installation]: https://skia-canvas.org/getting-started#installation
327
+ [global_settings]: https://skia-canvas.org/getting-started#global-settings
416
328
  [multithreading]: https://skia-canvas.org/getting-started#multithreading
417
- [node_napi]: https://nodejs.org/api/n-api.html#node-api-version-matrix
418
- [node_env]: https://nodejs.org/en/learn/command-line/how-to-read-environment-variables-from-nodejs
419
- [node_env_arg]: https://nodejs.org/dist/latest-v22.x/docs/api/cli.html#--env-fileconfig
420
- [rayon]: https://crates.io/crates/rayon
421
329
  [sharp]: https://sharp.pixelplumbing.com
422
330
  [VariableFonts]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
423
331
  [filter]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter