Improve svg shapes attrs handling

And collaterally it improves performance since now the attrs
processing is done in the import and not in the render.
This commit is contained in:
Andrey Antukh 2023-09-14 16:24:24 +02:00 committed by Alonso Torres
parent 807f475a2d
commit 4f23852bca
8 changed files with 292 additions and 225 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
*-init.clj *-init.clj
*.css.json
*.jar *.jar
*.orig *.orig
*.penpot *.penpot

View file

@ -6,4 +6,4 @@
(ns app.common.files.defaults) (ns app.common.files.defaults)
(def version 31) (def version 32)

View file

@ -18,6 +18,7 @@
[app.common.math :as mth] [app.common.math :as mth]
[app.common.pages.changes :as cpc] [app.common.pages.changes :as cpc]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.common.svg :as csvg]
[app.common.text :as txt] [app.common.text :as txt]
[app.common.types.shape :as cts] [app.common.types.shape :as cts]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
@ -587,7 +588,23 @@
(update-container [container] (update-container [container]
(d/update-when container :objects update-vals update-object))] (d/update-when container :objects update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(defmethod migrate 32
[data]
(letfn [(update-object [object]
(as-> object object
(if (contains? object :svg-attrs)
(update object :svg-attrs csvg/attrs->props)
object)
(if (contains? object :svg-viewbox)
(update object :svg-viewbox grc/make-rect)
object)))
(update-container [container]
(update container :objects update-vals update-object))]
(-> data (-> data
(update :pages-index update-vals update-container) (update :pages-index update-vals update-container)
(update :components update-vals update-container)))) (update :components update-vals update-container))))

View file

@ -6,10 +6,9 @@
(ns app.common.svg (ns app.common.svg
(:require (:require
#?(:cljs ["./svg_optimizer.js" :as svgo]) #?(:cljs ["./svg_optimizer.js" :as svgo])
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.matrix :as gmt] [app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
@ -548,34 +547,74 @@
[num-str] [num-str]
(cond (cond
(str/starts-with? num-str ".") (str/starts-with? num-str ".")
(str "0" num-str) (dm/str "0" num-str)
(str/starts-with? num-str "-.") (str/starts-with? num-str "-.")
(str "-0" (subs num-str 1)) (dm/str "-0" (subs num-str 1))
:else :else
num-str)) num-str))
(defn format-styles (defn- camelize
"Transforms attributes to their react equivalent" [s]
[attrs] (when (string? s)
(letfn [(format-styles [style-str] #?(:cljs (js* "~{}.replace(\":\", \"-\").replace(/-./g, x=>x[1].toUpperCase())", s)
(if (string? style-str) :clj (str/camel s))))
(->> (str/split style-str ";")
(map str/trim)
(map #(str/split % ":"))
(group-by first)
(map (fn [[key val]]
(vector (keyword key) (second (first val)))))
(into {}))
style-str))]
(cond-> attrs (defn parse-style
(contains? attrs :style) [style]
(update :style format-styles)))) (reduce (fn [res item]
(let [[k v] (-> (str/trim item) (str/split ":" 2))
k (keyword k)]
(if (contains? res k)
res
(assoc res (keyword k) v))))
{}
(str/split style ";")))
;; FIXME: rename to `format-style` or directly use parse-style on code...
(defn format-styles
"Transform string based styles found on attrs map to key-value map."
[attrs]
(if (contains? attrs :style)
(update attrs :style
(fn [style]
(if (string? style)
(parse-style style)
style)))
attrs))
(defn attrs->props
"Transforms and cleans svg attributes to react compatible props"
([attrs]
(attrs->props attrs true))
([attrs whitelist?]
(reduce-kv (fn [res k v]
(if (or (not whitelist?)
(contains? svg-attr-list k)
(contains? svg-present-list k))
(cond
(= k :class)
(assoc res :className val)
(= k :style)
(let [v (if (string? v) (parse-style v) v)]
(assoc res k (attrs->props v false)))
:else
(let [k (if (contains? non-react-props k)
k
(-> k d/name camelize keyword))]
(assoc res k v)))
res))
{}
attrs)))
(defn clean-attrs (defn clean-attrs
"Transforms attributes to their react equivalent" "Transforms attributes to their react equivalent
DEPRECATED: replaced by attrs->props"
([attrs] ([attrs]
(clean-attrs attrs true)) (clean-attrs attrs true))
@ -590,8 +629,7 @@
#?(:cljs (js* "~{}.replace(\":\", \"-\").replace(/-./g, x=>x[1].toUpperCase())", s) #?(:cljs (js* "~{}.replace(\":\", \"-\").replace(/-./g, x=>x[1].toUpperCase())", s)
:clj (str/camel s)))) :clj (str/camel s))))
(transform-key [key]
(transform-att [key]
(if (contains? non-react-props key) (if (contains? non-react-props key)
key key
(-> (d/name key) (-> (d/name key)
@ -604,17 +642,26 @@
(map #(str/split % ":")) (map #(str/split % ":"))
(group-by first) (group-by first)
(map (fn [[key val]] (map (fn [[key val]]
[(transform-att key) [(transform-key key)
(second (first val))])) (second (first val))]))
(into {}))) (into {})))
(clean-att [[att val]] (clean-key [[key val]]
(let [att (keyword att)] (let [key (keyword key)]
(cond (cond
(= att :class) [:className val] (= key :class)
(and (= att :style) (string? val)) [att (format-styles val)] [:className val]
(and (= att :style) (map? val)) [att (clean-attrs val false)]
:else [(transform-att att) val])))] (and (= key :style)
(string? val))
[key (format-styles val)]
(and (= key :style)
(map? val))
[key (clean-attrs val false)]
:else
[(transform-key key) val])))]
;; Removed this warning because slows a lot rendering with big svgs ;; Removed this warning because slows a lot rendering with big svgs
#_(let [filtered-props (->> attrs (remove known-property?) (map first))] #_(let [filtered-props (->> attrs (remove known-property?) (map first))]
@ -623,7 +670,7 @@
(into {} (into {}
(comp (filter known-property?) (comp (filter known-property?)
(map clean-att)) (map clean-key))
attrs)))) attrs))))
(defn update-attr-ids (defn update-attr-ids
@ -649,16 +696,17 @@
(defn replace-attrs-ids (defn replace-attrs-ids
"Replaces the ids inside a property" "Replaces the ids inside a property"
[attrs ids-mapping] [attrs ids-mapping]
(if (and ids-mapping (seq ids-mapping)) (if (empty? ids-mapping)
(update-attr-ids attrs (fn [id] (get ids-mapping id id))) attrs
;; Ids-mapping is null (update-attr-ids attrs (fn [id] (get ids-mapping id id)))))
attrs))
(defn generate-id-mapping [content] (defn generate-id-mapping
[content]
(letfn [(visit-node [result node] (letfn [(visit-node [result node]
(let [element-id (get-in node [:attrs :id]) (let [element-id (dm/get-in node [:attrs :id])
result (cond-> result result (if (some? element-id)
element-id (assoc element-id (str (uuid/next))))] (assoc result element-id (dm/str (uuid/next)))
result)]
(reduce visit-node result (:content node))))] (reduce visit-node result (:content node))))]
(visit-node {} content))) (visit-node {} content)))

View file

@ -14,6 +14,8 @@
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.rect :as grc] [app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.geom.shapes.common :as gsc]
[app.common.geom.shapes.transforms :as gst]
[app.common.math :as mth] [app.common.math :as mth]
[app.common.pages.changes-builder :as pcb] [app.common.pages.changes-builder :as pcb]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
@ -65,7 +67,8 @@
(contains? cts/blend-modes clean-value)) (contains? cts/blend-modes clean-value))
clean-value)) clean-value))
(defn- svg-dimensions [data] (defn- svg-dimensions
[data]
(let [width (dm/get-in data [:attrs :width] 100) (let [width (dm/get-in data [:attrs :width] 100)
height (dm/get-in data [:attrs :height] 100) height (dm/get-in data [:attrs :height] 100)
viewbox (dm/get-in data [:attrs :viewBox] (str "0 0 " width " " height)) viewbox (dm/get-in data [:attrs :viewBox] (str "0 0 " width " " height))
@ -81,12 +84,15 @@
(defn tag->name (defn tag->name
"Given a tag returns its layer name" "Given a tag returns its layer name"
[tag] [tag]
(str "svg-" (cond (string? tag) tag (let [suffix (cond
(keyword? tag) (d/name tag) (string? tag) tag
(nil? tag) "node" (keyword? tag) (d/name tag)
:else (str tag)))) (nil? tag) "node"
:else (dm/str tag))]
(dm/str "svg-" suffix)))
(defn setup-fill [shape] (defn setup-fill
[shape]
(let [color-attr (str/trim (dm/get-in shape [:svg-attrs :fill])) (let [color-attr (str/trim (dm/get-in shape [:svg-attrs :fill]))
color-attr (if (= color-attr "currentColor") clr/black color-attr) color-attr (if (= color-attr "currentColor") clr/black color-attr)
color-style (str/trim (dm/get-in shape [:svg-attrs :style :fill])) color-style (str/trim (dm/get-in shape [:svg-attrs :style :fill]))
@ -104,16 +110,16 @@
(update :svg-attrs dissoc :fill) (update :svg-attrs dissoc :fill)
(assoc-in [:fills 0 :fill-color] (uc/parse-color color-style))) (assoc-in [:fills 0 :fill-color] (uc/parse-color color-style)))
(dm/get-in shape [:svg-attrs :fill-opacity]) (dm/get-in shape [:svg-attrs :fillOpacity])
(-> (update :svg-attrs dissoc :fill-opacity) (-> (update :svg-attrs dissoc :fillOpacity)
(update-in [:svg-attrs :style] dissoc :fill-opacity) (update-in [:svg-attrs :style] dissoc :fillOpacity)
(assoc-in [:fills 0 :fill-opacity] (-> (dm/get-in shape [:svg-attrs :fill-opacity]) (assoc-in [:fills 0 :fill-opacity] (-> (dm/get-in shape [:svg-attrs :fillOpacity])
(d/parse-double 1)))) (d/parse-double 1))))
(dm/get-in shape [:svg-attrs :style :fill-opacity]) (dm/get-in shape [:svg-attrs :style :fillOpacity])
(-> (update-in [:svg-attrs :style] dissoc :fill-opacity) (-> (update-in [:svg-attrs :style] dissoc :fillOpacity)
(update :svg-attrs dissoc :fill-opacity) (update :svg-attrs dissoc :fillOpacity)
(assoc-in [:fills 0 :fill-opacity] (-> (dm/get-in shape [:svg-attrs :style :fill-opacity]) (assoc-in [:fills 0 :fill-opacity] (-> (dm/get-in shape [:svg-attrs :style :fillOpacity])
(d/parse-double 1))))))) (d/parse-double 1)))))))
(defn- setup-stroke (defn- setup-stroke
@ -131,30 +137,30 @@
opacity (when (some? color) opacity (when (some? color)
(d/parse-double (d/parse-double
(or (:stroke-opacity attrs) (or (:strokeOpacity attrs)
(:stroke-opacity style)) (:strokeOpacity style))
1)) 1))
width (when (some? color) width (when (some? color)
(d/parse-double (d/parse-double
(or (:stroke-width attrs) (or (:strokeWidth attrs)
(:stroke-width style)) (:strokeWidth style))
1)) 1))
linecap (or (get attrs :stroke-linecap) linecap (or (get attrs :strokeLinecap)
(get style :stroke-linecap)) (get style :strokeLinecap))
linecap (some-> linecap str/trim keyword) linecap (some-> linecap str/trim keyword)
attrs (-> attrs attrs (-> attrs
(dissoc :stroke) (dissoc :stroke)
(dissoc :stroke-width) (dissoc :strokeWidth)
(dissoc :stroke-opacity) (dissoc :strokeOpacity)
(update :style (fn [style] (update :style (fn [style]
(-> style (-> style
(dissoc :stroke) (dissoc :stroke)
(dissoc :stroke-linecap) (dissoc :strokeLinecap)
(dissoc :stroke-width) (dissoc :strokeWidth)
(dissoc :stroke-opacity)))))] (dissoc :strokeOpacity)))))]
(cond-> (assoc shape :svg-attrs attrs) (cond-> (assoc shape :svg-attrs attrs)
(some? color) (some? color)
@ -172,8 +178,8 @@
:stroke-cap-end linecap) :stroke-cap-end linecap)
(d/any-key? (dm/get-in shape [:strokes 0]) (d/any-key? (dm/get-in shape [:strokes 0])
:stroke-color :stroke-opacity :stroke-width :strokeColor :strokeOpacity :strokeWidth
:stroke-cap-start :stroke-cap-end) :strokeCapStart :strokeCapEnd)
(assoc-in [:strokes 0 :stroke-style] :svg)))) (assoc-in [:strokes 0 :stroke-style] :svg))))
(defn setup-opacity [shape] (defn setup-opacity [shape]
@ -188,51 +194,52 @@
(assoc :opacity (-> (dm/get-in shape [:svg-attrs :style :opacity]) (assoc :opacity (-> (dm/get-in shape [:svg-attrs :style :opacity])
(d/parse-double 1)))) (d/parse-double 1))))
(dm/get-in shape [:svg-attrs :mix-blend-mode]) (dm/get-in shape [:svg-attrs :mixBlendMode])
(-> (update :svg-attrs dissoc :mix-blend-mode) (-> (update :svg-attrs dissoc :mixBlendMode)
(assoc :blend-mode (-> (dm/get-in shape [:svg-attrs :mix-blend-mode]) assert-valid-blend-mode))) (assoc :blend-mode (-> (dm/get-in shape [:svg-attrs :mixBlendMode]) assert-valid-blend-mode)))
(dm/get-in shape [:svg-attrs :style :mix-blend-mode]) (dm/get-in shape [:svg-attrs :style :mixBlendMode])
(-> (update-in [:svg-attrs :style] dissoc :mix-blend-mode) (-> (update-in [:svg-attrs :style] dissoc :mixBlendMode)
(assoc :blend-mode (-> (dm/get-in shape [:svg-attrs :style :mix-blend-mode]) assert-valid-blend-mode))))) (assoc :blend-mode (-> (dm/get-in shape [:svg-attrs :style :mixBlendMode]) assert-valid-blend-mode)))))
(defn create-raw-svg (defn create-raw-svg
[name frame-id {:keys [x y width height offset-x offset-y]} {:keys [attrs] :as data}] [name frame-id {:keys [x y width height offset-x offset-y]} {:keys [attrs] :as data}]
(cts/setup-shape (let [props (csvg/attrs->props attrs)
{:type :svg-raw vbox (grc/make-rect offset-x offset-y width height)]
:name name (cts/setup-shape
:frame-id frame-id {:type :svg-raw
:width width :name name
:height height :frame-id frame-id
:x x :width width
:y y :height height
:content (cond-> data :x x
(map? data) (update :attrs csvg/clean-attrs)) :y y
:svg-attrs attrs :content data
:svg-viewbox {:width width :svg-attrs props
:height height :svg-viewbox vbox})))
:x offset-x
:y offset-y}}))
(defn create-svg-root (defn create-svg-root
[frame-id parent-id {:keys [name x y width height offset-x offset-y attrs]}] [frame-id parent-id {:keys [name x y width height offset-x offset-y attrs]}]
(cts/setup-shape (let [props (-> (dissoc attrs :viewBox :view-box :xmlns)
{:type :group (d/without-keys csvg/inheritable-props)
:name name (csvg/attrs->props))]
:frame-id frame-id (cts/setup-shape
:parent-id parent-id {:type :group
:width width :name name
:height height :frame-id frame-id
:x (+ x offset-x) :parent-id parent-id
:y (+ y offset-y) :width width
:svg-attrs (-> attrs :height height
(dissoc :viewBox) :x (+ x offset-x)
(dissoc :xmlns) :y (+ y offset-y)
(d/without-keys csvg/inheritable-props))})) :svg-attrs props})))
(defn create-group (defn create-group
[name frame-id {:keys [x y width height offset-x offset-y] :as svg-data} {:keys [attrs]}] [name frame-id {:keys [x y width height offset-x offset-y] :as svg-data} {:keys [attrs]}]
(let [svg-transform (csvg/parse-transform (:transform attrs))] (let [transform (csvg/parse-transform (:transform attrs))
attrs (-> (d/without-keys attrs csvg/inheritable-props)
(csvg/attrs->props))
vbox (grc/make-rect offset-x offset-y width height)]
(cts/setup-shape (cts/setup-shape
{:type :group {:type :group
:name name :name name
@ -241,28 +248,22 @@
:y (+ y offset-y) :y (+ y offset-y)
:width width :width width
:height height :height height
:svg-transform svg-transform :svg-transform transform
:svg-attrs (d/without-keys attrs csvg/inheritable-props) :svg-attrs attrs
:svg-viewbox vbox})))
:svg-viewbox {:width width
:height height
:x offset-x
:y offset-y}})))
(defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}] (defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}]
(when (and (contains? attrs :d) (seq (:d attrs))) (when (and (contains? attrs :d) (seq (:d attrs)))
(let [transform (csvg/parse-transform (:transform attrs))
content (cond-> (upp/parse-path (:d attrs))
(some? transform)
(gsh/transform-content transform))
(let [svg-transform (csvg/parse-transform (:transform attrs)) selrect (gsh/content->selrect content)
path-content (upp/parse-path (:d attrs)) points (grc/rect->points selrect)
content (cond-> path-content origin (gpt/negate (gpt/point svg-data))
svg-transform attrs (-> (dissoc attrs :d :transform)
(gsh/transform-content svg-transform)) (csvg/attrs->props))]
selrect (gsh/content->selrect content)
points (grc/rect->points selrect)
origin (gpt/negate (gpt/point svg-data))]
(-> (cts/setup-shape (-> (cts/setup-shape
{:type :path {:type :path
:name name :name name
@ -270,17 +271,20 @@
:content content :content content
:selrect selrect :selrect selrect
:points points :points points
:svg-viewbox (select-keys selrect [:x :y :width :height]) :svg-viewbox selrect
:svg-attrs (dissoc attrs :d :transform) :svg-attrs attrs
:svg-transform svg-transform}) :svg-transform transform
:fills []})
(gsh/translate-to-frame origin))))) (gsh/translate-to-frame origin)))))
(defn calculate-rect-metadata [rect-data transform] (defn calculate-rect-metadata
(let [points (-> (grc/make-rect rect-data) [rect transform]
(grc/rect->points) (let [points (-> rect
(gsh/transform-points transform)) (grc/rect->points)
(gsh/transform-points transform))
[selrect transform transform-inverse] (gsh/calculate-geometry points)] center (gsc/points->center points)
selrect (gst/calculate-selrect points center)
transform (gst/calculate-transform points center selrect)]
{:x (:x selrect) {:x (:x selrect)
:y (:y selrect) :y (:y selrect)
@ -289,107 +293,116 @@
:selrect selrect :selrect selrect
:points points :points points
:transform transform :transform transform
:transform-inverse transform-inverse})) :transform-inverse (when (some? transform)
(gmt/inverse transform))}))
(defn- parse-rect-attrs (defn- parse-rect-attrs
[{:keys [x y width height]}] [{:keys [x y width height]}]
{:x (d/parse-double x 0) (grc/make-rect
:y (d/parse-double y 0) (d/parse-double x 0)
:width (d/parse-double width 1) (d/parse-double y 0)
:height (d/parse-double height 1)}) (d/parse-double width 1)
(d/parse-double height 1)))
(defn create-rect-shape [name frame-id svg-data {:keys [attrs] :as data}] (defn create-rect-shape [name frame-id svg-data {:keys [attrs] :as data}]
(let [transform (->> (csvg/parse-transform (:transform attrs)) (let [transform (->> (csvg/parse-transform (:transform attrs))
(gmt/transform-in (gpt/point svg-data))) (gmt/transform-in (gpt/point svg-data)))
origin (gpt/negate (gpt/point svg-data)) origin (gpt/negate (gpt/point svg-data))
rect (-> (parse-rect-attrs attrs)
rect-data (-> (parse-rect-attrs attrs)
(update :x - (:x origin)) (update :x - (:x origin))
(update :y - (:y origin)))] (update :y - (:y origin)))
props (-> (dissoc attrs :x :y :width :height :rx :ry :transform)
(csvg/attrs->props))]
(cts/setup-shape (cts/setup-shape
(-> (calculate-rect-metadata rect-data transform) (-> (calculate-rect-metadata rect transform)
(assoc :type :rect) (assoc :type :rect)
(assoc :name name) (assoc :name name)
(assoc :frame-id frame-id) (assoc :frame-id frame-id)
(assoc :svg-viewbox (select-keys rect-data [:x :y :width :height])) (assoc :svg-viewbox rect)
(assoc :svg-attrs (dissoc attrs :x :y :width :height :rx :ry :transform)) (assoc :svg-attrs props)
;; We need to ensure fills are empty on import process
;; because setup-shape assings one by default.
(assoc :fills [])
(cond-> (contains? attrs :rx) (cond-> (contains? attrs :rx)
(assoc :rx (d/parse-double (:rx attrs) 0))) (assoc :rx (d/parse-double (:rx attrs) 0)))
(cond-> (contains? attrs :ry) (cond-> (contains? attrs :ry)
(assoc :ry (d/parse-double (:ry attrs) 0))))))) (assoc :ry (d/parse-double (:ry attrs) 0)))))))
(defn- parse-circle-attrs (defn- parse-circle-attrs
[attrs] [attrs]
(into [] (comp (map (d/getf attrs)) (into [] (comp (map (d/getf attrs))
(map d/parse-double)) (map d/parse-double))
[:cx :cy :r :rx :ry])) [:cx :cy :r :rx :ry]))
(defn create-circle-shape [name frame-id svg-data {:keys [attrs] :as data}] (defn create-circle-shape
[name frame-id svg-data {:keys [attrs] :as data}]
(let [[cx cy r rx ry] (let [[cx cy r rx ry]
(parse-circle-attrs attrs) (parse-circle-attrs attrs)
transform (->> (csvg/parse-transform (:transform attrs)) transform (->> (csvg/parse-transform (:transform attrs))
(gmt/transform-in (gpt/point svg-data))) (gmt/transform-in (gpt/point svg-data)))
rx (or r rx) rx (d/nilv r rx)
ry (or r ry) ry (d/nilv r ry)
origin (gpt/negate (gpt/point svg-data)) origin (gpt/negate (gpt/point svg-data))
rect-data {:x (- cx rx (:x origin)) rect (grc/make-rect
:y (- cy ry (:y origin)) (- cx rx (:x origin))
:width (* 2 rx) (- cy ry (:y origin))
:height (* 2 ry)}] (* 2 rx)
(* 2 ry))
props (-> (dissoc attrs :cx :cy :r :rx :ry :transform)
(csvg/attrs->props))]
(cts/setup-shape (cts/setup-shape
(-> (calculate-rect-metadata rect-data transform) (-> (calculate-rect-metadata rect transform)
(assoc :type :circle) (assoc :type :circle)
(assoc :name name) (assoc :name name)
(assoc :frame-id frame-id) (assoc :frame-id frame-id)
(assoc :svg-viewbox rect-data) (assoc :svg-viewbox rect)
(assoc :svg-attrs (dissoc attrs :cx :cy :r :rx :ry :transform)))))) (assoc :svg-attrs props)
(assoc :fills [])))))
(defn create-image-shape [name frame-id svg-data {:keys [attrs] :as data}] (defn create-image-shape
[name frame-id svg-data {:keys [attrs] :as data}]
(let [transform (->> (csvg/parse-transform (:transform attrs)) (let [transform (->> (csvg/parse-transform (:transform attrs))
(gmt/transform-in (gpt/point svg-data))) (gmt/transform-in (gpt/point svg-data)))
image-url (or (:href attrs) (:xlink:href attrs)) image-url (or (:href attrs) (:xlink:href attrs))
image-data (dm/get-in svg-data [:image-data image-url]) image-data (dm/get-in svg-data [:image-data image-url])
metadata {:width (:width image-data) metadata {:width (:width image-data)
:height (:height image-data) :height (:height image-data)
:mtype (:mtype image-data) :mtype (:mtype image-data)
:id (:id image-data)} :id (:id image-data)}
origin (gpt/negate (gpt/point svg-data)) origin (gpt/negate (gpt/point svg-data))
rect-data (-> (parse-rect-attrs attrs) rect (-> (parse-rect-attrs attrs)
(update :x - (:x origin)) (update :x - (:x origin))
(update :y - (:y origin)))] (update :y - (:y origin)))
props (-> (dissoc attrs :x :y :width :height :href :xlink:href)
(csvg/attrs->props))]
(when (some? image-data) (when (some? image-data)
(cts/setup-shape (cts/setup-shape
(-> (calculate-rect-metadata rect-data transform) (-> (calculate-rect-metadata rect transform)
(assoc :type :image) (assoc :type :image)
(assoc :name name) (assoc :name name)
(assoc :frame-id frame-id) (assoc :frame-id frame-id)
(assoc :metadata metadata) (assoc :metadata metadata)
(assoc :svg-viewbox (select-keys rect-data [:x :y :width :height])) (assoc :svg-viewbox rect)
(assoc :svg-attrs (dissoc attrs :x :y :width :height :href :xlink:href))))))) (assoc :svg-attrs props))))))
(defn parse-svg-element
(defn parse-svg-element [frame-id svg-data {:keys [tag attrs hidden] :as element-data} unames] [frame-id svg-data {:keys [tag attrs hidden] :as element} unames]
(let [attrs (csvg/format-styles attrs) (let [name (or (:id attrs) (tag->name tag))
element-data (cond-> element-data (map? element-data) (assoc :attrs attrs))
name (or (:id attrs) (tag->name tag))
att-refs (csvg/find-attr-references attrs) att-refs (csvg/find-attr-references attrs)
references (csvg/find-def-references (:defs svg-data) att-refs) defs (get svg-data :defs)
references (csvg/find-def-references defs att-refs)
href-id (-> (or (:href attrs) (:xlink:href attrs) "") (subs 1)) href-id (-> (or (:href attrs) (:xlink:href attrs) "") (subs 1))
defs (:defs svg-data)
use-tag? (and (= :use tag) (contains? defs href-id))] use-tag? (and (= :use tag) (contains? defs href-id))]
(if use-tag? (if use-tag?
@ -397,38 +410,43 @@
use-data (-> (get defs href-id) use-data (-> (get defs href-id)
(update :attrs #(d/deep-merge % (dissoc attrs :xlink:href :href)))) (update :attrs #(d/deep-merge % (dissoc attrs :xlink:href :href))))
displacement (gpt/point (d/parse-double (:x attrs "0")) (d/parse-double (:y attrs "0"))) displacement (gpt/point (d/parse-double (:x attrs "0")) (d/parse-double (:y attrs "0")))
disp-matrix (str (gmt/translate-matrix displacement)) disp-matrix (dm/str (gmt/translate-matrix displacement))
element-data (-> element-data element (-> element
(assoc :tag :g) (assoc :tag :g)
(update :attrs dissoc :x :y :width :height :href :xlink:href :transform) (update :attrs dissoc :x :y :width :height :href :xlink:href :transform)
(update :attrs csvg/add-transform disp-matrix) (update :attrs csvg/add-transform disp-matrix)
(assoc :content [use-data]))] (assoc :content [use-data]))]
(parse-svg-element frame-id svg-data element-data unames)) (parse-svg-element frame-id svg-data element unames))
(let [;; SVG graphic elements
;; :circle :ellipse :image :line :path :polygon :polyline :rect :text :use
shape (case tag
(:g :a :svg) (create-group name frame-id svg-data element)
:rect (create-rect-shape name frame-id svg-data element)
(:circle
:ellipse) (create-circle-shape name frame-id svg-data element)
:path (create-path-shape name frame-id svg-data element)
:polyline (create-path-shape name frame-id svg-data (-> element csvg/polyline->path))
:polygon (create-path-shape name frame-id svg-data (-> element csvg/polygon->path))
:line (create-path-shape name frame-id svg-data (-> element csvg/line->path))
:image (create-image-shape name frame-id svg-data element)
#_other (create-raw-svg name frame-id svg-data element))]
;; SVG graphic elements
;; :circle :ellipse :image :line :path :polygon :polyline :rect :text :use
(let [shape (-> (case tag
(:g :a :svg) (create-group name frame-id svg-data element-data)
:rect (create-rect-shape name frame-id svg-data element-data)
(:circle
:ellipse) (create-circle-shape name frame-id svg-data element-data)
:path (create-path-shape name frame-id svg-data element-data)
:polyline (create-path-shape name frame-id svg-data (-> element-data csvg/polyline->path))
:polygon (create-path-shape name frame-id svg-data (-> element-data csvg/polygon->path))
:line (create-path-shape name frame-id svg-data (-> element-data csvg/line->path))
:image (create-image-shape name frame-id svg-data element-data)
#_other (create-raw-svg name frame-id svg-data element-data)))]
(when (some? shape) (when (some? shape)
(let [shape (-> shape (let [shape (-> shape
(assoc :svg-defs (select-keys (:defs svg-data) references)) (assoc :svg-defs (select-keys defs references))
(setup-fill) (setup-fill)
(setup-stroke) (setup-stroke)
(setup-opacity))] (setup-opacity)
(update :svg-attrs (fn [attrs]
(if (empty? (:style attrs))
(dissoc attrs :style)
attrs))))]
[(cond-> shape [(cond-> shape
hidden (assoc :hidden true)) hidden (assoc :hidden true))
(cond->> (:content element-data) (cond->> (:content element)
(contains? csvg/parent-tags tag) (contains? csvg/parent-tags tag)
(mapv #(csvg/inherit-attributes attrs %)))])))))) (mapv #(csvg/inherit-attributes attrs %)))]))))))
@ -449,19 +467,6 @@
[unames children]))) [unames children])))
(defn data-uri->blob
[data-uri]
(let [[mtype b64-data] (str/split data-uri ";base64,")
mtype (subs mtype (inc (str/index-of mtype ":")))
decoded (.atob js/window b64-data)
size (.-length ^js decoded)
content (js/Uint8Array. size)]
(doseq [i (range 0 size)]
(aset content i (.charCodeAt decoded i)))
(wapi/create-blob content mtype)))
(defn extract-name [url] (defn extract-name [url]
(let [query-idx (str/last-index-of url "?") (let [query-idx (str/last-index-of url "?")
url (if (> query-idx 0) (subs url 0 query-idx) url) url (if (> query-idx 0) (subs url 0 query-idx) url)
@ -481,7 +486,7 @@
:url uri} :url uri}
(if (str/starts-with? uri "data:") (if (str/starts-with? uri "data:")
{:name "image" {:name "image"
:content (data-uri->blob uri)} :content (wapi/data-uri->blob uri)}
{:name (extract-name uri)})))) {:name (extract-name uri)}))))
(rx/mapcat (fn [uri-data] (rx/mapcat (fn [uri-data]
(->> (rp/cmd! (if (contains? uri-data :content) (->> (rp/cmd! (if (contains? uri-data :content)
@ -524,10 +529,6 @@
(csvg/fix-percents) (csvg/fix-percents)
(csvg/extract-defs)) (csvg/extract-defs))
svg-data (assoc svg-data :defs def-nodes)
root-shape (create-svg-root frame-id parent-id svg-data)
root-id (:id root-shape)
;; In penpot groups have the size of their children. To ;; In penpot groups have the size of their children. To
;; respect the imported svg size and empty space let's create ;; respect the imported svg size and empty space let's create
;; a transparent shape as background to respect the imported ;; a transparent shape as background to respect the imported
@ -547,6 +548,9 @@
(assoc :defs def-nodes) (assoc :defs def-nodes)
(assoc :content (into [background] (:content svg-data)))) (assoc :content (into [background] (:content svg-data))))
root-shape (create-svg-root frame-id parent-id svg-data)
root-id (:id root-shape)
;; Create the root shape ;; Create the root shape
root-attrs (-> (:attrs svg-data) root-attrs (-> (:attrs svg-data)
(csvg/format-styles)) (csvg/format-styles))
@ -561,7 +565,6 @@
(defn add-svg-shapes (defn add-svg-shapes
[svg-data position] [svg-data position]
;; (app.common.pprint/pprint svg-data {:length 100 :level 100})
(ptk/reify ::add-svg-shapes (ptk/reify ::add-svg-shapes
ptk/WatchEvent ptk/WatchEvent
(watch [it state _] (watch [it state _]

View file

@ -160,14 +160,10 @@
(empty? attrs)) (empty? attrs))
#js {} #js {}
(-> attrs (-> attrs
;; TODO: revisit, why we need to execute it each render? Can
;; we do this operation on importation and avoid unnecesary
;; work on render?
(csvg/clean-attrs)
(csvg/update-attr-ids (csvg/update-attr-ids
(fn [id] (fn [id]
(if (contains? defs id) (if (contains? defs id)
(str render-id "-" id) (dm/str render-id "-" id)
id))) id)))
(dissoc :id) (dissoc :id)
(obj/map->obj))))) (obj/map->obj)))))

View file

@ -15,23 +15,25 @@
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
;; Graphic tags ;; Graphic tags
(defonce graphic-element? (def graphic-element
#{:svg :circle :ellipse :image :line :path :polygon :polyline :rect :symbol :text :textPath :use}) #{:svg :circle :ellipse :image :line :path :polygon :polyline :rect :symbol :text :textPath :use})
;; Context to store a re-mapping of the ids ;; Context to store a re-mapping of the ids
(def svg-ids-ctx (mf/create-context nil)) (def svg-ids-ctx (mf/create-context nil))
(defn set-styles [attrs shape render-id] (defn set-styles [attrs shape render-id]
(let [custom-attrs (-> (usa/get-style-props shape render-id) (let [props (-> (usa/get-style-props shape render-id)
(obj/unset! "transform")) (obj/unset! "transform"))
attrs (or attrs {}) attrs (if (map? attrs)
attrs (cond-> attrs (-> attrs csvg/attrs->props obj/map->obj)
(string? (:style attrs)) csvg/clean-attrs) #js {})
style (obj/merge! (clj->js (:style attrs {}))
(obj/get custom-attrs "style"))] style (obj/merge (obj/get attrs "style")
(-> (clj->js attrs) (obj/get props "style"))]
(obj/merge! custom-attrs)
(-> attrs
(obj/merge! props)
(obj/set! "style" style)))) (obj/set! "style" style))))
(defn translate-shape [attrs shape] (defn translate-shape [attrs shape]
@ -39,7 +41,7 @@
" " " "
(:transform attrs ""))] (:transform attrs ""))]
(cond-> attrs (cond-> attrs
(and (:svg-viewbox shape) (graphic-element? (-> shape :content :tag))) (and (:svg-viewbox shape) (contains? graphic-element (-> shape :content :tag)))
(assoc :transform transform)))) (assoc :transform transform))))
(mf/defc svg-root (mf/defc svg-root

View file

@ -61,7 +61,7 @@
childs (unchecked-get props "childs") childs (unchecked-get props "childs")
frame (unchecked-get props "frame") frame (unchecked-get props "frame")
render-wrapper? (or (not= :svg-raw (:type shape)) render-wrapper? (or (not= :svg-raw (:type shape))
(svg-raw/graphic-element? (get-in shape [:content :tag])))] (contains? svg-raw/graphic-element (get-in shape [:content :tag])))]
(if render-wrapper? (if render-wrapper?
[:> shape-container {:shape shape [:> shape-container {:shape shape