mirror of
https://github.com/penpot/penpot.git
synced 2025-02-12 18:18:24 -05:00
🐛 Fix problem when exporting texts with gradients or opacity
This commit is contained in:
parent
bce0e9194c
commit
214c64c49e
7 changed files with 271 additions and 62 deletions
|
@ -13,6 +13,7 @@
|
|||
- Fix problem with exporting before the document is saved [Taiga #2189](https://tree.taiga.io/project/penpot/issue/2189)
|
||||
- Fix undo stacking when changing color from color-picker [Taiga #2191](https://tree.taiga.io/project/penpot/issue/2191)
|
||||
- Fix pages dropdown in viewer [Taiga #2087](https://tree.taiga.io/project/penpot/issue/2087)
|
||||
- Fix problem when exporting texts with gradients or opacity [Taiga #2200](https://tree.taiga.io/project/penpot/issue/2200)
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
[app.renderer.bitmap :refer [create-cookie]]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(log/set-level "app.http.export-svg" :trace)
|
||||
(log/set-level "app.renderer.svg" :trace)
|
||||
|
||||
(defn- xml->clj
|
||||
[data]
|
||||
|
@ -146,15 +146,61 @@
|
|||
{:color color
|
||||
:svgdata data}))))))
|
||||
|
||||
(join-color-layers [{:keys [x y width height] :as node} layers]
|
||||
(log/trace :fn :join-color-layers)
|
||||
(set-path-color [id color mapping node]
|
||||
(let [color-mapping (get mapping color)]
|
||||
(cond
|
||||
(and (some? color-mapping)
|
||||
(= "transparent" (get color-mapping "type")))
|
||||
(update node "attributes" assoc
|
||||
"fill" (get color-mapping "hex")
|
||||
"fill-opacity" (get color-mapping "opacity"))
|
||||
|
||||
(and (some? color-mapping)
|
||||
(= "gradient" (get color-mapping "type")))
|
||||
(update node "attributes" assoc
|
||||
"fill" (str "url(#gradient-" id "-" (subs color 1) ")"))
|
||||
|
||||
:else
|
||||
(update node "attributes" assoc "fill" color))))
|
||||
|
||||
(get-stops [data]
|
||||
(->> (get-in data ["gradient" "stops"])
|
||||
(mapv (fn [stop-data]
|
||||
{"type" "element"
|
||||
"name" "stop"
|
||||
"attributes" {"offset" (get stop-data "offset")
|
||||
"stop-color" (get stop-data "color")
|
||||
"stop-opacity" (get stop-data "opacity")}}))))
|
||||
|
||||
(data->gradient-def [id [color data]]
|
||||
(let [id (str "gradient-" id "-" (subs color 1))]
|
||||
(if (= type "linear")
|
||||
{"type" "element"
|
||||
"name" "linearGradient"
|
||||
"attributes" {"id" id "x1" "0.5" "y1" "1" "x2" "0.5" "y2" "0"}
|
||||
"elements" (get-stops data)}
|
||||
|
||||
{"type" "element"
|
||||
"name" "radialGradient"
|
||||
"attributes" {"id" id "cx" "0.5" "cy" "0.5" "r" "0.5"}
|
||||
"elements" (get-stops data)}
|
||||
)))
|
||||
|
||||
(get-gradients [id mapping]
|
||||
(->> mapping
|
||||
(filter (fn [[color data]]
|
||||
(= (get data "type") "gradient")))
|
||||
(mapv (partial data->gradient-def id))))
|
||||
|
||||
(join-color-layers [{:keys [id x y width height mapping] :as node} layers]
|
||||
(log/trace :fn :join-color-layers :mapping mapping)
|
||||
(loop [result (-> (:svgdata (first layers))
|
||||
(assoc "elements" []))
|
||||
layers (seq layers)]
|
||||
(if-let [{:keys [color svgdata]} (first layers)]
|
||||
(recur (->> (get svgdata "elements")
|
||||
(filter #(= (get % "name") "g"))
|
||||
(map #(update % "attributes" assoc "fill" color))
|
||||
(map (partial set-path-color id color mapping))
|
||||
(update result "elements" d/concat))
|
||||
(rest layers))
|
||||
|
||||
|
@ -166,11 +212,12 @@
|
|||
(parse-viewbox))
|
||||
transform (str/fmt "translate(%s, %s) scale(%s, %s)" x y
|
||||
(/ width (:width vbox))
|
||||
(/ height (:height vbox)))]
|
||||
(-> result
|
||||
(assoc "name" "g")
|
||||
(assoc "attributes" {})
|
||||
(update "elements" (fn [elements]
|
||||
(/ height (:height vbox)))
|
||||
|
||||
gradient-defs (get-gradients id mapping)
|
||||
|
||||
elements
|
||||
(->> (get result "elements")
|
||||
(mapv (fn [group]
|
||||
(let [paths (get group "elements")]
|
||||
(if (= 1 (count paths))
|
||||
|
@ -180,8 +227,18 @@
|
|||
(-> attrs
|
||||
(d/merge (get group "attributes"))
|
||||
(update "transform" #(str transform " " %))))))
|
||||
(update-in group ["attributes" "transform"] #(str transform " " %)))))
|
||||
elements))))))))
|
||||
(update-in group ["attributes" "transform"] #(str transform " " %)))))))
|
||||
|
||||
|
||||
elements (cond->> elements
|
||||
(not (empty? gradient-defs))
|
||||
(d/concat [{"type" "element" "name" "defs" "attributes" {}
|
||||
"elements" gradient-defs}]))]
|
||||
|
||||
(-> result
|
||||
(assoc "name" "g")
|
||||
(assoc "attributes" {})
|
||||
(assoc "elements" elements))))))
|
||||
|
||||
(convert-to-svg [ppmpath {:keys [colors] :as node}]
|
||||
(log/trace :fn :convert-to-svg :ppmpath ppmpath :colors colors)
|
||||
|
@ -202,13 +259,15 @@
|
|||
|
||||
(extract-element-attrs [^js element]
|
||||
(let [^js attrs (.. element -attributes)
|
||||
^js colors (.. element -dataset -colors)]
|
||||
^js colors (.. element -dataset -colors)
|
||||
^js mapping (.. element -dataset -mapping)]
|
||||
#js {:id (.. attrs -id -value)
|
||||
:x (.. attrs -x -value)
|
||||
:y (.. attrs -y -value)
|
||||
:width (.. attrs -width -value)
|
||||
:height (.. attrs -height -value)
|
||||
:colors (.split colors ",")}))
|
||||
:colors (.split colors ",")
|
||||
:mapping (js/JSON.parse mapping)}))
|
||||
|
||||
(extract-single-node [[shot node]]
|
||||
(log/trace :fn :extract-single-node)
|
||||
|
@ -220,6 +279,7 @@
|
|||
:width (unchecked-get attrs "width")
|
||||
:height (unchecked-get attrs "height")
|
||||
:colors (vec (unchecked-get attrs "colors"))
|
||||
:mapping (js->clj (unchecked-get attrs "mapping"))
|
||||
:data shot}))
|
||||
|
||||
(resolve-text-node [page node]
|
||||
|
@ -313,3 +373,4 @@
|
|||
".svg"))
|
||||
:length (alength content)
|
||||
:mime-type "image/svg+xml"}))
|
||||
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
(def render-ctx (mf/create-context nil))
|
||||
(def def-ctx (mf/create-context false))
|
||||
|
||||
;; This content is used to replace complex colors to simple ones
|
||||
;; for text shapes in the export process
|
||||
(def text-plain-colors-ctx (mf/create-context false))
|
||||
|
||||
(def current-route (mf/create-context nil))
|
||||
(def current-team-id (mf/create-context nil))
|
||||
(def current-project-id (mf/create-context nil))
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
[app.main.exports :as exports]
|
||||
[app.main.repo :as repo]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.shapes.embed :as embed]
|
||||
[app.main.ui.shapes.export :as ed]
|
||||
[app.main.ui.shapes.filters :as filters]
|
||||
|
@ -124,6 +125,7 @@
|
|||
;; Auxiliary SVG for rendering text-shapes
|
||||
(when render-texts?
|
||||
(for [object text-shapes]
|
||||
[:& (mf/provider muc/text-plain-colors-ctx) {:value true}
|
||||
[:svg {:id (str "screenshot-text-" (:id object))
|
||||
:view-box (str "0 0 " (:width object) " " (:height object))
|
||||
:width (:width object)
|
||||
|
@ -131,7 +133,7 @@
|
|||
:version "1.1"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"}
|
||||
[:& shape-wrapper {:shape (-> object (assoc :x 0 :y 0))}]]))]))
|
||||
[:& shape-wrapper {:shape (-> object (assoc :x 0 :y 0))}]]]))]))
|
||||
|
||||
(defn- adapt-root-frame
|
||||
[objects object-id]
|
||||
|
|
|
@ -10,24 +10,10 @@
|
|||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.shapes.export :as ed]
|
||||
[app.util.object :as obj]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc linear-gradient [{:keys [id gradient shape]}]
|
||||
(let [transform (when (= :path (:type shape)) (gsh/transform-matrix shape nil (gpt/point 0.5 0.5)))]
|
||||
[:> :linearGradient #js {:id id
|
||||
:x1 (:start-x gradient)
|
||||
:y1 (:start-y gradient)
|
||||
:x2 (:end-x gradient)
|
||||
:y2 (:end-y gradient)
|
||||
:gradientTransform transform
|
||||
:penpot:gradient "true"}
|
||||
(for [{:keys [offset color opacity]} (:stops gradient)]
|
||||
[:stop {:key (str id "-stop-" offset)
|
||||
:offset (or offset 0)
|
||||
:stop-color color
|
||||
:stop-opacity opacity}])]))
|
||||
|
||||
(defn add-metadata [props gradient]
|
||||
(-> props
|
||||
(obj/set! "penpot:gradient" "true")
|
||||
|
@ -38,6 +24,30 @@
|
|||
(obj/set! "penpot:end-y" (:end-y gradient))
|
||||
(obj/set! "penpot:width" (:width gradient))))
|
||||
|
||||
(mf/defc linear-gradient [{:keys [id gradient shape]}]
|
||||
(let [transform (when (= :path (:type shape)) (gsh/transform-matrix shape nil (gpt/point 0.5 0.5)))
|
||||
base-props #js {:id id
|
||||
:x1 (:start-x gradient)
|
||||
:y1 (:start-y gradient)
|
||||
:x2 (:end-x gradient)
|
||||
:y2 (:end-y gradient)
|
||||
:gradientTransform transform}
|
||||
|
||||
include-metadata? (mf/use-ctx ed/include-metadata-ctx)
|
||||
|
||||
props (cond-> base-props
|
||||
include-metadata?
|
||||
(add-metadata gradient))]
|
||||
|
||||
[:> :linearGradient props
|
||||
(for [{:keys [offset color opacity]} (:stops gradient)]
|
||||
[:stop {:key (str id "-stop-" offset)
|
||||
:offset (or offset 0)
|
||||
:stop-color color
|
||||
:stop-opacity opacity}])]))
|
||||
|
||||
|
||||
|
||||
(mf/defc radial-gradient [{:keys [id gradient shape]}]
|
||||
(let [{:keys [x y width height]} (:selrect shape)
|
||||
transform (if (= :path (:type shape))
|
||||
|
@ -73,7 +83,11 @@
|
|||
:gradientUnits "userSpaceOnUse"
|
||||
:gradientTransform transform}
|
||||
|
||||
props (-> base-props (add-metadata gradient))]
|
||||
include-metadata? (mf/use-ctx ed/include-metadata-ctx)
|
||||
|
||||
props (cond-> base-props
|
||||
include-metadata?
|
||||
(add-metadata gradient))]
|
||||
[:> :radialGradient props
|
||||
(for [{:keys [offset color opacity]} (:stops gradient)]
|
||||
[:stop {:key (str id "-stop-" offset)
|
||||
|
|
|
@ -8,9 +8,12 @@
|
|||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.shapes.attrs :as attrs]
|
||||
[app.main.ui.shapes.text.styles :as sts]
|
||||
[app.util.color :as uc]
|
||||
[app.util.object :as obj]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc render-text
|
||||
|
@ -73,12 +76,111 @@
|
|||
(obj/set! "key" index))]
|
||||
[:> render-node props]))])))))
|
||||
|
||||
(defn- next-color
|
||||
"Given a set of colors try to get a color not yet used"
|
||||
[colors]
|
||||
(assert (set? colors))
|
||||
(loop [current-rgb [0 0 0]]
|
||||
(let [current-hex (uc/rgb->hex current-rgb)]
|
||||
(if (contains? colors current-hex)
|
||||
(recur (uc/next-rgb current-rgb))
|
||||
current-hex))))
|
||||
|
||||
(defn- remap-colors
|
||||
"Returns a new content replacing the original colors by their mapped 'simple color'"
|
||||
[content color-mapping]
|
||||
|
||||
(cond-> content
|
||||
(and (:fill-opacity content) (< (:fill-opacity content) 1.0))
|
||||
(-> (assoc :fill-color (get color-mapping [(:fill-color content) (:fill-opacity content)]))
|
||||
(assoc :fill-opacity 1.0))
|
||||
|
||||
(some? (:fill-color-gradient content))
|
||||
(-> (assoc :fill-color (get color-mapping (:fill-color-gradient content)))
|
||||
(assoc :fill-opacity 1.0)
|
||||
(dissoc :fill-color-gradient))
|
||||
|
||||
(contains? content :children)
|
||||
(update :children #(mapv (fn [node] (remap-colors node color-mapping)) %))))
|
||||
|
||||
(defn- fill->color
|
||||
"Given a content node returns the information about that node fill color"
|
||||
[{:keys [fill-color fill-opacity fill-color-gradient]}]
|
||||
|
||||
(cond
|
||||
(some? fill-color-gradient)
|
||||
{:type :gradient
|
||||
:gradient fill-color-gradient}
|
||||
|
||||
(and (string? fill-color) (some? fill-opacity) (not= fill-opacity 1))
|
||||
{:type :transparent
|
||||
:hex fill-color
|
||||
:opacity fill-opacity}
|
||||
|
||||
(string? fill-color)
|
||||
{:type :solid
|
||||
:hex fill-color
|
||||
:map-to fill-color}))
|
||||
|
||||
(defn- retrieve-colors
|
||||
"Given a text shape returns a triple with the values:
|
||||
- colors used as fills
|
||||
- a mapping from simple solid colors to complex ones (transparents/gradients)
|
||||
- the inverse of the previous mapping (to restore the value in the SVG)"
|
||||
[shape]
|
||||
(let [colors (->> (:content shape)
|
||||
(let [color-data
|
||||
(->> (:content shape)
|
||||
(tree-seq map? :children)
|
||||
(into #{"#000000"} (comp (map :fill-color) (filter string?))))]
|
||||
(apply str (interpose "," colors))))
|
||||
(map fill->color)
|
||||
(filter some?))
|
||||
|
||||
colors (->> color-data
|
||||
(into #{"#000000"}
|
||||
(comp (filter #(= :solid (:type %)))
|
||||
(map :hex))))
|
||||
|
||||
[colors color-data]
|
||||
(loop [colors colors
|
||||
head (first color-data)
|
||||
tail (rest color-data)
|
||||
result []]
|
||||
|
||||
(if (nil? head)
|
||||
[colors result]
|
||||
|
||||
(if (= :solid (:type head))
|
||||
(recur colors
|
||||
(first tail)
|
||||
(rest tail)
|
||||
(conj result head))
|
||||
|
||||
(let [next-color (next-color colors)
|
||||
head (assoc head :map-to next-color)
|
||||
colors (conj colors next-color)]
|
||||
(recur colors
|
||||
(first tail)
|
||||
(rest tail)
|
||||
(conj result head))))))
|
||||
|
||||
color-mapping-inverse
|
||||
(->> color-data
|
||||
(remove #(= :solid (:type %)))
|
||||
(group-by :map-to)
|
||||
(d/mapm #(first %2)))
|
||||
|
||||
color-mapping
|
||||
(merge
|
||||
(->> color-data
|
||||
(filter #(= :transparent (:type %)))
|
||||
(map #(vector [(:hex %) (:opacity %)] (:map-to %)))
|
||||
(into {}))
|
||||
|
||||
(->> color-data
|
||||
(filter #(= :gradient (:type %)))
|
||||
(map #(vector (:gradient %) (:map-to %)))
|
||||
(into {})))]
|
||||
|
||||
[colors color-mapping color-mapping-inverse]))
|
||||
|
||||
(mf/defc text-shape
|
||||
{::mf/wrap-props false
|
||||
|
@ -88,11 +190,19 @@
|
|||
grow-type (obj/get props "grow-type") ;; This is only needed in workspace
|
||||
;; We add 8px to add a padding for the exporter
|
||||
;; width (+ width 8)
|
||||
]
|
||||
[colors color-mapping color-mapping-inverse] (retrieve-colors shape)
|
||||
|
||||
plain-colors? (mf/use-ctx muc/text-plain-colors-ctx)
|
||||
|
||||
content (cond-> content
|
||||
plain-colors?
|
||||
(remap-colors color-mapping))]
|
||||
|
||||
[:foreignObject {:x x
|
||||
:y y
|
||||
:id id
|
||||
:data-colors (retrieve-colors shape)
|
||||
:data-colors (->> colors (str/join ","))
|
||||
:data-mapping (-> color-mapping-inverse (clj->js) (js/JSON.stringify))
|
||||
:transform (geom/transform-matrix shape)
|
||||
:width (if (#{:auto-width} grow-type) 100000 width)
|
||||
:height (if (#{:auto-height :auto-width} grow-type) 100000 height)
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
(ns app.util.color
|
||||
"Color conversion utils."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.util.object :as obj]
|
||||
[cuerdas.core :as str]
|
||||
[goog.color :as gcolor]))
|
||||
|
@ -155,3 +156,19 @@
|
|||
|
||||
(def empty-color
|
||||
(into {} (map #(vector % nil)) [:color :id :file-id :gradient :opacity]))
|
||||
|
||||
(defn next-rgb
|
||||
"Given a color in rgb returns the next color"
|
||||
[[r g b]]
|
||||
(cond
|
||||
(and (= 255 r) (= 255 g) (= 255 b))
|
||||
(ex/raise "Cannot get next color")
|
||||
|
||||
(and (= 255 g) (= 255 b))
|
||||
[(inc r) 0 0]
|
||||
|
||||
(= 255 b)
|
||||
[r (inc g) 0]
|
||||
|
||||
:else
|
||||
[r g (inc b)]))
|
||||
|
|
Loading…
Add table
Reference in a new issue