diff options
author | RazrFalcon <razrfalcon@gmail.com> | 2018-12-24 20:00:02 +0200 |
---|---|---|
committer | RazrFalcon <razrfalcon@gmail.com> | 2018-12-24 20:00:02 +0200 |
commit | 78683370cd41ae10dcc426f2c3237e2bb55e9820 (patch) | |
tree | 71ea77490f66f9d095aba91e53cd5d2dd059e369 | |
parent | 07b5191683f18be693a08f66334568f098ea5429 (diff) |
Added marker support.
37 files changed, 1114 insertions, 80 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index ab9a61d..0069dd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,17 +8,18 @@ This changelog also contains important changes in dependencies. ## [Unreleased] ### Added -- (resvg) Partial `baseline-shift` support. -- (resvg) `letter-spacing` support. +- Added marker support. +- Partial `baseline-shift` support. +- `letter-spacing` support. - (qt-backend) `word-spacing` support. Does not work on the cairo backend. -- (resvg) Keep invisible shapes during *export by ID*. - Required for a proper bbox resolving. ### Fixed - (usvg) `offset` attribute resolving inside the `stop` element. - (usvg) Ungrouping of groups with non-inheritable attributes. - (usvg) `rotate` attribute resolving. +- (usvg) Paths without stroke and fill will no longer be removed. + Required for a proper bbox resolving. - (svgdom) `stroke-miterlimit` attribute parsing. - (svgdom) `length` and `number` attribute types parsing. - (svgdom) `offset` attribute parsing. @@ -547,19 +547,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "svgdom" version = "0.15.0" -source = "git+https://github.com/RazrFalcon/svgdom?rev=f53af72#f53af72283fbc6fdfc3ff00a3238f3e4b3e053c1" +source = "git+https://github.com/RazrFalcon/svgdom?rev=6ff5579#6ff557934d84be85f25223c626057df8ebb9004c" dependencies = [ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "roxmltree 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "simplecss 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "slab 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "svgtypes 0.3.0 (git+https://github.com/RazrFalcon/svgtypes?rev=3034692)", + "svgtypes 0.3.0 (git+https://github.com/RazrFalcon/svgtypes?rev=63fc6da)", ] [[package]] name = "svgtypes" version = "0.3.0" -source = "git+https://github.com/RazrFalcon/svgtypes?rev=3034692#3034692f4ef2dc31f6da62d23c3d363029aeeaca" +source = "git+https://github.com/RazrFalcon/svgtypes?rev=63fc6da#63fc6da5b1cb226b056344db546510ef8b923923" dependencies = [ "float-cmp 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "phf 0.7.23 (registry+https://github.com/rust-lang/crates.io-index)", @@ -605,7 +605,7 @@ dependencies = [ "lyon_geom 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)", "pretty_assertions 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "rctree 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "svgdom 0.15.0 (git+https://github.com/RazrFalcon/svgdom?rev=f53af72)", + "svgdom 0.15.0 (git+https://github.com/RazrFalcon/svgdom?rev=6ff5579)", "unicode-segmentation 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -699,8 +699,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum simplecss 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "135685097a85a64067df36e28a243e94a94f76d829087ce0be34eeb014260c0e" "checksum siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" "checksum slab 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5f9776d6b986f77b35c6cf846c11ad986ff128fe0b2b63a3628e3755e8d3102d" -"checksum svgdom 0.15.0 (git+https://github.com/RazrFalcon/svgdom?rev=f53af72)" = "<none>" -"checksum svgtypes 0.3.0 (git+https://github.com/RazrFalcon/svgtypes?rev=3034692)" = "<none>" +"checksum svgdom 0.15.0 (git+https://github.com/RazrFalcon/svgdom?rev=6ff5579)" = "<none>" +"checksum svgtypes 0.3.0 (git+https://github.com/RazrFalcon/svgtypes?rev=63fc6da)" = "<none>" "checksum syn 0.14.9 (registry+https://github.com/rust-lang/crates.io-index)" = "261ae9ecaa397c42b960649561949d69311f08eeaea86a65696e6e46517cf741" "checksum time 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "847da467bf0db05882a9e2375934a8a55cffdc9db0d128af1518200260ba1f6c" "checksum unicode-segmentation 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aa6024fc12ddfd1c6dbc14a80fa2324d4568849869b779f6bd37e5e4c03344d1" @@ -63,7 +63,7 @@ of the SVG Tiny 1.2 subset. In simple terms - it correctly renders only primitiv *resvg* is aiming to support only the [static](http://www.w3.org/TR/SVG11/feature#SVG-static) SVG subset. E.g. no `a`, `script`, `view`, `cursor` elements, no events and no animations. -Also, `marker`, `textPath` and +Also, `textPath` and [embedded fonts](https://www.w3.org/TR/SVG11/feature#Font) are not yet implemented. A full list can be found [here](docs/unsupported.md). diff --git a/docs/backend_requirements.md b/docs/backend_requirements.md index 8919018..8f02165 100644 --- a/docs/backend_requirements.md +++ b/docs/backend_requirements.md @@ -49,6 +49,8 @@ List of features required from the 2D graphics library to implement a backend fo - Stretch - Variant: *normal*, *small cap* - Size + - Letter spacing + - Word spacing - Font metrics: - Text bounding box - Ascent/baseline diff --git a/docs/unsupported.md b/docs/unsupported.md index 3868e89..cd1d0a9 100644 --- a/docs/unsupported.md +++ b/docs/unsupported.md @@ -26,7 +26,6 @@ - `altGlyphItem` - `glyphRef` - `color-profile` -- `marker` - `textPath` - `use` with a reference to an external SVG @@ -49,10 +48,6 @@ with `BackgroundImage`, `BackgroundAlpha`, `FillPaint`, `StrokePaint` - `image-rendering` - `kerning` -- `lighting-color` -- `marker-start` -- `marker-mid` -- `marker-end` - `shape-rendering` - `text-rendering` - `unicode-bidi` diff --git a/src/backend_cairo/clippath.rs b/src/backend_cairo/clippath.rs index 574dc37..374435a 100644 --- a/src/backend_cairo/clippath.rs +++ b/src/backend_cairo/clippath.rs @@ -46,7 +46,7 @@ pub fn apply( match *node.borrow() { usvg::NodeKind::Path(ref p) => { - path::draw(&node.tree(), p, opt, &clip_cr); + path::draw(&node.tree(), p, opt, layers, &clip_cr); } usvg::NodeKind::Text(ref text) => { text::draw(&node.tree(), text, opt, &clip_cr); @@ -103,7 +103,7 @@ fn clip_group( clip_cr.paint(); clip_cr.set_matrix(cr.get_matrix()); - draw_group_child(&node, opt, &clip_cr); + draw_group_child(&node, opt, layers, &clip_cr); apply(clip_node, cp, opt, bbox, layers, &clip_cr); @@ -120,6 +120,7 @@ fn clip_group( fn draw_group_child( node: &usvg::Node, opt: &Options, + layers: &mut CairoLayers, cr: &cairo::Context, ) { if let Some(child) = node.first_child() { @@ -127,7 +128,7 @@ fn draw_group_child( match *child.borrow() { usvg::NodeKind::Path(ref path_node) => { - path::draw(&child.tree(), path_node, opt, cr); + path::draw(&child.tree(), path_node, opt, layers, cr); } usvg::NodeKind::Text(ref text) => { text::draw(&child.tree(), text, opt, cr); diff --git a/src/backend_cairo/marker.rs b/src/backend_cairo/marker.rs new file mode 100644 index 0000000..755cac1 --- /dev/null +++ b/src/backend_cairo/marker.rs @@ -0,0 +1,94 @@ +// 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/. + +// external +use cairo; + +// self +use super::prelude::*; +use backend_utils::marker::*; + + +pub fn apply( + tree: &usvg::Tree, + path: &usvg::Path, + opt: &Options, + layers: &mut CairoLayers, + cr: &cairo::Context, +) { + let mut draw_marker = |id: &Option<String>, kind: MarkerKind| { + if let Some(ref id) = id { + if let Some(node) = tree.defs_by_id(id) { + if let usvg::NodeKind::Marker(ref marker) = *node.borrow() { + _apply(path, marker, &node, kind, opt, layers, cr); + } + } + } + }; + + draw_marker(&path.marker.start, MarkerKind::Start); + draw_marker(&path.marker.mid, MarkerKind::Middle); + draw_marker(&path.marker.end, MarkerKind::End); +} + +fn _apply( + path: &usvg::Path, + marker: &usvg::Marker, + marker_node: &usvg::Node, + marker_kind: MarkerKind, + opt: &Options, + layers: &mut CairoLayers, + cr: &cairo::Context, +) { + let stroke_scale = try_opt!(stroke_scale(marker, path), ()); + + let r = marker.rect; + debug_assert!(r.is_valid()); + + let draw_marker = |x: f64, y: f64, idx: usize| { + let old_ts = cr.get_matrix(); + cr.translate(x, y); + + let angle = match marker.orientation { + usvg::MarkerOrientation::Auto => calc_vertex_angle(&path.segments, idx), + usvg::MarkerOrientation::Angle(angle) => angle, + }; + + if !angle.is_fuzzy_zero() { + let ts = usvg::Transform::new_rotate(angle); + cr.transform(ts.to_native()); + } + + if let Some(vbox) = marker.view_box { + let size = Size::new(r.width * stroke_scale, r.height * stroke_scale); + let ts = utils::view_box_to_transform(vbox.rect, vbox.aspect, size); + cr.transform(ts.to_native()); + + cr.translate(vbox.rect.x, vbox.rect.y); + } else { + cr.scale(stroke_scale, stroke_scale); + } + + cr.translate(-r.x, -r.y); + + match marker.overflow { + usvg::Overflow::Hidden | usvg::Overflow::Scroll => { + if let Some(vbox) = marker.view_box { + cr.rectangle(vbox.rect.x, vbox.rect.y, vbox.rect.width, vbox.rect.height); + } else { + cr.rectangle(0.0, 0.0, r.width, r.height); + } + cr.clip(); + } + _ => {} + } + + super::render_group(marker_node, opt, layers, cr); + + cr.set_matrix(old_ts); + cr.reset_clip(); + }; + + draw_markers(&path.segments, marker_kind, draw_marker); +} diff --git a/src/backend_cairo/mod.rs b/src/backend_cairo/mod.rs index 832caae..5d326d4 100644 --- a/src/backend_cairo/mod.rs +++ b/src/backend_cairo/mod.rs @@ -37,6 +37,7 @@ mod fill; mod filter; mod gradient; mod image; +mod marker; mod mask; mod path; mod pattern; @@ -264,7 +265,7 @@ fn render_node( Some(render_group(node, opt, layers, cr)) } usvg::NodeKind::Path(ref path) => { - Some(path::draw(&node.tree(), path, opt, cr)) + Some(path::draw(&node.tree(), path, opt, layers, cr)) } usvg::NodeKind::Text(ref text) => { Some(text::draw(&node.tree(), text, opt, cr)) diff --git a/src/backend_cairo/path.rs b/src/backend_cairo/path.rs index 98eecb9..27a8727 100644 --- a/src/backend_cairo/path.rs +++ b/src/backend_cairo/path.rs @@ -10,6 +10,7 @@ use super::prelude::*; use super::{ fill, stroke, + marker, }; @@ -17,6 +18,7 @@ pub fn draw( tree: &usvg::Tree, path: &usvg::Path, opt: &Options, + layers: &mut CairoLayers, cr: &cairo::Context, ) -> Rect { let mut is_square_cap = false; @@ -42,6 +44,8 @@ pub fn draw( cr.fill(); } + marker::apply(tree, path, opt, layers, cr); + bbox } diff --git a/src/backend_cairo/stroke.rs b/src/backend_cairo/stroke.rs index a0efac8..d30d41d 100644 --- a/src/backend_cairo/stroke.rs +++ b/src/backend_cairo/stroke.rs @@ -59,7 +59,7 @@ pub fn apply( cr.set_line_join(linejoin); match stroke.dasharray { - Some(ref list) => cr.set_dash(list, stroke.dashoffset), + Some(ref list) => cr.set_dash(list, stroke.dashoffset as f64), None => cr.set_dash(&[], 0.0), } diff --git a/src/backend_qt/clippath.rs b/src/backend_qt/clippath.rs index eba05bf..ef1219a 100644 --- a/src/backend_qt/clippath.rs +++ b/src/backend_qt/clippath.rs @@ -41,7 +41,7 @@ pub fn apply( match *node.borrow() { usvg::NodeKind::Path(ref path_node) => { - path::draw(&node.tree(), path_node, opt, &mut clip_p); + path::draw(&node.tree(), path_node, opt, layers, &mut clip_p); } usvg::NodeKind::Text(ref text) => { text::draw(&node.tree(), text, opt, &mut clip_p); @@ -90,7 +90,7 @@ fn clip_group( let mut clip_p = qt::Painter::new(&mut clip_img); clip_p.set_transform(&p.get_transform()); - draw_group_child(&node, opt, &mut clip_p); + draw_group_child(&node, opt, layers, &mut clip_p); apply(clip_node, cp, opt, bbox, layers, &mut clip_p); clip_p.end(); @@ -106,6 +106,7 @@ fn clip_group( fn draw_group_child( node: &usvg::Node, opt: &Options, + layers: &mut QtLayers, p: &mut qt::Painter, ) { if let Some(child) = node.first_child() { @@ -113,7 +114,7 @@ fn draw_group_child( match *child.borrow() { usvg::NodeKind::Path(ref path_node) => { - path::draw(&child.tree(), path_node, opt, p); + path::draw(&child.tree(), path_node, opt, layers, p); } usvg::NodeKind::Text(ref text) => { text::draw(&child.tree(), text, opt, p); diff --git a/src/backend_qt/marker.rs b/src/backend_qt/marker.rs new file mode 100644 index 0000000..694f432 --- /dev/null +++ b/src/backend_qt/marker.rs @@ -0,0 +1,93 @@ +// 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/. + +// external +use qt; + +// self +use super::prelude::*; +use backend_utils::marker::*; + + +pub fn apply( + tree: &usvg::Tree, + path: &usvg::Path, + opt: &Options, + layers: &mut QtLayers, + p: &mut qt::Painter, +) { + let mut draw_marker = |id: &Option<String>, kind: MarkerKind| { + if let Some(ref id) = id { + if let Some(node) = tree.defs_by_id(id) { + if let usvg::NodeKind::Marker(ref marker) = *node.borrow() { + _apply(path, marker, &node, kind, opt, layers, p); + } + } + } + }; + + draw_marker(&path.marker.start, MarkerKind::Start); + draw_marker(&path.marker.mid, MarkerKind::Middle); + draw_marker(&path.marker.end, MarkerKind::End); +} + +fn _apply( + path: &usvg::Path, + marker: &usvg::Marker, + marker_node: &usvg::Node, + marker_kind: MarkerKind, + opt: &Options, + layers: &mut QtLayers, + p: &mut qt::Painter, +) { + let stroke_scale = try_opt!(stroke_scale(marker, path), ()); + + let r = marker.rect; + debug_assert!(r.is_valid()); + + let draw_marker = |x: f64, y: f64, idx: usize| { + let old_ts = p.get_transform(); + p.translate(x, y); + + let angle = match marker.orientation { + usvg::MarkerOrientation::Auto => calc_vertex_angle(&path.segments, idx), + usvg::MarkerOrientation::Angle(angle) => angle, + }; + + if !angle.is_fuzzy_zero() { + let ts = usvg::Transform::new_rotate(angle); + p.apply_transform(&ts.to_native()); + } + + if let Some(vbox) = marker.view_box { + let size = Size::new(r.width * stroke_scale, r.height * stroke_scale); + let ts = utils::view_box_to_transform(vbox.rect, vbox.aspect, size); + p.apply_transform(&ts.to_native()); + + p.translate(vbox.rect.x, vbox.rect.y); + } else { + p.scale(stroke_scale, stroke_scale); + } + + p.translate(-r.x, -r.y); + + match marker.overflow { + usvg::Overflow::Hidden | usvg::Overflow::Scroll => { + if let Some(vbox) = marker.view_box { + p.set_clip_rect(vbox.rect.x, vbox.rect.y, vbox.rect.width, vbox.rect.height); + } else { + p.set_clip_rect(0.0, 0.0, r.width, r.height); + } + } + _ => {} + } + + super::render_group(marker_node, opt, layers, p); + + p.set_transform(&old_ts); + p.reset_clip_path(); + }; + + draw_markers(&path.segments, marker_kind, draw_marker); +} diff --git a/src/backend_qt/mod.rs b/src/backend_qt/mod.rs index 27302ed..b4d8b94 100644 --- a/src/backend_qt/mod.rs +++ b/src/backend_qt/mod.rs @@ -33,6 +33,7 @@ mod fill; mod filter; mod gradient; mod image; +mod marker; mod mask; mod path; mod pattern; @@ -221,7 +222,7 @@ fn render_node( Some(render_group(node, opt, layers, p)) } usvg::NodeKind::Path(ref path) => { - Some(path::draw(&node.tree(), path, opt, p)) + Some(path::draw(&node.tree(), path, opt, layers, p)) } usvg::NodeKind::Text(ref text) => { Some(text::draw(&node.tree(), text, opt, p)) diff --git a/src/backend_qt/path.rs b/src/backend_qt/path.rs index dc3dad3..ef47cf4 100644 --- a/src/backend_qt/path.rs +++ b/src/backend_qt/path.rs @@ -10,6 +10,7 @@ use super::prelude::*; use super::{ fill, stroke, + marker, }; @@ -17,6 +18,7 @@ pub fn draw( tree: &usvg::Tree, path: &usvg::Path, opt: &Options, + layers: &mut QtLayers, p: &mut qt::Painter, ) -> Rect { let mut p_path = qt::PainterPath::new(); @@ -40,6 +42,8 @@ pub fn draw( p.draw_path(&p_path); + marker::apply(tree, path, opt, layers, p); + bbox } diff --git a/src/backend_qt/pattern.rs b/src/backend_qt/pattern.rs index e177024..ff30b8d 100644 --- a/src/backend_qt/pattern.rs +++ b/src/backend_qt/pattern.rs @@ -23,6 +23,7 @@ pub fn apply( pattern.rect }; + // TODO: wrong let global_ts = usvg::Transform::from_native(&global_ts); let (sx, sy) = global_ts.get_scale(); // Only integer scaling is allowed. diff --git a/src/backend_qt/stroke.rs b/src/backend_qt/stroke.rs index ab19d0b..07e94fe 100644 --- a/src/backend_qt/stroke.rs +++ b/src/backend_qt/stroke.rs @@ -71,7 +71,7 @@ pub fn apply( pen.set_width(stroke.width.value()); if let Some(ref list) = stroke.dasharray { - pen.set_dash_offset(stroke.dashoffset); + pen.set_dash_offset(stroke.dashoffset as f64); pen.set_dash_array(list); } diff --git a/src/backend_utils/marker.rs b/src/backend_utils/marker.rs new file mode 100644 index 0000000..da09139 --- /dev/null +++ b/src/backend_utils/marker.rs @@ -0,0 +1,282 @@ +// 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/. + +// external +use usvg::PathSegment as Segment; + +// self +use super::prelude::*; + + +pub enum MarkerKind { + Start, + Middle, + End, +} + +pub fn stroke_scale(marker: &usvg::Marker, path: &usvg::Path) -> Option<f64> { + match marker.units { + usvg::MarkerUnits::StrokeWidth => { + match path.marker.stroke { + Some(sw) => Some(sw.value()), + None => None, + } + } + usvg::MarkerUnits::UserSpaceOnUse => Some(1.0), + } +} + +pub fn draw_markers<P>(segments: &[Segment], kind: MarkerKind, mut draw_marker: P) + where P: FnMut(f64, f64, usize) +{ + match kind { + MarkerKind::Start => { + if let Some(Segment::MoveTo { x, y }) = segments.first() { + draw_marker(*x, *y, 0); + } + } + MarkerKind::Middle => { + let total = segments.len() - 1; + let mut i = 1; + while i < total { + let (x, y) = match segments[i] { + Segment::MoveTo { x, y } => (x, y), + Segment::LineTo { x, y } => (x, y), + Segment::CurveTo { x, y, .. } => (x, y), + _ => { + i += 1; + continue + } + }; + + draw_marker(x, y, i); + + i += 1; + } + } + MarkerKind::End => { + let idx = segments.len() - 1; + match segments.last() { + Some(Segment::LineTo { x, y }) => { + draw_marker(*x, *y, idx); + } + Some(Segment::CurveTo { x, y, .. }) => { + draw_marker(*x, *y, idx); + } + Some(Segment::ClosePath) => { + let (x, y) = get_subpath_start(segments, idx); + draw_marker(x, y, idx); + } + _ => {} + } + } + } +} + +pub fn calc_vertex_angle(segments: &[Segment], idx: usize) -> f64 { + if idx == 0 { + // First segment. + + debug_assert!(segments.len() > 1); + + let seg1 = segments[0]; + let seg2 = segments[1]; + + match (seg1, seg2) { + (Segment::MoveTo { x: mx, y: my }, Segment::LineTo { x, y }) => { + calc_line_angle(mx, my, x, y) + } + (Segment::MoveTo { x: mx, y: my }, Segment::CurveTo { x1, y1, x, y, .. }) => { + if mx.fuzzy_eq(&x1) && my.fuzzy_eq(&y1) { + calc_line_angle(mx, my, x, y) + } else { + calc_line_angle(mx, my, x1, y1) + } + } + _ => 0.0, + } + } else if idx == segments.len() - 1 { + // Last segment. + + let seg1 = segments[idx - 1]; + let seg2 = segments[idx]; + + match (seg1, seg2) { + (_, Segment::MoveTo { .. }) => 0.0, // unreachable + (_, Segment::LineTo { x, y }) => { + let (px, py) = get_prev_vertex(segments, idx); + calc_line_angle(px, py, x, y) + } + (_, Segment::CurveTo { x2, y2, x, y, .. }) => { + if x2.fuzzy_eq(&x) && y2.fuzzy_eq(&y) { + let (px, py) = get_prev_vertex(segments, idx); + calc_line_angle(px, py, x, y) + } else { + calc_line_angle(x2, y2, x, y) + } + } + (Segment::LineTo { x, y }, Segment::ClosePath) => { + let (nx, ny) = get_subpath_start(segments, idx); + calc_line_angle(x, y, nx, ny) + } + (Segment::CurveTo { x2, y2, x, y, .. }, Segment::ClosePath) => { + let (px, py) = get_prev_vertex(segments, idx); + let (nx, ny) = get_subpath_start(segments, idx); + calc_curves_angle( + px, py, x2, y2, + x, y, + nx, ny, nx, ny, + ) + } + (_, Segment::ClosePath) => 0.0, + } + } else { + // Middle segments. + + let seg1 = segments[idx]; + let seg2 = segments[idx + 1]; + + // Not sure if there is a better way. + match (seg1, seg2) { + (Segment::MoveTo { x: mx, y: my }, Segment::LineTo { x, y }) => { + calc_line_angle(mx, my, x, y) + } + (Segment::MoveTo { x: mx, y: my }, Segment::CurveTo { x1, y1, .. }) => { + calc_line_angle(mx, my, x1, y1) + } + (Segment::LineTo { x: x1, y: y1 }, Segment::LineTo { x: x2, y: y2 }) => { + let (px, py) = get_prev_vertex(segments, idx); + calc_angle(px, py, x1, y1, + x1, y1, x2, y2) + } + (Segment::CurveTo { x2: c1_x2, y2: c1_y2, x, y, .. }, + Segment::CurveTo { x1: c2_x1, y1: c2_y1, x: nx, y: ny, .. }) => { + let (px, py) = get_prev_vertex(segments, idx); + calc_curves_angle( + px, py, c1_x2, c1_y2, + x, y, + c2_x1, c2_y1, nx, ny, + ) + } + (Segment::LineTo { x, y }, + Segment::CurveTo { x1, y1, x: nx, y: ny, .. }) => { + let (px, py) = get_prev_vertex(segments, idx); + calc_curves_angle( + px, py, px, py, + x, y, + x1, y1, nx, ny, + ) + } + (Segment::CurveTo { x2, y2, x, y, .. }, + Segment::LineTo { x: nx, y: ny }) => { + let (px, py) = get_prev_vertex(segments, idx); + calc_curves_angle( + px, py, x2, y2, + x, y, + nx, ny, nx, ny, + ) + } + (Segment::LineTo { x, y }, Segment::MoveTo { .. }) => { + let (px, py) = get_prev_vertex(segments, idx); + calc_line_angle(px, py, x, y) + } + (Segment::CurveTo { x2, y2, x, y, .. }, Segment::MoveTo { .. }) => { + if x.fuzzy_eq(&x2) && y.fuzzy_eq(&y2) { + let (px, py) = get_prev_vertex(segments, idx); + calc_line_angle(px, py, x, y) + } else { + calc_line_angle(x2, y2, x, y) + } + } + (Segment::LineTo { x, y }, Segment::ClosePath) => { + let (px, py) = get_prev_vertex(segments, idx); + let (nx, ny) = get_subpath_start(segments, idx); + calc_angle(px, py, x, y, + x, y, nx, ny) + } + (_, Segment::ClosePath) => { + let (px, py) = get_prev_vertex(segments, idx); + let (nx, ny) = get_subpath_start(segments, idx); + calc_line_angle(px, py, nx, ny) + } + (_, Segment::MoveTo { .. }) | + (Segment::ClosePath, _) => { + 0.0 + } + } + } +} + +fn calc_line_angle( + x1: f64, y1: f64, + x2: f64, y2: f64, +) -> f64 { + calc_angle(x1, y1, x2, y2, x1, y1, x2, y2) +} + +fn calc_curves_angle( + px: f64, py: f64, // previous vertex + cx1: f64, cy1: f64, // previous control point + x: f64, y: f64, // current vertex + cx2: f64, cy2: f64, // next control point + nx: f64, ny: f64, // next vertex +) -> f64 { + if cx1.fuzzy_eq(&x) && cy1.fuzzy_eq(&y) { + calc_angle(px, py, x, y, x, y, cx2, cy2) + } else if x.fuzzy_eq(&cx2) && y.fuzzy_eq(&cy2) { + calc_angle(cx1, cy1, x, y, x, y, nx, ny) + } else { + calc_angle(cx1, cy1, x, y, x, y, cx2, cy2) + } +} + +fn calc_angle( + x1: f64, y1: f64, + x2: f64, y2: f64, + x3: f64, y3: f64, + x4: f64, y4: f64, +) -> f64 { + use std::f64::consts::*; + + fn normalize(rad: f64) -> f64 { + let v = rad % (PI * 2.0); + if v < 0.0 { v + PI * 2.0 } else { v } + } + + fn vector_angle(vx: f64, vy: f64) -> f64 { + let rad = vy.atan2(vx); + if rad.is_nan() { 0.0 } else { normalize(rad) } + } + + let in_a = vector_angle(x2 - x1, y2 - y1); + let out_a = vector_angle(x4 - x3, y4 - y3); + let d = (out_a - in_a) * 0.5; + + let mut angle = in_a + d; + if FRAC_PI_2 < d.abs() { + angle -= PI; + } + + normalize(angle) * 180.0 / PI +} + +fn get_subpath_start(segments: &[Segment], idx: usize) -> (f64, f64) { + let offset = segments.len() - idx; + for seg in segments.iter().rev().skip(offset) { + if let Segment::MoveTo { x, y } = seg { + return (*x, *y); + } + } + + return (0.0, 0.0) +} + +fn get_prev_vertex(segments: &[Segment], idx: usize) -> (f64, f64) { + match segments[idx - 1] { + Segment::MoveTo { x, y } => (x, y), + Segment::LineTo { x, y } => (x, y), + Segment::CurveTo { x, y, .. } => (x, y), + Segment::ClosePath => get_subpath_start(segments, idx), + } +} diff --git a/src/backend_utils/mod.rs b/src/backend_utils/mod.rs index d16f7e4..acdf593 100644 --- a/src/backend_utils/mod.rs +++ b/src/backend_utils/mod.rs @@ -4,6 +4,7 @@ pub mod filter; pub mod image; +pub mod marker; pub mod mask; pub mod text; diff --git a/testing_tools/regression/allow-cairo.txt b/testing_tools/regression/allow-cairo.txt index 77edfdc..2fdfb87 100644 --- a/testing_tools/regression/allow-cairo.txt +++ b/testing_tools/regression/allow-cairo.txt @@ -1,12 +1,62 @@ -a-letter-spacing-001.svg -a-letter-spacing-002.svg -a-letter-spacing-003.svg -a-letter-spacing-004.svg -a-letter-spacing-005.svg -a-letter-spacing-006.svg -a-word-spacing-001.svg -a-word-spacing-002.svg -a-word-spacing-003.svg -a-word-spacing-004.svg -a-word-spacing-005.svg -a-word-spacing-006.svg +a-marker-end-001.svg +a-marker-mid-001.svg +a-marker-start-001.svg +a-overflow-001.svg +a-overflow-002.svg +a-overflow-003.svg +e-marker-001.svg +e-marker-002.svg +e-marker-003.svg +e-marker-004.svg +e-marker-005.svg +e-marker-006.svg +e-marker-007.svg +e-marker-008.svg +e-marker-009.svg +e-marker-010.svg +e-marker-011.svg +e-marker-012.svg +e-marker-013.svg +e-marker-014.svg +e-marker-015.svg +e-marker-016.svg +e-marker-017.svg +e-marker-018.svg +e-marker-019.svg +e-marker-020.svg +e-marker-021.svg +e-marker-022.svg +e-marker-023.svg +e-marker-024.svg +e-marker-025.svg +e-marker-026.svg +e-marker-027.svg +e-marker-028.svg +e-marker-029.svg +e-marker-030.svg +e-marker-031.svg +e-marker-032.svg +e-marker-033.svg +e-marker-034.svg +e-marker-035.svg +e-marker-036.svg +e-marker-037.svg +e-marker-038.svg +e-marker-039.svg +e-marker-040.svg +e-marker-041.svg +e-marker-042.svg +e-marker-043.svg +e-marker-044.svg +e-marker-045.svg +e-marker-046.svg +e-marker-047.svg +e-marker-048.svg +e-marker-049.svg +e-marker-050.svg +e-marker-051.svg +e-marker-052.svg +e-marker-053.svg +e-marker-054.svg +e-marker-055.svg +e-marker-056.svg diff --git a/testing_tools/regression/allow-qt.txt b/testing_tools/regression/allow-qt.txt index 77edfdc..2fdfb87 100644 --- a/testing_tools/regression/allow-qt.txt +++ b/testing_tools/regression/allow-qt.txt @@ -1,12 +1,62 @@ -a-letter-spacing-001.svg -a-letter-spacing-002.svg -a-letter-spacing-003.svg -a-letter-spacing-004.svg -a-letter-spacing-005.svg -a-letter-spacing-006.svg -a-word-spacing-001.svg -a-word-spacing-002.svg -a-word-spacing-003.svg -a-word-spacing-004.svg -a-word-spacing-005.svg -a-word-spacing-006.svg +a-marker-end-001.svg +a-marker-mid-001.svg +a-marker-start-001.svg +a-overflow-001.svg +a-overflow-002.svg +a-overflow-003.svg +e-marker-001.svg +e-marker-002.svg +e-marker-003.svg +e-marker-004.svg +e-marker-005.svg +e-marker-006.svg +e-marker-007.svg +e-marker-008.svg +e-marker-009.svg +e-marker-010.svg +e-marker-011.svg +e-marker-012.svg +e-marker-013.svg +e-marker-014.svg +e-marker-015.svg +e-marker-016.svg +e-marker-017.svg +e-marker-018.svg +e-marker-019.svg +e-marker-020.svg +e-marker-021.svg +e-marker-022.svg +e-marker-023.svg +e-marker-024.svg +e-marker-025.svg +e-marker-026.svg +e-marker-027.svg +e-marker-028.svg +e-marker-029.svg +e-marker-030.svg +e-marker-031.svg +e-marker-032.svg +e-marker-033.svg +e-marker-034.svg +e-marker-035.svg +e-marker-036.svg +e-marker-037.svg +e-marker-038.svg +e-marker-039.svg +e-marker-040.svg +e-marker-041.svg +e-marker-042.svg +e-marker-043.svg +e-marker-044.svg +e-marker-045.svg +e-marker-046.svg +e-marker-047.svg +e-marker-048.svg +e-marker-049.svg +e-marker-050.svg +e-marker-051.svg +e-marker-052.svg +e-marker-053.svg +e-marker-054.svg +e-marker-055.svg +e-marker-056.svg diff --git a/usvg/CHANGELOG.md b/usvg/CHANGELOG.md index 15b5193..734436a 100644 --- a/usvg/CHANGELOG.md +++ b/usvg/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added +- Added marker support. - Implement `FuzzyEq` for `Rect`, `Size` and `Point`. - `StrokeMiterlimit` and `FontSize` wrappers for `f64`. - `letter-spacing` and `word-spacing` support. @@ -18,6 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). have an input has it's own `filter_input` field now. - Rename `filter_input` fields into `input`. - Filter primitives inputs and results will be resolved now. +- `Stroke::dashoffset` is `f32` and not `f64` now. ### Fixed - `offset` attribute resolving inside the `stop` element. diff --git a/usvg/Cargo.toml b/usvg/Cargo.toml index 52f5d11..180b983 100644 --- a/usvg/Cargo.toml +++ b/usvg/Cargo.toml @@ -18,7 +18,7 @@ libflate = "0.1" log = "0.4" lyon_geom = "0.12" rctree = "0.2.1" -svgdom = { git = "https://github.com/RazrFalcon/svgdom", rev = "f53af72" } +svgdom = { git = "https://github.com/RazrFalcon/svgdom", rev = "6ff5579" } #svgdom = { path = "../../svgdom" } unicode-segmentation = "1.2.1" diff --git a/usvg/docs/usvg_spec.adoc b/usvg/docs/usvg_spec.adoc index 96d80bc..44d7d11 100644 --- a/usvg/docs/usvg_spec.adoc +++ b/usvg/docs/usvg_spec.adoc @@ -2,6 +2,7 @@ :toc: :1H: # +:star: * == Elements @@ -104,7 +105,7 @@ A group can be empty when it has `filter` attribute. Children: `g`, `path`, `text` and `image`. -Attributes: `id`, `transform`, `opacity`, `clip-path` and `mask`. +Attributes: `id`, `transform`, `opacity`, `clip-path`, `mask` and `filter`. * `id` is optional but never empty. @@ -116,9 +117,13 @@ MoveTo, LineTo, CurveTo and ClosePath segments. Attributes: `id`, <<fill_attrs, filling>>, <<stroke_attrs,stroking>>, `clip-rule` (when inside the `clipPath`), `clip-path` (when inside the `clipPath`), -`visibility` and `transform`. +`visibility`, `marker-start`, `marker-mid`, `marker-end` and `transform`. * `id` is optional but never empty. +* `marker-{star}` attributes will be set only on paths that were originally + `path`, `line`, `polyline` or `polygon`. +* If a path contains an ArcTo segment and a marker - it will be rendered incorrectly, + because `usvg` will convert ArcTo into series of CurveTo's. === text diff --git a/usvg/src/convert/marker.rs b/usvg/src/convert/marker.rs new file mode 100644 index 0000000..0d3f433 --- /dev/null +++ b/usvg/src/convert/marker.rs @@ -0,0 +1,84 @@ +// 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/. + +// external +use svgdom; + +// self +use tree; +use super::prelude::*; + + +pub fn convert( + node: &svgdom::Node, + tree: &mut tree::Tree, +) -> Option<tree::Node> { + let ref attrs = node.attributes(); + + let rect = convert_rect(attrs); + if !rect.is_valid() { + warn!("Marker '{}' has an invalid size. Skipped.", node.id()); + return None; + } + + let view_box = node.get_viewbox().map(|vb| + tree::ViewBox { + rect: vb, + aspect: super::convert_aspect(attrs), + } + ); + + Some(tree.append_to_defs(tree::NodeKind::Marker(tree::Marker { + id: node.id().clone(), + units: convert_units(attrs), + rect, + view_box, + orientation: convert_orientation(attrs), + overflow: convert_overflow(attrs), + }))) +} + +fn convert_rect(attrs: &svgdom::Attributes) -> Rect { + ( + attrs.get_number_or(AId::RefX, 0.0), + attrs.get_number_or(AId::RefY, 0.0), + attrs.get_number_or(AId::MarkerWidth, 3.0), + attrs.get_number_or(AId::MarkerHeight, 3.0), + ).into() +} + +fn convert_units(attrs: &svgdom::Attributes) -> tree::MarkerUnits { + match attrs.get_str(AId::MarkerUnits) { + Some("userSpaceOnUse") => tree::MarkerUnits::UserSpaceOnUse, + _ => tree::MarkerUnits::StrokeWidth, + } +} + +fn convert_overflow(attrs: &svgdom::Attributes) -> tree::Overflow { + match attrs.get_str(AId::Overflow) { + Some("visible") => tree::Overflow::Visible, + Some("hidden") => tree::Overflow::Hidden, + Some("scroll") => tree::Overflow::Scroll, + Some("auto") => tree::Overflow::Auto, + _ => tree::Overflow::Hidden, + } +} + +fn convert_orientation(attrs: &svgdom::Attributes) -> tree::MarkerOrientation { + match attrs.get_value(AId::Orient) { + Some(AValue::Angle(angle)) => { + let a = match angle.unit { + svgdom::AngleUnit::Degrees => angle.num, + svgdom::AngleUnit::Gradians => angle.num * 180.0 / 200.0, + svgdom::AngleUnit::Radians => angle.num * 180.0 / std::f64::consts::PI, + }; + + tree::MarkerOrientation::Angle(a) + } + Some(AValue::String(s)) if s == "auto" => { + tree::MarkerOrientation::Auto + } + _ => tree::MarkerOrientation::Angle(0.0), + } +} diff --git a/usvg/src/convert/mod.rs b/usvg/src/convert/mod.rs index c37ba8e..5a07eba 100644 --- a/usvg/src/convert/mod.rs +++ b/usvg/src/convert/mod.rs @@ -33,6 +33,7 @@ mod fill; mod filter; mod gradient; mod image; +mod marker; mod mask; mod path; mod pattern; @@ -154,6 +155,11 @@ fn convert_ref_nodes( later_nodes.push((node, new_node)); } } + EId::Marker => { + if let Some(new_node) = marker::convert(&node, tree) { + later_nodes.push((node, new_node)); + } + } EId::Filter => { filter::convert(&node, opt, tree); } @@ -185,6 +191,13 @@ fn convert_ref_nodes( warn!("Mask '{}' has no children. Skipped.", node.id()); new_node.detach(); } + } else if node.is_tag_name(EId::Marker) { + convert_nodes(&node, &mut new_node, opt, tree); + + if !new_node.has_children() { + warn!("Marker '{}' has no children. Skipped.", node.id()); + new_node.detach(); + } } else if node.is_tag_name(EId::Pattern) { convert_nodes(&node, &mut new_node.clone(), opt, tree); diff --git a/usvg/src/convert/path.rs b/usvg/src/convert/path.rs index 6104db3..4841fd8 100644 --- a/usvg/src/convert/path.rs +++ b/usvg/src/convert/path.rs @@ -36,24 +36,26 @@ pub fn convert( let transform = attrs.get_transform(AId::Transform).unwrap_or_default(); let mut visibility = super::convert_visibility(&attrs); - // Shapes without a bbox cannot be filled, - // and if there is no stroke than there is nothing to render. - if !has_bbox && stroke.is_none() { - return; - } - // If a path doesn't have a fill or a stroke than it's invisible. // By setting `visibility` to `hidden` we are disabling the rendering of this path. if fill.is_none() && stroke.is_none() { visibility = tree::Visibility::Hidden } + let marker = Box::new(tree::PathMarker { + start: conv_marker(AId::MarkerStart, node, tree), + mid: conv_marker(AId::MarkerMid, node, tree), + end: conv_marker(AId::MarkerEnd, node, tree), + stroke: conv_stroke_width(&attrs), + }); + parent.append_kind(tree::NodeKind::Path(tree::Path { id: node.id().clone(), transform, visibility, fill, stroke, + marker, segments: d, })); } @@ -303,3 +305,30 @@ fn has_bbox(segments: &[tree::PathSegment]) -> bool { false } + +fn conv_marker( + aid: AId, + node: &svgdom::Node, + tree: &tree::Tree, +) -> Option<String> { + let attrs = node.attributes(); + if let Some(&AValue::FuncLink(ref link)) = attrs.get_type(aid) { + if link.is_tag_name(EId::Marker) { + if let Some(node) = tree.defs_by_id(&link.id()) { + return Some(node.id().to_string()); + } + } + } + + None +} + +fn conv_stroke_width(attrs: &svgdom::Attributes) -> Option<tree::StrokeWidth> { + let width = attrs.get_number_or(AId::StrokeWidth, 1.0); + + if !(width > 0.0) { + return None; + } + + Some(tree::StrokeWidth::new(width)) +} diff --git a/usvg/src/convert/stroke.rs b/usvg/src/convert/stroke.rs index eb6dc98..12b41ab 100644 --- a/usvg/src/convert/stroke.rs +++ b/usvg/src/convert/stroke.rs @@ -15,7 +15,7 @@ pub fn convert( attrs: &svgdom::Attributes, has_bbox: bool, ) -> Option<tree::Stroke> { - let dashoffset = attrs.get_number_or(AId::StrokeDashoffset, 0.0); + let dashoffset = attrs.get_number_or(AId::StrokeDashoffset, 0.0) as f32; let miterlimit = attrs.get_number_or(AId::StrokeMiterlimit, 4.0); let opacity = attrs.get_number_or(AId::StrokeOpacity, 1.0).into(); let width = attrs.get_number_or(AId::StrokeWidth, 1.0); diff --git a/usvg/src/geom.rs b/usvg/src/geom.rs index 6ef4b6f..a0ecdbc 100644 --- a/usvg/src/geom.rs +++ b/usvg/src/geom.rs @@ -192,7 +192,7 @@ impl Rect { self.y + self.height } - /// Checks that rect contains a point. + /// Checks that the rect contains a point. pub fn contains(&self, p: Point) -> bool { if p.x < self.x || p.x > self.x + self.width - 1.0 { return false; @@ -204,6 +204,11 @@ impl Rect { true } + + /// Checks that the rect has a valid size. + pub fn is_valid(&self) -> bool { + self.width > 0.0 && self.height > 0.0 + } } impl FuzzyEq for Rect { diff --git a/usvg/src/preproc/fix_recursive_links.rs b/usvg/src/preproc/fix_recursive_links.rs index c78319b..a86063e 100644 --- a/usvg/src/preproc/fix_recursive_links.rs +++ b/usvg/src/preproc/fix_recursive_links.rs @@ -7,6 +7,7 @@ use super::prelude::*; pub fn fix_recursive_links(doc: &Document) { fix_pattern(doc); + fix_marker(doc); fix_func_iri(doc, EId::ClipPath, AId::ClipPath); fix_func_iri(doc, EId::Mask, AId::Mask); fix_func_iri(doc, EId::Filter, AId::Filter); @@ -43,6 +44,38 @@ fn fix_pattern(doc: &Document) { } } +fn fix_marker(doc: &Document) { + for marker_node in doc.root().descendants().filter(|n| n.is_tag_name(EId::Marker)) { + for mut node in marker_node.descendants() { + let mut check_attr = |aid: AId| { + let av = node.attributes().get_value(aid).cloned(); + if let Some(AValue::FuncLink(link)) = av { + if link == marker_node { + // If a marker child has a link to the marker itself + // then we have to remove it. + // Otherwise we will get endless loop/recursion and stack overflow. + node.remove_attribute(aid); + } else { + // Check that linked node children doesn't link this marker. + for node2 in link.descendants() { + let av2 = node2.attributes().get_value(aid).cloned(); + if let Some(AValue::FuncLink(link2)) = av2 { + if link2 == marker_node { + node.remove_attribute(aid); + } + } + } + } + } + }; + + check_attr(AId::MarkerStart); + check_attr(AId::MarkerMid); + check_attr(AId::MarkerEnd); + } + } +} + fn fix_func_iri(doc: &Document, eid: EId, aid: AId) { for node in doc.root().descendants().filter(|n| n.is_tag_name(eid)) { for mut child in node.descendants() { diff --git a/usvg/src/preproc/mod.rs b/usvg/src/preproc/mod.rs index 7757d76..e43e103 100644 --- a/usvg/src/preproc/mod.rs +++ b/usvg/src/preproc/mod.rs @@ -42,6 +42,7 @@ mod rm_non_svg_data; mod rm_unused_defs; mod ungroup_a; mod ungroup_groups; +mod prepare_marker; use self::conv_units::*; @@ -74,6 +75,7 @@ use self::rm_non_svg_data::*; use self::rm_unused_defs::*; use self::ungroup_a::*; use self::ungroup_groups::*; +use self::prepare_marker::*; mod prelude { @@ -178,6 +180,8 @@ pub fn prepare_doc(doc: &mut svgdom::Document, opt: &Options) { prepare_text_decoration(doc); resolve_style_attributes(doc, opt); + rm_marker_attributes(doc); + // Should be done only after style resolving. remove_invalid_gradients(doc); diff --git a/usvg/src/preproc/prepare_marker.rs b/usvg/src/preproc/prepare_marker.rs new file mode 100644 index 0000000..1278919 --- /dev/null +++ b/usvg/src/preproc/prepare_marker.rs @@ -0,0 +1,35 @@ +// 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/. + +use super::prelude::*; + + +/// Removes marker attributes from unsupported elements. +/// +/// `marker-*` attributes can only be set on `path`, `line`, `polyline` and `polygon`. +/// +/// Also, `marker-*` attributes cannot be set on shapes inside the `clipPath`. +pub fn rm_marker_attributes(doc: &Document) { + for mut node in doc.root().descendants() { + let is_valid_elem = + node.is_tag_name(EId::Path) + || node.is_tag_name(EId::Line) + || node.is_tag_name(EId::Polyline) + || node.is_tag_name(EId::Polygon); + + if !is_valid_elem { + node.remove_attribute(AId::MarkerStart); + node.remove_attribute(AId::MarkerMid); + node.remove_attribute(AId::MarkerEnd); + } + } + + for node in doc.root().descendants().filter(|n| n.is_tag_name(EId::ClipPath)) { + for mut child in node.descendants() { + child.remove_attribute(AId::MarkerStart); + child.remove_attribute(AId::MarkerMid); + child.remove_attribute(AId::MarkerEnd); + } + } +} diff --git a/usvg/src/preproc/resolve_style_attrs.rs b/usvg/src/preproc/resolve_style_attrs.rs index d1bbac8..7ff0e10 100644 --- a/usvg/src/preproc/resolve_style_attrs.rs +++ b/usvg/src/preproc/resolve_style_attrs.rs @@ -38,6 +38,12 @@ fn resolve_inherit(parent: &Node, opt: &Options) { resolve(&mut node, AId::StrokeWidth); } + if node.is_shape() { + resolve(&mut node, AId::MarkerStart); + resolve(&mut node, AId::MarkerMid); + resolve(&mut node, AId::MarkerEnd); + } + if node.is_graphic() && node.parent().unwrap().is_tag_name(EId::ClipPath) { resolve(&mut node, AId::ClipRule); } diff --git a/usvg/src/tree/attributes.rs b/usvg/src/tree/attributes.rs index c38971c..516bc76 100644 --- a/usvg/src/tree/attributes.rs +++ b/usvg/src/tree/attributes.rs @@ -57,8 +57,6 @@ pub enum FillRule { /// An element units. -/// -/// `*Units` attribute in the SVG. #[allow(missing_docs)] #[derive(Clone, Copy, PartialEq, Debug)] pub enum Units { @@ -67,6 +65,26 @@ pub enum Units { } +/// A marker units. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum MarkerUnits { + StrokeWidth, + UserSpaceOnUse, +} + + +/// A marker orientation. +#[derive(Clone, Copy, Debug)] +pub enum MarkerOrientation { + /// Requires an automatic rotation. + Auto, + + /// A rotation angle in degrees. + Angle(f64), +} + + /// A spread method. /// /// `spreadMethod` attribute in the SVG. @@ -91,6 +109,30 @@ pub enum Visibility { } +/// An overflow property. +/// +/// `overflow` attribute in the SVG. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum Overflow { + Visible, + Hidden, + Scroll, + Auto, +} + +impl ToString for Overflow { + fn to_string(&self) -> String { + match self { + Overflow::Visible => "visible", + Overflow::Hidden => "hidden", + Overflow::Scroll => "scroll", + Overflow::Auto => "auto", + }.to_string() + } +} + + /// A text decoration style. /// /// Defines the style of the line that should be rendered. @@ -263,7 +305,7 @@ impl Default for Fill { pub struct Stroke { pub paint: Paint, pub dasharray: Option<NumberList>, - pub dashoffset: f64, + pub dashoffset: f32, pub miterlimit: StrokeMiterlimit, pub opacity: Opacity, pub width: StrokeWidth, @@ -327,8 +369,8 @@ pub struct ViewBox { /// A path absolute segment. /// -/// Unlike the SVG spec can contain only `M`, `L`, `C` and `Z` segments. -/// All other segments will be converted to this one. +/// Unlike the SVG spec, can contain only `M`, `L`, `C` and `Z` segments. +/// All other segments will be converted into this one. #[allow(missing_docs)] #[derive(Clone, Copy, Debug)] pub enum PathSegment { @@ -510,3 +552,31 @@ pub enum BaselineShift { Percent(f64), Number(f64), } + + +/// A path marker properties. +#[derive(Clone, Debug)] +pub struct PathMarker { + /// Start marker. + /// + /// `marker-start` in SVG. + pub start: Option<String>, + + /// Middle marker + /// + /// `marker-mid` in SVG. + pub mid: Option<String>, + + /// End marker + /// + /// `marker-end` in SVG. + pub end: Option<String>, + + /// Marker stroke. + /// + /// This value contains a copy of the `stroke-width` value. + /// `usvg` will set `Path::stroke` to `None` if a path doesn't have a stroke, + /// but marker rendering still relies on the `stroke-width` value, even when `stroke=none`. + /// So we have to store it separately. + pub stroke: Option<StrokeWidth>, +} diff --git a/usvg/src/tree/convert.rs b/usvg/src/tree/convert.rs index 9de1431..28e13c8 100644 --- a/usvg/src/tree/convert.rs +++ b/usvg/src/tree/convert.rs @@ -123,6 +123,38 @@ fn conv_defs( conv_transform(AId::PatternTransform, &pattern.transform, &mut pattern_elem); later_nodes.push((n.clone(), pattern_elem.clone())); } + NodeKind::Marker(ref marker) => { + let mut marker_elem = new_doc.create_element(EId::Marker); + defs.append(marker_elem.clone()); + + marker_elem.set_id(marker.id.clone()); + + marker_elem.set_attribute((AId::MarkerUnits, + match marker.units { + MarkerUnits::UserSpaceOnUse => "userSpaceOnUse", + MarkerUnits::StrokeWidth => "strokeWidth", + } + )); + + marker_elem.set_attribute((AId::RefX, marker.rect.x)); + marker_elem.set_attribute((AId::RefY, marker.rect.y)); + marker_elem.set_attribute((AId::MarkerWidth, marker.rect.width)); + marker_elem.set_attribute((AId::MarkerHeight, marker.rect.height)); + + if let Some(vbox) = marker.view_box { + conv_viewbox(&vbox, &mut marker_elem); + } + + let orientation: AValue = match marker.orientation { + MarkerOrientation::Auto => "auto".into(), + MarkerOrientation::Angle(a) => a.into(), + }; + marker_elem.set_attribute((AId::Orient, orientation)); + + marker_elem.set_attribute((AId::Overflow, marker.overflow.to_string())); + + later_nodes.push((n.clone(), marker_elem.clone())); + } NodeKind::Filter(ref filter) => { let mut filter_elem = new_doc.create_element(EId::Filter); defs.append(filter_elem.clone()); @@ -302,6 +334,31 @@ fn conv_elements( conv_fill(tree, &p.fill, defs, parent, &mut path_elem); conv_stroke(tree, &p.stroke, defs, &mut path_elem); + + if let Some(ref id) = p.marker.start { + conv_link(tree, defs, AId::MarkerStart, id, &mut path_elem); + } + + if let Some(ref id) = p.marker.mid { + conv_link(tree, defs, AId::MarkerMid, id, &mut path_elem); + } + + if let Some(ref id) = p.marker.end { + conv_link(tree, defs, AId::MarkerEnd, id, &mut path_elem); + } + + // Set `stroke-width` if path has a marker. + // Even if `stroke` is not set, the `stroke-width` attribute + // will still affect the marker rendering. + let has_marker = p.marker.start.is_some() + || p.marker.mid.is_some() + || p.marker.end.is_some(); + + if p.stroke.is_none() && has_marker { + if let Some(sw) = p.marker.stroke { + path_elem.set_attribute((AId::StrokeWidth, sw.value())); + } + } } NodeKind::Text(ref text) => { let mut text_elem = new_doc.create_element(EId::Text); @@ -503,7 +560,7 @@ fn conv_stroke( } node.set_attribute((AId::StrokeOpacity, stroke.opacity.value())); - node.set_attribute((AId::StrokeDashoffset, stroke.dashoffset)); + node.set_attribute((AId::StrokeDashoffset, stroke.dashoffset as f64)); node.set_attribute((AId::StrokeMiterlimit, stroke.miterlimit.value())); node.set_attribute((AId::StrokeWidth, stroke.width.value())); diff --git a/usvg/src/tree/nodes.rs b/usvg/src/tree/nodes.rs index 8f6e4ac..d4f8af9 100644 --- a/usvg/src/tree/nodes.rs +++ b/usvg/src/tree/nodes.rs @@ -20,6 +20,7 @@ pub enum NodeKind { ClipPath(ClipPath), Mask(Mask), Pattern(Pattern), + Marker(Marker), Filter(Filter), Path(Path), Text(Text), @@ -41,6 +42,7 @@ impl NodeKind { NodeKind::ClipPath(ref e) => e.id.as_str(), NodeKind::Mask(ref e) => e.id.as_str(), NodeKind::Pattern(ref e) => e.id.as_str(), + NodeKind::Marker(ref e) => e.id.as_str(), NodeKind::Filter(ref e) => e.id.as_str(), NodeKind::Path(ref e) => e.id.as_str(), NodeKind::Text(ref e) => e.id.as_str(), @@ -62,6 +64,7 @@ impl NodeKind { NodeKind::ClipPath(ref e) => e.transform, NodeKind::Mask(_) => Transform::default(), NodeKind::Pattern(ref e) => e.transform, + NodeKind::Marker(_) => Transform::default(), NodeKind::Filter(_) => Transform::default(), NodeKind::Path(ref e) => e.transform, NodeKind::Text(ref e) => e.transform, @@ -113,6 +116,9 @@ pub struct Path { /// Stroke style. pub stroke: Option<Stroke>, + /// Marker. + pub marker: Box<PathMarker>, + /// Segments list. /// /// All segments are in absolute coordinates. @@ -485,6 +491,42 @@ pub struct Pattern { } +/// A marker element. +/// +/// `marker` element in SVG. +#[derive(Clone, Debug)] +pub struct Marker { + /// Element's ID. + /// + /// Taken from the SVG itself. + /// Can't be empty. + pub id: String, + + /// Coordinate system units. + /// + /// `markerUnits` in SVG. + pub units: MarkerUnits, + + /// Marker rectangle. + /// + /// `refX`, `refY`, `markerWidth` and `markerHeight` in SVG. + pub rect: Rect, + + /// Marker viewbox. + pub view_box: Option<ViewBox>, + + /// Marker orientation. + /// + /// `orient` in SVG. + pub orientation: MarkerOrientation, + + /// Marker overflow. + /// + /// `overflow` in SVG. + pub overflow: Overflow, +} + + /// A filter element. /// /// `filter` element in the SVG. diff --git a/usvg/testing_tools/allow.csv b/usvg/testing_tools/allow.csv index 677e80b..1869858 100644 --- a/usvg/testing_tools/allow.csv +++ b/usvg/testing_tools/allow.csv @@ -20,6 +20,8 @@ a-letter-spacing-005,2171,chrome bug a-word-spacing-005,730,chrome bug a-opacity-001,580 a-opacity-002,137 +a-overflow-001,299 +a-overflow-003,299 a-stroke-dasharray-012,244 a-stroke-width-004,960,chrome bug a-systemLanguage-001,25604,chrome bug @@ -86,6 +88,9 @@ e-image-028,22690,bug #13 e-image-029,24795,bug #13 e-image-030,18864,bug #13 e-image-031,18864,bug #13 +e-marker-015,846 +e-marker-030,704 +e-marker-035,1155,bug e-mask-015,38 e-mask-017,22344,bug #14 e-mask-020,12804,bug diff --git a/usvg/testing_tools/cache.csv b/usvg/testing_tools/cache.csv index 3deb23d..2681c2b 100644 --- a/usvg/testing_tools/cache.csv +++ b/usvg/testing_tools/cache.csv @@ -126,12 +126,12 @@ a-stroke-006,cdb0324b a-stroke-007,35ba0648
a-stroke-008,bf48479d
a-stroke-009,e9fc55d3
-a-stroke-010,22e1c426
+a-stroke-010,765346d4
a-stroke-011,3e34be29
-a-stroke-012,01e50923
+a-stroke-012,4a0cc7cb
a-stroke-013,1e292886
-a-stroke-014,aa0f5699
-a-stroke-015,aa0f5699
+a-stroke-014,7b219da8
+a-stroke-015,47644d90
a-stroke-dasharray-001,6f36c050
a-stroke-dasharray-002,bd2f3486
a-stroke-dasharray-003,bd2f3486
@@ -146,7 +146,7 @@ a-stroke-dasharray-011,c0ffdc35 a-stroke-dasharray-012,018df324
a-stroke-dashoffset-001,bd2f3486
a-stroke-dashoffset-002,21be0853
-a-stroke-dashoffset-003,109e1fc8
+a-stroke-dashoffset-003,52db6329
a-stroke-dashoffset-004,dbce2c47
a-stroke-dashoffset-005,2365f1b2
a-stroke-dashoffset-006,64093725
@@ -546,12 +546,12 @@ e-path-030,19efd46c e-path-031,2cb8c8b6
e-path-032,7f149c64
e-path-033,405d6486
-e-path-034,2739396f
-e-path-035,84a79cbb
+e-path-034,ef582b10
+e-path-035,fd0d0d04
e-path-036,bde418bf
-e-path-037,050496f5
-e-path-038,a8d5218b
-e-path-039,050496f5
+e-path-037,46015de7
+e-path-038,daacdeed
+e-path-039,46015de7
e-path-040,69b6f2ed
e-path-041,69b6f2ed
e-path-042,6fa3fd25
@@ -565,9 +565,9 @@ e-pattern-005,c7498dda e-pattern-006,030f0964
e-pattern-007,9f4e924c
e-pattern-008,fae06cc3
-e-pattern-009,6c78ecb7
-e-pattern-010,494eec45
-e-pattern-011,f52b6448
+e-pattern-009,e5617a38
+e-pattern-010,ca8d710d
+e-pattern-011,938e478d
e-pattern-012,ed4e93e6
e-pattern-013,ed4e93e6
e-pattern-014,745d0dd7
@@ -864,3 +864,66 @@ a-word-spacing-003,b12e5267 a-word-spacing-004,7ffef6bd
a-word-spacing-005,7c3f6f89
a-word-spacing-006,ec89c95f
+a-marker-end-001,83157013
+a-marker-mid-001,b0968f0f
+a-marker-start-001,aaf419ba
+a-overflow-001,fcb11a00
+a-overflow-002,2e6595da
+a-overflow-003,637a1b8b
+e-clipPath-038,bcf9789e
+e-marker-001,685c8215
+e-marker-002,ac86ce31
+e-marker-003,2602ed72
+e-marker-004,ac86ce31
+e-marker-005,139b75d6
+e-marker-006,6a958e47
+e-marker-007,650df3d2
+e-marker-008,4bc7d273
+e-marker-009,972c821f
+e-marker-010,1eae734a
+e-marker-011,8e98192b
+e-marker-012,4cb167b4
+e-marker-013,e528c1e3
+e-marker-014,0c04cc45
+e-marker-015,a8afae9f
+e-marker-016,b8249769
+e-marker-017,559cc288
+e-marker-018,a11e3b08
+e-marker-019,1aed08b9
+e-marker-020,dc2a311c
+e-marker-021,dc2a311c
+e-marker-022,a7ec8a6e
+e-marker-023,8e98192b
+e-marker-024,8e98192b
+e-marker-025,8e98192b
+e-marker-026,8e98192b
+e-marker-027,8e98192b
+e-marker-028,dc2a311c
+e-marker-029,dc2a311c
+e-marker-030,ac427a97
+e-marker-031,178b4f7f
+e-marker-032,ec96b09d
+e-marker-033,08ee7de2
+e-marker-034,e59f062d
+e-marker-035,bfdd5d9d
+e-marker-036,15c61b20
+e-marker-037,1e5caea2
+e-marker-038,ee9b5611
+e-marker-039,81d1c0f6
+e-marker-040,35629b1d
+e-marker-041,bc3ebb35
+e-marker-042,74464750
+e-marker-043,020752cd
+e-marker-044,bb3232f1
+e-marker-045,bf6a8252
+e-marker-046,afe1f6c5
+e-marker-047,4aac76a6
+e-marker-048,4085e7c8
+e-marker-049,d99e4f75
+e-marker-050,ec050c14
+e-marker-051,5ddafdc5
+e-marker-052,d4f6f60b
+e-marker-053,83702e9d
+e-marker-054,fda46a82
+e-marker-055,d7c0f54a
+e-marker-056,b8f5cf2e
|