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 ;; Copyright (c) KALEIDOS INC
(ns app.common.svg (ns app.common.svg
#?(:cljs (:require
(:require #?(:cljs ["./svg_optimizer.js" :as svgo])
["./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 #?(:cljs
(defn optimize (defn optimize

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -42,9 +42,3 @@
(let [st (str/trim (str/lower search-term)) (let [st (str/trim (str/lower search-term))
nm (str/trim (str/lower name))] nm (str/trim (str/lower name))]
(str/includes? nm st)))) (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 )))