From 40209b6fe8a9dc0b37c831747de6f141138a4077 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Wed, 6 Mar 2019 18:25:08 -0800 Subject: [PATCH] Show a warning when unsupported SVG features are used --- Cargo.lock | 2 + demo/common/Cargo.toml | 1 + demo/common/src/lib.rs | 109 +++++++++++++++---- demo/common/src/ui.rs | 31 +++++- svg/Cargo.toml | 1 + svg/src/lib.rs | 235 +++++++++++++++++++++++++++++++---------- 6 files changed, 299 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8e5fc6f..4e828286 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -518,6 +518,7 @@ dependencies = [ "pathfinder_ui 0.1.0", "rayon 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "sdl2 0.32.1 (registry+https://github.com/rust-lang/crates.io-index)", + "sdl2-sys 0.32.5 (registry+https://github.com/rust-lang/crates.io-index)", "usvg 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -580,6 +581,7 @@ version = "0.3.0" name = "pathfinder_svg" version = "0.1.0" dependencies = [ + "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", "pathfinder_geometry 0.3.0", "pathfinder_renderer 0.1.0", "usvg 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/demo/common/Cargo.toml b/demo/common/Cargo.toml index b38dfffc..264ee582 100644 --- a/demo/common/Cargo.toml +++ b/demo/common/Cargo.toml @@ -11,6 +11,7 @@ jemallocator = "0.1" nfd = "0.0.4" rayon = "1.0" sdl2 = "0.32" +sdl2-sys = "0.32" usvg = "0.4" [dependencies.image] diff --git a/demo/common/src/lib.rs b/demo/common/src/lib.rs index 9f3d54d2..4c6b27c9 100644 --- a/demo/common/src/lib.rs +++ b/demo/common/src/lib.rs @@ -29,18 +29,20 @@ use pathfinder_renderer::gpu_data::BuiltScene; use pathfinder_renderer::post::{DEFRINGING_KERNEL_CORE_GRAPHICS, STEM_DARKENING_FACTORS}; use pathfinder_renderer::scene::Scene; use pathfinder_renderer::z_buffer::ZBuffer; -use pathfinder_svg::SceneExt; +use pathfinder_svg::BuiltSVG; use pathfinder_ui::UIEvent; use rayon::ThreadPoolBuilder; -use sdl2::{EventPump, Sdl, VideoSubsystem}; +use sdl2::EventPump; use sdl2::event::{Event, WindowEvent}; use sdl2::keyboard::Keycode; use sdl2::video::{GLContext, GLProfile, Window}; +use sdl2_sys::{SDL_Event, SDL_UserEvent}; use std::f32::consts::FRAC_PI_4; use std::mem; use std::panic; use std::path::{Path, PathBuf}; use std::process; +use std::ptr; use std::sync::mpsc::{self, Receiver, Sender}; use std::thread; use std::time::{Duration, Instant}; @@ -72,6 +74,8 @@ const GROUND_LINE_COLOR: ColorU = ColorU { r: 127, g: 127, b: 127, a: 255 }; const APPROX_FONT_SIZE: f32 = 16.0; +const MESSAGE_TIMEOUT_SECS: u64 = 5; + pub const GRIDLINE_COUNT: u8 = 10; mod device; @@ -79,10 +83,6 @@ mod ui; pub struct DemoApp { window: Window, - #[allow(dead_code)] - sdl_context: Sdl, - #[allow(dead_code)] - sdl_video: VideoSubsystem, sdl_event_pump: EventPump, #[allow(dead_code)] gl_context: GLContext, @@ -98,6 +98,8 @@ pub struct DemoApp { exit: bool, mouselook_enabled: bool, dirty: bool, + expire_message_event_id: u32, + message_epoch: u32, ui: DemoUI, scene_thread_proxy: SceneThreadProxy, @@ -112,6 +114,7 @@ impl DemoApp { pub fn new() -> DemoApp { let sdl_context = sdl2::init().unwrap(); let sdl_video = sdl_context.video().unwrap(); + let sdl_event = sdl_context.event().unwrap(); let gl_attributes = sdl_video.gl_attr(); gl_attributes.set_context_profile(GLProfile::Core); @@ -130,6 +133,8 @@ impl DemoApp { let gl_context = window.gl_create_context().unwrap(); gl::load_with(|name| sdl_video.gl_get_proc_address(name) as *const _); + let expire_message_event_id = unsafe { sdl_event.register_event().unwrap() }; + let sdl_event_pump = sdl_context.event_pump().unwrap(); let device = GLDevice::new(); @@ -140,10 +145,12 @@ impl DemoApp { let (drawable_width, drawable_height) = window.drawable_size(); let drawable_size = Point2DI32::new(drawable_width as i32, drawable_height as i32); - let base_scene = load_scene(&options.input_path); - let scene_view_box = base_scene.view_box; + let built_svg = load_scene(&options.input_path); + let message = get_svg_building_message(&built_svg); + + let scene_view_box = built_svg.scene.view_box; let renderer = Renderer::new(device, &resources, drawable_size); - let scene_thread_proxy = SceneThreadProxy::new(base_scene, options.clone()); + let scene_thread_proxy = SceneThreadProxy::new(built_svg.scene, options.clone()); update_drawable_size(&window, &scene_thread_proxy); let camera = if options.three_d { @@ -156,16 +163,23 @@ impl DemoApp { let ground_solid_vertex_array = GroundSolidVertexArray::new(&renderer.device, &ground_program, - &renderer.quad_vertex_positions_buffer()); + &renderer.quad_vertex_positions_buffer()); let ground_line_vertex_array = GroundLineVertexArray::new(&renderer.device, &ground_program); - let ui = DemoUI::new(&renderer.device, &resources, options); + let mut ui = DemoUI::new(&renderer.device, &resources, options); + let mut message_epoch = 0; + emit_message(&mut ui, &mut message_epoch, expire_message_event_id, message); + + // Leak our SDL stuff. It'll last the entire duration of the process anyway, and it means + // we don't have to deal with any nasty issues regarding synchronizing background threads + // during shutdown. + mem::forget(sdl_event); + mem::forget(sdl_video); + mem::forget(sdl_context); DemoApp { window, - sdl_context, - sdl_video, sdl_event_pump, gl_context, @@ -180,6 +194,8 @@ impl DemoApp { exit: false, mouselook_enabled: false, dirty: true, + expire_message_event_id, + message_epoch, ui, scene_thread_proxy, @@ -333,6 +349,12 @@ impl DemoApp { self.dirty = true; } } + Event::User { type_: event_id, code: expected_epoch, .. } if + event_id == self.expire_message_event_id && + expected_epoch as u32 == self.message_epoch => { + self.ui.message = String::new(); + self.dirty = true; + } _ => continue, } } @@ -497,19 +519,21 @@ impl DemoApp { UIAction::None => {} UIAction::OpenFile(ref path) => { - let scene = load_scene(&path); - self.scene_view_box = scene.view_box; + let built_svg = load_scene(&path); + self.ui.message = get_svg_building_message(&built_svg); + + self.scene_view_box = built_svg.scene.view_box; update_drawable_size(&self.window, &self.scene_thread_proxy); let drawable_size = current_drawable_size(&self.window); self.camera = if self.ui.three_d_enabled { - Camera::new_3d(scene.view_box) + Camera::new_3d(built_svg.scene.view_box) } else { - Camera::new_2d(scene.view_box, drawable_size) + Camera::new_2d(built_svg.scene.view_box, drawable_size) }; - self.scene_thread_proxy.load_scene(scene); + self.scene_thread_proxy.load_scene(built_svg.scene); self.dirty = true; } @@ -690,8 +714,8 @@ impl Options { } } -fn load_scene(input_path: &Path) -> Scene { - Scene::from_tree(Tree::from_file(input_path, &UsvgOptions::default()).unwrap()) +fn load_scene(input_path: &Path) -> BuiltSVG { + BuiltSVG::from_tree(Tree::from_file(input_path, &UsvgOptions::default()).unwrap()) } fn build_scene(scene: &Scene, build_options: BuildOptions, jobs: Option) -> BuiltScene { @@ -835,3 +859,48 @@ fn get_mouse_position(sdl_event_pump: &EventPump, scale_factor: f32) -> Point2DF let mouse_state = sdl_event_pump.mouse_state(); Point2DI32::new(mouse_state.x(), mouse_state.y()).to_f32().scale(scale_factor) } + +fn get_svg_building_message(built_svg: &BuiltSVG) -> String { + if built_svg.result_flags.is_empty() { + return String::new(); + } + format!("Warning: These features in the SVG are unsupported: {}.", built_svg.result_flags) +} + +fn emit_message(ui: &mut DemoUI, + message_epoch: &mut u32, + expire_message_event_id: u32, + message: String) { + if message.is_empty() { + return; + } + + ui.message = message; + let expected_epoch = *message_epoch + 1; + *message_epoch = expected_epoch; + thread::spawn(move || { + thread::sleep(Duration::from_secs(MESSAGE_TIMEOUT_SECS)); + push_sdl_user_event(SDL_UserEvent { + timestamp: 0, + windowID: 0, + type_: expire_message_event_id, + code: expected_epoch as i32, + data1: ptr::null_mut(), + data2: ptr::null_mut(), + }).unwrap(); + }); +} + +// Posts an event from any thread. +// +// TODO(pcwalton): The fact that this is necessary is really a `rust-sdl2` bug, filed at +// https://github.com/Rust-SDL2/rust-sdl2/issues/747. +fn push_sdl_user_event(mut event: SDL_UserEvent) -> Result<(), String> { + unsafe { + if sdl2_sys::SDL_PushEvent(&mut event as *mut SDL_UserEvent as *mut SDL_Event) == 1 { + Ok(()) + } else { + Err(sdl2::get_error()) + } + } +} diff --git a/demo/common/src/ui.rs b/demo/common/src/ui.rs index 52df39ee..f7eb3bbd 100644 --- a/demo/common/src/ui.rs +++ b/demo/common/src/ui.rs @@ -14,8 +14,8 @@ use pathfinder_geometry::basic::point::Point2DI32; use pathfinder_geometry::basic::rect::RectI32; use pathfinder_gpu::{Device, Resources}; use pathfinder_renderer::gpu::debug::DebugUI; -use pathfinder_ui::{BUTTON_HEIGHT, BUTTON_TEXT_OFFSET, BUTTON_WIDTH, PADDING, SWITCH_SIZE}; -use pathfinder_ui::{TEXT_COLOR, WINDOW_COLOR}; +use pathfinder_ui::{BUTTON_HEIGHT, BUTTON_TEXT_OFFSET, BUTTON_WIDTH, FONT_ASCENT, PADDING}; +use pathfinder_ui::{SWITCH_SIZE, TEXT_COLOR, TOOLTIP_HEIGHT, WINDOW_COLOR}; use std::f32::consts::PI; use std::path::PathBuf; @@ -54,12 +54,15 @@ pub struct DemoUI where D: Device { effects_panel_visible: bool, rotate_panel_visible: bool, + // FIXME(pcwalton): Factor the below out into a model class. + pub three_d_enabled: bool, pub dark_background_enabled: bool, pub gamma_correction_effect_enabled: bool, pub stem_darkening_effect_enabled: bool, pub subpixel_aa_effect_enabled: bool, pub rotation: i32, + pub message: String, } impl DemoUI where D: Device { @@ -92,6 +95,7 @@ impl DemoUI where D: Device { stem_darkening_effect_enabled: false, subpixel_aa_effect_enabled: false, rotation: SLIDER_WIDTH / 2, + message: String::new(), } } @@ -100,6 +104,12 @@ impl DemoUI where D: Device { } pub fn update(&mut self, device: &D, debug_ui: &mut DebugUI, action: &mut UIAction) { + // Draw message text. + + self.draw_message_text(device, debug_ui); + + // Draw button strip. + let bottom = debug_ui.ui.framebuffer_size().y() - PADDING; let mut position = Point2DI32::new(PADDING, bottom - BUTTON_HEIGHT); @@ -181,6 +191,23 @@ impl DemoUI where D: Device { self.draw_rotate_panel(device, debug_ui, action); } + fn draw_message_text(&mut self, device: &D, debug_ui: &mut DebugUI) { + if self.message.is_empty() { + return; + } + + let message_size = debug_ui.ui.measure_text(&self.message); + let window_origin = Point2DI32::new(PADDING, PADDING); + let window_size = Point2DI32::new(PADDING * 2 + message_size, TOOLTIP_HEIGHT); + debug_ui.ui.draw_solid_rounded_rect(device, + RectI32::new(window_origin, window_size), + WINDOW_COLOR); + debug_ui.ui.draw_text(device, + &self.message, + window_origin + Point2DI32::new(PADDING, PADDING + FONT_ASCENT), + false); + } + fn draw_effects_panel(&mut self, device: &D, debug_ui: &mut DebugUI) { if !self.effects_panel_visible { return; diff --git a/svg/Cargo.toml b/svg/Cargo.toml index faa862f9..61d0280a 100644 --- a/svg/Cargo.toml +++ b/svg/Cargo.toml @@ -5,6 +5,7 @@ edition = "2018" authors = ["Patrick Walton "] [dependencies] +bitflags = "1.0" usvg = "0.4" [dependencies.pathfinder_geometry] diff --git a/svg/src/lib.rs b/svg/src/lib.rs index 419bce68..4c913944 100644 --- a/svg/src/lib.rs +++ b/svg/src/lib.rs @@ -10,6 +10,9 @@ //! Converts a subset of SVG to a Pathfinder scene. +#[macro_use] +extern crate bitflags; + use pathfinder_geometry::basic::line_segment::LineSegmentF32; use pathfinder_geometry::basic::point::Point2DF32; use pathfinder_geometry::basic::rect::RectF32; @@ -20,29 +23,57 @@ use pathfinder_geometry::segment::{Segment, SegmentFlags}; use pathfinder_geometry::stroke::OutlineStrokeToFill; use pathfinder_renderer::paint::Paint; use pathfinder_renderer::scene::{PathObject, PathObjectKind, Scene}; +use std::fmt::{Display, Formatter, Result as FormatResult}; use std::mem; use usvg::{Color as SvgColor, Node, NodeExt, NodeKind, Paint as UsvgPaint}; -use usvg::{PathSegment as UsvgPathSegment, Rect as UsvgRect, Transform as UsvgTransform, Tree}; +use usvg::{PathSegment as UsvgPathSegment, Rect as UsvgRect, Transform as UsvgTransform}; +use usvg::{Tree, Visibility}; const HAIRLINE_STROKE_WIDTH: f32 = 0.1; -pub trait SceneExt { - fn from_tree(tree: Tree) -> Self; +pub struct BuiltSVG { + pub scene: Scene, + pub result_flags: BuildResultFlags, } -impl SceneExt for Scene { +bitflags! { + // NB: If you change this, make sure to update the `Display` + // implementation as well. + pub struct BuildResultFlags: u16 { + const UNSUPPORTED_CLIP_PATH_NODE = 0x0001; + const UNSUPPORTED_DEFS_NODE = 0x0002; + const UNSUPPORTED_FILTER_NODE = 0x0004; + const UNSUPPORTED_IMAGE_NODE = 0x0008; + const UNSUPPORTED_LINEAR_GRADIENT_NODE = 0x0010; + const UNSUPPORTED_MASK_NODE = 0x0020; + const UNSUPPORTED_PATTERN_NODE = 0x0040; + const UNSUPPORTED_RADIAL_GRADIENT_NODE = 0x0080; + const UNSUPPORTED_NESTED_SVG_NODE = 0x0100; + const UNSUPPORTED_TEXT_NODE = 0x0200; + const UNSUPPORTED_LINK_PAINT = 0x0400; + const UNSUPPORTED_CLIP_PATH_ATTR = 0x0800; + const UNSUPPORTED_FILTER_ATTR = 0x1000; + const UNSUPPORTED_MASK_ATTR = 0x2000; + const UNSUPPORTED_OPACITY_ATTR = 0x4000; + } +} + +impl BuiltSVG { // TODO(pcwalton): Allow a global transform to be set. - fn from_tree(tree: Tree) -> Scene { + pub fn from_tree(tree: Tree) -> BuiltSVG { let global_transform = Transform2DF32::default(); - let mut scene = Scene::new(); + let mut built_svg = BuiltSVG { + scene: Scene::new(), + result_flags: BuildResultFlags::empty(), + }; let root = &tree.root(); match *root.borrow() { NodeKind::Svg(ref svg) => { - scene.view_box = usvg_rect_to_euclid_rect(&svg.view_box.rect); + built_svg.scene.view_box = usvg_rect_to_euclid_rect(&svg.view_box.rect); for kid in root.children() { - process_node(&mut scene, &kid, &global_transform); + built_svg.process_node(&kid, &global_transform); } } _ => unreachable!(), @@ -52,77 +83,165 @@ impl SceneExt for Scene { // recursively dropping reference counts on very large SVGs. :( mem::forget(tree); - scene + built_svg + } + + fn process_node(&mut self, node: &Node, transform: &Transform2DF32) { + let node_transform = usvg_transform_to_transform_2d(&node.transform()); + let transform = transform.pre_mul(&node_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); + } + if group.mask.is_some() { + self.result_flags.insert(BuildResultFlags::UNSUPPORTED_MASK_ATTR); + } + if group.opacity.is_some() { + self.result_flags.insert(BuildResultFlags::UNSUPPORTED_OPACITY_ATTR); + } + + for kid in node.children() { + self.process_node(&kid, &transform) + } + } + 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, + &mut self.result_flags)); + + let path = UsvgPathToSegments::new(path.segments.iter().cloned()); + let path = Transform2DF32PathIter::new(path, &transform); + let outline = Outline::from_segments(path); + + self.scene.bounds = self.scene.bounds.union_rect(outline.bounds()); + self.scene.objects.push(PathObject::new( + outline, + style, + node.id().to_string(), + PathObjectKind::Fill, + )); + } + + if let Some(ref stroke) = path.stroke { + let style = + self.scene.push_paint(&Paint::from_svg_paint(&stroke.paint, + &mut self.result_flags)); + let stroke_width = + f32::max(stroke.width.value() as f32, HAIRLINE_STROKE_WIDTH); + + let path = UsvgPathToSegments::new(path.segments.iter().cloned()); + let path = Transform2DF32PathIter::new(path, &transform); + let outline = Outline::from_segments(path); + + let mut stroke_to_fill = OutlineStrokeToFill::new(outline, stroke_width); + stroke_to_fill.offset(); + let outline = stroke_to_fill.outline; + + self.scene.bounds = self.scene.bounds.union_rect(outline.bounds()); + self.scene.objects.push(PathObject::new( + outline, + style, + node.id().to_string(), + PathObjectKind::Stroke, + )); + } + } + NodeKind::Path(..) => {} + NodeKind::ClipPath(..) => { + self.result_flags.insert(BuildResultFlags::UNSUPPORTED_CLIP_PATH_NODE); + } + NodeKind::Defs { .. } => { + self.result_flags.insert(BuildResultFlags::UNSUPPORTED_DEFS_NODE); + } + NodeKind::Filter(..) => { + self.result_flags.insert(BuildResultFlags::UNSUPPORTED_FILTER_NODE); + } + NodeKind::Image(..) => { + self.result_flags.insert(BuildResultFlags::UNSUPPORTED_IMAGE_NODE); + } + NodeKind::LinearGradient(..) => { + self.result_flags.insert(BuildResultFlags::UNSUPPORTED_LINEAR_GRADIENT_NODE); + } + NodeKind::Mask(..) => { + self.result_flags.insert(BuildResultFlags::UNSUPPORTED_MASK_NODE); + } + NodeKind::Pattern(..) => { + self.result_flags.insert(BuildResultFlags::UNSUPPORTED_PATTERN_NODE); + } + NodeKind::RadialGradient(..) => { + self.result_flags.insert(BuildResultFlags::UNSUPPORTED_RADIAL_GRADIENT_NODE); + } + NodeKind::Svg(..) => { + self.result_flags.insert(BuildResultFlags::UNSUPPORTED_NESTED_SVG_NODE); + } + NodeKind::Text(..) => { + self.result_flags.insert(BuildResultFlags::UNSUPPORTED_TEXT_NODE); + } + } } } -fn process_node(scene: &mut Scene, node: &Node, transform: &Transform2DF32) { - let node_transform = usvg_transform_to_transform_2d(&node.transform()); - let transform = transform.pre_mul(&node_transform); - - match *node.borrow() { - NodeKind::Group(_) => { - for kid in node.children() { - process_node(scene, &kid, &transform) - } +impl Display for BuildResultFlags { + fn fmt(&self, formatter: &mut Formatter) -> FormatResult { + if self.is_empty() { + return Ok(()) } - NodeKind::Path(ref path) => { - if let Some(ref fill) = path.fill { - let style = scene.push_paint(&Paint::from_svg_paint(&fill.paint)); - let path = UsvgPathToSegments::new(path.segments.iter().cloned()); - let path = Transform2DF32PathIter::new(path, &transform); - let outline = Outline::from_segments(path); - - scene.bounds = scene.bounds.union_rect(outline.bounds()); - scene.objects.push(PathObject::new( - outline, - style, - node.id().to_string(), - PathObjectKind::Fill, - )); + let mut first = true; + for (bit, name) in NAMES.iter().enumerate() { + if (self.bits() >> bit) & 1 == 0 { + continue; } - - if let Some(ref stroke) = path.stroke { - let style = scene.push_paint(&Paint::from_svg_paint(&stroke.paint)); - let stroke_width = - f32::max(stroke.width.value() as f32, HAIRLINE_STROKE_WIDTH); - - let path = UsvgPathToSegments::new(path.segments.iter().cloned()); - let path = Transform2DF32PathIter::new(path, &transform); - let outline = Outline::from_segments(path); - - let mut stroke_to_fill = OutlineStrokeToFill::new(outline, stroke_width); - stroke_to_fill.offset(); - let outline = stroke_to_fill.outline; - - scene.bounds = scene.bounds.union_rect(outline.bounds()); - scene.objects.push(PathObject::new( - outline, - style, - node.id().to_string(), - PathObjectKind::Stroke, - )); + if !first { + formatter.write_str(", ")?; + } else { + first = false; } + formatter.write_str(name)?; } - _ => { - // TODO(pcwalton): Handle these by punting to WebRender. - } + + return Ok(()); + + // Must match the order in `BuildResultFlags`. + static NAMES: &'static [&'static str] = &[ + "", + "", + "", + "", + "", + "", + "", + "", + "nested ", + "", + "paint server element", + "clip-path attribute", + "filter attribute", + "mask attribute", + "opacity attribute", + ]; } } trait PaintExt { - fn from_svg_paint(svg_paint: &UsvgPaint) -> Self; + fn from_svg_paint(svg_paint: &UsvgPaint, result_flags: &mut BuildResultFlags) -> Self; } impl PaintExt for Paint { #[inline] - fn from_svg_paint(svg_paint: &UsvgPaint) -> Paint { + fn from_svg_paint(svg_paint: &UsvgPaint, result_flags: &mut BuildResultFlags) -> Paint { Paint { color: match *svg_paint { UsvgPaint::Color(color) => ColorU::from_svg_color(color), UsvgPaint::Link(_) => { // TODO(pcwalton) + result_flags.insert(BuildResultFlags::UNSUPPORTED_LINK_PAINT); ColorU::black() } }