diff --git a/exporter/src/app/renderer.cljs b/exporter/src/app/renderer.cljs
index 2cc0c424e4..320ffa8060 100644
--- a/exporter/src/app/renderer.cljs
+++ b/exporter/src/app/renderer.cljs
@@ -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]
diff --git a/exporter/src/app/wasm.cljs b/exporter/src/app/wasm.cljs
index dfebf84d39..cd6cbc09ea 100644
--- a/exporter/src/app/wasm.cljs
+++ b/exporter/src/app/wasm.cljs
@@ -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))))
diff --git a/exporter/src/app/wasm/render.cljs b/exporter/src/app/wasm/render.cljs
index d286c6cc1f..6993c53bba 100644
--- a/exporter/src/app/wasm/render.cljs
+++ b/exporter/src/app/wasm/render.cljs
@@ -19,8 +19,7 @@
because that namespace still exists as the proxy. Reviewable as a rename:
`git show :exporter/src/app/renderer/wasm.cljs | diff -u - `.
- 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)
diff --git a/frontend/src/app/main/data/exports/assets.cljs b/frontend/src/app/main/data/exports/assets.cljs
index 4055da67ca..f351746b49 100644
--- a/frontend/src/app/main/data/exports/assets.cljs
+++ b/frontend/src/app/main/data/exports/assets.cljs
@@ -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?