diff --git a/common/src/app/common/svg.cljc b/common/src/app/common/svg.cljc index e9524182c..3e6559cbc 100644 --- a/common/src/app/common/svg.cljc +++ b/common/src/app/common/svg.cljc @@ -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 diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index 2b404d95a..c3ad55e5b 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -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 { }." [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])) diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index c44d6a915..5acde896b 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -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) diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs index 7554de8b0..9aa49313a 100644 --- a/frontend/src/app/main/ui/shapes/export.cljs +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -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}])] diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index f369b1afb..4d017e22c 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -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 diff --git a/frontend/src/app/main/ui/shapes/svg_defs.cljs b/frontend/src/app/main/ui/shapes/svg_defs.cljs index 69ddbf400..274451af9 100644 --- a/frontend/src/app/main/ui/shapes/svg_defs.cljs +++ b/frontend/src/app/main/ui/shapes/svg_defs.cljs @@ -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)) diff --git a/frontend/src/app/main/ui/shapes/svg_raw.cljs b/frontend/src/app/main/ui/shapes/svg_raw.cljs index bb13fc109..6903bc732 100644 --- a/frontend/src/app/main/ui/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/shapes/svg_raw.cljs @@ -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? diff --git a/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs b/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs index 27b9aed91..774fa61fa 100644 --- a/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs @@ -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}]] diff --git a/frontend/src/app/util/path/parser.cljs b/frontend/src/app/util/path/parser.cljs index 921c02bb1..bc2ab6714 100644 --- a/frontend/src/app/util/path/parser.cljs +++ b/frontend/src/app/util/path/parser.cljs @@ -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) diff --git a/frontend/src/app/util/strings.cljs b/frontend/src/app/util/strings.cljs index 3f74c9e6c..edbe86354 100644 --- a/frontend/src/app/util/strings.cljs +++ b/frontend/src/app/util/strings.cljs @@ -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))) diff --git a/frontend/src/app/util/svg.cljs b/frontend/src/app/util/svg.cljs deleted file mode 100644 index e724136a5..000000000 --- a/frontend/src/app/util/svg.cljs +++ /dev/null @@ -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 )))