rsvg/
length.rs

1//! CSS length values.
2//!
3//! [`CssLength`] is the struct librsvg uses to represent CSS lengths.
4//! See its documentation for examples of how to construct it.
5//!
6//! `CssLength` values need to know whether they will be normalized with respect to the width,
7//! height, or both dimensions of the current viewport.  `CssLength` values can be signed or
8//! unsigned.  So, a `CssLength` has two type parameters, [`Normalize`] and [`Validate`];
9//! the full type is `CssLength<N: Normalize, V: Validate>`.  We provide [`Horizontal`],
10//! [`Vertical`], and [`Both`] implementations of [`Normalize`]; these let length values know
11//! how to normalize themselves with respect to the current viewport.  We also provide
12//! [`Signed`] and [`Unsigned`] implementations of [`Validate`].
13//!
14//! For ease of use, we define two type aliases [`Length`] and [`ULength`] corresponding to
15//! signed and unsigned.
16//!
17//! For example, the implementation of [`Circle`][crate::shapes::Circle] defines this
18//! structure with fields for the `(center_x, center_y, radius)`:
19//!
20//! ```
21//! # use rsvg::doctest_only::{Length,ULength,Horizontal,Vertical,Both};
22//! pub struct Circle {
23//!     cx: Length<Horizontal>,
24//!     cy: Length<Vertical>,
25//!     r: ULength<Both>,
26//! }
27//! ```
28//!
29//! This means that:
30//!
31//! * `cx` and `cy` define the center of the circle, they can be positive or negative, and
32//!   they will be normalized with respect to the current viewport's width and height,
33//!   respectively.  If the SVG document specified `<circle cx="50%" cy="30%">`, the values
34//!   would be normalized to be at 50% of the the viewport's width, and 30% of the viewport's
35//!   height.
36//!
37//! * `r` is non-negative and needs to be resolved against the [normalized diagonal][diag]
38//!   of the current viewport.
39//!
40//! The `N` type parameter of `CssLength<N, I>` is enough to know how to normalize a length
41//! value; the [`CssLength::to_user`] method will handle it automatically.
42//!
43//! [diag]: https://www.w3.org/TR/SVG/coords.html#Units
44
45use cssparser::{Parser, Token, match_ignore_ascii_case};
46use std::f64::consts::*;
47use std::fmt;
48use std::marker::PhantomData;
49
50use crate::dpi::Dpi;
51use crate::drawing_ctx::Viewport;
52use crate::error::*;
53use crate::parsers::{Parse, finite_f32};
54use crate::properties::{ComputedValues, FontSize, TextOrientation, WritingMode};
55use crate::rect::Rect;
56use crate::viewbox::ViewBox;
57
58/// Units for length values.
59// This needs to be kept in sync with `rsvg.h:RsvgUnit`.
60#[non_exhaustive]
61#[repr(C)]
62#[derive(Debug, PartialEq, Copy, Clone)]
63pub enum LengthUnit {
64    /// `1.0` means 100%
65    Percent,
66
67    /// Pixels, or the CSS default unit
68    Px,
69
70    /// Size of the current font
71    Em,
72
73    /// x-height of the current font
74    Ex,
75
76    /// Inches (25.4 mm)
77    In,
78
79    /// Centimeters
80    Cm,
81
82    /// Millimeters
83    Mm,
84
85    /// Points (1/72 inch)
86    Pt,
87
88    /// Picas (12 points)
89    Pc,
90
91    /// Advance measure of a '0' character (depends on the text orientation)
92    Ch,
93}
94
95/// A CSS length value.
96///
97/// This is equivalent to [CSS lengths].
98///
99/// [CSS lengths]: https://www.w3.org/TR/CSS22/syndata.html#length-units
100///
101/// It is up to the calling application to convert lengths in non-pixel units (i.e. those
102/// where the [`unit`][RsvgLength::unit] field is not [`LengthUnit::Px`]) into something
103/// meaningful to the application.  For example, if your application knows the
104/// dots-per-inch (DPI) it is using, it can convert lengths with [`unit`] in
105/// [`LengthUnit::In`] or other physical units.
106// Keep this in sync with rsvg.h:RsvgLength
107#[repr(C)]
108#[derive(Debug, PartialEq, Copy, Clone)]
109pub struct RsvgLength {
110    /// Numeric part of the length
111    pub length: f64,
112
113    /// Unit part of the length
114    pub unit: LengthUnit,
115}
116
117impl RsvgLength {
118    /// Constructs a CSS length value.
119    pub fn new(l: f64, unit: LengthUnit) -> RsvgLength {
120        RsvgLength { length: l, unit }
121    }
122}
123
124/// Used for the `N` type parameter of `CssLength<N: Normalize, V: Validate>`.
125pub trait Normalize {
126    /// Computes an orientation-based scaling factor.
127    ///
128    /// This is used in the [`CssLength::to_user`] method to resolve lengths with percentage
129    /// units; they need to be resolved with respect to the width, height, or [normalized
130    /// diagonal][diag] of the current viewport.
131    ///
132    /// [diag]: https://www.w3.org/TR/SVG/coords.html#Units
133    fn normalize(x: f64, y: f64) -> f64;
134}
135
136/// Allows declaring `CssLength<Horizontal>`.
137#[derive(Debug, PartialEq, Copy, Clone)]
138pub struct Horizontal;
139
140/// Allows declaring `CssLength<Vertical>`.
141#[derive(Debug, PartialEq, Copy, Clone)]
142pub struct Vertical;
143
144/// Allows declaring `CssLength<Both>`.
145#[derive(Debug, PartialEq, Copy, Clone)]
146pub struct Both;
147
148impl Normalize for Horizontal {
149    #[inline]
150    fn normalize(x: f64, _y: f64) -> f64 {
151        x
152    }
153}
154
155impl Normalize for Vertical {
156    #[inline]
157    fn normalize(_x: f64, y: f64) -> f64 {
158        y
159    }
160}
161
162impl Normalize for Both {
163    #[inline]
164    fn normalize(x: f64, y: f64) -> f64 {
165        viewport_percentage(x, y)
166    }
167}
168
169/// Used for the `V` type parameter of `CssLength<N: Normalize, V: Validate>`.
170pub trait Validate {
171    /// Checks if the specified value is acceptable
172    ///
173    /// This is used when parsing a length value
174    fn validate(v: f64) -> Result<f64, ValueErrorKind> {
175        Ok(v)
176    }
177}
178
179/// Used to implement `CssLength<N, Signed>`.
180///
181/// Signed lengths do not require validation, so they use the default implementation of of
182/// [Validate].
183#[derive(Debug, PartialEq, Copy, Clone)]
184pub struct Signed;
185
186impl Validate for Signed {}
187
188/// Used to implement `CssLength<N, Unsigned>`.
189///
190/// Unsigned lengths need validation to ensure that their value is non-negative,
191/// so they have a custom implementation of [Validate].
192#[derive(Debug, PartialEq, Copy, Clone)]
193pub struct Unsigned;
194
195impl Validate for Unsigned {
196    fn validate(v: f64) -> Result<f64, ValueErrorKind> {
197        if v >= 0.0 {
198            Ok(v)
199        } else {
200            Err(ValueErrorKind::Value(
201                "value must be non-negative".to_string(),
202            ))
203        }
204    }
205}
206
207/// A CSS length value.
208///
209/// This is equivalent to [CSS lengths].
210///
211/// [CSS lengths]: https://www.w3.org/TR/CSS22/syndata.html#length-units
212///
213/// `CssLength` implements the [`Parse`] trait, so it can be parsed out of a
214/// [`cssparser::Parser`].
215///
216/// This type will be normally used through the type aliases [`Length`] and [`ULength`]
217///
218/// Examples of construction:
219///
220/// ```
221/// # use rsvg::doctest_only::{Length,ULength,LengthUnit,Horizontal,Vertical,Both};
222/// # use rsvg::doctest_only::Parse;
223/// // Explicit type
224/// let width: Length<Horizontal> = Length::new(42.0, LengthUnit::Cm);
225///
226/// // Inferred type
227/// let height = Length::<Vertical>::new(42.0, LengthUnit::Cm);
228///
229/// // Parsed
230/// let radius = ULength::<Both>::parse_str("5px").unwrap();
231/// ```
232///
233/// During the rendering phase, a `CssLength` needs to be converted to user-space
234/// coordinates with the [`CssLength::to_user`] method.
235#[derive(Debug, PartialEq, Copy, Clone)]
236pub struct CssLength<N: Normalize, V: Validate> {
237    /// Numeric part of the length
238    pub length: f64,
239
240    /// Unit part of the length
241    pub unit: LengthUnit,
242
243    /// Dummy; used internally for the type parameter `N`
244    orientation: PhantomData<N>,
245
246    /// Dummy; used internally for the type parameter `V`
247    validation: PhantomData<V>,
248}
249
250impl<N: Normalize, V: Validate> From<CssLength<N, V>> for RsvgLength {
251    fn from(l: CssLength<N, V>) -> RsvgLength {
252        RsvgLength {
253            length: l.length,
254            unit: l.unit,
255        }
256    }
257}
258
259impl<N: Normalize, V: Validate> Default for CssLength<N, V> {
260    fn default() -> Self {
261        CssLength::new(0.0, LengthUnit::Px)
262    }
263}
264
265pub const POINTS_PER_INCH: f64 = 72.0;
266const CM_PER_INCH: f64 = 2.54;
267const MM_PER_INCH: f64 = 25.4;
268const PICA_PER_INCH: f64 = 6.0;
269
270impl<N: Normalize, V: Validate> Parse for CssLength<N, V> {
271    fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<CssLength<N, V>, ParseError<'i>> {
272        let l_value;
273        let l_unit;
274
275        let token = parser.next()?.clone();
276
277        match token {
278            Token::Number { value, .. } => {
279                l_value = value;
280                l_unit = LengthUnit::Px;
281            }
282
283            Token::Percentage { unit_value, .. } => {
284                l_value = unit_value;
285                l_unit = LengthUnit::Percent;
286            }
287
288            Token::Dimension {
289                value, ref unit, ..
290            } => {
291                l_value = value;
292
293                l_unit = match_ignore_ascii_case! {unit.as_ref(),
294                    "px" => LengthUnit::Px,
295                    "em" => LengthUnit::Em,
296                    "ex" => LengthUnit::Ex,
297                    "in" => LengthUnit::In,
298                    "cm" => LengthUnit::Cm,
299                    "mm" => LengthUnit::Mm,
300                    "pt" => LengthUnit::Pt,
301                    "pc" => LengthUnit::Pc,
302                    "ch" => LengthUnit::Ch,
303
304                    _ => return Err(parser.new_unexpected_token_error(token)),
305                };
306            }
307
308            _ => return Err(parser.new_unexpected_token_error(token)),
309        }
310
311        let l_value = f64::from(finite_f32(l_value).map_err(|e| parser.new_custom_error(e))?);
312
313        <V as Validate>::validate(l_value)
314            .map_err(|e| parser.new_custom_error(e))
315            .map(|l_value| CssLength::new(l_value, l_unit))
316    }
317}
318
319/// Parameters for length normalization extracted from [`ComputedValues`].
320///
321/// This is a precursor to [`NormalizeParams::from_values`], for cases where it is inconvenient
322/// to keep a [`ComputedValues`] around.
323pub struct NormalizeValues {
324    font_size: FontSize,
325    is_vertical_text: bool,
326}
327
328impl NormalizeValues {
329    pub fn new(values: &ComputedValues) -> NormalizeValues {
330        let is_vertical_text = matches!(
331            (values.writing_mode(), values.text_orientation()),
332            (WritingMode::VerticalLr, TextOrientation::Upright)
333                | (WritingMode::VerticalRl, TextOrientation::Upright)
334        );
335
336        NormalizeValues {
337            font_size: values.font_size(),
338            is_vertical_text,
339        }
340    }
341}
342
343/// Parameters to normalize [`Length`] values to user-space distances.
344pub struct NormalizeParams {
345    vbox: ViewBox,
346    font_size: f64,
347    dpi: Dpi,
348    is_vertical_text: bool,
349}
350
351impl NormalizeParams {
352    /// Extracts the information needed to normalize [`Length`] values from a set of
353    /// [`ComputedValues`] and the viewport size in [`Viewport`].
354    pub fn new(values: &ComputedValues, viewport: &Viewport) -> NormalizeParams {
355        let v = NormalizeValues::new(values);
356        NormalizeParams::from_values(&v, viewport)
357    }
358
359    pub fn from_values(v: &NormalizeValues, viewport: &Viewport) -> NormalizeParams {
360        NormalizeParams {
361            vbox: viewport.vbox,
362            font_size: font_size_from_values(v, viewport.dpi),
363            dpi: viewport.dpi,
364            is_vertical_text: v.is_vertical_text,
365        }
366    }
367
368    /// Just used by rsvg-convert, where there is no font size nor viewport.
369    pub fn from_dpi(dpi: Dpi) -> NormalizeParams {
370        NormalizeParams {
371            vbox: ViewBox::from(Rect::default()),
372            font_size: 1.0,
373            dpi,
374            is_vertical_text: false,
375        }
376    }
377}
378
379impl<N: Normalize, V: Validate> CssLength<N, V> {
380    /// Creates a CssLength.
381    ///
382    /// The compiler needs to know the type parameters `N` and `V` which represents the
383    /// length's orientation and validation.
384    /// You can specify them explicitly, or call the parametrized method:
385    ///
386    /// ```
387    /// # use rsvg::doctest_only::{Length,LengthUnit,Horizontal,Vertical};
388    /// // Explicit type
389    /// let width: Length<Horizontal> = Length::new(42.0, LengthUnit::Cm);
390    ///
391    /// // Inferred type
392    /// let height = Length::<Vertical>::new(42.0, LengthUnit::Cm);
393    /// ```
394    pub fn new(l: f64, unit: LengthUnit) -> CssLength<N, V> {
395        CssLength {
396            length: l,
397            unit,
398            orientation: PhantomData,
399            validation: PhantomData,
400        }
401    }
402
403    /// Convert a Length with units into user-space coordinates.
404    ///
405    /// Lengths may come with non-pixel units, and when rendering, they need to be normalized
406    /// to pixels based on the current viewport (e.g. for lengths with percent units), and
407    /// based on the current element's set of [`ComputedValues`] (e.g. for lengths with `Em`
408    /// units that need to be resolved against the current font size).
409    ///
410    /// Those parameters can be obtained with [`NormalizeParams::new()`].
411    pub fn to_user(&self, params: &NormalizeParams) -> f64 {
412        match self.unit {
413            LengthUnit::Px => self.length,
414
415            LengthUnit::Percent => {
416                self.length * <N as Normalize>::normalize(params.vbox.width(), params.vbox.height())
417            }
418
419            LengthUnit::Em => self.length * params.font_size,
420
421            LengthUnit::Ex => self.length * params.font_size / 2.0,
422
423            // how far "0" advances the text, so it varies depending on orientation
424            // we're using the 0.5em or 1.0em (based on orientation) fallback from the spec
425            LengthUnit::Ch => {
426                if params.is_vertical_text {
427                    self.length * params.font_size
428                } else {
429                    self.length * params.font_size / 2.0
430                }
431            }
432
433            LengthUnit::In => self.length * <N as Normalize>::normalize(params.dpi.x, params.dpi.y),
434
435            LengthUnit::Cm => {
436                self.length * <N as Normalize>::normalize(params.dpi.x, params.dpi.y) / CM_PER_INCH
437            }
438
439            LengthUnit::Mm => {
440                self.length * <N as Normalize>::normalize(params.dpi.x, params.dpi.y) / MM_PER_INCH
441            }
442
443            LengthUnit::Pt => {
444                self.length * <N as Normalize>::normalize(params.dpi.x, params.dpi.y)
445                    / POINTS_PER_INCH
446            }
447
448            LengthUnit::Pc => {
449                self.length * <N as Normalize>::normalize(params.dpi.x, params.dpi.y)
450                    / PICA_PER_INCH
451            }
452        }
453    }
454
455    /// Converts a Length to points.  Pixels are taken to be respect with the DPI.
456    ///
457    /// # Panics
458    ///
459    /// Will panic if the length is in Percent, Em, or Ex units.
460    pub fn to_points(&self, params: &NormalizeParams) -> f64 {
461        match self.unit {
462            LengthUnit::Px => {
463                self.length / <N as Normalize>::normalize(params.dpi.x, params.dpi.y) * 72.0
464            }
465
466            LengthUnit::Percent => {
467                panic!("Cannot convert a percentage length into an absolute length");
468            }
469
470            LengthUnit::Em => {
471                panic!("Cannot convert an Em length into an absolute length");
472            }
473
474            LengthUnit::Ex => {
475                panic!("Cannot convert an Ex length into an absolute length");
476            }
477
478            LengthUnit::In => self.length * POINTS_PER_INCH,
479
480            LengthUnit::Cm => self.length / CM_PER_INCH * POINTS_PER_INCH,
481
482            LengthUnit::Mm => self.length / MM_PER_INCH * POINTS_PER_INCH,
483
484            LengthUnit::Pt => self.length,
485
486            LengthUnit::Pc => self.length / PICA_PER_INCH * POINTS_PER_INCH,
487
488            LengthUnit::Ch => {
489                panic!("Cannot convert a Ch length into an absolute length");
490            }
491        }
492    }
493
494    pub fn to_inches(&self, params: &NormalizeParams) -> f64 {
495        self.to_points(params) / POINTS_PER_INCH
496    }
497
498    pub fn to_cm(&self, params: &NormalizeParams) -> f64 {
499        self.to_inches(params) * CM_PER_INCH
500    }
501
502    pub fn to_mm(&self, params: &NormalizeParams) -> f64 {
503        self.to_inches(params) * MM_PER_INCH
504    }
505
506    pub fn to_picas(&self, params: &NormalizeParams) -> f64 {
507        self.to_inches(params) * PICA_PER_INCH
508    }
509}
510
511fn font_size_from_values(values: &NormalizeValues, dpi: Dpi) -> f64 {
512    let v = values.font_size.value();
513
514    match v.unit {
515        LengthUnit::Percent => unreachable!("ComputedValues can't have a relative font size"),
516
517        LengthUnit::Px => v.length,
518
519        // The following implies that our default font size is 12, which
520        // matches the default from the FontSize property.
521        LengthUnit::Em => v.length * 12.0,
522        LengthUnit::Ex => v.length * 12.0 / 2.0,
523        LengthUnit::Ch => v.length * 12.0 / 2.0,
524
525        // FontSize always is a Both, per properties.rs
526        LengthUnit::In => v.length * Both::normalize(dpi.x, dpi.y),
527        LengthUnit::Cm => v.length * Both::normalize(dpi.x, dpi.y) / CM_PER_INCH,
528        LengthUnit::Mm => v.length * Both::normalize(dpi.x, dpi.y) / MM_PER_INCH,
529        LengthUnit::Pt => v.length * Both::normalize(dpi.x, dpi.y) / POINTS_PER_INCH,
530        LengthUnit::Pc => v.length * Both::normalize(dpi.x, dpi.y) / PICA_PER_INCH,
531    }
532}
533
534fn viewport_percentage(x: f64, y: f64) -> f64 {
535    // https://www.w3.org/TR/SVG/coords.html#Units
536    // "For any other length value expressed as a percentage of the viewport, the
537    // percentage is calculated as the specified percentage of
538    // sqrt((actual-width)**2 + (actual-height)**2))/sqrt(2)."
539    (x * x + y * y).sqrt() / SQRT_2
540}
541
542/// Alias for `CssLength` types that can have negative values
543pub type Length<N> = CssLength<N, Signed>;
544
545/// Alias for `CssLength` types that are non negative
546pub type ULength<N> = CssLength<N, Unsigned>;
547
548#[derive(Debug, Default, PartialEq, Copy, Clone)]
549pub enum LengthOrAuto<N: Normalize> {
550    #[default]
551    Auto,
552    Length(CssLength<N, Unsigned>),
553}
554
555impl<N: Normalize> Parse for LengthOrAuto<N> {
556    fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<LengthOrAuto<N>, ParseError<'i>> {
557        if parser
558            .try_parse(|i| i.expect_ident_matching("auto"))
559            .is_ok()
560        {
561            Ok(LengthOrAuto::Auto)
562        } else {
563            Ok(LengthOrAuto::Length(CssLength::parse(parser)?))
564        }
565    }
566}
567
568impl fmt::Display for LengthUnit {
569    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
570        let unit = match &self {
571            LengthUnit::Percent => "%",
572            LengthUnit::Px => "px",
573            LengthUnit::Em => "em",
574            LengthUnit::Ex => "ex",
575            LengthUnit::In => "in",
576            LengthUnit::Cm => "cm",
577            LengthUnit::Mm => "mm",
578            LengthUnit::Pt => "pt",
579            LengthUnit::Pc => "pc",
580            LengthUnit::Ch => "ch",
581        };
582
583        write!(f, "{unit}")
584    }
585}
586
587#[cfg(test)]
588mod tests {
589    use super::*;
590
591    use crate::properties::{ParsedProperty, SpecifiedValue, SpecifiedValues};
592    use crate::{assert_approx_eq_cairo, float_eq_cairo::ApproxEqCairo};
593
594    #[test]
595    fn parses_default() {
596        assert_eq!(
597            Length::<Horizontal>::parse_str("42").unwrap(),
598            Length::<Horizontal>::new(42.0, LengthUnit::Px)
599        );
600
601        assert_eq!(
602            Length::<Horizontal>::parse_str("-42px").unwrap(),
603            Length::<Horizontal>::new(-42.0, LengthUnit::Px)
604        );
605    }
606
607    #[test]
608    fn parses_percent() {
609        assert_eq!(
610            Length::<Horizontal>::parse_str("50.0%").unwrap(),
611            Length::<Horizontal>::new(0.5, LengthUnit::Percent)
612        );
613    }
614
615    #[test]
616    fn parses_font_em() {
617        assert_eq!(
618            Length::<Vertical>::parse_str("22.5em").unwrap(),
619            Length::<Vertical>::new(22.5, LengthUnit::Em)
620        );
621    }
622
623    #[test]
624    fn parses_font_ex() {
625        assert_eq!(
626            Length::<Vertical>::parse_str("22.5ex").unwrap(),
627            Length::<Vertical>::new(22.5, LengthUnit::Ex)
628        );
629    }
630
631    #[test]
632    fn parses_font_ch() {
633        assert_eq!(
634            Length::<Vertical>::parse_str("22.5ch").unwrap(),
635            Length::<Vertical>::new(22.5, LengthUnit::Ch)
636        );
637    }
638
639    #[test]
640    fn parses_physical_units() {
641        assert_eq!(
642            Length::<Both>::parse_str("72pt").unwrap(),
643            Length::<Both>::new(72.0, LengthUnit::Pt)
644        );
645
646        assert_eq!(
647            Length::<Both>::parse_str("-22.5in").unwrap(),
648            Length::<Both>::new(-22.5, LengthUnit::In)
649        );
650
651        assert_eq!(
652            Length::<Both>::parse_str("-254cm").unwrap(),
653            Length::<Both>::new(-254.0, LengthUnit::Cm)
654        );
655
656        assert_eq!(
657            Length::<Both>::parse_str("254mm").unwrap(),
658            Length::<Both>::new(254.0, LengthUnit::Mm)
659        );
660
661        assert_eq!(
662            Length::<Both>::parse_str("60pc").unwrap(),
663            Length::<Both>::new(60.0, LengthUnit::Pc)
664        );
665    }
666
667    #[test]
668    fn parses_unsigned() {
669        assert_eq!(
670            ULength::<Horizontal>::parse_str("42").unwrap(),
671            ULength::<Horizontal>::new(42.0, LengthUnit::Px)
672        );
673
674        assert_eq!(
675            ULength::<Both>::parse_str("0pt").unwrap(),
676            ULength::<Both>::new(0.0, LengthUnit::Pt)
677        );
678
679        assert!(ULength::<Horizontal>::parse_str("-42px").is_err());
680    }
681
682    #[test]
683    fn empty_length_yields_error() {
684        assert!(Length::<Both>::parse_str("").is_err());
685    }
686
687    #[test]
688    fn invalid_unit_yields_error() {
689        assert!(Length::<Both>::parse_str("8furlong").is_err());
690    }
691
692    #[test]
693    fn normalize_default_works() {
694        let viewport = Viewport::new(Dpi::new(40.0, 40.0), 100.0, 100.0);
695        let values = ComputedValues::default();
696        let params = NormalizeParams::new(&values, &viewport);
697
698        assert_approx_eq_cairo!(
699            Length::<Both>::new(10.0, LengthUnit::Px).to_user(&params),
700            10.0
701        );
702    }
703
704    #[test]
705    fn normalize_absolute_units_works() {
706        let viewport = Viewport::new(Dpi::new(40.0, 50.0), 100.0, 100.0);
707        let values = ComputedValues::default();
708        let params = NormalizeParams::new(&values, &viewport);
709
710        assert_approx_eq_cairo!(
711            Length::<Horizontal>::new(10.0, LengthUnit::In).to_user(&params),
712            400.0
713        );
714        assert_approx_eq_cairo!(
715            Length::<Vertical>::new(10.0, LengthUnit::In).to_user(&params),
716            500.0
717        );
718
719        assert_approx_eq_cairo!(
720            Length::<Horizontal>::new(10.0, LengthUnit::Cm).to_user(&params),
721            400.0 / CM_PER_INCH
722        );
723        assert_approx_eq_cairo!(
724            Length::<Horizontal>::new(10.0, LengthUnit::Mm).to_user(&params),
725            400.0 / MM_PER_INCH
726        );
727        assert_approx_eq_cairo!(
728            Length::<Horizontal>::new(10.0, LengthUnit::Pt).to_user(&params),
729            400.0 / POINTS_PER_INCH
730        );
731        assert_approx_eq_cairo!(
732            Length::<Horizontal>::new(10.0, LengthUnit::Pc).to_user(&params),
733            400.0 / PICA_PER_INCH
734        );
735    }
736
737    #[test]
738    fn normalize_percent_works() {
739        let viewport = Viewport::new(Dpi::new(40.0, 40.0), 100.0, 200.0);
740        let values = ComputedValues::default();
741        let params = NormalizeParams::new(&values, &viewport);
742
743        assert_approx_eq_cairo!(
744            Length::<Horizontal>::new(0.05, LengthUnit::Percent).to_user(&params),
745            5.0
746        );
747        assert_approx_eq_cairo!(
748            Length::<Vertical>::new(0.05, LengthUnit::Percent).to_user(&params),
749            10.0
750        );
751    }
752
753    #[test]
754    fn normalize_font_em_ex_ch_works() {
755        let mut values = ComputedValues::default();
756        let viewport = Viewport::new(Dpi::new(40.0, 40.0), 100.0, 200.0);
757        let mut params = NormalizeParams::new(&values, &viewport);
758
759        // These correspond to the default size for the font-size
760        // property and the way we compute Em/Ex from that.
761
762        assert_approx_eq_cairo!(
763            Length::<Vertical>::new(1.0, LengthUnit::Em).to_user(&params),
764            12.0
765        );
766
767        assert_approx_eq_cairo!(
768            Length::<Vertical>::new(1.0, LengthUnit::Ex).to_user(&params),
769            6.0
770        );
771
772        assert_approx_eq_cairo!(
773            Length::<Vertical>::new(1.0, LengthUnit::Ch).to_user(&params),
774            6.0
775        );
776
777        // check for vertical upright text
778        let mut specified = SpecifiedValues::default();
779        specified.set_parsed_property(&ParsedProperty::TextOrientation(SpecifiedValue::Specified(
780            TextOrientation::Upright,
781        )));
782        specified.set_parsed_property(&ParsedProperty::WritingMode(SpecifiedValue::Specified(
783            WritingMode::VerticalLr,
784        )));
785        specified.to_computed_values(&mut values);
786        params = NormalizeParams::new(&values, &viewport);
787        assert_approx_eq_cairo!(
788            Length::<Vertical>::new(1.0, LengthUnit::Ch).to_user(&params),
789            12.0
790        );
791    }
792
793    #[test]
794    fn to_points_works() {
795        let params = NormalizeParams::from_dpi(Dpi::new(40.0, 96.0));
796
797        assert_approx_eq_cairo!(
798            Length::<Horizontal>::new(80.0, LengthUnit::Px).to_points(&params),
799            2.0 * 72.0
800        );
801        assert_approx_eq_cairo!(
802            Length::<Vertical>::new(192.0, LengthUnit::Px).to_points(&params),
803            2.0 * 72.0
804        );
805    }
806}