0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-04 13:50:12 -05:00

🎉 Basic wasm support for svg attrs and svg defs

This commit is contained in:
Alejandro Alonso 2024-12-26 13:16:37 +01:00
parent c0cfa8dc42
commit f509edc151
12 changed files with 564 additions and 74 deletions

View file

@ -128,11 +128,13 @@
(defn svg-raw-wrapper-factory
[objects]
(let [shape-wrapper (shape-wrapper-factory objects)
svg-raw-shape (svg-raw/svg-raw-shape shape-wrapper)]
svg-raw-shape (svg-raw/svg-raw-shape shape-wrapper)]
(mf/fnc svg-raw-wrapper
[{:keys [shape] :as props}]
(let [childs (mapv #(get objects %) (:shapes shape))]
(if (and (map? (:content shape))
;; tspan shouldn't be contained in a group or have svg defs
(not= :tspan (get-in shape [:content :tag]))
(or (= :svg (get-in shape [:content :tag]))
(contains? shape :svg-attrs)))
[:> shape-container {:shape shape}

View file

@ -13,15 +13,25 @@
[app.main.ui.components.title-bar :refer [title-bar]]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.functions :as uf]
[app.util.i18n :refer [tr]]
[rumext.v2 :as mf]))
(mf/defc attribute-value [{:keys [attr value on-change on-delete] :as props}]
(let [handle-change
(let [last-value (mf/use-state value)
handle-change*
(mf/use-fn
(mf/deps attr on-change)
(uf/debounce (fn [val]
(on-change attr val))
300))
handle-change
(mf/use-fn
(mf/deps attr on-change handle-change*)
(fn [event]
(on-change attr (dom/get-target-val event))))
(reset! last-value (dom/get-target-val event))
(handle-change* (dom/get-target-val event))))
handle-delete
(mf/use-fn
@ -35,7 +45,7 @@
[:div {:class (stl/css :attr-content)}
[:span {:class (stl/css :attr-name)} label]
[:div {:class (stl/css :attr-input)}
[:input {:value value
[:input {:value @last-value
:on-change handle-change}]]
[:div {:class (stl/css :attr-actions)}
[:button {:class (stl/css :attr-action-btn)

View file

@ -7,19 +7,26 @@
(ns app.render-wasm.api
"A WASM based render API"
(:require
["react-dom/server" :as rds]
[app.common.colors :as cc]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.math :as mth]
[app.common.svg.path :as path]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.refs :as refs]
[app.main.render :as render]
[app.render-wasm.helpers :as h]
[app.util.debug :as dbg]
[app.util.functions :as fns]
[app.util.http :as http]
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[goog.object :as gobj]
[promesa.core :as p]))
[promesa.core :as p]
[rumext.v2 :as mf]))
(defonce internal-frame-id nil)
(defonce internal-module #js {})
@ -28,6 +35,54 @@
(def dpr
(if use-dpr? js/window.devicePixelRatio 1.0))
;; (mf/defc svg-raw-element
;; {::mf/props :obj}
;; [{:keys [tag attrs content] :as props}]
;; [:& (name tag) attrs
;; (for [child content]
;; (if (string? child)
;; child
;; [:& svg-raw-element child]))])
;; (mf/defc svg-raw
;; {::mf/props :obj}
;; [{:keys [shape] :as props}]
;; (let [content (:content shape)]
;; [:svg {:version "1.1"
;; :xmlns "http://www.w3.org/2000/svg"
;; :xmlnsXlink "http://www.w3.org/1999/xlink"
;; :fill "none"}
;; (if (string? content)
;; content
;; (let [svg-attrs (:svg-attrs shape)
;; content (->
;; (:content shape)
;; (update :attrs merge svg-attrs))]
;; (println "content" content)
;; (println "svg-attrs" svg-attrs)
;; [:& svg-raw-element content]))]))
;; Based on app.main.render/object-svg
(mf/defc object-svg
{::mf/props :obj}
[{:keys [shape] :as props}]
(let [objects (mf/deref refs/workspace-page-objects)
shape-wrapper
(mf/with-memo [shape]
(render/shape-wrapper-factory objects))]
[:svg {:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:fill "none"}
[:& shape-wrapper {:shape shape}]]))
(defn get-static-markup
[shape]
(->
(mf/element object-svg #js {:shape shape})
(rds/renderToStaticMarkup)))
;; This should never be called from the outside.
;; This function receives a "time" parameter that we're not using but maybe in the future could be useful (it is the time since
;; the window started rendering elements so it could be useful to measure time between frames).
@ -237,16 +292,52 @@
(store-image id))))))
fills))
;; (defn serialize-path-style
;; [style]
;; (reduce
;; (fn [acc [key value]]
;; (str/concat acc (str/kebab key) ": " value ";"))
;; ""
;; style))
(defn serialize-path-attrs
[svg-attrs]
(reduce
(fn [acc [key value]]
(str/concat
acc
(str/kebab key) "\0"
value "\0")) "" svg-attrs))
(defn set-shape-path-attrs
[attrs]
(let [style (:style attrs)
attrs (-> attrs
(dissoc :style)
(merge style))
str (serialize-path-attrs attrs)
size (count str)
ptr (h/call internal-module "_alloc_bytes" size)]
(h/call internal-module "stringToUTF8" str ptr size)
(h/call internal-module "_set_shape_path_attrs" (count attrs))))
(defn set-shape-path-content
[content]
(let [buffer (path/content->buffer content)
size (.-byteLength buffer)
ptr (h/call internal-module "_alloc_bytes" size)
(let [buffer (path/content->buffer content)
size (.-byteLength buffer)
ptr (h/call internal-module "_alloc_bytes" size)
heap (gobj/get ^js internal-module "HEAPU8")
mem (js/Uint8Array. (.-buffer heap) ptr size)]
(.set mem (js/Uint8Array. buffer))
(h/call internal-module "_set_shape_path_content")))
(defn set-shape-svg-raw-content
[content]
(let [size (get-string-length content)
ptr (h/call internal-module "_alloc_bytes" size)]
(h/call internal-module "stringToUTF8" content ptr size)
(h/call internal-module "_set_shape_svg_raw_content")))
(defn- translate-blend-mode
[blend-mode]
(case blend-mode
@ -290,6 +381,11 @@
(h/call internal-module "_navigate")
(debounce-render-without-cache))
(defn add-svg-fill
[svg-data]
{:fill-color (:fill svg-data)
:fill-opacity (-> svg-data (:fill-opacity "1") d/parse-double)})
(defn set-objects
[objects]
(let [shapes (into [] (vals objects))
@ -310,7 +406,8 @@
blend-mode (dm/get-prop shape :blend-mode)
opacity (dm/get-prop shape :opacity)
hidden (dm/get-prop shape :hidden)
content (dm/get-prop shape :content)]
content (dm/get-prop shape :content)
svg-attrs (dm/get-prop shape :svg-attrs)]
(use-shape id)
(set-shape-type type)
@ -319,13 +416,21 @@
(set-shape-rotation rotation)
(set-shape-transform transform)
(set-shape-blend-mode blend-mode)
(set-shape-children children)
(set-shape-opacity opacity)
(set-shape-hidden hidden)
(when (and (some? content) (= type :path)) (set-shape-path-content content))
(set-shape-children children)
(when (= :path type)
(set-shape-path-attrs svg-attrs)
(set-shape-path-content content))
(when (= :svg-raw type)
(set-shape-svg-raw-content (get-static-markup shape)))
(let [pending-fills (doall (set-shape-fills fills))]
(recur (inc index) (into pending pending-fills))))
pending))]
(request-render)
(when-let [pending (seq pending)]
(->> (rx/from pending)
@ -340,9 +445,9 @@
:alpha true})
(defn clear-canvas
[]
;; TODO: perform corresponding cleaning
)
[])
;; TODO: Perform the corresponding cleanup."
(defn resize-viewbox
[width height]

View file

@ -121,7 +121,14 @@
:opacity (api/set-shape-opacity v)
:hidden (api/set-shape-hidden v)
:shapes (api/set-shape-children v)
:content (api/set-shape-path-content v)
:svg-attrs (when (= (:type self) :path)
(api/set-shape-path-attrs v))
:content (cond
(= (:type self) :path)
(api/set-shape-path-content v)
(= (:type self) :svg-raw)
(api/set-shape-svg-raw-content (api/get-static-markup self)))
nil)
;; when something synced with wasm
;; is modified, we need to request

135
render-wasm/Cargo.lock generated
View file

@ -8,6 +8,19 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"getrandom",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
@ -93,12 +106,35 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "edit-xml"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f955ed8607e62368a0ff8c4235d8a9b5ebb8d7dbf0139a457cf324ce5ed5d6a"
dependencies = [
"ahash",
"encoding_rs",
"memchr",
"quick-xml",
"thiserror",
"tracing",
]
[[package]]
name = "either"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]]
name = "equivalent"
version = "1.0.1"
@ -293,12 +329,24 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project-lite"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
[[package]]
name = "prettyplease"
version = "0.2.24"
@ -318,6 +366,15 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "quick-xml"
version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.37"
@ -369,6 +426,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
name = "render"
version = "0.1.0"
dependencies = [
"edit-xml",
"gl",
"skia-safe",
"uuid",
@ -509,6 +567,26 @@ dependencies = [
"xattr",
]
[[package]]
name = "thiserror"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "toml"
version = "0.8.19"
@ -543,6 +621,37 @@ dependencies = [
"winnow",
]
[[package]]
name = "tracing"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [
"once_cell",
]
[[package]]
name = "unicode-ident"
version = "1.0.13"
@ -558,6 +667,12 @@ dependencies = [
"getrandom",
]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@ -671,3 +786,23 @@ name = "xml-rs"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26"
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View file

@ -11,6 +11,7 @@ name = "render_wasm"
path = "src/main.rs"
[dependencies]
edit-xml = "0.1.0"
gl = "0.14.0"
skia-safe = { version = "0.80.1", default-features = false, features = ["gl", "svg", "textlayout", "binary-cache"]}
uuid = { version = "1.11.0", features = ["v4"] }

View file

@ -16,7 +16,7 @@ export EMCC_CFLAGS="--no-entry \
-sMAX_WEBGL_VERSION=2 \
-sMODULARIZE=1 \
-sEXPORT_NAME=createRustSkiaModule \
-sEXPORTED_RUNTIME_METHODS=GL \
-sEXPORTED_RUNTIME_METHODS=GL,stringToUTF8 \
-sEXPORT_ES6=1"
EMSDK_QUIET=1 . /usr/local/emsdk/emsdk_env.sh;

View file

@ -327,6 +327,21 @@ pub extern "C" fn clear_shape_fills() {
}
}
#[no_mangle]
pub extern "C" fn set_shape_svg_raw_content() {
let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer");
if let Some(shape) = state.current_shape() {
let bytes = mem::bytes();
let svg_raw_content = String::from_utf8(bytes)
.unwrap()
.trim_end_matches('\0')
.to_string();
shape
.set_svg_raw_content(svg_raw_content)
.expect("Failed to set svg raw content");
}
}
#[no_mangle]
pub extern "C" fn set_shape_blend_mode(mode: i32) {
let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer");
@ -367,6 +382,39 @@ pub extern "C" fn set_shape_path_content() {
}
}
// Extracts a string from the bytes slice until the next null byte (0) and returns the result as a `String`.
// Updates the `start` index to the end of the extracted string.
fn extract_string(start: &mut usize, bytes: &[u8]) -> String {
match bytes[*start..].iter().position(|&b| b == 0) {
Some(pos) => {
let end = *start + pos;
let slice = &bytes[*start..end];
*start = end + 1; // Move the `start` pointer past the null byte
// Call to unsafe function within an unsafe block
unsafe { String::from_utf8_unchecked(slice.to_vec()) }
}
None => {
*start = bytes.len(); // Move `start` to the end if no null byte is found
String::new()
}
}
}
#[no_mangle]
pub extern "C" fn set_shape_path_attrs(num_attrs: u32) {
let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer");
if let Some(shape) = state.current_shape() {
let bytes = mem::bytes();
let mut start = 0;
for _ in 0..num_attrs {
let name = extract_string(&mut start, &bytes);
let value = extract_string(&mut start, &bytes);
shape.set_path_attr(name, value);
}
}
}
fn main() {
init_gl();
}

View file

@ -1,7 +1,6 @@
use std::collections::HashMap;
use skia::Contains;
use skia_safe as skia;
use std::collections::HashMap;
use uuid::Uuid;
use crate::math;
@ -31,6 +30,7 @@ pub trait Renderable {
fn hidden(&self) -> bool;
fn clip(&self) -> bool;
fn children_ids(&self) -> Vec<Uuid>;
fn is_recursive(&self) -> bool;
}
pub(crate) struct CachedSurfaceImage {
@ -188,6 +188,7 @@ impl RenderState {
skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest),
Some(&paint),
);
self.drawing_surface
.canvas()
.clear(skia::Color::TRANSPARENT);
@ -318,43 +319,49 @@ impl RenderState {
// Returns a boolean indicating if the viewbox contains the rendered shapes
fn render_shape_tree(&mut self, root_id: &Uuid, tree: &HashMap<Uuid, impl Renderable>) -> bool {
let element = tree.get(&root_id).unwrap();
let mut is_complete = self.viewbox.area.contains(element.bounds());
if let Some(element) = tree.get(&root_id) {
let mut is_complete = self.viewbox.area.contains(element.bounds());
if !root_id.is_nil() {
if !element.bounds().intersects(self.viewbox.area) || element.hidden() {
self.render_debug_element(element, false);
// TODO: This means that not all the shapes are renderer so we
// need to call a render_all on the zoom out.
return is_complete; // TODO return is_complete or return false??
} else {
self.render_debug_element(element, true);
if !root_id.is_nil() {
if !element.bounds().intersects(self.viewbox.area) || element.hidden() {
self.render_debug_element(element, false);
// TODO: This means that not all the shapes are renderer so we
// need to call a render_all on the zoom out.
return is_complete; // TODO return is_complete or return false??
} else {
self.render_debug_element(element, true);
}
}
}
// This is needed so the next non-children shape does not carry this shape's transform
self.final_surface.canvas().save();
self.drawing_surface.canvas().save();
// This is needed so the next non-children shape does not carry this shape's transform
self.final_surface.canvas().save();
self.drawing_surface.canvas().save();
if !root_id.is_nil() {
self.render_single_element(element);
if element.clip() {
self.drawing_surface.canvas().clip_rect(
element.bounds(),
skia::ClipOp::Intersect,
true,
);
if !root_id.is_nil() {
self.render_single_element(element);
if element.clip() {
self.drawing_surface.canvas().clip_rect(
element.bounds(),
skia::ClipOp::Intersect,
true,
);
}
}
// draw all the children shapes
if element.is_recursive() {
for id in element.children_ids() {
is_complete = self.render_shape_tree(&id, tree) && is_complete;
}
}
self.final_surface.canvas().restore();
self.drawing_surface.canvas().restore();
return is_complete;
} else {
eprintln!("Error: Element with root_id {root_id} not found in the tree.");
return false;
}
// draw all the children shapes
for id in element.children_ids() {
is_complete = self.render_shape_tree(&id, tree) && is_complete;
}
self.final_surface.canvas().restore();
self.drawing_surface.canvas().restore();
return is_complete;
}
}

View file

@ -1,5 +1,6 @@
use crate::math;
use skia_safe as skia;
use std::collections::HashMap;
use uuid::Uuid;
use crate::render::{BlendMode, Renderable};
@ -9,17 +10,20 @@ mod images;
mod matrix;
mod paths;
mod renderable;
mod svgraw;
pub use fills::*;
pub use images::*;
use matrix::*;
pub use paths::*;
pub use svgraw::*;
#[derive(Debug, Clone, PartialEq)]
pub enum Kind {
Rect(math::Rect),
Circle(math::Rect),
Path(Path),
SVGRaw(SVGRaw),
}
pub type Color = skia::Color;
@ -38,6 +42,7 @@ pub struct Shape {
blend_mode: BlendMode,
opacity: f32,
hidden: bool,
svg_attrs: HashMap<String, String>,
}
impl Shape {
@ -54,6 +59,7 @@ impl Shape {
blend_mode: BlendMode::default(),
opacity: 1.,
hidden: false,
svg_attrs: HashMap::new(),
}
}
@ -135,10 +141,28 @@ impl Shape {
Ok(())
}
pub fn set_path_attr(&mut self, name: String, value: String) {
match &mut self.kind {
Kind::Path(_) => {
self.set_svg_attr(name, value);
}
Kind::Rect(_) | Kind::Circle(_) | Kind::SVGRaw(_) => todo!(),
};
}
pub fn set_svg_raw_content(&mut self, content: String) -> Result<(), String> {
self.kind = Kind::SVGRaw(SVGRaw::from_content(content));
Ok(())
}
pub fn set_blend_mode(&mut self, mode: BlendMode) {
self.blend_mode = mode;
}
pub fn set_svg_attr(&mut self, name: String, value: String) {
self.svg_attrs.insert(name, value);
}
fn to_path_transform(&self) -> Option<skia::Matrix> {
match self.kind {
Kind::Path(_) => {

View file

@ -1,4 +1,6 @@
use edit_xml::Document;
use skia_safe as skia;
use std::collections::HashMap;
use uuid::Uuid;
use super::{Fill, Image, Kind, Shape};
@ -23,21 +25,38 @@ impl Renderable for Shape {
surface.canvas().concat(&matrix);
for fill in self.fills().rev() {
render_fill(
surface,
images,
fill,
self.selrect,
&self.kind,
self.to_path_transform().as_ref(),
);
}
let mut paint = skia::Paint::default();
paint.set_blend_mode(self.blend_mode.into());
paint.set_alpha_f(self.opacity);
match &self.kind {
Kind::SVGRaw(sr) => render_svg(&sr.content.to_string(), surface, font_provider),
_ => {
// let svg_canvas_required =
// matches!(&self.kind, Kind::Path(_)) && !self.svg_attrs.is_empty();
// if svg_canvas_required {
// let svg_canvas = build_svg_canvas(self.selrect);
// render_fills_for_kind(
// self,
// &svg_canvas,
// images,
// self.to_path_transform().as_ref(),
// );
// render_svg_path_attrs(
// svg_canvas,
// &self.svg_attrs,
// self.selrect,
// surface,
// font_provider,
// );
// } else {
let canvas = surface.canvas();
render_fills_for_kind(
self,
&canvas,
images,
self.to_path_transform().as_ref(),
&self.svg_attrs,
);
// }
}
};
Ok(())
}
@ -64,22 +83,68 @@ impl Renderable for Shape {
fn children_ids(&self) -> Vec<Uuid> {
self.children.clone()
}
fn is_recursive(&self) -> bool {
!matches!(self.kind, Kind::SVGRaw(_))
}
}
fn render_fills_for_kind(
shape: &Shape,
canvas: &skia::Canvas,
images: &ImageStore,
path_transform: Option<&skia::Matrix>,
svg_attrs: &HashMap<String, String>,
) {
for fill in shape.fills().rev() {
render_fill(
canvas,
images,
fill,
shape.selrect,
&shape.kind,
path_transform,
svg_attrs,
);
}
//TODO: remove when strokes are implemented, this is just for testing paths with no fills
if shape.fills().len() == 0 {
if let Kind::Path(ref path) = shape.kind {
let mut p = skia::Paint::default();
p.set_style(skia_safe::PaintStyle::Stroke);
p.set_stroke_width(2.0);
p.set_anti_alias(true);
p.set_blend_mode(skia::BlendMode::SrcOver);
if let Some("round") = svg_attrs.get("stroke-linecap").map(String::as_str) {
p.set_stroke_cap(skia::paint::Cap::Round);
}
if let Some("round") = svg_attrs.get("stroke-linejoin").map(String::as_str) {
p.set_stroke_join(skia::paint::Join::Round);
}
let mut skia_path = &mut path.to_skia_path();
skia_path = skia_path.transform(path_transform.unwrap());
canvas.draw_path(&skia_path, &p);
}
}
}
fn render_fill(
surface: &mut skia::Surface,
canvas: &skia::Canvas,
images: &ImageStore,
fill: &Fill,
selrect: Rect,
kind: &Kind,
path_transform: Option<&skia::Matrix>,
svg_attrs: &HashMap<String, String>,
) {
match (fill, kind) {
(Fill::Image(image_fill), kind) => {
let image = images.get(&image_fill.id());
if let Some(image) = image {
draw_image_in_container(
surface.canvas(),
canvas,
&image,
image_fill.size(),
kind,
@ -90,16 +155,22 @@ fn render_fill(
}
}
(_, Kind::Rect(rect)) => {
surface.canvas().draw_rect(rect, &fill.to_paint(&selrect));
canvas.draw_rect(rect, &fill.to_paint(&selrect));
}
(_, Kind::Circle(rect)) => {
surface.canvas().draw_oval(rect, &fill.to_paint(&selrect));
canvas.draw_oval(rect, &fill.to_paint(&selrect));
}
(_, Kind::Path(path)) => {
surface.canvas().draw_path(
&path.to_skia_path().transform(path_transform.unwrap()),
&fill.to_paint(&selrect),
);
let mut skia_path = &mut path.to_skia_path();
skia_path = skia_path.transform(path_transform.unwrap());
if let Some("evenodd") = svg_attrs.get("fill-rule").map(String::as_str) {
skia_path.set_fill_type(skia::PathFillType::EvenOdd);
}
canvas.draw_path(&skia_path, &fill.to_paint(&selrect));
}
(_, Kind::SVGRaw(_sr)) => {
// NOOP
}
}
}
@ -162,6 +233,9 @@ pub fn draw_image_in_container(
true,
);
}
Kind::SVGRaw(_) => {
canvas.clip_rect(container, skia::ClipOp::Intersect, true);
}
}
// Draw the image with the calculated destination rectangle
@ -170,3 +244,70 @@ pub fn draw_image_in_container(
// Restore the canvas to remove the clipping
canvas.restore();
}
fn render_svg(
svg: &str,
surface: &mut skia::Surface,
font_provider: &skia::textlayout::TypefaceFontProvider,
) {
let font_manager = skia::FontMgr::from(font_provider.clone());
let dom_result = skia::svg::Dom::from_str(svg, font_manager);
match dom_result {
Ok(dom) => {
dom.render(surface.canvas());
}
Err(e) => {
eprintln!("Error parsing SVG. Error: {}", e);
}
}
}
fn build_svg_canvas(selrect: Rect) -> skia::svg::Canvas {
let canvas = skia::svg::Canvas::new(
skia::Rect::from_size((
selrect.right - selrect.left + 1.,
selrect.bottom - selrect.top + 1.,
)),
None,
);
// SVG canvas needs positive sizes
canvas.concat(&skia::Matrix::translate(skia::Point::new(
-selrect.left,
-selrect.top,
)));
canvas
}
fn render_svg_path_attrs(
canvas: skia::svg::Canvas,
svg_attrs: &HashMap<String, String>,
selrect: Rect,
surface: &mut skia_safe::Surface,
font_provider: &skia::textlayout::TypefaceFontProvider,
) {
let svg_data = canvas.end();
let svg = String::from_utf8_lossy(svg_data.as_bytes());
let mut doc = Document::parse_str(&svg).unwrap();
let root = doc.root_element().unwrap();
for path in root.find_all(&doc, "path") {
for (name, value) in svg_attrs {
path.set_attribute(&mut doc, name, value);
}
}
let svg_mod = doc.write_str().unwrap();
let dom =
skia::svg::Dom::from_str(svg_mod, skia::FontMgr::from(font_provider.clone())).unwrap();
surface
.canvas()
.concat(&skia::Matrix::translate(skia::Point::new(
selrect.left,
selrect.top,
)));
dom.render(surface.canvas());
}

View file

@ -0,0 +1,10 @@
#[derive(Debug, Clone, PartialEq)]
pub struct SVGRaw {
pub content: String,
}
impl SVGRaw {
pub fn from_content(svg: String) -> SVGRaw {
SVGRaw { content: svg }
}
}