Render multiple-object SVG exports with render-wasm (#11475)

This commit is contained in:
Elena Torró 2026-09-02 14:45:32 +02:00 committed by GitHub
parent b92112da54
commit 70b443a716
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 106 additions and 34 deletions

View File

@ -41,8 +41,8 @@
(defn headless?
"Whether `params` renders with render-wasm rather than a browser."
[{:keys [type is-wasm]}]
(and is-wasm (contains? cf/flags :wasm-export) (not= :svg type)))
[{:keys [is-wasm]}]
(and is-wasm (contains? cf/flags :wasm-export)))
(defn render
[{:keys [type is-wasm] :as params} on-object]

View File

@ -33,8 +33,8 @@
;; render_shape_raster / render_shape_pixels result header: [len u32][w u32][h u32].
(def ^:private RASTER-HEADER-BYTES 12)
;; render_shape_pdf result header: [len u32] only.
(def ^:private PDF-HEADER-BYTES 4)
;; render_shape_pdf / render_shape_svg result header: [len u32] only.
(def ^:private LEN-HEADER-BYTES 4)
;; get_fonts_for_shape entry: [uuid 16 bytes][weight u32][style u32].
(def ^:private FONT-ENTRY-BYTES 24)
@ -146,6 +146,20 @@
(aget id 0) (aget id 1) (aget id 2) (aget id 3)
weight style (boolean emoji?) (boolean fallback?))))
(defn store-font-url!
"Registers the public URL a font family was loaded from. The SVG export emits
one `@font-face` per family from these, and skips families without one, so
this must run for every family `store-font!` uploads.
Does NOT call `mem/free`, for the same reason as `store-font!`."
[{:keys [id weight style]} url]
(let [bytes (js/Buffer.from url "utf-8")
ptr (mem/alloc (.-byteLength bytes))]
(mem/write-buffer ptr (mem/get-heap-u8) bytes)
(h/call wasm/internal-module "_store_font_url"
(aget id 0) (aget id 1) (aget id 2) (aget id 3)
weight style)))
(defn clear-fonts!
"Resets the WASM font store. Must be called once per render request because
the shared module would otherwise accumulate fonts across requests."
@ -217,13 +231,18 @@
(defn provision-fonts!
"Resolves and uploads every font needed by `shape-ids`, each family fetched
once. `resolve-font` is an injected fn of the family map -> promise of TTF
bytes (or nil to skip). This keeps the font *source* (gfonts proxy / custom
assets / backend) out of the driver."
[shape-ids resolve-font]
bytes (or nil to skip); optional `font-url` is a fn of the family map -> the
public URL those bytes came from. This keeps the font *source* (gfonts proxy
/ custom assets / backend) out of the driver."
[shape-ids resolve-font & {:keys [font-url]}]
(->> (fonts-for-shapes shape-ids)
(map (fn [family]
(->> (resolve-font family)
(p/fmap (fn [bytes] (when bytes (store-font! family bytes)))))))
(p/fmap (fn [bytes]
(when bytes
(store-font! family bytes)
(when-let [url (when font-url (font-url family))]
(store-font-url! family url))))))))
(p/all)))
;; --- RENDER
@ -256,4 +275,13 @@
(-> (h/call wasm/internal-module "_render_shape_pdf"
(aget buf 0) (aget buf 1) (aget buf 2) (aget buf 3)
scale)
(read-render-result PDF-HEADER-BYTES))))
(read-render-result LEN-HEADER-BYTES))))
(defn render-shape-svg
"Renders the shape subtree to SVG markup bytes (Uint8Array)."
[shape-id scale]
(let [buf (uuid/get-u32 shape-id)]
(-> (h/call wasm/internal-module "_render_shape_svg"
(aget buf 0) (aget buf 1) (aget buf 2) (aget buf 3)
scale)
(read-render-result LEN-HEADER-BYTES))))

View File

@ -19,8 +19,7 @@
because that namespace still exists as the proxy. Reviewable as a rename:
`git show <base>:exporter/src/app/renderer/wasm.cljs | diff -u - <this file>`.
Handles png/jpeg/webp (Skia encodes all three) and pdf; `:svg` stays on the
browser path."
Handles png/jpeg/webp (Skia encodes all three), pdf and svg."
(:require
["node:fs" :as fs]
["undici" :as http]
@ -71,6 +70,15 @@
(u/join path)
(str)))
(defn- public-uri
"Absolute URI for `path` on the public endpoint. Whoever opens an exported SVG
resolves its `@font-face` sources, so those cannot use the internal endpoint."
[path]
(-> (cf/get :public-uri)
(u/ensure-path-slash)
(u/join path)
(str)))
(defn- error-detail
"Node's fetch reports every transport failure as a bare `TypeError: fetch
failed`; the actual reason (TLS rejection, DNS, ECONNREFUSED) is buried in a
@ -221,23 +229,32 @@
[ttf-file]
(cached-ttf-bytes ttf-file #(fetch-ttf-bytes (internal-uri (str "fonts/" ttf-file)))))
(defn- family-uuid
[id]
(uuid/from-unsigned-parts (aget id 0) (aget id 1) (aget id 2) (aget id 3)))
(defn- find-variant
"Custom variant for a family: uuid+weight+style first, degrading to
uuid+weight then uuid."
[variants font-uuid weight style]
(let [style-str (if (zero? style) "normal" "italic")]
(or (d/seek (fn [v] (and (= (:font-id v) font-uuid)
(= (:font-weight v) weight)
(= (name (:font-style v)) style-str)))
variants)
(d/seek (fn [v] (and (= (:font-id v) font-uuid)
(= (:font-weight v) weight)))
variants)
(d/seek (fn [v] (= (:font-id v) font-uuid)) variants))))
(defn- make-resolve-font
"Builds a `resolve-font` fn (family map -> promise of TTF bytes). Custom
variants first, matching uuid+weight+style then degrading to uuid+weight then
uuid; the bundled fonts for `uuid/zero`, which is what `font-id->uuid` maps
every builtin family to; google catalog otherwise."
variants first; the bundled fonts for `uuid/zero`, which is what
`font-id->uuid` maps every builtin family to; google catalog otherwise."
[variants params]
(fn [{:keys [id weight style]}]
(let [font-uuid (uuid/from-unsigned-parts (aget id 0) (aget id 1) (aget id 2) (aget id 3))
style-str (if (zero? style) "normal" "italic")
variant (or (d/seek (fn [v] (and (= (:font-id v) font-uuid)
(= (:font-weight v) weight)
(= (name (:font-style v)) style-str)))
variants)
(d/seek (fn [v] (and (= (:font-id v) font-uuid)
(= (:font-weight v) weight)))
variants)
(d/seek (fn [v] (= (:font-id v) font-uuid)) variants))]
(let [font-uuid (family-uuid id)
variant (find-variant variants font-uuid weight style)]
(cond
(:ttf-file-id variant)
(fetch-asset-bytes (:ttf-file-id variant) params)
@ -250,6 +267,25 @@
(fetch-gfont-bytes gurl)
(p/resolved nil))))))
(defn- make-font-url
"Builds a `font-url` fn (family map -> public URL of its TTF), the same
sources `make-resolve-font` downloads from but addressed publicly. The SVG
export emits one `@font-face` per family from these."
[variants]
(fn [{:keys [id weight style]}]
(let [font-uuid (family-uuid id)
variant (find-variant variants font-uuid weight style)]
(cond
(:ttf-file-id variant)
(public-uri (str "assets/by-id/" (:ttf-file-id variant)))
(= uuid/zero font-uuid)
(public-uri (str "fonts/" (cfnt/resolve-ttf-file weight style)))
:else
(some-> (cfnt/resolve-ttf-url font-uuid weight style)
(cfnt/gstatic->proxy-url (public-uri "internal/gfonts/font")))))))
;; --- fallback fonts (emoji + per-script noto fonts)
;;
;; Emoji and non-latin scripts render through fallback families, not through
@ -376,13 +412,19 @@
(defn- render-object-bytes
[type id scale]
(if (= :pdf type)
(let [bytes (wasm/render-shape-pdf id scale)]
(l/dbg :hint "PDF generated via Skia (render-wasm headless)"
:object-id (str id)
:backend "skia-wasm"
:bytes (.-length bytes))
bytes)
(case type
:pdf (let [bytes (wasm/render-shape-pdf id scale)]
(l/dbg :hint "PDF generated via Skia (render-wasm headless)"
:object-id (str id)
:backend "skia-wasm"
:bytes (.-length bytes))
bytes)
:svg (let [bytes (wasm/render-shape-svg id scale)]
(l/dbg :hint "SVG generated via Skia (render-wasm headless)"
:object-id (str id)
:backend "skia-wasm"
:bytes (.-length bytes))
bytes)
(wasm/render-shape-raster id scale type)))
(defn- render*
@ -406,11 +448,14 @@
(provision-fallback-fonts! scene)])
(p/mcat
(fn [[variants _]]
(let [resolve-font (make-resolve-font (or variants []) params)]
(let [variants (or variants [])
resolve-font (make-resolve-font variants params)
font-url (make-font-url variants)]
;; Before rendering, so the relayout below sees real
;; font metrics. Deduped across objects: shapes
;; sharing one family download its TTF once.
(wasm/provision-fonts! (map :id objects) resolve-font))))
(wasm/provision-fonts! (map :id objects) resolve-font
:font-url font-url))))
(p/mcat
(fn [_]
(relayout-text! scene)

View File

@ -252,7 +252,6 @@
;; there is nothing left to stop.
(rx/catch (fn [_] settle)))))))))
;; TODO: Remove once we support WASM SVG export
(def ^:private wasm-export-types #{:jpeg :webp :png :pdf :svg})
(defn- wasm-export-enabled?