use std::str::FromStr;
use std::sync::Arc;
use strict_num::PositiveF32;
use svgtypes::{Length, LengthUnit as Unit};
use super::converter::{self, Cache, SvgColorExt};
use super::svgtree::{AId, EId, SvgNode};
use super::OptionLog;
use crate::*;
pub(crate) enum ServerOrColor {
Server(Paint),
Color { color: Color, opacity: Opacity },
}
pub(crate) fn convert(
node: SvgNode,
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: SvgNode, state: &converter::State) -> Option<ServerOrColor> {
let id = NonEmptyString::new(node.element_id().to_string())?;
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 = node.resolve_transform(AId::GradientTransform, state);
let gradient = LinearGradient {
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 {
id,
units,
transform,
spread_method: convert_spread_method(node),
stops,
},
};
Some(ServerOrColor::Server(Paint::LinearGradient(Arc::new(
gradient,
))))
}
#[inline(never)]
fn convert_radial(node: SvgNode, state: &converter::State) -> Option<ServerOrColor> {
let id = NonEmptyString::new(node.element_id().to_string())?;
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 as f64));
let fy = resolve_number(node, AId::Fy, units, state, Length::new_number(cy as f64));
let transform = node.resolve_transform(AId::GradientTransform, state);
let gradient = RadialGradient {
cx,
cy,
r: PositiveF32::new(r).unwrap(),
fx,
fy,
base: BaseGradient {
id,
units,
transform,
spread_method,
stops,
},
};
Some(ServerOrColor::Server(Paint::RadialGradient(Arc::new(
gradient,
))))
}
#[inline(never)]
fn convert_pattern(
node: SvgNode,
state: &converter::State,
cache: &mut converter::Cache,
) -> Option<ServerOrColor> {
let node_with_children = find_pattern_with_children(node)?;
let id = NonEmptyString::new(node.element_id().to_string())?;
let view_box = {
let n1 = resolve_attr(node, AId::ViewBox);
let n2 = resolve_attr(node, AId::PreserveAspectRatio);
n1.parse_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 = node.resolve_transform(AId::PatternTransform, state);
let rect = NonZeroRect::from_xywh(
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,
units,
content_units,
transform,
rect,
view_box,
root: Group::empty(),
};
if patt.view_box.is_some()
&& patt.units == Units::UserSpaceOnUse
&& patt.content_units == Units::UserSpaceOnUse
{
let mut g = Group::empty();
g.transform = view_box.unwrap().to_transform(rect.size());
g.abs_transform = g.transform;
converter::convert_children(node_with_children, state, cache, &mut g);
if !g.has_children() {
return None;
}
g.calculate_bounding_boxes();
patt.root.children.push(Node::Group(Box::new(g)));
} else {
converter::convert_children(node_with_children, state, cache, &mut patt.root);
if !patt.root.has_children() {
return None;
}
}
patt.root.calculate_bounding_boxes();
Some(ServerOrColor::Server(Paint::Pattern(Arc::new(patt))))
}
fn convert_spread_method(node: SvgNode) -> SpreadMethod {
let node = resolve_attr(node, AId::SpreadMethod);
node.attribute(AId::SpreadMethod).unwrap_or_default()
}
pub(crate) fn convert_units(node: SvgNode, name: AId, def: Units) -> Units {
let node = resolve_attr(node, name);
node.attribute(name).unwrap_or(def)
}
fn find_gradient_with_stops<'a, 'input: 'a>(
node: SvgNode<'a, 'input>,
) -> Option<SvgNode<'a, 'input>> {
for link in node.href_iter() {
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.tag_name() == Some(EId::Stop)) {
return Some(link);
}
}
None
}
fn find_pattern_with_children<'a, 'input: 'a>(
node: SvgNode<'a, 'input>,
) -> Option<SvgNode<'a, 'input>> {
for link in node.href_iter() {
if link.tag_name() != Some(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: SvgNode) -> Vec<Stop> {
let mut stops = Vec::new();
{
let mut prev_offset = Length::zero();
for stop in grad.children() {
if stop.tag_name() != Some(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,
};
prev_offset = Length::new_number(offset);
let offset = crate::f32_bound(0.0, offset as f32, 1.0);
let (color, opacity) = match stop.attribute(AId::StopColor) {
Some("currentColor") => stop
.find_attribute(AId::Color)
.unwrap_or_else(svgtypes::Color::black),
Some(value) => {
if let Ok(c) = svgtypes::Color::from_str(value) {
c
} else {
log::warn!("Failed to parse stop-color value: '{}'.", value);
svgtypes::Color::black()
}
}
_ => svgtypes::Color::black(),
}
.split_alpha();
let stop_opacity = stop
.attribute::<Opacity>(AId::StopOpacity)
.unwrap_or(Opacity::ONE);
stops.push(Stop {
offset: StopOffset::new_clamped(offset),
color,
opacity: opacity * stop_opacity,
});
}
}
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.approx_eq_ulps(&offset2, 4) && offset2.approx_eq_ulps(&offset3, 4) {
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.approx_eq_ulps(&0.0, 4) && offset2.approx_eq_ulps(&0.0, 4) {
stops[i + 1].offset = StopOffset::new_clamped(offset1 + f32::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.approx_eq_ulps(&offset2, 4) {
let new_offset = offset1 - f32::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: SvgNode,
name: AId,
units: Units,
state: &converter::State,
def: Length,
) -> f32 {
resolve_attr(node, name).convert_length(name, units, state, def)
}
fn resolve_attr<'a, 'input: 'a>(node: SvgNode<'a, 'input>, name: AId) -> SvgNode<'a, 'input> {
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<'a, 'input: 'a>(node: SvgNode<'a, 'input>, name: AId) -> SvgNode<'a, 'input> {
for link in node.href_iter() {
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<'a, 'input>(node: SvgNode<'a, 'input>, name: AId) -> SvgNode<'a, 'input> {
for link in node.href_iter() {
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<'a, 'input: 'a>(
node: SvgNode<'a, 'input>,
name: AId,
) -> SvgNode<'a, 'input> {
for link in node.href_iter() {
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<'a, 'input: 'a>(node: SvgNode<'a, 'input>, aid: AId) -> SvgNode<'a, 'input> {
for link in node.href_iter() {
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,
})
}
}
pub fn update_paint_servers(
group: &mut Group,
context_transform: Transform,
context_bbox: Option<Rect>,
text_bbox: Option<Rect>,
cache: &mut Cache,
) {
for child in &mut group.children {
let (context_transform, context_bbox) = if group.is_context_element {
(group.abs_transform, Some(group.bounding_box))
} else {
(context_transform, context_bbox)
};
node_to_user_coordinates(child, context_transform, context_bbox, text_bbox, cache);
}
}
fn node_to_user_coordinates(
node: &mut Node,
context_transform: Transform,
context_bbox: Option<Rect>,
text_bbox: Option<Rect>,
cache: &mut Cache,
) {
match node {
Node::Group(ref mut g) => {
if let Some(ref mut mask) = g.mask {
if let Some(ref mut mask) = Arc::get_mut(mask) {
update_paint_servers(
&mut mask.root,
context_transform,
context_bbox,
None,
cache,
);
if let Some(ref mut sub_mask) = mask.mask {
if let Some(ref mut sub_mask) = Arc::get_mut(sub_mask) {
update_paint_servers(
&mut sub_mask.root,
context_transform,
context_bbox,
None,
cache,
);
}
}
}
}
for filter in &mut g.filters {
if let Some(ref mut filter) = Arc::get_mut(filter) {
for primitive in &mut filter.primitives {
if let filter::Kind::Image(ref mut image) = primitive.kind {
update_paint_servers(
&mut image.root,
context_transform,
context_bbox,
None,
cache,
);
}
}
}
}
update_paint_servers(g, context_transform, context_bbox, text_bbox, cache);
}
Node::Path(ref mut path) => {
let bbox = text_bbox.unwrap_or(path.bounding_box);
process_fill(
&mut path.fill,
path.abs_transform,
context_transform,
context_bbox,
bbox,
cache,
);
process_stroke(
&mut path.stroke,
path.abs_transform,
context_transform,
context_bbox,
bbox,
cache,
);
}
Node::Image(ref mut image) => {
if let ImageKind::SVG(ref mut tree) = image.kind {
update_paint_servers(&mut tree.root, context_transform, context_bbox, None, cache);
}
}
Node::Text(ref mut text) => {
let bbox = text.bounding_box;
for chunk in &mut text.chunks {
for span in &mut chunk.spans {
process_fill(
&mut span.fill,
text.abs_transform,
context_transform,
context_bbox,
bbox,
cache,
);
process_stroke(
&mut span.stroke,
text.abs_transform,
context_transform,
context_bbox,
bbox,
cache,
);
process_text_decoration(&mut span.decoration.underline, bbox, cache);
process_text_decoration(&mut span.decoration.overline, bbox, cache);
process_text_decoration(&mut span.decoration.line_through, bbox, cache);
}
}
#[cfg(feature = "text")]
for span in &mut text.layouted {
process_fill(
&mut span.fill,
text.abs_transform,
context_transform,
context_bbox,
bbox,
cache,
);
process_stroke(
&mut span.stroke,
text.abs_transform,
context_transform,
context_bbox,
bbox,
cache,
);
let mut process_decoration = |path: &mut Path| {
process_fill(
&mut path.fill,
text.abs_transform,
context_transform,
context_bbox,
bbox,
cache,
);
process_stroke(
&mut path.stroke,
text.abs_transform,
context_transform,
context_bbox,
bbox,
cache,
);
};
if let Some(ref mut path) = span.overline {
process_decoration(path);
}
if let Some(ref mut path) = span.underline {
process_decoration(path);
}
if let Some(ref mut path) = span.line_through {
process_decoration(path);
}
}
update_paint_servers(
&mut text.flattened,
context_transform,
context_bbox,
Some(bbox),
cache,
);
}
}
}
fn process_fill(
fill: &mut Option<Fill>,
path_transform: Transform,
context_transform: Transform,
context_bbox: Option<Rect>,
bbox: Rect,
cache: &mut Cache,
) {
let mut ok = false;
if let Some(ref mut fill) = fill {
ok = process_paint(
&mut fill.paint,
matches!(fill.context_element, Some(ContextElement::UseNode)),
context_transform,
context_bbox,
path_transform,
bbox,
cache,
);
}
if !ok {
*fill = None;
}
}
fn process_stroke(
stroke: &mut Option<Stroke>,
path_transform: Transform,
context_transform: Transform,
context_bbox: Option<Rect>,
bbox: Rect,
cache: &mut Cache,
) {
let mut ok = false;
if let Some(ref mut stroke) = stroke {
ok = process_paint(
&mut stroke.paint,
matches!(stroke.context_element, Some(ContextElement::UseNode)),
context_transform,
context_bbox,
path_transform,
bbox,
cache,
);
}
if !ok {
*stroke = None;
}
}
fn process_context_paint(
paint: &mut Paint,
context_transform: Transform,
path_transform: Transform,
cache: &mut Cache,
) -> Option<()> {
let rev_transform = context_transform
.invert()?
.pre_concat(path_transform)
.invert()?;
match paint {
Paint::Color(_) => {}
Paint::LinearGradient(ref lg) => {
let transform = lg.transform.post_concat(rev_transform);
*paint = Paint::LinearGradient(Arc::new(LinearGradient {
x1: lg.x1,
y1: lg.y1,
x2: lg.x2,
y2: lg.y2,
base: BaseGradient {
id: cache.gen_linear_gradient_id(),
units: lg.units,
transform,
spread_method: lg.spread_method,
stops: lg.stops.clone(),
},
}));
}
Paint::RadialGradient(ref rg) => {
let transform = rg.transform.post_concat(rev_transform);
*paint = Paint::RadialGradient(Arc::new(RadialGradient {
cx: rg.cx,
cy: rg.cy,
r: rg.r,
fx: rg.fx,
fy: rg.fy,
base: BaseGradient {
id: cache.gen_radial_gradient_id(),
units: rg.units,
transform,
spread_method: rg.spread_method,
stops: rg.stops.clone(),
},
}))
}
Paint::Pattern(ref pat) => {
let transform = pat.transform.post_concat(rev_transform);
*paint = Paint::Pattern(Arc::new(Pattern {
id: cache.gen_pattern_id(),
units: pat.units,
content_units: pat.content_units,
transform,
rect: pat.rect,
view_box: pat.view_box,
root: pat.root.clone(),
}))
}
}
Some(())
}
pub(crate) fn process_paint(
paint: &mut Paint,
has_context: bool,
context_transform: Transform,
context_bbox: Option<Rect>,
path_transform: Transform,
bbox: Rect,
cache: &mut Cache,
) -> bool {
if paint.units() == Units::ObjectBoundingBox
|| paint.content_units() == Units::ObjectBoundingBox
{
let bbox = if has_context {
let Some(bbox) = context_bbox else {
return false;
};
bbox
} else {
bbox
};
if paint.to_user_coordinates(bbox, cache).is_none() {
return false;
}
}
if let Paint::Pattern(ref mut patt) = paint {
if let Some(ref mut patt) = Arc::get_mut(patt) {
update_paint_servers(&mut patt.root, Transform::default(), None, None, cache);
}
}
if has_context {
process_context_paint(paint, context_transform, path_transform, cache);
}
true
}
fn process_text_decoration(style: &mut Option<TextDecorationStyle>, bbox: Rect, cache: &mut Cache) {
if let Some(ref mut style) = style {
process_fill(
&mut style.fill,
Transform::default(),
Transform::default(),
None,
bbox,
cache,
);
process_stroke(
&mut style.stroke,
Transform::default(),
Transform::default(),
None,
bbox,
cache,
);
}
}
impl Paint {
fn to_user_coordinates(&mut self, bbox: Rect, cache: &mut Cache) -> Option<()> {
let name = if matches!(self, Paint::Pattern(_)) {
"Pattern"
} else {
"Gradient"
};
let bbox = bbox
.to_non_zero_rect()
.log_none(|| log::warn!("{} on zero-sized shapes is not allowed.", name))?;
match self {
Paint::Color(_) => {} Paint::LinearGradient(ref mut lg) => {
let transform = lg.transform.post_concat(Transform::from_bbox(bbox));
if let Some(ref mut lg) = Arc::get_mut(lg) {
lg.base.transform = transform;
lg.base.units = Units::UserSpaceOnUse;
} else {
*lg = Arc::new(LinearGradient {
x1: lg.x1,
y1: lg.y1,
x2: lg.x2,
y2: lg.y2,
base: BaseGradient {
id: cache.gen_linear_gradient_id(),
units: Units::UserSpaceOnUse,
transform,
spread_method: lg.spread_method,
stops: lg.stops.clone(),
},
});
}
}
Paint::RadialGradient(ref mut rg) => {
let transform = rg.transform.post_concat(Transform::from_bbox(bbox));
if let Some(ref mut rg) = Arc::get_mut(rg) {
rg.base.transform = transform;
rg.base.units = Units::UserSpaceOnUse;
} else {
*rg = Arc::new(RadialGradient {
cx: rg.cx,
cy: rg.cy,
r: rg.r,
fx: rg.fx,
fy: rg.fy,
base: BaseGradient {
id: cache.gen_radial_gradient_id(),
units: Units::UserSpaceOnUse,
transform,
spread_method: rg.spread_method,
stops: rg.stops.clone(),
},
});
}
}
Paint::Pattern(ref mut patt) => {
let rect = if patt.units == Units::ObjectBoundingBox {
patt.rect.bbox_transform(bbox)
} else {
patt.rect
};
if let Some(ref mut patt) = Arc::get_mut(patt) {
patt.rect = rect;
patt.units = Units::UserSpaceOnUse;
if patt.content_units == Units::ObjectBoundingBox && patt.view_box.is_none() {
let transform = Transform::from_scale(bbox.width(), bbox.height());
push_pattern_transform(&mut patt.root, transform);
}
if let Some(view_box) = patt.view_box {
push_pattern_transform(&mut patt.root, view_box.to_transform(rect.size()));
}
patt.content_units = Units::UserSpaceOnUse;
} else {
let mut root = if patt.content_units == Units::ObjectBoundingBox
&& patt.view_box.is_none()
{
let transform = Transform::from_scale(bbox.width(), bbox.height());
let mut g = patt.root.clone();
push_pattern_transform(&mut g, transform);
g
} else {
patt.root.clone()
};
if let Some(view_box) = patt.view_box {
push_pattern_transform(&mut root, view_box.to_transform(rect.size()));
}
*patt = Arc::new(Pattern {
id: cache.gen_pattern_id(),
units: Units::UserSpaceOnUse,
content_units: Units::UserSpaceOnUse,
transform: patt.transform,
rect,
view_box: patt.view_box,
root,
})
}
}
}
Some(())
}
}
fn push_pattern_transform(root: &mut Group, transform: Transform) {
let mut g = std::mem::replace(root, Group::empty());
g.transform = transform;
g.abs_transform = transform;
root.children.push(Node::Group(Box::new(g)));
root.calculate_bounding_boxes();
}
impl Paint {
#[inline]
pub(crate) fn units(&self) -> Units {
match self {
Self::Color(_) => Units::UserSpaceOnUse,
Self::LinearGradient(ref lg) => lg.units,
Self::RadialGradient(ref rg) => rg.units,
Self::Pattern(ref patt) => patt.units,
}
}
#[inline]
pub(crate) fn content_units(&self) -> Units {
match self {
Self::Pattern(ref patt) => patt.content_units,
_ => Units::UserSpaceOnUse,
}
}
}