diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index cb4c95563..0a2ad2205 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -164,27 +164,28 @@ (gsh/setup-selrect)))) (defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}] - (let [svg-transform (usvg/parse-transform (:transform attrs)) - path-content (ugp/path->content (:d attrs)) - content (cond-> path-content - svg-transform - (gsh/transform-content svg-transform)) + (when (and (contains? attrs :d) (not (empty? (:d attrs)) )) + (let [svg-transform (usvg/parse-transform (:transform attrs)) + path-content (ugp/path->content (:d attrs)) + content (cond-> path-content + svg-transform + (gsh/transform-content svg-transform)) - selrect (gsh/content->selrect content) - points (gsh/rect->points selrect) + selrect (gsh/content->selrect content) + points (gsh/rect->points selrect) - origin (gpt/negate (gpt/point svg-data))] - (-> {:id (uuid/next) - :type :path - :name name - :frame-id frame-id - :content content - :selrect selrect - :points points} - (assoc :svg-viewbox (select-keys selrect [:x :y :width :height])) - (assoc :svg-attrs (dissoc attrs :d :transform)) - (assoc :svg-transform svg-transform) - (gsh/translate-to-frame origin)))) + origin (gpt/negate (gpt/point svg-data))] + (-> {:id (uuid/next) + :type :path + :name name + :frame-id frame-id + :content content + :selrect selrect + :points points} + (assoc :svg-viewbox (select-keys selrect [:x :y :width :height])) + (assoc :svg-attrs (dissoc attrs :d :transform)) + (assoc :svg-transform svg-transform) + (gsh/translate-to-frame origin))))) (defn calculate-rect-metadata [rect-data transform] (let [points (-> (gsh/rect->points rect-data) @@ -333,7 +334,7 @@ ;; SVG graphic elements ;; :circle :ellipse :image :line :path :polygon :polyline :rect :text :use (let [shape (-> (case tag - (:g :a) (create-group name frame-id svg-data element-data) + (:g :a :svg) (create-group name frame-id svg-data element-data) :rect (create-rect-shape name frame-id svg-data element-data) (:circle :ellipse) (create-circle-shape name frame-id svg-data element-data) @@ -344,34 +345,42 @@ :image (create-image-shape name frame-id svg-data element-data) #_other (create-raw-svg name frame-id svg-data element-data)) - (assoc :svg-defs (select-keys (:defs svg-data) references)) - (setup-fill) - (setup-stroke)) + ) + shape (when (some? shape) + (-> shape + (assoc :svg-defs (select-keys (:defs svg-data) references)) + (setup-fill) + (setup-stroke))) + children (cond->> (:content element-data) (= tag :g) (mapv #(usvg/inherit-attributes attrs %)))] [shape children])))) (defn add-svg-child-changes [page-id objects selected frame-id parent-id svg-data [unames [rchs uchs]] [index data]] - (let [[shape children] (parse-svg-element frame-id svg-data data unames) - shape-id (:id shape) + (let [[shape children] (parse-svg-element frame-id svg-data data unames)] + (if (some? shape) + (let [shape-id (:id shape) - [rch1 uch1] (dwc/add-shape-changes page-id objects selected shape false) + [rch1 uch1] (dwc/add-shape-changes page-id objects selected shape false) - ;; Mov-objects won't have undo because we "delete" the object in the undo of the - ;; previous operation - rch2 [{:type :mov-objects - :parent-id parent-id - :frame-id frame-id - :page-id page-id - :index index - :shapes [shape-id]}] + ;; Mov-objects won't have undo because we "delete" the object in the undo of the + ;; previous operation + rch2 [{:type :mov-objects + :parent-id parent-id + :frame-id frame-id + :page-id page-id + :index index + :shapes [shape-id]}] - ;; Careful! the undo changes are concatenated reversed (we undo in reverse order - changes [(d/concat rchs rch1 rch2) (d/concat uch1 uchs)] - unames (conj unames (:name shape)) - reducer-fn (partial add-svg-child-changes page-id objects selected frame-id shape-id svg-data)] - (reduce reducer-fn [unames changes] (d/enumerate children)))) + ;; Careful! the undo changes are concatenated reversed (we undo in reverse order + changes [(d/concat rchs rch1 rch2) (d/concat uch1 uchs)] + unames (conj unames (:name shape)) + reducer-fn (partial add-svg-child-changes page-id objects selected frame-id shape-id svg-data)] + (reduce reducer-fn [unames changes] (d/enumerate children))) + + ;; Cannot create the data from curren tags + [unames [rchs uchs]]))) (declare create-svg-shapes) diff --git a/frontend/src/app/main/ui/shapes/group.cljs b/frontend/src/app/main/ui/shapes/group.cljs index c355b30d5..22a877b31 100644 --- a/frontend/src/app/main/ui/shapes/group.cljs +++ b/frontend/src/app/main/ui/shapes/group.cljs @@ -32,20 +32,24 @@ mask (when show-mask? (first childs)) childs (if show-mask? (rest childs) childs) - props (-> (attrs/extract-style-attrs shape) - (obj/merge! - #js {:pointerEvents pointer-events - :clipPath (when (and mask (not expand-mask)) (clip-str mask)) - :mask (when (and mask (not expand-mask)) (mask-str mask))}))] + mask-props (when (and mask (not expand-mask)) + #js {:clipPath (clip-str mask) + :mask (mask-str mask)}) + mask-wrapper (if (and mask (not expand-mask)) + "g" + mf/Fragment) - [:> :g props - (when mask - [:> render-mask #js {:frame frame :mask mask}]) + props (-> (attrs/extract-style-attrs shape))] - (for [item childs] - [:& shape-wrapper {:frame frame - :shape item - :key (:id item)}])])))) + [:> mask-wrapper mask-props + [:> :g (attrs/extract-style-attrs shape) + (when mask + [:> render-mask #js {:frame frame :mask mask}]) + + (for [item childs] + [:& shape-wrapper {:frame frame + :shape item + :key (:id item)}])]])))) diff --git a/frontend/src/app/main/ui/shapes/svg_defs.cljs b/frontend/src/app/main/ui/shapes/svg_defs.cljs index 846ec4321..ee5857551 100644 --- a/frontend/src/app/main/ui/shapes/svg_defs.cljs +++ b/frontend/src/app/main/ui/shapes/svg_defs.cljs @@ -60,15 +60,19 @@ transform-filter? (and (contains? usvg/filter-tags tag) (= "userSpaceOnUse" (get attrs :filterUnits "objectBoundingBox"))) + transform-mask? (and (= :mask tag) + (= "userSpaceOnUse" (get attrs :maskUnits "objectBoundingBox"))) + attrs (-> attrs (usvg/update-attr-ids prefix-id) (usvg/clean-attrs) (cond-> - transform-gradient? (add-matrix :gradientTransform transform) - transform-pattern? (add-matrix :patternTransform transform) - transform-clippath? (add-matrix :transform transform) - transform-filter? (transform-region transform))) + transform-gradient? (add-matrix :gradientTransform transform) + transform-pattern? (add-matrix :patternTransform transform) + transform-clippath? (add-matrix :transform transform) + (or transform-filter? + transform-mask?) (transform-region transform))) [wrapper wrapper-props] (if (= tag :mask) ["g" #js {:transform (str transform)}] diff --git a/frontend/src/app/util/svg.cljs b/frontend/src/app/util/svg.cljs index bc3979a8c..4547b3091 100644 --- a/frontend/src/app/util/svg.cljs +++ b/frontend/src/app/util/svg.cljs @@ -24,6 +24,472 @@ (defonce matrices-regex #"(matrix|translate|scale|rotate|skewX|skewY)\(([^\)]*)\)") (defonce number-regex #"[+-]?\d*(\.\d+)?(e[+-]?\d+)?") +(defonce tags-to-remove #{:defs :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 + :script + :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 + [: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}) + +;; 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 basicaly 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] (->> (re-seq xml-id-regex val) (mapv second))) @@ -61,35 +527,52 @@ (defn clean-attrs "Transforms attributes to their react equivalent" - [attrs] - (letfn [(transform-key [key] - (-> (d/name key) - (str/replace ":" "-") - (str/camel) - (keyword))) + ([attrs] (clean-attrs attrs true)) + ([attrs whitelist?] + (letfn [(known-property? [[key _]] + (or (not whitelist?) + (contains? svg-attr-list key ) + (contains? svg-present-list key ))) - (format-styles [style-str] - (->> (str/split style-str ";") - (map str/trim) - (map #(str/split % ":")) - (group-by first) - (map (fn [[key val]] - (vector - (transform-key key) - (second (first val))))) - (into {}))) + (transform-key [key] + (if (contains? non-react-props key) + key + (-> (d/name key) + (str/replace ":" "-") + (str/camel) + (keyword)))) - (map-fn [[key val]] - (let [key (keyword key)] - (cond - (= key :class) [:className val] - (and (= key :style) (string? val)) [key (format-styles val)] - (and (= key :style) (map? val)) [key (clean-attrs val)] - :else (vector (transform-key key) val))))] + (lowercase-key [key] + (-> (d/name key) + (str/lower) + (keyword))) - (->> attrs - (map map-fn) - (into {})))) + (format-styles [style-str] + (->> (str/split style-str ";") + (map str/trim) + (map #(str/split % ":")) + (group-by first) + (map (fn [[key val]] + (vector + (transform-key key) + (second (first val))))) + (into {}))) + + (map-fn [[key val]] + (let [key (keyword key)] + (cond + (= key :class) [:className val] + (and (= key :style) (string? val)) [key (format-styles val)] + (and (= key :style) (map? val)) [key (clean-attrs val false)] + :else (vector (transform-key key) val)))) + + ] + + (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 map-fn)) attrs)))) (defn update-attr-ids "Replaces the ids inside a property" @@ -126,18 +609,16 @@ (reduce visit-node result (:content node))))] (visit-node {} content))) -(def remove-tags #{:defs :linearGradient}) - (defn extract-defs [{:keys [tag attrs content] :as node}] (if-not (map? node) [{} node] - (let [remove-node? (fn [{:keys [tag]}] (contains? remove-tags tag)) - + (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)) @@ -319,76 +800,6 @@ transform (update :transform append-transform)))) -(defonce inheritable-props - [: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}) - (defn inherit-attributes [group-attrs {:keys [attrs] :as node}] (if (map? node) (let [attrs (-> (format-styles attrs) @@ -425,31 +836,6 @@ (reduce-content (:content node))) value))) -;; Defaults for some tags per spec https://www.w3.org/TR/SVG11/single-page.html -;; they are basicaly 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 fix-default-values "Gives values to some SVG elements which defaults won't work when imported into the platform" [svg-data]