Skip to main content

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#[derive(Debug, PartialEq, Copy, Clone)]
180pub struct Signed;
181
182impl Validate for Signed {}
183
184#[derive(Debug, PartialEq, Copy, Clone)]
185pub struct Unsigned;
186
187impl Validate for Unsigned {
188    fn validate(v: f64) -> Result<f64, ValueErrorKind> {
189        if v >= 0.0 {
190            Ok(v)
191        } else {
192            Err(ValueErrorKind::Value(
193                "value must be non-negative".to_string(),
194            ))
195        }
196    }
197}
198
199/// A CSS length value.
200///
201/// This is equivalent to [CSS lengths].
202///
203/// [CSS lengths]: https://www.w3.org/TR/CSS22/syndata.html#length-units
204///
205/// `CssLength` implements the [`Parse`] trait, so it can be parsed out of a
206/// [`cssparser::Parser`].
207///
208/// This type will be normally used through the type aliases [`Length`] and [`ULength`]
209///
210/// Examples of construction:
211///
212/// ```
213/// # use rsvg::doctest_only::{Length,ULength,LengthUnit,Horizontal,Vertical,Both};
214/// # use rsvg::doctest_only::Parse;
215/// // Explicit type
216/// let width: Length<Horizontal> = Length::new(42.0, LengthUnit::Cm);
217///
218/// // Inferred type
219/// let height = Length::<Vertical>::new(42.0, LengthUnit::Cm);
220///
221/// // Parsed
222/// let radius = ULength::<Both>::parse_str("5px").unwrap();
223/// ```
224///
225/// During the rendering phase, a `CssLength` needs to be converted to user-space
226/// coordinates with the [`CssLength::to_user`] method.
227#[derive(Debug, PartialEq, Copy, Clone)]
228pub struct CssLength<N: Normalize, V: Validate> {
229    /// Numeric part of the length
230    pub length: f64,
231
232    /// Unit part of the length
233    pub unit: LengthUnit,
234
235    /// Dummy; used internally for the type parameter `N`
236    orientation: PhantomData<N>,
237
238    /// Dummy; used internally for the type parameter `V`
239    validation: PhantomData<V>,
240}
241
242impl<N: Normalize, V: Validate> From<CssLength<N, V>> for RsvgLength {
243    fn from(l: CssLength<N, V>) -> RsvgLength {
244        RsvgLength {
245            length: l.length,
246            unit: l.unit,
247        }
248    }
249}
250
251impl<N: Normalize, V: Validate> Default for CssLength<N, V> {
252    fn default() -> Self {
253        CssLength::new(0.0, LengthUnit::Px)
254    }
255}
256
257pub const POINTS_PER_INCH: f64 = 72.0;
258const CM_PER_INCH: f64 = 2.54;
259const MM_PER_INCH: f64 = 25.4;
260const PICA_PER_INCH: f64 = 6.0;
261
262impl<N: Normalize, V: Validate> Parse for CssLength<N, V> {
263    fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<CssLength<N, V>, ParseError<'i>> {
264        let l_value;
265        let l_unit;
266
267        let token = parser.next()?.clone();
268
269        match token {
270            Token::Number { value, .. } => {
271                l_value = value;
272                l_unit = LengthUnit::Px;
273            }
274
275            Token::Percentage { unit_value, .. } => {
276                l_value = unit_value;
277                l_unit = LengthUnit::Percent;
278            }
279
280            Token::Dimension {
281                value, ref unit, ..
282            } => {
283                l_value = value;
284
285                l_unit = match_ignore_ascii_case! {unit.as_ref(),
286                    "px" => LengthUnit::Px,
287                    "em" => LengthUnit::Em,
288                    "ex" => LengthUnit::Ex,
289                    "in" => LengthUnit::In,
290                    "cm" => LengthUnit::Cm,
291                    "mm" => LengthUnit::Mm,
292                    "pt" => LengthUnit::Pt,
293                    "pc" => LengthUnit::Pc,
294                    "ch" => LengthUnit::Ch,
295
296                    _ => return Err(parser.new_unexpected_token_error(token)),
297                };
298            }
299
300            _ => return Err(parser.new_unexpected_token_error(token)),
301        }
302
303        let l_value = f64::from(finite_f32(l_value).map_err(|e| parser.new_custom_error(e))?);
304
305        <V as Validate>::validate(l_value)
306            .map_err(|e| parser.new_custom_error(e))
307            .map(|l_value| CssLength::new(l_value, l_unit))
308    }
309}
310
311/// Parameters for length normalization extracted from [`ComputedValues`].
312///
313/// This is a precursor to [`NormalizeParams::from_values`], for cases where it is inconvenient
314/// to keep a [`ComputedValues`] around.
315pub struct NormalizeValues {
316    font_size: FontSize,
317    is_vertical_text: bool,
318}
319
320impl NormalizeValues {
321    pub fn new(values: &ComputedValues) -> NormalizeValues {
322        let is_vertical_text = matches!(
323            (values.writing_mode(), values.text_orientation()),
324            (WritingMode::VerticalLr, TextOrientation::Upright)
325                | (WritingMode::VerticalRl, TextOrientation::Upright)
326        );
327
328        NormalizeValues {
329            font_size: values.font_size(),
330            is_vertical_text,
331        }
332    }
333}
334
335/// Parameters to normalize [`Length`] values to user-space distances.
336pub struct NormalizeParams {
337    vbox: ViewBox,
338    font_size: f64,
339    dpi: Dpi,
340    is_vertical_text: bool,
341}
342
343impl NormalizeParams {
344    /// Extracts the information needed to normalize [`Length`] values from a set of
345    /// [`ComputedValues`] and the viewport size in [`Viewport`].
346    pub fn new(values: &ComputedValues, viewport: &Viewport) -> NormalizeParams {
347        let v = NormalizeValues::new(values);
348        NormalizeParams::from_values(&v, viewport)
349    }
350
351    pub fn from_values(v: &NormalizeValues, viewport: &Viewport) -> NormalizeParams {
352        NormalizeParams {
353            vbox: viewport.vbox,
354            font_size: font_size_from_values(v, viewport.dpi),
355            dpi: viewport.dpi,
356            is_vertical_text: v.is_vertical_text,
357        }
358    }
359
360    /// Just used by rsvg-convert, where there is no font size nor viewport.
361    pub fn from_dpi(dpi: Dpi) -> NormalizeParams {
362        NormalizeParams {
363            vbox: ViewBox::from(Rect::default()),
364            font_size: 1.0,
365            dpi,
366            is_vertical_text: false,
367        }
368    }
369}
370
371impl<N: Normalize, V: Validate> CssLength<N, V> {
372    /// Creates a CssLength.
373    ///
374    /// The compiler needs to know the type parameters `N` and `V` which represents the
375    /// length's orientation and validation.
376    /// You can specify them explicitly, or call the parametrized method:
377    ///
378    /// ```
379    /// # use rsvg::doctest_only::{Length,LengthUnit,Horizontal,Vertical};
380    /// // Explicit type
381    /// let width: Length<Horizontal> = Length::new(42.0, LengthUnit::Cm);
382    ///
383    /// // Inferred type
384    /// let height = Length::<Vertical>::new(42.0, LengthUnit::Cm);
385    /// ```
386    pub fn new(l: f64, unit: LengthUnit) -> CssLength<N, V> {
387        CssLength {
388            length: l,
389            unit,
390            orientation: PhantomData,
391            validation: PhantomData,
392        }
393    }
394
395    /// Convert a Length with units into user-space coordinates.
396    ///
397    /// Lengths may come with non-pixel units, and when rendering, they need to be normalized
398    /// to pixels based on the current viewport (e.g. for lengths with percent units), and
399    /// based on the current element's set of [`ComputedValues`] (e.g. for lengths with `Em`
400    /// units that need to be resolved against the current font size).
401    ///
402    /// Those parameters can be obtained with [`NormalizeParams::new()`].
403    pub fn to_user(&self, params: &NormalizeParams) -> f64 {
404        match self.unit {
405            LengthUnit::Px => self.length,
406
407            LengthUnit::Percent => {
408                self.length * <N as Normalize>::normalize(params.vbox.width(), params.vbox.height())
409            }
410
411            LengthUnit::Em => self.length * params.font_size,
412
413            LengthUnit::Ex => self.length * params.font_size / 2.0,
414
415            // how far "0" advances the text, so it varies depending on orientation
416            // we're using the 0.5em or 1.0em (based on orientation) fallback from the spec
417            LengthUnit::Ch => {
418                if params.is_vertical_text {
419                    self.length * params.font_size
420                } else {
421                    self.length * params.font_size / 2.0
422                }
423            }
424
425            LengthUnit::In => self.length * <N as Normalize>::normalize(params.dpi.x, params.dpi.y),
426
427            LengthUnit::Cm => {
428                self.length * <N as Normalize>::normalize(params.dpi.x, params.dpi.y) / CM_PER_INCH
429            }
430
431            LengthUnit::Mm => {
432                self.length * <N as Normalize>::normalize(params.dpi.x, params.dpi.y) / MM_PER_INCH
433            }
434
435            LengthUnit::Pt => {
436                self.length * <N as Normalize>::normalize(params.dpi.x, params.dpi.y)
437                    / POINTS_PER_INCH
438            }
439
440            LengthUnit::Pc => {
441                self.length * <N as Normalize>::normalize(params.dpi.x, params.dpi.y)
442                    / PICA_PER_INCH
443            }
444        }
445    }
446
447    /// Converts a Length to points.  Pixels are taken to be respect with the DPI.
448    ///
449    /// # Panics
450    ///
451    /// Will panic if the length is in Percent, Em, or Ex units.
452    pub fn to_points(&self, params: &NormalizeParams) -> f64 {
453        match self.unit {
454            LengthUnit::Px => {
455                self.length / <N as Normalize>::normalize(params.dpi.x, params.dpi.y) * 72.0
456            }
457
458            LengthUnit::Percent => {
459                panic!("Cannot convert a percentage length into an absolute length");
460            }
461
462            LengthUnit::Em => {
463                panic!("Cannot convert an Em length into an absolute length");
464            }
465
466            LengthUnit::Ex => {
467                panic!("Cannot convert an Ex length into an absolute length");
468            }
469
470            LengthUnit::In => self.length * POINTS_PER_INCH,
471
472            LengthUnit::Cm => self.length / CM_PER_INCH * POINTS_PER_INCH,
473
474            LengthUnit::Mm => self.length / MM_PER_INCH * POINTS_PER_INCH,
475
476            LengthUnit::Pt => self.length,
477
478            LengthUnit::Pc => self.length / PICA_PER_INCH * POINTS_PER_INCH,
479
480            LengthUnit::Ch => {
481                panic!("Cannot convert a Ch length into an absolute length");
482            }
483        }
484    }
485
486    pub fn to_inches(&self, params: &NormalizeParams) -> f64 {
487        self.to_points(params) / POINTS_PER_INCH
488    }
489
490    pub fn to_cm(&self, params: &NormalizeParams) -> f64 {
491        self.to_inches(params) * CM_PER_INCH
492    }
493
494    pub fn to_mm(&self, params: &NormalizeParams) -> f64 {
495        self.to_inches(params) * MM_PER_INCH
496    }
497
498    pub fn to_picas(&self, params: &NormalizeParams) -> f64 {
499        self.to_inches(params) * PICA_PER_INCH
500    }
501}
502
503fn font_size_from_values(values: &NormalizeValues, dpi: Dpi) -> f64 {
504    let v = values.font_size.value();
505
506    match v.unit {
507        LengthUnit::Percent => unreachable!("ComputedValues can't have a relative font size"),
508
509        LengthUnit::Px => v.length,
510
511        // The following implies that our default font size is 12, which
512        // matches the default from the FontSize property.
513        LengthUnit::Em => v.length * 12.0,
514        LengthUnit::Ex => v.length * 12.0 / 2.0,
515        LengthUnit::Ch => v.length * 12.0 / 2.0,
516
517        // FontSize always is a Both, per properties.rs
518        LengthUnit::In => v.length * Both::normalize(dpi.x, dpi.y),
519        LengthUnit::Cm => v.length * Both::normalize(dpi.x, dpi.y) / CM_PER_INCH,
520        LengthUnit::Mm => v.length * Both::normalize(dpi.x, dpi.y) / MM_PER_INCH,
521        LengthUnit::Pt => v.length * Both::normalize(dpi.x, dpi.y) / POINTS_PER_INCH,
522        LengthUnit::Pc => v.length * Both::normalize(dpi.x, dpi.y) / PICA_PER_INCH,
523    }
524}
525
526fn viewport_percentage(x: f64, y: f64) -> f64 {
527    // https://www.w3.org/TR/SVG/coords.html#Units
528    // "For any other length value expressed as a percentage of the viewport, the
529    // percentage is calculated as the specified percentage of
530    // sqrt((actual-width)**2 + (actual-height)**2))/sqrt(2)."
531    (x * x + y * y).sqrt() / SQRT_2
532}
533
534/// Alias for `CssLength` types that can have negative values
535pub type Length<N> = CssLength<N, Signed>;
536
537/// Alias for `CssLength` types that are non negative
538pub type ULength<N> = CssLength<N, Unsigned>;
539
540#[derive(Debug, Default, PartialEq, Copy, Clone)]
541pub enum LengthOrAuto<N: Normalize> {
542    #[default]
543    Auto,
544    Length(CssLength<N, Unsigned>),
545}
546
547impl<N: Normalize> Parse for LengthOrAuto<N> {
548    fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<LengthOrAuto<N>, ParseError<'i>> {
549        if parser
550            .try_parse(|i| i.expect_ident_matching("auto"))
551            .is_ok()
552        {
553            Ok(LengthOrAuto::Auto)
554        } else {
555            Ok(LengthOrAuto::Length(CssLength::parse(parser)?))
556        }
557    }
558}
559
560impl fmt::Display for LengthUnit {
561    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
562        let unit = match &self {
563            LengthUnit::Percent => "%",
564            LengthUnit::Px => "px",
565            LengthUnit::Em => "em",
566            LengthUnit::Ex => "ex",
567            LengthUnit::In => "in",
568            LengthUnit::Cm => "cm",
569            LengthUnit::Mm => "mm",
570            LengthUnit::Pt => "pt",
571            LengthUnit::Pc => "pc",
572            LengthUnit::Ch => "ch",
573        };
574
575        write!(f, "{unit}")
576    }
577}
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582
583    use crate::properties::{ParsedProperty, SpecifiedValue, SpecifiedValues};
584    use crate::{assert_approx_eq_cairo, float_eq_cairo::ApproxEqCairo};
585
586    #[test]
587    fn parses_default() {
588        assert_eq!(
589            Length::<Horizontal>::parse_str("42").unwrap(),
590            Length::<Horizontal>::new(42.0, LengthUnit::Px)
591        );
592
593        assert_eq!(
594            Length::<Horizontal>::parse_str("-42px").unwrap(),
595            Length::<Horizontal>::new(-42.0, LengthUnit::Px)
596        );
597    }
598
599    #[test]
600    fn parses_percent() {
601        assert_eq!(
602            Length::<Horizontal>::parse_str("50.0%").unwrap(),
603            Length::<Horizontal>::new(0.5, LengthUnit::Percent)
604        );
605    }
606
607    #[test]
608    fn parses_font_em() {
609        assert_eq!(
610            Length::<Vertical>::parse_str("22.5em").unwrap(),
611            Length::<Vertical>::new(22.5, LengthUnit::Em)
612        );
613    }
614
615    #[test]
616    fn parses_font_ex() {
617        assert_eq!(
618            Length::<Vertical>::parse_str("22.5ex").unwrap(),
619            Length::<Vertical>::new(22.5, LengthUnit::Ex)
620        );
621    }
622
623    #[test]
624    fn parses_font_ch() {
625        assert_eq!(
626            Length::<Vertical>::parse_str("22.5ch").unwrap(),
627            Length::<Vertical>::new(22.5, LengthUnit::Ch)
628        );
629    }
630
631    #[test]
632    fn parses_physical_units() {
633        assert_eq!(
634            Length::<Both>::parse_str("72pt").unwrap(),
635            Length::<Both>::new(72.0, LengthUnit::Pt)
636        );
637
638        assert_eq!(
639            Length::<Both>::parse_str("-22.5in").unwrap(),
640            Length::<Both>::new(-22.5, LengthUnit::In)
641        );
642
643        assert_eq!(
644            Length::<Both>::parse_str("-254cm").unwrap(),
645            Length::<Both>::new(-254.0, LengthUnit::Cm)
646        );
647
648        assert_eq!(
649            Length::<Both>::parse_str("254mm").unwrap(),
650            Length::<Both>::new(254.0, LengthUnit::Mm)
651        );
652
653        assert_eq!(
654            Length::<Both>::parse_str("60pc").unwrap(),
655            Length::<Both>::new(60.0, LengthUnit::Pc)
656        );
657    }
658
659    #[test]
660    fn parses_unsigned() {
661        assert_eq!(
662            ULength::<Horizontal>::parse_str("42").unwrap(),
663            ULength::<Horizontal>::new(42.0, LengthUnit::Px)
664        );
665
666        assert_eq!(
667            ULength::<Both>::parse_str("0pt").unwrap(),
668            ULength::<Both>::new(0.0, LengthUnit::Pt)
669        );
670
671        assert!(ULength::<Horizontal>::parse_str("-42px").is_err());
672    }
673
674    #[test]
675    fn empty_length_yields_error() {
676        assert!(Length::<Both>::parse_str("").is_err());
677    }
678
679    #[test]
680    fn invalid_unit_yields_error() {
681        assert!(Length::<Both>::parse_str("8furlong").is_err());
682    }
683
684    #[test]
685    fn normalize_default_works() {
686        let viewport = Viewport::new(Dpi::new(40.0, 40.0), 100.0, 100.0);
687        let values = ComputedValues::default();
688        let params = NormalizeParams::new(&values, &viewport);
689
690        assert_approx_eq_cairo!(
691            Length::<Both>::new(10.0, LengthUnit::Px).to_user(&params),
692            10.0
693        );
694    }
695
696    #[test]
697    fn normalize_absolute_units_works() {
698        let viewport = Viewport::new(Dpi::new(40.0, 50.0), 100.0, 100.0);
699        let values = ComputedValues::default();
700        let params = NormalizeParams::new(&values, &viewport);
701
702        assert_approx_eq_cairo!(
703            Length::<Horizontal>::new(10.0, LengthUnit::In).to_user(&params),
704            400.0
705        );
706        assert_approx_eq_cairo!(
707            Length::<Vertical>::new(10.0, LengthUnit::In).to_user(&params),
708            500.0
709        );
710
711        assert_approx_eq_cairo!(
712            Length::<Horizontal>::new(10.0, LengthUnit::Cm).to_user(&params),
713            400.0 / CM_PER_INCH
714        );
715        assert_approx_eq_cairo!(
716            Length::<Horizontal>::new(10.0, LengthUnit::Mm).to_user(&params),
717            400.0 / MM_PER_INCH
718        );
719        assert_approx_eq_cairo!(
720            Length::<Horizontal>::new(10.0, LengthUnit::Pt).to_user(&params),
721            400.0 / POINTS_PER_INCH
722        );
723        assert_approx_eq_cairo!(
724            Length::<Horizontal>::new(10.0, LengthUnit::Pc).to_user(&params),
725            400.0 / PICA_PER_INCH
726        );
727    }
728
729    #[test]
730    fn normalize_percent_works() {
731        let viewport = Viewport::new(Dpi::new(40.0, 40.0), 100.0, 200.0);
732        let values = ComputedValues::default();
733        let params = NormalizeParams::new(&values, &viewport);
734
735        assert_approx_eq_cairo!(
736            Length::<Horizontal>::new(0.05, LengthUnit::Percent).to_user(&params),
737            5.0
738        );
739        assert_approx_eq_cairo!(
740            Length::<Vertical>::new(0.05, LengthUnit::Percent).to_user(&params),
741            10.0
742        );
743    }
744
745    #[test]
746    fn normalize_font_em_ex_ch_works() {
747        let mut values = ComputedValues::default();
748        let viewport = Viewport::new(Dpi::new(40.0, 40.0), 100.0, 200.0);
749        let mut params = NormalizeParams::new(&values, &viewport);
750
751        // These correspond to the default size for the font-size
752        // property and the way we compute Em/Ex from that.
753
754        assert_approx_eq_cairo!(
755            Length::<Vertical>::new(1.0, LengthUnit::Em).to_user(&params),
756            12.0
757        );
758
759        assert_approx_eq_cairo!(
760            Length::<Vertical>::new(1.0, LengthUnit::Ex).to_user(&params),
761            6.0
762        );
763
764        assert_approx_eq_cairo!(
765            Length::<Vertical>::new(1.0, LengthUnit::Ch).to_user(&params),
766            6.0
767        );
768
769        // check for vertical upright text
770        let mut specified = SpecifiedValues::default();
771        specified.set_parsed_property(&ParsedProperty::TextOrientation(SpecifiedValue::Specified(
772            TextOrientation::Upright,
773        )));
774        specified.set_parsed_property(&ParsedProperty::WritingMode(SpecifiedValue::Specified(
775            WritingMode::VerticalLr,
776        )));
777        specified.to_computed_values(&mut values);
778        params = NormalizeParams::new(&values, &viewport);
779        assert_approx_eq_cairo!(
780            Length::<Vertical>::new(1.0, LengthUnit::Ch).to_user(&params),
781            12.0
782        );
783    }
784
785    #[test]
786    fn to_points_works() {
787        let params = NormalizeParams::from_dpi(Dpi::new(40.0, 96.0));
788
789        assert_approx_eq_cairo!(
790            Length::<Horizontal>::new(80.0, LengthUnit::Px).to_points(&params),
791            2.0 * 72.0
792        );
793        assert_approx_eq_cairo!(
794            Length::<Vertical>::new(192.0, LengthUnit::Px).to_points(&params),
795            2.0 * 72.0
796        );
797    }
798}