Skip to main content

latexml_core/common/
dimension.rs

1use std::fmt;
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5
6use crate::{
7  Digested, RegisterValue,
8  common::{
9    error::*,
10    numeric_ops::{NumericOps, UNITY, UNITY_F64, fixpoint_unit, kround, round_to},
11    object::Object,
12  },
13  definition::register::RegisterType,
14  state::*,
15  tokens::Tokens,
16};
17
18static SPEC_RE: Lazy<Regex> =
19  Lazy::new(|| Regex::new(r"^(-?\d*\.?\d*)([a-zA-Z][a-zA-Z])$").unwrap());
20
21#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
22pub struct Dimension(pub i64);
23
24impl Object for Dimension {
25  fn revert(&self) -> Result<Tokens> { Ok(Tokens::new(ExplodeText!(&self.to_string()))) }
26  fn be_digested(self) -> Result<Digested>
27  where
28    Self: Sized,
29    Self: fmt::Debug,
30  {
31    Ok(Digested::from(RegisterValue::Dimension(self)))
32  }
33}
34impl NumericOps for Dimension {
35  fn new(number: i64) -> Self { Dimension(number) }
36  fn new_f64(number: f64) -> Self { Dimension(kround(number)) }
37  fn value_of(self) -> i64 { self.0 }
38  fn register_type(&self) -> RegisterType { RegisterType::Dimension }
39  fn unit(&self) -> Option<&'static str> { Some("pt") }
40  fn to_attribute(&self) -> String { attribute_format(self.value_of(), self.unit()) }
41}
42
43impl fmt::Display for Dimension {
44  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
45    write!(f, "{}", fixedformat(self.0, self.unit()))
46  }
47}
48
49impl Dimension {
50  /// Convert dimension to em units, using the given font's EM width (or current font if None).
51  pub fn em_value(&self, prec: Option<u8>, font: Option<&crate::common::font::Font>) -> f64 {
52    let em_width: f64 = if let Some(f) = font {
53      f.get_em_width() as f64
54    } else {
55      lookup_font()
56        .map(|f| f.get_em_width() as f64)
57        .unwrap_or(UNITY_F64 * 10.0) // fallback: 10pt
58    };
59    round_to(self.0 as f64 / em_width, prec)
60  }
61
62  pub fn spec_to_f64(spec: &str) -> Result<f64> {
63    if spec.is_empty() {
64      Ok(0.0)
65    } else if let Some(cap) = SPEC_RE.captures(spec) {
66      // Dimensions given. SPEC_RE's numeric part is `-?\d*\.?\d*` which
67      // can match empty (e.g. input "pt"); Perl's `fixpoint($1, ...)`
68      // coerces "" → 0 via numeric context, so unwrap_or(0.0) keeps parity.
69      let num_str = cap.get(1).map_or("", |m| m.as_str());
70      let num: f64 = num_str.parse::<f64>().unwrap_or(0.0);
71      let unit = cap.get(2).map_or("", |m| m.as_str());
72      let (conv_num, conv_den) = convert_unit_ratio(unit);
73      Ok(fixpoint_unit(num, conv_num, conv_den) as f64)
74    } else {
75      // When scaled points passed in (typically the result of Perl calculations on other
76      // Dimensions), you might think truncation (int) is more TeX-like.
77      // Recall that TeX arithmatic truncates, whereas reading by Gullet tends to round.
78      // The Perl arithmetic is replacing an unknown combination of those truncates & rounds.
79      // As it turns out, using int() here results in non-terminating loops in pgf/tikz.
80      // So, we use round (Knuth style)
81      // Note that divide and such explicitly use int(), however!
82      // Perl parity: `kround($spec || 0)` — non-numeric input coerces to 0.
83      Ok(kround(spec.parse::<f64>().unwrap_or(0.0)) as f64)
84    }
85  }
86}
87
88impl std::str::FromStr for Dimension {
89  type Err = Error;
90  fn from_str(spec: &str) -> Result<Dimension> {
91    Ok(Dimension::new_f64(Dimension::spec_to_f64(spec)?))
92  }
93}
94
95// Dimension!() macro is in setup.rs, since it binds state
96
97// This is Knuth's print_scaled (See TeX the Program, \S 103)
98// It (should) round-trip with kround.
99pub fn fixedformat(mut s: i64, unit_opt: Option<&str>) -> String {
100  use std::fmt::Write as _;
101  // Format directly into `string`; `write!` into a String is infallible.
102  // Avoids the per-iteration `i64::to_string()` temporaries the Perl-faithful
103  // algorithm would otherwise leak on a very hot path (≈1M calls/doc).
104  let mut string = String::new();
105  if s < 0 {
106    string.push('-');
107    s = -s;
108  }
109  write!(string, "{}", s / UNITY).unwrap();
110  string.push('.');
111  s = 10 * (s % UNITY) + 5;
112  let mut delta = 10;
113  loop {
114    if delta > UNITY {
115      s += 0x8000 - 50000;
116    }
117    write!(string, "{}", s / UNITY).unwrap();
118    s = 10 * (s % UNITY);
119    delta *= 10;
120    if s <= delta {
121      break;
122    }
123  }
124  if let Some(unit) = unit_opt {
125    string.push_str(unit);
126  }
127  string
128}
129
130pub fn attribute_format(sp: i64, unit_opt: Option<&str>) -> String {
131  let unit = unit_opt.unwrap_or("pt");
132  s!("{:.1}{unit}", round_to(sp as f64 / UNITY_F64, Some(1)))
133}
134
135#[cfg(test)]
136mod tests {
137  use super::*;
138
139  #[test]
140  fn fixedformat_zero() {
141    // Knuth's print_scaled of 0 with a unit.
142    assert_eq!(fixedformat(0, Some("pt")), "0.0pt");
143    assert_eq!(fixedformat(0, None), "0.0");
144  }
145
146  #[test]
147  fn fixedformat_one_sp_unit() {
148    // UNITY = 65536 scaled-points = 1pt.
149    assert_eq!(fixedformat(UNITY, Some("pt")), "1.0pt");
150    assert_eq!(fixedformat(2 * UNITY, Some("pt")), "2.0pt");
151  }
152
153  #[test]
154  fn fixedformat_negative() {
155    assert_eq!(fixedformat(-UNITY, Some("pt")), "-1.0pt");
156    assert_eq!(fixedformat(-2 * UNITY, Some("pt")), "-2.0pt");
157  }
158
159  #[test]
160  fn fixedformat_half_pt() {
161    // 0.5 pt = UNITY/2 = 32768 sp.
162    let out = fixedformat(UNITY / 2, Some("pt"));
163    assert_eq!(out, "0.5pt");
164  }
165
166  #[test]
167  fn attribute_format_defaults_to_pt() {
168    assert_eq!(attribute_format(UNITY, None), "1.0pt");
169    assert_eq!(attribute_format(UNITY, Some("pt")), "1.0pt");
170  }
171
172  #[test]
173  fn attribute_format_other_unit() {
174    assert_eq!(attribute_format(UNITY, Some("in")), "1.0in");
175  }
176
177  #[test]
178  fn attribute_format_rounds_to_one_decimal() {
179    // sp = UNITY * 1.25 → 1.3pt (round to 1 decimal, half-up).
180    let sp = (UNITY_F64 * 1.25) as i64;
181    let out = attribute_format(sp, None);
182    assert_eq!(out, "1.3pt", "got {out:?}");
183  }
184
185  #[test]
186  fn spec_to_f64_empty_is_zero() {
187    assert_eq!(Dimension::spec_to_f64("").unwrap(), 0.0);
188  }
189
190  #[test]
191  fn spec_to_f64_bare_number_is_scaled() {
192    // No unit → treated as scaled-points directly; the value is
193    // kround()-ed through, so "65536" stays 65536.
194    let out = Dimension::spec_to_f64("65536").unwrap();
195    assert_eq!(out, 65536.0);
196  }
197}