penpot/render-wasm/README.md
Elena Torró 38b990ef90
🔧 Add exporter headless backend (#10875)
*  Add headless wasm render backend to the exporter

* ♻️ Move render-wasm bridge to common and split wasm builds

* 🔧 Upload builtin font variants in the wasm exporter

* ♻️ Move shared font and resources utils out of render_wasm

*  Fetch only the exported roots in the wasm exporter

*  Bound save_layer rects in the vector export path
2026-08-06 16:13:06 +02:00

87 lines
3.0 KiB
Markdown

# Penpot WASM render
This is the canvas-based WebAssembly render engine for Penpot.
## Rust & Emscripten
This project is a Rust crate that targets [Emscripten](https://emscripten.org/) (`wasm32-unknown-emscripten`).
We use `wasm32-unknown-emscripten` compilation target:
* It compiles Rust code into WASM
* It generates the JavaScript code (“glue”) to load and run the WASM code
![How Rust, Emscripten, and WASM are connected](docs/images/rust_wasm_schema.png)
### Skia
We use Skia, an Open Source 2D graphics library. In particular, the render engine uses Skia via [custom binaries](https://github.com/penpot/skia-binaries/releases/) of the [rust-skia crate](https://github.com/rust-skia/rust-skia).
## How to build
With the [Penpot Development Environment](https://help.penpot.app/technical-guide/developer/devenv/) running, create a new tab in the tmux.
```sh
cd penpot/render-wasm
./build
```
You can also use `./watch` to run the build on every change.
The build script will compile the project and copy the `.js` and `.wasm` files to their correct location within the frontend app.
### Render targets
The same Rust source produces two artifacts, which differ only in compiler
options:
| Target | Tuned for | Cargo profile | Consumed by |
| ---------- | --------- | ----------------- | ------------------------------ |
| `frontend` | speed | `release` (`-O3`) | `frontend/resources/public/js` |
| `export` | size | `size` (`-Oz`) | `exporter/resources/wasm` |
```sh
./build # both targets, frontend first
./build frontend # workspace / viewer renderer
./build export # headless exporter renderer
```
`./watch` still follows a single target (`frontend` unless you pass one),
since watching both would rebuild twice on every keystroke.
Each target keeps its own `CARGO_TARGET_DIR` (`target/<target>`), so switching
between them does not invalidate the other's cache. Set `BUILD_MODE=release`
(or `NODE_ENV=production`) for an optimized build; the default is `debug`.
Each target writes its own generated `shared.js` (the enum discriminants the
CLJS side compiles against) next to the code that imports it — respectively
`frontend/src/app/render_wasm/api/shared.js` and
`exporter/src/app/wasm/shared.js`. Neither build writes to the other's paths.
![Architecture overview](docs/images/architecture_schema.png)
Edit your local `frontend/resources/public/js/config.js` to add the following flags:
- `enable-feature-render-wasm` to enable this render engine.
- `enable-render-wasm-dpr` (optional), to enable using the device pixel ratio.
## How to test
We currently have two types of tests:
- Unit tests
```sh
cd penpot/render-wasm
./test
```
- [Visual Regression Test](./docs/visual_regression_tests.md)
## Technical documentation
- [Rendering Architecture (Live vs Vector/PDF)](./docs/rendering_architecture.md)
- [Serialization](./docs/serialization.md)
- [Tile Rendering](./docs/tile_rendering.md)
- [Texts](./docs/texts.md)