From 1f510ae22c91325c83a5b15af2603fc7d49fab9a Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Wed, 29 Jul 2026 10:31:10 +0200 Subject: [PATCH] :recycle: Move render-wasm bridge to common and split wasm builds --- .gitignore | 2 + backend/scripts/_env | 21 +--- common/deps.edn | 2 +- .../resources/fonts/gfonts.2025.11.28.json | 0 common/src/app/common/flags.cljc | 4 + common/src/app/common/render_wasm/README.md | 19 +++ .../app/common}/render_wasm/api/props.cljs | 16 +-- .../app/common}/render_wasm/api/shapes.cljs | 12 +- common/src/app/common/render_wasm/enums.clj | 54 +++++++++ .../common}/render_wasm/fallback_fonts.cljs | 2 +- .../src/app/common/render_wasm/gfonts.clj | 6 +- common/src/app/common/render_wasm/gfonts.cljs | 111 ++++++++++++++++++ .../src/app/common}/render_wasm/helpers.cljc | 4 +- .../src/app/common}/render_wasm/mem.cljs | 6 +- .../app/common}/render_wasm/mem/heap32.cljs | 2 +- .../app/common}/render_wasm/resources.cljs | 4 +- .../common}/render_wasm/serialize_shape.cljs | 6 +- .../app/common}/render_wasm/serializers.cljs | 6 +- .../render_wasm/serializers/color.cljs | 2 +- .../app/common}/render_wasm/text_content.cljs | 27 +++-- .../src/app/common}/render_wasm/wasm.cljs | 55 +++------ docker/images/Dockerfile.exporter | 3 +- exporter/deps.edn | 3 - exporter/package.json | 2 +- exporter/scripts/build | 17 +-- exporter/src/app/config.cljs | 7 +- exporter/src/app/core.cljs | 6 +- exporter/src/app/renderer.cljs | 10 +- exporter/src/app/renderer/wasm.cljs | 58 ++++----- exporter/src/app/wasm.cljs | 28 ++--- exporter/src/app/wasm/enums.cljs | 19 +++ exporter/src/app/wasm/gfonts.cljs | 43 ------- exporter/src/app/wasm/serialize.cljs | 8 +- exporter/src/app/wasm/text.cljs | 41 ++----- frontend/deps.edn | 3 +- frontend/package.json | 2 +- frontend/scripts/build | 2 +- frontend/scripts/build-fonts-preview.js | 2 +- frontend/src/app/main/data/workspace.cljs | 2 +- frontend/src/app/main/fonts.cljs | 48 ++------ frontend/src/app/main/render_viewer_wasm.cljs | 2 +- frontend/src/app/render_wasm/api.cljs | 18 +-- frontend/src/app/render_wasm/api/enums.cljs | 19 +++ frontend/src/app/render_wasm/api/fonts.cljs | 54 ++------- frontend/src/app/render_wasm/api/texts.cljs | 13 +- frontend/src/app/render_wasm/api/webgl.cljs | 2 +- frontend/src/app/render_wasm/text_editor.cljs | 12 +- frontend/src/app/worker/thumbnails.cljs | 2 +- frontend/src/debug.cljs | 6 +- .../render_wasm/process_objects_test.cljs | 4 +- .../text_editor_caret_color_test.cljs | 2 +- render-wasm/Cargo.toml | 4 + render-wasm/README.md | 28 +++++ render-wasm/_build_env | 80 ++++++++++--- render-wasm/build | 23 +++- render-wasm/lint | 2 +- render-wasm/test | 2 +- render-wasm/watch | 7 +- 58 files changed, 538 insertions(+), 407 deletions(-) rename {frontend => common}/resources/fonts/gfonts.2025.11.28.json (100%) create mode 100644 common/src/app/common/render_wasm/README.md rename {frontend/src/app => common/src/app/common}/render_wasm/api/props.cljs (96%) rename {frontend/src/app => common/src/app/common}/render_wasm/api/shapes.cljs (96%) create mode 100644 common/src/app/common/render_wasm/enums.clj rename {frontend/src/app => common/src/app/common}/render_wasm/fallback_fonts.cljs (99%) rename frontend/src/app/main/fonts.clj => common/src/app/common/render_wasm/gfonts.clj (97%) create mode 100644 common/src/app/common/render_wasm/gfonts.cljs rename {frontend/src/app => common/src/app/common}/render_wasm/helpers.cljc (94%) rename {frontend/src/app => common/src/app/common}/render_wasm/mem.cljs (97%) rename {frontend/src/app => common/src/app/common}/render_wasm/mem/heap32.cljs (97%) rename {frontend/src/app => common/src/app/common}/render_wasm/resources.cljs (93%) rename {frontend/src/app => common/src/app/common}/render_wasm/serialize_shape.cljs (93%) rename {frontend/src/app => common/src/app/common}/render_wasm/serializers.cljs (99%) rename {frontend/src/app => common/src/app/common}/render_wasm/serializers/color.cljs (90%) rename {frontend/src/app => common/src/app/common}/render_wasm/text_content.cljs (90%) rename {frontend/src/app => common/src/app/common}/render_wasm/wasm.cljs (58%) create mode 100644 exporter/src/app/wasm/enums.cljs delete mode 100644 exporter/src/app/wasm/gfonts.cljs create mode 100644 frontend/src/app/render_wasm/api/enums.cljs diff --git a/.gitignore b/.gitignore index 76da22b35f..b0bf2099b5 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,8 @@ opencode.json /docker/images/bundle* /exporter/target /exporter/.shadow-cljs +/exporter/resources/wasm/ +/exporter/src/app/wasm/shared.js /frontend/.storybook/preview-body.html /frontend/.storybook/preview-head.html /frontend/playwright-report/ diff --git a/backend/scripts/_env b/backend/scripts/_env index ee231262f1..6e685ee207 100644 --- a/backend/scripts/_env +++ b/backend/scripts/_env @@ -12,6 +12,10 @@ export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key # PENPOT_DATABASE_*, PENPOT_REDIS_URI, PENPOT_OBJECTS_STORAGE_*, AWS_*) is owned by # docker/devenv/defaults.env and injected via the main service's env block. +if [ -f /home/selfsigned.crt ]; then + export NODE_EXTRA_CA_CERTS=/home/selfsigned.crt; +fi + # Background worker flag is per-instance. Defaults to enabled (ws0); ws1+ # overlays set PENPOT_BACKEND_WORKER=false so scheduled and async tasks only # run on ws0, keeping notification Pub/Sub bound to a single Valkey. See @@ -73,21 +77,6 @@ export PENPOT_USER_FEEDBACK_DESTINATION="support@example.com" export PENPOT_NITRATE_BACKEND_URI=http://localhost:3000/admin-console -# Headless WASM export: render `:is-wasm` exports via the in-process Skia/WASM -# pipeline (no browser). Reads the render-wasm artifact from PENPOT_WASM_DIR, -# defaulting to ../frontend/resources/public/js (render-wasm.js/.wasm) — which -# is where render-wasm/build leaves it in devenv. The docker image ships the -# artifact inside the exporter bundle and points PENPOT_WASM_DIR at it. -# -# PENPOT_INTERNAL_URI must be set with it: the devenv public-uri is https with -# a self-signed cert that undici rejects. Port 3450 is the same backend over -# plain HTTP, and follows PENPOT_PUBLIC_HTTP_PORT. Leave it unset otherwise — -# the exporter falls back to public-uri, which the Playwright backends want. -# export PENPOT_WASM_HEADLESS=true -# export PENPOT_INTERNAL_URI=http://localhost:3450 -# export PENPOT_WASM_DIR=../frontend/resources/public/js -# export PENPOT_WASM_IMAGE_CACHE_MB=256 - export JAVA_OPTS="\ -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \ -Djdk.attach.allowAttachSelf \ @@ -112,5 +101,3 @@ function setup_minio() { mc alias set penpot-s3/ "${PENPOT_OBJECTS_STORAGE_S3_ENDPOINT}" minioadmin minioadmin -q mc mb "penpot-s3/${PENPOT_OBJECTS_STORAGE_S3_BUCKET}" -p -q } - - diff --git a/common/deps.edn b/common/deps.edn index edffb487f0..36f4b63b16 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -55,7 +55,7 @@ io.aviso/pretty {:mvn/version "1.4.4"} environ/environ {:mvn/version "1.2.0"}} - :paths ["src" "vendor" "target/classes"] + :paths ["src" "vendor" "resources" "target/classes"] :aliases {:dev {:extra-deps diff --git a/frontend/resources/fonts/gfonts.2025.11.28.json b/common/resources/fonts/gfonts.2025.11.28.json similarity index 100% rename from frontend/resources/fonts/gfonts.2025.11.28.json rename to common/resources/fonts/gfonts.2025.11.28.json diff --git a/common/src/app/common/flags.cljc b/common/src/app/common/flags.cljc index 27519d487c..26529cd87f 100644 --- a/common/src/app/common/flags.cljc +++ b/common/src/app/common/flags.cljc @@ -178,6 +178,10 @@ :stroke-path :stroke-per-side + ;; Exporter only: uses render-wasm for export instead of browser + ;; renderer. + :wasm-export + :custom-shortcuts}) (def all-flags diff --git a/common/src/app/common/render_wasm/README.md b/common/src/app/common/render_wasm/README.md new file mode 100644 index 0000000000..440fac81f7 --- /dev/null +++ b/common/src/app/common/render_wasm/README.md @@ -0,0 +1,19 @@ +# `app.common.render-wasm.*` + +The host-agnostic ClojureScript side of the render-wasm binary protocol: byte +layouts, memory helpers and serializers that turn Penpot shapes into the buffers +`render-wasm` consumes, plus the google fonts catalog (baked from +`common/resources/fonts/gfonts.*.json`). + +The workspace drives it from `app.render-wasm.*`, the headless exporter from +`app.wasm.*` — same code underneath, so the two cannot drift. + +`shared.js` is not here: it is a per-build artifact, so each host compiles +against the copy from its own render-wasm build and passes it to +`wasm/init-serializers!` (see `app.render-wasm.api.enums`, `app.wasm.enums`). + +## Rules for anything added here + +**Nothing here may depend on a browser (no DOM, no WebGL, no app state) or on +`frontend/src`.** Dependencies are `app.common.*` and this subtree only. It also +has to run under plain Node — a `js/document` here breaks the exporter. diff --git a/frontend/src/app/render_wasm/api/props.cljs b/common/src/app/common/render_wasm/api/props.cljs similarity index 96% rename from frontend/src/app/render_wasm/api/props.cljs rename to common/src/app/common/render_wasm/api/props.cljs index 26bbdadc3a..6caaac7de8 100644 --- a/frontend/src/app/render_wasm/api/props.cljs +++ b/common/src/app/common/render_wasm/api/props.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC Sucursal en España SL -(ns app.render-wasm.api.props +(ns app.common.render-wasm.api.props "Browser-free WASM shape property setters, shared by the workspace render orchestrator (`app.render-wasm.api`) and the headless exporter (`app.wasm.serialize`). @@ -15,15 +15,15 @@ data sources (fonts, image bytes, SVG static markup) stay in `app.render-wasm.api`." (:require [app.common.math :as mth] + [app.common.render-wasm.helpers :as h] + [app.common.render-wasm.mem :as mem] + [app.common.render-wasm.mem.heap32 :as mem.h32] + [app.common.render-wasm.serializers :as sr] + [app.common.render-wasm.serializers.color :as sr-clr] + [app.common.render-wasm.wasm :as wasm] [app.common.types.fills :as types.fills] [app.common.types.fills.impl :as types.fills.impl] - [app.common.types.path :as path] - [app.render-wasm.helpers :as h] - [app.render-wasm.mem :as mem] - [app.render-wasm.mem.heap32 :as mem.h32] - [app.render-wasm.serializers :as sr] - [app.render-wasm.serializers.color :as sr-clr] - [app.render-wasm.wasm :as wasm])) + [app.common.types.path :as path])) (def ^:const MAX_BUFFER_CHUNK_SIZE (* 256 1024)) diff --git a/frontend/src/app/render_wasm/api/shapes.cljs b/common/src/app/common/render_wasm/api/shapes.cljs similarity index 96% rename from frontend/src/app/render_wasm/api/shapes.cljs rename to common/src/app/common/render_wasm/api/shapes.cljs index 02c2c91ee2..2469ab8548 100644 --- a/frontend/src/app/render_wasm/api/shapes.cljs +++ b/common/src/app/common/render_wasm/api/shapes.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC Sucursal en España SL -(ns app.render-wasm.api.shapes +(ns app.common.render-wasm.api.shapes "Batched shape property serialization for improved WASM performance. This module provides a single WASM call to set all base shape properties, @@ -13,11 +13,11 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.uuid :as uuid] - [app.render-wasm.helpers :as h] - [app.render-wasm.mem :as mem] - [app.render-wasm.serializers :as sr] - [app.render-wasm.wasm :as wasm])) + [app.common.render-wasm.helpers :as h] + [app.common.render-wasm.mem :as mem] + [app.common.render-wasm.serializers :as sr] + [app.common.render-wasm.wasm :as wasm] + [app.common.uuid :as uuid])) ;; Binary layout constants matching Rust implementation: ;; diff --git a/common/src/app/common/render_wasm/enums.clj b/common/src/app/common/render_wasm/enums.clj new file mode 100644 index 0000000000..3bc382ec8d --- /dev/null +++ b/common/src/app/common/render_wasm/enums.clj @@ -0,0 +1,54 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC Sucursal en España SL + +(ns app.common.render-wasm.enums + "Serializer enum table from `shared.js`") + +(def ^:private serializer-exports + [["raster-format" "RasterFormat"] + ["blur-type" "RawBlurType"] + ["blend-mode" "RawBlendMode"] + ["bool-type" "RawBoolType"] + ["font-style" "RawFontStyle"] + ["flex-direction" "RawFlexDirection"] + ["grid-direction" "RawGridDirection"] + ["grow-type" "RawGrowType"] + ["align-items" "RawAlignItems"] + ["align-self" "RawAlignSelf"] + ["align-content" "RawAlignContent"] + ["justify-items" "RawJustifyItems"] + ["justify-content" "RawJustifyContent"] + ["justify-self" "RawJustifySelf"] + ["wrap-type" "RawWrapType"] + ["grid-track-type" "RawGridTrackType"] + ["shadow-style" "RawShadowStyle"] + ["guide-kind" "RawGuideKind"] + ["stroke-style" "RawStrokeStyle"] + ["stroke-cap" "RawStrokeCap"] + ["shape-type" "RawShapeType"] + ["constraint-h" "RawConstraintH"] + ["constraint-v" "RawConstraintV"] + ["sizing" "RawSizing"] + ["vertical-align" "RawVerticalAlign"] + ["fill-data" "RawFillData"] + ["text-align" "RawTextAlign"] + ["text-direction" "RawTextDirection"] + ["text-decoration" "RawTextDecoration"] + ["text-transform" "RawTextTransform"] + ["multiple-state" "MultipleState"] + ["transform-entry-kind" "RawTransformEntryKind"] + ["segment-data" "RawSegmentData"] + ["stroke-linecap" "RawStrokeLineCap"] + ["stroke-linejoin" "RawStrokeLineJoin"] + ["fill-rule" "RawFillRule"]]) + +(defmacro serializers + [alias] + (let [alias (name alias)] + `(cljs.core/js-obj + ~@(mapcat (fn [[key export]] + [key (symbol alias export)]) + serializer-exports)))) diff --git a/frontend/src/app/render_wasm/fallback_fonts.cljs b/common/src/app/common/render_wasm/fallback_fonts.cljs similarity index 99% rename from frontend/src/app/render_wasm/fallback_fonts.cljs rename to common/src/app/common/render_wasm/fallback_fonts.cljs index 80f52be51c..52760f6a5b 100644 --- a/frontend/src/app/render_wasm/fallback_fonts.cljs +++ b/common/src/app/common/render_wasm/fallback_fonts.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC Sucursal en España SL -(ns app.render-wasm.fallback-fonts +(ns app.common.render-wasm.fallback-fonts "Host-agnostic fallback-font knowledge: which scripts/emoji a text uses and which (google) fallback fonts cover them. Pure data + pure fns — no browser or Node dependencies — so the workspace (`api.texts`/`api.fonts`) and the diff --git a/frontend/src/app/main/fonts.clj b/common/src/app/common/render_wasm/gfonts.clj similarity index 97% rename from frontend/src/app/main/fonts.clj rename to common/src/app/common/render_wasm/gfonts.clj index c8abbd0478..3e03c0ca6d 100644 --- a/frontend/src/app/main/fonts.clj +++ b/common/src/app/common/render_wasm/gfonts.clj @@ -4,8 +4,9 @@ ;; ;; Copyright (c) KALEIDOS INC Sucursal en España SL -(ns app.main.fonts +(ns app.common.render-wasm.gfonts "A fonts loading macros." + (:require [app.common.uuid :as uuid] [clojure.data.json :as json] @@ -47,6 +48,3 @@ (let [data (slurp (io/resource path)) data (json/read-str data)] `~(mapv parse-gfont (get data "items")))) - - - diff --git a/common/src/app/common/render_wasm/gfonts.cljs b/common/src/app/common/render_wasm/gfonts.cljs new file mode 100644 index 0000000000..cd6cdb9c9d --- /dev/null +++ b/common/src/app/common/render_wasm/gfonts.cljs @@ -0,0 +1,111 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC Sucursal en España SL + +(ns app.common.render-wasm.gfonts + (:require-macros [app.common.render-wasm.gfonts :refer [preload-gfonts]]) + (:require + [app.common.data :as d] + [app.common.uuid :as uuid] + [cuerdas.core :as str])) + +;; --- catalog + +(def catalog + (preload-gfonts "fonts/gfonts.2025.11.28.json")) + +(def ^:private by-id + (reduce (fn [m font] (assoc m (:id font) font)) {} catalog)) + +(def ^:private by-uuid + (reduce (fn [m font] (assoc m (:uuid font) font)) {} catalog)) + +(defn gfont-id->uuid + "Maps a `gfont-` id to its (compilation-stable) catalog uuid, or nil." + [gfont-id] + (:uuid (get by-id gfont-id))) + +;; --- font-id -> wasm uuid + +(def ^:private custom-prefix "custom-") +(def ^:private gfont-prefix "gfont-") + +(defn font-id->uuid + "Maps a content font-id to the uuid WASM keys fonts by: + + - `gfont-` -> the catalog uuid, + - `custom-` -> that uuid, + - anything else (builtin, unknown, malformed) -> `uuid/zero`, which WASM + resolves to the default font." + + [font-id] + (cond + (not (string? font-id)) + uuid/zero + + (str/starts-with? font-id gfont-prefix) + (or (gfont-id->uuid font-id) uuid/zero) + + (str/starts-with? font-id custom-prefix) + (or (uuid/parse* (subs font-id (count custom-prefix))) uuid/zero) + + :else + uuid/zero)) + +;; --- proxy urls + +(def ^:private gstatic-prefix + "https://fonts.gstatic.com/s") + +(defn gstatic->proxy-url + [s base] + (let [base (str/rtrim (str base) "/")] + (str/replace (str s) gstatic-prefix base))) + +;; --- variant resolution + +(defn closest-variant + [variants target-weight target-style] + (when-let [target-weight (d/parse-integer target-weight)] + (let [result + (reduce + (fn [closest-match variant] + (let [weight (d/parse-integer (:weight variant)) + distance (abs (- target-weight weight)) + matches-style? (= target-style (:style variant)) + current {:variant variant + :weight weight + :distance distance}] + (cond + ;; Exact match found + (and (zero? distance) + (if target-style matches-style? true)) + (reduced current) + + (nil? closest-match) current + + ;; Update best match if this variant is closer or equal distance but higher weight + (or (< distance (:distance closest-match)) + (and (= distance (:distance closest-match)) + (> weight (:weight closest-match)))) + current + + ;; Same weight as the `closest-match` but the style matches `target-style` + (and (= weight (:weight closest-match)) matches-style?) + current + + :else + closest-match))) + nil + variants)] + (:variant result)))) + +(defn resolve-ttf-url + [font-uuid weight style] + (when-let [font (get by-uuid font-uuid)] + (let [style (if (zero? style) "normal" "italic") + variants (:variants font)] + (:ttf-url (or (closest-variant variants weight style) + (first variants)))))) diff --git a/frontend/src/app/render_wasm/helpers.cljc b/common/src/app/common/render_wasm/helpers.cljc similarity index 94% rename from frontend/src/app/render_wasm/helpers.cljc rename to common/src/app/common/render_wasm/helpers.cljc index 452e3f1eb4..236a952248 100644 --- a/frontend/src/app/render_wasm/helpers.cljc +++ b/common/src/app/common/render_wasm/helpers.cljc @@ -4,8 +4,8 @@ ;; ;; Copyright (c) KALEIDOS INC Sucursal en España SL -(ns app.render-wasm.helpers - #?(:cljs (:require-macros [app.render-wasm.helpers])) +(ns app.common.render-wasm.helpers + #?(:cljs (:require-macros [app.common.render-wasm.helpers])) (:require [app.common.data :as d])) (def error-code diff --git a/frontend/src/app/render_wasm/mem.cljs b/common/src/app/common/render_wasm/mem.cljs similarity index 97% rename from frontend/src/app/render_wasm/mem.cljs rename to common/src/app/common/render_wasm/mem.cljs index d90d2f7fa7..3a64315ba0 100644 --- a/frontend/src/app/render_wasm/mem.cljs +++ b/common/src/app/common/render_wasm/mem.cljs @@ -4,11 +4,11 @@ ;; ;; Copyright (c) KALEIDOS INC Sucursal en España SL -(ns app.render-wasm.mem +(ns app.common.render-wasm.mem (:require [app.common.buffer :as buf] - [app.render-wasm.helpers :as h] - [app.render-wasm.wasm :as wasm])) + [app.common.render-wasm.helpers :as h] + [app.common.render-wasm.wasm :as wasm])) (defn ->offset-32 "Convert a 8-bit (1 byte) offset to a 32-bit (4 bytes) offset" diff --git a/frontend/src/app/render_wasm/mem/heap32.cljs b/common/src/app/common/render_wasm/mem/heap32.cljs similarity index 97% rename from frontend/src/app/render_wasm/mem/heap32.cljs rename to common/src/app/common/render_wasm/mem/heap32.cljs index 07ac6b749f..bfe7c49053 100644 --- a/frontend/src/app/render_wasm/mem/heap32.cljs +++ b/common/src/app/common/render_wasm/mem/heap32.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC Sucursal en España SL -(ns app.render-wasm.mem.heap32 +(ns app.common.render-wasm.mem.heap32 "A memory write helpers that uses 32 bits addressed offsets." (:require [app.common.data.macros :as dm] diff --git a/frontend/src/app/render_wasm/resources.cljs b/common/src/app/common/render_wasm/resources.cljs similarity index 93% rename from frontend/src/app/render_wasm/resources.cljs rename to common/src/app/common/render_wasm/resources.cljs index 9f564aa990..f42fccf76e 100644 --- a/frontend/src/app/render_wasm/resources.cljs +++ b/common/src/app/common/render_wasm/resources.cljs @@ -4,12 +4,12 @@ ;; ;; Copyright (c) KALEIDOS INC Sucursal en España SL -(ns app.render-wasm.resources +(ns app.common.render-wasm.resources "Host-agnostic enumeration of the external resources a scene needs to render: which image bytes its shapes reference. Pure data walking — no browser or Node dependencies — so the workspace and the headless exporter derive the same set from the same source (sibling of - `app.render-wasm.fallback-fonts`, which does the same for fonts)." + `app.common.render-wasm.fallback-fonts`, which does the same for fonts)." (:require [app.common.types.fills :as types.fills])) diff --git a/frontend/src/app/render_wasm/serialize_shape.cljs b/common/src/app/common/render_wasm/serialize_shape.cljs similarity index 93% rename from frontend/src/app/render_wasm/serialize_shape.cljs rename to common/src/app/common/render_wasm/serialize_shape.cljs index ffc659757e..a0e776a62f 100644 --- a/frontend/src/app/render_wasm/serialize_shape.cljs +++ b/common/src/app/common/render_wasm/serialize_shape.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC Sucursal en España SL -(ns app.render-wasm.serialize-shape +(ns app.common.render-wasm.serialize-shape "Single source of truth for the host-independent part of serializing a whole shape into the WASM design state. @@ -24,8 +24,8 @@ The incremental workspace edit path (`set-wasm-attr!`) is unaffected; it keeps dispatching per changed key through the same underlying `props` setters." (:require - [app.render-wasm.api.props :as props] - [app.render-wasm.api.shapes :as shapes])) + [app.common.render-wasm.api.props :as props] + [app.common.render-wasm.api.shapes :as shapes])) (defn serialize-shape! "Applies every host-independent WASM property of `shape`. `set-shape-base-props` diff --git a/frontend/src/app/render_wasm/serializers.cljs b/common/src/app/common/render_wasm/serializers.cljs similarity index 99% rename from frontend/src/app/render_wasm/serializers.cljs rename to common/src/app/common/render_wasm/serializers.cljs index 83ecf4f4d2..089785da23 100644 --- a/frontend/src/app/render_wasm/serializers.cljs +++ b/common/src/app/common/render_wasm/serializers.cljs @@ -4,16 +4,16 @@ ;; ;; Copyright (c) KALEIDOS INC Sucursal en España SL - (ns app.render-wasm.serializers + (ns app.common.render-wasm.serializers (:require [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] + [app.common.render-wasm.serializers.color :as sr-clr] + [app.common.render-wasm.wasm :as wasm] [app.common.types.color :as clr] [app.common.types.shape-tree :as ctst] [app.common.uuid :as uuid] - [app.render-wasm.serializers.color :as sr-clr] - [app.render-wasm.wasm :as wasm] [cuerdas.core :as str])) (defn u8 diff --git a/frontend/src/app/render_wasm/serializers/color.cljs b/common/src/app/common/render_wasm/serializers/color.cljs similarity index 90% rename from frontend/src/app/render_wasm/serializers/color.cljs rename to common/src/app/common/render_wasm/serializers/color.cljs index 7d5d28b8c7..c3ef27fdfc 100644 --- a/frontend/src/app/render_wasm/serializers/color.cljs +++ b/common/src/app/common/render_wasm/serializers/color.cljs @@ -1,4 +1,4 @@ -(ns app.render-wasm.serializers.color +(ns app.common.render-wasm.serializers.color (:require [app.common.math :as mth])) diff --git a/frontend/src/app/render_wasm/text_content.cljs b/common/src/app/common/render_wasm/text_content.cljs similarity index 90% rename from frontend/src/app/render_wasm/text_content.cljs rename to common/src/app/common/render_wasm/text_content.cljs index 457a633066..9f7333eefb 100644 --- a/frontend/src/app/render_wasm/text_content.cljs +++ b/common/src/app/common/render_wasm/text_content.cljs @@ -4,23 +4,24 @@ ;; ;; Copyright (c) KALEIDOS INC Sucursal en España SL -(ns app.render-wasm.text-content +(ns app.common.render-wasm.text-content "Single source of truth for writing a text shape's content into the WASM design state. The binary layout ([num-spans][paragraph attrs][span attrs][text]) is - identical for the workspace and the headless exporter — only *font resolution* - differs (the workspace uses the loaded fonts DB; the exporter uses its gfonts - catalog + custom variants). So the byte-writing lives here and font resolution - is injected via the `opts` map passed to `write-shape-text!`. + identical for the workspace and the headless exporter, and so is the font-id + -> uuid mapping (`gf/font-id->uuid`). Only *variant* resolution differs — + the workspace has a loaded fonts DB, the exporter does not — so that part is + injected via the `opts` map passed to `write-shape-text!`. Fully portable (no store/DOM/React), so it runs under Node too." (:require [app.common.data :as d] + [app.common.render-wasm.gfonts :as gf] + [app.common.render-wasm.helpers :as h] + [app.common.render-wasm.mem :as mem] + [app.common.render-wasm.serializers :as sr] + [app.common.render-wasm.wasm :as wasm] [app.common.types.fills.impl :as types.fills.impl] [app.common.uuid :as uuid] - [app.render-wasm.helpers :as h] - [app.render-wasm.mem :as mem] - [app.render-wasm.serializers :as sr] - [app.render-wasm.wasm :as wasm] [cuerdas.core :as str])) (def ^:const PARAGRAPH-ATTR-U8-SIZE 12) @@ -169,13 +170,15 @@ "Writes one paragraph's spans + text into WASM and appends it to the current shape via `_set_shape_text_content`. - `opts` injects host-specific font resolution: - - `:normalize-font-id` (string font-id -> wasm uuid) — required in practice, + `opts` injects host-specific font handling: + - `:normalize-font-id` (string font-id -> wasm uuid) defaults to the shared + `gf/font-id->uuid`, which is what both hosts want — a host only + overrides it if it keys its font store some other way, - `:normalize-paragraph`/`:normalize-span` — font-variant normalization from a fonts DB (workspace); default to identity (the exporter resolves variants differently / not at all)." [spans paragraph text {:keys [normalize-font-id normalize-paragraph normalize-span] - :or {normalize-font-id identity + :or {normalize-font-id gf/font-id->uuid normalize-paragraph identity normalize-span (fn [span _paragraph] span)}}] (let [paragraph (normalize-paragraph paragraph) diff --git a/frontend/src/app/render_wasm/wasm.cljs b/common/src/app/common/render_wasm/wasm.cljs similarity index 58% rename from frontend/src/app/render_wasm/wasm.cljs rename to common/src/app/common/render_wasm/wasm.cljs index 933b200530..657b44047d 100644 --- a/frontend/src/app/render_wasm/wasm.cljs +++ b/common/src/app/common/render_wasm/wasm.cljs @@ -4,8 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC Sucursal en España SL -(ns app.render-wasm.wasm - (:require ["./api/shared.js" :as shared])) +(ns app.common.render-wasm.wasm) (defonce internal-frame-id nil) (defonce internal-frame-type 0) @@ -65,41 +64,19 @@ (set! gl-context nil) (set! context-initialized? false)) -(defonce serializers - #js {:raster-format shared/RasterFormat - :blur-type shared/RawBlurType - :blend-mode shared/RawBlendMode - :bool-type shared/RawBoolType - :font-style shared/RawFontStyle - :flex-direction shared/RawFlexDirection - :grid-direction shared/RawGridDirection - :grow-type shared/RawGrowType - :align-items shared/RawAlignItems - :align-self shared/RawAlignSelf - :align-content shared/RawAlignContent - :justify-items shared/RawJustifyItems - :justify-content shared/RawJustifyContent - :justify-self shared/RawJustifySelf - :wrap-type shared/RawWrapType - :grid-track-type shared/RawGridTrackType - :shadow-style shared/RawShadowStyle - :guide-kind shared/RawGuideKind - :stroke-style shared/RawStrokeStyle - :stroke-cap shared/RawStrokeCap - :shape-type shared/RawShapeType - :constraint-h shared/RawConstraintH - :constraint-v shared/RawConstraintV - :sizing shared/RawSizing - :vertical-align shared/RawVerticalAlign - :fill-data shared/RawFillData - :text-align shared/RawTextAlign - :text-direction shared/RawTextDirection - :text-decoration shared/RawTextDecoration - :text-transform shared/RawTextTransform - :multiple-state shared/MultipleState - :transform-entry-kind shared/RawTransformEntryKind - :segment-data shared/RawSegmentData - :stroke-linecap shared/RawStrokeLineCap - :stroke-linejoin shared/RawStrokeLineJoin - :fill-rule shared/RawFillRule}) +(defonce serializers nil) + +(defn init-serializers! + "Binds the enum table produced by the `enums/serializers` macro." + [table] + (let [missing (array)] + (doseq [key (js/Object.keys table)] + (when (undefined? (unchecked-get table key)) + (.push missing key))) + + (when (pos? (alength missing)) + (throw (ex-info "stale or incomplete render-wasm shared.js" + {:missing (vec missing)}))) + + (set! serializers table))) diff --git a/docker/images/Dockerfile.exporter b/docker/images/Dockerfile.exporter index 853b1ca3cf..7c0b1a14ff 100644 --- a/docker/images/Dockerfile.exporter +++ b/docker/images/Dockerfile.exporter @@ -5,8 +5,7 @@ ENV LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 \ DEBIAN_FRONTEND=noninteractive \ PATH=/opt/imagick/bin:$PATH \ - PLAYWRIGHT_BROWSERS_PATH=/opt/penpot/browsers \ - PENPOT_WASM_DIR=/opt/penpot/exporter/js + PLAYWRIGHT_BROWSERS_PATH=/opt/penpot/browsers RUN set -ex; \ apt-get -qq update; \ diff --git a/exporter/deps.edn b/exporter/deps.edn index 5197dd2173..9495142719 100644 --- a/exporter/deps.edn +++ b/exporter/deps.edn @@ -1,9 +1,6 @@ {:paths ["src" "vendor" "resources" "test"] :deps {penpot/common {:local/root "../common"} - ;; For the CLJS side of the render-wasm binary protocol (`app.render-wasm.*`), - ;; shared with the workspace so the two cannot drift. - penpot/frontend {:local/root "../frontend"} org.clojure/clojure {:mvn/version "1.12.5"} binaryage/devtools {:mvn/version "1.0.7"} metosin/reitit-core {:mvn/version "0.10.1"} diff --git a/exporter/package.json b/exporter/package.json index 3b01f846be..31ff04ccfb 100644 --- a/exporter/package.json +++ b/exporter/package.json @@ -33,7 +33,7 @@ "watch:app": "pnpm run clear:shadow-cache && clojure -M:dev:shadow-cljs watch main", "watch": "pnpm run watch:app", "build:app": "clojure -M:dev:shadow-cljs release main", - "build:wasm": "../render-wasm/build", + "build:wasm": "../render-wasm/build export", "build": "pnpm run clear:shadow-cache && pnpm run build:app", "fmt": "cljfmt fix --parallel=true src/", "check-fmt": "cljfmt check --parallel=true src/", diff --git a/exporter/scripts/build b/exporter/scripts/build index 87b407c18b..a240c17b75 100755 --- a/exporter/scripts/build +++ b/exporter/scripts/build @@ -8,18 +8,10 @@ export NODE_ENV=production; corepack enable; corepack install || exit 1; pnpm install || exit 1; - -# The exporter compiles against render-wasm's `shared.js` and ships its `.wasm` -# in the bundle, so build it here rather than depending on some other module's -# build having run first — same as `frontend/scripts/build` does. pnpm run build:wasm; -# Both are produced by `render-wasm/build` and are git-ignored. Verify they -# arrived where this build expects: `shared.js` is a compile dependency, so a -# path drift would surface as an opaque dependency trace from `pnpm run build` -# below rather than the message here. -WASM_SRC="../frontend/resources/public/js"; -WASM_SHARED="../frontend/common/src/app/render_wasm_common/api/shared.js"; +WASM_SRC="resources/wasm"; +WASM_SHARED="src/app/wasm/shared.js"; if [ ! -f "$WASM_SRC/render-wasm.wasm" ] || [ ! -f "$WASM_SHARED" ]; then echo "ERROR: the render-wasm build did not produce:" >&2; echo " $WASM_SRC/render-wasm.wasm" >&2; @@ -37,9 +29,8 @@ cp pnpm-workspace.yaml target/; cp package.json target/; touch target/pnpm-workspace.yaml; -# Ship the artifact in the bundle; the docker image points PENPOT_WASM_DIR here. -mkdir -p target/js; -cp "$WASM_SRC/render-wasm.js" "$WASM_SRC/render-wasm.wasm" target/js/; +mkdir -p target/$WASM_SRC; +cp "$WASM_SRC/render-wasm.js" "$WASM_SRC/render-wasm.wasm" target/$WASM_SRC/; cat < serialize -> provision fonts and images -> relayout text with the real fonts -> render each object. @@ -26,6 +25,9 @@ [app.common.geom.point] [app.common.geom.rect] [app.common.logging :as l] + [app.common.render-wasm.fallback-fonts :as fbf] + [app.common.render-wasm.gfonts :as gf] + [app.common.render-wasm.resources :as resources] [app.common.transit :as t] [app.common.types.fills.impl] [app.common.types.objects-map] @@ -34,12 +36,9 @@ [app.common.uri :as u] [app.common.uuid :as uuid] [app.config :as cf] - [app.render-wasm.fallback-fonts :as fbf] - [app.render-wasm.resources :as resources] [app.util.mime :as mime] [app.util.shell :as sh] [app.wasm :as wasm] - [app.wasm.gfonts :as gfonts] [app.wasm.serialize :as serialize] [cuerdas.core :as str] [promesa.core :as p])) @@ -72,9 +71,8 @@ ;; --- backend endpoints ;; -;; Every fetch targets the internal endpoint (falling back to public-uri), same -;; as the Playwright backends: in a deployment the exporter reaches the backend -;; over the container network, not the public ingress. +;; Every fetch targets the internal endpoint (falling back to public-uri), +;; in a deployment the exporter reaches the backend over the container network (defn- internal-uri "Absolute URI for `path` on the internal (backend) endpoint." @@ -146,9 +144,9 @@ share-id (assoc :share-id share-id))) uri (internal-uri "api/rpc/command/get-page")] (l/dbg :hint "wasm render: get-page" - :uri uri - :file-id (str file-id) - :page-id (str page-id)) + :uri uri + :file-id (str file-id) + :page-id (str page-id)) (->> (fetch! uri #js {:method "POST" :headers headers :body body}) (p/mcat (fn [^js resp] (if (= 200 (.-status resp)) @@ -169,7 +167,7 @@ ;; ;; The text serializer keeps each font's real uuid, so `wasm/fonts-for-shape` ;; reports it. Custom (team) fonts resolve through the file's font variants, -;; google fonts through the `app.wasm.gfonts` catalog; builtin falls back to +;; google fonts through the shared `app.common.render-wasm.gfonts` catalog; builtin falls back to ;; the bundled default. (defn- fetch-font-variants @@ -209,11 +207,10 @@ (p/resolved nil)))))) (defn- gfont-proxy-url - "Rewrites a gstatic ttf url to the local gfonts proxy (mirrors the browser's - `google-font-ttf-url`)." + "Rewrites a gstatic ttf url to the local gfonts proxy (same rewrite as the + browser's `google-font-ttf-url`)." [ttf-url] - (let [proxy (internal-uri "internal/gfonts/font/")] - (str/replace ttf-url "https://fonts.gstatic.com/s/" proxy))) + (gf/gstatic->proxy-url ttf-url (internal-uri "internal/gfonts/font"))) (defn- fetch-gfont-bytes "Downloads a google font TTF through the local gfonts proxy." @@ -247,7 +244,7 @@ (d/seek (fn [v] (= (:font-id v) font-uuid)) variants))] (if-let [ttf-id (:ttf-file-id variant)] (fetch-asset-bytes ttf-id params) - (if-let [gurl (gfonts/resolve-ttf-url font-uuid weight style)] + (if-let [gurl (gf/resolve-ttf-url font-uuid weight style)] (fetch-gfont-bytes gurl) (p/resolved nil)))))) @@ -286,8 +283,8 @@ (let [cache-key [font-id weight style]] (if-let [bytes (get @fallback-font-bytes* cache-key)] (p/resolved bytes) - (let [font-uuid (gfonts/gfont-id->uuid font-id) - ttf-url (some-> font-uuid (gfonts/resolve-ttf-url weight style))] + (let [font-uuid (gf/gfont-id->uuid font-id) + ttf-url (some-> font-uuid (gf/resolve-ttf-url weight style))] (if ttf-url (->> (fetch-gfont-bytes ttf-url) (p/fmap (fn [buf] @@ -299,7 +296,7 @@ [scene] (->> (scene-fallback-fonts scene) (map (fn [{:keys [font-id weight style is-emoji is-fallback] :as font}] - (if-let [font-uuid (gfonts/gfont-id->uuid font-id)] + (if-let [font-uuid (gf/gfont-id->uuid font-id)] (->> (fetch-fallback-font-bytes font) (p/fmap (fn [buf] (if buf @@ -343,15 +340,15 @@ (defn- provision-images! "Fetches and stores every image the scene references (shape, stroke and - text-span fills, enumerated by `app.render-wasm.resources`). Unlike fonts, + text-span fills, enumerated by `app.common.render-wasm.resources`). Unlike fonts, the image store is not reset per request, so already-held images are skipped and repeated exports of a file reuse them." [scene params] (let [all-ids (resources/scene-image-ids scene) new-ids (remove wasm/image-cached? all-ids)] (l/dbg :hint "wasm render: provisioning images" - :total (count all-ids) - :cached (- (count all-ids) (count new-ids))) + :total (count all-ids) + :cached (- (count all-ids) (count new-ids))) (->> new-ids (map (fn [image-id] (->> (fetch-file-media-bytes image-id params) @@ -359,8 +356,8 @@ (if buf (do (l/dbg :hint "wasm render: image stored" - :media-id (str image-id) - :bytes (.-byteLength ^js buf)) + :media-id (str image-id) + :bytes (.-byteLength ^js buf)) (wasm/store-image! image-id buf)) (l/warn :hint "wasm render: image unavailable" :media-id (str image-id)))))))) @@ -385,15 +382,8 @@ :backend "skia-wasm" :bytes (.-length bytes)) bytes) - ;; The export type doubles as the encoder format: Skia encodes png/jpeg/webp - ;; natively, so webp needs no imagemagick pass like the Playwright backend. (wasm/render-shape-raster id scale type))) -;; NOTE: `prepare-exports` splits an export into partitions of 50 objects, each -;; arriving as its own request, so every partition re-fetches and re-serializes -;; the whole page scene. Acceptable while the module is a single shared instance; -;; revisit with module pooling. - (defn- render* [{:keys [scale type objects] :as params} on-object] (l/dbg :hint "wasm render: start" @@ -429,7 +419,7 @@ path (sh/tempfile :prefix "penpot.tmp.wasm." :suffix (mime/get-extension type))] (l/dbg :hint "wasm render: object rendered" - :object-id (str id) :bytes (.-length bytes)) + :object-id (str id) :bytes (.-length bytes)) (fs/writeFileSync path bytes) ;; `on-object` returns a plain value (zip append) or ;; a promise (single export's file move); `p/do` @@ -439,7 +429,7 @@ (p/fmap (fn [result] ;; After the request, never mid-render, so an image can't ;; disappear under a running export. - (let [evicted (wasm/evict-images! (wasm/image-cache-mb))] + (let [evicted (wasm/evict-images! wasm/image-cache-mb)] (when (pos? evicted) (l/info :hint "wasm render: evicted cached images" :count evicted))) result)) diff --git a/exporter/src/app/wasm.cljs b/exporter/src/app/wasm.cljs index b092109202..8a06b9b4f2 100644 --- a/exporter/src/app/wasm.cljs +++ b/exporter/src/app/wasm.cljs @@ -18,12 +18,13 @@ ["node:path" :as path] [app.common.data :as d] [app.common.logging :as l] + [app.common.render-wasm.helpers :as h] + [app.common.render-wasm.mem :as mem] + [app.common.render-wasm.serializers :as sr] + [app.common.render-wasm.wasm :as wasm] [app.common.uuid :as uuid] - [app.config :as cf] - [app.render-wasm.helpers :as h] - [app.render-wasm.mem :as mem] - [app.render-wasm.serializers :as sr] - [app.render-wasm.wasm :as wasm] + ;; Required for side effects: binds the generated enums. + [app.wasm.enums] [promesa.core :as p] [shadow.esm :refer [dynamic-import]])) @@ -37,17 +38,14 @@ ;; get_fonts_for_shape entry: [uuid 16 bytes][weight u32][style u32]. (def ^:private FONT-ENTRY-BYTES 24) -(defn artifact-dir - "Directory holding the built render-wasm artifact. The docker image points - `PENPOT_WASM_DIR` at the copy inside the exporter bundle; the default is - where `render-wasm/build` leaves it in devenv." - [] - (cf/get :wasm-dir "../frontend/resources/public/js")) +(def artifact-dir + "Built render-wasm artifact, relative to the process working directory. Same + path in devenv and inside the bundle, so it is a constant." + "resources/wasm") -(defn image-cache-mb +(def image-cache-mb "Byte budget (MB) the image store is trimmed to between requests." - [] - (cf/get :wasm-image-cache-mb 256)) + 256) (defn- read-result-bytes "Reads `len` bytes from the WASM heap starting at `offset`, copying them out @@ -63,7 +61,7 @@ Idempotent-ish: callers should hold the returned module." ([] (init! default-viewport-width default-viewport-height)) ([width height] - (let [dir (artifact-dir) + (let [dir artifact-dir js-path (path/resolve dir "render-wasm.js") wasm-path (path/resolve dir "render-wasm.wasm") wasm-bytes (fs/readFileSync wasm-path)] diff --git a/exporter/src/app/wasm/enums.cljs b/exporter/src/app/wasm/enums.cljs new file mode 100644 index 0000000000..9c1a429522 --- /dev/null +++ b/exporter/src/app/wasm/enums.cljs @@ -0,0 +1,19 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC Sucursal en España SL + +(ns app.wasm.enums + "Binds this build's generated enums into the shared bridge. + + `shared.js` is emitted next to this file by `render-wasm/build export` and is + not committed. Requiring this namespace is what makes + `app.common.render-wasm.wasm/serializers` usable." + (:require + ["./shared.js" :as shared] + [app.common.render-wasm.wasm :as wasm]) + (:require-macros + [app.common.render-wasm.enums :as enums])) + +(wasm/init-serializers! (enums/serializers shared)) diff --git a/exporter/src/app/wasm/gfonts.cljs b/exporter/src/app/wasm/gfonts.cljs deleted file mode 100644 index 1ef5b7dd51..0000000000 --- a/exporter/src/app/wasm/gfonts.cljs +++ /dev/null @@ -1,43 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC Sucursal en España SL - -(ns app.wasm.gfonts - "Compile-time Google Fonts catalog for the headless exporter, built from the - same `gfonts.json` the browser uses via the `preload-gfonts` .clj macro (a - macro namespace, so requiring it does not pull in `app.main.fonts` itself). - - `parse-gfont` assigns each font a `uuid/random` at macro-expansion time, so - this catalog MUST stay the single shared instance: `app.wasm.text` maps - `gfont-` to the uuid and `app.renderer.wasm` maps it back to a ttf url." - (:require-macros [app.main.fonts :refer [preload-gfonts]])) - -(def ^:private catalog - (preload-gfonts "fonts/gfonts.2025.11.28.json")) - -(def ^:private by-id - (reduce (fn [m font] (assoc m (:id font) font)) {} catalog)) - -(def ^:private by-uuid - (reduce (fn [m font] (assoc m (:uuid font) font)) {} catalog)) - -(defn gfont-id->uuid - "Maps a `gfont-` id to its (build-stable) catalog uuid, or nil." - [gfont-id] - (:uuid (get by-id gfont-id))) - -(defn resolve-ttf-url - "Given a google font uuid + numeric weight + style int (0 = normal, else - italic), returns the variant's gstatic ttf url (or nil if not a google font / - no variants). Degrades weight+style -> weight -> first variant." - [font-uuid weight style] - (when-let [font (get by-uuid font-uuid)] - (let [w (str weight) - s (if (zero? style) "normal" "italic") - variants (:variants font) - variant (or (some (fn [v] (when (and (= (:weight v) w) (= (:style v) s)) v)) variants) - (some (fn [v] (when (= (:weight v) w) v)) variants) - (first variants))] - (:ttf-url variant)))) diff --git a/exporter/src/app/wasm/serialize.cljs b/exporter/src/app/wasm/serialize.cljs index 5bcd67fc1d..cbb08c27f3 100644 --- a/exporter/src/app/wasm/serialize.cljs +++ b/exporter/src/app/wasm/serialize.cljs @@ -14,10 +14,10 @@ Covers everything except svg-raw. Image bytes and fonts are provisioned separately by `app.renderer.wasm`." (:require - [app.render-wasm.api.props :as props] - [app.render-wasm.helpers :as h] - [app.render-wasm.serialize-shape :as serialize-shape] - [app.render-wasm.wasm :as wasm] + [app.common.render-wasm.api.props :as props] + [app.common.render-wasm.helpers :as h] + [app.common.render-wasm.serialize-shape :as serialize-shape] + [app.common.render-wasm.wasm :as wasm] [app.wasm.text :as text])) (defn set-shape! diff --git a/exporter/src/app/wasm/text.cljs b/exporter/src/app/wasm/text.cljs index 4399a0af89..714542d7f1 100644 --- a/exporter/src/app/wasm/text.cljs +++ b/exporter/src/app/wasm/text.cljs @@ -5,39 +5,21 @@ ;; Copyright (c) KALEIDOS INC Sucursal en España SL (ns app.wasm.text - "Browser-free text-content serialization for the headless exporter. The - binary layout is shared via `app.render-wasm.text-content`; only font-id - resolution is local, since the exporter has no fonts DB." + "Browser-free text-content serialization for the headless exporter. Only the + paragraph walk is local: the binary layout and the font-id -> uuid mapping + both come from `app.common.render-wasm.text-content`." (:require - [app.common.uuid :as uuid] - [app.render-wasm.helpers :as h] - [app.render-wasm.serializers :as sr] - [app.render-wasm.text-content :as tc] - [app.render-wasm.wasm :as wasm] - [app.wasm.gfonts :as gfonts] - [cuerdas.core :as str])) - -(defn- normalize-font-id - "Maps a content font-id to its wasm uuid. The provisioning side keys on the - same uuid." - [font-id] - (try - (cond - (str/starts-with? font-id "gfont-") - (or (gfonts/gfont-id->uuid font-id) uuid/zero) - - (str/includes? font-id "-") - (let [no-prefix (subs font-id (inc (str/index-of font-id "-")))] - (if (str/blank? no-prefix) uuid/zero (uuid/parse no-prefix))) - - :else uuid/zero) - (catch :default _ uuid/zero))) + [app.common.render-wasm.helpers :as h] + [app.common.render-wasm.serializers :as sr] + [app.common.render-wasm.text-content :as tc] + [app.common.render-wasm.wasm :as wasm])) (defn set-shape-text! "Serializes a text shape's content into the current WASM shape. Mirrors the editor's sequence: clear -> vertical-align -> append each paragraph -> layout. - Byte writing is the shared `text-content/write-shape-text!`; only font-id - resolution is injected." + Byte writing and font resolution are the shared + `text-content/write-shape-text!` defaults; the exporter has no fonts DB, so + it injects no variant normalization." [content] (when content (h/call wasm/internal-module "_clear_shape_text") @@ -49,6 +31,5 @@ (let [spans (get paragraph :children)] (when (seq spans) (let [text (apply str (map :text spans))] - (tc/write-shape-text! spans paragraph text - {:normalize-font-id normalize-font-id})))))) + (tc/write-shape-text! spans paragraph text {})))))) (h/call wasm/internal-module "_update_shape_text_layout"))) diff --git a/frontend/deps.edn b/frontend/deps.edn index a662b7086d..f060448343 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -1,6 +1,7 @@ {:paths ["src" "vendor" "resources" "test"] :deps - {penpot/common + {;; Carries `app.common.render-wasm.*`, shared with the headless exporter. + penpot/common {:local/root "../common"} org.clojure/clojure {:mvn/version "1.12.2"} diff --git a/frontend/package.json b/frontend/package.json index 54063ed48f..a466de837b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,7 +19,7 @@ "build:storybook": "(cd packages/ui && pnpm run build) && pnpm run build:storybook:assets && pnpm run build:storybook:cljs && storybook build", "build:storybook:assets": "node ./scripts/build-storybook-assets.js", "build:storybook:cljs": "clojure -M:dev:shadow-cljs compile storybook", - "build:wasm": "../render-wasm/build", + "build:wasm": "../render-wasm/build frontend", "build:app:libs": "node ./scripts/build-libs.js", "build:app:main": "clojure -M:dev:shadow-cljs release main worker", "build:app:worker": "clojure -M:dev:shadow-cljs release worker", diff --git a/frontend/scripts/build b/frontend/scripts/build index 2cc812e228..fe40d223bc 100755 --- a/frontend/scripts/build +++ b/frontend/scripts/build @@ -30,7 +30,7 @@ mkdir -p target/dist; # Build render wasm binary pushd ../render-wasm; -./build +./build frontend popd pushd ../mcp; diff --git a/frontend/scripts/build-fonts-preview.js b/frontend/scripts/build-fonts-preview.js index b1aa7c839c..6c609252b4 100644 --- a/frontend/scripts/build-fonts-preview.js +++ b/frontend/scripts/build-fonts-preview.js @@ -68,7 +68,7 @@ function slug(value) { } async function findGfontsJson() { - const dir = "resources/fonts"; + const dir = "../common/resources/fonts"; const entries = await fs.readdir(dir); const matches = entries.filter((f) => /^gfonts\..*\.json$/.test(f)).sort(); if (matches.length === 0) { diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index cedfad1d96..8917f2e97f 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -18,6 +18,7 @@ [app.common.geom.shapes :as gsh] [app.common.logging :as log] [app.common.path-names :as cpn] + [app.common.render-wasm.wasm :as wasm-state] [app.common.transit :as t] [app.common.types.component :as ctc] [app.common.types.components-list :as ctkl] @@ -77,7 +78,6 @@ [app.plugins.register :as preg] [app.render-wasm :as wasm] [app.render-wasm.api :as wasm.api] - [app.render-wasm.wasm :as wasm-state] [app.util.dom :as dom] [app.util.globals :as ug] [app.util.http :as http] diff --git a/frontend/src/app/main/fonts.cljs b/frontend/src/app/main/fonts.cljs index 677f8aa1fb..58158f4b6d 100644 --- a/frontend/src/app/main/fonts.cljs +++ b/frontend/src/app/main/fonts.cljs @@ -6,11 +6,11 @@ (ns app.main.fonts "Fonts management and loading logic." - (:require-macros [app.main.fonts :refer [preload-gfonts]]) (:require [app.common.data :as d] [app.common.data.macros :as dm] [app.common.logging :as log] + [app.common.render-wasm.gfonts :as gf] [app.common.types.text :as txt] [app.common.uri :as u] [app.config :as cf] @@ -25,8 +25,7 @@ (log/set-level! :warn) -(def google-fonts - (preload-gfonts "fonts/gfonts.2025.11.28.json")) +(def google-fonts gf/catalog) (def local-fonts [{:id "sourcesanspro" @@ -266,8 +265,7 @@ (defn- process-gfont-css [css] - (let [base (u/join cf/public-uri "internal/gfonts/font")] - (str/replace css "https://fonts.gstatic.com/s" (dm/str base)))) + (gf/gstatic->proxy-url css (u/join cf/public-uri "internal/gfonts/font"))) (defn- fetch-gfont-css [url] @@ -397,42 +395,12 @@ (defn find-closest-variant "Find the closest font weight variant in `font` for `target-weight` with optional `target-style` match. - When exactly between two weights, choose the higher one." + When exactly between two weights, choose the higher one. + + The algorithm lives in `app.common.render-wasm.gfonts` so the headless exporter resolves the + same variant for the same text." [font target-weight target-style] - (when-let [target-weight (d/parse-integer target-weight)] - (let [variants (:variants font []) - result - (reduce - (fn [closest-match variant] - (let [weight (d/parse-integer (:weight variant)) - distance (abs (- target-weight weight)) - matches-style? (= target-style (:style variant)) - current {:variant variant - :weight weight - :distance distance}] - (cond - ;; Exact match found - (and (zero? distance) - (if target-style matches-style? true)) - (reduced current) - - (nil? closest-match) current - - ;; Update best match if this variant is closer or equal distance but higher weight - (or (< distance (:distance closest-match)) - (and (= distance (:distance closest-match)) - (> weight (:weight closest-match)))) - current - - ;; Same weight as the `closest-match` but the style matches `target-style` - (and (= weight (:weight closest-match)) matches-style?) - current - - :else - closest-match))) - nil - variants)] - (:variant result)))) + (gf/closest-variant (:variants font []) target-weight target-style)) ;; Font embedding functions (defn get-node-fonts diff --git a/frontend/src/app/main/render_viewer_wasm.cljs b/frontend/src/app/main/render_viewer_wasm.cljs index 45f7b83f73..1d4cd89506 100644 --- a/frontend/src/app/main/render_viewer_wasm.cljs +++ b/frontend/src/app/main/render_viewer_wasm.cljs @@ -9,8 +9,8 @@ (:require [app.common.data.macros :as dm] [app.common.exceptions :as ex] + [app.common.render-wasm.wasm :as wasm] [app.render-wasm.api :as wasm.api] - [app.render-wasm.wasm :as wasm] [app.util.dom :as dom] [app.util.timers :as ts] [app.util.webapi :as webapi] diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index c24bcf0460..e79fea8e44 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -15,6 +15,14 @@ [app.common.files.helpers :as cfh] [app.common.logging :as log] [app.common.math :as mth] + [app.common.render-wasm.api.props :as props] + [app.common.render-wasm.helpers :as h] + [app.common.render-wasm.mem :as mem] + [app.common.render-wasm.mem.heap32 :as mem.h32] + [app.common.render-wasm.serialize-shape :as serialize-shape] + [app.common.render-wasm.serializers :as sr] + [app.common.render-wasm.serializers.color :as sr-clr] + [app.common.render-wasm.wasm :as wasm] [app.common.types.color :as clr] [app.common.types.fills :as types.fills] [app.common.types.path :as path] @@ -31,23 +39,17 @@ [app.main.router :as rt] [app.main.store :as st] [app.main.ui.shapes.text] + ;; Required for side effects: binds the generated enums. + [app.render-wasm.api.enums] [app.render-wasm.api.fonts :as f] - [app.render-wasm.api.props :as props] [app.render-wasm.api.texts :as t] [app.render-wasm.api.webgl :as webgl] [app.render-wasm.deserializers :as dr] [app.render-wasm.gesture :as wasm-gesture] - [app.render-wasm.helpers :as h] - [app.render-wasm.mem :as mem] - [app.render-wasm.mem.heap32 :as mem.h32] [app.render-wasm.performance :as perf] [app.render-wasm.rulers-state :as rulers-state] - [app.render-wasm.serialize-shape :as serialize-shape] - [app.render-wasm.serializers :as sr] - [app.render-wasm.serializers.color :as sr-clr] [app.render-wasm.svg-filters :as svg-filters] [app.render-wasm.text-editor :as text-editor] - [app.render-wasm.wasm :as wasm] [app.util.debug :as dbg] [app.util.dom :as dom] [app.util.functions :as fns] diff --git a/frontend/src/app/render_wasm/api/enums.cljs b/frontend/src/app/render_wasm/api/enums.cljs new file mode 100644 index 0000000000..de132c4638 --- /dev/null +++ b/frontend/src/app/render_wasm/api/enums.cljs @@ -0,0 +1,19 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC Sucursal en España SL + +(ns app.render-wasm.api.enums + "Binds this build's generated enums into the shared bridge. + + `shared.js` is emitted next to this file by `render-wasm/build frontend` and + is not committed. Requiring this namespace is what makes + `app.common.render-wasm.wasm/serializers` usable." + (:require + ["./shared.js" :as shared] + [app.common.render-wasm.wasm :as wasm]) + (:require-macros + [app.common.render-wasm.enums :as enums])) + +(wasm/init-serializers! (enums/serializers shared)) diff --git a/frontend/src/app/render_wasm/api/fonts.cljs b/frontend/src/app/render_wasm/api/fonts.cljs index c3d5a32a35..ee2c4479c3 100644 --- a/frontend/src/app/render_wasm/api/fonts.cljs +++ b/frontend/src/app/render_wasm/api/fonts.cljs @@ -9,14 +9,15 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.logging :as log] + [app.common.render-wasm.fallback-fonts :as fbf] + [app.common.render-wasm.gfonts :as gf] + [app.common.render-wasm.helpers :as h] + [app.common.render-wasm.wasm :as wasm] [app.common.types.text :as txt] [app.common.uuid :as uuid] [app.config :as cf] [app.main.fonts :as fonts] [app.main.store :as st] - [app.render-wasm.fallback-fonts :as fbf] - [app.render-wasm.helpers :as h] - [app.render-wasm.wasm :as wasm] [app.util.http :as http] [app.util.timers :as tm] [beicon.v2.core :as rx] @@ -39,23 +40,6 @@ (def ^:private default-line-height 1.2) (def ^:private default-letter-spacing 0.0) -(defn- google-font-id->uuid - "Returns the UUID for a Google Font ID. Uses uuid/zero as fallback when the - font is not found in fontsdb. uuid/zero maps to the default font (Source - Sans Pro) in WASM. - A font id may not exist for different reasons: - - the gfonts.json catalog was updated and fonts were renamed or removed, - - the file was imported from another Penpot instance with different fonts, - ..." - [font-id] - (let [font (fonts/get-font-data font-id) - result (:uuid font)] - (or result uuid/zero))) - -(defn- custom-font-id->uuid - [font-id] - (uuid/uuid (subs font-id (inc (str/index-of font-id "-"))))) - (defn- font-backend [font-id] (cond @@ -75,15 +59,6 @@ variant closest-variant))) -(defn- font-id->uuid [font-id] - (case (font-backend font-id) - :google - (google-font-id->uuid font-id) - :custom - (custom-font-id->uuid font-id) - :builtin - uuid/zero)) - (defn uuid->font-id [font-uuid] (if (= font-uuid uuid/zero) @@ -104,7 +79,7 @@ :google font-id :custom - (let [font-uuid (custom-font-id->uuid font-id) + (let [font-uuid (gf/font-id->uuid font-id) matching-font (some (fn [[_ font]] (and (= (:font-id font) font-uuid) (= (str (:font-weight font)) (str font-weight)) @@ -194,9 +169,8 @@ (defn- google-font-ttf-url [font-id font-variant-id font-weight font-style] (let [variant (font-db-data font-id font-variant-id font-weight font-style)] - (if-let [ttf-url (:ttf-url variant)] - (str/replace ttf-url "https://fonts.gstatic.com/s/" (u/join cf/public-uri "internal/gfonts/font/")) - nil))) + (when-let [ttf-url (:ttf-url variant)] + (gf/gstatic->proxy-url ttf-url (u/join cf/public-uri "internal/gfonts/font"))))) (defn- font-id->ttf-url [font-id asset-id font-variant-id font-weight font-style] @@ -245,18 +219,6 @@ "italic" 1 0)) -(defn normalize-font-id - [font-id] - (try - (if ^boolean (str/starts-with? font-id "gfont-") - (google-font-id->uuid font-id) - (let [no-prefix (subs font-id (inc (str/index-of font-id "-")))] - (if (or (nil? no-prefix) (not (string? no-prefix)) (str/blank? no-prefix)) - uuid/zero - (uuid/parse no-prefix)))) - (catch :default _e - uuid/zero))) - (defn normalize-span-font [span paragraph] (let [font-id (:font-id span) @@ -358,7 +320,7 @@ emoji? (get font :is-emoji false) fallback? (get font :is-fallback false) font-data (font-db-data font-id normalized-variant-id font-weight-fallback font-style-fallback) - wasm-id (font-id->uuid font-id) + wasm-id (gf/font-id->uuid font-id) raw-weight (or (:weight font-data) font-weight-fallback) weight (serialize-font-weight raw-weight) style (cond diff --git a/frontend/src/app/render_wasm/api/texts.cljs b/frontend/src/app/render_wasm/api/texts.cljs index 3ea55dfdb3..cc942bdf1b 100644 --- a/frontend/src/app/render_wasm/api/texts.cljs +++ b/frontend/src/app/render_wasm/api/texts.cljs @@ -6,21 +6,20 @@ (ns app.render-wasm.api.texts (:require - [app.render-wasm.api.fonts :as f] - [app.render-wasm.fallback-fonts :as fbf] - [app.render-wasm.text-content :as tc])) + [app.common.render-wasm.fallback-fonts :as fbf] + [app.common.render-wasm.text-content :as tc] + [app.render-wasm.api.fonts :as f])) (defn write-shape-text "Workspace text serialization: the byte writing is shared via - `app.render-wasm.text-content`; font resolution is the workspace's (fonts DB)." + `app.common.render-wasm.text-content`; font resolution is the workspace's (fonts DB)." [spans paragraph text] (tc/write-shape-text! spans paragraph text - {:normalize-font-id f/normalize-font-id - :normalize-paragraph f/normalize-paragraph-font + {:normalize-paragraph f/normalize-paragraph-font :normalize-span f/normalize-span-font})) ;; Emoji/script detection lives in the host-agnostic -;; `app.render-wasm.fallback-fonts`; kept re-exported here for existing +;; `app.common.render-wasm.fallback-fonts`; kept re-exported here for existing ;; workspace callers. (def contains-emoji? fbf/contains-emoji?) (def collect-used-languages fbf/collect-used-languages) diff --git a/frontend/src/app/render_wasm/api/webgl.cljs b/frontend/src/app/render_wasm/api/webgl.cljs index fc7a3fe37f..268dfe4462 100644 --- a/frontend/src/app/render_wasm/api/webgl.cljs +++ b/frontend/src/app/render_wasm/api/webgl.cljs @@ -8,7 +8,7 @@ "WebGL utilities for pixel capture and rendering" (:require [app.common.logging :as log] - [app.render-wasm.wasm :as wasm] + [app.common.render-wasm.wasm :as wasm] [promesa.core :as p])) (defn get-webgl-context diff --git a/frontend/src/app/render_wasm/text_editor.cljs b/frontend/src/app/render_wasm/text_editor.cljs index 9686889dab..bf5efe8a1a 100644 --- a/frontend/src/app/render_wasm/text_editor.cljs +++ b/frontend/src/app/render_wasm/text_editor.cljs @@ -7,16 +7,18 @@ (ns app.render-wasm.text-editor "Text editor WASM bindings" (:require + [app.common.render-wasm.helpers :as h] + [app.common.render-wasm.mem :as mem] + [app.common.render-wasm.serializers :as sr] + [app.common.render-wasm.serializers.color :as sr-clr] + [app.common.render-wasm.wasm :as wasm] [app.common.types.fills.impl :as types.fills.impl] [app.common.types.text :as txt] [app.common.uuid :as uuid] [app.main.fonts :as main-fonts] + ;; Required for side effects: binds the generated enums. + [app.render-wasm.api.enums] [app.render-wasm.api.fonts :as fonts] - [app.render-wasm.helpers :as h] - [app.render-wasm.mem :as mem] - [app.render-wasm.serializers :as sr] - [app.render-wasm.serializers.color :as sr-clr] - [app.render-wasm.wasm :as wasm] [app.util.color :as uc] [app.util.dom :as dom])) diff --git a/frontend/src/app/worker/thumbnails.cljs b/frontend/src/app/worker/thumbnails.cljs index 21d4e37190..0dd8916cf7 100644 --- a/frontend/src/app/worker/thumbnails.cljs +++ b/frontend/src/app/worker/thumbnails.cljs @@ -11,6 +11,7 @@ [app.common.geom.rect :as grc] [app.common.geom.shapes.bounds :as gsb] [app.common.logging :as log] + [app.common.render-wasm.wasm :as wasm] [app.common.types.color :as cc] [app.common.uri :as u] [app.common.uuid :as uuid] @@ -18,7 +19,6 @@ [app.main.fonts :as fonts] [app.main.render :as render] [app.render-wasm.api :as wasm.api] - [app.render-wasm.wasm :as wasm] [app.util.http :as http] [app.worker.impl :as impl] [beicon.v2.core :as rx] diff --git a/frontend/src/debug.cljs b/frontend/src/debug.cljs index fb5cd5b5fe..f2ec88f412 100644 --- a/frontend/src/debug.cljs +++ b/frontend/src/debug.cljs @@ -15,6 +15,9 @@ [app.common.json :as json] [app.common.logging :as l] [app.common.pprint :as pp] + [app.common.render-wasm.helpers :as wasm.h] + [app.common.render-wasm.mem :as wasm.mem] + [app.common.render-wasm.wasm :as wasm] [app.common.transit :as t] [app.common.types.component :as ctk] [app.common.types.components-list :as ctkl] @@ -36,9 +39,6 @@ [app.main.errors :as errors] [app.main.repo :as rp] [app.main.store :as st] - [app.render-wasm.helpers :as wasm.h] - [app.render-wasm.mem :as wasm.mem] - [app.render-wasm.wasm :as wasm] [app.util.debug :as dbg] [app.util.dom :as dom] [app.util.http :as http] diff --git a/frontend/test/frontend_tests/render_wasm/process_objects_test.cljs b/frontend/test/frontend_tests/render_wasm/process_objects_test.cljs index c985b336ae..cf9e68670a 100644 --- a/frontend/test/frontend_tests/render_wasm/process_objects_test.cljs +++ b/frontend/test/frontend_tests/render_wasm/process_objects_test.cljs @@ -15,9 +15,9 @@ font URL get no callback (fetch-font returns nil when the URL is already in :fetching) and are permanently stuck with fallback-font layout metrics." (:require + [app.common.render-wasm.mem :as mem] + [app.common.render-wasm.wasm :as wasm] [app.render-wasm.api :as wasm.api] - [app.render-wasm.mem :as mem] - [app.render-wasm.wasm :as wasm] [beicon.v2.core :as rx] [cljs.test :as t :include-macros true])) diff --git a/frontend/test/frontend_tests/render_wasm/text_editor_caret_color_test.cljs b/frontend/test/frontend_tests/render_wasm/text_editor_caret_color_test.cljs index 2594e84995..09203f4bae 100644 --- a/frontend/test/frontend_tests/render_wasm/text_editor_caret_color_test.cljs +++ b/frontend/test/frontend_tests/render_wasm/text_editor_caret_color_test.cljs @@ -11,7 +11,7 @@ anything else (no fill, gradient, image fills, mixed selection) it falls back to an inverted caret (white painted with a Difference blend)." (:require - [app.render-wasm.serializers.color :as sr-clr] + [app.common.render-wasm.serializers.color :as sr-clr] [app.render-wasm.text-editor :as text-editor] [cljs.test :as t :include-macros true])) diff --git a/render-wasm/Cargo.toml b/render-wasm/Cargo.toml index 15f8f39335..b17e3aa69f 100644 --- a/render-wasm/Cargo.toml +++ b/render-wasm/Cargo.toml @@ -42,3 +42,7 @@ opt-level = 3 lto = "fat" strip = true codegen-units = 1 + +[profile.size] +inherits = "release" +opt-level = "z" diff --git a/render-wasm/README.md b/render-wasm/README.md index b1010bb70c..4ed4830bcd 100644 --- a/render-wasm/README.md +++ b/render-wasm/README.md @@ -29,6 +29,34 @@ 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/`), 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) diff --git a/render-wasm/_build_env b/render-wasm/_build_env index 0b506e415d..e034580dfd 100644 --- a/render-wasm/_build_env +++ b/render-wasm/_build_env @@ -1,15 +1,25 @@ #!/usr/bin/env bash export VERSION_TAG=${VERSION:-develop}; +export RENDER_TARGET="${RENDER_TARGET:-${1:-frontend}}"; + +case "$RENDER_TARGET" in + frontend|export) ;; + *) + echo "ERROR: unknown render target '$RENDER_TARGET' (expected 'frontend' or 'export')" >&2; + exit 1; + ;; +esac if [ "$NODE_ENV" = "production" ]; then export BUILD_MODE="release"; else - export BUILD_MODE=${1:-debug}; + export BUILD_MODE=${BUILD_MODE:-debug}; fi export BUILD_NAME="${BUILD_NAME:-render-wasm}" export CARGO_BUILD_TARGET=${CARGO_BUILD_TARGET:-"wasm32-unknown-emscripten"}; +export CARGO_TARGET_DIR=${CARGO_TARGET_DIR:-"target/$RENDER_TARGET"}; export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp.tar.gz"} # 256 MB of initial heap to perform less @@ -51,9 +61,21 @@ export EM_CACHE="/tmp/emsdk_cache"; export CARGO_PARAMS="${@:2}"; +export CARGO_PROFILE_DIR="debug"; + if [ "$BUILD_MODE" = "release" ]; then - export CARGO_PARAMS="--release $CARGO_PARAMS" - export EMCC_CFLAGS="-O3 -sASSERTIONS=0 $EMCC_CFLAGS" + case "$RENDER_TARGET" in + frontend) + export CARGO_PARAMS="--release $CARGO_PARAMS"; + export CARGO_PROFILE_DIR="release"; + export EMCC_CFLAGS="-O3 -sASSERTIONS=0 $EMCC_CFLAGS"; + ;; + export) + export CARGO_PARAMS="--profile size $CARGO_PARAMS"; + export CARGO_PROFILE_DIR="size"; + export EMCC_CFLAGS="-Oz -sASSERTIONS=0 $EMCC_CFLAGS"; + ;; + esac else # TODO: Extra parameters that could be good to look into: # -gseparate-dwarf @@ -62,6 +84,12 @@ else export EMCC_CFLAGS="-g -sASSERTIONS=1 -sVERBOSE=1 $EMCC_CFLAGS" fi +export FRONTEND_DEST="../frontend/resources/public/js"; +export EXPORT_DEST="../exporter/resources/wasm"; + +export FRONTEND_SHARED_DEST="../frontend/src/app/render_wasm/api/shared.js"; +export EXPORT_SHARED_DEST="../exporter/src/app/wasm/shared.js"; + function clean { cargo clean; } @@ -78,26 +106,48 @@ function build { function copy_artifacts { DEST=$1; + SRC="$CARGO_TARGET_DIR/$CARGO_BUILD_TARGET/$CARGO_PROFILE_DIR"; mkdir -p $DEST; - cp target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.js $DEST/$BUILD_NAME.js; - cp target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.wasm $DEST/$BUILD_NAME.wasm; - if [ -f target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.wasm.map ]; then - cp target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.wasm.map $DEST/$BUILD_NAME.wasm.map; + cp $SRC/render_wasm.js $DEST/$BUILD_NAME.js; + cp $SRC/render_wasm.wasm $DEST/$BUILD_NAME.wasm; + if [ -f $SRC/render_wasm.wasm.map ]; then + cp $SRC/render_wasm.wasm.map $DEST/$BUILD_NAME.wasm.map; fi sed -i "s/render_wasm.wasm/$BUILD_NAME.wasm?version=$VERSION_TAG/g" $DEST/$BUILD_NAME.js; - pnpm exec esbuild target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.js \ - --log-level=error \ - --outfile=$DEST/worker/render.js \ - --platform=neutral \ - --format=iife \ - --global-name=WasmModule; + # The worker bundle is a browser concern; the exporter imports the ESM + # module directly under Node. + if [ "$RENDER_TARGET" = "frontend" ]; then + pnpm exec esbuild $SRC/render_wasm.js \ + --log-level=error \ + --outfile=$DEST/worker/render.js \ + --platform=neutral \ + --format=iife \ + --global-name=WasmModule; + fi } function copy_shared_artifact { - SHARED_FILE=$(find target/wasm32-unknown-emscripten -name render_wasm_shared.js | head -n 1); - cp $SHARED_FILE ../frontend/src/app/render_wasm/api/shared.js; + DEST=$1; + SHARED_FILE=$(find $CARGO_TARGET_DIR/$CARGO_BUILD_TARGET -name render_wasm_shared.js | head -n 1); + + cp $SHARED_FILE $DEST; +} + +# Copies whatever the current RENDER_TARGET produced to where that target's +# consumer reads it. +function copy_target_artifacts { + case "$RENDER_TARGET" in + frontend) + copy_artifacts "$FRONTEND_DEST"; + copy_shared_artifact "$FRONTEND_SHARED_DEST"; + ;; + export) + copy_artifacts "$EXPORT_DEST"; + copy_shared_artifact "$EXPORT_SHARED_DEST"; + ;; + esac } diff --git a/render-wasm/build b/render-wasm/build index 2505ae0cf9..2a832fc3f4 100755 --- a/render-wasm/build +++ b/render-wasm/build @@ -1,8 +1,26 @@ #!/usr/bin/env bash +# Usage: ./build [frontend|export] [extra cargo params...] +# +# With no target, builds both. Set BUILD_MODE=release (or NODE_ENV=production) +# for an optimized build. See `_build_env` for what each target changes. + +_SCRIPT_DIR=$(dirname $0); + +# Each target needs its own `_build_env`, so re-enter per target. +case "${1:-}" in + frontend|export) + ;; + *) + for _target in frontend export; do + "$_SCRIPT_DIR/build" "$_target" "$@" || exit $?; + done + exit 0; + ;; +esac + EMSDK_QUIET=1 . /opt/emsdk/emsdk_env.sh -_SCRIPT_DIR=$(dirname $0); pushd $_SCRIPT_DIR; . ./_build_env @@ -11,8 +29,7 @@ set -ex; setup; build; -copy_artifacts "../frontend/resources/public/js"; -copy_shared_artifact; +copy_target_artifacts; exit $?; diff --git a/render-wasm/lint b/render-wasm/lint index e94145189a..4ed7d6826d 100755 --- a/render-wasm/lint +++ b/render-wasm/lint @@ -8,7 +8,7 @@ if [[ "$1" == "--debug" ]]; then set -x fi -. ./_build_env +. ./_build_env frontend export CARGO_BUILD_TARGET=${CARGO_BUILD_TARGET:-"wasm32-unknown-emscripten"}; export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp.tar.gz"} diff --git a/render-wasm/test b/render-wasm/test index f416e6c6bb..8019b1fecd 100755 --- a/render-wasm/test +++ b/render-wasm/test @@ -7,7 +7,7 @@ export CARGO_BUILD_TARGET=${CARGO_BUILD_TARGET:-"x86_64-unknown-linux-gnu"}; _SCRIPT_DIR=$(dirname $0); pushd $_SCRIPT_DIR; -. ./_build_env +. ./_build_env frontend cargo test --bin render_wasm -- --show-output diff --git a/render-wasm/watch b/render-wasm/watch index 90c08c9ffd..1825bf7667 100755 --- a/render-wasm/watch +++ b/render-wasm/watch @@ -1,5 +1,7 @@ #!/usr/bin/env bash +# Usage: ./watch [frontend|export] + _SCRIPT_DIR=$(dirname $0); pushd $_SCRIPT_DIR; @@ -7,8 +9,7 @@ pushd $_SCRIPT_DIR; set -x build; -copy_artifacts "../frontend/resources/public/js"; -copy_shared_artifact; +copy_target_artifacts; pushd $_SCRIPT_DIR; @@ -16,7 +17,7 @@ cargo watch \ --why \ -i "_tmp*" \ -x "build $CARGO_PARAMS" \ - -s "./build" \ + -s "./build $RENDER_TARGET" \ -s "echo 'DONE\n'"; popd