1use 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#[non_exhaustive]
61#[repr(C)]
62#[derive(Debug, PartialEq, Copy, Clone)]
63pub enum LengthUnit {
64 Percent,
66
67 Px,
69
70 Em,
72
73 Ex,
75
76 In,
78
79 Cm,
81
82 Mm,
84
85 Pt,
87
88 Pc,
90
91 Ch,
93}
94
95#[repr(C)]
108#[derive(Debug, PartialEq, Copy, Clone)]
109pub struct RsvgLength {
110 pub length: f64,
112
113 pub unit: LengthUnit,
115}
116
117impl RsvgLength {
118 pub fn new(l: f64, unit: LengthUnit) -> RsvgLength {
120 RsvgLength { length: l, unit }
121 }
122}
123
124pub trait Normalize {
126 fn normalize(x: f64, y: f64) -> f64;
134}
135
136#[derive(Debug, PartialEq, Copy, Clone)]
138pub struct Horizontal;
139
140#[derive(Debug, PartialEq, Copy, Clone)]
142pub struct Vertical;
143
144#[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
169pub trait Validate {
171 fn validate(v: f64) -> Result<f64, ValueErrorKind> {
175 Ok(v)
176 }
177}
178
179#[derive(Debug, PartialEq, Copy, Clone)]
184pub struct Signed;
185
186impl Validate for Signed {}
187
188#[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#[derive(Debug, PartialEq, Copy, Clone)]
236pub struct CssLength<N: Normalize, V: Validate> {
237 pub length: f64,
239
240 pub unit: LengthUnit,
242
243 orientation: PhantomData<N>,
245
246 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
319pub 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
343pub struct NormalizeParams {
345 vbox: ViewBox,
346 font_size: f64,
347 dpi: Dpi,
348 is_vertical_text: bool,
349}
350
351impl NormalizeParams {
352 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 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 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 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 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 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 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 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 (x * x + y * y).sqrt() / SQRT_2
540}
541
542pub type Length<N> = CssLength<N, Signed>;
544
545pub 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(¶ms),
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(¶ms),
712 400.0
713 );
714 assert_approx_eq_cairo!(
715 Length::<Vertical>::new(10.0, LengthUnit::In).to_user(¶ms),
716 500.0
717 );
718
719 assert_approx_eq_cairo!(
720 Length::<Horizontal>::new(10.0, LengthUnit::Cm).to_user(¶ms),
721 400.0 / CM_PER_INCH
722 );
723 assert_approx_eq_cairo!(
724 Length::<Horizontal>::new(10.0, LengthUnit::Mm).to_user(¶ms),
725 400.0 / MM_PER_INCH
726 );
727 assert_approx_eq_cairo!(
728 Length::<Horizontal>::new(10.0, LengthUnit::Pt).to_user(¶ms),
729 400.0 / POINTS_PER_INCH
730 );
731 assert_approx_eq_cairo!(
732 Length::<Horizontal>::new(10.0, LengthUnit::Pc).to_user(¶ms),
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(¶ms),
745 5.0
746 );
747 assert_approx_eq_cairo!(
748 Length::<Vertical>::new(0.05, LengthUnit::Percent).to_user(¶ms),
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 assert_approx_eq_cairo!(
763 Length::<Vertical>::new(1.0, LengthUnit::Em).to_user(¶ms),
764 12.0
765 );
766
767 assert_approx_eq_cairo!(
768 Length::<Vertical>::new(1.0, LengthUnit::Ex).to_user(¶ms),
769 6.0
770 );
771
772 assert_approx_eq_cairo!(
773 Length::<Vertical>::new(1.0, LengthUnit::Ch).to_user(¶ms),
774 6.0
775 );
776
777 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(¶ms),
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(¶ms),
799 2.0 * 72.0
800 );
801 assert_approx_eq_cairo!(
802 Length::<Vertical>::new(192.0, LengthUnit::Px).to_points(¶ms),
803 2.0 * 72.0
804 );
805 }
806}