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?