diff --git a/Cargo.lock b/Cargo.lock index b8db32c..d0267e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -656,6 +656,7 @@ dependencies = [ "roxmltree", "structopt", "svg2gcode", + "svgtypes", ] [[package]] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index d927ec5..4ec0002 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -15,6 +15,7 @@ g-code = "0" codespan-reporting = "0.11" structopt = "0.3" roxmltree = "0" +svgtypes = "0" [[bin]] name = "svg2gcode" diff --git a/cli/src/main.rs b/cli/src/main.rs index 1c0b555..2dfee20 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -10,6 +10,7 @@ use std::{ path::PathBuf, }; use structopt::StructOpt; +use svgtypes::LengthListParser; use svg2gcode::{ set_origin, svg2program, ConversionOptions, Machine, SupportedFunctionality, Turtle, @@ -48,6 +49,13 @@ struct Opt { /// Coordinates for the bottom left corner of the machine #[structopt(long, default_value = "0,0")] origin: String, + /// Override the width and height of the SVG (i.e. 210mm,297mm) + /// + /// Useful when the SVG does not specify these (see https://github.com/sameer/svg2gcode/pull/16) + /// + /// Passing "210mm," or ",297mm" calculates the missing dimension to conform to the viewBox aspect ratio. + #[structopt(long)] + dimensions: Option, /// Whether to use circular arcs when generating g-code /// /// Please check if your machine supports G2/G3 commands before enabling this. @@ -79,10 +87,33 @@ fn main() -> io::Result<()> { } }; + let mut dimensions = [None, None]; + + if let Some(dimensions_str) = opt.dimensions { + dimensions_str + .split(',') + .map(|dimension_str| { + if dimension_str.is_empty() { + None + } else { + LengthListParser::from(dimension_str) + .next() + .transpose() + .expect("could not parse dimension") + } + }) + .take(2) + .enumerate() + .for_each(|(i, dimension_origin)| { + dimensions[i] = dimension_origin; + }); + } + let options = ConversionOptions { tolerance: opt.tolerance, feedrate: opt.feedrate, dpi: opt.dpi, + dimensions, }; let snippets = [ diff --git a/lib/src/converter.rs b/lib/src/converter.rs index 6a4c4f4..f7c2b7c 100644 --- a/lib/src/converter.rs +++ b/lib/src/converter.rs @@ -9,11 +9,14 @@ use lyon_geom::{ }; use roxmltree::{Document, Node}; use svgtypes::{ - LengthListParser, PathParser, PathSegment, TransformListParser, TransformListToken, ViewBox, + Length, LengthListParser, PathParser, PathSegment, TransformListParser, TransformListToken, + ViewBox, }; use crate::turtle::*; +const SVG_TAG_NAME: &str = "svg"; + /// High-level output options #[derive(Debug)] pub struct ConversionOptions { @@ -23,6 +26,10 @@ pub struct ConversionOptions { pub feedrate: f64, /// Dots per inch for pixels, picas, points, etc. pub dpi: f64, + /// Width and height override + /// + /// Useful when an SVG does not have a set width and height or you want to override it. + pub dimensions: [Option; 2], } impl Default for ConversionOptions { @@ -31,6 +38,7 @@ impl Default for ConversionOptions { tolerance: 0.002, feedrate: 300.0, dpi: 96.0, + dimensions: [None; 2], } } } @@ -61,11 +69,14 @@ pub fn svg2program<'input>( node_stack.push((parent, children)); child } + // Last node in this group has been processed None => { if parent.has_attribute("viewBox") || parent.has_attribute("transform") || parent.has_attribute("width") || parent.has_attribute("height") + || (parent.has_tag_name(SVG_TAG_NAME) + && options.dimensions.iter().any(Option::is_some)) { turtle.pop_transform(); } @@ -85,18 +96,74 @@ pub fn svg2program<'input>( } let mut transforms = vec![]; - if let Some(view_box) = node.attribute("viewBox") { - let view_box = ViewBox::from_str(view_box).expect("could not parse viewBox"); - transforms.push( - Transform2D::translation(-view_box.x, -view_box.y) - .then_scale(1. / view_box.w, 1. / view_box.h) - // Part 2 of converting from SVG to g-code coordinates - .then_translate(vector(0., -1.)), - ); + + let view_box = node + .attribute("viewBox") + .map(ViewBox::from_str) + .transpose() + .expect("could not parse viewBox"); + let dimensions = ( + node.attribute("width") + .map(LengthListParser::from) + .and_then(|mut parser| parser.next()) + .transpose() + .expect("could not parse width") + .map(|width| length_to_mm(width, options.dpi)), + node.attribute("height") + .map(LengthListParser::from) + .and_then(|mut parser| parser.next()) + .transpose() + .expect("could not parse height") + .map(|height| length_to_mm(height, options.dpi)), + ); + let aspect_ratio = match (view_box, dimensions) { + (Some(ref view_box), (None, _)) | (Some(ref view_box), (_, None)) => { + view_box.w / view_box.h + } + (_, (Some(ref width), Some(ref height))) => *width / *height, + (None, (None, _)) | (None, (_, None)) => 1., + }; + + if let Some(ref view_box) = view_box { + let view_box_transform = Transform2D::translation(-view_box.x, -view_box.y) + .then_scale(1. / view_box.w, 1. / view_box.h); + if node.has_tag_name(SVG_TAG_NAME) { + // Part 2 of converting from SVG to g-code coordinates + transforms.push(view_box_transform.then_translate(vector(0., -1.))); + } else { + transforms.push(view_box_transform); + } } - if let Some(transform) = width_and_height_into_transform(&options, &node) { - transforms.push(transform); + let dimensions_override = [ + options.dimensions[0].map(|dim_x| length_to_mm(dim_x, options.dpi)), + options.dimensions[1].map(|dim_y| length_to_mm(dim_y, options.dpi)), + ]; + + match (dimensions_override, dimensions) { + ([Some(dim_x), Some(dim_y)], _) if node.has_tag_name(SVG_TAG_NAME) => { + transforms.push(Transform2D::scale(dim_x, dim_y)); + } + ([Some(dim_x), None], _) if node.has_tag_name(SVG_TAG_NAME) => { + transforms.push(Transform2D::scale(dim_x, dim_x / aspect_ratio)); + } + ([None, Some(dim_y)], _) if node.has_tag_name(SVG_TAG_NAME) => { + transforms.push(Transform2D::scale(aspect_ratio * dim_y, dim_y)); + } + (_, (Some(width), Some(height))) => { + transforms.push(Transform2D::scale(width, height)); + } + (_, (Some(width), None)) => { + transforms.push(Transform2D::scale(width, width / aspect_ratio)); + } + (_, (None, Some(height))) => { + transforms.push(Transform2D::scale(aspect_ratio * height, height)); + } + (_, (None, None)) => { + if view_box.is_some() && node.has_tag_name(SVG_TAG_NAME) { + transforms.push(Transform2D::scale(aspect_ratio, 1.)); + } + } } if let Some(transform) = node.attribute("transform") { @@ -166,31 +233,6 @@ fn node_name(node: &Node) -> String { name } -fn width_and_height_into_transform( - options: &ConversionOptions, - node: &Node, -) -> Option> { - if let (Some(mut width), Some(mut height)) = ( - node.attribute("width").map(LengthListParser::from), - node.attribute("height").map(LengthListParser::from), - ) { - let width = width - .next() - .expect("no width in width property") - .expect("cannot parse width"); - let height = height - .next() - .expect("no height in height property") - .expect("cannot parse height"); - let width_in_mm = length_to_mm(width, options.dpi); - let height_in_mm = length_to_mm(height, options.dpi); - - Some(Transform2D::scale(width_in_mm, height_in_mm)) - } else { - None - } -} - fn apply_path<'input>( turtle: &'_ mut Turtle<'input>, options: &ConversionOptions, diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 3970563..24860cc 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -21,9 +21,17 @@ pub use turtle::Turtle; mod test { use super::*; use g_code::emit::{format_gcode_fmt, FormatOptions}; + use svgtypes::{Length, LengthUnit}; - fn get_actual(input: &str, circular_interpolation: bool) -> String { - let options = ConversionOptions::default(); + fn get_actual( + input: &str, + circular_interpolation: bool, + dimensions: [Option; 2], + ) -> String { + let options = ConversionOptions { + dimensions, + ..Default::default() + }; let document = roxmltree::Document::parse(input).unwrap(); let mut turtle = Turtle::new(Machine::new( @@ -46,15 +54,41 @@ mod test { #[test] fn square_produces_expected_gcode() { let square = include_str!("../tests/square.svg"); - let actual = get_actual(square, false); + let actual = get_actual(square, false, [None; 2]); assert_eq!(actual, include_str!("../tests/square.gcode")) } + #[test] + fn square_dimension_override_produces_expected_gcode() { + let side_length = Length { + number: 10., + unit: LengthUnit::Mm, + }; + + for square in [ + include_str!("../tests/square.svg"), + include_str!("../tests/square_dimensionless.svg"), + ] { + assert_eq!( + get_actual(square, false, [Some(side_length); 2]), + include_str!("../tests/square.gcode") + ); + assert_eq!( + get_actual(square, false, [Some(side_length), None]), + include_str!("../tests/square.gcode") + ); + assert_eq!( + get_actual(square, false, [None, Some(side_length)]), + include_str!("../tests/square.gcode") + ); + } + } + #[test] fn square_transformed_produces_expected_gcode() { let square_transformed = include_str!("../tests/square_transformed.svg"); - let actual = get_actual(square_transformed, false); + let actual = get_actual(square_transformed, false, [None; 2]); assert_eq!(actual, include_str!("../tests/square_transformed.gcode")) } @@ -62,7 +96,7 @@ mod test { #[test] fn square_viewport_produces_expected_gcode() { let square_transformed = include_str!("../tests/square_viewport.svg"); - let actual = get_actual(square_transformed, false); + let actual = get_actual(square_transformed, false, [None; 2]); assert_eq!(actual, include_str!("../tests/square_viewport.gcode")) } @@ -70,7 +104,7 @@ mod test { #[test] fn circular_interpolation_produces_expected_gcode() { let circular_interpolation = include_str!("../tests/circular_interpolation.svg"); - let actual = get_actual(circular_interpolation, true); + let actual = get_actual(circular_interpolation, true, [None; 2]); assert_eq!( actual, @@ -82,12 +116,12 @@ mod test { fn svg_with_smooth_curves_produces_expected_gcode() { let svg = include_str!("../tests/smooth_curves.svg"); assert_eq!( - get_actual(svg, false), + get_actual(svg, false, [None; 2]), include_str!("../tests/smooth_curves.gcode") ); assert_eq!( - get_actual(svg, true), + get_actual(svg, true, [None; 2]), include_str!("../tests/smooth_curves_circular_interpolation.gcode") ); } diff --git a/lib/tests/square_dimensionless.svg b/lib/tests/square_dimensionless.svg new file mode 100644 index 0000000..a5b669e --- /dev/null +++ b/lib/tests/square_dimensionless.svg @@ -0,0 +1,69 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/web/src/inputs.rs b/web/src/inputs.rs index 50904af..a39bcc0 100644 --- a/web/src/inputs.rs +++ b/web/src/inputs.rs @@ -425,7 +425,7 @@ pub fn settings_form() -> Html { /> {" "}