use cssparser::{Parser, Token, match_ignore_ascii_case};
use std::f64::consts::*;
use std::fmt;
use std::marker::PhantomData;
use crate::dpi::Dpi;
use crate::drawing_ctx::Viewport;
use crate::error::*;
use crate::parsers::{Parse, finite_f32};
use crate::properties::{ComputedValues, FontSize, TextOrientation, WritingMode};
use crate::rect::Rect;
use crate::viewbox::ViewBox;
#[non_exhaustive]
#[repr(C)]
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum LengthUnit {
Percent,
Px,
Em,
Ex,
In,
Cm,
Mm,
Pt,
Pc,
Ch,
}
#[repr(C)]
#[derive(Debug, PartialEq, Copy, Clone)]
pub struct RsvgLength {
pub length: f64,
pub unit: LengthUnit,
}
impl RsvgLength {
pub fn new(l: f64, unit: LengthUnit) -> RsvgLength {
RsvgLength { length: l, unit }
}
}
pub trait Normalize {
fn normalize(x: f64, y: f64) -> f64;
}
#[derive(Debug, PartialEq, Copy, Clone)]
pub struct Horizontal;
#[derive(Debug, PartialEq, Copy, Clone)]
pub struct Vertical;
#[derive(Debug, PartialEq, Copy, Clone)]
pub struct Both;
impl Normalize for Horizontal {
#[inline]
fn normalize(x: f64, _y: f64) -> f64 {
x
}
}
impl Normalize for Vertical {
#[inline]
fn normalize(_x: f64, y: f64) -> f64 {
y
}
}
impl Normalize for Both {
#[inline]
fn normalize(x: f64, y: f64) -> f64 {
viewport_percentage(x, y)
}
}
pub trait Validate {
fn validate(v: f64) -> Result<f64, ValueErrorKind> {
Ok(v)
}
}
#[derive(Debug, PartialEq, Copy, Clone)]
pub struct Signed;
impl Validate for Signed {}
#[derive(Debug, PartialEq, Copy, Clone)]
pub struct Unsigned;
impl Validate for Unsigned {
fn validate(v: f64) -> Result<f64, ValueErrorKind> {
if v >= 0.0 {
Ok(v)
} else {
Err(ValueErrorKind::Value(
"value must be non-negative".to_string(),
))
}
}
}
#[derive(Debug, PartialEq, Copy, Clone)]
pub struct CssLength<N: Normalize, V: Validate> {
pub length: f64,
pub unit: LengthUnit,
orientation: PhantomData<N>,
validation: PhantomData<V>,
}
impl<N: Normalize, V: Validate> From<CssLength<N, V>> for RsvgLength {
fn from(l: CssLength<N, V>) -> RsvgLength {
RsvgLength {
length: l.length,
unit: l.unit,
}
}
}
impl<N: Normalize, V: Validate> Default for CssLength<N, V> {
fn default() -> Self {
CssLength::new(0.0, LengthUnit::Px)
}
}
pub const POINTS_PER_INCH: f64 = 72.0;
const CM_PER_INCH: f64 = 2.54;
const MM_PER_INCH: f64 = 25.4;
const PICA_PER_INCH: f64 = 6.0;
impl<N: Normalize, V: Validate> Parse for CssLength<N, V> {
fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<CssLength<N, V>, ParseError<'i>> {
let l_value;
let l_unit;
let token = parser.next()?.clone();
match token {
Token::Number { value, .. } => {
l_value = value;
l_unit = LengthUnit::Px;
}
Token::Percentage { unit_value, .. } => {
l_value = unit_value;
l_unit = LengthUnit::Percent;
}
Token::Dimension {
value, ref unit, ..
} => {
l_value = value;
l_unit = match_ignore_ascii_case! {unit.as_ref(),
"px" => LengthUnit::Px,
"em" => LengthUnit::Em,
"ex" => LengthUnit::Ex,
"in" => LengthUnit::In,
"cm" => LengthUnit::Cm,
"mm" => LengthUnit::Mm,
"pt" => LengthUnit::Pt,
"pc" => LengthUnit::Pc,
"ch" => LengthUnit::Ch,
_ => return Err(parser.new_unexpected_token_error(token)),
};
}
_ => return Err(parser.new_unexpected_token_error(token)),
}
let l_value = f64::from(finite_f32(l_value).map_err(|e| parser.new_custom_error(e))?);
<V as Validate>::validate(l_value)
.map_err(|e| parser.new_custom_error(e))
.map(|l_value| CssLength::new(l_value, l_unit))
}
}
pub struct NormalizeValues {
font_size: FontSize,
is_vertical_text: bool,
}
impl NormalizeValues {
pub fn new(values: &ComputedValues) -> NormalizeValues {
let is_vertical_text = matches!(
(values.writing_mode(), values.text_orientation()),
(WritingMode::VerticalLr, TextOrientation::Upright)
| (WritingMode::VerticalRl, TextOrientation::Upright)
);
NormalizeValues {
font_size: values.font_size(),
is_vertical_text,
}
}
}
pub struct NormalizeParams {
vbox: ViewBox,
font_size: f64,
dpi: Dpi,
is_vertical_text: bool,
}
impl NormalizeParams {
pub fn new(values: &ComputedValues, viewport: &Viewport) -> NormalizeParams {
let v = NormalizeValues::new(values);
NormalizeParams::from_values(&v, viewport)
}
pub fn from_values(v: &NormalizeValues, viewport: &Viewport) -> NormalizeParams {
NormalizeParams {
vbox: viewport.vbox,
font_size: font_size_from_values(v, viewport.dpi),
dpi: viewport.dpi,
is_vertical_text: v.is_vertical_text,
}
}
pub fn from_dpi(dpi: Dpi) -> NormalizeParams {
NormalizeParams {
vbox: ViewBox::from(Rect::default()),
font_size: 1.0,
dpi,
is_vertical_text: false,
}
}
}
impl<N: Normalize, V: Validate> CssLength<N, V> {
pub fn new(l: f64, unit: LengthUnit) -> CssLength<N, V> {
CssLength {
length: l,
unit,
orientation: PhantomData,
validation: PhantomData,
}
}
pub fn to_user(&self, params: &NormalizeParams) -> f64 {
match self.unit {
LengthUnit::Px => self.length,
LengthUnit::Percent => {
self.length * <N as Normalize>::normalize(params.vbox.width(), params.vbox.height())
}
LengthUnit::Em => self.length * params.font_size,
LengthUnit::Ex => self.length * params.font_size / 2.0,
LengthUnit::Ch => {
if params.is_vertical_text {
self.length * params.font_size
} else {
self.length * params.font_size / 2.0
}
}
LengthUnit::In => self.length * <N as Normalize>::normalize(params.dpi.x, params.dpi.y),
LengthUnit::Cm => {
self.length * <N as Normalize>::normalize(params.dpi.x, params.dpi.y) / CM_PER_INCH
}
LengthUnit::Mm => {
self.length * <N as Normalize>::normalize(params.dpi.x, params.dpi.y) / MM_PER_INCH
}
LengthUnit::Pt => {
self.length * <N as Normalize>::normalize(params.dpi.x, params.dpi.y)
/ POINTS_PER_INCH
}
LengthUnit::Pc => {
self.length * <N as Normalize>::normalize(params.dpi.x, params.dpi.y)
/ PICA_PER_INCH
}
}
}
pub fn to_points(&self, params: &NormalizeParams) -> f64 {
match self.unit {
LengthUnit::Px => {
self.length / <N as Normalize>::normalize(params.dpi.x, params.dpi.y) * 72.0
}
LengthUnit::Percent => {
panic!("Cannot convert a percentage length into an absolute length");
}
LengthUnit::Em => {
panic!("Cannot convert an Em length into an absolute length");
}
LengthUnit::Ex => {
panic!("Cannot convert an Ex length into an absolute length");
}
LengthUnit::In => self.length * POINTS_PER_INCH,
LengthUnit::Cm => self.length / CM_PER_INCH * POINTS_PER_INCH,
LengthUnit::Mm => self.length / MM_PER_INCH * POINTS_PER_INCH,
LengthUnit::Pt => self.length,
LengthUnit::Pc => self.length / PICA_PER_INCH * POINTS_PER_INCH,
LengthUnit::Ch => {
panic!("Cannot convert a Ch length into an absolute length");
}
}
}
pub fn to_inches(&self, params: &NormalizeParams) -> f64 {
self.to_points(params) / POINTS_PER_INCH
}
pub fn to_cm(&self, params: &NormalizeParams) -> f64 {
self.to_inches(params) * CM_PER_INCH
}
pub fn to_mm(&self, params: &NormalizeParams) -> f64 {
self.to_inches(params) * MM_PER_INCH
}
pub fn to_picas(&self, params: &NormalizeParams) -> f64 {
self.to_inches(params) * PICA_PER_INCH
}
}
fn font_size_from_values(values: &NormalizeValues, dpi: Dpi) -> f64 {
let v = values.font_size.value();
match v.unit {
LengthUnit::Percent => unreachable!("ComputedValues can't have a relative font size"),
LengthUnit::Px => v.length,
LengthUnit::Em => v.length * 12.0,
LengthUnit::Ex => v.length * 12.0 / 2.0,
LengthUnit::Ch => v.length * 12.0 / 2.0,
LengthUnit::In => v.length * Both::normalize(dpi.x, dpi.y),
LengthUnit::Cm => v.length * Both::normalize(dpi.x, dpi.y) / CM_PER_INCH,
LengthUnit::Mm => v.length * Both::normalize(dpi.x, dpi.y) / MM_PER_INCH,
LengthUnit::Pt => v.length * Both::normalize(dpi.x, dpi.y) / POINTS_PER_INCH,
LengthUnit::Pc => v.length * Both::normalize(dpi.x, dpi.y) / PICA_PER_INCH,
}
}
fn viewport_percentage(x: f64, y: f64) -> f64 {
(x * x + y * y).sqrt() / SQRT_2
}
pub type Length<N> = CssLength<N, Signed>;
pub type ULength<N> = CssLength<N, Unsigned>;
#[derive(Debug, Default, PartialEq, Copy, Clone)]
pub enum LengthOrAuto<N: Normalize> {
#[default]
Auto,
Length(CssLength<N, Unsigned>),
}
impl<N: Normalize> Parse for LengthOrAuto<N> {
fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<LengthOrAuto<N>, ParseError<'i>> {
if parser
.try_parse(|i| i.expect_ident_matching("auto"))
.is_ok()
{
Ok(LengthOrAuto::Auto)
} else {
Ok(LengthOrAuto::Length(CssLength::parse(parser)?))
}
}
}
impl fmt::Display for LengthUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let unit = match &self {
LengthUnit::Percent => "%",
LengthUnit::Px => "px",
LengthUnit::Em => "em",
LengthUnit::Ex => "ex",
LengthUnit::In => "in",
LengthUnit::Cm => "cm",
LengthUnit::Mm => "mm",
LengthUnit::Pt => "pt",
LengthUnit::Pc => "pc",
LengthUnit::Ch => "ch",
};
write!(f, "{unit}")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::properties::{ParsedProperty, SpecifiedValue, SpecifiedValues};
use crate::{assert_approx_eq_cairo, float_eq_cairo::ApproxEqCairo};
#[test]
fn parses_default() {
assert_eq!(
Length::<Horizontal>::parse_str("42").unwrap(),
Length::<Horizontal>::new(42.0, LengthUnit::Px)
);
assert_eq!(
Length::<Horizontal>::parse_str("-42px").unwrap(),
Length::<Horizontal>::new(-42.0, LengthUnit::Px)
);
}
#[test]
fn parses_percent() {
assert_eq!(
Length::<Horizontal>::parse_str("50.0%").unwrap(),
Length::<Horizontal>::new(0.5, LengthUnit::Percent)
);
}
#[test]
fn parses_font_em() {
assert_eq!(
Length::<Vertical>::parse_str("22.5em").unwrap(),
Length::<Vertical>::new(22.5, LengthUnit::Em)
);
}
#[test]
fn parses_font_ex() {
assert_eq!(
Length::<Vertical>::parse_str("22.5ex").unwrap(),
Length::<Vertical>::new(22.5, LengthUnit::Ex)
);
}
#[test]
fn parses_font_ch() {
assert_eq!(
Length::<Vertical>::parse_str("22.5ch").unwrap(),
Length::<Vertical>::new(22.5, LengthUnit::Ch)
);
}
#[test]
fn parses_physical_units() {
assert_eq!(
Length::<Both>::parse_str("72pt").unwrap(),
Length::<Both>::new(72.0, LengthUnit::Pt)
);
assert_eq!(
Length::<Both>::parse_str("-22.5in").unwrap(),
Length::<Both>::new(-22.5, LengthUnit::In)
);
assert_eq!(
Length::<Both>::parse_str("-254cm").unwrap(),
Length::<Both>::new(-254.0, LengthUnit::Cm)
);
assert_eq!(
Length::<Both>::parse_str("254mm").unwrap(),
Length::<Both>::new(254.0, LengthUnit::Mm)
);
assert_eq!(
Length::<Both>::parse_str("60pc").unwrap(),
Length::<Both>::new(60.0, LengthUnit::Pc)
);
}
#[test]
fn parses_unsigned() {
assert_eq!(
ULength::<Horizontal>::parse_str("42").unwrap(),
ULength::<Horizontal>::new(42.0, LengthUnit::Px)
);
assert_eq!(
ULength::<Both>::parse_str("0pt").unwrap(),
ULength::<Both>::new(0.0, LengthUnit::Pt)
);
assert!(ULength::<Horizontal>::parse_str("-42px").is_err());
}
#[test]
fn empty_length_yields_error() {
assert!(Length::<Both>::parse_str("").is_err());
}
#[test]
fn invalid_unit_yields_error() {
assert!(Length::<Both>::parse_str("8furlong").is_err());
}
#[test]
fn normalize_default_works() {
let viewport = Viewport::new(Dpi::new(40.0, 40.0), 100.0, 100.0);
let values = ComputedValues::default();
let params = NormalizeParams::new(&values, &viewport);
assert_approx_eq_cairo!(
Length::<Both>::new(10.0, LengthUnit::Px).to_user(¶ms),
10.0
);
}
#[test]
fn normalize_absolute_units_works() {
let viewport = Viewport::new(Dpi::new(40.0, 50.0), 100.0, 100.0);
let values = ComputedValues::default();
let params = NormalizeParams::new(&values, &viewport);
assert_approx_eq_cairo!(
Length::<Horizontal>::new(10.0, LengthUnit::In).to_user(¶ms),
400.0
);
assert_approx_eq_cairo!(
Length::<Vertical>::new(10.0, LengthUnit::In).to_user(¶ms),
500.0
);
assert_approx_eq_cairo!(
Length::<Horizontal>::new(10.0, LengthUnit::Cm).to_user(¶ms),
400.0 / CM_PER_INCH
);
assert_approx_eq_cairo!(
Length::<Horizontal>::new(10.0, LengthUnit::Mm).to_user(¶ms),
400.0 / MM_PER_INCH
);
assert_approx_eq_cairo!(
Length::<Horizontal>::new(10.0, LengthUnit::Pt).to_user(¶ms),
400.0 / POINTS_PER_INCH
);
assert_approx_eq_cairo!(
Length::<Horizontal>::new(10.0, LengthUnit::Pc).to_user(¶ms),
400.0 / PICA_PER_INCH
);
}
#[test]
fn normalize_percent_works() {
let viewport = Viewport::new(Dpi::new(40.0, 40.0), 100.0, 200.0);
let values = ComputedValues::default();
let params = NormalizeParams::new(&values, &viewport);
assert_approx_eq_cairo!(
Length::<Horizontal>::new(0.05, LengthUnit::Percent).to_user(¶ms),
5.0
);
assert_approx_eq_cairo!(
Length::<Vertical>::new(0.05, LengthUnit::Percent).to_user(¶ms),
10.0
);
}
#[test]
fn normalize_font_em_ex_ch_works() {
let mut values = ComputedValues::default();
let viewport = Viewport::new(Dpi::new(40.0, 40.0), 100.0, 200.0);
let mut params = NormalizeParams::new(&values, &viewport);
assert_approx_eq_cairo!(
Length::<Vertical>::new(1.0, LengthUnit::Em).to_user(¶ms),
12.0
);
assert_approx_eq_cairo!(
Length::<Vertical>::new(1.0, LengthUnit::Ex).to_user(¶ms),
6.0
);
assert_approx_eq_cairo!(
Length::<Vertical>::new(1.0, LengthUnit::Ch).to_user(¶ms),
6.0
);
let mut specified = SpecifiedValues::default();
specified.set_parsed_property(&ParsedProperty::TextOrientation(SpecifiedValue::Specified(
TextOrientation::Upright,
)));
specified.set_parsed_property(&ParsedProperty::WritingMode(SpecifiedValue::Specified(
WritingMode::VerticalLr,
)));
specified.to_computed_values(&mut values);
params = NormalizeParams::new(&values, &viewport);
assert_approx_eq_cairo!(
Length::<Vertical>::new(1.0, LengthUnit::Ch).to_user(¶ms),
12.0
);
}
#[test]
fn to_points_works() {
let params = NormalizeParams::from_dpi(Dpi::new(40.0, 96.0));
assert_approx_eq_cairo!(
Length::<Horizontal>::new(80.0, LengthUnit::Px).to_points(¶ms),
2.0 * 72.0
);
assert_approx_eq_cairo!(
Length::<Vertical>::new(192.0, LengthUnit::Px).to_points(¶ms),
2.0 * 72.0
);
}
}