Lines
94.04 %
Functions
46.92 %
Branches
49.05 %
//! Gradient paint servers; the `linearGradient` and `radialGradient` elements.
use cssparser::{Color, Parser};
use markup5ever::{
expanded_name, local_name, namespace_url, ns, ExpandedName, LocalName, Namespace,
};
use crate::coord_units;
use crate::coord_units::CoordUnits;
use crate::document::{AcquiredNodes, NodeId, NodeStack};
use crate::drawing_ctx::Viewport;
use crate::element::{set_attribute, ElementData, ElementTrait};
use crate::error::*;
use crate::href::{is_href, set_href};
use crate::length::*;
use crate::node::{CascadedValues, Node, NodeBorrow};
use crate::paint_server::resolve_color;
use crate::parse_identifiers;
use crate::parsers::{Parse, ParseValue};
use crate::rect::{rect_to_transform, Rect};
use crate::session::Session;
use crate::transform::{Transform, TransformAttribute};
use crate::unit_interval::UnitInterval;
use crate::xml::Attributes;
/// Contents of a `<stop>` element for gradient color stops
#[derive(Copy, Clone)]
pub struct ColorStop {
/// `<stop offset="..."/>`
pub offset: UnitInterval,
/// `<stop stop-color="..." stop-opacity="..."/>`
pub color: Color,
}
// gradientUnits attribute; its default is objectBoundingBox
coord_units!(GradientUnits, CoordUnits::ObjectBoundingBox);
/// spreadMethod attribute for gradients
#[derive(Debug, Default, Copy, Clone, PartialEq)]
pub enum SpreadMethod {
#[default]
Pad,
Reflect,
Repeat,
impl Parse for SpreadMethod {
fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<SpreadMethod, ParseError<'i>> {
Ok(parse_identifiers!(
parser,
"pad" => SpreadMethod::Pad,
"reflect" => SpreadMethod::Reflect,
"repeat" => SpreadMethod::Repeat,
)?)
/// Node for the `<stop>` element
#[derive(Default)]
pub struct Stop {
offset: UnitInterval,
/* stop-color and stop-opacity are not attributes; they are properties, so
* they go into property_defs.rs */
impl ElementTrait for Stop {
fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
for (attr, value) in attrs.iter() {
if attr.expanded() == expanded_name!("", "offset") {
set_attribute(&mut self.offset, attr.parse(value), session);
/// Parameters specific to each gradient type, before being resolved.
/// These will be composed together with UnreseolvedVariant from fallback
/// nodes (referenced with e.g. `<linearGradient xlink:href="#fallback">`) to form
/// a final, resolved Variant.
enum UnresolvedVariant {
Linear {
x1: Option<Length<Horizontal>>,
y1: Option<Length<Vertical>>,
x2: Option<Length<Horizontal>>,
y2: Option<Length<Vertical>>,
},
Radial {
cx: Option<Length<Horizontal>>,
cy: Option<Length<Vertical>>,
r: Option<Length<Both>>,
fx: Option<Length<Horizontal>>,
fy: Option<Length<Vertical>>,
fr: Option<Length<Both>>,
/// Parameters specific to each gradient type, after resolving.
#[derive(Clone)]
enum ResolvedGradientVariant {
x1: Length<Horizontal>,
y1: Length<Vertical>,
x2: Length<Horizontal>,
y2: Length<Vertical>,
cx: Length<Horizontal>,
cy: Length<Vertical>,
r: Length<Both>,
fx: Length<Horizontal>,
fy: Length<Vertical>,
fr: Length<Both>,
/// Parameters specific to each gradient type, after normalizing to user-space units.
pub enum GradientVariant {
x1: f64,
y1: f64,
x2: f64,
y2: f64,
cx: f64,
cy: f64,
r: f64,
fx: f64,
fy: f64,
fr: f64,
impl UnresolvedVariant {
fn into_resolved(self) -> ResolvedGradientVariant {
assert!(self.is_resolved());
match self {
UnresolvedVariant::Linear { x1, y1, x2, y2 } => ResolvedGradientVariant::Linear {
x1: x1.unwrap(),
y1: y1.unwrap(),
x2: x2.unwrap(),
y2: y2.unwrap(),
UnresolvedVariant::Radial {
cx,
cy,
r,
fx,
fy,
fr,
} => ResolvedGradientVariant::Radial {
cx: cx.unwrap(),
cy: cy.unwrap(),
r: r.unwrap(),
fx: fx.unwrap(),
fy: fy.unwrap(),
fr: fr.unwrap(),
fn is_resolved(&self) -> bool {
match *self {
UnresolvedVariant::Linear { x1, y1, x2, y2 } => {
x1.is_some() && y1.is_some() && x2.is_some() && y2.is_some()
} => {
cx.is_some()
&& cy.is_some()
&& r.is_some()
&& fx.is_some()
&& fy.is_some()
&& fr.is_some()
fn resolve_from_fallback(&self, fallback: &UnresolvedVariant) -> UnresolvedVariant {
match (*self, *fallback) {
(
UnresolvedVariant::Linear { x1, y1, x2, y2 },
UnresolvedVariant::Linear {
x1: fx1,
y1: fy1,
x2: fx2,
y2: fy2,
) => UnresolvedVariant::Linear {
x1: x1.or(fx1),
y1: y1.or(fy1),
x2: x2.or(fx2),
y2: y2.or(fy2),
cx: f_cx,
cy: f_cy,
r: f_r,
fx: f_fx,
fy: f_fy,
fr: f_fr,
) => UnresolvedVariant::Radial {
cx: cx.or(f_cx),
cy: cy.or(f_cy),
r: r.or(f_r),
fx: fx.or(f_fx),
fy: fy.or(f_fy),
fr: fr.or(f_fr),
_ => *self, // If variants are of different types, then nothing to resolve
// https://www.w3.org/TR/SVG/pservers.html#LinearGradients
// https://www.w3.org/TR/SVG/pservers.html#RadialGradients
fn resolve_from_defaults(&self) -> UnresolvedVariant {
UnresolvedVariant::Linear { x1, y1, x2, y2 } => UnresolvedVariant::Linear {
x1: x1.or_else(|| Some(Length::<Horizontal>::parse_str("0%").unwrap())),
y1: y1.or_else(|| Some(Length::<Vertical>::parse_str("0%").unwrap())),
x2: x2.or_else(|| Some(Length::<Horizontal>::parse_str("100%").unwrap())),
y2: y2.or_else(|| Some(Length::<Vertical>::parse_str("0%").unwrap())),
let cx = cx.or_else(|| Some(Length::<Horizontal>::parse_str("50%").unwrap()));
let cy = cy.or_else(|| Some(Length::<Vertical>::parse_str("50%").unwrap()));
let r = r.or_else(|| Some(Length::<Both>::parse_str("50%").unwrap()));
// fx and fy fall back to the presentational value of cx and cy
let fx = fx.or(cx);
let fy = fy.or(cy);
let fr = fr.or_else(|| Some(Length::<Both>::parse_str("0%").unwrap()));
/// Fields shared by all gradient nodes
struct Common {
units: Option<GradientUnits>,
transform: Option<TransformAttribute>,
spread: Option<SpreadMethod>,
fallback: Option<NodeId>,
/// Node for the `<linearGradient>` element
pub struct LinearGradient {
common: Common,
/// Node for the `<radialGradient>` element
pub struct RadialGradient {
/// Main structure used during gradient resolution. For unresolved
/// gradients, we store all fields as `Option<T>` - if `None`, it means
/// that the field is not specified; if `Some(T)`, it means that the
/// field was specified.
struct UnresolvedGradient {
stops: Option<Vec<ColorStop>>,
variant: UnresolvedVariant,
/// Resolved gradient; this is memoizable after the initial resolution.
pub struct ResolvedGradient {
units: GradientUnits,
transform: TransformAttribute,
spread: SpreadMethod,
stops: Vec<ColorStop>,
variant: ResolvedGradientVariant,
/// Gradient normalized to user-space units.
pub struct UserSpaceGradient {
pub transform: Transform,
pub spread: SpreadMethod,
pub stops: Vec<ColorStop>,
pub variant: GradientVariant,
impl UnresolvedGradient {
fn into_resolved(self) -> ResolvedGradient {
let UnresolvedGradient {
units,
transform,
spread,
stops,
variant,
} = self;
match variant {
UnresolvedVariant::Linear { .. } => ResolvedGradient {
units: units.unwrap(),
transform: transform.unwrap(),
spread: spread.unwrap(),
stops: stops.unwrap(),
variant: variant.into_resolved(),
UnresolvedVariant::Radial { .. } => ResolvedGradient {
/// Helper for add_color_stops_from_node()
fn add_color_stop(&mut self, offset: UnitInterval, color: Color) {
if self.stops.is_none() {
self.stops = Some(Vec::<ColorStop>::new());
if let Some(ref mut stops) = self.stops {
let last_offset = if !stops.is_empty() {
stops[stops.len() - 1].offset
} else {
UnitInterval(0.0)
let offset = if offset > last_offset {
offset
last_offset
stops.push(ColorStop { offset, color });
unreachable!();
/// Looks for `<stop>` children inside a linearGradient or radialGradient node,
/// and adds their info to the UnresolvedGradient &self.
fn add_color_stops_from_node(&mut self, node: &Node, opacity: UnitInterval) {
assert!(matches!(
*node.borrow_element_data(),
ElementData::LinearGradient(_) | ElementData::RadialGradient(_)
));
for child in node.children().filter(|c| c.is_element()) {
if let ElementData::Stop(ref stop) = &*child.borrow_element_data() {
let cascaded = CascadedValues::new_from_node(&child);
let values = cascaded.get();
let UnitInterval(stop_opacity) = values.stop_opacity().0;
let UnitInterval(o) = opacity;
let composed_opacity = UnitInterval(stop_opacity * o);
let stop_color =
resolve_color(&values.stop_color().0, composed_opacity, &values.color().0);
self.add_color_stop(stop.offset, stop_color);
self.units.is_some()
&& self.transform.is_some()
&& self.spread.is_some()
&& self.stops.is_some()
&& self.variant.is_resolved()
fn resolve_from_fallback(&self, fallback: &UnresolvedGradient) -> UnresolvedGradient {
let units = self.units.or(fallback.units);
let transform = self.transform.or(fallback.transform);
let spread = self.spread.or(fallback.spread);
let stops = self.stops.clone().or_else(|| fallback.stops.clone());
let variant = self.variant.resolve_from_fallback(&fallback.variant);
UnresolvedGradient {
fn resolve_from_defaults(&self) -> UnresolvedGradient {
let units = self.units.or_else(|| Some(GradientUnits::default()));
let transform = self
.transform
.or_else(|| Some(TransformAttribute::default()));
let spread = self.spread.or_else(|| Some(SpreadMethod::default()));
let stops = self.stops.clone().or_else(|| Some(Vec::<ColorStop>::new()));
let variant = self.variant.resolve_from_defaults();
/// State used during the gradient resolution process
///
/// This is the current node's gradient information, plus the fallback
/// that should be used in case that information is not complete for a
/// resolved gradient yet.
struct Unresolved {
gradient: UnresolvedGradient,
impl LinearGradient {
fn get_unresolved_variant(&self) -> UnresolvedVariant {
x1: self.x1,
y1: self.y1,
x2: self.x2,
y2: self.y2,
impl RadialGradient {
cx: self.cx,
cy: self.cy,
r: self.r,
fx: self.fx,
fy: self.fy,
fr: self.fr,
impl Common {
match attr.expanded() {
expanded_name!("", "gradientUnits") => {
set_attribute(&mut self.units, attr.parse(value), session)
expanded_name!("", "gradientTransform") => {
set_attribute(&mut self.transform, attr.parse(value), session);
expanded_name!("", "spreadMethod") => {
set_attribute(&mut self.spread, attr.parse(value), session)
ref a if is_href(a) => {
let mut href = None;
set_attribute(
&mut href,
NodeId::parse(value).map(Some).attribute(attr.clone()),
session,
);
set_href(a, &mut self.fallback, href);
_ => (),
impl ElementTrait for LinearGradient {
self.common.set_attributes(attrs, session);
expanded_name!("", "x1") => set_attribute(&mut self.x1, attr.parse(value), session),
expanded_name!("", "y1") => set_attribute(&mut self.y1, attr.parse(value), session),
expanded_name!("", "x2") => set_attribute(&mut self.x2, attr.parse(value), session),
expanded_name!("", "y2") => set_attribute(&mut self.y2, attr.parse(value), session),
macro_rules! impl_gradient {
($gradient_type:ident, $other_type:ident) => {
impl $gradient_type {
fn get_unresolved(&self, node: &Node, opacity: UnitInterval) -> Unresolved {
let mut gradient = UnresolvedGradient {
units: self.common.units,
transform: self.common.transform,
spread: self.common.spread,
stops: None,
variant: self.get_unresolved_variant(),
gradient.add_color_stops_from_node(node, opacity);
Unresolved {
gradient,
fallback: self.common.fallback.clone(),
pub fn resolve(
&self,
node: &Node,
acquired_nodes: &mut AcquiredNodes<'_>,
opacity: UnitInterval,
) -> Result<ResolvedGradient, AcquireError> {
let Unresolved {
mut gradient,
mut fallback,
} = self.get_unresolved(node, opacity);
let mut stack = NodeStack::new();
while !gradient.is_resolved() {
if let Some(node_id) = fallback {
let acquired = acquired_nodes.acquire(&node_id)?;
let acquired_node = acquired.get();
if stack.contains(acquired_node) {
return Err(AcquireError::CircularReference(acquired_node.clone()));
let unresolved = match *acquired_node.borrow_element_data() {
ElementData::$gradient_type(ref g) => {
g.get_unresolved(&acquired_node, opacity)
ElementData::$other_type(ref g) => {
_ => return Err(AcquireError::InvalidLinkType(node_id.clone())),
gradient = gradient.resolve_from_fallback(&unresolved.gradient);
fallback = unresolved.fallback;
stack.push(acquired_node);
gradient = gradient.resolve_from_defaults();
break;
Ok(gradient.into_resolved())
impl_gradient!(LinearGradient, RadialGradient);
impl_gradient!(RadialGradient, LinearGradient);
impl ElementTrait for RadialGradient {
// Create a local expanded name for "fr" because markup5ever doesn't have built-in
let expanded_name_fr = ExpandedName {
ns: &Namespace::from(""),
local: &LocalName::from("fr"),
let attr_expanded = attr.expanded();
match attr_expanded {
expanded_name!("", "cx") => set_attribute(&mut self.cx, attr.parse(value), session),
expanded_name!("", "cy") => set_attribute(&mut self.cy, attr.parse(value), session),
expanded_name!("", "r") => set_attribute(&mut self.r, attr.parse(value), session),
expanded_name!("", "fx") => set_attribute(&mut self.fx, attr.parse(value), session),
expanded_name!("", "fy") => set_attribute(&mut self.fy, attr.parse(value), session),
a if a == expanded_name_fr => {
set_attribute(&mut self.fr, attr.parse(value), session)
impl ResolvedGradient {
pub fn to_user_space(
object_bbox: &Option<Rect>,
viewport: &Viewport,
values: &NormalizeValues,
) -> Option<UserSpaceGradient> {
let units = self.units.0;
let transform = rect_to_transform(object_bbox, units).ok()?;
let viewport = viewport.with_units(units);
let params = NormalizeParams::from_values(values, &viewport);
let gradient_transform = self.transform.to_transform();
let transform = transform.pre_transform(&gradient_transform).invert()?;
let variant = match self.variant {
ResolvedGradientVariant::Linear { x1, y1, x2, y2 } => GradientVariant::Linear {
x1: x1.to_user(¶ms),
y1: y1.to_user(¶ms),
x2: x2.to_user(¶ms),
y2: y2.to_user(¶ms),
ResolvedGradientVariant::Radial {
} => GradientVariant::Radial {
cx: cx.to_user(¶ms),
cy: cy.to_user(¶ms),
r: r.to_user(¶ms),
fx: fx.to_user(¶ms),
fy: fy.to_user(¶ms),
fr: fr.to_user(¶ms),
Some(UserSpaceGradient {
spread: self.spread,
stops: self.stops.clone(),
})
#[cfg(test)]
mod tests {
use super::*;
use markup5ever::{namespace_url, ns, QualName};
use crate::borrow_element_as;
use crate::node::{Node, NodeData};
#[test]
fn parses_spread_method() {
assert_eq!(SpreadMethod::parse_str("pad").unwrap(), SpreadMethod::Pad);
assert_eq!(
SpreadMethod::parse_str("reflect").unwrap(),
SpreadMethod::Reflect
SpreadMethod::parse_str("repeat").unwrap(),
SpreadMethod::Repeat
assert!(SpreadMethod::parse_str("foobar").is_err());
fn gradient_resolved_from_defaults_is_really_resolved() {
let session = Session::default();
let node = Node::new(NodeData::new_element(
&session,
&QualName::new(None, ns!(svg), local_name!("linearGradient")),
Attributes::new(),
let unresolved = borrow_element_as!(node, LinearGradient)
.get_unresolved(&node, UnitInterval::clamp(1.0));
let gradient = unresolved.gradient.resolve_from_defaults();
assert!(gradient.is_resolved());
&QualName::new(None, ns!(svg), local_name!("radialGradient")),
let unresolved = borrow_element_as!(node, RadialGradient)