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"
|
||||
dependencies = [
|
||||
"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_content 0.1.0",
|
||||
"pathfinder_geometry 0.4.0",
|
||||
|
|
|
@ -6,6 +6,7 @@ authors = ["Patrick Walton <pcwalton@mimiga.net>"]
|
|||
|
||||
[dependencies]
|
||||
bitflags = "1.0"
|
||||
hashbrown = "0.7"
|
||||
usvg = "0.9"
|
||||
|
||||
[dependencies.pathfinder_color]
|
||||
|
|
127
svg/src/lib.rs
127
svg/src/lib.rs
|
@ -13,6 +13,7 @@
|
|||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
|
||||
use hashbrown::HashMap;
|
||||
use pathfinder_color::ColorU;
|
||||
use pathfinder_content::outline::Outline;
|
||||
use pathfinder_content::segment::{Segment, SegmentFlags};
|
||||
|
@ -23,7 +24,7 @@ use pathfinder_geometry::rect::RectF;
|
|||
use pathfinder_geometry::transform2d::Transform2F;
|
||||
use pathfinder_geometry::vector::Vector2F;
|
||||
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::mem;
|
||||
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 scene: Scene,
|
||||
pub result_flags: BuildResultFlags,
|
||||
pub clip_paths: HashMap<String, ClipPathId>,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
|
@ -62,11 +64,12 @@ bitflags! {
|
|||
impl BuiltSVG {
|
||||
// TODO(pcwalton): Allow a global transform to be set.
|
||||
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 {
|
||||
scene: Scene::new(),
|
||||
result_flags: BuildResultFlags::empty(),
|
||||
clip_paths: HashMap::new(),
|
||||
};
|
||||
|
||||
let root = &tree.root();
|
||||
|
@ -74,7 +77,7 @@ impl BuiltSVG {
|
|||
NodeKind::Svg(ref svg) => {
|
||||
built_svg.scene.set_view_box(usvg_rect_to_euclid_rect(&svg.view_box.rect));
|
||||
for kid in root.children() {
|
||||
built_svg.process_node(&kid, &global_transform);
|
||||
built_svg.process_node(&kid, &State::new(), &mut None);
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
|
@ -87,16 +90,16 @@ impl BuiltSVG {
|
|||
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 transform = node_transform * *transform;
|
||||
state.transform = node_transform * state.transform;
|
||||
|
||||
match *node.borrow() {
|
||||
NodeKind::Group(ref group) => {
|
||||
if group.clip_path.is_some() {
|
||||
self.result_flags
|
||||
.insert(BuildResultFlags::UNSUPPORTED_CLIP_PATH_ATTR);
|
||||
}
|
||||
if group.filter.is_some() {
|
||||
self.result_flags
|
||||
.insert(BuildResultFlags::UNSUPPORTED_FILTER_ATTR);
|
||||
|
@ -106,34 +109,35 @@ impl BuiltSVG {
|
|||
.insert(BuildResultFlags::UNSUPPORTED_MASK_ATTR);
|
||||
}
|
||||
|
||||
if let Some(ref clip_path_name) = group.clip_path {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
for kid in node.children() {
|
||||
self.process_node(&kid, &transform)
|
||||
self.process_node(&kid, &state, clip_outline)
|
||||
}
|
||||
}
|
||||
NodeKind::Path(ref path) if path.visibility == Visibility::Visible => {
|
||||
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 = 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 style = self.scene.push_paint(&Paint::from_svg_paint(
|
||||
&fill.paint,
|
||||
fill.opacity,
|
||||
&mut self.result_flags,
|
||||
));
|
||||
|
||||
let path = UsvgPathToSegments::new(path.data.iter().cloned());
|
||||
let path = Transform2FPathIter::new(path, &transform);
|
||||
let path = Transform2FPathIter::new(path, &state.transform);
|
||||
let outline = Outline::from_segments(path);
|
||||
|
||||
// TODO(pcwalton): Clip paths.
|
||||
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 {
|
||||
let style = self.scene.push_paint(&Paint::from_svg_paint(
|
||||
&stroke.paint,
|
||||
stroke.opacity,
|
||||
&mut self.result_flags,
|
||||
));
|
||||
|
||||
let stroke_style = StrokeStyle {
|
||||
line_width: f32::max(stroke.width.value() as f32, HAIRLINE_STROKE_WIDTH),
|
||||
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);
|
||||
stroke_to_fill.offset();
|
||||
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());
|
||||
self.scene.push_path(DrawPath::new(outline, style, None, name));
|
||||
self.push_draw_path(outline, name, &state, &stroke.paint, stroke.opacity);
|
||||
}
|
||||
}
|
||||
NodeKind::Path(..) => {}
|
||||
NodeKind::ClipPath(..) => {
|
||||
self.result_flags
|
||||
.insert(BuildResultFlags::UNSUPPORTED_CLIP_PATH_NODE);
|
||||
NodeKind::ClipPath(_) => {
|
||||
let mut clip_outline = None;
|
||||
state.path_destination = PathDestination::Clip;
|
||||
for kid in node.children() {
|
||||
self.process_node(&kid, &state, &mut clip_outline);
|
||||
}
|
||||
|
||||
if let Some(clip_outline) = clip_outline {
|
||||
let name = format!("ClipPath({})", node.id());
|
||||
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 { .. } => {
|
||||
if node.has_children() {
|
||||
self.result_flags
|
||||
.insert(BuildResultFlags::UNSUPPORTED_DEFS_NODE);
|
||||
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(..) => {
|
||||
|
@ -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 {
|
||||
|
@ -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