Permalink
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
389 lines (341 sloc) 13.1 KB
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use svgdom::{
AspectRatio,
Length,
};
use super::prelude::*;
/// Resolves `linearGradient` attributes.
pub fn resolve_linear_gradient_attributes(doc: &Document) {
for node in &mut gen_order(doc, EId::LinearGradient) {
resolve_attr(node, AId::GradientUnits, Some(AValue::from("objectBoundingBox")));
resolve_attr(node, AId::SpreadMethod, Some(AValue::from("pad")));
resolve_attr(node, AId::X1, Some(AValue::from(0.0)));
resolve_attr(node, AId::Y1, Some(AValue::from(0.0)));
resolve_attr(node, AId::X2, Some(AValue::from(Length::new(100.0, Unit::Percent))));
resolve_attr(node, AId::Y2, Some(AValue::from(0.0)));
resolve_attr(node, AId::GradientTransform, None);
fix_units(node, AId::GradientUnits, "objectBoundingBox");
}
}
/// Resolves `radialGradient` attributes.
pub fn resolve_radial_gradient_attributes(doc: &Document) {
for node in &mut gen_order(doc, EId::RadialGradient) {
resolve_attr(node, AId::GradientUnits, Some(AValue::from("objectBoundingBox")));
resolve_attr(node, AId::SpreadMethod, Some(AValue::from("pad")));
resolve_attr(node, AId::Cx, Some(AValue::from(Length::new(50.0, Unit::Percent))));
resolve_attr(node, AId::Cy, Some(AValue::from(Length::new(50.0, Unit::Percent))));
resolve_attr(node, AId::R, Some(AValue::from(Length::new(50.0, Unit::Percent))));
resolve_attr(node, AId::GradientTransform, None);
// If `fx` is not found then set it to `cx`.
let cx = node.attributes().get_value(AId::Cx).cloned();
resolve_attr(node, AId::Fx, cx);
// If `fy` is not found then set it to `cy`.
let cy = node.attributes().get_value(AId::Cy).cloned();
resolve_attr(node, AId::Fy, cy);
fix_units(node, AId::GradientUnits, "objectBoundingBox");
}
}
// Should be done after `convert_units`.
pub fn fix_radial_gradient_attributes(doc: &Document) {
for node in &mut gen_order(doc, EId::RadialGradient) {
// Replace negative `r` with zero
// otherwise `fx` and `fy` may became NaN.
//
// Note: negative `r` is an error and UB, so we can resolve it
// in whatever way we want.
// By replacing it with zero we will get the same behavior as in Chrome.
let r = node.attributes().get_number(AId::R).unwrap();
if r < 0.0 {
node.set_attribute((AId::R, 0.0));
}
prepare_focal(node);
}
}
/// Resolves `pattern` attributes.
pub fn resolve_pattern_attributes(doc: &Document) {
for node in &mut gen_order(doc, EId::Pattern) {
resolve_attr(node, AId::PatternUnits, Some(AValue::from("objectBoundingBox")));
resolve_attr(node, AId::PatternContentUnits, Some(AValue::from("userSpaceOnUse")));
resolve_attr(node, AId::PatternTransform, None);
resolve_attr(node, AId::X, Some(AValue::from(0.0)));
resolve_attr(node, AId::Y, Some(AValue::from(0.0)));
resolve_attr(node, AId::Width, Some(AValue::from(0.0)));
resolve_attr(node, AId::Height, Some(AValue::from(0.0)));
resolve_attr(node, AId::PreserveAspectRatio, Some(AValue::from(AspectRatio::default())));
resolve_attr(node, AId::ViewBox, None);
fix_units(node, AId::PatternUnits, "objectBoundingBox");
fix_units(node, AId::PatternContentUnits, "userSpaceOnUse");
}
}
/// Resolves `filter` attributes.
pub fn resolve_filter_attributes(doc: &Document) {
for node in &mut gen_order(doc, EId::Filter) {
resolve_attr(node, AId::FilterUnits, Some(AValue::from("objectBoundingBox")));
resolve_attr(node, AId::PrimitiveUnits, Some(AValue::from("userSpaceOnUse")));
resolve_attr(node, AId::X, Some(AValue::from(Length::new(-10.0, Unit::Percent))));
resolve_attr(node, AId::Y, Some(AValue::from(Length::new(-10.0, Unit::Percent))));
resolve_attr(node, AId::Width, Some(AValue::from(Length::new(120.0, Unit::Percent))));
resolve_attr(node, AId::Height, Some(AValue::from(Length::new(120.0, Unit::Percent))));
fix_units(node, AId::FilterUnits, "objectBoundingBox");
fix_units(node, AId::PrimitiveUnits, "userSpaceOnUse");
}
}
fn fix_units(node: &mut Node, aid: AId, default: &str) {
let is_valid_filter_units = {
let attrs = node.attributes();
let filter_units = attrs.get_str_or(aid, "");
filter_units == "objectBoundingBox" || filter_units == "userSpaceOnUse"
};
if !is_valid_filter_units {
node.set_attribute((aid, default));
}
}
/// Generates a list of elements from less used to most used.
///
/// If an element has an `xlink:href` attribute then it can inherit specific
/// attributes from the linked element.
/// But if it also has an `xlink:href` attribute than we have to follow it too.
/// And on and on. Until the element with a required attribute.
///
/// Let's say we have an SVG like this:
/// ```text
/// <linearGradient id="lg1" x1="5" y2="20"/>
/// <linearGradient id="lg2" xlink:href="#lg1" x2="10"/>
/// <linearGradient id="lg3" xlink:href="#lg2" y1="15"/>
/// ```
///
/// It should be resolved to:
/// ```text
/// <linearGradient id="lg1" x1="5" y2="20"/>
/// <linearGradient id="lg2" x1="5" x2="10" y2="20"/>
/// <linearGradient id="lg3" x1="5" x2="10" y1="15" y2="20"/>
/// ```
///
/// But in the `radialGradient` case, the `fx` and `fy` attributes
/// should fallback to `cx` and `cy` attributes and only after
/// the `fx` and `fy` attributes are resolved.
///
/// So an SVG like this:
/// ```text
/// <radialGradient id="rg2" xlink:href="#rg1" cx="10"/>
/// <radialGradient id="rg3" xlink:href="#rg2" fy="15"/>
/// <radialGradient id="rg1" fx="5"/>
/// ```
///
/// Should be resolved in order: `rg1` -> `rg2` -> `rg3`.
/// And will produce:
/// ```text
/// <radialGradient id="rg2" fx="5" cx="10"/>
/// <radialGradient id="rg3" cx="10" fx="5" fy="15"/>
/// <radialGradient id="rg1" fx="5"/>
/// ```
///
/// And not in `rg2` -> `rg3` -> `rg1` order.
/// Because it will produce:
/// ```text
/// <radialGradient id="rg2" fx="10" fy="10" cx="10"/>
/// <radialGradient id="rg3" fx="10" fy="15" cx="10"/> <!-- fx is 10, not 5 -->
/// <radialGradient id="rg1" fx="5"/>
/// ```
fn gen_order(doc: &Document, eid: EId) -> Vec<Node> {
let nodes = doc.root().descendants().filter(|n| n.is_tag_name(eid))
.collect::<Vec<Node>>();
let mut order = Vec::with_capacity(nodes.len());
while order.len() != nodes.len() {
for node in &nodes {
if order.iter().any(|n| n == node) {
continue;
}
let c = node.linked_nodes().iter().filter(|n| {
n.is_tag_name(eid) && !order.iter().any(|on| on == *n)
}).count();
if c == 0 {
order.push(node.clone());
}
}
}
order
}
fn resolve_attr(node: &mut Node, id: AId, def_value: Option<AValue>) {
if node.has_attribute(id) {
return;
}
let v = match node.tag_id().unwrap() {
EId::LinearGradient => resolve_lg_attr(node, id, def_value),
EId::RadialGradient => resolve_rg_attr(node, id, def_value),
EId::Pattern => resolve_patt_attr(node, id, def_value),
EId::Filter => resolve_filter_attr(node, id, def_value),
_ => None,
};
if let Some(v) = v {
node.set_attribute((id, v));
}
}
fn resolve_lg_attr(
node: &Node,
aid: AId,
def_value: Option<AValue>,
) -> Option<AValue> {
if node.has_attribute(aid) {
return node.attributes().get_value(aid).cloned();
}
// Check for referenced element first.
let link = match node.attributes().get_value(AId::Href) {
Some(&AValue::Link(ref link)) => link.clone(),
_ => {
// Use current element.
return match node.attributes().get_value(aid) {
Some(v) => Some(v.clone()),
None => def_value,
};
}
};
// If `link` is not an SVG element - return `def_value`.
let eid = match link.tag_id() {
Some(eid) => eid,
None => return def_value,
};
match (aid, eid) {
// Coordinates can be resolved only from
// ref element with the same type.
(AId::X1, EId::LinearGradient)
| (AId::Y1, EId::LinearGradient)
| (AId::X2, EId::LinearGradient)
| (AId::Y2, EId::LinearGradient)
// Other attributes can be resolved
// from any kind of gradient.
| (AId::GradientUnits, EId::LinearGradient)
| (AId::GradientUnits, EId::RadialGradient)
| (AId::SpreadMethod, EId::LinearGradient)
| (AId::SpreadMethod, EId::RadialGradient)
| (AId::GradientTransform, EId::LinearGradient)
| (AId::GradientTransform, EId::RadialGradient) => {
resolve_lg_attr(&link, aid, def_value)
}
_ => def_value
}
}
fn resolve_rg_attr(
node: &Node,
aid: AId,
def_value: Option<AValue>,
) -> Option<AValue> {
if node.has_attribute(aid) {
return node.attributes().get_value(aid).cloned();
}
// Check for referenced element first.
let link = match node.attributes().get_value(AId::Href) {
Some(&AValue::Link(ref link)) => link.clone(),
_ => {
// Use current element.
return match node.attributes().get_value(aid) {
Some(v) => Some(v.clone()),
None => def_value,
};
}
};
// If `link` is not an SVG element - return `def_value`.
let eid = match link.tag_id() {
Some(eid) => eid,
None => return def_value,
};
match (aid, eid) {
// Coordinates can be resolved only from
// ref element with the same type.
(AId::Cx, EId::RadialGradient)
| (AId::Cy, EId::RadialGradient)
| (AId::R, EId::RadialGradient)
| (AId::Fx, EId::RadialGradient)
| (AId::Fy, EId::RadialGradient)
// Other attributes can be resolved
// from any kind of gradient.
| (AId::GradientUnits, EId::LinearGradient)
| (AId::GradientUnits, EId::RadialGradient)
| (AId::SpreadMethod, EId::LinearGradient)
| (AId::SpreadMethod, EId::RadialGradient)
| (AId::GradientTransform, EId::LinearGradient)
| (AId::GradientTransform, EId::RadialGradient) => {
resolve_rg_attr(&link, aid, def_value)
}
_ => def_value
}
}
fn resolve_patt_attr(
node: &Node,
aid: AId,
def_value: Option<AValue>,
) -> Option<AValue> {
if node.has_attribute(aid) {
return node.attributes().get_value(aid).cloned();
}
// Check for referenced element first.
let link = match node.attributes().get_value(AId::Href) {
Some(&AValue::Link(ref link)) => link.clone(),
_ => {
// Use current element.
return match node.attributes().get_value(aid) {
Some(v) => Some(v.clone()),
None => def_value,
};
}
};
// If `link` is not an SVG element - return `def_value`.
match link.tag_id() {
Some(EId::Pattern) => resolve_patt_attr(&link, aid, def_value),
_ => def_value,
}
}
fn resolve_filter_attr(
node: &Node,
aid: AId,
def_value: Option<AValue>,
) -> Option<AValue> {
if node.has_attribute(aid) {
return node.attributes().get_value(aid).cloned();
}
// Check for referenced element first.
let link = match node.attributes().get_value(AId::Href) {
Some(&AValue::Link(ref link)) => link.clone(),
_ => {
// Use current element.
return match node.attributes().get_value(aid) {
Some(v) => Some(v.clone()),
None => def_value,
};
}
};
// If `link` is not an SVG element - return `def_value`.
match link.tag_id() {
Some(EId::Filter) => resolve_filter_attr(&link, aid, def_value),
_ => def_value,
}
}
/// Prepares the radial gradient focal radius.
///
/// According to the SVG spec:
///
/// If the point defined by `fx` and `fy` lies outside the circle defined by
/// `cx`, `cy` and `r`, then the user agent shall set the focal point to the
/// intersection of the line from (`cx`, `cy`) to (`fx`, `fy`) with the circle
/// defined by `cx`, `cy` and `r`.
fn prepare_focal(node: &mut Node) {
let (new_fx, new_fy) = {
let attrs = node.attributes();
// Unwrap is safe, because we just resolved all this attributes.
let cx = attrs.get_number(AId::Cx).unwrap();
let cy = attrs.get_number(AId::Cy).unwrap();
let r = attrs.get_number(AId::R).unwrap();
let fx = attrs.get_number(AId::Fx).unwrap();
let fy = attrs.get_number(AId::Fy).unwrap();
let max_r = r - r * 0.001;
let mut line = Line::new(cx, cy, fx, fy);
if line.length() > max_r {
line.set_length(max_r);
}
(line.x2, line.y2)
};
node.set_attribute((AId::Fx, new_fx));
node.set_attribute((AId::Fy, new_fy));
}