Skip to content

Commit 3ae1a44

Browse files
committed
Add support for SVG gradient units import/export (rebased on master)
1 parent 6a54dcb commit 3ae1a44

7 files changed

Lines changed: 122 additions & 19 deletions

File tree

editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use graph_craft::document::{NodeId, NodeInput};
1313
use graphene_std::list::List;
1414
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
1515
use graphene_std::text::{Font, TypesettingConfig};
16-
use graphene_std::vector::style::{GradientSpreadMethod, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
16+
use graphene_std::vector::style::{GradientSpreadMethod, GradientStop, GradientStops, GradientType, GradientUnits, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
1717
use graphene_std::{Artboard, Color};
1818

1919
#[derive(ExtractField)]
@@ -562,6 +562,51 @@ fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops>
562562
result
563563
}
564564

565+
#[allow(dead_code)]
566+
fn extract_svg_gradient_units(svg: &str) -> HashMap<String, GradientUnits> {
567+
let mut result = HashMap::new();
568+
let mut gradient_nodes = HashMap::new();
569+
570+
let doc = match usvg::roxmltree::Document::parse(svg) {
571+
Ok(doc) => doc,
572+
Err(_) => return result,
573+
};
574+
575+
for node in doc.descendants() {
576+
if matches!(node.tag_name().name(), "linearGradient" | "radialGradient")
577+
&& let Some(gradient_id) = node.attribute("id")
578+
{
579+
gradient_nodes.insert(gradient_id, node);
580+
}
581+
}
582+
583+
fn resolve_units<'a>(node: usvg::roxmltree::Node<'a, 'a>, gradient_nodes: &HashMap<&'a str, usvg::roxmltree::Node<'a, 'a>>, seen: &mut Vec<&'a str>) -> Option<GradientUnits> {
584+
match node.attribute("gradientUnits") {
585+
Some("userSpaceOnUse") => return Some(GradientUnits::UserSpaceOnUse),
586+
Some("objectBoundingBox") => return Some(GradientUnits::ObjectBoundingBox),
587+
Some(_) => return None,
588+
None => {}
589+
}
590+
591+
let href = node.attribute("href").or_else(|| node.attribute(("http://www.w3.org/1999/xlink", "href")))?;
592+
let referenced_id = href.strip_prefix('#')?;
593+
if seen.contains(&referenced_id) {
594+
return None;
595+
}
596+
let referenced_node = *gradient_nodes.get(referenced_id)?;
597+
seen.push(referenced_id);
598+
resolve_units(referenced_node, gradient_nodes, seen)
599+
}
600+
601+
for (&gradient_id, &node) in &gradient_nodes {
602+
// Per SVG spec, the default gradientUnits is objectBoundingBox.
603+
let units = resolve_units(node, &gradient_nodes, &mut vec![gradient_id]).unwrap_or(GradientUnits::ObjectBoundingBox);
604+
result.insert(gradient_id.to_string(), units);
605+
}
606+
607+
result
608+
}
609+
565610
fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option<Color> {
566611
let hex = hex.strip_prefix('#')?;
567612
if hex.len() != 6 {
@@ -692,7 +737,13 @@ fn import_usvg_node_inner(
692737
}
693738

694739
/// Helper to apply path data (vector geometry, fill, stroke, transform) to a layer.
695-
fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, path: &usvg::Path, layer: LayerNodeIdentifier, graphite_gradient_stops: &HashMap<String, GradientStops>) {
740+
fn import_usvg_path(
741+
modify_inputs: &mut ModifyInputsContext,
742+
node: &usvg::Node,
743+
path: &usvg::Path,
744+
layer: LayerNodeIdentifier,
745+
graphite_gradient_stops: &HashMap<String, GradientStops>,
746+
) {
696747
let subpaths = convert_usvg_path(path);
697748

698749
// Skip creating a Transform node entirely when the SVG-native transform is identity.

node-graph/libraries/core-types/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ pub use graphene_hash;
2525
pub use graphene_hash::CacheHash;
2626
pub use list::{
2727
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_END,
28-
ATTR_FONT, ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL,
29-
ATTR_SPREAD_METHOD, ATTR_START, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_TYPE,
28+
ATTR_FONT, ATTR_FONT_SIZE, ATTR_GRADIENT_LEGACY, ATTR_GRADIENT_TYPE, ATTR_GRADIENT_UNITS, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH,
29+
ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_START, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_TYPE,
3030
};
3131
pub use memo::MemoHash;
3232
pub use no_std_types::AsU32;

node-graph/libraries/core-types/src/list.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ pub const ATTR_CLIP: &str = "clip";
5858
pub const ATTR_SPREAD_METHOD: &str = "spread_method";
5959
/// Gradient's `GradientType` (`Linear` or `Radial`).
6060
pub const ATTR_GRADIENT_TYPE: &str = "gradient_type";
61+
/// Gradient's SVG coordinate system (`userSpaceOnUse` or `objectBoundingBox`).
62+
pub const ATTR_GRADIENT_UNITS: &str = "gradient_units";
63+
// TODO: Eventually remove this document upgrade code
64+
/// `bool` runtime marker (never serialized) flagging a gradient that came from the legacy bounding-box-relative `Fill::Gradient`,
65+
/// so the renderer reproduces the pre-#4241 positioning instead of the new absolute path.
66+
pub const ATTR_GRADIENT_LEGACY: &str = "gradient_legacy";
6167
/// Vector graphics object's filled area paint, of type List<T> where T is any graphic type.
6268
pub const ATTR_FILL: &str = "fill";
6369
/// Vector graphics object's stroke paint, of type List<T> where T is any graphic type.

node-graph/libraries/rendering/src/render_ext.rs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ use crate::{Render, RenderSvgSegmentList, SvgRender};
33
use core_types::color::SRGBA8;
44
use core_types::list::List;
55
use core_types::uuid::generate_uuid;
6-
use core_types::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color};
6+
use core_types::{ATTR_GRADIENT_TYPE, ATTR_GRADIENT_UNITS, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color};
77
use glam::{DAffine2, DVec2};
88
use graphic_types::Graphic;
9-
use graphic_types::vector_types::gradient::GradientType;
9+
use graphic_types::vector_types::gradient::{GradientType, GradientUnits};
1010
use graphic_types::vector_types::vector::style::{PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
1111
use std::fmt::Write;
1212
use vector_types::GradientStops;
@@ -18,6 +18,16 @@ pub enum PaintTarget {
1818
Stroke,
1919
}
2020

21+
fn svg_gradient_transform(transform: DAffine2, bounds: DAffine2, units: GradientUnits) -> (GradientUnits, String) {
22+
let (units, transform) = match units {
23+
GradientUnits::UserSpaceOnUse => (GradientUnits::UserSpaceOnUse, transform),
24+
GradientUnits::ObjectBoundingBox if transform_is_invertible(bounds) => (GradientUnits::ObjectBoundingBox, bounds.inverse() * transform),
25+
GradientUnits::ObjectBoundingBox => (GradientUnits::UserSpaceOnUse, transform),
26+
};
27+
28+
(units, format_transform_matrix(transform))
29+
}
30+
2131
impl PaintTarget {
2232
fn paint_attr(self) -> &'static str {
2333
match self {
@@ -94,6 +104,7 @@ impl RenderExt for List<GradientStops> {
94104

95105
let Some(stops) = self.element(0) else { return 0 };
96106
let gradient_type: GradientType = self.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, 0);
107+
let gradient_units: GradientUnits = self.attribute_cloned_or_default(ATTR_GRADIENT_UNITS, 0);
97108
let local_gradient_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
98109
let spread_method: GradientSpreadMethod = self.attribute_cloned_or_default(ATTR_SPREAD_METHOD, 0);
99110

@@ -122,7 +133,7 @@ impl RenderExt for List<GradientStops> {
122133
let document_transform = item_transform * local_gradient_transform;
123134

124135
let placement = gradient_placement(document_transform, gradient_type);
125-
let gradient_transform = format_transform_matrix(element_transform_inverse * placement);
136+
let (gradient_units, gradient_transform) = svg_gradient_transform(element_transform_inverse * placement, _bounds, gradient_units);
126137
let gradient_transform = if gradient_transform.is_empty() {
127138
String::new()
128139
} else {
@@ -141,15 +152,19 @@ impl RenderExt for List<GradientStops> {
141152
GradientType::Linear => {
142153
let _ = write!(
143154
svg_defs,
144-
r#"<linearGradient id="{}" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="1" y2="0"{spread_method}{gradient_transform}>{}</linearGradient>"#,
145-
gradient_id, stop
155+
r#"<linearGradient id="{}" gradientUnits="{}" x1="0" y1="0" x2="1" y2="0"{spread_method}{gradient_transform}>{}</linearGradient>"#,
156+
gradient_id,
157+
gradient_units.svg_name(),
158+
stop
146159
);
147160
}
148161
GradientType::Radial => {
149162
let _ = write!(
150163
svg_defs,
151-
r#"<radialGradient id="{}" gradientUnits="userSpaceOnUse" cx="0" cy="0" r="1"{spread_method}{gradient_transform}>{}</radialGradient>"#,
152-
gradient_id, stop
164+
r#"<radialGradient id="{}" gradientUnits="{}" cx="0" cy="0" r="1"{spread_method}{gradient_transform}>{}</radialGradient>"#,
165+
gradient_id,
166+
gradient_units.svg_name(),
167+
stop
153168
);
154169
}
155170
}

node-graph/libraries/rendering/src/renderer.rs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ use core_types::transform::Footprint;
1414
use core_types::uuid::{NodeId, generate_uuid};
1515
use core_types::{
1616
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_FONT,
17-
ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD,
18-
ATTR_TEXT_ALIGN, ATTR_TRANSFORM,
17+
ATTR_FONT_SIZE, ATTR_GRADIENT_LEGACY, ATTR_GRADIENT_TYPE, ATTR_GRADIENT_UNITS, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH,
18+
ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TEXT_ALIGN, ATTR_TRANSFORM,
1919
};
2020
use dyn_any::DynAny;
2121
use glam::{DAffine2, DMat2, DVec2};
@@ -39,7 +39,7 @@ use std::fmt::Write;
3939
use std::hash::Hash;
4040
use std::ops::Deref;
4141
use std::sync::{Arc, LazyLock};
42-
use vector_types::gradient::GradientSpreadMethod;
42+
use vector_types::gradient::{GradientSpreadMethod, GradientUnits};
4343
use vello::*;
4444

4545
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -2064,12 +2064,14 @@ impl Render for List<GradientStops> {
20642064
for index in 0..self.len() {
20652065
let Some(gradient) = self.element(index) else { continue };
20662066
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
2067+
let mut gradient_units: GradientUnits = self.attribute_cloned_or_default(ATTR_GRADIENT_UNITS, index);
20672068
let blend_mode: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
20682069
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
20692070
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
20702071
let spread_method: GradientSpreadMethod = self.attribute_cloned_or_default(ATTR_SPREAD_METHOD, index);
20712072
let gradient_type: GradientType = self.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, index);
20722073
let tag = if thumbnail_rect.is_some() { "rect" } else { "polyline" };
2074+
const MAX: f64 = 1e7;
20732075
render.leaf_tag(tag, |attributes| {
20742076
if let Some((min, size)) = thumbnail_rect {
20752077
attributes.push("x", min.x.to_string());
@@ -2080,7 +2082,6 @@ impl Render for List<GradientStops> {
20802082
// Stand-in for an infinite background. Chrome's SVG renderer keeps internal coordinates in f32 and loses
20812083
// precision past ~2^24 (~16.7 million), causing tile-boundary artifacts that pop in and out during panning.
20822084
// 1e7 stays under that limit while still being far larger than any practical document extent.
2083-
const MAX: f64 = 1e7;
20842085
attributes.push("points", format!("{MAX},{MAX} -{MAX},{MAX} -{MAX},-{MAX} {MAX},-{MAX}"));
20852086
}
20862087

@@ -2097,7 +2098,16 @@ impl Render for List<GradientStops> {
20972098
}
20982099

20992100
// render_thumbnail already added the footprint transform
2100-
let gradient_transform = if render_params.thumbnail { transform } else { render_params.footprint.transform * transform };
2101+
let mut gradient_transform = if render_params.thumbnail { transform } else { render_params.footprint.transform * transform };
2102+
if gradient_units == GradientUnits::ObjectBoundingBox {
2103+
let (min, size) = thumbnail_rect.unwrap_or((DVec2::splat(-MAX), DVec2::splat(MAX * 2.)));
2104+
let bounds = DAffine2::from_scale_angle_translation(size, 0., min);
2105+
if transform_is_invertible(bounds) {
2106+
gradient_transform = bounds.inverse() * gradient_transform;
2107+
} else {
2108+
gradient_units = GradientUnits::UserSpaceOnUse;
2109+
}
2110+
}
21012111
let gradient_transform_matrix = format_transform_matrix(gradient_transform);
21022112
let gradient_transform_attribute = if gradient_transform_matrix.is_empty() {
21032113
String::new()
@@ -2117,13 +2127,15 @@ impl Render for List<GradientStops> {
21172127
GradientType::Linear => {
21182128
let _ = write!(
21192129
&mut attributes.0.svg_defs,
2120-
r#"<linearGradient id="{gradient_id}" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="1" y2="0"{spread_method_attribute}{gradient_transform_attribute}>{stop_string}</linearGradient>"#
2130+
r#"<linearGradient id="{gradient_id}" gradientUnits="{}" x1="0" y1="0" x2="1" y2="0"{spread_method_attribute}{gradient_transform_attribute}>{stop_string}</linearGradient>"#,
2131+
gradient_units.svg_name()
21212132
);
21222133
}
21232134
GradientType::Radial => {
21242135
let _ = write!(
21252136
&mut attributes.0.svg_defs,
2126-
r#"<radialGradient id="{gradient_id}" gradientUnits="userSpaceOnUse" cx="0" cy="0" r="1"{spread_method_attribute}{gradient_transform_attribute}>{stop_string}</radialGradient>"#
2137+
r#"<radialGradient id="{gradient_id}" gradientUnits="{}" cx="0" cy="0" r="1"{spread_method_attribute}{gradient_transform_attribute}>{stop_string}</radialGradient>"#,
2138+
gradient_units.svg_name()
21272139
);
21282140
}
21292141
}

node-graph/libraries/vector-types/src/gradient.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,24 @@ pub enum GradientType {
1414
Radial,
1515
}
1616

17+
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
18+
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, DynAny)]
19+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20+
pub enum GradientUnits {
21+
#[default]
22+
UserSpaceOnUse,
23+
ObjectBoundingBox,
24+
}
25+
26+
impl GradientUnits {
27+
pub fn svg_name(&self) -> &'static str {
28+
match self {
29+
GradientUnits::UserSpaceOnUse => "userSpaceOnUse",
30+
GradientUnits::ObjectBoundingBox => "objectBoundingBox",
31+
}
32+
}
33+
}
34+
1735
// TODO: Someday we could switch this to a Box[T] to avoid over-allocation
1836
/// A list of colors (linear, unassociated alpha) associated with positions (in the range 0 to 1) along a gradient.
1937
///
@@ -575,3 +593,4 @@ impl core_types::bounds::BoundingBox for GradientStops {
575593
core_types::bounds::RenderBoundingBox::Rectangle([start.min(end), start.max(end)])
576594
}
577595
}
596+

node-graph/libraries/vector-types/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ pub mod vector;
88

99
// Re-export commonly used types at the crate root
1010
pub use core_types as gcore;
11-
pub use gradient::{GradientSpreadMethod, GradientStop, GradientStops, GradientType};
11+
pub use gradient::{GradientSpreadMethod, GradientStop, GradientStops, GradientType, GradientUnits};
1212
pub use math::{QuadExt, RectExt};
1313
pub use subpath::Subpath;
1414
pub use vector::Vector;

0 commit comments

Comments
 (0)