Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 121 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions star/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ license.workspace = true

[features]
serde = ["dep:serde"]
image = ["dep:image", "dep:base64"]

[dependencies]
peg = "0.8"
Expand All @@ -26,5 +27,15 @@ workspace = true
optional = true
features = ["derive", "std"]

[dependencies.image]
version = "0.25"
optional = true
default-features = false
features = ["png", "jpeg"]

[dependencies.base64]
version = "0.22"
optional = true

[dev-dependencies]
serde_json.workspace = true
48 changes: 48 additions & 0 deletions star/src/lower/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,54 @@ impl<'a, T: Turtle> XmlVisitor for ConversionVisitor<'a, T> {
}
}
}
#[cfg(feature = "image")]
"image" => {
use base64::{Engine, engine::general_purpose::STANDARD};

use crate::turtle::elements::RasterImage;

let Some(href) = node
.attribute("href")
.or_else(|| node.attribute(("http://www.w3.org/1999/xlink", "href")))
else {
warn!("image element has no href: {node:?}");
return;
};
let Some(b64) = href
.strip_prefix("data:image/png;base64,")
.or_else(|| href.strip_prefix("data:image/jpeg;base64,"))
else {
warn!("Unsupported image href {href}");
return;
};

let b64_no_whitespace: String =
b64.chars().filter(|c| !c.is_ascii_whitespace()).collect();
let bytes = match STANDARD.decode(&b64_no_whitespace) {
Ok(bytes) => bytes,
Err(err) => {
warn!("image base64 decode failed: {err}");
return;
}
};
let image = match image::load_from_memory(&bytes) {
Ok(img) => img,
Err(e) => {
warn!("image decode failed: {e}");
return;
}
};

let x = self.length_attr_to_user_units(&node, "x").unwrap_or(0.);
let y = self.length_attr_to_user_units(&node, "y").unwrap_or(0.);
let width = self.length_attr_to_user_units(&node, "width").unwrap_or(0.);
let height = self
.length_attr_to_user_units(&node, "height")
.unwrap_or(0.);

self.comment(&node);
self.terrarium.image(image, x, y, width, height);
}
// No-op tags
SVG_TAG_NAME | GROUP_TAG_NAME | USE_TAG_NAME | SYMBOL_TAG_NAME => {}
_ => {
Expand Down
11 changes: 11 additions & 0 deletions star/src/turtle/dpi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,15 @@ impl<T: Turtle> Turtle for DpiConvertingTurtle<T> {
ctrl: self.point_to_mm(ctrl),
})
}

#[cfg(feature = "image")]
fn image(&mut self, img: super::elements::RasterImage) {
self.inner.image(super::elements::RasterImage {
x: self.to_mm(img.x),
y: self.to_mm(img.y),
width: self.to_mm(img.width),
height: self.to_mm(img.height),
image: img.image,
})
}
}
10 changes: 10 additions & 0 deletions star/src/turtle/elements/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ mod arc;
/// Reorders strokes to minimize pen-up travel using TSP heuristics
mod tsp;

/// Raster image decoded from an inline PNG/JPEG.
///
/// <https://www.w3.org/TR/SVG/embedded.html#ImageElement>
#[cfg(feature = "image")]
pub struct RasterImage {
pub position: Point<f64>,
pub dimensions: Vector<f64>,
pub image: image::DynamicImage,
}

/// Atomic unit of a [Stroke].
#[derive(Debug, Clone)]
pub enum DrawCommand {
Expand Down
20 changes: 20 additions & 0 deletions star/src/turtle/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ pub trait Turtle: Debug {
fn arc(&mut self, svg_arc: SvgArc<f64>);
fn cubic_bezier(&mut self, cbs: CubicBezierSegment<f64>);
fn quadratic_bezier(&mut self, qbs: QuadraticBezierSegment<f64>);
#[cfg(feature = "image")]
/// This is the only function with a default as most turtles have no way to handle a raster image.
fn image(&mut self, _image: self::elements::RasterImage) {}
}

/// Handles SVG complexities outside of [Turtle] scope (transforms, position, offsets, etc.)
Expand Down Expand Up @@ -330,6 +333,23 @@ impl<T: Turtle + std::fmt::Debug> Terrarium<T> {
self.turtle.arc(svg_arc);
}

/// <https://www.w3.org/TR/SVG/embedded.html#ImageElement>
#[cfg(feature = "image")]
pub fn image(&mut self, image: image::DynamicImage, x: f64, y: f64, width: f64, height: f64) {
// Transform the corners to get the final x, y, width, height.
let t0 = self.current_transform.transform_point(point(x, y));
let t1 = self
.current_transform
.transform_point(point(x, y) + vector(width, height));
self.turtle.image(crate::turtle::elements::RasterImage {
// After transformation, the corners may be swapped resulting in a new x y.
// Also need to pick the larger y because of the G-Code coordinate space swap (?).
position: point(t0.x.min(t1.x), t0.y.max(t1.y)),
dimensions: (t1 - t0).abs(),
image,
});
}

/// Push a generic transform onto the stack
/// Could be any valid CSS transform https://drafts.csswg.org/css-transforms-1/#typedef-transform-function
/// <https://www.w3.org/TR/SVG/coords.html#InterfaceSVGTransform>
Expand Down