skia-canvas 2.0.2 → 3.0.1

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
@@ -16,21 +16,24 @@
16
16
  <a href="https://github.com/samizdatco/skia-canvas/discussions">Discussion Forum</a>
17
17
  </div>
18
18
 
19
- ---
19
+ <div align="center">
20
+
21
+ ### [Version 3.0 now available](https://github.com/samizdatco/skia-canvas/discussions/255)
22
+
23
+ </div>
20
24
 
21
- Skia Canvas is a browser-less implementation of the HTML Canvas drawing API for Node.js. It is based on Google’s [Skia](https://skia.org) graphics engine and, accordingly, produces very similar results to Chrome’s `<canvas>` element. The library is well suited for use on desktop machines where you can render hardware-accelerated graphics to a window and on the server where it can output a variety of image formats.
25
+ ---
22
26
 
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.
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.
24
28
 
25
29
  In particular, Skia Canvas:
26
30
 
27
- - is fast and compact since rendering takes place on the GPU and all the heavy lifting is done by native code written in Rust and C++
28
- - can render to [windows][window] using an OS-native graphics pipeline and provides a browser-like [UI event][win_bind] framework
29
- - generates images in both raster (JPEG, PNG, & WEBP) and vector (PDF & SVG) formats
30
- - can save images to [files][saveAs], return them as [Buffers][toBuffer], or encode [dataURL][toDataURL_ext] strings
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
31
34
  - 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][saveAs] them as a single, multi-page PDF or an image-sequence saved to multiple files
33
- - 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]
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]
34
37
  - provides [3D perspective][createProjection()] transformations in addition to [scaling][scale()], [rotation][rotate()], and [translation][translate()]
35
38
  - can fill shapes with vector-based [Textures][createTexture()] in addition to bitmap-based [Patterns][createPattern()] and supports line-drawing with custom [markers][lineDashMarker]
36
39
  - supports the full set of [CSS filter][filter] image processing operators
@@ -41,6 +44,7 @@ In particular, Skia Canvas:
41
44
  - proportional [letter-spacing][letterSpacing], [word-spacing][wordSpacing], and [leading][c2d_font]
42
45
  - support for [variable fonts][VariableFonts] and transparent mapping of weight values
43
46
  - 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
44
48
 
45
49
  ## Installation
46
50
 
@@ -51,68 +55,161 @@ npm install skia-canvas
51
55
 
52
56
  This will download a pre-compiled library from the project’s most recent [release](https://github.com/samizdatco/skia-canvas/releases).
53
57
 
54
- ## Platform Support
55
58
 
56
- The underlying Rust library uses [N-API][node_napi] v8 which allows it to run on Node.js versions:
57
- - v12.22+
58
- - v14.17+
59
- - v15.12+
60
- - v16.0.0 and later
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
61
 
62
- Pre-compiled binaries are available for:
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:
63
67
 
64
- - Linux (x64 & arm64)
65
- - macOS (x64 & Apple silicon)
66
- - Windows (x64)
68
+ ```bash
69
+ pnpm install skia-canvas --allow-build=skia-canvas
70
+ ```
67
71
 
68
- Nearly everything you need is statically linked into the library. A notable exception is the [Fontconfig](https://www.freedesktop.org/wiki/Software/fontconfig/) library which must be installed separately if you’re running on Linux.
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
+ ```
69
80
 
70
- ## Running in Docker
71
81
 
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
82
 
74
- 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:18`, `node:16`, `node:14-buster`, `node:12-buster`, `node:bullseye`, `node:buster`, or simply:
75
- ```dockerfile
76
- FROM node
77
- ```
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
78
90
 
79
- You can also use the ‘slim’ image if you manually install fontconfig:
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.
80
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:
81
96
  ```dockerfile
82
- FROM node:slim
83
- RUN apt-get update && apt-get install -y -q --no-install-recommends libfontconfig1
97
+ FROM node
84
98
  ```
85
99
 
86
100
  If you wish to use Alpine as the underlying distribution, you can start with something along the lines of:
87
101
 
88
102
  ```dockerfile
89
103
  FROM node:alpine
90
- RUN apk update && apk add fontconfig
91
104
  ```
92
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
+
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
+
93
171
  ## Compiling from Source
94
172
 
95
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.
96
174
 
97
175
  Start by installing:
98
176
 
99
- 1. The [Rust compiler](https://www.rust-lang.org/tools/install) and cargo package manager using [`rustup`](https://rust-lang.github.io/rustup/)
100
- 2. A C compiler toolchain (either LLVM/Clang or MSVC)
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)
101
180
  4. Python 3 (used by Skia's [build process](https://skia.org/docs/user/build/))
102
- 3. The [Ninja](https://ninja-build.org) build system
103
- 5. On Linux: Fontconfig and OpenSSL
181
+ 5. The [Ninja](https://ninja-build.org) build system
182
+ 6. On Linux: Fontconfig and OpenSSL
104
183
 
105
- [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. 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).
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).
106
185
 
107
- ## Multithreading
186
+ ## Global Settings
108
187
 
109
- When rendering canvases in the background (e.g., by using the asynchronous [saveAs][saveAs] 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.
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.
110
193
 
111
194
  For example, you can limit your asynchronous processing to two simultaneous tasks by running your script with:
112
195
  ```bash
113
196
  SKIA_CANVAS_THREADS=2 node my-canvas-script.js
114
197
  ```
115
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)
206
+ ```
207
+
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.
212
+
116
213
  ## Example Usage
117
214
 
118
215
  ### Generating image files
@@ -197,6 +294,87 @@ win.on("draw", e => {
197
294
  ctx.fill()
198
295
  })
199
296
  ```
297
+
298
+ ### Integrating with [Sharp.js][sharp]
299
+
300
+ ```js
301
+ import sharp from 'sharp'
302
+ import {Canvas, loadImage} from 'skia-canvas'
303
+
304
+ let canvas = new Canvas(400, 400),
305
+ ctx = canvas.getContext("2d"),
306
+ {width, height} = canvas,
307
+ [x, y] = [width/2, height/2]
308
+
309
+ ctx.fillStyle = 'red'
310
+ ctx.fillRect(0, 0, x, y)
311
+ ctx.fillStyle = 'orange'
312
+ ctx.fillRect(x, y, x, y)
313
+
314
+ // Render the canvas to a Sharp object on a background thread then desaturate
315
+ await canvas.toSharp().modulate({saturation:.25}).jpeg().toFile("faded.jpg")
316
+
317
+ // Convert an ImageData to a Sharp object and save a grayscale version
318
+ let imgData = ctx.getImageData(0, 0, width, height, {matte:'white', density:2})
319
+ await imgData.toSharp().grayscale().png().toFile("black-and-white.png")
320
+
321
+ // Create an image using Sharp then draw it to the canvas as an Image object
322
+ let sharpImage = sharp({create:{ width:x, height:y, channels:4, background:"skyblue" }})
323
+ let canvasImage = await loadImage(sharpImage)
324
+ ctx.drawImage(canvasImage, x, 0)
325
+ await canvas.saveAs('mosaic.png')
326
+ ```
327
+
328
+ ## Benchmarks
329
+ 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
+
331
+ [See full results here…](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-08-15/index.md)
332
+
333
+ ### [Startup latency](https://github.com/samizdatco/canvas-benchmarks/tree/main/tests/cold-start.js)
334
+ | Library | Per Run | Total Time (100 iterations) |
335
+ | -------------------- | --------- | --------------------------------------------- |
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) |
339
+ | *skia-canvas*    | `  <1 ms` | `  33 ms` ![ ](./docs/assets/benchmarks.svg#cold-start_skia-sync) |
340
+
341
+ ### [Bezier curves](https://github.com/samizdatco/canvas-benchmarks/tree/main/tests/beziers.js)
342
+ | Library | Per Run | Total Time (20 iterations) |
343
+ | ------------------------------------------------------------- | --------- | ------------------------------------------- |
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) |
349
+
350
+ ### [SVG to PNG](https://github.com/samizdatco/canvas-benchmarks/tree/main/tests/from-svg.js)
351
+ | Library | Per Run | Total Time (100 iterations) |
352
+ | -------------------------------------------------------------- | --------- | -------------------------------------------- |
353
+ | 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) |
358
+
359
+ ### [Scale/rotate images](https://github.com/samizdatco/canvas-benchmarks/tree/main/tests/image-blit.js)
360
+ | Library | Per Run | Total Time (50 iterations) |
361
+ | ---------------------------------------------------------------- | --------- | ---------------------------------------------- |
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) |
367
+
368
+ ### [Basic text](https://github.com/samizdatco/canvas-benchmarks/tree/main/tests/text.js)
369
+ | Library | Per Run | Total Time (200 iterations) |
370
+ | ---------------------------------------------------------- | --------- | ---------------------------------------- |
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
+
377
+
200
378
  ## Acknowledgements
201
379
 
202
380
  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 +382,7 @@ This project is deeply indebted to the work of the [Rust Skia project](https://g
204
382
 
205
383
  ### Notable contributors
206
384
 
207
- - [@mpaparno](https://github.com/mpaparno) contributed support for SVG rendering, raw image-buffer handling, WEBP import/export and numerous bugfixes
385
+ - [@mpaparno](https://github.com/mpaparno) contributed support for SVG rendering, raw image-buffer handling, WEBP import/export and numerous bug fixes
208
386
  - [@Salmondx](https://github.com/Salmondx) developed the initial Raw image loading & rendering routines
209
387
  - [@lucasmerlin](https://github.com/lucasmerlin) helped get GPU rendering working on Vulkan
210
388
  - [@cprecioso](https://github.com/cprecioso) & [@saantonandre](https://github.com/saantonandre) corrected and expanded upon the TypeScript type definitions
@@ -229,15 +407,18 @@ This project is deeply indebted to the work of the [Rust Skia project](https://g
229
407
  [p2d_round]: https://skia-canvas.org/api/path2d#round
230
408
  [p2d_simplify]: https://skia-canvas.org/api/path2d#simplify
231
409
  [p2d_trim]: https://skia-canvas.org/api/path2d#trim
232
- [saveAs]: https://skia-canvas.org/api/canvas#saveas
410
+ [toFile]: https://skia-canvas.org/api/canvas#tofile
233
411
  [textwrap]: https://skia-canvas.org/api/context#textwrap
234
412
  [toBuffer]: https://skia-canvas.org/api/canvas#tobuffer
235
- [toDataURL_ext]: https://skia-canvas.org/api/canvas#todataurl
413
+ [toURL]: https://skia-canvas.org/api/canvas#tourl
236
414
  [win_bind]: https://skia-canvas.org/api/window#on--off--once
237
415
  [window]: https://skia-canvas.org/api/window
238
416
  [multithreading]: https://skia-canvas.org/getting-started#multithreading
239
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
240
420
  [rayon]: https://crates.io/crates/rayon
421
+ [sharp]: https://sharp.pixelplumbing.com
241
422
  [VariableFonts]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
242
423
  [filter]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter
243
424
  [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
 
@@ -21,8 +24,6 @@ class Canvas{
21
24
  let elt = document.createElement('canvas'),
22
25
  pages = []
23
26
 
24
- Object.defineProperty(elt, "async", {value:true, writable:false, enumerable:true})
25
-
26
27
  for (var [prop, get] of Object.entries({
27
28
  png: () => asBuffer(elt, 'image/png'),
28
29
  jpg: () => asBuffer(elt, 'image/jpeg'),
@@ -42,7 +43,11 @@ class Canvas{
42
43
  return Object.assign(elt, {width, height}).getContext("2d")
43
44
  },
44
45
 
45
- saveAs(filename, args){
46
+ saveAs(){
47
+ throw Error("Canvas.saveAs() has been renamed to Canvas.toFile")
48
+ },
49
+
50
+ toFile(filename, args){
46
51
  args = typeof args=='number' ? {quality:args} : args
47
52
  let opts = exportOptions(this.pages, {filename, ...args}),
48
53
  {pattern, padding, mime, quality, matte, density, archive} = opts,
@@ -59,163 +64,18 @@ class Canvas{
59
64
  return asBuffer(canvas, mime, quality, matte)
60
65
  },
61
66
 
62
- [_toURL_]: elt.toDataURL.bind(elt),
63
- toDataURL(extension="png", args={}){
67
+ toURL(extension="png", args={}){
64
68
  args = typeof args=='number' ? {quality:args} : args
65
69
  let opts = exportOptions(this.pages, {extension, ...args}),
66
70
  {mime, quality, matte, pages, density} = opts,
67
71
  canvas = atScale(pages, density, matte)[0],
68
- url = canvas[canvas===elt ? _toURL_ : 'toDataURL'](mime, quality);
72
+ url = canvas.toDataURL(mime, quality);
69
73
  return Promise.resolve(url)
70
74
  }
71
75
  })
72
76
  }
73
77
  }
74
78
 
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
79
  //
220
80
  // Browser helpers for converting canvas elements to blobs/buffers/files/zips
221
81
  //
@@ -241,16 +101,23 @@ const asDownload = async (canvas, mime, quality, matte, filename) => {
241
101
  }
242
102
 
243
103
  const asZipDownload = async (pages, mime, quality, matte, archive, pattern, padding) => {
244
- let filenames = i => pattern.replace('{}', String(i+1).padStart(padding, '0')),
245
- folder = basename(archive, '.zip') || 'archive',
246
- zip = new Zip(folder)
247
-
248
- await Promise.all(pages.map(async (page, i) => {
249
- let filename = filenames(i) // serialize filename(s) before awaiting
250
- await zip.add(filename, await asBlob(page, mime, quality, matte))
251
- }))
252
-
253
- _download(`${folder}.zip`, zip.blob)
104
+ await import("jszip").then(async ({default:JSZip}) => {
105
+ let filenames = i => pattern.replace('{}', String(i+1).padStart(padding, '0')),
106
+ zip = new JSZip(),
107
+ folder = basename(archive, '.zip') || 'archive',
108
+ payload = zip.folder(folder)
109
+
110
+ await Promise.all(pages.map(async (page, i) => {
111
+ let filename = filenames(i) // serialize filename(s) before awaiting
112
+ payload.file(filename, await asBlob(page, mime, quality, matte))
113
+ }))
114
+
115
+ zip.generateAsync({type:"blob"})
116
+ .then(content => _download(`${folder}.zip`, content))
117
+ })
118
+ .catch(() => {
119
+ console.log("Multi-page downloads require JSZip to be bundled: https://www.npmjs.com/package/jszip")
120
+ })
254
121
  }
255
122
 
256
123
  const _download = (filename, blob) => {
@@ -318,11 +185,18 @@ class Format{
318
185
  // Validation of the options dict shared by the Canvas saveAs, toBuffer, and toDataURL methods
319
186
  //
320
187
 
321
- import {basename, extname} from 'path'
188
+ function basename(str, ext) {
189
+ let stub = str.substring(str.lastIndexOf('/') + 1)
190
+ return ext && stub.endsWith(ext) ? stub.slice(0, -ext.length) : stub
191
+ }
192
+
193
+ function extname(str){
194
+ return str.substring(str.lastIndexOf('.'))
195
+ }
322
196
 
323
197
  function exportOptions(pages, {filename='', extension='', format, page, quality, matte, density, archive}={}){
324
198
  var {fromMime, toMime, expected} = new Format(),
325
- archive = archive || 'canvas',
199
+ archive = ''+(archive || 'canvas'),
326
200
  ext = format || extension.replace(/@\d+x$/i,'') || extname(filename),
327
201
  format = fromMime(toMime(ext) || ext),
328
202
  mime = toMime(format),