diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 7ae98f3..b060801 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,22 +1,37 @@ name: Rust on: - push: - branches: [ main ] - pull_request: - branches: [ main ] + push: env: - CARGO_TERM_COLOR: always + CARGO_TERM_COLOR: always jobs: - build: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install fonts + run: sudo apt install -y pkg-config libfreetype6-dev libfontconfig1-dev + - name: Run tests + run: cargo test --verbose - runs-on: ubuntu-latest + format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Format + run: cargo fmt - steps: - - uses: actions/checkout@v2 - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose + examples: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install fonts + run: sudo apt install -y pkg-config libfreetype6-dev libfontconfig1-dev + - name: Copy current shape dir + run: cp -r examples/out examples/out-current + - name: Execute all examples + run: ./run-all-examples.sh + - name: Compare the 2 directories + run: diff examples/out-current examples/out diff --git a/.gitignore b/.gitignore index 96ef6c0..16d5636 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target Cargo.lock +.DS_Store diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..10efcb2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug", + "program": "${workspaceFolder}/", + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..291162b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix" +} diff --git a/Cargo.toml b/Cargo.toml index 23b7dc4..afea6cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,25 @@ [workspace] +resolver = "2" +members = [ + # Export + "dessin", + "dessin-svg", + "dessin-pdf", + "dessin-image", + "dessin-dioxus", + "dessin-macros", -members = ["dessin", "dessin-svg", "dessin-pdf", "examples/svg-pdf"] + # Internal + "examples", +] + +[workspace.package] +version = "0.8.0" +license = "MIT" +edition = "2021" +repository = "https://github.com/432-Technologies/dessin" +keywords = ["dessin", "drawing", "pdf", "svg", "image", "layout"] +authors = [ + "Olivier Lemoine ", + "François Morillon ", +] diff --git a/README.md b/README.md index 952fb69..b911eb6 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,72 @@ -# [Dessin](https://docs.rs/dessin/) +# [dessin](https://docs.rs/dessin/) **Try out the new [API](https://github.com/432-Technologies/dessin/tree/v0.8-pre)** +Generate complex drawing for PDF, SVG, images and Dioxus ! -Generate complex drawing for PDF, SVG, and many more to come ! - -### How ? - -First, let's create a drawing and give it a bunch of things. -``` rust -let mut drawing = Drawing::empty().with_canvas_size(vec2(100., 100.)); - -drawing.add( - Text::new("Hello World".to_owned()) - .at(vec2(50., 50.)) - ) - .add( - Line::from(vec2(0., 0.)).to(vec2(100., 100.)) - ) - .add( - Circle::new() - .at(vec2(50., 50.)).with_radius(10.) - ) - .add( - Image::new(ImageFormat::PNG(include_bytes!("../rustacean-flat-happy.png").to_vec())) - .at(vec2(50., 50.)) - .with_size(vec2(10., 10.)) - ); -``` -We can even add sub drawings to our drawing. -``` rust -let other_drawing = Drawing::empty() - .with_canvas_size(vec2(210., 297.)) - .add( - EmbeddedDrawing::new(drawing) - .at(vec2(100., 100.)) - .with_size(vec2(10., 10.)) - ); +## Getting started + +First of all, add `dessin` to build beautiful drawings ! + +```bash +cargo add dessin ``` -Then, we export our drawing to [PDF](https://docs.rs/dessin-pdf/), [SVG](https://docs.rs/dessin-svg/), PNG, etc. -``` rust -use dessin_svg::ToSVG; +Then, and either/or `dessin-svg`, `dessin-pdf`, `dessin-image` or `dessin-dioxus` depending of the export you need. + +### Crates + +- [dessin](./dessin/README.md): the main composing logic +- [dessin-macros](./dessin-macros/README.md): the macros `dessin!()` and `#[derive(Shape)]` +- [dessin-svg](./dessin-svg/README.md): the SVG exporter +- [dessin-pdf](./dessin-pdf/README.md): the PDF exporter +- [dessin-image](./dessin-image/README.md): the image exporter +- [dessin-dioxus](./dessin-dioxus/README.md): the Dioxus exporter + +### Overview + +```rust +use dessin::prelude::*; +use palette::{named, Srgb}; +// We also reexport palette, in case you need it +// use dessin::palette::{named, Srgb}; + +#[derive(Default, Shape)] +struct MyShape { + text: String, +} +impl MyShape { + fn say_this(&mut self, what: &str) { + self.text = format!("{} And check this out: `{what}`", self.text); + } +} +impl From for Shape { + fn from(MyShape { text }: MyShape) -> Self { + dessin!(*Text(fill = Srgb::::from_format(named::RED).into_linear(), { text })).into() + } +} + +fn main() { + let dessin = dessin!(for x in 0..10 { + let radius = x as f32 * 10.; + + dessin!([ + *Circle( + fill = Srgb::::from_format(named::RED).into_linear(), + { radius }, + translate = [x as f32 * 5., 10.], + ), + *Text(fill = Srgb::::from_format(named::BLACK).into_linear(), font_size = 10., text = "Hi !",), + ]) + }); + + let dessin = dessin!([ + { dessin }(scale = [2., 2.]), + MyShape(say_this = "Hello world"), + ]); + + // let svg = dessin_svg::to_string(&dessin).unwrap(); + // let pdf = dessin_pdf::to_string(&dessin).unwrap(); + // Etc... +} -let svg = drawing.to_svg().unwrap(); -dbg!(svg); ``` diff --git a/algebr/.gitignore b/algebr/.gitignore deleted file mode 100644 index 96ef6c0..0000000 --- a/algebr/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target -Cargo.lock diff --git a/algebr/Cargo.toml b/algebr/Cargo.toml deleted file mode 100644 index c0feb53..0000000 --- a/algebr/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "algebr" -version = "0.1.1" -authors = ["Olivier Lemoine "] -edition = "2021" -license-file = "LICENSE" -description = "Basic algebra" -homepage = "https://github.com/daedalus-aero-space/algebr" - -[dependencies] diff --git a/algebr/LICENSE b/algebr/LICENSE deleted file mode 100644 index bbebca0..0000000 --- a/algebr/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 DaedalusAerospace - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/algebr/README.md b/algebr/README.md deleted file mode 100644 index d7995fd..0000000 --- a/algebr/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Algebr - -Basic algebra. - -For now support Vec2 and Angle operation. \ No newline at end of file diff --git a/algebr/src/angle.rs b/algebr/src/angle.rs deleted file mode 100644 index 359c6fc..0000000 --- a/algebr/src/angle.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::f32::consts::PI; - -#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] -pub enum Angle { - Radians(f32), - Degrees(f32), -} -impl Angle { - pub const fn rad(rad: f32) -> Angle { - Angle::Radians(rad) - } - - pub const fn deg(deg: f32) -> Angle { - Angle::Degrees(deg) - } - - pub const fn radians(radians: f32) -> Angle { - Angle::Radians(radians) - } - - pub const fn degrees(degrees: f32) -> Angle { - Angle::Degrees(degrees) - } - - pub fn to_rad(&self) -> f32 { - self.to_radians() - } - - pub fn to_deg(&self) -> f32 { - self.to_degrees() - } - - pub fn to_radians(&self) -> f32 { - match self { - Angle::Radians(radians) => *radians, - Angle::Degrees(degrees) => PI * degrees / 180.0, - } - } - - pub fn to_degrees(&self) -> f32 { - match self { - Angle::Radians(radians) => 180.0 * radians / PI, - Angle::Degrees(degrees) => *degrees, - } - } -} diff --git a/algebr/src/lib.rs b/algebr/src/lib.rs deleted file mode 100644 index 56a48e0..0000000 --- a/algebr/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod angle; -mod vec2; - -pub use angle::Angle; -pub use vec2::{vec2, Vec2}; diff --git a/algebr/src/vec2.rs b/algebr/src/vec2.rs deleted file mode 100644 index 2e8f134..0000000 --- a/algebr/src/vec2.rs +++ /dev/null @@ -1,188 +0,0 @@ -use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; - -pub const fn vec2(x: f32, y: f32) -> Vec2 { - Vec2::from_cartesian((x, y)) -} - -/// Struct representing a vector or a point in 2D space. -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)] -pub struct Vec2 { - pub x: f32, - pub y: f32, -} -impl Vec2 { - /// Vector of zeros. - pub const fn zero() -> Self { - Vec2::from_cartesian((0., 0.)) - } - - /// Vector of ones. - pub const fn ones() -> Self { - Vec2::from_cartesian((1., 1.)) - } - - pub const fn from_cartesian((x, y): (f32, f32)) -> Self { - Vec2 { x, y } - } - - pub fn from_polar_deg(mag: f32, angle_deg: f32) -> Self { - Self::from_polar_rad(mag, angle_deg.to_radians()) - } - - pub fn from_polar_rad(mag: f32, angle_rad: f32) -> Self { - Vec2 { - x: mag * angle_rad.cos(), - y: mag * angle_rad.sin(), - } - } - - pub fn rot_deg(&self, deg: f32) -> Self { - self.rot_rad(deg.to_radians()) - } - - pub fn rot_rad(&self, rad: f32) -> Self { - Vec2 { - x: rad.cos() * self.x + rad.sin() * self.y, - y: rad.sin() * self.x - rad.cos() * self.y, - } - } - - /// Absolute value. - pub fn abs(&self) -> Self { - Vec2 { - x: self.x.abs(), - y: self.y.abs(), - } - } - - /// Dot product. - pub fn dot(a: &Self, b: &Self) -> f32 { - a.x * b.x + a.y * b.y - } -} - -impl From<(f32, f32)> for Vec2 { - fn from((x, y): (f32, f32)) -> Self { - Self::from_cartesian((x, y)) - } -} - -impl Neg for Vec2 { - type Output = Vec2; - fn neg(self) -> Self::Output { - Vec2 { - x: -self.x, - y: -self.y, - } - } -} - -impl Add for Vec2 { - type Output = Self; - fn add(mut self, rhs: Vec2) -> Self::Output { - self.x += rhs.x; - self.y += rhs.y; - self - } -} - -impl Add for Vec2 { - type Output = Self; - fn add(self, rhs: f32) -> Self::Output { - Vec2 { - x: self.x + rhs, - y: self.y + rhs, - } - } -} - -impl Sub for Vec2 { - type Output = Vec2; - fn sub(self, rhs: Self) -> Self::Output { - Vec2 { - x: self.x - rhs.x, - y: self.y - rhs.y, - } - } -} - -impl Sub for Vec2 { - type Output = Vec2; - fn sub(self, rhs: f32) -> Self::Output { - Vec2 { - x: self.x - rhs, - y: self.y - rhs, - } - } -} - -impl Mul for Vec2 { - type Output = Self; - fn mul(mut self, rhs: Self) -> Self::Output { - self.x *= rhs.x; - self.y *= rhs.y; - self - } -} - -impl Mul for Vec2 { - type Output = Self; - fn mul(self, rhs: f32) -> Self::Output { - Vec2 { - x: self.x * rhs, - y: self.y * rhs, - } - } -} - -impl Div for Vec2 { - type Output = Vec2; - fn div(self, rhs: f32) -> Self::Output { - Vec2 { - x: self.x / rhs, - y: self.y / rhs, - } - } -} - -impl AddAssign for Vec2 { - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } -} - -impl AddAssign for Vec2 { - fn add_assign(&mut self, rhs: f32) { - *self = *self + rhs; - } -} - -impl SubAssign for Vec2 { - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } -} - -impl SubAssign for Vec2 { - fn sub_assign(&mut self, rhs: f32) { - *self = *self - rhs; - } -} - -impl MulAssign for Vec2 { - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; - } -} - -impl MulAssign for Vec2 { - fn mul_assign(&mut self, rhs: f32) { - *self = *self * rhs; - } -} - -impl DivAssign for Vec2 { - fn div_assign(&mut self, rhs: f32) { - *self = *self / rhs; - } -} diff --git a/dessin-dioxus/Cargo.toml b/dessin-dioxus/Cargo.toml new file mode 100644 index 0000000..e510a9a --- /dev/null +++ b/dessin-dioxus/Cargo.toml @@ -0,0 +1,21 @@ +[package] +authors = [ + "Olivier Lemoine ", + "Francois Morillon ", +] +description = "Drawing SVG" +categories = ["graphics", "gui", "rendering", "template-engine"] +keywords = ["graphics", "draw", "layout", "dioxus"] +edition = "2021" +license = "MIT" +name = "dessin-dioxus" +repository = "https://github.com/432-technologies/dessin" +version = "0.8.0" + +[dependencies] +data-encoding = "^2.8.0" +dessin = { version = "0.8.0", path = "../dessin" } +image = "0.24" +nalgebra = "^0.33" +rand = "^0.9.0" +dioxus = { version = "0.6" } diff --git a/dessin-dioxus/README.md b/dessin-dioxus/README.md new file mode 100644 index 0000000..7e460ff --- /dev/null +++ b/dessin-dioxus/README.md @@ -0,0 +1,3 @@ +# dessin-dioxus + +License: MIT diff --git a/dessin-dioxus/src/lib.rs b/dessin-dioxus/src/lib.rs new file mode 100644 index 0000000..f2819c8 --- /dev/null +++ b/dessin-dioxus/src/lib.rs @@ -0,0 +1,356 @@ +#![doc = include_str!("../README.md")] + +use ::image::ImageFormat; +use dessin::prelude::*; +use dioxus::prelude::*; +use font::FontRef; +use nalgebra::{Scale2, Transform2}; +use std::{ + collections::HashSet, + fmt::{self}, + io::Cursor, +}; + +#[derive(Debug)] +pub enum SVGError { + WriteError(fmt::Error), + CurveHasNoStartingPoint(CurvePosition), + RenderError(RenderError), +} +impl fmt::Display for SVGError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } +} +impl From for SVGError { + fn from(value: fmt::Error) -> Self { + SVGError::WriteError(value) + } +} +impl From for SVGError { + fn from(value: RenderError) -> Self { + SVGError::RenderError(value) + } +} +impl std::error::Error for SVGError {} + +#[derive(Default, Clone, Copy, PartialEq)] +pub enum ViewPort { + /// Create a viewport centered around (0, 0), with size (width, height) + ManualCentered { width: f32, height: f32 }, + /// Create a viewport centered around (x, y), with size (width, height) + ManualViewport { + x: f32, + y: f32, + width: f32, + height: f32, + }, + /// Create a Viewport centered around (0, 0), with auto size that include all [Shapes][`dessin::prelude::Shape`] + AutoCentered, + #[default] + /// Create a Viewport centered around the centered of the shapes, with auto size that include all [Shapes][`dessin::prelude::Shape`] + AutoBoundingBox, +} + +#[derive(Default, Clone, PartialEq)] +pub struct SVGOptions { + pub viewport: ViewPort, +} + +#[component] +pub fn SVG(shape: ReadOnlySignal, options: Option) -> Element { + let options = options.unwrap_or_default(); + + let view_box = use_memo(move || { + let shape = shape(); + + let (min_x, min_y, span_x, span_y) = match options.viewport { + ViewPort::ManualCentered { width, height } => { + (-width / 2., -height / 2., width, height) + } + ViewPort::ManualViewport { + x, + y, + width, + height, + } => (x - width / 2., y - height / 2., width, height), + ViewPort::AutoCentered => { + let bb = shape.local_bounding_box().straigthen(); + + let mirror_bb = bb + .transform(&nalgebra::convert::<_, Transform2>(Scale2::new( + -1., -1., + ))) + .into_straight(); + + let overall_bb = bb.join(mirror_bb); + + ( + -overall_bb.width() / 2., + -overall_bb.height() / 2., + overall_bb.width(), + overall_bb.height(), + ) + } + ViewPort::AutoBoundingBox => { + let bb = shape.local_bounding_box().straigthen(); + + (bb.top_left().x, -bb.top_left().y, bb.width(), bb.height()) + } + }; + + format!("{min_x} {min_y} {span_x} {span_y}") + }); + + let used_font = use_signal(|| HashSet::new()); + + rsx! { + svg { view_box, + Shaper { + shape, + parent_transform: nalgebra::convert(Scale2::new(1., -1.)), + used_font, + } + } + } +} + +#[component] +fn Shaper( + shape: ReadOnlySignal, + parent_transform: Transform2, + used_font: Signal>, +) -> Element { + match shape() { + Shape::Group(dessin::shapes::Group { + local_transform, + shapes, + metadata, + }) => { + let parent_transform = parent_transform * local_transform; + + // let attributes = metadata + // .into_iter() + // .map(|(name, value)| Attribute { + // name, + // value: dioxus_core::AttributeValue::Text(value), + // namespace: todo!(), + // volatile: todo!(), + // }) + // .collect::>(); + + rsx! { + g { + for shape in shapes { + Shaper { shape, parent_transform, used_font } + } + } + } + } + Shape::Style { + fill, + stroke, + shape, + } => { + let fill = fill + .map(|color| { + format!( + "rgb({} {} {} / {:.3})", + (color.red * 255.) as u32, + (color.green * 255.) as u32, + (color.blue * 255.) as u32, + color.alpha + ) + }) + .unwrap_or_else(|| "none".to_string()); + + let (stroke, stroke_width, stroke_dasharray) = match stroke { + Some(Stroke::Dashed { + color, + width, + on, + off, + }) => ( + Some(format!( + "rgb({} {} {} / {:.3})", + (color.red * 255.) as u32, + (color.green * 255.) as u32, + (color.blue * 255.) as u32, + color.alpha + )), + Some(width), + Some(format!("{on},{off}")), + ), + Some(Stroke::Full { color, width }) => ( + Some(format!( + "rgb({} {} {} / {:.3})", + (color.red * 255.) as u32, + (color.green * 255.) as u32, + (color.blue * 255.) as u32, + color.alpha + )), + Some(width), + None, + ), + None => (None, None, None), + }; + + rsx! { + g { + fill, + stroke, + stroke_width, + stroke_dasharray, + + Shaper { parent_transform, shape: *shape, used_font } + } + } + } + Shape::Ellipse(ellipse) => { + let ellipse = ellipse.position(&parent_transform); + let x = ellipse.center.x; + let y = ellipse.center.y; + let r = -ellipse.rotation.to_degrees(); + + rsx! { + ellipse { + rx: ellipse.semi_major_axis, + ry: ellipse.semi_minor_axis, + transform: "translate({x} {y}) rotate({r})", + } + } + } + Shape::Image(image) => { + let image = image.position(&parent_transform); + + let mut raw_image = Cursor::new(vec![]); + image + .image + .write_to(&mut raw_image, ImageFormat::Png) + .unwrap(); + + let data = data_encoding::BASE64.encode(&raw_image.into_inner()); + + let r = (-image.rotation.to_degrees() + 360.) % 360.; + + rsx! { + image { + width: image.width, + height: image.height, + x: image.center.x - image.width / 2., + y: image.center.y - image.width / 2., + transform: "rotate({r})", + href: "data:image/png;base64,{data}", + } + } + } + Shape::Text(text) => { + let id = rand::random::().to_string(); + + let text = text.position(&parent_transform); + + let weight = match text.font_weight { + FontWeight::Bold | FontWeight::BoldItalic => "bold", + _ => "normal", + }; + let text_style = match text.font_weight { + FontWeight::Italic | FontWeight::BoldItalic => "italic", + _ => "normal", + }; + let align = match text.align { + TextAlign::Center => "middle", + TextAlign::Left => "start", + TextAlign::Right => "end", + }; + + let font = text.font.clone().unwrap_or(FontRef::default()); + used_font.write().insert((font.clone(), text.font_weight)); + let font = font.name(text.font_weight); + + let x = text.reference_start.x; + let y = text.reference_start.y; + let r = text.direction.y.atan2(text.direction.x).to_degrees(); + + rsx! { + text { + font_family: "{font}", + text_anchor: "{align}", + font_size: "{text.font_size}px", + font_weight: "{weight}", + "text-style": "{text_style}", + transform: "translate({x} {y}) rotate({r})", + if let Some(curve) = text.on_curve { + path { id: id.clone(), d: write_curve(curve) } + textPath { href: id, {text.text} } + } else { + {text.text} + } + } + } + } + Shape::Curve(curve) => rsx! { + path { d: write_curve(curve.position(&parent_transform)) } + }, + Shape::Dynamic { + local_transform, + shaper, + } => rsx! { + Shaper { + parent_transform: parent_transform * local_transform, + shape: shaper(), + used_font, + } + }, + } +} + +fn write_curve(curve: CurvePosition) -> String { + let mut acc = String::new(); + let mut has_start = false; + + for keypoint in &curve.keypoints { + match keypoint { + KeypointPosition::Point(p) => { + if has_start { + acc.push_str("L "); + } else { + acc.push_str("M "); + has_start = true; + } + acc.push_str(&format!("{} {} ", p.x, p.y)); + } + KeypointPosition::Bezier(b) => { + if has_start { + if let Some(v) = b.start { + acc.push_str(&format!("L {} {} ", v.x, v.y)); + } + } else { + if let Some(v) = b.start { + acc.push_str(&format!("M {} {} ", v.x, v.y)); + has_start = true; + } else { + return String::new(); + } + } + + acc.push_str(&format!( + "C {start_ctrl_x} {start_ctrl_y} {end_ctrl_x} {end_ctrl_y} {end_x} {end_y} ", + start_ctrl_x = b.start_control.x, + start_ctrl_y = b.start_control.y, + end_ctrl_x = b.end_control.x, + end_ctrl_y = b.end_control.y, + end_x = b.end.x, + end_y = b.end.y, + )); + } + } + + has_start = true; + } + + if curve.closed { + acc.push_str(&format!("Z")); + } + + acc +} diff --git a/dessin-image/Cargo.toml b/dessin-image/Cargo.toml new file mode 100644 index 0000000..adbad8f --- /dev/null +++ b/dessin-image/Cargo.toml @@ -0,0 +1,29 @@ +[package] +authors = [ + "Olivier Lemoine ", + "Francois Morillon ", +] +description = "Dessin into image" +categories = [ + "graphics", + "gui", + "rendering", + "template-engine", + "multimedia::images", +] +keywords = ["graphics", "draw", "layout", "image"] +edition = "2021" +license = "MIT" +name = "dessin-image" +repository = "https://github.com/432-technologies/dessin" +version = "0.8.0" + +[dependencies] +data-encoding = "^2.8.0" +dessin = { version = "0.8.0", path = "../dessin" } +font-kit = "0.14.2" +image = "0.24" +imageproc = "^0.25.0" +nalgebra = "^0.33" +rand = "^0.9.0" +raqote = "0.8.5" diff --git a/dessin-image/README.md b/dessin-image/README.md new file mode 100644 index 0000000..975ad67 --- /dev/null +++ b/dessin-image/README.md @@ -0,0 +1,3 @@ +# dessin-image + +License: MIT diff --git a/dessin-image/src/lib.rs b/dessin-image/src/lib.rs new file mode 100644 index 0000000..29005c1 --- /dev/null +++ b/dessin-image/src/lib.rs @@ -0,0 +1,354 @@ +#![doc = include_str!("../README.md")] + +use ::image::{DynamicImage, RgbaImage}; +use dessin::{ + export::{Export, Exporter}, + prelude::*, +}; +use nalgebra::{Point2, Transform2, Translation2, Vector2}; +use raqote::{ + DrawOptions, DrawTarget, LineCap, LineJoin, PathBuilder, Point, SolidSource, Source, + StrokeStyle, +}; +use std::fmt; + +#[derive(Debug)] +pub enum ImageError { + WriteError(fmt::Error), + CurveHasNoStartingPoint(CurvePosition), + FontLoadingError(font_kit::error::FontLoadingError), + ImageError, +} +impl fmt::Display for ImageError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } +} +impl From for ImageError { + fn from(value: fmt::Error) -> Self { + ImageError::WriteError(value) + } +} +impl std::error::Error for ImageError {} + +#[derive(Default)] +pub struct ImageOptions { + pub canvas: Option<(f32, f32)>, +} + +pub struct ImageExporter { + buffer: DrawTarget, + style: Vec, +} + +impl ImageExporter { + fn new(width: u32, height: u32) -> Self { + ImageExporter { + buffer: DrawTarget::new(width as i32, height as i32), + style: vec![], + } + } + + fn finalize(self) -> DrawTarget { + self.buffer + } + + fn style(&self) -> StylePosition { + let mut acc = StylePosition { + stroke: None, + fill: None, + }; + + for style in self.style.iter().rev() { + match (acc.fill, style.fill) { + (None, Some(s)) => acc.fill = Some(s), + _ => {} + } + + match (acc.stroke, style.stroke) { + (None, Some(s)) => acc.stroke = Some(s), + _ => {} + } + + if acc.fill.is_some() && acc.fill.is_some() { + break; + } + } + + acc + } +} + +impl Exporter for ImageExporter { + type Error = ImageError; + + const CAN_EXPORT_ELLIPSE: bool = false; + + fn start_style(&mut self, style: StylePosition) -> Result<(), Self::Error> { + self.style.push(style); + Ok(()) + } + + fn end_style(&mut self) -> Result<(), Self::Error> { + self.style.pop(); + Ok(()) + } + + fn export_image( + &mut self, + ImagePosition { + top_left: _, + top_right: _, + bottom_right: _, + bottom_left: _, + center: _, + width: _, + height: _, + rotation: _, + image: _, + }: ImagePosition, + ) -> Result<(), Self::Error> { + // let mut raw_image = Cursor::new(vec![]); + // image.write_to(&mut raw_image, ImageFormat::Png).unwrap(); + + // let data = data_encoding::BASE64.encode(&raw_image.into_inner()); + + // write!( + // self.acc, + // r#" 10e-6 { + // write!( + // self.acc, + // r#" transform="rotate({rot})" "#, + // rot = -rotation.to_degrees() + // )?; + // } + + // write!(self.acc, r#"href="data:image/png;base64,{data}"/>"#,)?; + + Ok(()) + } + + fn export_curve( + &mut self, + curve: CurvePosition, + StylePosition { stroke, fill }: StylePosition, + ) -> Result<(), Self::Error> { + let mut path = PathBuilder::new(); + + for (idx, k) in curve.keypoints.iter().enumerate() { + let is_first = idx == 0; + + match k { + KeypointPosition::Point(p) if is_first => path.move_to(p.x, p.y), + KeypointPosition::Point(p) => path.line_to(p.x, p.y), + KeypointPosition::Bezier(b) => { + match (is_first, b.start) { + (true, None) => return Err(ImageError::CurveHasNoStartingPoint(curve)), + (true, Some(s)) => path.move_to(s.x, s.y), + (false, None) => {} + (false, Some(s)) => path.line_to(s.x, s.y), + } + + path.cubic_to( + b.start_control.x, + b.start_control.y, + b.end_control.x, + b.end_control.y, + b.end.x, + b.end.y, + ); + } + } + } + + if curve.closed { + path.close() + } + + let path = path.finish(); + + let style = self.style(); + + if let Some(color) = style.fill { + let (r, g, b, a) = ( + color.into_format::().red, + color.into_format::().green, + color.into_format::().blue, + color.into_format::().alpha, + ); + self.buffer.fill( + &path, + &Source::Solid(SolidSource { r: b, g, b: r, a }), + &DrawOptions::new(), + ) + } + + match style.stroke { + Some(Stroke::Full { color, width }) => { + let (r, g, b, a) = ( + color.into_format::().red, + color.into_format::().green, + color.into_format::().blue, + color.into_format::().alpha, + ); + self.buffer.stroke( + &path, + &Source::Solid(SolidSource { r: b, g, b: r, a }), + &StrokeStyle { + cap: LineCap::Butt, + join: LineJoin::Miter, + width, + miter_limit: 2., + dash_array: vec![], + dash_offset: 0., + }, + &DrawOptions::new(), + ); + } + Some(Stroke::Dashed { + color, + width, + on, + off, + }) => { + let (r, g, b, a) = ( + color.into_format::().red, + color.into_format::().green, + color.into_format::().blue, + color.into_format::().alpha, + ); + self.buffer.stroke( + &path, + &Source::Solid(SolidSource { r: b, g, b: r, a }), + &StrokeStyle { + cap: LineCap::Butt, + join: LineJoin::Miter, + width, + miter_limit: 2., + dash_array: vec![on, off], + dash_offset: 0., + }, + &DrawOptions::new(), + ); + } + None => {} + } + + Ok(()) + } + + fn export_text( + &mut self, + TextPosition { + text, + align: _, + font_weight, + on_curve: _, + font_size, + reference_start, + direction: _, + font, + }: TextPosition, + ) -> Result<(), Self::Error> { + let font = font.clone().unwrap_or_default(); + let fg = dessin::font::get(font); + let font = fg.get(font_weight).as_bytes(); + + //dt.set_transform(&Transform::create_translation(50.0, 0.0)); + // dt.set_transform(&Transform::rotation(euclid::Angle::degrees(15.0))); + + let color = match self.style().fill { + Some(color) => color, + None => return Ok(()), + }; + let (r, g, b, a) = ( + color.into_format::().red, //----------------------------------------------------------------------------- + color.into_format::().green, //before : color.rgba(); + color.into_format::().blue, //rgba() modification should be better + color.into_format::().alpha, //----------------------------------------------------------------------------- + ); + + let font = font_kit::loader::Loader::from_bytes(std::sync::Arc::new(font.to_vec()), 0) + .map_err(|e| ImageError::FontLoadingError(e))?; + self.buffer.draw_text( + &font, + font_size, + text, + Point::new(reference_start.x, reference_start.y), + &Source::Solid(SolidSource { r: b, g, b: r, a }), + &DrawOptions::new(), + ); + + Ok(()) + } +} + +pub trait ToImage { + fn rasterize(&self) -> Result; +} + +impl ToImage for Shape { + fn rasterize(&self) -> Result { + let bb = self.local_bounding_box().straigthen(); + + let center: Vector2 = bb.center() - Point2::origin(); + let translation = + Translation2::from(Vector2::new(bb.width() / 2., bb.height() / 2.) - center); + let scale = nalgebra::Scale2::new(1., -1.); + let transform = nalgebra::convert::<_, Transform2>(translation) + * nalgebra::convert::<_, Transform2>(scale); + + let width = bb.width().ceil() as u32; + let height = bb.height().ceil() as u32; + let mut exporter = ImageExporter::new(width, height); + + // self.write_into_exporter( + // &mut exporter, + // &transform, + // StylePosition { + // stroke: None, + // fill: None, + // }, + // )?; + + if let Shape::Style { fill, stroke, .. } = self { + self.write_into_exporter( + &mut exporter, + &transform, + StylePosition { + fill: *fill, + stroke: *stroke, + }, + )? //Needed to be complete + } else { + self.write_into_exporter( + &mut exporter, + &transform, + StylePosition { + fill: None, + stroke: None, + }, + )? + } + + let raw: Vec = exporter.finalize().into_vec(); + let raw: Vec = unsafe { + let cap = raw.capacity(); + let len = raw.len(); + let ptr = Box::into_raw(raw.into_boxed_slice()); + + Vec::from_raw_parts(ptr.cast(), len * 4, cap * 4) + }; + + let img = DynamicImage::ImageRgba8( + RgbaImage::from_raw(width, height, raw).ok_or(ImageError::ImageError)?, + ); + + Ok(img) + } +} diff --git a/dessin-image/src/res.png b/dessin-image/src/res.png new file mode 100644 index 0000000..846e061 Binary files /dev/null and b/dessin-image/src/res.png differ diff --git a/dessin-macros/Cargo.toml b/dessin-macros/Cargo.toml new file mode 100644 index 0000000..445da4a --- /dev/null +++ b/dessin-macros/Cargo.toml @@ -0,0 +1,21 @@ +[package] +authors = [ + "Olivier Lemoine ", + "Francois Morillon ", +] +description = "Macros for the crate `dessin`" +categories = ["graphics", "gui", "rendering", "template-engine"] +keywords = ["graphics", "draw", "layout", "proc-macros"] +edition = "2021" +license = "MIT" +name = "dessin-macros" +repository = "https://github.com/432-technologies/dessin" +version = "0.8.0" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2.0", features = ["full"] } diff --git a/dessin-macros/README.md b/dessin-macros/README.md new file mode 100644 index 0000000..e69de29 diff --git a/dessin-macros/src/dessin_macro.rs b/dessin-macros/src/dessin_macro.rs new file mode 100644 index 0000000..f8266f6 --- /dev/null +++ b/dessin-macros/src/dessin_macro.rs @@ -0,0 +1,645 @@ +use proc_macro2::{Ident, TokenStream}; +use quote::quote; +use syn::{ + braced, bracketed, parenthesized, + parse::{Parse, ParseStream}, + punctuated::Punctuated, + token::{Brace, Bracket, Comma, Paren}, + Expr, ExprAssign, ExprForLoop, ExprLet, Pat, Path, Result, Token, +}; + +enum Action { + WithArgs(ExprAssign), + WithoutArgs(Ident), + SameName(Ident), +} +impl Parse for Action { + fn parse(input: ParseStream) -> Result { + if input.peek(Brace) { + let arg; + let _ = braced!(arg in input); + Ok(Action::SameName(arg.parse()?)) + } else { + match input.fork().parse::() { + Ok(_) => input.parse().map(Action::WithArgs), + Err(_) => input.parse().map(Action::WithoutArgs), + } + } + } +} +impl From for TokenStream { + fn from(value: Action) -> Self { + match value { + Action::WithArgs(ExprAssign { + attrs: _, + left, + eq_token: _, + right, + }) => quote!(__current_shape__.#left(#right);), + Action::WithoutArgs(member) => quote!(__current_shape__.#member();), + Action::SameName(name) => quote!(__current_shape__.#name(#name);), + } + } +} + +struct Actions { + actions: Punctuated, +} +impl Parse for Actions { + fn parse(input: ParseStream) -> Result { + let actions; + let _ = parenthesized!(actions in input); + let actions = actions.parse_terminated(Action::parse, Comma)?; + + Ok(Actions { actions }) + } +} +impl From for TokenStream { + fn from(Actions { actions }: Actions) -> Self { + actions + .into_iter() + .map(TokenStream::from) + .collect::() + } +} + +struct DessinItem { + add_style: bool, + item: Path, + actions: Actions, +} +impl Parse for DessinItem { + fn parse(input: ParseStream) -> Result { + let add_style = input.peek(Token![*]); + if add_style { + input.parse::()?; + } + + let item = input.parse::()?; + let actions = input.parse::()?; + + Ok(DessinItem { + add_style, + item, + actions, + }) + } +} +impl From for TokenStream { + fn from( + DessinItem { + add_style, + item, + actions, + }: DessinItem, + ) -> Self { + let base = if add_style { + quote!(::dessin::prelude::Style::new(<#item>::default())) + } else { + quote!(<#item>::default()) + }; + + if actions.actions.is_empty() { + return base; + } + + let actions = TokenStream::from(actions); + + quote!({ + let mut __current_shape__ = #base; + #actions + __current_shape__ + }) + } +} + +struct DessinVar { + var: Expr, + actions: Option, +} +impl Parse for DessinVar { + fn parse(input: ParseStream) -> Result { + let var; + let _ = braced!(var in input); + let var = var.parse::()?; + let actions = if input.peek(Paren) { + Some(input.parse::()?) + } else { + None + }; + + Ok(DessinVar { var, actions }) + } +} +impl From for TokenStream { + fn from(DessinVar { var, actions }: DessinVar) -> Self { + let Some(actions) = actions else { + return quote!(#var); + }; + + if actions.actions.is_empty() { + quote!(#var) + } else { + let actions = TokenStream::from(actions); + + quote!({ + let mut __current_shape__ = #var; + #actions + __current_shape__ + }) + } + } +} + +struct DessinFor { + expr: ExprForLoop, +} +impl Parse for DessinFor { + fn parse(input: ParseStream) -> Result { + let expr = input.parse::()?; + + Ok(DessinFor { expr }) + } +} +impl From for TokenStream { + fn from( + DessinFor { + expr: + ExprForLoop { + attrs: _, + label: _, + for_token: _, + pat, + in_token: _, + expr, + body, + }, + }: DessinFor, + ) -> Self { + quote!(::dessin::prelude::Shape::Group(::dessin::prelude::Group { + metadata: ::std::vec::Vec::new(), + local_transform: ::dessin::nalgebra::Transform2::default(), + shapes: { + let __current_iterator__ = (#expr).into_iter(); + let mut __current_shapes__ = ::std::vec::Vec::with_capacity(__current_iterator__.size_hint().0); + for #pat in __current_iterator__ { + let __current_shape__ = ::dessin::prelude::Shape::from(#body); + __current_shapes__.push(__current_shape__); + } + __current_shapes__ + }, + })) + } +} + +enum DessinIfElseArg { + Let(ExprLet), + Ident(Ident), + Expr(Expr), +} +impl Parse for DessinIfElseArg { + fn parse(input: ParseStream) -> Result { + if input.peek(Token![let]) { + let let_exp = ExprLet { + attrs: vec![], + let_token: input.parse().unwrap(), + pat: Box::new(Pat::parse_single(&input).unwrap()), + eq_token: input.parse().unwrap(), + expr: Box::new(Expr::parse_without_eager_brace(&input).unwrap()), + }; + return Ok(DessinIfElseArg::Let(let_exp)); + } + + let is_ident = input.peek(syn::Ident) && input.peek2(Brace); + if is_ident { + let ident: Ident = input.parse()?; + return Ok(DessinIfElseArg::Ident(ident)); + } + + let expr: Expr = input.parse()?; + Ok(DessinIfElseArg::Expr(expr)) + } +} +impl From for TokenStream { + fn from(dessin_arg: DessinIfElseArg) -> Self { + match dessin_arg { + DessinIfElseArg::Let(v) => quote!(#v), + DessinIfElseArg::Ident(v) => quote!(#v), + DessinIfElseArg::Expr(v) => quote!(#v), + } + } +} + +struct DessinIfElse { + condition: DessinIfElseArg, + if_body: Box, + else_body: Option>, +} +impl Parse for DessinIfElse { + fn parse(input: ParseStream) -> Result { + let _ = input.parse::()?; + let condition = input.parse::()?; + + let if_body; + let _ = braced!(if_body in input); + let if_body: Dessin = if_body.parse()?; + let else_body = if input.parse::().is_ok() { + let else_body; + let _ = braced!(else_body in input); + Some(Box::new(else_body.parse()?)) + } else { + None + }; + + Ok(DessinIfElse { + condition, + if_body: Box::new(if_body), + else_body, + }) + } +} +impl From for TokenStream { + fn from( + DessinIfElse { + condition, + if_body, + else_body, + }: DessinIfElse, + ) -> Self { + let else_body = if let Some(else_body) = else_body { + TokenStream::from(*else_body) + } else { + TokenStream::from(DessinType::Empty) + }; + + let condition = TokenStream::from(condition); + let if_body = TokenStream::from(*if_body); + + quote!( + if #condition { + ::dessin::prelude::Shape::from(#if_body) + } else { + ::dessin::prelude::Shape::from(#else_body) + } + ) + } +} + +struct DessinGroup(Punctuated); +impl Parse for DessinGroup { + fn parse(input: ParseStream) -> Result { + let children; + let _ = bracketed!(children in input); + + let children = children.parse_terminated(Dessin::parse, Token![,])?; + + Ok(DessinGroup(children)) + } +} +impl From for TokenStream { + fn from(DessinGroup(children): DessinGroup) -> Self { + let children = children + .into_iter() + .map(TokenStream::from) + .collect::>(); + + quote!(::dessin::prelude::Shape::Group(::dessin::prelude::Group { + local_transform: ::dessin::nalgebra::Transform2::default(), + metadata: ::std::vec::Vec::new(), + shapes: ::std::vec![ + #(::dessin::prelude::Shape::from(#children)),* + ], + })) + } +} + +enum DessinType { + Empty, + Item(DessinItem), + Var(DessinVar), + Group(DessinGroup), + For(DessinFor), + IfElse(DessinIfElse), +} +impl Parse for DessinType { + fn parse(input: ParseStream) -> Result { + if input.is_empty() { + Ok(DessinType::Empty) + } else if input.peek(Brace) { + input.parse().map(DessinType::Var) + } else if input.peek(Token![for]) { + input.parse().map(DessinType::For) + } else if input.peek(Token![if]) { + input.parse().map(DessinType::IfElse) + } else if input.peek(Bracket) { + input.parse().map(DessinType::Group) + } else { + input.parse().map(DessinType::Item) + } + } +} +impl From for TokenStream { + fn from(value: DessinType) -> Self { + match value { + DessinType::Empty => quote!(::dessin::prelude::Shape::default()), + DessinType::Item(i) => i.into(), + DessinType::Group(g) => g.into(), + DessinType::Var(v) => v.into(), + DessinType::For(f) => f.into(), + DessinType::IfElse(i) => i.into(), + } + } +} + +/// +pub struct Dessin { + dessin_type: DessinType, + erased_type_shape_add_style: bool, + erased_type_shape_actions: Option, +} +impl Parse for Dessin { + fn parse(input: ParseStream) -> Result { + let mut erased_type_shape_add_style = false; + + let dessin_type: DessinType = input.parse()?; + let erased_type_shape_actions = if input.peek(Token![>]) { + let _: Token![>] = input.parse()?; + + if input.peek(Token![*]) { + let _: Token![*] = input.parse()?; + erased_type_shape_add_style = true; + } + + let actions = input.parse::()?; + + Some(actions) + } else { + None + }; + + Ok(Dessin { + dessin_type, + erased_type_shape_add_style, + erased_type_shape_actions, + }) + } +} +impl From for TokenStream { + fn from( + Dessin { + dessin_type, + erased_type_shape_add_style, + erased_type_shape_actions, + }: Dessin, + ) -> Self { + let base = TokenStream::from(dessin_type); + + let Some(actions) = erased_type_shape_actions else { + return base; + }; + + let base = if erased_type_shape_add_style { + quote!(::dessin::prelude::Style::new( + ::dessin::prelude::Shape::from(#base) + )) + } else { + quote!(::dessin::prelude::Shape::from(#base)) + }; + + if actions.actions.is_empty() { + return base; + } + + if actions.actions.is_empty() { + return base; + } + + let actions = TokenStream::from(actions); + return quote!({ + let mut __current_shape__ = #base; + #actions + __current_shape__ + }); + } +} + +#[test] +fn simple() { + syn::parse_str::("Item()").unwrap(); +} +#[test] +fn simple_with_style() { + syn::parse_str::("*Item()").unwrap(); +} +#[test] +fn simple_with_style_and_generic() { + syn::parse_str::("*Item>()").unwrap(); +} +#[test] +fn complex_with_style() { + syn::parse_str::("*Item() > *()").unwrap(); +} +#[test] +fn simple_and_actions() { + syn::parse_str::("Item( my_fn=(1., 1.), {close}, closed )").unwrap(); +} +#[test] +fn var_no_args() { + syn::parse_str::("{ v }").unwrap(); +} +#[test] +fn var_args() { + syn::parse_str::("{ v }( my_fn=(1., 1.), {close}, closed )").unwrap(); +} +#[test] +fn group() { + syn::parse_str::("[ Item(), Item() ]").unwrap(); +} +#[test] +fn as_shape() { + syn::parse_str::("Item() > ()").unwrap(); +} +#[test] +fn group_complex() { + syn::parse_str::("[ Item(), Item() ] > ()").unwrap(); +} +#[test] +fn for_loop() { + syn::parse_str::( + "for x in 0..10 { + let y = x as f32 * 2.; + dessin!(Circle( radius={y}) ) + }", + ) + .unwrap(); +} +#[test] +fn for_loop_par() { + syn::parse_str::( + "for x in (it) { + let y = x as f32 * 2.; + dessin!(Circle( radius={y}) ) + }", + ) + .unwrap(); +} +#[test] +fn for_loop_var() { + syn::parse_str::( + "for x in it { + let y = x as f32 * 2.; + dessin!(Circle ( radius={y}) ) + }", + ) + .unwrap(); +} +// #[test] +// fn for_loop_range_var() { +// syn::parse_str::( +// "for x in 0..n { +// let y = x as f32 * 2.; +// dessin!(Circle: ( radius={y}) ) +// }", +// ) +// .unwrap(); +// } +#[test] +fn simple_for_loop() { + syn::parse_str::( + "for x in xs { + let y = x as f32 * 2.; + dessin!(Circle( radius={y}) ) + }", + ) + .unwrap(); +} +#[test] +fn for_loop_range_var_par() { + syn::parse_str::( + "for x in 0..(n) { + let y = x as f32 * 2.; + dessin!(Circle( radius={y}) ) + }", + ) + .unwrap(); +} +#[test] +fn branch_if() { + syn::parse_str::( + "if test_fn() == 2 { + Circle() + }", + ) + .unwrap(); +} +#[test] +fn branch_if_else() { + syn::parse_str::( + "if test_fn() == 2 { + Circle() + } else { + Ellipse() + }", + ) + .unwrap(); +} +#[test] +fn combined_group_erased() { + syn::parse_str::( + "[ + Shape(), + Shape() > (), + { var } > (), + ] > ()", + ) + .unwrap(); +} +#[test] +fn simple_if() { + syn::parse_str::( + "if my_condition { + Circle() + }", + ) + .unwrap(); +} +#[test] +fn if_let() { + syn::parse_str::( + "if let Some(x) = my_condition { + Circle() + }", + ) + .unwrap(); +} +#[test] +fn combined_if() { + syn::parse_str::( + "if test_fn() == 2 { + Circle() > () + }", + ) + .unwrap(); +} +#[test] +fn mod_if() { + syn::parse_str::( + "if test_fn() == 2 { + my_mod::Circle() > () + }", + ) + .unwrap(); +} +#[test] +fn var_if() { + syn::parse_str::( + "if test_fn() == 2 { + { circle } > () + }", + ) + .unwrap(); +} +#[test] +fn if_if_group() { + syn::parse_str::( + "[ + { circle }(), + if test_fn() == 2 { + { circle } > () + }, + for x in 0..1 { + dessin!() + }, + Circle(), + ]", + ) + .unwrap(); +} +#[test] +fn group_in_group() { + syn::parse_str::( + "[ + [ + Circle(), + { circle } > (), + if test_fn() == 2 { + { circle } > () + }, + { circle }, + ], + { circle }, + for x in (var) { + dessin!() + }, + [], + if test_fn() == 2 { + [ + [], + { circle }, + ] + }, + Circle(), + ]", + ) + .unwrap(); +} diff --git a/dessin-macros/src/lib.rs b/dessin-macros/src/lib.rs new file mode 100644 index 0000000..ce16ef5 --- /dev/null +++ b/dessin-macros/src/lib.rs @@ -0,0 +1,312 @@ +//! Macros for the [dessin](https://docs.rs/dessin/latest/dessin/) crate. + +#![warn(missing_docs)] +#![allow(clippy::tabs_in_doc_comments)] + +extern crate proc_macro; + +mod dessin_macro; + +use proc_macro2::TokenStream; +use quote::{__private::mk_ident, quote, spanned::Spanned}; +use syn::{parse_macro_input, DataStruct, DeriveInput, Fields, FieldsNamed, Type}; + +/// Entry point to build drawings +/// ```ignore +/// dessin!([ +/// *Text( +/// text = "Hi", +/// fill = Srgba::new(255, 0, 0, 255), +/// ), +/// Line( +/// from = [0., 10.], +/// to = [10., 0.], +/// ), +/// ] > *( +/// translate = [-5., 5.], +/// fill = Srgba::new(0, 255, 0, 255), +/// )) +#[proc_macro] +pub fn dessin(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream { + let dessin = parse_macro_input!(tokens as dessin_macro::Dessin); + + TokenStream::from(dessin).into() +} + +/// Auto implements setter for each members +/// +/// ```rust +/// # #[macro_use] extern crate dessin_macros; +/// # use std::sync::{Arc, RwLock}; +/// +/// #[derive(Shape)] +/// struct MyShape { +/// // fn my_parameter(&mut self, v: u32) +/// my_parameter: u32, +/// +/// // fn my_bool(&mut self) +/// // set my_bool to true if called +/// #[shape(bool)] +/// my_bool: bool, +/// +/// // No fn generated +/// #[shape(skip)] +/// skip_this: Arc>>, +/// +/// // fn skip_option(&mut self, v: u32) +/// // set skip_option to Some(v) if called +/// #[shape(some)] +/// skip_option: Option, +/// // fn or_not(&mut self, v: Option) +/// or_not: Option, +/// +/// // fn into_string>(&mut self, v: V) +/// #[shape(into)] +/// into_string: String, +/// +/// // fn maybe_into_string>(&mut self, v: V) +/// // set maybe_into_string to Some(v.into()) if called +/// #[shape(into_some)] +/// maybe_into_string: Option, +/// } +/// ``` +#[proc_macro_derive(Shape, attributes(shape, local_transform))] +pub fn shape(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + // let vis = input.vis; + + let mut local_transform = None; + + let fields = match input.data { + syn::Data::Struct(DataStruct { + fields: Fields::Named(FieldsNamed { named: fields, .. }), + .. + }) => fields + .into_iter() + .map(|field| { + let ident = field.ident.unwrap(); + let ty = field.ty; + + let mut skip = false; + let mut into = false; + let mut boolean = false; + let mut some = false; + let mut into_some = false; + let mut doc = None; + for attr in field.attrs { + if attr.path().is_ident("doc") { + doc = Some(attr); + continue; + } + + if attr.path().is_ident("local_transform") { + if local_transform.is_some() { + panic!("Only one field can be a local_transform"); + } + + local_transform = Some(ident.clone()); + skip = true; + } + + if attr.path().is_ident("shape") { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("skip") { + skip = true; + } + + if meta.path.is_ident("into") { + into = true; + } + + if meta.path.is_ident("bool") { + boolean = true; + } + + if meta.path.is_ident("some") { + some = true; + } + + if meta.path.is_ident("into_some") { + into_some = true; + } + + Ok(()) + }) + .unwrap() + } + } + + if skip { + return quote!(); + } + + let with_ident = mk_ident(&format!("with_{ident}"), None); + if boolean { + quote!( + #doc + #[inline] + pub fn #ident(&mut self) -> &mut Self { + self.#ident = true; + self + } + + #doc + #[inline] + pub fn #with_ident(mut self) -> Self { + self.#ident(); + self + } + ) + } else if into { + quote!( + #doc + #[inline] + pub fn #ident<__INTO__T: Into<#ty>>(&mut self, value: __INTO__T) -> &mut Self { + self.#ident = value.into(); + self + } + + #doc + #[inline] + pub fn #with_ident<__INTO__T: Into<#ty>>(mut self, value: __INTO__T) -> Self { + self.#ident(value); + self + } + ) + } else if some { + let err_msg = syn::Error::new(ty.__span(), "Not supported").to_compile_error(); + let Type::Path(syn::TypePath { + path: syn::Path { segments, .. }, + .. + }) = ty + else { + return err_msg; + }; + + let ty = match segments.iter().next() { + Some(syn::PathSegment { + arguments: + syn::PathArguments::AngleBracketed( + syn::AngleBracketedGenericArguments { args, .. }, + ), + .. + }) => match args.iter().next() { + Some(syn::GenericArgument::Type(t)) => t, + _ => return err_msg, + }, + _ => return err_msg, + }; + + quote!( + #doc + #[inline] + pub fn #ident(&mut self, value: #ty) -> &mut Self { + self.#ident = Some(value); + self + } + + #doc + #[inline] + pub fn #with_ident(mut self, value: #ty) -> Self { + self.#ident(value); + self + } + ) + } else if into_some { + let err_msg = syn::Error::new(ty.__span(), "Not supported").to_compile_error(); + let Type::Path(syn::TypePath { + path: syn::Path { segments, .. }, + .. + }) = ty + else { + return err_msg; + }; + + let ty = match segments.iter().next() { + Some(syn::PathSegment { + arguments: + syn::PathArguments::AngleBracketed( + syn::AngleBracketedGenericArguments { args, .. }, + ), + .. + }) => match args.iter().next() { + Some(syn::GenericArgument::Type(t)) => t, + _ => return err_msg, + }, + _ => return err_msg, + }; + + quote!( + #doc + #[inline] + pub fn #ident<__INTO__T: Into<#ty>>(&mut self, value: __INTO__T) -> &mut Self { + self.#ident = Some(value.into()); + self + } + + #doc + #[inline] + pub fn #with_ident<__INTO__T: Into<#ty>>(mut self, value: __INTO__T) -> Self { + self.#ident(value); + self + } + ) + } else { + quote!( + #doc + #[inline] + pub fn #ident(&mut self, value: #ty) -> &mut Self { + self.#ident = value; + self + } + + #doc + #[inline] + pub fn #with_ident(mut self, value: #ty) -> Self { + self.#ident(value); + self + } + ) + } + }) + .collect::>(), + syn::Data::Struct(_) => { + unreachable!() + } + syn::Data::Enum(_) => { + unreachable!() + } + syn::Data::Union(_) => { + unreachable!() + } + }; + + let shape_op_impl = if let Some(lt) = local_transform { + quote!( + impl #impl_generics ::dessin::prelude::ShapeOp for #name #ty_generics #where_clause { + #[inline] + fn transform(&mut self, transform_matrix: ::dessin::nalgebra::Transform2) -> &mut Self { + self.#lt = transform_matrix * self.#lt; + self + } + + #[inline] + fn local_transform(&self) -> &::dessin::nalgebra::Transform2 { + &self.#lt + } + } + ) + } else { + quote!() + }; + + proc_macro::TokenStream::from(quote! { + impl #impl_generics #name #ty_generics #where_clause { + #(#fields)* + } + + #shape_op_impl + }) +} diff --git a/dessin-pdf/Cargo.toml b/dessin-pdf/Cargo.toml index 327263e..fa6abee 100644 --- a/dessin-pdf/Cargo.toml +++ b/dessin-pdf/Cargo.toml @@ -1,15 +1,20 @@ [package] -name = "dessin-pdf" -version = "0.7.3" +authors = [ + "Olivier Lemoine ", + "Francois Morillon ", +] +description = "Dessin to PDF" +categories = ["graphics", "gui", "rendering", "template-engine"] +keywords = ["graphics", "draw", "layout", "pdf"] edition = "2021" -repository = "https://github.com/daedalus-aero-space/dessin" license = "MIT" -description = "Dessin to PDF" +name = "dessin-pdf" +repository = "https://github.com/432-technologies/dessin" +version = "0.8.0" [dependencies] -dessin = { version = "0.7.3", path = "../dessin" } -printpdf = { git = "https://github.com/fschutt/printpdf", rev = "09958f9", version="0.5.3", features = [ - "webp", - "svg", -] } -rusttype = "0.9.3" +dessin = { version = "0.8.0", path = "../dessin" } +fontdue = "^0.9" +image = "0.24" +nalgebra = "^0.33" +printpdf = { version = "^0.7", features = ["webp", "svg"] } diff --git a/dessin-pdf/README.md b/dessin-pdf/README.md index a45e5dd..dba3e3a 100644 --- a/dessin-pdf/README.md +++ b/dessin-pdf/README.md @@ -1 +1,3 @@ -# Dessin PDF \ No newline at end of file +# dessin-pdf + +License: MIT diff --git a/dessin-pdf/src/Arial Bold Italic.ttf b/dessin-pdf/src/Arial Bold Italic.ttf deleted file mode 100644 index 52fd177..0000000 Binary files a/dessin-pdf/src/Arial Bold Italic.ttf and /dev/null differ diff --git a/dessin-pdf/src/Arial Bold.ttf b/dessin-pdf/src/Arial Bold.ttf deleted file mode 100644 index 940e255..0000000 Binary files a/dessin-pdf/src/Arial Bold.ttf and /dev/null differ diff --git a/dessin-pdf/src/Arial Italic.ttf b/dessin-pdf/src/Arial Italic.ttf deleted file mode 100644 index eac8b35..0000000 Binary files a/dessin-pdf/src/Arial Italic.ttf and /dev/null differ diff --git a/dessin-pdf/src/Arial.ttf b/dessin-pdf/src/Arial.ttf deleted file mode 100644 index ab68fb1..0000000 Binary files a/dessin-pdf/src/Arial.ttf and /dev/null differ diff --git a/dessin-pdf/src/lib.rs b/dessin-pdf/src/lib.rs index c84f755..afdce80 100644 --- a/dessin-pdf/src/lib.rs +++ b/dessin-pdf/src/lib.rs @@ -1,59 +1,419 @@ -mod shapes; +use dessin::{ + export::{Export, Exporter}, + font::FontRef, + prelude::*, +}; +use nalgebra::Translation2; +use printpdf::{ + color, + path::{PaintMode, WindingOrder}, + BuiltinFont, IndirectFontRef, Line, Mm, PdfDocument, PdfDocumentReference, PdfLayerReference, + Point, +}; +use std::{collections::HashMap, fmt}; -use dessin::{vec2, Drawing, Vec2}; -use printpdf::{IndirectFontRef, Mm, PdfDocument, PdfDocumentReference, PdfLayerReference}; -use std::{error::Error, io::BufWriter}; +#[derive(Debug)] +pub enum PDFError { + PrintPDF(printpdf::Error), + WriteError(fmt::Error), + CurveHasNoStartingPoint(Curve), + UnknownBuiltinFont(String), + OrphelinLayer, +} +impl From for PDFError { + fn from(e: fmt::Error) -> Self { + PDFError::WriteError(e) + } +} +impl From for PDFError { + fn from(e: printpdf::Error) -> Self { + PDFError::PrintPDF(e) + } +} -const ARIAL_REGULAR: &[u8] = include_bytes!("Arial.ttf"); -const ARIAL_BOLD: &[u8] = include_bytes!("Arial Bold.ttf"); -const ARIAL_ITALIC: &[u8] = include_bytes!("Arial Italic.ttf"); -const ARIAL_BOLD_ITALIC: &[u8] = include_bytes!("Arial Bold Italic.ttf"); +type PDFFontHolder = HashMap<(FontRef, FontWeight), IndirectFontRef>; -const DPI: f64 = 96.; +#[derive(Default)] +pub struct PDFOptions { + pub size: Option<(f32, f32)>, + pub used_font: PDFFontHolder, +} -pub struct PDF(pub PdfDocumentReference); -impl PDF { - pub fn into_bytes(self) -> Result, Box> { - let mut buff = BufWriter::new(vec![]); - self.0.save(&mut buff)?; - Ok(buff.into_inner()?) - } +pub struct PDFExporter<'a> { + layer: PdfLayerReference, + doc: &'a PdfDocumentReference, + used_font: PDFFontHolder, } +impl<'a> PDFExporter<'a> { + pub fn new_with_font( + layer: PdfLayerReference, + doc: &'a PdfDocumentReference, + used_font: PDFFontHolder, + ) -> Self { + PDFExporter { + layer, + doc, + used_font, + } + } -pub trait ToPDF { - fn to_pdf(&self) -> Result>; + pub fn new(layer: PdfLayerReference, doc: &'a PdfDocumentReference) -> Self { + let stock: PDFFontHolder = HashMap::default(); + PDFExporter { + layer, + doc, + used_font: stock, + } + } } -impl ToPDF for Drawing { - fn to_pdf(&self) -> Result> { - let Vec2 { - x: width, - y: height, - } = self.canvas_size(); +impl Exporter for PDFExporter<'_> { + type Error = PDFError; + + const CAN_EXPORT_ELLIPSE: bool = false; + + fn start_style( + &mut self, + StylePosition { fill, stroke }: StylePosition, + ) -> Result<(), Self::Error> { + if let Some(fill) = fill { + let (r, g, b) = match fill { + color => ( + color.into_format::().red, + color.into_format::().green, + color.into_format::().blue, + ), + }; + + self.layer + .set_fill_color(printpdf::Color::Rgb(printpdf::Rgb { + r, + g, + b, + icc_profile: None, + })); + } + + if let Some(stroke) = stroke { + let ((r, g, b), w) = match stroke { + Stroke::Full { color, width } => ( + ( + color.into_format::().red, + color.into_format::().green, + color.into_format::().blue, + ), + width, + ), + Stroke::Dashed { + color, + width, + on, + off, + } => { + self.layer.set_line_dash_pattern(printpdf::LineDashPattern { + offset: 0, + dash_1: Some(on as i64), + gap_1: Some(off as i64), + dash_2: None, + gap_2: None, + dash_3: None, + gap_3: None, + }); + + ( + ( + color.into_format::().red, + color.into_format::().green, + color.into_format::().blue, + ), + width, + ) + } + }; + + self.layer + .set_outline_color(printpdf::Color::Rgb(printpdf::Rgb { + r, + g, + b, + icc_profile: None, + })); + + self.layer + .set_outline_thickness(printpdf::Mm(w).into_pt().0); + } + + // if let None = stroke { + // // self.layer.set_overprint_fill(false) + // } + + // if let None = stroke { + // // just works if we have a white background + // let (r, g, b) = (1., 1., 1.); + // self.layer + // .set_outline_color(printpdf::Color::Rgb(printpdf::Rgb { + // r, + // g, + // b, + // icc_profile: None, + // })); + // self.layer + // .set_outline_thickness(printpdf::Mm(0.).into_pt().0) + // } + + Ok(()) + } + + fn end_style(&mut self) -> Result<(), Self::Error> { + self.layer + .set_outline_color(printpdf::Color::Rgb(printpdf::Rgb { + r: 0., + g: 0., + b: 0., + icc_profile: None, + })); + self.layer.set_outline_thickness(0.); + self.layer.set_line_dash_pattern(printpdf::LineDashPattern { + offset: 0, + dash_1: None, + gap_1: None, + dash_2: None, + gap_2: None, + dash_3: None, + gap_3: None, + }); + + self.layer + .set_fill_color(printpdf::Color::Rgb(printpdf::Rgb { + r: 0., + g: 0., + b: 0., + icc_profile: None, + })); + + Ok(()) + } + + fn export_image( + &mut self, + ImagePosition { + top_left: _, + top_right: _, + bottom_right: _, + bottom_left, + center: _, + width, + height, + rotation, + image, + }: ImagePosition, + ) -> Result<(), Self::Error> { + let width_px = image.width(); + let height_px = image.height(); + + let dpi = 300.; + let raw_width = width_px as f32 * 25.4 / dpi; + let raw_height = height_px as f32 * 25.4 / dpi; - let (doc, page1, layer1) = - PdfDocument::new("PDF", Mm(width as f64), Mm(height as f64), "Layer1"); + let scale_width = width / raw_width; + let scale_height = height / raw_height; - let font = doc.add_external_font(ARIAL_REGULAR)?; - let current_layer = doc.get_page(page1).get_layer(layer1); + printpdf::Image::from_dynamic_image(image).add_to_layer( + self.layer.clone(), + printpdf::ImageTransform { + translate_x: Some(Mm(bottom_left.x)), + translate_y: Some(Mm(bottom_left.y)), + rotate: Some(printpdf::ImageRotation { + angle_ccw_degrees: rotation.to_degrees(), + rotation_center_x: printpdf::Px((width_px / 2) as usize), + rotation_center_y: printpdf::Px((height_px / 2) as usize), + }), + scale_x: Some(scale_width), + scale_y: Some(scale_height), + dpi: Some(dpi), + }, + ); - let offset = vec2(self.canvas_size().x / 2., self.canvas_size().x / 2.); + Ok(()) + } - self.shapes() + fn export_curve( + &mut self, + curve: CurvePosition, + StylePosition { fill, stroke }: StylePosition, + ) -> Result<(), Self::Error> { + let points1 = curve + .keypoints .iter() - .map(|v| v.to_pdf_part(DPI, offset, &font, ¤t_layer)) - .collect::>>()?; + .enumerate() + .flat_map(|(i, key_point)| { + let next_control = matches!(curve.keypoints.get(i + 1), Some(KeypointPosition::Bezier(b)) if b.start.is_none()); + match key_point { + KeypointPosition::Point(p) => { + vec![(Point::new(Mm(p.x), Mm(p.y)), next_control)] + } + KeypointPosition::Bezier(b) => { + let mut res = vec![]; + if let Some(start) = b.start { + res.push((Point::new(Mm(start.x), Mm(start.y)), true)); + } + res.append(&mut vec![ + ( + Point::new(Mm(b.start_control.x), Mm(b.start_control.y)), + true, + ), + (Point::new(Mm(b.end_control.x), Mm(b.end_control.y)), false), + (Point::new(Mm(b.end.x), Mm(b.end.y)), next_control), + ]); + res + } + } + }) + .collect(); + //------------------------------------------------------------ + + // let line = Line { + // // Seems to be good -- + // points: points1, + // is_closed: curve.closed, + // }; + // self.layer.add_line(line); + //----------------------------------------------------------------- + let line = printpdf::Polygon { + rings: vec![points1], + // mode: PaintMode::FillStroke, + mode: match (fill, stroke) { + (Some(fill), None) => PaintMode::Fill, + (None, Some(stroke)) => PaintMode::Stroke, + (Some(fill), Some(stroke)) => PaintMode::FillStroke, + (None, None) => PaintMode::Clip, + }, + winding_order: WindingOrder::NonZero, // WindingOrder::EvenOdd, is also possible -- + }; + + self.layer.add_polygon(line); + + //----------------------------------------------------------------- + Ok(()) + } + + fn export_text( + &mut self, + TextPosition { + text, + align: _, + font_weight, + on_curve: _, + font_size, + reference_start, + direction, + font, + }: TextPosition, + ) -> Result<(), Self::Error> { + let font = font.clone().unwrap_or(FontRef::default()); + + // search if (font_ref, font_weight) is stocked in used_font + let font = self + .used_font + .entry((font.clone(), font_weight)) + .or_insert_with(|| match font::get(font.clone()).get(font_weight) { + dessin::font::Font::OTF(b) | dessin::font::Font::TTF(b) => { + if let Err(err) = self.doc.add_external_font(b.as_slice()) { + println!("Failed to add external font : {}", err); + Err(err).unwrap() + } else { + self.doc.add_external_font(b.as_slice()).unwrap() + } + } + }); + + self.layer.begin_text_section(); + self.layer.set_font(&font, font_size); + // if let Some(te) = text.on_curve { + // self.layer.add_polygon() + // todo!() + // } + let rotation = direction.y.atan2(direction.x).to_degrees(); + self.layer + .set_text_rendering_mode(printpdf::TextRenderingMode::Fill); + self.layer + .set_text_matrix(printpdf::TextMatrix::TranslateRotate( + Mm(reference_start.x).into_pt(), + Mm(reference_start.y).into_pt(), + rotation, + )); + + self.layer.write_text(text, &font); + self.layer.end_text_section(); + + Ok(()) + } +} + +pub fn write_to_pdf_with_options( + shape: &Shape, + layer: PdfLayerReference, + options: PDFOptions, + doc: &PdfDocumentReference, +) -> Result<(), PDFError> { + let (width, height) = options.size.unwrap_or_else(|| { + let bb = shape.local_bounding_box(); + (bb.width(), bb.height()) + }); + let mut exporter = PDFExporter::new_with_font(layer, doc, options.used_font); + let translation = Translation2::new(width / 2., height / 2.); + let parent_transform = nalgebra::convert(translation); + + if let Shape::Style { fill, stroke, .. } = shape { + shape.write_into_exporter( + &mut exporter, + &parent_transform, + StylePosition { + fill: *fill, + stroke: *stroke, + }, + ) //TODO Needed to be complete ? --MathNuba + } else { + shape.write_into_exporter( + &mut exporter, + &parent_transform, + StylePosition { + fill: None, + stroke: None, + }, + ) + } +} + +pub fn to_pdf_with_options( + shape: &Shape, + mut options: PDFOptions, +) -> Result { + let size = options.size.get_or_insert_with(|| { + let bb = shape.local_bounding_box(); + (bb.width(), bb.height()) + }); + let (doc, page, layer) = PdfDocument::new("", Mm(size.0), Mm(size.1), "Layer 1"); + + let layer = doc.get_page(page).get_layer(layer); + + write_to_pdf_with_options(shape, layer, options, &doc)?; + + Ok(doc) +} + +pub fn write_to_pdf( + shape: &Shape, + layer: PdfLayerReference, + doc: &PdfDocumentReference, +) -> Result<(), PDFError> { + write_to_pdf_with_options(shape, layer, PDFOptions::default(), doc) +} - Ok(PDF(doc)) - } +pub fn to_pdf(shape: &Shape) -> Result { + to_pdf_with_options(shape, PDFOptions::default()) } -trait ToPDFPart { - fn to_pdf_part( - &self, - dpi: f64, - offset: Vec2, - font: &IndirectFontRef, - layer: &PdfLayerReference, - ) -> Result<(), Box>; +pub fn to_pdf_bytes(shape: &Shape) -> Result, PDFError> { + Ok(to_pdf(shape)?.save_to_bytes()?) } diff --git a/dessin-svg/Cargo.toml b/dessin-svg/Cargo.toml index e47373b..0731a89 100644 --- a/dessin-svg/Cargo.toml +++ b/dessin-svg/Cargo.toml @@ -1,15 +1,20 @@ [package] authors = [ - "Olivier Lemoine ", - "Francois Morillon ", + "Olivier Lemoine ", + "Francois Morillon ", ] description = "Drawing SVG" +categories = ["graphics", "gui", "rendering", "template-engine"] +keywords = ["graphics", "draw", "layout", "svg"] edition = "2021" license = "MIT" name = "dessin-svg" -repository = "https://github.com/daedalus-aero-space/dessin" -version = "0.7.3" +repository = "https://github.com/432-technologies/dessin" +version = "0.8.0" [dependencies] -base64 = "0.21.0" -dessin = { version = "0.7.3", path = "../dessin" } +data-encoding = "^2.8.0" +dessin = { version = "0.8.0", path = "../dessin" } +image = "0.24" +nalgebra = "^0.33" +rand = "^0.9.0" diff --git a/dessin-svg/README.md b/dessin-svg/README.md index 5ac6978..b2c4e91 100644 --- a/dessin-svg/README.md +++ b/dessin-svg/README.md @@ -1 +1,3 @@ -# Dessin SVG \ No newline at end of file +# dessin-svg + +License: MIT diff --git a/dessin-svg/src/lib.rs b/dessin-svg/src/lib.rs index 5000875..54ea12b 100644 --- a/dessin-svg/src/lib.rs +++ b/dessin-svg/src/lib.rs @@ -1,98 +1,489 @@ -//! Export a [`Drawing`] in [`SVG`]. -//! -//! [`Drawing`]: https://github.com/daedalus-aero-space/drawing -//! [`SVG`]: https://www.w3.org/Graphics/SVG/ -//! -//! After importing [`ToSVG`][ToSVG] in scope, one can call [`ToSVG::to_svg`][ToSVG::to_svg] on any [`Drawing`][Drawing]. -//! -//! See the [`Dessin`] crate for more details on how to build a drawing. -//! -//! [`Dessin`]: https://github.com/daedalus-aero-space/drawing -//! ``` -//! use dessin::{shape::Text, style::{FontWeight, Fill, Color}, vec2, Drawing}; -//! use dessin_svg::ToSVG; -//! -//! let mut drawing = Drawing::empty().with_canvas_size(vec2(50., 50.)); -//! -//! drawing.add( -//! Text::new("Hello, world!".to_owned()) -//! .at(vec2(10., 10.)) -//! .with_font_weight(FontWeight::Bold) -//! .with_fill(Fill::Color(Color::U32(0xFF0000))) -//! ); -//! -//! let svg = drawing.to_svg().unwrap(); -//! -//! assert_eq!(svg, r#"Hello, world!"#); -//! ``` - -mod shapes; - -use dessin::Drawing; -use std::error::Error; - -pub trait ToSVG { - fn to_svg(&self) -> Result>; +use ::image::ImageFormat; +use dessin::{ + export::{Export, Exporter}, + font::FontRef, + prelude::*, +}; +use nalgebra::{Scale2, Transform2}; +use std::{ + collections::HashSet, + fmt::{self, Write}, + io::Cursor, +}; + +#[derive(Debug)] +pub enum SVGError { + WriteError(fmt::Error), + CurveHasNoStartingPoint(CurvePosition), +} +impl fmt::Display for SVGError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } +} +impl From for SVGError { + fn from(value: fmt::Error) -> Self { + SVGError::WriteError(value) + } +} +impl std::error::Error for SVGError {} + +#[derive(Default, Clone, Copy, PartialEq)] +pub enum ViewPort { + /// Create a viewport centered around (0, 0), with size (width, height) + ManualCentered { width: f32, height: f32 }, + /// Create a viewport centered around (x, y), with size (width, height) + ManualViewport { + x: f32, + y: f32, + width: f32, + height: f32, + }, + /// Create a Viewport centered around (0, 0), with auto size that include all [Shapes][`dessin::prelude::Shape`] + AutoCentered, + #[default] + /// Create a Viewport centered around the centered of the shapes, with auto size that include all [Shapes][`dessin::prelude::Shape`] + AutoBoundingBox, } -/// Implementation of ToSVG for Drawing. -/// ``` -/// # use dessin::{shape::*, style::*, *}; -/// # use dessin_svg::ToSVG; -/// -/// let mut drawing = Drawing::empty().with_canvas_size(vec2(50., 50.)); -/// -/// drawing.add( -/// Text::new("Hello, world!".to_owned()) -/// .at(vec2(10., 10.)) -/// .with_font_weight(FontWeight::Bold) -/// .with_fill(Fill::Color(Color::U32(0xFF0000))) -/// ); -/// -/// let svg = drawing.to_svg().unwrap(); -/// -/// assert_eq!(svg, r#"Hello, world!"#); -/// ``` -impl ToSVG for Drawing { - fn to_svg(&self) -> Result> { - let offset = -self.canvas_size() / 2.; - Ok(format!( - r#"{}"#, - self.shapes().to_svg()?, - // self.shapes()[0], - offset_x = offset.x, - offset_y = offset.y, - max_x = self.canvas_size().x, - max_y = self.canvas_size().y, - )) - } +#[derive(Default, Clone)] +pub struct SVGOptions { + pub viewport: ViewPort, } -impl ToSVG for Vec { - fn to_svg(&self) -> Result> { - self.iter() - .map(|v| v.to_svg()) - .collect::>>() - } +pub struct SVGExporter { + start: String, + acc: String, + used_font: HashSet<(FontRef, FontWeight)>, } -#[cfg(test)] -mod tests { - use super::*; +impl SVGExporter { + // fn new(min_x: f32, min_y: f32, span_x: f32, span_y: f32) -> Self { + fn new(min_x: f32, min_y: f32, span_x: f32, span_y: f32) -> Self { + const SCHEME: &str = + r#"xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink""#; + + let start = format!(r#""#,); + let acc = String::new(); + let stock: HashSet<(FontRef, FontWeight)> = HashSet::default(); + + SVGExporter { + start, + acc, + used_font: stock, + } + } + + fn write_style(&mut self, style: StylePosition) -> Result<(), SVGError> { + match style.fill { + Some(color) => write!( + self.acc, + "fill='rgb({} {} {} / {:.3})' ", + (color.red * 255.) as u32, + (color.green * 255.) as u32, + (color.blue * 255.) as u32, + color.alpha + )?, // pass [0;1] number to [0;255] for a working CSS code (not needed for alpha) + + None => write!(self.acc, "fill='none' ")?, + } - struct MyStruct; - impl ToSVG for MyStruct { - fn to_svg(&self) -> Result> { - Ok("MyStruct".to_owned()) + match style.stroke { + Some(Stroke::Dashed { + color, + width, + on, + off, + }) => write!( + self.acc, + "stroke='rgb({} {} {} / {:.3})' stroke-width='{width}' stroke-dasharray='{on},{off}' ", + (color.red * 255.) as u32, + (color.green * 255.) as u32, + (color.blue * 255.) as u32, + color.alpha + )?, + Some(Stroke::Full { color, width }) => { + write!(self.acc, "stroke='rgb({} {} {} / {:.3})' stroke-width='{width}' ", + (color.red * 255.) as u32, + (color.green * 255.) as u32, + (color.blue * 255.) as u32, + color.alpha + )? + } + + None => {} } - } - - #[test] - fn test_vec() { - let mut v = Vec::::new(); - v.push(MyStruct); - assert_eq!(v.to_svg().unwrap(), "MyStruct".to_owned()); - v.push(MyStruct); - assert_eq!(v.to_svg().unwrap(), "MyStructMyStruct".to_owned()); - } + + Ok(()) + } + + #[allow(unused)] + fn write_curve(&mut self, curve: CurvePosition) -> Result<(), SVGError> { + let mut has_start = false; + + for keypoint in &curve.keypoints { + match keypoint { + KeypointPosition::Point(p) => { + if has_start { + write!(self.acc, "L ")?; + } else { + write!(self.acc, "M ")?; + has_start = true; + } + write!(self.acc, "{} {} ", p.x, p.y)?; + } + KeypointPosition::Bezier(b) => { + if has_start { + if let Some(v) = b.start { + write!(self.acc, "L {} {} ", v.x, v.y)?; + } + } else { + if let Some(v) = b.start { + write!(self.acc, "M {} {} ", v.x, v.y)?; + has_start = true; + } else { + return Err(SVGError::CurveHasNoStartingPoint(curve)); + } + } + + write!( + self.acc, + "C {start_ctrl_x} {start_ctrl_y} {end_ctrl_x} {end_ctrl_y} {end_x} {end_y} ", + start_ctrl_x = b.start_control.x, + start_ctrl_y = b.start_control.y, + end_ctrl_x = b.end_control.x, + end_ctrl_y = b.end_control.y, + end_x = b.end.x, + end_y = b.end.y, + )?; + } + } + + has_start = true; + } + + if curve.closed { + write!(self.acc, "Z",)?; + } + + Ok(()) + } + + fn finish(self) -> String { + let return_fonts = self + .used_font + .into_iter() + .map(move |(font_ref, font_weight)| { + let font_name = font_ref.name(font_weight); + let font_group = font::get(font_ref); + let (mime, bytes) = match font_group.get(font_weight) { + dessin::font::Font::OTF(bytes) => ("font/otf", bytes), + dessin::font::Font::TTF(bytes) => ("font/ttf", bytes), + }; + + // creates a base 64 ending font using previous imports + let encoded_font_bytes = data_encoding::BASE64.encode(&bytes); + format!( + r#"@font-face{{font-family:{font_name};src:url("data:{mime};base64,{encoded_font_bytes}");}}"# + ) + }) + .collect::(); + + if return_fonts.is_empty() { + format!("{}{}", self.start, self.acc) + } else { + format!( + "{}{}", + self.start, self.acc + ) + } + } +} + +impl Exporter for SVGExporter { + type Error = SVGError; + + const CAN_EXPORT_ELLIPSE: bool = true; + + fn start_style(&mut self, style: StylePosition) -> Result<(), Self::Error> { + write!(self.acc, "")?; + + Ok(()) + } + + fn end_style(&mut self) -> Result<(), Self::Error> { + write!(self.acc, "")?; + Ok(()) + } + + fn start_block(&mut self, _metadata: &[(String, String)]) -> Result<(), Self::Error> { + if !_metadata.is_empty() { + write!(self.acc, "")?; + } + + Ok(()) + } + + fn end_block(&mut self, _metadata: &[(String, String)]) -> Result<(), Self::Error> { + if !_metadata.is_empty() { + write!(self.acc, "")?; + } + Ok(()) + } + + fn export_image( + &mut self, + ImagePosition { + top_left: _, + top_right: _, + bottom_right: _, + bottom_left: _, + center, + width, + height, + rotation, + image, + }: ImagePosition, + ) -> Result<(), Self::Error> { + let mut raw_image = Cursor::new(vec![]); + image.write_to(&mut raw_image, ImageFormat::Png).unwrap(); + + let data = data_encoding::BASE64.encode(&raw_image.into_inner()); + + write!( + self.acc, + r#" 10e-6 { + write!( + self.acc, + r#" transform="rotate({rot})" "#, + rot = (-rotation.to_degrees() + 360.) % 360. + )?; + } + + write!(self.acc, r#"href="data:image/png;base64,{data}"/>"#,)?; + + Ok(()) + } + + fn export_ellipse( + &mut self, + EllipsePosition { + center, + semi_major_axis, + semi_minor_axis, + rotation, + }: EllipsePosition, + ) -> Result<(), Self::Error> { + write!( + self.acc, + r#" 10e-6 { + write!(self.acc, r#"rotate({rot}) "#, rot = -rotation.to_degrees())?; + } + + write!(self.acc, r#""/>"#)?; + + Ok(()) + } + + fn export_curve( + &mut self, + curve: CurvePosition, + StylePosition { fill, stroke }: StylePosition, + ) -> Result<(), Self::Error> { + write!(self.acc, r#""#)?; + + Ok(()) + } + + fn export_text( + &mut self, + TextPosition { + text, + align, + font_weight, + on_curve, + font_size, + reference_start, + direction, + font, + }: TextPosition, + ) -> Result<(), Self::Error> { + let id = rand::random::().to_string(); + + let weight = match font_weight { + FontWeight::Bold | FontWeight::BoldItalic => "bold", + _ => "normal", + }; + let text_style = match font_weight { + FontWeight::Italic | FontWeight::BoldItalic => "italic", + _ => "normal", + }; + let align = match align { + TextAlign::Center => "middle", + TextAlign::Left => "start", + TextAlign::Right => "end", + }; + + let text = text.replace("<", "<").replace(">", ">"); + + let font = font.clone().unwrap_or(FontRef::default()); + + self.used_font.insert((font.clone(), font_weight)); + + // let font_group = font::get(font.clone()); + + let font = font.name(font_weight); + + // let raw_font = match font_weight { + // FontWeight::Regular => font_group.regular, + // FontWeight::Bold => font_group + // .bold + // .as_ref() + // .unwrap_or_else(|| &font_group.regular) + // .clone(), + // FontWeight::BoldItalic => font_group + // .bold_italic + // .as_ref() + // .unwrap_or_else(|| &font_group.regular) + // .clone(), + // FontWeight::Italic => font_group + // .italic + // .as_ref() + // .unwrap_or_else(|| &font_group.regular) + // .clone(), + // }; + + write!( + self.acc, + r#" 10e-6 { + write!(self.acc, r#"rotate({rot}) "#, rot = rotation.to_degrees())?; + } + + write!(self.acc, r#"">"#)?; + + if let Some(curve) = on_curve { + write!(self.acc, r#""#)?; + + write!(self.acc, r##"{text}"##)?; + } else { + write!(self.acc, "{text}")?; + } + write!(self.acc, r#""#)?; + + Ok(()) + } +} + +pub fn to_string_with_options( + shape: &Shape, + options: SVGOptions, + // StylePosition { fill, stroke }: StylePosition, -- +) -> Result { + let (min_x, min_y, span_x, span_y) = match options.viewport { + ViewPort::ManualCentered { width, height } => (-width / 2., -height / 2., width, height), + ViewPort::ManualViewport { + x, + y, + width, + height, + } => (x - width / 2., y - height / 2., width, height), + ViewPort::AutoCentered => { + let bb = shape.local_bounding_box().straigthen(); + + let mirror_bb = bb + .transform(&nalgebra::convert::<_, Transform2>(Scale2::new( + -1., -1., + ))) + .into_straight(); + + let overall_bb = bb.join(mirror_bb); + + ( + -overall_bb.width() / 2., + -overall_bb.height() / 2., + overall_bb.width(), + overall_bb.height(), + ) + } + ViewPort::AutoBoundingBox => { + let bb = shape.local_bounding_box().straigthen(); + + (bb.top_left().x, -bb.top_left().y, bb.width(), bb.height()) + } + }; + + let mut exporter = SVGExporter::new(min_x, min_y, span_x, span_y); + + let parent_transform = nalgebra::convert(Scale2::new(1., -1.)); + + // shape.write_into_exporter( + // &mut exporter, + // &parent_transform, + // StylePosition { + // fill: None, + // stroke: None, + // }, + // )?; + + if let Shape::Style { fill, stroke, .. } = shape { + shape.write_into_exporter( + &mut exporter, + &parent_transform, + StylePosition { + fill: *fill, + stroke: *stroke, + }, + )? //Needed to be complete + } else { + shape.write_into_exporter( + &mut exporter, + &parent_transform, + StylePosition { + fill: None, + stroke: None, + }, + )? + } + + Ok(exporter.finish()) +} + +pub fn to_string(shape: &Shape) -> Result { + to_string_with_options(shape, SVGOptions::default()) // Needed to add StylePosition { fill, stroke } using shape } diff --git a/dessin-svg/src/shapes.rs b/dessin-svg/src/shapes.rs deleted file mode 100644 index a2a3e21..0000000 --- a/dessin-svg/src/shapes.rs +++ /dev/null @@ -1,269 +0,0 @@ -use crate::ToSVG; -use base64::Engine; -use dessin::{shape::*, style::*, Shape, ShapeType}; -use std::error::Error; - -impl ToSVG for Shape { - fn to_svg(&self) -> Result> { - let pos = self.pos.position_from_center(); - let size = self.pos.size(); - match &self.shape_type { - ShapeType::Text { - text, - align, - font_size, - font_weight, - } => Ok(format!( - r#"{text}"#, - x = pos.x, - y = -pos.y, - anchor = align.to_svg()?, - size = font_size, - weight = font_weight.to_svg()?, - style = self.style.to_svg()?, - text = text, - )), - ShapeType::Line { from, to } => Ok(format!( - r#""#, - x1 = from.x, - y1 = -from.y, - x2 = to.x, - y2 = -to.y, - style = self.style.to_svg()?, - )), - ShapeType::Circle { radius } => Ok(format!( - r#""#, - x = pos.x, - y = -pos.y, - r = radius, - style = self.style.to_svg()?, - )), - ShapeType::Image { data } => Ok(format!( - r#""#, - x = pos.x - size.x / 2., - y = -pos.y - size.y / 2., - width = size.x, - height = size.y, - href = match data { - ImageFormat::PNG(ref d) => { - format!("data:image/png;base64,{}", base64::engine::general_purpose::STANDARD_NO_PAD.encode(d)) - } - ImageFormat::JPEG(ref d) => { - format!("data:image/jpeg;base64,{}", base64::engine::general_purpose::STANDARD_NO_PAD.encode(d)) - } - ImageFormat::Webp(ref d) => { - format!("data:image/webp;base64,{}", base64::engine::general_purpose::STANDARD_NO_PAD.encode(d)) - } - } - )), - ShapeType::Drawing(shapes) => shapes.to_svg(), - ShapeType::Path { keypoints, closed } => { - let start = keypoints.first().ok_or("No start")?; - let rest = &keypoints[1..]; - - Ok(format!( - r#""#, - style = self.style.to_svg()?, - start = if let Keypoint::Point(start) = start { - format!("M {} {} ", start.x, -start.y) - } else { - unreachable!(); - }, - rest = rest - .iter() - .map(|v| match v { - Keypoint::Point(p) => format!("L {} {} ", p.x, -p.y), - Keypoint::BezierQuad { to, control } => - format!("Q {} {} {} {} ", control.x, -control.y, to.x, -to.y,), - Keypoint::BezierCubic { - to, - control_from, - control_to, - } => format!( - "C {} {} {} {} {} {} ", - control_from.x, - -control_from.y, - control_to.x, - -control_to.y, - to.x, - -to.y, - ), - }) - .collect::(), - close = if *closed { "Z" } else { "" } - )) - } - } - } -} - -impl ToSVG for OptionHere wewritesometext \ No newline at end of file diff --git a/examples/out/text_rotation.png b/examples/out/text_rotation.png new file mode 100644 index 0000000..35d65f3 Binary files /dev/null and b/examples/out/text_rotation.png differ diff --git a/examples/out/text_rotation.svg b/examples/out/text_rotation.svg new file mode 100644 index 0000000..f60078c --- /dev/null +++ b/examples/out/text_rotation.svg @@ -0,0 +1 @@ +Helloworld!Thisisme! \ No newline at end of file diff --git a/examples/out/yellow_thick_arc.svg b/examples/out/yellow_thick_arc.svg new file mode 100644 index 0000000..791174a --- /dev/null +++ b/examples/out/yellow_thick_arc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/purple_padding.rs b/examples/purple_padding.rs new file mode 100644 index 0000000..20de0fa --- /dev/null +++ b/examples/purple_padding.rs @@ -0,0 +1,50 @@ +use dessin::prelude::*; +use palette::Srgba; +use project_root::get_project_root; +use std::fs; + +fn main() { + let rectangle_1 = dessin!(*Rectangle( + width = 3., + height = 2., + translate = [1., 0.], + fill = Srgba::new(1.0, 0.0, 1.0, 1.0) + )); + + let base = dessin!(Padding( // here, we can replace 'Shape' with 'Rectangle' but in case we want to use the + // Padding to a multiple geometric form, using Shape become a must + shape = rectangle_1.clone(), + padding_left = 1.5, + padding_right = 1., + padding_top = 0.8, + padding_bottom = 1., + + )); + + let rectangle_2 = dessin!(*Rectangle( + width = 5.5, + height = 3.8, + stroke = Stroke::new_full(Srgba::new(0.0, 0.7, 0.0, 1.0), 0.2), + translate = [0.75, -0.1] + )); + + let base = Shape::from(base); + let rectangle_1 = Shape::from(rectangle_1); + let rectangle_2 = Shape::from(rectangle_2); + + // creates a group + let mut group = Group::default(); + + group.shapes = vec![]; + + group.shapes.push(base); + group.shapes.push(rectangle_1); + group.shapes.push(rectangle_2); + + // prints in svg version + fs::write( + get_project_root().unwrap().join("examples/out/padding.svg"), + dessin_svg::to_string(&Shape::Group(group)).unwrap(), + ) + .unwrap(); +} diff --git a/examples/red_circle_with_macro.rs b/examples/red_circle_with_macro.rs new file mode 100644 index 0000000..033ca8a --- /dev/null +++ b/examples/red_circle_with_macro.rs @@ -0,0 +1,25 @@ +use dessin::{nalgebra::Rotation2, prelude::*}; +use palette::{Srgb, Srgba}; +use project_root::get_project_root; +use std::fs; + +fn main() { + let circle: Shape = dessin!([*Circle( + // chooses a radius of 11 + radius = 11., //11. is like a proportion of the box allowed + // paints the inside of the circle in red + fill = Srgba::new(1.0, 0.0, 0.0, 1.0), + // creates a grey margin of 0.2 (0.1 outside and 0.1 inside the circle) + stroke = Stroke::new_full(Srgb::new(0.576, 0.576, 0.576), 0.2), + rotate = Rotation2::new(0_f32.to_radians()), //not visible yet but it's possible to see it in some conditions + ),]); + + // prints in svg version + fs::write( + get_project_root() + .unwrap() + .join("examples/out/red_circle.svg"), + dessin_svg::to_string(&circle).unwrap(), + ) + .unwrap(); +} diff --git a/examples/red_circle_without_macro.rs b/examples/red_circle_without_macro.rs new file mode 100644 index 0000000..a374430 --- /dev/null +++ b/examples/red_circle_without_macro.rs @@ -0,0 +1,30 @@ +use dessin::prelude::*; +use palette::{Srgb, Srgba}; +use project_root::get_project_root; +use std::fs; + +fn main() { + // creates a circle with radius of 11 + let circle = Circle::default().with_radius(11.); + + let mut circle = Style::new(circle); + + // paints the inside of the circle in red + circle.fill(Srgba::new(1.0, 0.0, 0.0, 1.0)); + + // creates a grey margin of 0.2 (0.1 outside and 0.1 inside the circle) + circle.stroke(Stroke::new_full(Srgba::new(0.576, 0.576, 0.576, 1.0), 0.2)); + + // let circle = Style::new(circle) + // .with_fill(Srgb::new(1.0, 0.0, 0.0)) + // .with_stroke(Stroke::new_full(Srgb::new(0.376, 0.376, 0.376), 0.2)); + + //prints in svg version + fs::write( + get_project_root() + .unwrap() + .join("examples/out/red_circle.svg"), + dessin_svg::to_string(&circle.into()).unwrap(), + ) + .unwrap(); +} diff --git a/examples/right_angle_triangle_with_macro.rs b/examples/right_angle_triangle_with_macro.rs new file mode 100644 index 0000000..e21294a --- /dev/null +++ b/examples/right_angle_triangle_with_macro.rs @@ -0,0 +1,34 @@ +use dessin::{nalgebra::Rotation2, prelude::*}; +use palette::{Srgb, Srgba}; +use project_root::get_project_root; +use std::{f32::consts::PI, fs}; + +fn main() { + let triangle: Shape = dessin!([ + *Triangle( + //chooses the size of the first side of the triangle which is on the x axis without rotation : 3 + width_x_axis = 3., + //chooses the size of the second side of the triangle : 4 + size_axis_angle = 4., + // chooses a right angle in radiant which is : PI/2 or 3PI/2 + angle = PI / 2., + // paints the inside of the triangle in green + fill = Srgb::new(0.0, 0.0, 0.392), + // creates a black pointing margin with a width of 0.1 (0.05 outside and the same inside the triangle), a length of 0.2 and + // a space of 0.1 between each of them + stroke = Stroke::new_dashed(Srgba::new(0.0, 0.0, 0.0, 0.2522115), 0.1, 0.2, 0.1), + // chooses a rotation of 0 radians in the trigonometric direction + rotate = Rotation2::new(0_f32.to_radians()) + ), + //here, the hypotenuse should be 5 + ]); + + // prints in svg version + fs::write( + get_project_root() + .unwrap() + .join("examples/out/right_angle_triangle.svg"), + dessin_svg::to_string(&triangle).unwrap(), + ) + .unwrap(); +} diff --git a/examples/right_angle_triangle_without_macro.rs b/examples/right_angle_triangle_without_macro.rs new file mode 100644 index 0000000..b016bd7 --- /dev/null +++ b/examples/right_angle_triangle_without_macro.rs @@ -0,0 +1,42 @@ +use dessin::{nalgebra::Rotation2, prelude::*}; +use palette::{Srgb, Srgba}; +use project_root::get_project_root; +use std::{f32::consts::PI, fs}; + +fn main() { + let triangle = Triangle::default(); + + let mut triangle = Style::new(triangle); + + // chooses the size of the first side of the triangle which is on the x axis without rotation : 3 + triangle.width_x_axis(3.); + + // chooses the size of the second side of the triangle : 4 + triangle.size_axis_angle(4.); + + // chooses a right angle in radiant which is : PI/2 or 5PI/2 + triangle.angle(PI / 2.); + + // paints the inside of the triangle in blue + triangle.fill(Srgb::new(0.0, 0.0, 0.392)); + + // creates a black margin of 0.1 (0.05 outside and 0.05 inside the triangle) + triangle.stroke(Stroke::new_dashed( + Srgba::new(0.0, 0.0, 0.0, 0.2522115), + 0.1, + 0.2, + 0.1, + )); + + // chooses a rotation of 0 radians in the trigonometric direction + triangle.rotate(Rotation2::new(0_f32.to_radians())); + + // prints in svg version + fs::write( + get_project_root() + .unwrap() + .join("examples/out/right_angle_triangle.svg"), + dessin_svg::to_string(&triangle.into()).unwrap(), + ) + .unwrap(); +} diff --git a/examples/src/lib.rs b/examples/src/lib.rs new file mode 100644 index 0000000..9d55749 --- /dev/null +++ b/examples/src/lib.rs @@ -0,0 +1,2 @@ +#[doc = include_str!("../../README.md")] +pub struct Doc {} diff --git a/examples/svg-pdf/.gitignore b/examples/svg-pdf/.gitignore deleted file mode 100644 index 52cf476..0000000 --- a/examples/svg-pdf/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -dessin.html -embedded.html -dessin.pdf -embedded.pdf \ No newline at end of file diff --git a/examples/svg-pdf/Cargo.toml b/examples/svg-pdf/Cargo.toml deleted file mode 100644 index ffc06b9..0000000 --- a/examples/svg-pdf/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "svg-pdf" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -dessin = { version = "0.7", path = "../../dessin" } -dessin-svg = { version = "0.7", path = "../../dessin-svg" } -dessin-pdf = { version = "0.7", path = "../../dessin-pdf" } diff --git a/examples/svg-pdf/src/main.rs b/examples/svg-pdf/src/main.rs deleted file mode 100644 index a665b3d..0000000 --- a/examples/svg-pdf/src/main.rs +++ /dev/null @@ -1,168 +0,0 @@ -use dessin::{ - contrib::{TextBox, TextLayout}, - shape::{Circle, Color, EmbeddedDrawing, Fill, Image, ImageFormat, Line, Stroke, Text}, - style::TextAlign, - vec2, Drawing, -}; -use dessin_pdf::ToPDF; -use dessin_svg::ToSVG; -use std::{error::Error, fs::write}; - -pub fn dessin() -> Drawing { - let mut drawing = Drawing::empty().with_canvas_size(vec2(300., 300.)); - - const TEXT_BOX_CONTENT: &str = - "This is a long long test to see if the textbox works as intended. -On top of that, the output should be the same on PDF and SVG!"; - - drawing - .add( - TextLayout::new(TEXT_BOX_CONTENT.to_owned()) - .add_box( - TextBox::new() - .at(vec2(-120., 120.)) - .with_size(vec2(30., 30.)) - .with_font_size(4.) - .with_spacing(4.) - .with_fill(Fill::Color(Color::RED)) - .with_align(TextAlign::Center), - ) - .add_box( - TextBox::new() - .at(vec2(120., 120.)) - .with_size(vec2(30., 60.)) - .with_font_size(5.) - .with_fill(Fill::Color(Color::BLACK)) - .with_align(TextAlign::Right), - ), - ) - .add( - Text::new("Hello World".to_owned()) - .at(vec2(0., -10.)) - .with_align(TextAlign::Center) - .with_fill(Fill::Color(Color::U32(0xFF0000))), - ) - .add( - Line::from(vec2(-50., 5.)) - .to(vec2(50., 5.)) - .with_stroke(Stroke::Dashed { - color: Color::U32(0xFF0000), - width: 2., - on: 4., - off: 1., - }), - ) - .add( - Line::from(vec2(-50., -15.)) - .to(vec2(50., -15.)) - .with_stroke(Stroke::Dashed { - color: Color::U32(0xFF0000), - width: 2., - on: 4., - off: 1., - }), - ) - .add( - Line::from(vec2(-50., -15.)) - .to(vec2(-50., 5.)) - .with_stroke(Stroke::Dashed { - color: Color::U32(0xFF0000), - width: 2., - on: 4., - off: 1., - }), - ) - .add( - Line::from(vec2(50., -15.)) - .to(vec2(50., 5.)) - .with_stroke(Stroke::Dashed { - color: Color::U32(0xFF0000), - width: 2., - on: 4., - off: 1., - }), - ) - .add( - Circle::new() - .at(vec2(0., 0.)) - .with_radius(100.) - .with_stroke(Stroke::Full { - color: Color::U32(0xFF0000), - width: 5., - }), - ); - - drawing -} - -pub fn embedded(dessin: Drawing) -> Drawing { - let mut parent = Drawing::empty().with_canvas_size(vec2(100., 100.)); - parent - .add( - EmbeddedDrawing::new(dessin.clone()) - .at(vec2(-35., -35.)) - .with_size(vec2(10., 10.)), - ) - .add( - EmbeddedDrawing::new(dessin) - .at(vec2(35., -35.)) - .with_size(vec2(10., 10.)), - ) - .add( - Text::new("Meta Hello World".to_owned()) - .at(vec2(-30., -38.)) - .with_font_size(8.) - .with_fill(Fill::Color(Color::U32(0x1000FF))), - ) - .add( - Line::from(vec2(-40., -40.)) - .to(vec2(40., -40.)) - .with_stroke(Stroke::Full { - color: Color::U32(0x1000FF), - width: 1., - }), - ) - .add( - Line::from(vec2(-40., -30.)) - .to(vec2(40., -30.)) - .with_stroke(Stroke::Full { - color: Color::U32(0x1000FF), - width: 1., - }), - ) - .add( - Image::new(ImageFormat::JPEG( - include_bytes!("rustacean-flat-happy.jpg").to_vec(), - )) - .at(vec2(-25., 0.)) - .with_size(vec2(50., 40.)), - ); - - parent -} - -pub fn main() -> Result<(), Box> { - // SVG - { - let dessin = dessin(); - write( - "./dessin.html", - format!(r#"{}"#, dessin.to_svg()?), - )?; - write( - "./embedded.html", - format!( - r#"{}"#, - embedded(dessin).to_svg()? - ), - )?; - } - // PDF - { - let dessin = dessin(); - write("./dessin.pdf", dessin.to_pdf()?.into_bytes()?)?; - write("./embedded.pdf", embedded(dessin).to_pdf()?.into_bytes()?)?; - } - - Ok(()) -} diff --git a/examples/svg-pdf/src/rustacean-flat-happy.jpg b/examples/svg-pdf/src/rustacean-flat-happy.jpg deleted file mode 100644 index 9b1a53b..0000000 Binary files a/examples/svg-pdf/src/rustacean-flat-happy.jpg and /dev/null differ diff --git a/examples/svg-pdf/src/rustacean-flat-happy.png b/examples/svg-pdf/src/rustacean-flat-happy.png deleted file mode 100644 index ebce1a1..0000000 Binary files a/examples/svg-pdf/src/rustacean-flat-happy.png and /dev/null differ diff --git a/examples/text_rotation.rs b/examples/text_rotation.rs new file mode 100644 index 0000000..e8bcf28 --- /dev/null +++ b/examples/text_rotation.rs @@ -0,0 +1,65 @@ +use dessin::{nalgebra::Rotation2, prelude::*}; +use dessin_image::ToImage; +use palette::{named, Srgb}; +use project_root::get_project_root; +use std::{f32::consts::PI, fs}; + +#[derive(Shape, Default)] +struct RotatedText { + #[shape(into)] + text: String, + rotation: f32, +} +impl From for Shape { + fn from(RotatedText { text, rotation }: RotatedText) -> Self { + let text = dessin!(*Text( + fill = Srgb::::from_format(named::BLACK).into_linear(), + font_size = 1., + align = TextAlign::Center, + vertical_align = TextVerticalAlign::Top, + { text }, + )); + + let bb = text.local_bounding_box(); + let width = bb.width(); + let height = bb.height(); + + dessin!( + [ + *Rectangle( + { width }, + { height }, + stroke = + Stroke::new_full(Srgb::::from_format(named::BLACK).into_linear(), 0.1) + ), + { text }, + ] > (translate = [0., 15.], rotate = Rotation2::new(rotation),) + ) + .into() + } +} + +fn main() { + let dessin = dessin!( + for (idx, text) in "Hello world! This is me!".split(" ").enumerate() { + dessin!(RotatedText(rotation = idx as f32 * -PI / 4., { text })) + } + ); + + let path = get_project_root().unwrap().join("examples/out/"); + + // SVG + fs::write( + path.join("text_rotation.svg"), + dessin_svg::to_string(&dessin.clone()).unwrap(), + ) + .unwrap(); + + // Image + dessin!({ dessin }(scale = [5., 5.])) + .rasterize() + .unwrap() + .into_rgba8() + .save(path.join("text_rotation.png")) + .unwrap(); +} diff --git a/examples/textbox_with_macro.rs b/examples/textbox_with_macro.rs new file mode 100644 index 0000000..cf3c754 --- /dev/null +++ b/examples/textbox_with_macro.rs @@ -0,0 +1,32 @@ +use dessin::{nalgebra::Rotation2, prelude::*}; +use palette::{Srgb, Srgba}; +use project_root::get_project_root; +use std::fs; + +fn main() { + let text: Shape = dessin!([*TextBox( + font_size = 5., + line_spacing = 1., + text = "Here we write some text", + width = 20., + height = 10., + font_weight = FontWeight::Italic, + // chooses centered vertical allign + vertical_align = TextVerticalAlign::Center, + // selects to align the beginning of the text on the left + align = TextAlign::Left, + // paints the inside of the text in bright orange + fill = Srgba::new(1.0, 0.749, 0.0, 1.0), + // We decide to not use stroke but it is possible + stroke = Stroke::new_full(Srgb::new(0.588, 0.039, 0.039), 0.1), + // chooses a rotation of 6 radians in the trigonometric direction + rotate = Rotation2::new(6_f32.to_radians()) + ),]); + + // prints in svg version + fs::write( + get_project_root().unwrap().join("examples/out/text.svg"), + dessin_svg::to_string(&text).unwrap(), + ) + .unwrap(); +} diff --git a/examples/textbox_without_macro.rs b/examples/textbox_without_macro.rs new file mode 100644 index 0000000..05a5766 --- /dev/null +++ b/examples/textbox_without_macro.rs @@ -0,0 +1,44 @@ +use dessin::{nalgebra::Rotation2, prelude::*}; +use palette::{Srgb, Srgba}; +use project_root::get_project_root; +use std::fs; + +fn main() { + // creates a text + let text = TextBox::default(); + + let mut text = Style::new(text); + + text.font_size(5.); + + text.line_spacing(1.); + + text.text("Here we write some text"); + + text.width(20.); + + text.height(10.); + + text.font_weight(FontWeight::Italic); + + // chooses centered vertical allign + text.vertical_align(TextVerticalAlign::Center); + + // selects to align the beginning of the text on the left + text.align(TextAlign::Left); + + // paints the inside of the text in bright orange + text.fill(Srgba::new(1.0, 0.749, 0.0, 1.0)); + + text.stroke(Stroke::new_full(Srgb::new(0.588, 0.039, 0.039), 0.1)); + + // chooses a rotation of -6 radians in the trigonometric direction + text.rotate(Rotation2::new(6_f32.to_radians())); + + // prints in svg version + fs::write( + get_project_root().unwrap().join("examples/out/text.svg"), + dessin_svg::to_string(&text.into()).unwrap(), + ) + .unwrap(); +} diff --git a/examples/yellow_thick_arc_with_macro.rs b/examples/yellow_thick_arc_with_macro.rs new file mode 100644 index 0000000..62af276 --- /dev/null +++ b/examples/yellow_thick_arc_with_macro.rs @@ -0,0 +1,30 @@ +use dessin::{nalgebra::Rotation2, prelude::*}; +use palette::Srgba; +use project_root::get_project_root; +use std::{f32::consts::PI, fs}; + +fn main() { + let thick_arc: Shape = dessin!([*ThickArc( + // chooses a radius of 10 for the outer curve + outer_radius = 10., + // chooses a radius of 5 for the inner curve + inner_radius = 5., + // chooses an angle of PI to show the area of the thick arc (which depends of the 2 curve and this angle) + span_angle = PI, + // paints the inside of the thick arc in yellow + fill = Srgba::new(1.0, 1.0, 0.0, 1.0), + // creates a black margin of 0.2 (0.05 outside and the same inside the thick arc) + stroke = Stroke::new_full(Srgba::new(0.0, 0.0, 0.0, 0.5), 0.5), + // chooses a rotation of Pi/3 in radians in the trigonometric direction + rotate = Rotation2::new(PI / 3_f32.to_radians()) + ),]); + + // prints in svg version + fs::write( + get_project_root() + .unwrap() + .join("examples/out/yellow_thick_arc.svg"), + dessin_svg::to_string(&thick_arc).unwrap(), + ) + .unwrap(); +} diff --git a/examples/yellow_thick_arc_without_macro.rs b/examples/yellow_thick_arc_without_macro.rs new file mode 100644 index 0000000..2f5fee5 --- /dev/null +++ b/examples/yellow_thick_arc_without_macro.rs @@ -0,0 +1,38 @@ +use dessin::{nalgebra::Rotation2, prelude::*}; +use palette::Srgba; +use project_root::get_project_root; +use std::{f32::consts::PI, fs}; + +fn main() { + // creates a rectangle with a width of 11 and a height of 6 + let thick_arc: ThickArc = ThickArc::default(); + + let mut thick_arc = Style::new(thick_arc); + + // chooses a radius of 10 for the outer curve + thick_arc.outer_radius = 10.; + + // chooses a radius of 5 for the inner curve + thick_arc.inner_radius = 5.; + + // chooses an angle of PI to show the area of the thick arc (which depends of the 2 curve and this angle) + thick_arc.span_angle(PI); + + // paints the inside of the thick_arc in yellow + thick_arc.fill(Srgba::new(1.0, 1.0, 0.0, 1.0)); + + // creates a black margin of 0.1 (0.05 outside and 0.05 inside the thick_arc) + thick_arc.stroke(Stroke::new_full(Srgba::new(0.0, 0.0, 0.0, 0.5), 0.5)); + + // chooses a rotation of PI/3 radians in the trigonometric direction + thick_arc.rotate(Rotation2::new(PI / 3_f32.to_radians())); + + // prints in svg version + fs::write( + get_project_root() + .unwrap() + .join("examples/out/yellow_thick_arc.svg"), + dessin_svg::to_string(&thick_arc.into()).unwrap(), + ) + .unwrap(); +} diff --git a/run-all-examples.sh b/run-all-examples.sh new file mode 100755 index 0000000..702d495 --- /dev/null +++ b/run-all-examples.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +for example in examples/*.rs +do + NO_ANIMATION=1 cargo run --example "$(basename "${example%.rs}")" +done \ No newline at end of file diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..f5d7c2b --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,7 @@ +hard_tabs = true +edition = "2021" +max_width = 100 +imports_granularity = "Crate" +newline_style = "Unix" +reorder_impl_items = true +group_imports = "One" diff --git a/test.svg b/test.svg new file mode 100644 index 0000000..09ce2c1 --- /dev/null +++ b/test.svg @@ -0,0 +1 @@ + \ No newline at end of file