use std::rc::Rc;
use strict_num::PositiveF64;
use svgtypes::{Length, LengthUnit as Unit};
use crate::geom::{FuzzyEq, FuzzyZero, IsValidLength, Rect, Transform, ViewBox};
use crate::svgtree::{self, AId, EId};
use crate::{converter, SvgColorExt, Units};
use crate::{Color, Group, Node, NodeKind, NormalizedF64, Opacity, OptionLog, Paint};
#[allow(missing_docs)]
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum SpreadMethod {
Pad,
Reflect,
Repeat,
}
impl_enum_default!(SpreadMethod, Pad);
impl_enum_from_str!(SpreadMethod,
"pad" => SpreadMethod::Pad,
"reflect" => SpreadMethod::Reflect,
"repeat" => SpreadMethod::Repeat
);
#[derive(Clone, Debug)]
pub struct BaseGradient {
pub units: Units,
pub transform: Transform,
pub spread_method: SpreadMethod,
pub stops: Vec<Stop>,
}
#[allow(missing_docs)]
#[derive(Clone, Debug)]
pub struct LinearGradient {
pub id: String,
pub x1: f64,
pub y1: f64,
pub x2: f64,
pub y2: f64,
pub base: BaseGradient,
}
impl std::ops::Deref for LinearGradient {
type Target = BaseGradient;
fn deref(&self) -> &Self::Target {
&self.base
}
}
#[allow(missing_docs)]
#[derive(Clone, Debug)]
pub struct RadialGradient {
pub id: String,
pub cx: f64,
pub cy: f64,
pub r: PositiveF64,
pub fx: f64,
pub fy: f64,
pub base: BaseGradient,
}
impl std::ops::Deref for RadialGradient {
type Target = BaseGradient;
fn deref(&self) -> &Self::Target {
&self.base
}
}
pub type StopOffset = NormalizedF64;
#[derive(Clone, Copy, Debug)]
pub struct Stop {
pub offset: StopOffset,
pub color: Color,
pub opacity: Opacity,
}
#[derive(Clone, Debug)]
pub struct Pattern {
pub id: String,
pub units: Units,
pub content_units: Units,
pub transform: Transform,
pub rect: Rect,
pub view_box: Option<ViewBox>,
pub root: Node,
}
pub(crate) enum ServerOrColor {
Server(Paint),
Color { color: Color, opacity: Opacity },
}
pub(crate) fn convert(
node: svgtree::Node,
state: &converter::State,
cache: &mut converter::Cache,
) -> Option<ServerOrColor> {
if let Some(paint) = cache.paint.get(node.element_id()) {
return Some(ServerOrColor::Server(paint.clone()));
}
let paint = match node.tag_name().unwrap() {
EId::LinearGradient => convert_linear(node, state),
EId::RadialGradient => convert_radial(node, state),
EId::Pattern => convert_pattern(node, state, cache),
_ => unreachable!(),
};
if let Some(ServerOrColor::Server(ref paint)) = paint {
cache
.paint
.insert(node.element_id().to_string(), paint.clone());
}
paint
}
#[inline(never)]
fn convert_linear(node: svgtree::Node, state: &converter::State) -> Option<ServerOrColor> {
let stops = convert_stops(find_gradient_with_stops(node)?);
if stops.len() < 2 {
return stops_to_color(&stops);
}
let units = convert_units(node, AId::GradientUnits, Units::ObjectBoundingBox);
let transform = resolve_attr(node, AId::GradientTransform)
.attribute(AId::GradientTransform)
.unwrap_or_default();
let gradient = LinearGradient {
id: node.element_id().to_string(),
x1: resolve_number(node, AId::X1, units, state, Length::zero()),
y1: resolve_number(node, AId::Y1, units, state, Length::zero()),
x2: resolve_number(
node,
AId::X2,
units,
state,
Length::new(100.0, Unit::Percent),
),
y2: resolve_number(node, AId::Y2, units, state, Length::zero()),
base: BaseGradient {
units,
transform,
spread_method: convert_spread_method(node),
stops,
},
};
Some(ServerOrColor::Server(Paint::LinearGradient(Rc::new(
gradient,
))))
}
#[inline(never)]
fn convert_radial(node: svgtree::Node, state: &converter::State) -> Option<ServerOrColor> {
let stops = convert_stops(find_gradient_with_stops(node)?);
if stops.len() < 2 {
return stops_to_color(&stops);
}
let units = convert_units(node, AId::GradientUnits, Units::ObjectBoundingBox);
let r = resolve_number(node, AId::R, units, state, Length::new(50.0, Unit::Percent));
if !r.is_valid_length() {
let stop = stops.last().unwrap();
return Some(ServerOrColor::Color {
color: stop.color,
opacity: stop.opacity,
});
}
let spread_method = convert_spread_method(node);
let cx = resolve_number(
node,
AId::Cx,
units,
state,
Length::new(50.0, Unit::Percent),
);
let cy = resolve_number(
node,
AId::Cy,
units,
state,
Length::new(50.0, Unit::Percent),
);
let fx = resolve_number(node, AId::Fx, units, state, Length::new_number(cx));
let fy = resolve_number(node, AId::Fy, units, state, Length::new_number(cy));
let transform = resolve_attr(node, AId::GradientTransform)
.attribute(AId::GradientTransform)
.unwrap_or_default();
let gradient = RadialGradient {
id: node.element_id().to_string(),
cx,
cy,
r: PositiveF64::new(r).unwrap(),
fx,
fy,
base: BaseGradient {
units,
transform,
spread_method,
stops,
},
};
Some(ServerOrColor::Server(Paint::RadialGradient(Rc::new(
gradient,
))))
}
#[inline(never)]
fn convert_pattern(
node: svgtree::Node,
state: &converter::State,
cache: &mut converter::Cache,
) -> Option<ServerOrColor> {
let node_with_children = find_pattern_with_children(node)?;
let view_box = {
let n1 = resolve_attr(node, AId::ViewBox);
let n2 = resolve_attr(node, AId::PreserveAspectRatio);
n1.get_viewbox().map(|vb| ViewBox {
rect: vb,
aspect: n2.attribute(AId::PreserveAspectRatio).unwrap_or_default(),
})
};
let units = convert_units(node, AId::PatternUnits, Units::ObjectBoundingBox);
let content_units = convert_units(node, AId::PatternContentUnits, Units::UserSpaceOnUse);
let transform = resolve_attr(node, AId::PatternTransform)
.attribute(AId::PatternTransform)
.unwrap_or_default();
let rect = Rect::new(
resolve_number(node, AId::X, units, state, Length::zero()),
resolve_number(node, AId::Y, units, state, Length::zero()),
resolve_number(node, AId::Width, units, state, Length::zero()),
resolve_number(node, AId::Height, units, state, Length::zero()),
);
let rect = rect.log_none(|| {
log::warn!(
"Pattern '{}' has an invalid size. Skipped.",
node.element_id()
)
})?;
let mut patt = Pattern {
id: node.element_id().to_string(),
units,
content_units,
transform,
rect,
view_box,
root: Node::new(NodeKind::Group(Group::default())),
};
converter::convert_children(node_with_children, state, cache, &mut patt.root);
if !patt.root.has_children() {
return None;
}
Some(ServerOrColor::Server(Paint::Pattern(Rc::new(patt))))
}
fn convert_spread_method(node: svgtree::Node) -> SpreadMethod {
let node = resolve_attr(node, AId::SpreadMethod);
node.attribute(AId::SpreadMethod).unwrap_or_default()
}
pub(crate) fn convert_units(node: svgtree::Node, name: AId, def: Units) -> Units {
let node = resolve_attr(node, name);
node.attribute(name).unwrap_or(def)
}
fn find_gradient_with_stops(node: svgtree::Node) -> Option<svgtree::Node> {
for link_id in node.href_iter() {
let link = node.document().get(link_id);
if !link.tag_name().unwrap().is_gradient() {
log::warn!(
"Gradient '{}' cannot reference '{}' via 'xlink:href'.",
node.element_id(),
link.tag_name().unwrap()
);
return None;
}
if link.children().any(|n| n.has_tag_name(EId::Stop)) {
return Some(link);
}
}
None
}
fn find_pattern_with_children(node: svgtree::Node) -> Option<svgtree::Node> {
for link_id in node.href_iter() {
let link = node.document().get(link_id);
if !link.has_tag_name(EId::Pattern) {
log::warn!(
"Pattern '{}' cannot reference '{}' via 'xlink:href'.",
node.element_id(),
link.tag_name().unwrap()
);
return None;
}
if link.has_children() {
return Some(link);
}
}
None
}
fn convert_stops(grad: svgtree::Node) -> Vec<Stop> {
let mut stops = Vec::new();
{
let mut prev_offset = Length::zero();
for stop in grad.children() {
if !stop.has_tag_name(EId::Stop) {
log::warn!("Invalid gradient child: '{:?}'.", stop.tag_name().unwrap());
continue;
}
let offset = stop.attribute(AId::Offset).unwrap_or(prev_offset);
let offset = match offset.unit {
Unit::None => offset.number,
Unit::Percent => offset.number / 100.0,
_ => prev_offset.number,
};
let offset = crate::utils::f64_bound(0.0, offset, 1.0);
prev_offset = Length::new_number(offset);
let (color, opacity) = match stop.attribute(AId::StopColor) {
Some(&svgtree::AttributeValue::CurrentColor) => stop
.find_attribute(AId::Color)
.unwrap_or_else(svgtypes::Color::black),
Some(&svgtree::AttributeValue::Color(c)) => c,
_ => svgtypes::Color::black(),
}
.split_alpha();
stops.push(Stop {
offset: StopOffset::new_clamped(offset),
color,
opacity: opacity * stop.attribute(AId::StopOpacity).unwrap_or(Opacity::ONE),
});
}
}
if stops.len() >= 3 {
let mut i = 0;
while i < stops.len() - 2 {
let offset1 = stops[i + 0].offset.get();
let offset2 = stops[i + 1].offset.get();
let offset3 = stops[i + 2].offset.get();
if offset1.fuzzy_eq(&offset2) && offset2.fuzzy_eq(&offset3) {
stops.remove(i + 1);
} else {
i += 1;
}
}
}
if stops.len() >= 2 {
let mut i = 0;
while i < stops.len() - 1 {
let offset1 = stops[i + 0].offset.get();
let offset2 = stops[i + 1].offset.get();
if offset1.is_fuzzy_zero() && offset2.is_fuzzy_zero() {
stops[i + 1].offset = StopOffset::new_clamped(offset1 + f64::EPSILON);
}
i += 1;
}
}
{
let mut i = 1;
while i < stops.len() {
let offset1 = stops[i - 1].offset.get();
let offset2 = stops[i - 0].offset.get();
if offset1 > offset2 || offset1.fuzzy_eq(&offset2) {
let new_offset = offset1 - f64::EPSILON;
stops[i - 1].offset = StopOffset::new_clamped(new_offset);
stops[i - 0].offset = StopOffset::new_clamped(offset1);
}
i += 1;
}
}
stops
}
#[inline(never)]
pub(crate) fn resolve_number(
node: svgtree::Node,
name: AId,
units: Units,
state: &converter::State,
def: Length,
) -> f64 {
resolve_attr(node, name).convert_length(name, units, state, def)
}
fn resolve_attr(node: svgtree::Node, name: AId) -> svgtree::Node {
if node.has_attribute(name) {
return node;
}
match node.tag_name().unwrap() {
EId::LinearGradient => resolve_lg_attr(node, name),
EId::RadialGradient => resolve_rg_attr(node, name),
EId::Pattern => resolve_pattern_attr(node, name),
EId::Filter => resolve_filter_attr(node, name),
_ => node,
}
}
fn resolve_lg_attr(node: svgtree::Node, name: AId) -> svgtree::Node {
for link_id in node.href_iter() {
let link = node.document().get(link_id);
let tag_name = match link.tag_name() {
Some(v) => v,
None => return node,
};
match (name, tag_name) {
(AId::X1, EId::LinearGradient)
| (AId::Y1, EId::LinearGradient)
| (AId::X2, EId::LinearGradient)
| (AId::Y2, EId::LinearGradient)
| (AId::GradientUnits, EId::LinearGradient)
| (AId::GradientUnits, EId::RadialGradient)
| (AId::SpreadMethod, EId::LinearGradient)
| (AId::SpreadMethod, EId::RadialGradient)
| (AId::GradientTransform, EId::LinearGradient)
| (AId::GradientTransform, EId::RadialGradient) => {
if link.has_attribute(name) {
return link;
}
}
_ => break,
}
}
node
}
fn resolve_rg_attr(node: svgtree::Node, name: AId) -> svgtree::Node {
for link_id in node.href_iter() {
let link = node.document().get(link_id);
let tag_name = match link.tag_name() {
Some(v) => v,
None => return node,
};
match (name, tag_name) {
(AId::Cx, EId::RadialGradient)
| (AId::Cy, EId::RadialGradient)
| (AId::R, EId::RadialGradient)
| (AId::Fx, EId::RadialGradient)
| (AId::Fy, EId::RadialGradient)
| (AId::GradientUnits, EId::LinearGradient)
| (AId::GradientUnits, EId::RadialGradient)
| (AId::SpreadMethod, EId::LinearGradient)
| (AId::SpreadMethod, EId::RadialGradient)
| (AId::GradientTransform, EId::LinearGradient)
| (AId::GradientTransform, EId::RadialGradient) => {
if link.has_attribute(name) {
return link;
}
}
_ => break,
}
}
node
}
fn resolve_pattern_attr(node: svgtree::Node, name: AId) -> svgtree::Node {
for link_id in node.href_iter() {
let link = node.document().get(link_id);
let tag_name = match link.tag_name() {
Some(v) => v,
None => return node,
};
if tag_name != EId::Pattern {
break;
}
if link.has_attribute(name) {
return link;
}
}
node
}
fn resolve_filter_attr(node: svgtree::Node, aid: AId) -> svgtree::Node {
for link_id in node.href_iter() {
let link = node.document().get(link_id);
let tag_name = match link.tag_name() {
Some(v) => v,
None => return node,
};
if tag_name != EId::Filter {
break;
}
if link.has_attribute(aid) {
return link;
}
}
node
}
fn stops_to_color(stops: &[Stop]) -> Option<ServerOrColor> {
if stops.is_empty() {
None
} else {
Some(ServerOrColor::Color {
color: stops[0].color,
opacity: stops[0].opacity,
})
}
}