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)]
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#[derive(Debug, PartialEq, Copy, Clone)]
228pub struct CssLength<N: Normalize, V: Validate> {
229 pub length: f64,
231
232 pub unit: LengthUnit,
234
235 orientation: PhantomData<N>,
237
238 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
311pub 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
335pub struct NormalizeParams {
337 vbox: ViewBox,
338 font_size: f64,
339 dpi: Dpi,
340 is_vertical_text: bool,
341}
342
343impl NormalizeParams {
344 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 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 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 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 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 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 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 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 (x * x + y * y).sqrt() / SQRT_2
532}
533
534pub type Length<N> = CssLength<N, Signed>;
536
537pub 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(¶ms),
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(¶ms),
704 400.0
705 );
706 assert_approx_eq_cairo!(
707 Length::<Vertical>::new(10.0, LengthUnit::In).to_user(¶ms),
708 500.0
709 );
710
711 assert_approx_eq_cairo!(
712 Length::<Horizontal>::new(10.0, LengthUnit::Cm).to_user(¶ms),
713 400.0 / CM_PER_INCH
714 );
715 assert_approx_eq_cairo!(
716 Length::<Horizontal>::new(10.0, LengthUnit::Mm).to_user(¶ms),
717 400.0 / MM_PER_INCH
718 );
719 assert_approx_eq_cairo!(
720 Length::<Horizontal>::new(10.0, LengthUnit::Pt).to_user(¶ms),
721 400.0 / POINTS_PER_INCH
722 );
723 assert_approx_eq_cairo!(
724 Length::<Horizontal>::new(10.0, LengthUnit::Pc).to_user(¶ms),
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(¶ms),
737 5.0
738 );
739 assert_approx_eq_cairo!(
740 Length::<Vertical>::new(0.05, LengthUnit::Percent).to_user(¶ms),
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 assert_approx_eq_cairo!(
755 Length::<Vertical>::new(1.0, LengthUnit::Em).to_user(¶ms),
756 12.0
757 );
758
759 assert_approx_eq_cairo!(
760 Length::<Vertical>::new(1.0, LengthUnit::Ex).to_user(¶ms),
761 6.0
762 );
763
764 assert_approx_eq_cairo!(
765 Length::<Vertical>::new(1.0, LengthUnit::Ch).to_user(¶ms),
766 6.0
767 );
768
769 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(¶ms),
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(¶ms),
791 2.0 * 72.0
792 );
793 assert_approx_eq_cairo!(
794 Length::<Vertical>::new(192.0, LengthUnit::Px).to_points(¶ms),
795 2.0 * 72.0
796 );
797 }
798}