mirror of
https://github.com/penpot/penpot.git
synced 2026-09-03 18:49:45 +00:00
✨ Render multiple-object SVG exports with render-wasm (#11475)
This commit is contained in:
parent
b92112da54
commit
70b443a716
@ -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]
|
||||
|
||||
@ -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))))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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?
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user