Add basic, incomplete support for clip paths in SVG
This commit is contained in:
parent
677c607a8c
commit
243f2cc9b2
|
@ -1779,6 +1779,7 @@ name = "pathfinder_svg"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"hashbrown 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"pathfinder_color 0.1.0",
|
"pathfinder_color 0.1.0",
|
||||||
"pathfinder_content 0.1.0",
|
"pathfinder_content 0.1.0",
|
||||||
"pathfinder_geometry 0.4.0",
|
"pathfinder_geometry 0.4.0",
|
||||||
|
|
|
@ -6,6 +6,7 @@ authors = ["Patrick Walton <pcwalton@mimiga.net>"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bitflags = "1.0"
|
bitflags = "1.0"
|
||||||
|
hashbrown = "0.7"
|
||||||
usvg = "0.9"
|
usvg = "0.9"
|
||||||
|
|
||||||
[dependencies.pathfinder_color]
|
[dependencies.pathfinder_color]
|
||||||
|
|
129
svg/src/lib.rs
129
svg/src/lib.rs
|
@ -13,6 +13,7 @@
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate bitflags;
|
extern crate bitflags;
|
||||||
|
|
||||||
|
use hashbrown::HashMap;
|
||||||
use pathfinder_color::ColorU;
|
use pathfinder_color::ColorU;
|
||||||
use pathfinder_content::outline::Outline;
|
use pathfinder_content::outline::Outline;
|
||||||
use pathfinder_content::segment::{Segment, SegmentFlags};
|
use pathfinder_content::segment::{Segment, SegmentFlags};
|
||||||
|
@ -23,7 +24,7 @@ use pathfinder_geometry::rect::RectF;
|
||||||
use pathfinder_geometry::transform2d::Transform2F;
|
use pathfinder_geometry::transform2d::Transform2F;
|
||||||
use pathfinder_geometry::vector::Vector2F;
|
use pathfinder_geometry::vector::Vector2F;
|
||||||
use pathfinder_renderer::paint::Paint;
|
use pathfinder_renderer::paint::Paint;
|
||||||
use pathfinder_renderer::scene::{DrawPath, Scene};
|
use pathfinder_renderer::scene::{ClipPath, ClipPathId, DrawPath, Scene};
|
||||||
use std::fmt::{Display, Formatter, Result as FormatResult};
|
use std::fmt::{Display, Formatter, Result as FormatResult};
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use usvg::{Color as SvgColor, LineCap as UsvgLineCap, LineJoin as UsvgLineJoin, Node, NodeExt};
|
use usvg::{Color as SvgColor, LineCap as UsvgLineCap, LineJoin as UsvgLineJoin, Node, NodeExt};
|
||||||
|
@ -35,6 +36,7 @@ const HAIRLINE_STROKE_WIDTH: f32 = 0.0333;
|
||||||
pub struct BuiltSVG {
|
pub struct BuiltSVG {
|
||||||
pub scene: Scene,
|
pub scene: Scene,
|
||||||
pub result_flags: BuildResultFlags,
|
pub result_flags: BuildResultFlags,
|
||||||
|
pub clip_paths: HashMap<String, ClipPathId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
|
@ -62,11 +64,12 @@ bitflags! {
|
||||||
impl BuiltSVG {
|
impl BuiltSVG {
|
||||||
// TODO(pcwalton): Allow a global transform to be set.
|
// TODO(pcwalton): Allow a global transform to be set.
|
||||||
pub fn from_tree(tree: Tree) -> BuiltSVG {
|
pub fn from_tree(tree: Tree) -> BuiltSVG {
|
||||||
let global_transform = Transform2F::default();
|
// TODO(pcwalton): Maybe have a `SVGBuilder` type to hold the clip path IDs and other
|
||||||
|
// transient data separate from `BuiltSVG`?
|
||||||
let mut built_svg = BuiltSVG {
|
let mut built_svg = BuiltSVG {
|
||||||
scene: Scene::new(),
|
scene: Scene::new(),
|
||||||
result_flags: BuildResultFlags::empty(),
|
result_flags: BuildResultFlags::empty(),
|
||||||
|
clip_paths: HashMap::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let root = &tree.root();
|
let root = &tree.root();
|
||||||
|
@ -74,7 +77,7 @@ impl BuiltSVG {
|
||||||
NodeKind::Svg(ref svg) => {
|
NodeKind::Svg(ref svg) => {
|
||||||
built_svg.scene.set_view_box(usvg_rect_to_euclid_rect(&svg.view_box.rect));
|
built_svg.scene.set_view_box(usvg_rect_to_euclid_rect(&svg.view_box.rect));
|
||||||
for kid in root.children() {
|
for kid in root.children() {
|
||||||
built_svg.process_node(&kid, &global_transform);
|
built_svg.process_node(&kid, &State::new(), &mut None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
|
@ -87,16 +90,16 @@ impl BuiltSVG {
|
||||||
built_svg
|
built_svg
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_node(&mut self, node: &Node, transform: &Transform2F) {
|
fn process_node(&mut self,
|
||||||
|
node: &Node,
|
||||||
|
state: &State,
|
||||||
|
clip_outline: &mut Option<Outline>) {
|
||||||
|
let mut state = (*state).clone();
|
||||||
let node_transform = usvg_transform_to_transform_2d(&node.transform());
|
let node_transform = usvg_transform_to_transform_2d(&node.transform());
|
||||||
let transform = node_transform * *transform;
|
state.transform = node_transform * state.transform;
|
||||||
|
|
||||||
match *node.borrow() {
|
match *node.borrow() {
|
||||||
NodeKind::Group(ref group) => {
|
NodeKind::Group(ref group) => {
|
||||||
if group.clip_path.is_some() {
|
|
||||||
self.result_flags
|
|
||||||
.insert(BuildResultFlags::UNSUPPORTED_CLIP_PATH_ATTR);
|
|
||||||
}
|
|
||||||
if group.filter.is_some() {
|
if group.filter.is_some() {
|
||||||
self.result_flags
|
self.result_flags
|
||||||
.insert(BuildResultFlags::UNSUPPORTED_FILTER_ATTR);
|
.insert(BuildResultFlags::UNSUPPORTED_FILTER_ATTR);
|
||||||
|
@ -106,34 +109,35 @@ impl BuiltSVG {
|
||||||
.insert(BuildResultFlags::UNSUPPORTED_MASK_ATTR);
|
.insert(BuildResultFlags::UNSUPPORTED_MASK_ATTR);
|
||||||
}
|
}
|
||||||
|
|
||||||
for kid in node.children() {
|
if let Some(ref clip_path_name) = group.clip_path {
|
||||||
self.process_node(&kid, &transform)
|
if let Some(clip_path_id) = self.clip_paths.get(clip_path_name) {
|
||||||
|
// TODO(pcwalton): Combine multiple clip paths if there's already one.
|
||||||
|
state.clip_path = Some(*clip_path_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NodeKind::Path(ref path) if path.visibility == Visibility::Visible => {
|
|
||||||
if let Some(ref fill) = path.fill {
|
|
||||||
let style = self.scene.push_paint(&Paint::from_svg_paint(
|
|
||||||
&fill.paint,
|
|
||||||
fill.opacity,
|
|
||||||
&mut self.result_flags,
|
|
||||||
));
|
|
||||||
|
|
||||||
|
for kid in node.children() {
|
||||||
|
self.process_node(&kid, &state, clip_outline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NodeKind::Path(ref path) if state.path_destination == PathDestination::Clip => {
|
||||||
|
// TODO(pcwalton): Multiple clip paths.
|
||||||
let path = UsvgPathToSegments::new(path.data.iter().cloned());
|
let path = UsvgPathToSegments::new(path.data.iter().cloned());
|
||||||
let path = Transform2FPathIter::new(path, &transform);
|
let path = Transform2FPathIter::new(path, &state.transform);
|
||||||
|
*clip_outline = Some(Outline::from_segments(path));
|
||||||
|
}
|
||||||
|
NodeKind::Path(ref path) if state.path_destination == PathDestination::Draw &&
|
||||||
|
path.visibility == Visibility::Visible => {
|
||||||
|
if let Some(ref fill) = path.fill {
|
||||||
|
let path = UsvgPathToSegments::new(path.data.iter().cloned());
|
||||||
|
let path = Transform2FPathIter::new(path, &state.transform);
|
||||||
let outline = Outline::from_segments(path);
|
let outline = Outline::from_segments(path);
|
||||||
|
|
||||||
// TODO(pcwalton): Clip paths.
|
|
||||||
let name = format!("Fill({})", node.id());
|
let name = format!("Fill({})", node.id());
|
||||||
self.scene.push_path(DrawPath::new(outline, style, None, name));
|
self.push_draw_path(outline, name, &state, &fill.paint, fill.opacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref stroke) = path.stroke {
|
if let Some(ref stroke) = path.stroke {
|
||||||
let style = self.scene.push_paint(&Paint::from_svg_paint(
|
|
||||||
&stroke.paint,
|
|
||||||
stroke.opacity,
|
|
||||||
&mut self.result_flags,
|
|
||||||
));
|
|
||||||
|
|
||||||
let stroke_style = StrokeStyle {
|
let stroke_style = StrokeStyle {
|
||||||
line_width: f32::max(stroke.width.value() as f32, HAIRLINE_STROKE_WIDTH),
|
line_width: f32::max(stroke.width.value() as f32, HAIRLINE_STROKE_WIDTH),
|
||||||
line_cap: LineCap::from_usvg_line_cap(stroke.linecap),
|
line_cap: LineCap::from_usvg_line_cap(stroke.linecap),
|
||||||
|
@ -147,22 +151,32 @@ impl BuiltSVG {
|
||||||
let mut stroke_to_fill = OutlineStrokeToFill::new(&outline, stroke_style);
|
let mut stroke_to_fill = OutlineStrokeToFill::new(&outline, stroke_style);
|
||||||
stroke_to_fill.offset();
|
stroke_to_fill.offset();
|
||||||
let mut outline = stroke_to_fill.into_outline();
|
let mut outline = stroke_to_fill.into_outline();
|
||||||
outline.transform(&transform);
|
outline.transform(&state.transform);
|
||||||
|
|
||||||
// TODO(pcwalton): Clip paths.
|
|
||||||
let name = format!("Stroke({})", node.id());
|
let name = format!("Stroke({})", node.id());
|
||||||
self.scene.push_path(DrawPath::new(outline, style, None, name));
|
self.push_draw_path(outline, name, &state, &stroke.paint, stroke.opacity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NodeKind::Path(..) => {}
|
NodeKind::Path(..) => {}
|
||||||
NodeKind::ClipPath(..) => {
|
NodeKind::ClipPath(_) => {
|
||||||
self.result_flags
|
let mut clip_outline = None;
|
||||||
.insert(BuildResultFlags::UNSUPPORTED_CLIP_PATH_NODE);
|
state.path_destination = PathDestination::Clip;
|
||||||
|
for kid in node.children() {
|
||||||
|
self.process_node(&kid, &state, &mut clip_outline);
|
||||||
}
|
}
|
||||||
NodeKind::Defs { .. } => {
|
|
||||||
if node.has_children() {
|
if let Some(clip_outline) = clip_outline {
|
||||||
self.result_flags
|
let name = format!("ClipPath({})", node.id());
|
||||||
.insert(BuildResultFlags::UNSUPPORTED_DEFS_NODE);
|
let clip_path = ClipPath::new(clip_outline, name);
|
||||||
|
let clip_path_id = self.scene.push_clip_path(clip_path);
|
||||||
|
self.clip_paths.insert(node.id().to_owned(), clip_path_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NodeKind::Defs => {
|
||||||
|
// FIXME(pcwalton): This is wrong.
|
||||||
|
state.path_destination = PathDestination::Defs;
|
||||||
|
for kid in node.children() {
|
||||||
|
self.process_node(&kid, &state, clip_outline);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NodeKind::Filter(..) => {
|
NodeKind::Filter(..) => {
|
||||||
|
@ -195,6 +209,18 @@ impl BuiltSVG {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn push_draw_path(&mut self,
|
||||||
|
outline: Outline,
|
||||||
|
name: String,
|
||||||
|
state: &State,
|
||||||
|
paint: &UsvgPaint,
|
||||||
|
opacity: Opacity) {
|
||||||
|
let style = self.scene.push_paint(&Paint::from_svg_paint(paint,
|
||||||
|
opacity,
|
||||||
|
&mut self.result_flags));
|
||||||
|
self.scene.push_path(DrawPath::new(outline, style, state.clip_path, name));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for BuildResultFlags {
|
impl Display for BuildResultFlags {
|
||||||
|
@ -408,3 +434,30 @@ impl LineJoinExt for LineJoin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct State {
|
||||||
|
// Where paths are being appended to.
|
||||||
|
path_destination: PathDestination,
|
||||||
|
// The current transform.
|
||||||
|
transform: Transform2F,
|
||||||
|
// The current clip path in effect.
|
||||||
|
clip_path: Option<ClipPathId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
fn new() -> State {
|
||||||
|
State {
|
||||||
|
path_destination: PathDestination::Draw,
|
||||||
|
transform: Transform2F::default(),
|
||||||
|
clip_path: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||||
|
enum PathDestination {
|
||||||
|
Draw,
|
||||||
|
Defs,
|
||||||
|
Clip,
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue