0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-10 00:58:26 -05:00

Move util.svg to common.svg and make it crossplatform

This commit is contained in:
Andrey Antukh 2023-09-13 13:01:38 +02:00 committed by Alonso Torres
parent 878f1d4090
commit aa8300c085
11 changed files with 1029 additions and 1039 deletions

View file

@ -5,9 +5,986 @@
;; Copyright (c) KALEIDOS INC
(ns app.common.svg
#?(:cljs
(:require
["./svg_optimizer.js" :as svgo])))
(:require
#?(:cljs ["./svg_optimizer.js" :as svgo])
[app.common.data :as d]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.uuid :as uuid]
[cuerdas.core :as str]))
;; Regex for XML ids per Spec
;; https://www.w3.org/TR/2008/REC-xml-20081126/#sec-common-syn
(def xml-id-regex #"#([:A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u10000-\uEFFFF][\.\-\:0-9\xB7A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0300-\u036F\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u10000-\uEFFFF]*)")
(def matrices-regex #"(matrix|translate|scale|rotate|skewX|skewY)\(([^\)]*)\)")
(def number-regex #"[+-]?\d*(\.\d+)?(e[+-]?\d+)?")
(def tags-to-remove #{:linearGradient :radialGradient :metadata :mask :clipPath :filter :title})
;; https://www.w3.org/TR/SVG11/eltindex.html
(def svg-tags-list
#{:a
:altGlyph
:altGlyphDef
:altGlyphItem
:animate
:animateColor
:animateMotion
:animateTransform
:circle
:clipPath
:color-profile
:cursor
:defs
:desc
:ellipse
:feBlend
:feColorMatrix
:feComponentTransfer
:feComposite
:feConvolveMatrix
:feDiffuseLighting
:feDisplacementMap
:feDistantLight
:feFlood
:feFuncA
:feFuncB
:feFuncG
:feFuncR
:feGaussianBlur
:feImage
:feMerge
:feMergeNode
:feMorphology
:feOffset
:fePointLight
:feSpecularLighting
:feSpotLight
:feTile
:feTurbulence
:filter
:font
:font-face
:font-face-format
:font-face-name
:font-face-src
:font-face-uri
:foreignObject
:g
:glyph
:glyphRef
:hkern
:image
:line
:linearGradient
:marker
:mask
:metadata
:missing-glyph
:mpath
:path
:pattern
:polygon
:polyline
:radialGradient
:rect
:set
:stop
:style
:svg
:switch
:symbol
:text
:textPath
:title
:tref
:tspan
:use
:view
:vkern
})
;; https://www.w3.org/TR/SVG11/attindex.html
(def svg-attr-list
#{:accent-height
:accumulate
:additive
:alphabetic
:amplitude
:arabic-form
:ascent
:attributeName
:attributeType
:azimuth
:baseFrequency
:baseProfile
:bbox
:begin
:bias
:by
:calcMode
:cap-height
:class
:clipPathUnits
:contentScriptType
:contentStyleType
:cx
:cy
:d
:descent
:diffuseConstant
:divisor
:dur
:dx
:dy
:edgeMode
:elevation
:end
:exponent
:externalResourcesRequired
:fill
:filterRes
:filterUnits
:font-family
:font-size
:font-stretch
:font-style
:font-variant
:font-weight
:format
:from
:fx
:fy
:g1
:g2
:glyph-name
:glyphRef
:gradientTransform
:gradientUnits
:hanging
:height
:horiz-adv-x
:horiz-origin-x
:horiz-origin-y
:id
:ideographic
:in
:in2
:intercept
:k
:k1
:k2
:k3
:k4
:kernelMatrix
:kernelUnitLength
:keyPoints
:keySplines
:keyTimes
:lang
:lengthAdjust
:limitingConeAngle
:local
:markerHeight
:markerUnits
:markerWidth
:maskContentUnits
:maskUnits
:mathematical
:max
:media
:method
:min
:mode
:name
:numOctaves
:offset
;; We don't support events
;;:onabort
;;:onactivate
;;:onbegin
;;:onclick
;;:onend
;;:onerror
;;:onfocusin
;;:onfocusout
;;:onload
;;:onmousedown
;;:onmousemove
;;:onmouseout
;;:onmouseover
;;:onmouseup
;;:onrepeat
;;:onresize
;;:onscroll
;;:onunload
;;:onzoom
:operator
:order
:orient
:orientation
:origin
:overline-position
:overline-thickness
:panose-1
:path
:pathLength
:patternContentUnits
:patternTransform
:patternUnits
:points
:pointsAtX
:pointsAtY
:pointsAtZ
:preserveAlpha
:preserveAspectRatio
:primitiveUnits
:r
:radius
:refX
:refY
:rendering-intent
:repeatCount
:repeatDur
:requiredExtensions
:requiredFeatures
:restart
:result
:rotate
:rx
:ry
:scale
:seed
:slope
:spacing
:specularConstant
:specularExponent
:spreadMethod
:startOffset
:stdDeviation
:stemh
:stemv
:stitchTiles
:strikethrough-position
:strikethrough-thickness
:string
:style
:surfaceScale
:systemLanguage
:tableValues
:target
:targetX
:targetY
:textLength
:title
:to
:transform
:type
:u1
:u2
:underline-position
:underline-thickness
:unicode
:unicode-range
:units-per-em
:v-alphabetic
:v-hanging
:v-ideographic
:v-mathematical
:values
:version
:vert-adv-y
:vert-origin-x
:vert-origin-y
:viewBox
:viewTarget
:width
:widths
:x
:x-height
:x1
:x2
:xChannelSelector
:xmlns:xlink
:xlink:actuate
:xlink:arcrole
:xlink:href
:xlink:role
:xlink:show
:xlink:title
:xlink:type
:xml:base
:xml:lang
:xml:space
:y
:y1
:y2
:yChannelSelector
:z
:zoomAndPan})
(def svg-present-list
#{:alignment-baseline
:baseline-shift
:clip-path
:clip-rule
:clip
:color-interpolation-filters
:color-interpolation
:color-profile
:color-rendering
:color
:cursor
:direction
:display
:dominant-baseline
:enable-background
:fill-opacity
:fill-rule
:fill
:filter
:flood-color
:flood-opacity
:font-family
:font-size-adjust
:font-size
:font-stretch
:font-style
:font-variant
:font-weight
:glyph-orientation-horizontal
:glyph-orientation-vertical
:image-rendering
:kerning
:letter-spacing
:lighting-color
:marker-end
:marker-mid
:marker-start
:mask
:opacity
:overflow
:pointer-events
:shape-rendering
:stop-color
:stop-opacity
:stroke-dasharray
:stroke-dashoffset
:stroke-linecap
:stroke-linejoin
:stroke-miterlimit
:stroke-opacity
:stroke-width
:stroke
:text-anchor
:text-decoration
:text-rendering
:unicode-bidi
:visibility
:word-spacing
:writing-mode
:mask-type})
(def inheritable-props
[:style
:clip-rule
:color
:color-interpolation
:color-interpolation-filters
:color-profile
:color-rendering
:cursor
:direction
:dominant-baseline
:fill
:fill-opacity
:fill-rule
:font
:font-family
:font-size
:font-size-adjust
:font-stretch
:font-style
:font-variant
:font-weight
:glyph-orientation-horizontal
:glyph-orientation-vertical
:image-rendering
:letter-spacing
:marker
:marker-end
:marker-mid
:marker-start
:paint-order
:pointer-events
:shape-rendering
:stroke
:stroke-dasharray
:stroke-dashoffset
:stroke-linecap
:stroke-linejoin
:stroke-miterlimit
:stroke-opacity
:stroke-width
:text-anchor
:text-rendering
:transform
:visibility
:word-spacing
:writing-mode])
(def gradient-tags
#{:linearGradient
:radialGradient})
(def filter-tags
#{:filter
:feBlend
:feColorMatrix
:feComponentTransfer
:feComposite
:feConvolveMatrix
:feDiffuseLighting
:feDisplacementMap
:feFlood
:feGaussianBlur
:feImage
:feMerge
:feMorphology
:feOffset
:feSpecularLighting
:feTile
:feTurbulence})
(def parent-tags
#{:g
:svg
:text
:tspan})
;; By spec: https://www.w3.org/TR/SVG11/single-page.html#struct-GElement
(def svg-group-safe-tags
#{:animate
:animateColor
:animateMotion
:animateTransform
:set
:desc
:metadata
:title
:circle
:ellipse
:line
:path
:polygon
:polyline
:rect
:defs
:g
:svg
:symbol
:use
:linearGradient
:radialGradient
:a
:altGlyphDef
:clipPath
:color-profile
:cursor
:filter
:font
:font-face
:foreignObject
:image
:marker
:mask
:pattern
:style
:switch
:text
:view})
;; Props not supported by react we need to keep them lowercase
(def non-react-props
#{:mask-type})
;; Defaults for some tags per spec https://www.w3.org/TR/SVG11/single-page.html
;; they are basically the defaults that can be percents and we need to replace because
;; otherwise won't work as expected in the workspace
(def svg-tag-defaults
(let [filter-default {:units :filterUnits
:default "objectBoundingBox"
"objectBoundingBox" {}
"userSpaceOnUse" {:x "-10%" :y "-10%" :width "120%" :height "120%"}}
filter-values (->> filter-tags
(reduce #(merge %1 (hash-map %2 filter-default)) {}))]
(merge {:linearGradient {:units :gradientUnits
:default "objectBoundingBox"
"objectBoundingBox" {}
"userSpaceOnUse" {:x1 "0%" :y1 "0%" :x2 "100%" :y2 "0%"}}
:radialGradient {:units :gradientUnits
:default "objectBoundingBox"
"objectBoundingBox" {}
"userSpaceOnUse" {:cx "50%" :cy "50%" :r "50%"}}
:mask {:units :maskUnits
:default "userSpaceOnUse"
"objectBoundingBox" {}
"userSpaceOnUse" {:x "-10%" :y "-10%" :width "120%" :height "120%"}}}
filter-values)))
(defn extract-ids [val]
(when (some? val)
(->> (re-seq xml-id-regex val)
(mapv second))))
(defn fix-dot-number
"Fixes decimal numbers starting in dot but without leading 0"
[num-str]
(cond
(str/starts-with? num-str ".")
(str "0" num-str)
(str/starts-with? num-str "-.")
(str "-0" (subs num-str 1))
:else
num-str))
(defn format-styles
"Transforms attributes to their react equivalent"
[attrs]
(letfn [(format-styles [style-str]
(if (string? style-str)
(->> (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
(contains? attrs :style)
(update :style format-styles))))
(defn clean-attrs
"Transforms attributes to their react equivalent"
([attrs]
(clean-attrs attrs true))
([attrs whitelist?]
(letfn [(known-property? [[key _]]
(or (not whitelist?)
(contains? svg-attr-list key)
(contains? svg-present-list key)))
(camelize [s]
(when (string? s)
#?(:cljs (js* "~{}.replace(\":\", \"-\").replace(/-./g, x=>x[1].toUpperCase())", s)
:clj (str/camel s))))
(transform-att [key]
(if (contains? non-react-props key)
key
(-> (d/name key)
(camelize)
(keyword))))
(format-styles [style-str]
(->> (str/split style-str ";")
(map str/trim)
(map #(str/split % ":"))
(group-by first)
(map (fn [[key val]]
[(transform-att key)
(second (first val))]))
(into {})))
(clean-att [[att val]]
(let [att (keyword att)]
(cond
(= att :class) [:className val]
(and (= att :style) (string? val)) [att (format-styles val)]
(and (= att :style) (map? val)) [att (clean-attrs val false)]
:else [(transform-att att) val])))]
;; Removed this warning because slows a lot rendering with big svgs
#_(let [filtered-props (->> attrs (remove known-property?) (map first))]
(when (seq filtered-props)
(.warn js/console "Unknown properties: " (str/join ", " filtered-props ))))
(into {}
(comp (filter known-property?)
(map clean-att))
attrs))))
(defn update-attr-ids
"Replaces the ids inside a property"
[attrs replace-fn]
(letfn [(update-ids [key val]
(cond
(map? val)
(d/mapm update-ids val)
(= key :id)
(replace-fn val)
:else
(let [replace-id
(fn [result it]
(let [to-replace (replace-fn it)]
(str/replace result (str "#" it) (str "#" to-replace))))]
(reduce replace-id val (extract-ids val)))))]
(d/mapm update-ids attrs)))
(defn replace-attrs-ids
"Replaces the ids inside a property"
[attrs ids-mapping]
(if (and ids-mapping (seq ids-mapping))
(update-attr-ids attrs (fn [id] (get ids-mapping id id)))
;; Ids-mapping is null
attrs))
(defn generate-id-mapping [content]
(letfn [(visit-node [result node]
(let [element-id (get-in node [:attrs :id])
result (cond-> result
element-id (assoc element-id (str (uuid/next))))]
(reduce visit-node result (:content node))))]
(visit-node {} content)))
(defn extract-defs [{:keys [attrs] :as node}]
(if-not (map? node)
[{} node]
(let [remove-node? (fn [{:keys [tag]}] (and (some? tag)
(or (contains? tags-to-remove tag)
(not (contains? svg-tags-list tag)))))
rec-result (->> (:content node) (map extract-defs))
node (assoc node :content (->> rec-result (map second) (filterv (comp not remove-node?))))
current-node-defs (if (contains? attrs :id)
(hash-map (:id attrs) node)
(hash-map))
node-defs (->> rec-result (map first) (reduce merge current-node-defs))]
[ node-defs node ])))
(defn find-attr-references [attrs]
(->> attrs
(mapcat (fn [[_ attr-value]]
(if (string? attr-value)
(extract-ids attr-value)
(find-attr-references attr-value))))))
(defn find-node-references [node]
(let [current (->> (find-attr-references (:attrs node)) (into #{}))
children (->> (:content node) (map find-node-references) (flatten) (into #{}))]
(vec (into current children))))
(defn find-def-references [defs references]
(loop [result (into #{} references)
checked? #{}
to-check (first references)
pending (rest references)]
(cond
(nil? to-check)
result
(checked? to-check)
(recur result
checked?
(first pending)
(rest pending))
:else
(let [node (get defs to-check)
new-refs (find-node-references node)
pending (concat pending new-refs)]
(recur (into result new-refs)
(conj checked? to-check)
(first pending)
(rest pending))))))
(defn svg-transform-matrix [shape]
(if (:svg-viewbox shape)
(let [{svg-x :x
svg-y :y
svg-width :width
svg-height :height} (:svg-viewbox shape)
{:keys [x y width height]} (:selrect shape)
scale-x (/ width svg-width)
scale-y (/ height svg-height)]
(gmt/multiply
(gmt/matrix)
;; Paths doesn't have transform so we have to transform its gradients
(if (or (= :path (:type shape))
(= :group (:type shape)))
(gsh/transform-matrix shape)
(gmt/matrix))
(gmt/translate-matrix (gpt/point (- x (* scale-x svg-x)) (- y (* scale-y svg-y))))
(gmt/scale-matrix (gpt/point scale-x scale-y))))
;; :else
(gmt/matrix)))
;; Parse transform attributes to native matrix format so we can transform paths instead of
;; relying in SVG transformation. This is necessary to import SVG's and not to break path tooling
;;
;; Transforms spec:
;; https://www.w3.org/TR/SVG11/single-page.html#coords-TransformAttribute
(defn format-translate-params [params]
(assert (or (= (count params) 1) (= (count params) 2)))
(if (= (count params) 1)
[(gpt/point (nth params 0) 0)]
[(gpt/point (nth params 0) (nth params 1))]))
(defn format-scale-params [params]
(assert (or (= (count params) 1) (= (count params) 2)))
(if (= (count params) 1)
[(gpt/point (nth params 0))]
[(gpt/point (nth params 0) (nth params 1))]))
(defn format-rotate-params [params]
(assert (or (= (count params) 1) (= (count params) 3)) (str "??" (count params)))
(if (= (count params) 1)
[(nth params 0) (gpt/point 0 0)]
[(nth params 0) (gpt/point (nth params 1) (nth params 2))]))
(defn format-skew-x-params [params]
(assert (= (count params) 1))
[(nth params 0) 0])
(defn format-skew-y-params [params]
(assert (= (count params) 1))
[0 (nth params 0)])
(defn to-matrix [{:keys [type params]}]
(assert (#{"matrix" "translate" "scale" "rotate" "skewX" "skewY"} type))
(case type
"matrix" (apply gmt/matrix params)
"translate" (apply gmt/translate-matrix (format-translate-params params))
"scale" (apply gmt/scale-matrix (format-scale-params params))
"rotate" (apply gmt/rotate-matrix (format-rotate-params params))
"skewX" (apply gmt/skew-matrix (format-skew-x-params params))
"skewY" (apply gmt/skew-matrix (format-skew-y-params params))))
(defn parse-transform [transform-attr]
(if transform-attr
(let [process-matrix
(fn [[_ type params]]
(let [params (->> (re-seq number-regex params)
(filter #(-> % first seq))
(map (comp d/parse-double first)))]
{:type type :params params}))
matrices (->> (re-seq matrices-regex transform-attr)
(map process-matrix)
(map to-matrix))]
(reduce gmt/multiply (gmt/matrix) matrices))
(gmt/matrix)))
(defn format-move [[x y]] (str "M" x " " y))
(defn format-line [[x y]] (str "L" x " " y))
(defn points->path [points-str]
(let [points (->> points-str
(re-seq number-regex)
(filter (comp not empty? first))
(mapv (comp d/parse-double first))
(partition 2))
head (first points)
other (rest points)]
(str (format-move head)
(->> other (map format-line) (str/join " ")))))
(defn polyline->path [{:keys [attrs] :as node}]
(let [tag :path
attrs (-> attrs
(dissoc :points)
(assoc :d (points->path (:points attrs))))]
(assoc node :attrs attrs :tag tag)))
(defn polygon->path [{:keys [attrs] :as node}]
(let [tag :path
attrs (-> attrs
(dissoc :points)
(assoc :d (str (points->path (:points attrs)) "Z")))]
(assoc node :attrs attrs :tag tag)))
(defn line->path [{:keys [attrs] :as node}]
(let [tag :path
{:keys [x1 y1 x2 y2]} attrs
x1 (or x1 0)
y1 (or y1 0)
x2 (or x2 0)
y2 (or y2 0)
attrs (-> attrs
(dissoc :x1 :x2 :y1 :y2)
(assoc :d (str "M" x1 "," y1 " L" x2 "," y2)))]
(assoc node :attrs attrs :tag tag)))
(defn add-transform [attrs transform]
(letfn [(append-transform [old-transform]
(if (or (nil? old-transform) (empty? old-transform))
transform
(str transform " " old-transform)))]
(cond-> attrs
transform
(update :transform append-transform))))
(defn inherit-attributes [group-attrs {:keys [attrs] :as node}]
(if (map? node)
(let [attrs (-> (format-styles attrs)
(add-transform (:transform group-attrs)))
attrs (d/deep-merge (select-keys group-attrs inheritable-props) attrs)]
(assoc node :attrs attrs))
node))
(defn map-nodes [mapfn node]
(let [update-content
(fn [content] (cond->> content
(vector? content)
(mapv (partial map-nodes mapfn))))]
(cond-> node
(map? node)
(-> (mapfn)
(d/update-when :content update-content)))))
(defn reduce-nodes [redfn value node]
(let [reduce-content
(fn [value content]
(loop [current (first content)
content (rest content)
value value]
(if (nil? current)
value
(recur (first content)
(rest content)
(reduce-nodes redfn value current)))))]
(if (map? node)
(-> (redfn value node)
(reduce-content (:content node)))
value)))
(defn fix-default-values
"Gives values to some SVG elements which defaults won't work when imported into the platform"
[svg-data]
(let [add-defaults
(fn [{:keys [tag attrs] :as node}]
(let [prop (get-in svg-tag-defaults [tag :units])
default-units (get-in svg-tag-defaults [tag :default])
units (get attrs prop default-units)
tag-default (get-in svg-tag-defaults [tag units])]
(d/update-when node :attrs #(merge tag-default %))))
fix-node-defaults
(fn [node]
(cond-> node
(contains? svg-tag-defaults (:tag node))
(add-defaults)))]
(->> svg-data (map-nodes fix-node-defaults))))
(defn calculate-ratio
;; sqrt((actual-width)**2 + (actual-height)**2)/sqrt(2).
[width height]
(/ (mth/hypot width height)
(mth/sqrt 2)))
(defn fix-percents
"Changes percents to a value according to the size of the svg imported"
[svg-data]
;; https://www.w3.org/TR/SVG11/single-page.html#coords-Units
(let [viewbox {:x (:offset-x svg-data)
:y (:offset-y svg-data)
:width (:width svg-data)
:height (:height svg-data)
:ratio (calculate-ratio (:width svg-data) (:height svg-data))}]
(letfn [(fix-length [prop-length val]
(* (get viewbox prop-length) (/ val 100.)))
(fix-coord [prop-coord prop-length val]
(+ (get viewbox prop-coord)
(fix-length prop-length val)))
(fix-percent-attr-viewbox [attr-key attr-val]
(let [is-percent? (str/ends-with? attr-val "%")
is-x? #{:x :x1 :x2 :cx}
is-y? #{:y :y1 :y2 :cy}
is-width? #{:width}
is-height? #{:height}
is-other? #{:r :stroke-width}]
(if is-percent?
;; JS parseFloat removes the % symbol
(let [attr-num (d/parse-double attr-val)]
(str (cond
(is-x? attr-key) (fix-coord :x :width attr-num)
(is-y? attr-key) (fix-coord :y :height attr-num)
(is-width? attr-key) (fix-length :width attr-num)
(is-height? attr-key) (fix-length :height attr-num)
(is-other? attr-key) (fix-length :ratio attr-num)
:else attr-val)))
attr-val)))
(fix-percent-attrs-viewbox [attrs]
(d/mapm fix-percent-attr-viewbox attrs))
(fix-percent-attr-numeric [_ attr-val]
(let [is-percent? (str/ends-with? attr-val "%")]
(if is-percent?
(str (let [attr-num (d/parse-double attr-val)]
(/ attr-num 100)))
attr-val)))
(fix-percent-attrs-numeric [attrs]
(d/mapm fix-percent-attr-numeric attrs))
(fix-percent-values [node]
(let [units (or (get-in node [:attrs :filterUnits])
(get-in node [:attrs :gradientUnits])
(get-in node [:attrs :patternUnits])
(get-in node [:attrs :clipUnits]))]
(cond-> node
(= "objectBoundingBox" units)
(update :attrs fix-percent-attrs-numeric)
(not= "objectBoundingBox" units)
(update :attrs fix-percent-attrs-viewbox))))]
(->> svg-data (map-nodes fix-percent-values)))))
(defn collect-images [svg-data]
(let [redfn (fn [acc {:keys [tag attrs]}]
(cond-> acc
(= :image tag)
(conj (or (:href attrs) (:xlink:href attrs)))))]
(reduce-nodes redfn [] svg-data )))
#?(:cljs
(defn optimize

View file

@ -18,6 +18,7 @@
[app.common.pages.changes-builder :as pcb]
[app.common.pages.helpers :as cph]
[app.common.schema :as sm :refer [max-safe-int min-safe-int]]
[app.common.svg :as csvg]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.main.data.workspace.changes :as dch]
@ -27,7 +28,6 @@
[app.main.repo :as rp]
[app.util.color :as uc]
[app.util.path.parser :as upp]
[app.util.svg :as usvg]
[app.util.webapi :as wapi]
[beicon.core :as rx]
[cuerdas.core :as str]
@ -207,7 +207,7 @@
:x x
:y y
:content (cond-> data
(map? data) (update :attrs usvg/clean-attrs))
(map? data) (update :attrs csvg/clean-attrs))
:svg-attrs attrs
:svg-viewbox {:width width
:height height
@ -228,11 +228,11 @@
:svg-attrs (-> attrs
(dissoc :viewBox)
(dissoc :xmlns)
(d/without-keys usvg/inheritable-props))}))
(d/without-keys csvg/inheritable-props))}))
(defn create-group
[name frame-id {:keys [x y width height offset-x offset-y] :as svg-data} {:keys [attrs]}]
(let [svg-transform (usvg/parse-transform (:transform attrs))]
(let [svg-transform (csvg/parse-transform (:transform attrs))]
(cts/setup-shape
{:type :group
:name name
@ -242,7 +242,7 @@
:width width
:height height
:svg-transform svg-transform
:svg-attrs (d/without-keys attrs usvg/inheritable-props)
:svg-attrs (d/without-keys attrs csvg/inheritable-props)
:svg-viewbox {:width width
:height height
@ -252,7 +252,7 @@
(defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}]
(when (and (contains? attrs :d) (seq (:d attrs)))
(let [svg-transform (usvg/parse-transform (:transform attrs))
(let [svg-transform (csvg/parse-transform (:transform attrs))
path-content (upp/parse-path (:d attrs))
content (cond-> path-content
svg-transform
@ -299,7 +299,7 @@
:height (d/parse-double height 1)})
(defn create-rect-shape [name frame-id svg-data {:keys [attrs] :as data}]
(let [transform (->> (usvg/parse-transform (:transform attrs))
(let [transform (->> (csvg/parse-transform (:transform attrs))
(gmt/transform-in (gpt/point svg-data)))
origin (gpt/negate (gpt/point svg-data))
@ -331,7 +331,7 @@
(let [[cx cy r rx ry]
(parse-circle-attrs attrs)
transform (->> (usvg/parse-transform (:transform attrs))
transform (->> (csvg/parse-transform (:transform attrs))
(gmt/transform-in (gpt/point svg-data)))
rx (or r rx)
@ -352,7 +352,7 @@
(assoc :svg-attrs (dissoc attrs :cx :cy :r :rx :ry :transform))))))
(defn create-image-shape [name frame-id svg-data {:keys [attrs] :as data}]
(let [transform (->> (usvg/parse-transform (:transform attrs))
(let [transform (->> (csvg/parse-transform (:transform attrs))
(gmt/transform-in (gpt/point svg-data)))
image-url (or (:href attrs) (:xlink:href attrs))
@ -381,11 +381,11 @@
(defn parse-svg-element [frame-id svg-data {:keys [tag attrs hidden] :as element-data} unames]
(let [attrs (usvg/format-styles attrs)
(let [attrs (csvg/format-styles attrs)
element-data (cond-> element-data (map? element-data) (assoc :attrs attrs))
name (or (:id attrs) (tag->name tag))
att-refs (usvg/find-attr-references attrs)
references (usvg/find-def-references (:defs svg-data) att-refs)
att-refs (csvg/find-attr-references attrs)
references (csvg/find-def-references (:defs svg-data) att-refs)
href-id (-> (or (:href attrs) (:xlink:href attrs) "") (subs 1))
defs (:defs svg-data)
@ -401,7 +401,7 @@
element-data (-> element-data
(assoc :tag :g)
(update :attrs dissoc :x :y :width :height :href :xlink:href :transform)
(update :attrs usvg/add-transform disp-matrix)
(update :attrs csvg/add-transform disp-matrix)
(assoc :content [use-data]))]
(parse-svg-element frame-id svg-data element-data unames))
@ -413,9 +413,9 @@
(: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 usvg/polyline->path))
:polygon (create-path-shape name frame-id svg-data (-> element-data usvg/polygon->path))
:line (create-path-shape name frame-id svg-data (-> element-data usvg/line->path))
: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)
@ -429,8 +429,8 @@
hidden (assoc :hidden true))
(cond->> (:content element-data)
(contains? usvg/parent-tags tag)
(mapv #(usvg/inherit-attributes attrs %)))]))))))
(contains? csvg/parent-tags tag)
(mapv #(csvg/inherit-attributes attrs %)))]))))))
(defn create-svg-children
[objects selected frame-id parent-id svg-data [unames children] [_index svg-element]]
@ -473,7 +473,7 @@
"Extract all bitmap images inside the svg data, and upload them, associated to the file.
Return a map {<url> <image-data>}."
[svg-data file-id]
(->> (rx/from (usvg/collect-images svg-data))
(->> (rx/from (csvg/collect-images svg-data))
(rx/map (fn [uri]
(merge
{:file-id file-id
@ -520,9 +520,9 @@
[def-nodes svg-data]
(-> svg-data
(usvg/fix-default-values)
(usvg/fix-percents)
(usvg/extract-defs))
(csvg/fix-default-values)
(csvg/fix-percents)
(csvg/extract-defs))
svg-data (assoc svg-data :defs def-nodes)
root-shape (create-svg-root frame-id parent-id svg-data)
@ -549,13 +549,13 @@
;; Create the root shape
root-attrs (-> (:attrs svg-data)
(usvg/format-styles))
(csvg/format-styles))
[_ children]
(reduce (partial create-svg-children objects selected frame-id root-id svg-data)
[unames []]
(d/enumerate (->> (:content svg-data)
(mapv #(usvg/inherit-attributes root-attrs %)))))]
(mapv #(csvg/inherit-attributes root-attrs %)))))]
[root-shape children]))

View file

@ -10,10 +10,10 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.shapes :as gsh]
[app.common.svg :as csvg]
[app.common.types.shape :refer [stroke-caps-line stroke-caps-marker]]
[app.common.types.shape.radius :as ctsr]
[app.util.object :as obj]
[app.util.svg :as usvg]
[cuerdas.core :as str]))
(defn- stroke-type->dasharray
@ -163,8 +163,8 @@
;; TODO: revisit, why we need to execute it each render? Can
;; we do this operation on importation and avoid unnecesary
;; work on render?
(usvg/clean-attrs)
(usvg/update-attr-ids
(csvg/clean-attrs)
(csvg/update-attr-ids
(fn [id]
(if (contains? defs id)
(str render-id "-" id)
@ -217,8 +217,8 @@
(= :group shape-type))
(empty? shape-fills))
(let [wstyle (get shape :wrapper-styles)
fill (obj/get wstyle "fill")
fill (d/nilv fill clr/black)]
fill (obj/get wstyle "fill")
fill (d/nilv fill clr/black)]
(obj/set! style "fill" fill))
(d/not-empty? shape-fills)

View file

@ -11,10 +11,10 @@
(:require
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
[app.common.svg :as csvg]
[app.main.ui.context :as muc]
[app.util.json :as json]
[app.util.object :as obj]
[app.util.svg :as usvg]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@ -25,7 +25,7 @@
(cond
(map? node)
[:> (d/name tag) (clj->js (usvg/clean-attrs attrs))
[:> (d/name tag) (clj->js (csvg/clean-attrs attrs))
(for [child content]
[:& render-xml {:xml child}])]

View file

@ -21,8 +21,7 @@
[app.util.object :as obj]
[rumext.v2 :as mf]))
;; FIXME: revisit this: breaks all memoization because of this new
;; property added to shapes
;; FIXME: revisit this:
(defn propagate-wrapper-styles-child
[child wrapper-props]
(let [child-props-childs

View file

@ -12,7 +12,7 @@
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.bounds :as gsb]
[app.util.svg :as usvg]
[app.common.svg :as csvg]
[rumext.v2 :as mf]))
(defn add-matrix [attrs transform-key transform-matrix]
@ -30,7 +30,7 @@
:else
(let [{:keys [tag attrs content]} node
transform-gradient? (and (contains? usvg/gradient-tags tag)
transform-gradient? (and (contains? csvg/gradient-tags tag)
(= "userSpaceOnUse" (get attrs :gradientUnits "objectBoundingBox")))
transform-pattern? (and (= :pattern tag)
@ -39,7 +39,7 @@
transform-clippath? (and (= :clipPath tag)
(= "userSpaceOnUse" (get attrs :clipPathUnits "userSpaceOnUse")))
transform-filter? (and (contains? usvg/filter-tags tag)
transform-filter? (and (contains? csvg/filter-tags tag)
(= "userSpaceOnUse" (get attrs :filterUnits "objectBoundingBox")))
transform-mask? (and (= :mask tag)
@ -47,8 +47,8 @@
attrs
(-> attrs
(usvg/update-attr-ids prefix-id)
(usvg/clean-attrs)
(csvg/update-attr-ids prefix-id)
(csvg/clean-attrs)
;; This clasname will be used to change the transform on the viewport
;; only necessary for groups because shapes have their own transform
(cond-> (and (or transform-gradient?
@ -92,7 +92,7 @@
(defn svg-def-bounds [svg-def shape transform]
(let [{:keys [tag]} svg-def]
(if (or (= tag :mask) (contains? usvg/filter-tags tag))
(if (or (= tag :mask) (contains? csvg/filter-tags tag))
(-> (grc/make-rect (d/parse-double (get-in svg-def [:attrs :x]))
(d/parse-double (get-in svg-def [:attrs :y]))
(d/parse-double (get-in svg-def [:attrs :width]))
@ -107,7 +107,7 @@
(mf/deps shape)
#(if (= :svg-raw (:type shape))
(gmt/matrix)
(usvg/svg-transform-matrix shape)))
(csvg/svg-transform-matrix shape)))
;; Paths doesn't have transform so we have to transform its gradients
transform (if (some? (:svg-transform shape))

View file

@ -8,10 +8,10 @@
(:require
[app.common.data.macros :as dm]
[app.common.geom.shapes :as gsh]
[app.common.svg :as csvg]
[app.main.ui.context :as muc]
[app.main.ui.shapes.attrs :as usa]
[app.util.object :as obj]
[app.util.svg :as usvg]
[rumext.v2 :as mf]))
;; Graphic tags
@ -27,7 +27,7 @@
attrs (or attrs {})
attrs (cond-> attrs
(string? (:style attrs)) usvg/clean-attrs)
(string? (:style attrs)) csvg/clean-attrs)
style (obj/merge! (clj->js (:style attrs {}))
(obj/get custom-attrs "style"))]
(-> (clj->js attrs)
@ -35,7 +35,7 @@
(obj/set! "style" style))))
(defn translate-shape [attrs shape]
(let [transform (dm/str (usvg/svg-transform-matrix shape)
(let [transform (dm/str (csvg/svg-transform-matrix shape)
" "
(:transform attrs ""))]
(cond-> attrs
@ -51,7 +51,7 @@
{:keys [x y width height]} shape
{:keys [attrs] :as content} (:content shape)
ids-mapping (mf/use-memo #(usvg/generate-id-mapping content))
ids-mapping (mf/use-memo #(csvg/generate-id-mapping content))
render-id (mf/use-ctx muc/render-id)
attrs (-> (set-styles attrs shape render-id)
@ -77,7 +77,7 @@
ids-mapping (mf/use-ctx svg-ids-ctx)
render-id (mf/use-ctx muc/render-id)
attrs (mf/use-memo #(usvg/replace-attrs-ids attrs ids-mapping))
attrs (mf/use-memo #(csvg/replace-attrs-ids attrs ids-mapping))
attrs (translate-shape attrs shape)
element-id (get-in content [:attrs :id])
@ -100,7 +100,7 @@
svg-root? (and (map? content) (= tag :svg))
svg-tag? (map? content)
svg-leaf? (string? content)
valid-tag? (contains? usvg/svg-tags-list tag)]
valid-tag? (contains? csvg/svg-tags-list tag)]
(cond
svg-root?

View file

@ -6,10 +6,10 @@
(ns app.main.ui.workspace.shapes.svg-raw
(:require
[app.common.svg :as csvg]
[app.main.refs :as refs]
[app.main.ui.shapes.shape :refer [shape-container]]
[app.main.ui.shapes.svg-raw :as svg-raw]
[app.util.svg :as usvg]
[rumext.v2 :as mf]))
(defn svg-raw-wrapper-factory
@ -23,7 +23,7 @@
childs-ref (mf/use-memo (mf/deps (:id shape)) #(refs/children-objects (:id shape)))
childs (mf/deref childs-ref)
svg-tag (get-in shape [:content :tag])]
(if (contains? usvg/svg-group-safe-tags svg-tag)
(if (contains? csvg/svg-group-safe-tags svg-tag)
[:> shape-container {:shape shape}
[:& svg-raw-shape {:shape shape
:childs childs}]]

View file

@ -10,10 +10,9 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes.path :as upg]
[app.common.path.commands :as upc]
[app.common.svg :as csvg]
[app.util.path.arc-to-curve :refer [a2c]]
[app.util.svg :as usvg]
[cuerdas.core :as str]))
;;
(def commands-regex #"(?i)[mzlhvcsqta][^mzlhvcsqta]*")
@ -36,7 +35,7 @@
match (re-find regex remain)]
(if match
(let [value (-> match first usvg/fix-dot-number d/read-string)
(let [value (-> match first csvg/fix-dot-number d/read-string)
remain (str/replace-first remain regex "")
current (assoc current param value)
extract-idx (inc extract-idx)

View file

@ -42,9 +42,3 @@
(let [st (str/trim (str/lower search-term))
nm (str/trim (str/lower name))]
(str/includes? nm st))))
(defn camelize
[str]
;; str.replace(":", "-").replace(/-./g, x=>x[1].toUpperCase())
(when (not (nil? str))
(js* "~{}.replace(\":\", \"-\").replace(/-./g, x=>x[1].toUpperCase())", str)))

View file

@ -1,979 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.util.svg
(:require
[app.common.data :as d]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.uuid :as uuid]
[app.util.strings :as ustr]
[cuerdas.core :as str]))
;; Regex for XML ids per Spec
;; https://www.w3.org/TR/2008/REC-xml-20081126/#sec-common-syn
(defonce xml-id-regex #"#([:A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u10000-\uEFFFF][\.\-\:0-9\xB7A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0300-\u036F\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u10000-\uEFFFF]*)")
(defonce matrices-regex #"(matrix|translate|scale|rotate|skewX|skewY)\(([^\)]*)\)")
(defonce number-regex #"[+-]?\d*(\.\d+)?(e[+-]?\d+)?")
(defonce tags-to-remove #{:linearGradient :radialGradient :metadata :mask :clipPath :filter :title})
;; https://www.w3.org/TR/SVG11/eltindex.html
(defonce svg-tags-list
#{:a
:altGlyph
:altGlyphDef
:altGlyphItem
:animate
:animateColor
:animateMotion
:animateTransform
:circle
:clipPath
:color-profile
:cursor
:defs
:desc
:ellipse
:feBlend
:feColorMatrix
:feComponentTransfer
:feComposite
:feConvolveMatrix
:feDiffuseLighting
:feDisplacementMap
:feDistantLight
:feFlood
:feFuncA
:feFuncB
:feFuncG
:feFuncR
:feGaussianBlur
:feImage
:feMerge
:feMergeNode
:feMorphology
:feOffset
:fePointLight
:feSpecularLighting
:feSpotLight
:feTile
:feTurbulence
:filter
:font
:font-face
:font-face-format
:font-face-name
:font-face-src
:font-face-uri
:foreignObject
:g
:glyph
:glyphRef
:hkern
:image
:line
:linearGradient
:marker
:mask
:metadata
:missing-glyph
:mpath
:path
:pattern
:polygon
:polyline
:radialGradient
:rect
:set
:stop
:style
:svg
:switch
:symbol
:text
:textPath
:title
:tref
:tspan
:use
:view
:vkern
})
;; https://www.w3.org/TR/SVG11/attindex.html
(defonce svg-attr-list
#{:accent-height
:accumulate
:additive
:alphabetic
:amplitude
:arabic-form
:ascent
:attributeName
:attributeType
:azimuth
:baseFrequency
:baseProfile
:bbox
:begin
:bias
:by
:calcMode
:cap-height
:class
:clipPathUnits
:contentScriptType
:contentStyleType
:cx
:cy
:d
:descent
:diffuseConstant
:divisor
:dur
:dx
:dy
:edgeMode
:elevation
:end
:exponent
:externalResourcesRequired
:fill
:filterRes
:filterUnits
:font-family
:font-size
:font-stretch
:font-style
:font-variant
:font-weight
:format
:from
:fx
:fy
:g1
:g2
:glyph-name
:glyphRef
:gradientTransform
:gradientUnits
:hanging
:height
:horiz-adv-x
:horiz-origin-x
:horiz-origin-y
:id
:ideographic
:in
:in2
:intercept
:k
:k1
:k2
:k3
:k4
:kernelMatrix
:kernelUnitLength
:keyPoints
:keySplines
:keyTimes
:lang
:lengthAdjust
:limitingConeAngle
:local
:markerHeight
:markerUnits
:markerWidth
:maskContentUnits
:maskUnits
:mathematical
:max
:media
:method
:min
:mode
:name
:numOctaves
:offset
;; We don't support events
;;:onabort
;;:onactivate
;;:onbegin
;;:onclick
;;:onend
;;:onerror
;;:onfocusin
;;:onfocusout
;;:onload
;;:onmousedown
;;:onmousemove
;;:onmouseout
;;:onmouseover
;;:onmouseup
;;:onrepeat
;;:onresize
;;:onscroll
;;:onunload
;;:onzoom
:operator
:order
:orient
:orientation
:origin
:overline-position
:overline-thickness
:panose-1
:path
:pathLength
:patternContentUnits
:patternTransform
:patternUnits
:points
:pointsAtX
:pointsAtY
:pointsAtZ
:preserveAlpha
:preserveAspectRatio
:primitiveUnits
:r
:radius
:refX
:refY
:rendering-intent
:repeatCount
:repeatDur
:requiredExtensions
:requiredFeatures
:restart
:result
:rotate
:rx
:ry
:scale
:seed
:slope
:spacing
:specularConstant
:specularExponent
:spreadMethod
:startOffset
:stdDeviation
:stemh
:stemv
:stitchTiles
:strikethrough-position
:strikethrough-thickness
:string
:style
:surfaceScale
:systemLanguage
:tableValues
:target
:targetX
:targetY
:textLength
:title
:to
:transform
:type
:u1
:u2
:underline-position
:underline-thickness
:unicode
:unicode-range
:units-per-em
:v-alphabetic
:v-hanging
:v-ideographic
:v-mathematical
:values
:version
:vert-adv-y
:vert-origin-x
:vert-origin-y
:viewBox
:viewTarget
:width
:widths
:x
:x-height
:x1
:x2
:xChannelSelector
:xmlns:xlink
:xlink:actuate
:xlink:arcrole
:xlink:href
:xlink:role
:xlink:show
:xlink:title
:xlink:type
:xml:base
:xml:lang
:xml:space
:y
:y1
:y2
:yChannelSelector
:z
:zoomAndPan})
(defonce svg-present-list
#{:alignment-baseline
:baseline-shift
:clip-path
:clip-rule
:clip
:color-interpolation-filters
:color-interpolation
:color-profile
:color-rendering
:color
:cursor
:direction
:display
:dominant-baseline
:enable-background
:fill-opacity
:fill-rule
:fill
:filter
:flood-color
:flood-opacity
:font-family
:font-size-adjust
:font-size
:font-stretch
:font-style
:font-variant
:font-weight
:glyph-orientation-horizontal
:glyph-orientation-vertical
:image-rendering
:kerning
:letter-spacing
:lighting-color
:marker-end
:marker-mid
:marker-start
:mask
:opacity
:overflow
:pointer-events
:shape-rendering
:stop-color
:stop-opacity
:stroke-dasharray
:stroke-dashoffset
:stroke-linecap
:stroke-linejoin
:stroke-miterlimit
:stroke-opacity
:stroke-width
:stroke
:text-anchor
:text-decoration
:text-rendering
:unicode-bidi
:visibility
:word-spacing
:writing-mode
:mask-type})
(defonce inheritable-props
[:style
:clip-rule
:color
:color-interpolation
:color-interpolation-filters
:color-profile
:color-rendering
:cursor
:direction
:dominant-baseline
:fill
:fill-opacity
:fill-rule
:font
:font-family
:font-size
:font-size-adjust
:font-stretch
:font-style
:font-variant
:font-weight
:glyph-orientation-horizontal
:glyph-orientation-vertical
:image-rendering
:letter-spacing
:marker
:marker-end
:marker-mid
:marker-start
:paint-order
:pointer-events
:shape-rendering
:stroke
:stroke-dasharray
:stroke-dashoffset
:stroke-linecap
:stroke-linejoin
:stroke-miterlimit
:stroke-opacity
:stroke-width
:text-anchor
:text-rendering
:transform
:visibility
:word-spacing
:writing-mode])
(defonce gradient-tags
#{:linearGradient
:radialGradient})
(defonce filter-tags
#{:filter
:feBlend
:feColorMatrix
:feComponentTransfer
:feComposite
:feConvolveMatrix
:feDiffuseLighting
:feDisplacementMap
:feFlood
:feGaussianBlur
:feImage
:feMerge
:feMorphology
:feOffset
:feSpecularLighting
:feTile
:feTurbulence})
(def parent-tags
#{:g
:svg
:text
:tspan})
;; By spec: https://www.w3.org/TR/SVG11/single-page.html#struct-GElement
(defonce svg-group-safe-tags
#{:animate
:animateColor
:animateMotion
:animateTransform
:set
:desc
:metadata
:title
:circle
:ellipse
:line
:path
:polygon
:polyline
:rect
:defs
:g
:svg
:symbol
:use
:linearGradient
:radialGradient
:a
:altGlyphDef
:clipPath
:color-profile
:cursor
:filter
:font
:font-face
:foreignObject
:image
:marker
:mask
:pattern
:style
:switch
:text
:view})
;; Props not supported by react we need to keep them lowercase
(defonce non-react-props
#{:mask-type})
;; Defaults for some tags per spec https://www.w3.org/TR/SVG11/single-page.html
;; they are basically the defaults that can be percents and we need to replace because
;; otherwise won't work as expected in the workspace
(defonce svg-tag-defaults
(let [filter-default {:units :filterUnits
:default "objectBoundingBox"
"objectBoundingBox" {}
"userSpaceOnUse" {:x "-10%" :y "-10%" :width "120%" :height "120%"}}
filter-values (->> filter-tags
(reduce #(merge %1 (hash-map %2 filter-default)) {}))]
(merge {:linearGradient {:units :gradientUnits
:default "objectBoundingBox"
"objectBoundingBox" {}
"userSpaceOnUse" {:x1 "0%" :y1 "0%" :x2 "100%" :y2 "0%"}}
:radialGradient {:units :gradientUnits
:default "objectBoundingBox"
"objectBoundingBox" {}
"userSpaceOnUse" {:cx "50%" :cy "50%" :r "50%"}}
:mask {:units :maskUnits
:default "userSpaceOnUse"
"objectBoundingBox" {}
"userSpaceOnUse" {:x "-10%" :y "-10%" :width "120%" :height "120%"}}}
filter-values)))
(defn extract-ids [val]
(when (some? val)
(->> (re-seq xml-id-regex val)
(mapv second))))
(defn fix-dot-number
"Fixes decimal numbers starting in dot but without leading 0"
[num-str]
(cond
(str/starts-with? num-str ".")
(str "0" num-str)
(str/starts-with? num-str "-.")
(str "-0" (subs num-str 1))
:else
num-str))
(defn format-styles
"Transforms attributes to their react equivalent"
[attrs]
(letfn [(format-styles [style-str]
(if (string? style-str)
(->> (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
(contains? attrs :style)
(update :style format-styles))))
(defn clean-attrs
"Transforms attributes to their react equivalent"
([attrs]
(clean-attrs attrs true))
([attrs whitelist?]
(letfn [(known-property? [[key _]]
(or (not whitelist?)
(contains? svg-attr-list key)
(contains? svg-present-list key)))
(transform-att [key]
(if (contains? non-react-props key)
key
(-> (d/name key)
(ustr/camelize)
(keyword))))
(format-styles [style-str]
(->> (str/split style-str ";")
(map str/trim)
(map #(str/split % ":"))
(group-by first)
(map (fn [[key val]]
(vector
(transform-att key)
(second (first val)))))
(into {})))
(clean-att [[att val]]
(let [att (keyword att)]
(cond
(= att :class) [:className val]
(and (= att :style) (string? val)) [att (format-styles val)]
(and (= att :style) (map? val)) [att (clean-attrs val false)]
:else [(transform-att att) val])))]
;; Removed this warning because slows a lot rendering with big svgs
#_(let [filtered-props (->> attrs (remove known-property?) (map first))]
(when (seq filtered-props)
(.warn js/console "Unknown properties: " (str/join ", " filtered-props ))))
(into {}
(comp (filter known-property?)
(map clean-att))
attrs))))
(defn update-attr-ids
"Replaces the ids inside a property"
[attrs replace-fn]
(letfn [(update-ids [key val]
(cond
(map? val)
(d/mapm update-ids val)
(= key :id)
(replace-fn val)
:else
(let [replace-id
(fn [result it]
(let [to-replace (replace-fn it)]
(str/replace result (str "#" it) (str "#" to-replace))))]
(reduce replace-id val (extract-ids val)))))]
(d/mapm update-ids attrs)))
(defn replace-attrs-ids
"Replaces the ids inside a property"
[attrs ids-mapping]
(if (and ids-mapping (seq ids-mapping))
(update-attr-ids attrs (fn [id] (get ids-mapping id id)))
;; Ids-mapping is null
attrs))
(defn generate-id-mapping [content]
(letfn [(visit-node [result node]
(let [element-id (get-in node [:attrs :id])
result (cond-> result
element-id (assoc element-id (str (uuid/next))))]
(reduce visit-node result (:content node))))]
(visit-node {} content)))
(defn extract-defs [{:keys [attrs] :as node}]
(if-not (map? node)
[{} node]
(let [remove-node? (fn [{:keys [tag]}] (and (some? tag)
(or (contains? tags-to-remove tag)
(not (contains? svg-tags-list tag)))))
rec-result (->> (:content node) (map extract-defs))
node (assoc node :content (->> rec-result (map second) (filterv (comp not remove-node?))))
current-node-defs (if (contains? attrs :id)
(hash-map (:id attrs) node)
(hash-map))
node-defs (->> rec-result (map first) (reduce merge current-node-defs))]
[ node-defs node ])))
(defn find-attr-references [attrs]
(->> attrs
(mapcat (fn [[_ attr-value]]
(if (string? attr-value)
(extract-ids attr-value)
(find-attr-references attr-value))))))
(defn find-node-references [node]
(let [current (->> (find-attr-references (:attrs node)) (into #{}))
children (->> (:content node) (map find-node-references) (flatten) (into #{}))]
(vec (into current children))))
(defn find-def-references [defs references]
(loop [result (into #{} references)
checked? #{}
to-check (first references)
pending (rest references)]
(cond
(nil? to-check)
result
(checked? to-check)
(recur result
checked?
(first pending)
(rest pending))
:else
(let [node (get defs to-check)
new-refs (find-node-references node)
pending (concat pending new-refs)]
(recur (into result new-refs)
(conj checked? to-check)
(first pending)
(rest pending))))))
(defn svg-transform-matrix [shape]
(if (:svg-viewbox shape)
(let [{svg-x :x
svg-y :y
svg-width :width
svg-height :height} (:svg-viewbox shape)
{:keys [x y width height]} (:selrect shape)
scale-x (/ width svg-width)
scale-y (/ height svg-height)]
(gmt/multiply
(gmt/matrix)
;; Paths doesn't have transform so we have to transform its gradients
(if (or (= :path (:type shape))
(= :group (:type shape)))
(gsh/transform-matrix shape)
(gmt/matrix))
(gmt/translate-matrix (gpt/point (- x (* scale-x svg-x)) (- y (* scale-y svg-y))))
(gmt/scale-matrix (gpt/point scale-x scale-y))))
;; :else
(gmt/matrix)))
;; Parse transform attributes to native matrix format so we can transform paths instead of
;; relying in SVG transformation. This is necessary to import SVG's and not to break path tooling
;;
;; Transforms spec:
;; https://www.w3.org/TR/SVG11/single-page.html#coords-TransformAttribute
(defn format-translate-params [params]
(assert (or (= (count params) 1) (= (count params) 2)))
(if (= (count params) 1)
[(gpt/point (nth params 0) 0)]
[(gpt/point (nth params 0) (nth params 1))]))
(defn format-scale-params [params]
(assert (or (= (count params) 1) (= (count params) 2)))
(if (= (count params) 1)
[(gpt/point (nth params 0))]
[(gpt/point (nth params 0) (nth params 1))]))
(defn format-rotate-params [params]
(assert (or (= (count params) 1) (= (count params) 3)) (str "??" (count params)))
(if (= (count params) 1)
[(nth params 0) (gpt/point 0 0)]
[(nth params 0) (gpt/point (nth params 1) (nth params 2))]))
(defn format-skew-x-params [params]
(assert (= (count params) 1))
[(nth params 0) 0])
(defn format-skew-y-params [params]
(assert (= (count params) 1))
[0 (nth params 0)])
(defn to-matrix [{:keys [type params]}]
(assert (#{"matrix" "translate" "scale" "rotate" "skewX" "skewY"} type))
(case type
"matrix" (apply gmt/matrix params)
"translate" (apply gmt/translate-matrix (format-translate-params params))
"scale" (apply gmt/scale-matrix (format-scale-params params))
"rotate" (apply gmt/rotate-matrix (format-rotate-params params))
"skewX" (apply gmt/skew-matrix (format-skew-x-params params))
"skewY" (apply gmt/skew-matrix (format-skew-y-params params))))
(defn parse-transform [transform-attr]
(if transform-attr
(let [process-matrix
(fn [[_ type params]]
(let [params (->> (re-seq number-regex params)
(filter #(-> % first seq))
(map (comp d/parse-double first)))]
{:type type :params params}))
matrices (->> (re-seq matrices-regex transform-attr)
(map process-matrix)
(map to-matrix))]
(reduce gmt/multiply (gmt/matrix) matrices))
(gmt/matrix)))
(defn format-move [[x y]] (str "M" x " " y))
(defn format-line [[x y]] (str "L" x " " y))
(defn points->path [points-str]
(let [points (->> points-str
(re-seq number-regex)
(filter (comp not empty? first))
(mapv (comp d/parse-double first))
(partition 2))
head (first points)
other (rest points)]
(str (format-move head)
(->> other (map format-line) (str/join " ")))))
(defn polyline->path [{:keys [attrs] :as node}]
(let [tag :path
attrs (-> attrs
(dissoc :points)
(assoc :d (points->path (:points attrs))))]
(assoc node :attrs attrs :tag tag)))
(defn polygon->path [{:keys [attrs] :as node}]
(let [tag :path
attrs (-> attrs
(dissoc :points)
(assoc :d (str (points->path (:points attrs)) "Z")))]
(assoc node :attrs attrs :tag tag)))
(defn line->path [{:keys [attrs] :as node}]
(let [tag :path
{:keys [x1 y1 x2 y2]} attrs
x1 (or x1 0)
y1 (or y1 0)
x2 (or x2 0)
y2 (or y2 0)
attrs (-> attrs
(dissoc :x1 :x2 :y1 :y2)
(assoc :d (str "M" x1 "," y1 " L" x2 "," y2)))]
(assoc node :attrs attrs :tag tag)))
(defn add-transform [attrs transform]
(letfn [(append-transform [old-transform]
(if (or (nil? old-transform) (empty? old-transform))
transform
(str transform " " old-transform)))]
(cond-> attrs
transform
(update :transform append-transform))))
(defn inherit-attributes [group-attrs {:keys [attrs] :as node}]
(if (map? node)
(let [attrs (-> (format-styles attrs)
(add-transform (:transform group-attrs)))
attrs (d/deep-merge (select-keys group-attrs inheritable-props) attrs)]
(assoc node :attrs attrs))
node))
(defn map-nodes [mapfn node]
(let [update-content
(fn [content] (cond->> content
(vector? content)
(mapv (partial map-nodes mapfn))))]
(cond-> node
(map? node)
(-> (mapfn)
(d/update-when :content update-content)))))
(defn reduce-nodes [redfn value node]
(let [reduce-content
(fn [value content]
(loop [current (first content)
content (rest content)
value value]
(if (nil? current)
value
(recur (first content)
(rest content)
(reduce-nodes redfn value current)))))]
(if (map? node)
(-> (redfn value node)
(reduce-content (:content node)))
value)))
(defn fix-default-values
"Gives values to some SVG elements which defaults won't work when imported into the platform"
[svg-data]
(let [add-defaults
(fn [{:keys [tag attrs] :as node}]
(let [prop (get-in svg-tag-defaults [tag :units])
default-units (get-in svg-tag-defaults [tag :default])
units (get attrs prop default-units)
tag-default (get-in svg-tag-defaults [tag units])]
(d/update-when node :attrs #(merge tag-default %))))
fix-node-defaults
(fn [node]
(cond-> node
(contains? svg-tag-defaults (:tag node))
(add-defaults)))]
(->> svg-data (map-nodes fix-node-defaults))))
(defn calculate-ratio
;; sqrt((actual-width)**2 + (actual-height)**2)/sqrt(2).
[width height]
(/ (mth/hypot width height)
(mth/sqrt 2)))
(defn fix-percents
"Changes percents to a value according to the size of the svg imported"
[svg-data]
;; https://www.w3.org/TR/SVG11/single-page.html#coords-Units
(let [viewbox {:x (:offset-x svg-data)
:y (:offset-y svg-data)
:width (:width svg-data)
:height (:height svg-data)
:ratio (calculate-ratio (:width svg-data) (:height svg-data))}]
(letfn [(fix-length [prop-length val]
(* (get viewbox prop-length) (/ val 100.)))
(fix-coord [prop-coord prop-length val]
(+ (get viewbox prop-coord)
(fix-length prop-length val)))
(fix-percent-attr-viewbox [attr-key attr-val]
(let [is-percent? (str/ends-with? attr-val "%")
is-x? #{:x :x1 :x2 :cx}
is-y? #{:y :y1 :y2 :cy}
is-width? #{:width}
is-height? #{:height}
is-other? #{:r :stroke-width}]
(if is-percent?
;; JS parseFloat removes the % symbol
(let [attr-num (d/parse-double attr-val)]
(str (cond
(is-x? attr-key) (fix-coord :x :width attr-num)
(is-y? attr-key) (fix-coord :y :height attr-num)
(is-width? attr-key) (fix-length :width attr-num)
(is-height? attr-key) (fix-length :height attr-num)
(is-other? attr-key) (fix-length :ratio attr-num)
:else attr-val)))
attr-val)))
(fix-percent-attrs-viewbox [attrs]
(d/mapm fix-percent-attr-viewbox attrs))
(fix-percent-attr-numeric [_ attr-val]
(let [is-percent? (str/ends-with? attr-val "%")]
(if is-percent?
(str (let [attr-num (d/parse-double attr-val)]
(/ attr-num 100)))
attr-val)))
(fix-percent-attrs-numeric [attrs]
(d/mapm fix-percent-attr-numeric attrs))
(fix-percent-values [node]
(let [units (or (get-in node [:attrs :filterUnits])
(get-in node [:attrs :gradientUnits])
(get-in node [:attrs :patternUnits])
(get-in node [:attrs :clipUnits]))]
(cond-> node
(= "objectBoundingBox" units)
(update :attrs fix-percent-attrs-numeric)
(not= "objectBoundingBox" units)
(update :attrs fix-percent-attrs-viewbox))))]
(->> svg-data (map-nodes fix-percent-values)))))
(defn collect-images [svg-data]
(let [redfn (fn [acc {:keys [tag attrs]}]
(cond-> acc
(= :image tag)
(conj (or (:href attrs) (:xlink:href attrs)))))]
(reduce-nodes redfn [] svg-data )))