Skip to content

Add vectorization support for raster images#4261

Draft
seam0s-dev wants to merge 3 commits into
GraphiteEditor:masterfrom
seam0s-dev:2332-raster-image-vectorization
Draft

Add vectorization support for raster images#4261
seam0s-dev wants to merge 3 commits into
GraphiteEditor:masterfrom
seam0s-dev:2332-raster-image-vectorization

Conversation

@seam0s-dev

Copy link
Copy Markdown
Contributor

As requested in #2332.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new vectorize node in the raster node-graph, integrating vtracer and visioncortex to convert raster images into vector subpaths. It also refactors SVG and usvg import utilities by moving them from the editor's graph operation handler into a reusable usvg_utils module within the rendering library. The review feedback highlights critical stability concerns, specifically potential editor crashes due to panics in error handling, and a compilation failure on Wasm targets due to the conditional compilation of the vectorize node. Additionally, the feedback suggests several code quality improvements, including avoiding unnecessary cloning of owned data, replacing println! statements with proper logging, removing commented-out legacy code, and making ParsedSvgText fields public for external access.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

path_precision: Some(6),
};

let vectorized_image = convert(color_image, config).expect("failed to obtain an SvgFile from vtracer.");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using .expect on convert will panic and crash the entire editor if vectorization fails (e.g., due to an empty image or memory allocation failure). It is much safer to log the error and return an empty vector.

			let vectorized_image = match convert(color_image, config) {
				Ok(img) => img,
				Err(e) => {
					log::error!("Failed to obtain an SvgFile from vtracer: {e}");
					return Item::from_parts(Vector::from_subpaths(vec![], true), row.attributes().clone());
				}
			};

Comment on lines +265 to +271
let svg_tree = match usvg::Tree::from_str(&image_svg, &usvg::Options::default()) {
Ok(t) => t,
Err(e) => {
// TODO: use proper error statements
panic!("Failed to create a usvg tree:\n{e}");
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Panicking on failed SVG parsing will crash the editor. Handle the error gracefully by logging it and returning an empty vector.

Suggested change
let svg_tree = match usvg::Tree::from_str(&image_svg, &usvg::Options::default()) {
Ok(t) => t,
Err(e) => {
// TODO: use proper error statements
panic!("Failed to create a usvg tree:\n{e}");
}
};
let svg_tree = match usvg::Tree::from_str(&image_svg, &usvg::Options::default()) {
Ok(t) => t,
Err(e) => {
log::error!("Failed to create a usvg tree: {e}");
return Item::from_parts(Vector::from_subpaths(vec![], true), row.attributes().clone());
}
};

Comment on lines +290 to +295
let parsed_group = if let ParsedSvgNode::Group(group) = parsed_node {
group
} else {
panic!("Must return a SVG group.");
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Avoid panicking if extract_usvg_node returns something other than a Group. Log an error and return gracefully instead.

						let parsed_node = extract_usvg_node(child, &graphite_gradient_stops);
						let parsed_group = if let ParsedSvgNode::Group(group) = parsed_node {
							group
						} else {
							log::error!("Expected a SVG group from extract_usvg_node.");
							return Item::from_parts(Vector::from_subpaths(vec![], true), row.attributes().clone());
						};

Comment on lines +233 to +235
#[cfg(not(target_family = "wasm"))]
#[node_macro::node(category("Raster"), path(core_types::vector))]
pub fn vectorize(_ctx: impl Ctx, image: List<Raster<CPU>>) -> List<Vector> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since Graphite is primarily compiled for Wasm, defining the vectorize node only for non-Wasm targets will cause compilation failures in the node registry or serialization code when compiling for Wasm. Providing a fallback implementation that logs a warning and returns an empty list ensures the codebase remains clean and compiles successfully across all targets.

#[cfg(target_family = "wasm")]
#[node_macro::node(category("Raster"), path(core_types::vector))]
pub fn vectorize(_ctx: impl Ctx, _image: List<Raster<CPU>>) -> List<Vector> {
	log::warn!("Vectorization is not supported on Wasm targets.");
	List::new()
}

#[cfg(not(target_family = "wasm"))]
#[node_macro::node(category("Raster"), path(core_types::vector))]
pub fn vectorize(_ctx: impl Ctx, image: List<Raster<CPU>>) -> List<Vector> {

Comment on lines +280 to +283
let parsed_path = extract_usvg_path(child, path, &graphite_gradient_stops);
let mut child_subpaths = parsed_path.subpaths.clone();
child_subpaths.iter_mut().for_each(|s| s.apply_transform(parsed_path.transform));
subpaths.extend(child_subpaths);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since parsed_path is owned, we can directly consume and modify its subpaths instead of cloning them, which avoids unnecessary allocations.

Suggested change
let parsed_path = extract_usvg_path(child, path, &graphite_gradient_stops);
let mut child_subpaths = parsed_path.subpaths.clone();
child_subpaths.iter_mut().for_each(|s| s.apply_transform(parsed_path.transform));
subpaths.extend(child_subpaths);
let mut parsed_path = extract_usvg_path(child, path, &graphite_gradient_stops);
parsed_path.subpaths.iter_mut().for_each(|s| s.apply_transform(parsed_path.transform));
subpaths.extend(parsed_path.subpaths);

Comment on lines +296 to +305
for child in parsed_group.children {
if let ParsedSvgNode::Path(path) = child {
let mut child_subpaths = path.subpaths.clone();
child_subpaths.iter_mut().for_each(|s| s.apply_transform(path.transform));
subpaths.extend(child_subpaths);

println!("Reading path (in a group) {} from a total of {}.", i, svg_tree.root().children().len());
i += 1;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since parsed_group.children is owned, we can iterate over it by value and move path.subpaths directly without cloning. Also, replace println! with log::debug!.

						for child in parsed_group.children {
							if let ParsedSvgNode::Path(mut path) = child {
								path.subpaths.iter_mut().for_each(|s| s.apply_transform(path.transform));
								subpaths.extend(path.subpaths);

								log::debug!("Reading path (in a group) {} from a total of {}.", i, svg_tree.root().children().len());
								i += 1;
							}
						}

Comment on lines +311 to +342
/*
let mut path = BezPath::new();
for subpath in vectorized_image.paths.iter() {
let svg_path = subpath.to_string();
println!("svg_path: {}", svg_path);
// TODO: use usvg instead to read from the svg path
let bezpath: BezPath = kurbo::BezPath::from_svg(svg_path.as_str()).expect("failed to convert SVG into BezPath.");

for curve in bezpath.segments() {
match curve {
PathSeg::Line(line) => {
println!("Inserting line: {:?}", line);
let a = transform.transform_point2(point_to_dvec2(line.p1));
path.line_to(kurbo::Point::new(a.x, a.y));
}
PathSeg::Quad(quad_bez) => {
println!("Inserting quad_bez: {:?}", quad_bez);
let a = transform.transform_point2(point_to_dvec2(quad_bez.p1));
let b = transform.transform_point2(point_to_dvec2(quad_bez.p2));
path.quad_to(kurbo::Point::new(a.x, a.y), kurbo::Point::new(b.x, b.y));
}
PathSeg::Cubic(cubic_bez) => {
println!("Inserting cubic_bez: {:?}", cubic_bez);
let a = transform.transform_point2(point_to_dvec2(cubic_bez.p1));
let b = transform.transform_point2(point_to_dvec2(cubic_bez.p2));
let c = transform.transform_point2(point_to_dvec2(cubic_bez.p3));
path.curve_to(kurbo::Point::new(a.x, a.y), kurbo::Point::new(b.x, b.y), kurbo::Point::new(c.x, c.y));
}
}
}
}
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Remove this large block of commented-out legacy code to keep the codebase clean and maintainable.

Comment on lines +168 to +171
pub struct ParsedSvgText {
text: String,
transform: DAffine2,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The fields of ParsedSvgText are currently private, which prevents them from being accessed outside of usvg_utils.rs. Making them pub ensures consistency with ParsedSvgGroup and ParsedSvgPath.

Suggested change
pub struct ParsedSvgText {
text: String,
transform: DAffine2,
}
pub struct ParsedSvgText {
pub text: String,
pub transform: DAffine2,
}

Comment on lines +288 to +306
// let mut child_extents_svg_order: Vec<u32> = Vec::new();
// let mut group_extents_map: HashMap<LayerNodeIdentifier, Vec<u32>> = HashMap::new();

// let get_child_extents = |group: &Box<Group>, group_extents_map: HashMap<LayerNodeIdentifier, Vec<u32>>| {
// let mut child_extents: Vec<u32> = Vec::new();
// for child in group.children() {
// let extent = get_child_extend();
// child_extents.push(extent);
// }

// let n = child_extents.len();
// let total_extent = if n == 0 {
// 0
// } else {
// (2 * STACK_VERTICAL_GAP as u32) * n as u32 - STACK_VERTICAL_GAP as u32 + child_extents.iter().sum::<u32>()
// };
// group_extents_map.insert(layer, child_extents);
// total_extent
// };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Remove this commented-out code block to keep the codebase clean.

Comment on lines +313 to +319
// match child {
// ParsedSvgNode::Group(parsed_group) => {
// parsed_group.extent = get_child_extents(group, group_extents_map);
// child_extents_svg_order.push(parsed_group.extent);
// }
// _ => {}
// }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Remove this commented-out code block to keep the codebase clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant