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.
- package/README.md +215 -40
- package/lib/browser.js +36 -160
- package/lib/classes/canvas.js +109 -54
- package/lib/classes/context.js +58 -37
- package/lib/classes/css.js +3 -4
- package/lib/classes/geometry.js +17 -4
- package/lib/classes/gui.js +195 -158
- package/lib/classes/imagery.js +170 -63
- package/lib/classes/neon.js +40 -8
- package/lib/classes/path.js +19 -16
- package/lib/classes/typography.js +7 -21
- package/lib/fonts/fonts.conf +25 -0
- package/lib/index.d.ts +164 -24
- package/lib/prebuild.mjs +131 -0
- package/package.json +34 -30
- package/CHANGELOG.md +0 -434
package/README.md
CHANGED
|
@@ -18,19 +18,16 @@
|
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
-
Skia Canvas is a
|
|
22
|
-
|
|
23
|
-
While the primary goal of this project is to provide a reliable emulation of the [standard API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) according to the [spec](https://html.spec.whatwg.org/multipage/canvas.html), it also extends it in a number of areas to take greater advantage of Skia's advanced graphical features and provide a more expressive coding environment.
|
|
21
|
+
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.
|
|
24
22
|
|
|
25
23
|
In particular, Skia Canvas:
|
|
26
24
|
|
|
27
|
-
-
|
|
28
|
-
- can
|
|
29
|
-
-
|
|
30
|
-
- can save images to [files][saveAs], return them as [Buffers][toBuffer], or encode [dataURL][toDataURL_ext] strings
|
|
25
|
+
- generates images in vector (PDF & SVG) as well as bitmap (JPEG, PNG, & WEBP) formats
|
|
26
|
+
- can draw to interactive GUI [windows][window] and provides a browser-like [event][win_bind] framework
|
|
27
|
+
- can save images to [files][toFile], encode to [dataURL][toURL] strings, and return [Buffers][toBuffer] or [Sharp][sharp] objects
|
|
31
28
|
- uses native threads in a [user-configurable][multithreading] worker pool for asynchronous rendering and file I/O
|
|
32
|
-
- can create [multiple ‘pages’][newPage] on a given canvas and then [output][
|
|
33
|
-
- can [simplify][p2d_simplify], [blunt][p2d_round], [combine][bool-ops], [excerpt][p2d_trim], and [atomize][p2d_points]
|
|
29
|
+
- 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
|
|
30
|
+
- 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]
|
|
34
31
|
- provides [3D perspective][createProjection()] transformations in addition to [scaling][scale()], [rotation][rotate()], and [translation][translate()]
|
|
35
32
|
- can fill shapes with vector-based [Textures][createTexture()] in addition to bitmap-based [Patterns][createPattern()] and supports line-drawing with custom [markers][lineDashMarker]
|
|
36
33
|
- supports the full set of [CSS filter][filter] image processing operators
|
|
@@ -41,6 +38,7 @@ In particular, Skia Canvas:
|
|
|
41
38
|
- proportional [letter-spacing][letterSpacing], [word-spacing][wordSpacing], and [leading][c2d_font]
|
|
42
39
|
- support for [variable fonts][VariableFonts] and transparent mapping of weight values
|
|
43
40
|
- use of non-system fonts [loaded][fontlibrary-use] from local files
|
|
41
|
+
- can be used for server-side image rendering on standard Linux hosts and ‘serverless’ platforms like Vercel and AWS Lambda
|
|
44
42
|
|
|
45
43
|
## Installation
|
|
46
44
|
|
|
@@ -51,68 +49,161 @@ npm install skia-canvas
|
|
|
51
49
|
|
|
52
50
|
This will download a pre-compiled library from the project’s most recent [release](https://github.com/samizdatco/skia-canvas/releases).
|
|
53
51
|
|
|
54
|
-
## Platform Support
|
|
55
52
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
- v14.17+
|
|
59
|
-
- v15.12+
|
|
60
|
-
- v16.0.0 and later
|
|
53
|
+
### `pnpm`
|
|
54
|
+
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
55
|
|
|
62
|
-
|
|
56
|
+
```bash
|
|
57
|
+
pnpm install skia-canvas
|
|
58
|
+
pnpm approve-builds
|
|
59
|
+
```
|
|
60
|
+
In non-interactive scenarios (like building via CI), you can approve the build step when you add `skia-canvas` to your project:
|
|
63
61
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
```bash
|
|
63
|
+
pnpm install skia-canvas --allow-build=skia-canvas
|
|
64
|
+
```
|
|
67
65
|
|
|
68
|
-
|
|
66
|
+
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:
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"pnpm": {
|
|
70
|
+
"onlyBuiltDependencies": ["skia-canvas"]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
69
74
|
|
|
70
|
-
## Running in Docker
|
|
71
75
|
|
|
72
|
-
The library is compatible with Linux systems using [glibc](https://www.gnu.org/software/libc/) 2.28 or later as well as Alpine Linux (x64 & arm64) and the [musl](https://musl.libc.org) C library it favors. In both cases, Fontconfig must be installed on the system for `skia-canvas` to operate correctly.
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
## Platform Support
|
|
78
|
+
|
|
79
|
+
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.
|
|
80
|
+
|
|
81
|
+
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+.
|
|
82
|
+
|
|
83
|
+
### Linux
|
|
78
84
|
|
|
79
|
-
|
|
85
|
+
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.
|
|
80
86
|
|
|
87
|
+
### Docker
|
|
88
|
+
|
|
89
|
+
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:
|
|
81
90
|
```dockerfile
|
|
82
|
-
FROM node
|
|
83
|
-
RUN apt-get update && apt-get install -y -q --no-install-recommends libfontconfig1
|
|
91
|
+
FROM node
|
|
84
92
|
```
|
|
85
93
|
|
|
86
94
|
If you wish to use Alpine as the underlying distribution, you can start with something along the lines of:
|
|
87
95
|
|
|
88
96
|
```dockerfile
|
|
89
97
|
FROM node:alpine
|
|
90
|
-
RUN apk update && apk add fontconfig
|
|
91
98
|
```
|
|
92
99
|
|
|
100
|
+
### AWS Lambda
|
|
101
|
+
|
|
102
|
+
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.
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
<details><summary>
|
|
106
|
+
|
|
107
|
+
**Detailed AWS instructions**
|
|
108
|
+
|
|
109
|
+
</summary>
|
|
110
|
+
|
|
111
|
+
#### Adding the Skia Canvas layer to your AWS account
|
|
112
|
+
|
|
113
|
+
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
|
|
114
|
+
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:
|
|
115
|
+
- **Name**: `skia-canvas` (or whatever you want)
|
|
116
|
+
- **Description**: you might want to note the Skia Canvas version here
|
|
117
|
+
- **Compatible architectures**: select **x86_64** or **arm64** depending on which zip you chose
|
|
118
|
+
- **Compatible runtimes**: select **Node.js 22.x** (and/or 20.x)
|
|
119
|
+
3. Click the **Choose file** button and select the zip file you downloaded in Step 1, then click **Create**
|
|
120
|
+
|
|
121
|
+
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.
|
|
122
|
+
```sh
|
|
123
|
+
#!/usr/bin/env bash
|
|
124
|
+
VERSION=3.0 # the skia-canvas version to include
|
|
125
|
+
PLATFORM=arm64 # arm64 or x64
|
|
126
|
+
|
|
127
|
+
curl -sLO https://github.com/samizdatco/skia-canvas/releases/download/v${VERSION}/aws-lambda-${PLATFORM}.zip
|
|
128
|
+
aws lambda publish-layer-version \
|
|
129
|
+
--layer-name "skia-canvas" \
|
|
130
|
+
--description "Skia Canvas ${VERSION} layer" \
|
|
131
|
+
--zip-file "fileb://aws-lambda-${PLATFORM}.zip" \
|
|
132
|
+
--compatible-runtimes "nodejs20.x" "nodejs22.x" \
|
|
133
|
+
--compatible-architectures "${X/#x/x86_}"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### Using the layer in a Lambda function
|
|
137
|
+
|
|
138
|
+
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.
|
|
139
|
+
|
|
140
|
+
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.
|
|
141
|
+
|
|
142
|
+
</details>
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
### Next.js / Webpack
|
|
146
|
+
|
|
147
|
+
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:
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
const nextConfig: NextConfig = {
|
|
151
|
+
serverExternalPackages: ['skia-canvas'],
|
|
152
|
+
webpack: (config, options) => {
|
|
153
|
+
if (options.isServer){
|
|
154
|
+
config.externals = [
|
|
155
|
+
...config.externals,
|
|
156
|
+
{'skia-canvas': 'commonjs skia-canvas'},
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
return config
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
|
|
93
165
|
## Compiling from Source
|
|
94
166
|
|
|
95
167
|
If prebuilt binaries aren’t available for your system you’ll need to compile the portions of this library that directly interface with Skia.
|
|
96
168
|
|
|
97
169
|
Start by installing:
|
|
98
170
|
|
|
99
|
-
1.
|
|
100
|
-
2.
|
|
171
|
+
1. A recent version of `git` (older versions have difficulties with Skia's submodules)
|
|
172
|
+
2. The [Rust compiler](https://www.rust-lang.org/tools/install) and cargo package manager using [`rustup`](https://rust-lang.github.io/rustup/)
|
|
173
|
+
3. A C compiler toolchain (either LLVM/Clang or MSVC)
|
|
101
174
|
4. Python 3 (used by Skia's [build process](https://skia.org/docs/user/build/))
|
|
102
|
-
|
|
103
|
-
|
|
175
|
+
5. The [Ninja](https://ninja-build.org) build system
|
|
176
|
+
6. On Linux: Fontconfig and OpenSSL
|
|
177
|
+
|
|
178
|
+
[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).
|
|
104
179
|
|
|
105
|
-
|
|
180
|
+
## Global Settings
|
|
106
181
|
|
|
107
|
-
|
|
182
|
+
> 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.
|
|
108
183
|
|
|
109
|
-
|
|
184
|
+
### Multithreading
|
|
185
|
+
|
|
186
|
+
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.
|
|
110
187
|
|
|
111
188
|
For example, you can limit your asynchronous processing to two simultaneous tasks by running your script with:
|
|
112
189
|
```bash
|
|
113
190
|
SKIA_CANVAS_THREADS=2 node my-canvas-script.js
|
|
114
191
|
```
|
|
115
192
|
|
|
193
|
+
### Argument Validation
|
|
194
|
+
|
|
195
|
+
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:
|
|
196
|
+
|
|
197
|
+
```js
|
|
198
|
+
ctx.fillRect(0, 0, 100, "october")
|
|
199
|
+
ctx.lineTo(NaN, 0)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
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).
|
|
204
|
+
|
|
205
|
+
Set the `SKIA_CANVAS_STRICT` environment variable to `1` or `true` to enable this mode.
|
|
206
|
+
|
|
116
207
|
## Example Usage
|
|
117
208
|
|
|
118
209
|
### Generating image files
|
|
@@ -197,6 +288,87 @@ win.on("draw", e => {
|
|
|
197
288
|
ctx.fill()
|
|
198
289
|
})
|
|
199
290
|
```
|
|
291
|
+
|
|
292
|
+
### Integrating with [Sharp.js][sharp]
|
|
293
|
+
|
|
294
|
+
```js
|
|
295
|
+
import sharp from 'sharp'
|
|
296
|
+
import {Canvas, loadImage} from 'skia-canvas'
|
|
297
|
+
|
|
298
|
+
let canvas = new Canvas(400, 400),
|
|
299
|
+
ctx = canvas.getContext("2d"),
|
|
300
|
+
{width, height} = canvas,
|
|
301
|
+
[x, y] = [width/2, height/2]
|
|
302
|
+
|
|
303
|
+
ctx.fillStyle = 'red'
|
|
304
|
+
ctx.fillRect(0, 0, x, y)
|
|
305
|
+
ctx.fillStyle = 'orange'
|
|
306
|
+
ctx.fillRect(x, y, x, y)
|
|
307
|
+
|
|
308
|
+
// Render the canvas to a Sharp object on a background thread then desaturate
|
|
309
|
+
await canvas.toSharp().modulate({saturation:.25}).jpeg().toFile("faded.jpg")
|
|
310
|
+
|
|
311
|
+
// Convert an ImageData to a Sharp object and save a grayscale version
|
|
312
|
+
let imgData = ctx.getImageData(0, 0, width, height, {matte:'white', density:2})
|
|
313
|
+
await imgData.toSharp().grayscale().png().toFile("black-and-white.png")
|
|
314
|
+
|
|
315
|
+
// Create an image using Sharp then draw it to the canvas as an Image object
|
|
316
|
+
let sharpImage = sharp({create:{ width:x, height:y, channels:4, background:"skyblue" }})
|
|
317
|
+
let canvasImage = await loadImage(sharpImage)
|
|
318
|
+
ctx.drawImage(canvasImage, x, 0)
|
|
319
|
+
await canvas.saveAs('mosaic.png')
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Benchmarks
|
|
323
|
+
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.
|
|
324
|
+
|
|
325
|
+
[See full results here…](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/index.md)
|
|
326
|
+
|
|
327
|
+
### [Startup latency](https://github.com/samizdatco/canvas-benchmarks/tree/main/tests/cold-start.js)
|
|
328
|
+
| Library | Per Run | Total Time (100 iterations) |
|
|
329
|
+
| -------------------- | --------- | --------------------------------------------- |
|
|
330
|
+
| *canvaskit-wasm* | ` 25 ms` | ` 2.46 s`  |
|
|
331
|
+
| *canvas* | ` 88 ms` | ` 8.76 s`  |
|
|
332
|
+
| *@napi-rs/canvas* | ` 73 ms` | ` 7.30 s`  |
|
|
333
|
+
| *skia-canvas* | ` <1 ms` | ` 33 ms`  |
|
|
334
|
+
|
|
335
|
+
### [Bezier curves](https://github.com/samizdatco/canvas-benchmarks/tree/main/tests/beziers.js)
|
|
336
|
+
| Library | Per Run | Total Time (20 iterations) |
|
|
337
|
+
| ------------------------------------------------------------- | --------- | ------------------------------------------- |
|
|
338
|
+
| *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`  |
|
|
339
|
+
| *canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/snapshots/beziers_canvas.png) | ` 488 ms` | ` 9.76 s`  |
|
|
340
|
+
| *@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`  |
|
|
341
|
+
| *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`  |
|
|
342
|
+
| *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`  |
|
|
343
|
+
|
|
344
|
+
### [SVG to PNG](https://github.com/samizdatco/canvas-benchmarks/tree/main/tests/from-svg.js)
|
|
345
|
+
| Library | Per Run | Total Time (100 iterations) |
|
|
346
|
+
| -------------------------------------------------------------- | --------- | -------------------------------------------- |
|
|
347
|
+
| canvaskit-wasm | ` ————— ` | ` ————— ` *not supported* |
|
|
348
|
+
| *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`  |
|
|
349
|
+
| *@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`  |
|
|
350
|
+
| *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`  |
|
|
351
|
+
| *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`  |
|
|
352
|
+
|
|
353
|
+
### [Scale/rotate images](https://github.com/samizdatco/canvas-benchmarks/tree/main/tests/image-blit.js)
|
|
354
|
+
| Library | Per Run | Total Time (50 iterations) |
|
|
355
|
+
| ---------------------------------------------------------------- | --------- | ---------------------------------------------- |
|
|
356
|
+
| *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`  |
|
|
357
|
+
| *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`  |
|
|
358
|
+
| *@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`  |
|
|
359
|
+
| *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`  |
|
|
360
|
+
| *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`  |
|
|
361
|
+
|
|
362
|
+
### [Basic text](https://github.com/samizdatco/canvas-benchmarks/tree/main/tests/text.js)
|
|
363
|
+
| Library | Per Run | Total Time (200 iterations) |
|
|
364
|
+
| ---------------------------------------------------------- | --------- | ---------------------------------------- |
|
|
365
|
+
| *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`  |
|
|
366
|
+
| *canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/snapshots/text_canvas.png) | ` 24 ms` | ` 4.86 s`  |
|
|
367
|
+
| *@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`  |
|
|
368
|
+
| *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`  |
|
|
369
|
+
| *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`  |
|
|
370
|
+
|
|
371
|
+
|
|
200
372
|
## Acknowledgements
|
|
201
373
|
|
|
202
374
|
This project is deeply indebted to the work of the [Rust Skia project](https://github.com/rust-skia/rust-skia) whose Skia bindings provide a safe and idiomatic interface to the mess of C++ that lies underneath. Many thanks to the developers of [node-canvas](https://github.com/Automattic/node-canvas) for their terrific set of unit tests. In the absence of an [Acid Test](https://www.acidtests.org) for canvas, these routines were invaluable.
|
|
@@ -204,7 +376,7 @@ This project is deeply indebted to the work of the [Rust Skia project](https://g
|
|
|
204
376
|
|
|
205
377
|
### Notable contributors
|
|
206
378
|
|
|
207
|
-
- [@mpaparno](https://github.com/mpaparno) contributed support for SVG rendering, raw image-buffer handling, WEBP import/export and numerous
|
|
379
|
+
- [@mpaparno](https://github.com/mpaparno) contributed support for SVG rendering, raw image-buffer handling, WEBP import/export and numerous bug fixes
|
|
208
380
|
- [@Salmondx](https://github.com/Salmondx) developed the initial Raw image loading & rendering routines
|
|
209
381
|
- [@lucasmerlin](https://github.com/lucasmerlin) helped get GPU rendering working on Vulkan
|
|
210
382
|
- [@cprecioso](https://github.com/cprecioso) & [@saantonandre](https://github.com/saantonandre) corrected and expanded upon the TypeScript type definitions
|
|
@@ -229,15 +401,18 @@ This project is deeply indebted to the work of the [Rust Skia project](https://g
|
|
|
229
401
|
[p2d_round]: https://skia-canvas.org/api/path2d#round
|
|
230
402
|
[p2d_simplify]: https://skia-canvas.org/api/path2d#simplify
|
|
231
403
|
[p2d_trim]: https://skia-canvas.org/api/path2d#trim
|
|
232
|
-
[
|
|
404
|
+
[toFile]: https://skia-canvas.org/api/canvas#tofile
|
|
233
405
|
[textwrap]: https://skia-canvas.org/api/context#textwrap
|
|
234
406
|
[toBuffer]: https://skia-canvas.org/api/canvas#tobuffer
|
|
235
|
-
[
|
|
407
|
+
[toURL]: https://skia-canvas.org/api/canvas#tourl
|
|
236
408
|
[win_bind]: https://skia-canvas.org/api/window#on--off--once
|
|
237
409
|
[window]: https://skia-canvas.org/api/window
|
|
238
410
|
[multithreading]: https://skia-canvas.org/getting-started#multithreading
|
|
239
411
|
[node_napi]: https://nodejs.org/api/n-api.html#node-api-version-matrix
|
|
412
|
+
[node_env]: https://nodejs.org/en/learn/command-line/how-to-read-environment-variables-from-nodejs
|
|
413
|
+
[node_env_arg]: https://nodejs.org/dist/latest-v22.x/docs/api/cli.html#--env-fileconfig
|
|
240
414
|
[rayon]: https://crates.io/crates/rayon
|
|
415
|
+
[sharp]: https://sharp.pixelplumbing.com
|
|
241
416
|
[VariableFonts]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
|
|
242
417
|
[filter]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter
|
|
243
418
|
[letterSpacing]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/letterSpacing
|
package/lib/browser.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// Browser equivalents of the skia-canvas convenience initializers and polyfills for
|
|
3
3
|
// the Canvas object’s newPage & export methods
|
|
4
4
|
//
|
|
5
|
+
// OPTIONAL DEPENDENCY: be sure to include JSZip in your project bundle if you want to
|
|
6
|
+
// make use of multi-page toFile() downloads
|
|
7
|
+
//
|
|
5
8
|
|
|
6
9
|
"use strict"
|
|
7
10
|
|
|
@@ -42,7 +45,11 @@ class Canvas{
|
|
|
42
45
|
return Object.assign(elt, {width, height}).getContext("2d")
|
|
43
46
|
},
|
|
44
47
|
|
|
45
|
-
saveAs(
|
|
48
|
+
saveAs(){
|
|
49
|
+
throw Error("Canvas.saveAs() has been renamed to Canvas.toFile")
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
toFile(filename, args){
|
|
46
53
|
args = typeof args=='number' ? {quality:args} : args
|
|
47
54
|
let opts = exportOptions(this.pages, {filename, ...args}),
|
|
48
55
|
{pattern, padding, mime, quality, matte, density, archive} = opts,
|
|
@@ -59,163 +66,18 @@ class Canvas{
|
|
|
59
66
|
return asBuffer(canvas, mime, quality, matte)
|
|
60
67
|
},
|
|
61
68
|
|
|
62
|
-
|
|
63
|
-
toDataURL(extension="png", args={}){
|
|
69
|
+
toURL(extension="png", args={}){
|
|
64
70
|
args = typeof args=='number' ? {quality:args} : args
|
|
65
71
|
let opts = exportOptions(this.pages, {extension, ...args}),
|
|
66
72
|
{mime, quality, matte, pages, density} = opts,
|
|
67
73
|
canvas = atScale(pages, density, matte)[0],
|
|
68
|
-
url = canvas
|
|
74
|
+
url = canvas.toDataURL(mime, quality);
|
|
69
75
|
return Promise.resolve(url)
|
|
70
76
|
}
|
|
71
77
|
})
|
|
72
78
|
}
|
|
73
79
|
}
|
|
74
80
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
//
|
|
78
|
-
// Zip (pace Phil Katz & q.v. https://github.com/jimmywarting/StreamSaver.js)
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
class Crc32 {
|
|
82
|
-
static for(data){
|
|
83
|
-
return new Crc32().append(data).get()
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
constructor(){ this.crc = -1 }
|
|
87
|
-
|
|
88
|
-
get(){ return ~this.crc }
|
|
89
|
-
|
|
90
|
-
append(data){
|
|
91
|
-
var crc = this.crc | 0,
|
|
92
|
-
table = this.table
|
|
93
|
-
for (var offset = 0, len = data.length | 0; offset < len; offset++) {
|
|
94
|
-
crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xFF]
|
|
95
|
-
}
|
|
96
|
-
this.crc = crc
|
|
97
|
-
return this
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
Crc32.prototype.table = (() => {
|
|
103
|
-
var i, j, t, table = []
|
|
104
|
-
for (i = 0; i < 256; i++) {
|
|
105
|
-
t = i
|
|
106
|
-
for (j = 0; j < 8; j++) {
|
|
107
|
-
t = (t & 1)
|
|
108
|
-
? (t >>> 1) ^ 0xEDB88320
|
|
109
|
-
: t >>> 1
|
|
110
|
-
}
|
|
111
|
-
table[i] = t
|
|
112
|
-
}
|
|
113
|
-
return table
|
|
114
|
-
})()
|
|
115
|
-
|
|
116
|
-
function calloc(size){
|
|
117
|
-
let array = new Uint8Array(size),
|
|
118
|
-
view = new DataView(array.buffer),
|
|
119
|
-
buf = {
|
|
120
|
-
array, view, size,
|
|
121
|
-
set8(at, to){ view.setUint8(at, to); return buf },
|
|
122
|
-
set16(at, to){ view.setUint16(at, to, true); return buf },
|
|
123
|
-
set32(at, to){ view.setUint32(at, to, true); return buf },
|
|
124
|
-
bytes(at, to){ array.set(to, at); return buf },
|
|
125
|
-
}
|
|
126
|
-
return buf
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
class Zip{
|
|
130
|
-
static encoder = new TextEncoder()
|
|
131
|
-
|
|
132
|
-
constructor(directory){
|
|
133
|
-
let now = new Date()
|
|
134
|
-
Object.assign(this, {
|
|
135
|
-
directory,
|
|
136
|
-
offset: 0,
|
|
137
|
-
files: [],
|
|
138
|
-
time: (((now.getHours() << 6) | now.getMinutes()) << 5) | now.getSeconds() / 2,
|
|
139
|
-
date: ((((now.getFullYear() - 1980) << 4) | (now.getMonth() + 1)) << 5) | now.getDate(),
|
|
140
|
-
})
|
|
141
|
-
this.add(directory)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async add(filename, blob){
|
|
145
|
-
let folder = !blob,
|
|
146
|
-
name = Zip.encoder.encode(`${this.directory}/${folder ? '' : filename}`),
|
|
147
|
-
data = new Uint8Array(folder ? 0 : await blob.arrayBuffer()),
|
|
148
|
-
preamble = 30 + name.length,
|
|
149
|
-
descriptor = preamble + data.length,
|
|
150
|
-
postamble = 16,
|
|
151
|
-
{offset} = this
|
|
152
|
-
|
|
153
|
-
let header = calloc(26)
|
|
154
|
-
.set32(0, 0x08080014) // zip version
|
|
155
|
-
.set16(6, this.time) // time
|
|
156
|
-
.set16(8, this.date) // date
|
|
157
|
-
.set32(10, Crc32.for(data)) // checksum
|
|
158
|
-
.set32(14, data.length) // compressed size (w/ zero compression)
|
|
159
|
-
.set32(18, data.length) // un-compressed size
|
|
160
|
-
.set16(22, name.length) // filename length (utf8 bytes)
|
|
161
|
-
offset += preamble
|
|
162
|
-
|
|
163
|
-
let payload = calloc(preamble + data.length + postamble)
|
|
164
|
-
.set32(0, 0x04034b50) // local header signature
|
|
165
|
-
.bytes(4, header.array) // ...header fields...
|
|
166
|
-
.bytes(30, name) // filename
|
|
167
|
-
.bytes(preamble, data) // blob bytes
|
|
168
|
-
offset += data.length
|
|
169
|
-
|
|
170
|
-
payload
|
|
171
|
-
.set32(descriptor, 0x08074b50) // signature
|
|
172
|
-
.bytes(descriptor + 4, header.array.slice(10,22)) // length & filemame
|
|
173
|
-
offset += postamble
|
|
174
|
-
|
|
175
|
-
this.files.push({offset, folder, name, header, payload})
|
|
176
|
-
this.offset = offset
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
toBuffer(){
|
|
180
|
-
// central directory record
|
|
181
|
-
let length = this.files.reduce((len, {name}) => 46 + name.length + len, 0),
|
|
182
|
-
cdr = calloc(length + 22),
|
|
183
|
-
index = 0
|
|
184
|
-
|
|
185
|
-
for (var {offset, name, header, folder} of this.files){
|
|
186
|
-
cdr.set32(index, 0x02014b50) // archive file signature
|
|
187
|
-
.set16(index + 4, 0x0014) // version
|
|
188
|
-
.bytes(index + 6, header.array) // ...header fields...
|
|
189
|
-
.set8(index + 38, folder ? 0x10 : 0) // is_dir flag
|
|
190
|
-
.set32(index + 42, offset) // file offset
|
|
191
|
-
.bytes(index + 46, name) // filename
|
|
192
|
-
index += 46 + name.length
|
|
193
|
-
}
|
|
194
|
-
cdr.set32(index, 0x06054b50) // signature
|
|
195
|
-
.set16(index + 8, this.files.length) // № files per-segment
|
|
196
|
-
.set16(index + 10, this.files.length) // № files this segment
|
|
197
|
-
.set32(index + 12, length) // central directory length
|
|
198
|
-
.set32(index + 16, this.offset) // file-offset of directory
|
|
199
|
-
|
|
200
|
-
// concatenated zipfile data
|
|
201
|
-
let output = new Uint8Array(this.offset + cdr.size),
|
|
202
|
-
cursor = 0;
|
|
203
|
-
|
|
204
|
-
for (var {payload} of this.files){
|
|
205
|
-
output.set(payload.array, cursor)
|
|
206
|
-
cursor += payload.size
|
|
207
|
-
}
|
|
208
|
-
output.set(cdr.array, cursor)
|
|
209
|
-
|
|
210
|
-
return output
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
get blob(){
|
|
214
|
-
return new Blob([this.toBuffer()], {type:"application/zip"})
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
81
|
//
|
|
220
82
|
// Browser helpers for converting canvas elements to blobs/buffers/files/zips
|
|
221
83
|
//
|
|
@@ -241,16 +103,23 @@ const asDownload = async (canvas, mime, quality, matte, filename) => {
|
|
|
241
103
|
}
|
|
242
104
|
|
|
243
105
|
const asZipDownload = async (pages, mime, quality, matte, archive, pattern, padding) => {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
await
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
106
|
+
await import("jszip").then(async ({default:JSZip}) => {
|
|
107
|
+
let filenames = i => pattern.replace('{}', String(i+1).padStart(padding, '0')),
|
|
108
|
+
zip = new JSZip(),
|
|
109
|
+
folder = basename(archive, '.zip') || 'archive',
|
|
110
|
+
payload = zip.folder(folder)
|
|
111
|
+
|
|
112
|
+
await Promise.all(pages.map(async (page, i) => {
|
|
113
|
+
let filename = filenames(i) // serialize filename(s) before awaiting
|
|
114
|
+
payload.file(filename, await asBlob(page, mime, quality, matte))
|
|
115
|
+
}))
|
|
116
|
+
|
|
117
|
+
zip.generateAsync({type:"blob"})
|
|
118
|
+
.then(content => _download(`${folder}.zip`, content))
|
|
119
|
+
})
|
|
120
|
+
.catch(() => {
|
|
121
|
+
console.log("Multi-page downloads require JSZip to be bundled: https://www.npmjs.com/package/jszip")
|
|
122
|
+
})
|
|
254
123
|
}
|
|
255
124
|
|
|
256
125
|
const _download = (filename, blob) => {
|
|
@@ -318,11 +187,18 @@ class Format{
|
|
|
318
187
|
// Validation of the options dict shared by the Canvas saveAs, toBuffer, and toDataURL methods
|
|
319
188
|
//
|
|
320
189
|
|
|
321
|
-
|
|
190
|
+
function basename(str, ext) {
|
|
191
|
+
let stub = str.substring(str.lastIndexOf('/') + 1)
|
|
192
|
+
return ext && stub.endsWith(ext) ? stub.slice(0, -ext.length) : stub
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function extname(str){
|
|
196
|
+
return str.substring(str.lastIndexOf('.'))
|
|
197
|
+
}
|
|
322
198
|
|
|
323
199
|
function exportOptions(pages, {filename='', extension='', format, page, quality, matte, density, archive}={}){
|
|
324
200
|
var {fromMime, toMime, expected} = new Format(),
|
|
325
|
-
archive = archive || 'canvas',
|
|
201
|
+
archive = ''+(archive || 'canvas'),
|
|
326
202
|
ext = format || extension.replace(/@\d+x$/i,'') || extname(filename),
|
|
327
203
|
format = fromMime(toMime(ext) || ext),
|
|
328
204
|
mime = toMime(format),
|