diff --git a/Cargo.toml b/Cargo.toml index e9da146..04532f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,4 +12,5 @@ repository = "https://github.com/COMP6991UNSW/unsvg" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +num-traits = "0.2.19" resvg = "0.35.0" diff --git a/src/lib.rs b/src/lib.rs index 3d2ddef..96a5c68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,50 +1,45 @@ +//! The `unsvg` crate provides a very simple interface for drawing images. +//! +//! See below for an example: +//! +//! ```rust +//! use unsvg::{Image, COLORS}; +//! +//! fn main() -> Result<(), String> { +//! let mut img: Image = Image::new(200, 200); +//! let (x1, y1) = (10, 10); +//! // Second point's x and y coordinates. +//! let (x2, y2) = img.draw_simple_line(x1, y1, 120, 100, COLORS[1])?; +//! // Third point's x and y coordinates. +//! let (x3, y3) = img.draw_simple_line(x2, y2, 240, 100, COLORS[2])?; +//! let _ = img.draw_simple_line(x3, y3, 0, 100, COLORS[3])?; +//! +//! img.save_svg("path_to.svg")?; +//! +//! Ok(()) +//! } +//! ``` +//! +//! Note that `unsvg`'s underlying SVG library uses floats to represent x and y +//! coordinates. `unsvg`, on the other hand, expects signed integer coordinate +//! values. This design decision was made to ensure consistent, deterministic +//! behaviour for all coordinate inputs, which is not a given when using floats +//! due to float imprecision. + +use num_traits::cast; use resvg::usvg::{NodeExt, TreeWriting, XmlOptions}; -/// The unsvg crate provides a very simple interface for drawing images. -/// -/// See below for an example: -/// -/// ```rust -/// use unsvg::{Image, COLORS, Error}; -/// -/// fn main() -> Result<(), Error> { -/// let mut img: Image = Image::new(200, 200); -/// let second_point = img.draw_simple_line(10.0, 10.0, 120, 100.0, COLORS[1])?; -/// let third_point = img.draw_simple_line(second_point.0, second_point.1, 240, 100.0, COLORS[2])?; -/// let _ = img.draw_simple_line(third_point.0, third_point.1, 0, 100.0, COLORS[3])?; -/// -/// img.save_svg("path_to.svg")?; -/// -/// Ok(()) -/// } -/// ``` use resvg::{tiny_skia, usvg}; use std::rc::Rc; pub use resvg::usvg::Color; -/// A type encapsulating some error encountered within `unsvg`. -#[derive(Debug)] -pub struct Error(String); - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::error::Error for Error {} - -/// All fallible functions provided by `unsvg` return our custom -/// [`Error`] type, so we redefine `Result` with fixed error. -type Result = core::result::Result; - /// This contains 16 simple colors which users can select from. /// These correspond to the 16 colors available in the original Logo language. /// The colors are: /// - Black /// - Blue -/// - Cyan /// - Green +/// - Cyan /// - Red /// - Magenta /// - Yellow @@ -140,18 +135,58 @@ pub static COLORS: [Color; 16] = [ }, ]; +fn u32_to_f32(num: u32) -> f32 { + cast(num).unwrap_or_else(|| panic!("failed to convert u32 '{num}' to f32")) +} + +fn f32_to_u32(num: f32) -> u32 { + cast(num.round()).unwrap_or_else(|| panic!("failed to convert f32 '{num}' to u32")) +} + +fn i32_to_f32(num: i32) -> f32 { + cast(num).unwrap_or_else(|| panic!("failed to convert i32 '{num}' to f32")) +} + +fn f32_to_i32(num: f32) -> i32 { + cast(num.round()).unwrap_or_else(|| panic!("failed to convert f32 '{num}' to i32")) +} + +/// Normalize a direction values in degrees to within [0, 360). +fn normalize_direction(direction: i32) -> i32 { + let normalized = direction % 360; + if normalized < 0 { + normalized + 360 + } else { + normalized + } +} + /// Tells you where a line will end, given a starting point, direction, and length. /// This is used by `draw_simple_line` to get the end point of a line. -pub fn get_end_coordinates(x: f32, y: f32, direction: i32, length: f32) -> (f32, f32) { +pub fn get_end_coordinates(x: i32, y: i32, direction: i32, length: i32) -> (i32, i32) { + let x = i32_to_f32(x); + let y = i32_to_f32(y); + let length = i32_to_f32(length); + + let (end_x, end_y) = get_end_coordinates_precise(x, y, direction, length); + + let end_x = f32_to_i32(end_x); + let end_y = f32_to_i32(end_y); + + (end_x, end_y) +} + +fn get_end_coordinates_precise(x: f32, y: f32, direction: i32, length: f32) -> (f32, f32) { let x = quantize(x); let y = quantize(y); + let direction = normalize_direction(direction); - // directions start at 0 degrees being straight up, and go clockwise + // directions start at 0 degrees being straight up, and go clockwise. // we need to add 90 degrees to make 0 degrees straight right. let direction_rad = ((direction as f32) - 90.0).to_radians(); - let end_x = quantize(x + (direction_rad.cos() * length as f32)); - let end_y = quantize(y + (direction_rad.sin() * length as f32)); + let end_x = quantize(x + (direction_rad.cos() * length)); + let end_y = quantize(y + (direction_rad.sin() * length)); (end_x, end_y) } @@ -215,12 +250,13 @@ impl Image { /// let image = Image::new(100, 100); /// image.save_png("image.png"); /// ``` - pub fn save_png>(&self, path: P) -> Result<()> { + pub fn save_png>(&self, path: P) -> Result<(), String> { let rtree = resvg::Tree::from_usvg(&self.tree); + let pixmap_size = rtree.size.to_int_size(); let mut pixmap = tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap(); rtree.render(tiny_skia::Transform::default(), &mut pixmap.as_mut()); - pixmap.save_png(path).map_err(|e| Error(e.to_string())) + pixmap.save_png(path).map_err(|e| e.to_string()) } /// Save the image to a file. @@ -229,24 +265,44 @@ impl Image { /// let image = Image::new(100, 100); /// image.save_svg("image.svg"); /// ``` - pub fn save_svg>(&self, path: P) -> Result<()> { - std::fs::write(path, self.tree.to_string(&XmlOptions::default())) - .map_err(|e| Error(e.to_string())) + pub fn save_svg>(&self, path: P) -> Result<(), String> { + std::fs::write(path, self.tree.to_string(&XmlOptions::default())).map_err(|e| e.to_string()) } /// Draw a line on the image, taking a starting point, direction, length, and color. /// We return the end point of the line as a tuple of (x, y). pub fn draw_simple_line( + &mut self, + x: i32, + y: i32, + direction: i32, + length: i32, + color: Color, + ) -> Result<(i32, i32), String> { + let x = i32_to_f32(x); + let y = i32_to_f32(y); + let length = i32_to_f32(length); + + self.draw_simple_line_precise(x, y, direction, length, color) + .map(|(end_x, end_y)| { + let end_x = f32_to_i32(end_x); + let end_y = f32_to_i32(end_y); + + (end_x, end_y) + }) + } + + fn draw_simple_line_precise( &mut self, x: f32, y: f32, direction: i32, length: f32, color: Color, - ) -> Result<(f32, f32)> { + ) -> Result<(f32, f32), String> { let x = quantize(x); let y = quantize(y); - let (end_x, end_y) = get_end_coordinates(x, y, direction, length); + let (end_x, end_y) = get_end_coordinates_precise(x, y, direction, length); let paint = usvg::Paint::Color(color); let mut path = tiny_skia::PathBuilder::new(); @@ -255,7 +311,7 @@ impl Image { let mut path = usvg::Path::new( path.finish() - .ok_or(Error("Could not draw line".to_string()))? + .ok_or("Could not draw line".to_string())? .into(), ); let mut stroke = usvg::Stroke::default();