Skip to main content

latex_rust/
dim.rs

1//! Layout dimension wrapping [zenith-float](https://crates.io/crates/zenith-float) 1.0 `ExactNum`.
2//!
3//! This crate depends on the published `zenith-float` crate, not on its internal
4//! kernel package. Every arithmetic path uses software limbs. Hardware `f32` /
5//! `f64` never appear as calculation terminals.
6
7use core::cmp::Ordering;
8use core::fmt;
9use core::ops::{Add, Div, Mul, Neg, Sub};
10use std::sync::Arc;
11
12use crate::error::Error;
13use zenith_float::{Consts, ExactNum, Radix, RoundingMode};
14
15/// Working precision for layout `Dim` values, in bits.
16pub const DIM_PREC: usize = 256;
17const RM: RoundingMode = RoundingMode::ToEven;
18
19fn consts() -> Consts {
20    Consts::new().expect("zenith-float constants cache")
21}
22
23fn wrap(n: ExactNum) -> Dim {
24    Dim { inner: Arc::new(n) }
25}
26
27/// TeX-style dimension: width, height, depth, italic correction, mu.
28///
29/// Values are zenith-float software floats. One unit is one em at the current
30/// math style unless a method says otherwise.
31///
32/// # Examples
33///
34/// ```
35/// use latex_rust::Dim;
36///
37/// let half = Dim::ratio(1, 2);
38/// assert!(half.eq_dim(&(&Dim::one() / &Dim::from_i64(2))));
39/// assert!(!Dim::zero().eq_dim(&Dim::one()));
40/// ```
41#[derive(Clone, Debug)]
42pub struct Dim {
43    inner: Arc<ExactNum>,
44}
45
46impl Dim {
47    /// Zero em.
48    #[must_use]
49    pub fn zero() -> Self {
50        wrap(ExactNum::new(DIM_PREC))
51    }
52
53    /// One em.
54    #[must_use]
55    pub fn one() -> Self {
56        wrap(ExactNum::from_i32(1, DIM_PREC))
57    }
58
59    /// Integer em count.
60    #[must_use]
61    pub fn from_i64(v: i64) -> Self {
62        wrap(ExactNum::from_i64(v, DIM_PREC))
63    }
64
65    /// Exact rational `num / den` em. `den == 0` yields NaN.
66    #[must_use]
67    pub fn ratio(num: i64, den: i64) -> Self {
68        Self::from_i64(num) / Self::from_i64(den)
69    }
70
71    /// Parse a decimal string (including scientific form) with zenith-float.
72    #[must_use]
73    pub fn parse(s: &str) -> Self {
74        let mut cc = consts();
75        wrap(ExactNum::parse(s, Radix::Dec, DIM_PREC, RM, &mut cc))
76    }
77
78    /// Convert integer font units to em: `units / units_per_em`.
79    #[must_use]
80    pub fn from_font_units(units: i64, units_per_em: u16) -> Self {
81        Self::from_i64(units) / Self::from_i64(i64::from(units_per_em))
82    }
83
84    /// One math unit (mu). TeX: `18 mu = 1 em`.
85    #[must_use]
86    pub fn mu() -> Self {
87        Self::ratio(1, 18)
88    }
89
90    /// Convert this em value to mu (`* 18`).
91    #[must_use]
92    pub fn to_mu(&self) -> Self {
93        self.clone() * Self::from_i64(18)
94    }
95
96    /// Convert a mu value to em (`/ 18`).
97    #[must_use]
98    pub fn from_mu(mu: &Self) -> Self {
99        mu.clone() / Self::from_i64(18)
100    }
101
102    /// Absolute value.
103    #[must_use]
104    pub fn abs(&self) -> Self {
105        wrap(self.inner.as_ref().abs())
106    }
107
108    /// Maximum of two dimensions.
109    #[must_use]
110    pub fn max(&self, other: &Self) -> Self {
111        match self.cmp(other) {
112            Some(Ordering::Less) => other.clone(),
113            _ => self.clone(),
114        }
115    }
116
117    /// Minimum of two dimensions.
118    #[must_use]
119    pub fn min(&self, other: &Self) -> Self {
120        match self.cmp(other) {
121            Some(Ordering::Greater) => other.clone(),
122            _ => self.clone(),
123        }
124    }
125
126    /// `max(self, 0)`.
127    #[must_use]
128    pub fn clamp_nonneg(&self) -> Self {
129        self.max(&Self::zero())
130    }
131
132    /// True when the value is NaN.
133    #[must_use]
134    pub fn is_nan(&self) -> bool {
135        self.inner.is_nan()
136    }
137
138    /// True when the value compares equal to zero.
139    #[must_use]
140    pub fn is_zero(&self) -> bool {
141        matches!(self.inner.cmp(&ExactNum::new(DIM_PREC)), Some(0))
142    }
143
144    /// Decimal string from zenith-float (gold-stable for a given precision).
145    #[must_use]
146    pub fn to_dec_string(&self) -> String {
147        let mut cc = consts();
148        self.inner
149            .format(Radix::Dec, RM, &mut cc)
150            .unwrap_or_else(|_| "NaN".into())
151    }
152
153    /// Compact decimal for SVG attributes (same zenith-float decimal as layout golds).
154    #[must_use]
155    pub fn to_svg_string(&self) -> String {
156        self.to_dec_string()
157    }
158
159    /// Layout dimension from an IEEE-754 binary32 bit pattern (ttf-parser outline boundary).
160    #[must_use]
161    pub fn from_ieee32_bits(bits: u32) -> Self {
162        wrap(zenith_float::Ieee32::from_bits(bits).to_exact(DIM_PREC))
163    }
164
165    /// Round to IEEE-754 binary32 bits for PNG / raster emission only.
166    ///
167    /// Layout arithmetic stays in [`Dim`]. This is the pixel-coordinate terminal
168    /// permitted for SVG/PNG backends.
169    #[must_use]
170    pub fn to_ieee32_bits(&self) -> u32 {
171        zenith_float::Ieee32::from_exact(self.inner.as_ref()).to_bits()
172    }
173
174    /// Largest `u32` that is not greater than `self`. Negative and NaN fail.
175    pub fn floor_to_u32(&self) -> Result<u32, Error> {
176        if self.is_nan() {
177            return Err(Error::InvalidOption {
178                what: "dimension is NaN".into(),
179            });
180        }
181        if matches!(self.cmp(&Self::zero()), Some(Ordering::Less)) {
182            return Err(Error::InvalidOption {
183                what: "negative dimension".into(),
184            });
185        }
186        const MAX: u32 = 1 << 20;
187        if matches!(
188            self.cmp(&Self::from_i64(i64::from(MAX))),
189            Some(Ordering::Greater) | Some(Ordering::Equal)
190        ) {
191            return Err(Error::InvalidOption {
192                what: "dimension exceeds raster limit".into(),
193            });
194        }
195        let mut ans = 0u32;
196        let mut bit = 1u32 << 19;
197        while bit > 0 {
198            let cand = ans + bit;
199            if Self::from_i64(i64::from(cand))
200                .cmp(self)
201                .is_some_and(|o| o != Ordering::Greater)
202            {
203                ans = cand;
204            }
205            bit /= 2;
206        }
207        Ok(ans)
208    }
209
210    /// Smallest `u32` that is not less than `self`.
211    pub fn ceil_to_u32(&self) -> Result<u32, Error> {
212        let floor = self.floor_to_u32()?;
213        if self.eq_dim(&Self::from_i64(i64::from(floor))) {
214            Ok(floor)
215        } else {
216            floor.checked_add(1).ok_or_else(|| Error::InvalidOption {
217                what: "dimension overflow".into(),
218            })
219        }
220    }
221
222    /// Compare two dimensions. `None` if either is NaN.
223    ///
224    /// Named `cmp` after zenith-float's partial compare. [`Ord`] is not
225    /// implemented because NaN has no total order.
226    #[must_use]
227    #[allow(clippy::should_implement_trait)]
228    pub fn cmp(&self, other: &Self) -> Option<Ordering> {
229        match self.inner.cmp(&other.inner) {
230            Some(0) => Some(Ordering::Equal),
231            Some(x) if x < 0 => Some(Ordering::Less),
232            Some(_) => Some(Ordering::Greater),
233            None => None,
234        }
235    }
236
237    /// True when `self` and `other` compare equal.
238    #[must_use]
239    pub fn eq_dim(&self, other: &Self) -> bool {
240        matches!(self.cmp(other), Some(Ordering::Equal))
241    }
242}
243
244impl PartialEq for Dim {
245    fn eq(&self, other: &Self) -> bool {
246        self.eq_dim(other)
247    }
248}
249
250impl Eq for Dim {}
251
252impl PartialOrd for Dim {
253    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
254        self.cmp(other)
255    }
256}
257
258impl fmt::Display for Dim {
259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260        f.write_str(&self.to_dec_string())
261    }
262}
263
264fn add_e(a: &ExactNum, b: &ExactNum) -> ExactNum {
265    ExactNum::add(a, b, DIM_PREC, RM)
266}
267
268fn sub_e(a: &ExactNum, b: &ExactNum) -> ExactNum {
269    ExactNum::sub(a, b, DIM_PREC, RM)
270}
271
272fn mul_e(a: &ExactNum, b: &ExactNum) -> ExactNum {
273    ExactNum::mul(a, b, DIM_PREC, RM)
274}
275
276fn div_e(a: &ExactNum, b: &ExactNum) -> ExactNum {
277    ExactNum::div(a, b, DIM_PREC, RM)
278}
279
280impl Neg for Dim {
281    type Output = Self;
282
283    fn neg(self) -> Self {
284        wrap(self.inner.as_ref().neg())
285    }
286}
287
288macro_rules! impl_dim_op {
289    ($Trait:ident, $method:ident, $helper:ident) => {
290        impl $Trait for Dim {
291            type Output = Dim;
292            fn $method(self, rhs: Dim) -> Dim {
293                wrap($helper(self.inner.as_ref(), rhs.inner.as_ref()))
294            }
295        }
296        impl $Trait<&Dim> for Dim {
297            type Output = Dim;
298            fn $method(self, rhs: &Dim) -> Dim {
299                wrap($helper(self.inner.as_ref(), rhs.inner.as_ref()))
300            }
301        }
302        impl $Trait<Dim> for &Dim {
303            type Output = Dim;
304            fn $method(self, rhs: Dim) -> Dim {
305                wrap($helper(self.inner.as_ref(), rhs.inner.as_ref()))
306            }
307        }
308        impl $Trait for &Dim {
309            type Output = Dim;
310            fn $method(self, rhs: &Dim) -> Dim {
311                wrap($helper(self.inner.as_ref(), rhs.inner.as_ref()))
312            }
313        }
314    };
315}
316
317impl_dim_op!(Add, add, add_e);
318impl_dim_op!(Sub, sub, sub_e);
319impl_dim_op!(Mul, mul, mul_e);
320impl_dim_op!(Div, div, div_e);