Skip to main content

latex_rust/
dim.rs

1//! Layout dimension: exact rational (`num / den`).
2//!
3//! TeX layout is ratios of integers (font units, mu = 1/18 em, style scales).
4//! Hardware `f32` / `f64` never appear as calculation terminals.
5
6use core::cmp::Ordering;
7use core::fmt;
8use core::ops::{Add, Div, Mul, Neg, Sub};
9
10use crate::error::Error;
11
12/// Kept in the public API. Layout values are exact rationals, not rounded bits.
13pub const DIM_PREC: usize = 256;
14
15fn gcd_u(mut a: u128, mut b: u128) -> u128 {
16    while b != 0 {
17        let t = a % b;
18        a = b;
19        b = t;
20    }
21    a
22}
23
24/// TeX-style dimension: width, height, depth, italic correction, mu.
25///
26/// One unit is one em at the current math style unless a method says otherwise.
27///
28/// # Examples
29///
30/// ```
31/// use latex_rust::Dim;
32///
33/// let half = Dim::ratio(1, 2);
34/// assert!(half.eq_dim(&(&Dim::one() / &Dim::from_i64(2))));
35/// assert!(!Dim::zero().eq_dim(&Dim::one()));
36/// ```
37#[derive(Clone, Debug)]
38pub struct Dim {
39    num: i128,
40    den: i128,
41    nan: bool,
42}
43
44impl Dim {
45    fn raw(num: i128, den: i128) -> Self {
46        if den == 0 {
47            return Self::nan();
48        }
49        if num == 0 {
50            return Self {
51                num: 0,
52                den: 1,
53                nan: false,
54            };
55        }
56        let g = gcd_u(num.unsigned_abs(), den.unsigned_abs());
57        let g = i128::try_from(g).unwrap_or(1);
58        let mut n = num / g;
59        let mut d = den / g;
60        if d < 0 {
61            n = -n;
62            d = -d;
63        }
64        Self {
65            num: n,
66            den: d,
67            nan: false,
68        }
69    }
70
71    fn nan() -> Self {
72        Self {
73            num: 0,
74            den: 1,
75            nan: true,
76        }
77    }
78
79    fn binop(
80        a: &Self,
81        b: &Self,
82        f: impl Fn(i128, i128, i128, i128) -> Option<(i128, i128)>,
83    ) -> Self {
84        if a.nan || b.nan {
85            return Self::nan();
86        }
87        match f(a.num, a.den, b.num, b.den) {
88            Some((n, d)) => Self::raw(n, d),
89            None => Self::nan(),
90        }
91    }
92
93    /// Zero em.
94    #[must_use]
95    pub fn zero() -> Self {
96        Self::raw(0, 1)
97    }
98
99    /// One em.
100    #[must_use]
101    pub fn one() -> Self {
102        Self::raw(1, 1)
103    }
104
105    /// Integer em count.
106    #[must_use]
107    pub fn from_i64(v: i64) -> Self {
108        Self::raw(i128::from(v), 1)
109    }
110
111    /// Exact rational `num / den` em. `den == 0` yields NaN.
112    #[must_use]
113    pub fn ratio(num: i64, den: i64) -> Self {
114        Self::raw(i128::from(num), i128::from(den))
115    }
116
117    /// Parse a decimal string (optional sign, fraction, `e` exponent).
118    #[must_use]
119    pub fn parse(s: &str) -> Self {
120        let t = s.trim();
121        if t.is_empty() || t.eq_ignore_ascii_case("nan") {
122            return Self::nan();
123        }
124        let mut rest = t;
125        let neg = if let Some(r) = rest.strip_prefix('-') {
126            rest = r;
127            true
128        } else if let Some(r) = rest.strip_prefix('+') {
129            rest = r;
130            false
131        } else {
132            false
133        };
134        let (mant, exp_s) = match rest.find(['e', 'E']) {
135            Some(i) => (&rest[..i], Some(&rest[i + 1..])),
136            None => (rest, None),
137        };
138        let (ip, fp) = match mant.find('.') {
139            Some(i) => (&mant[..i], &mant[i + 1..]),
140            None => (mant, ""),
141        };
142        if ip.is_empty() && fp.is_empty() {
143            return Self::nan();
144        }
145        if !ip.chars().all(|c| c.is_ascii_digit()) || !fp.chars().all(|c| c.is_ascii_digit()) {
146            return Self::nan();
147        }
148        let mut num: i128 = 0;
149        for c in ip.bytes().chain(fp.bytes()) {
150            let d = i128::from(c - b'0');
151            num = match num.checked_mul(10).and_then(|n| n.checked_add(d)) {
152                Some(n) => n,
153                None => return Self::nan(),
154            };
155        }
156        let mut den = 1i128;
157        for _ in 0..fp.len() {
158            den = match den.checked_mul(10) {
159                Some(d) => d,
160                None => return Self::nan(),
161            };
162        }
163        if let Some(es) = exp_s {
164            let e: i32 = match es.parse() {
165                Ok(v) => v,
166                Err(_) => return Self::nan(),
167            };
168            if e > 0 {
169                for _ in 0..e {
170                    num = match num.checked_mul(10) {
171                        Some(n) => n,
172                        None => return Self::nan(),
173                    };
174                }
175            } else {
176                for _ in 0..(-e) {
177                    den = match den.checked_mul(10) {
178                        Some(d) => d,
179                        None => return Self::nan(),
180                    };
181                }
182            }
183        }
184        if neg {
185            num = -num;
186        }
187        Self::raw(num, den)
188    }
189
190    /// Convert integer font units to em: `units / units_per_em`.
191    #[must_use]
192    pub fn from_font_units(units: i64, units_per_em: u16) -> Self {
193        Self::raw(i128::from(units), i128::from(units_per_em))
194    }
195
196    /// One math unit (mu). TeX: `18 mu = 1 em`.
197    #[must_use]
198    pub fn mu() -> Self {
199        Self::ratio(1, 18)
200    }
201
202    /// Convert this em value to mu (`* 18`).
203    #[must_use]
204    pub fn to_mu(&self) -> Self {
205        self.clone() * Self::from_i64(18)
206    }
207
208    /// Convert a mu value to em (`/ 18`).
209    #[must_use]
210    pub fn from_mu(mu: &Self) -> Self {
211        mu.clone() / Self::from_i64(18)
212    }
213
214    /// Absolute value.
215    #[must_use]
216    pub fn abs(&self) -> Self {
217        if self.nan {
218            return Self::nan();
219        }
220        if self.num == i128::MIN {
221            return Self::nan();
222        }
223        Self::raw(self.num.abs(), self.den)
224    }
225
226    /// Maximum of two dimensions.
227    #[must_use]
228    pub fn max(&self, other: &Self) -> Self {
229        match self.cmp(other) {
230            Some(Ordering::Less) => other.clone(),
231            _ => self.clone(),
232        }
233    }
234
235    /// Minimum of two dimensions.
236    #[must_use]
237    pub fn min(&self, other: &Self) -> Self {
238        match self.cmp(other) {
239            Some(Ordering::Greater) => other.clone(),
240            _ => self.clone(),
241        }
242    }
243
244    /// `max(self, 0)`.
245    #[must_use]
246    pub fn clamp_nonneg(&self) -> Self {
247        self.max(&Self::zero())
248    }
249
250    /// True when the value is NaN.
251    #[must_use]
252    pub fn is_nan(&self) -> bool {
253        self.nan
254    }
255
256    /// True when the value compares equal to zero.
257    #[must_use]
258    pub fn is_zero(&self) -> bool {
259        !self.nan && self.num == 0
260    }
261
262    /// The exact rational value in em, as `(numerator, denominator)`, or
263    /// `None` if this dimension is NaN.
264    ///
265    /// The denominator is positive and the fraction is in lowest terms. This is
266    /// the only accessor that does not round: [`Self::to_dec_string`] formats a
267    /// decimal expansion and [`Self::to_ieee32_bits`] rounds to binary32. An
268    /// external renderer that wants to convert at its own pixel boundary,
269    /// rather than take the crate's rounding, should start here.
270    ///
271    /// # Examples
272    ///
273    /// ```
274    /// use latex_rust::Dim;
275    ///
276    /// assert_eq!(Dim::ratio(2, 6).as_ratio(), Some((1, 3)));
277    /// assert_eq!(Dim::ratio(1, -2).as_ratio(), Some((-1, 2)));
278    /// assert_eq!(Dim::zero().as_ratio(), Some((0, 1)));
279    /// assert_eq!(Dim::ratio(1, 0).as_ratio(), None);
280    /// ```
281    #[must_use]
282    pub fn as_ratio(&self) -> Option<(i128, i128)> {
283        (!self.nan).then_some((self.num, self.den))
284    }
285
286    /// Decimal string (exact terminating expansion, or scientific).
287    #[must_use]
288    pub fn to_dec_string(&self) -> String {
289        if self.nan {
290            return "NaN".into();
291        }
292        format_rational(self.num, self.den)
293    }
294
295    /// Compact decimal for SVG attributes.
296    #[must_use]
297    pub fn to_svg_string(&self) -> String {
298        self.to_dec_string()
299    }
300
301    /// Layout dimension from an IEEE-754 binary32 bit pattern (outline boundary).
302    #[must_use]
303    pub fn from_ieee32_bits(bits: u32) -> Self {
304        let sign = (bits >> 31) != 0;
305        let exp = (bits >> 23) & 0xff;
306        let frac = bits & 0x7f_ffff;
307        if exp == 255 {
308            return Self::nan();
309        }
310        let (num, den, sh) = if exp == 0 {
311            if frac == 0 {
312                return Self::zero();
313            }
314            (i128::from(frac), 1i128, 149i32)
315        } else {
316            (i128::from(frac) + (1i128 << 23), 1i128, 150i32 - exp as i32)
317        };
318        let mut n = num;
319        let mut d = den;
320        if sh >= 0 {
321            for _ in 0..sh {
322                d = match d.checked_mul(2) {
323                    Some(v) => v,
324                    None => return Self::nan(),
325                };
326            }
327        } else {
328            for _ in 0..(-sh) {
329                n = match n.checked_mul(2) {
330                    Some(v) => v,
331                    None => return Self::nan(),
332                };
333            }
334        }
335        if sign {
336            n = -n;
337        }
338        Self::raw(n, d)
339    }
340
341    /// Round to IEEE-754 binary32 bits for PNG / raster emission only.
342    #[must_use]
343    pub fn to_ieee32_bits(&self) -> u32 {
344        if self.nan {
345            return 0x7fc0_0000;
346        }
347        if self.num == 0 {
348            return 0;
349        }
350        let sign = if self.num < 0 { 1u32 << 31 } else { 0 };
351        let n = self.num.unsigned_abs();
352        let d = self.den.unsigned_abs();
353        // Find e such that 2^e <= n/d < 2^{e+1}.
354        let mut e: i32 = 0;
355        // Compare n/d vs 1: n ? d
356        if n < d {
357            let mut t = n;
358            while t < d {
359                t = match t.checked_mul(2) {
360                    Some(v) => v,
361                    None => break,
362                };
363                e -= 1;
364                if e < -149 {
365                    return sign;
366                }
367            }
368        } else {
369            let mut t = d;
370            while t <= n / 2 {
371                t = match t.checked_mul(2) {
372                    Some(v) => v,
373                    None => break,
374                };
375                e += 1;
376                if e > 127 {
377                    return sign | 0x7f80_0000;
378                }
379            }
380        }
381        // mantissa: floor((n/d) / 2^(e-23)) = floor(n * 2^(23-e) / d)
382        let mut shift = 23i32 - e;
383        let mut num = n;
384        let den = d;
385        while shift > 0 {
386            num = match num.checked_mul(2) {
387                Some(v) => v,
388                None => return sign | 0x7f80_0000,
389            };
390            shift -= 1;
391        }
392        while shift < 0 {
393            num /= 2;
394            shift += 1;
395        }
396        let mut mant = num / den;
397        let rem = num % den;
398        // round to nearest even
399        if rem * 2 > den || (rem * 2 == den && mant & 1 == 1) {
400            mant += 1;
401        }
402        if mant >= (1u128 << 24) {
403            mant >>= 1;
404            e += 1;
405        }
406        if e > 127 {
407            return sign | 0x7f80_0000;
408        }
409        if e < -126 {
410            // subnormal
411            let sub_shift = -126 - e;
412            if sub_shift >= 24 {
413                return sign;
414            }
415            mant >>= sub_shift as u32;
416            let frac = (mant as u32) & 0x7f_ffff;
417            return sign | frac;
418        }
419        let biased = (e + 127) as u32;
420        let frac = (mant as u32) & 0x7f_ffff;
421        sign | (biased << 23) | frac
422    }
423
424    /// Largest `u32` that is not greater than `self`. Negative and NaN fail.
425    pub fn floor_to_u32(&self) -> Result<u32, Error> {
426        if self.is_nan() {
427            return Err(Error::InvalidOption {
428                what: "dimension is NaN".into(),
429            });
430        }
431        if matches!(self.cmp(&Self::zero()), Some(Ordering::Less)) {
432            return Err(Error::InvalidOption {
433                what: "negative dimension".into(),
434            });
435        }
436        const MAX: u32 = 1 << 20;
437        if matches!(
438            self.cmp(&Self::from_i64(i64::from(MAX))),
439            Some(Ordering::Greater) | Some(Ordering::Equal)
440        ) {
441            return Err(Error::InvalidOption {
442                what: "dimension exceeds raster limit".into(),
443            });
444        }
445        let q = (self.num / self.den) as u32;
446        Ok(q)
447    }
448
449    /// Smallest `u32` that is not less than `self`.
450    pub fn ceil_to_u32(&self) -> Result<u32, Error> {
451        let floor = self.floor_to_u32()?;
452        if self.eq_dim(&Self::from_i64(i64::from(floor))) {
453            Ok(floor)
454        } else {
455            floor.checked_add(1).ok_or_else(|| Error::InvalidOption {
456                what: "dimension overflow".into(),
457            })
458        }
459    }
460
461    /// Compare two dimensions. `None` if either is NaN.
462    #[must_use]
463    #[allow(clippy::should_implement_trait)]
464    pub fn cmp(&self, other: &Self) -> Option<Ordering> {
465        if self.nan || other.nan {
466            return None;
467        }
468        // n1/d1 vs n2/d2 => n1*d2 vs n2*d1
469        let left = self.num.checked_mul(other.den)?;
470        let right = other.num.checked_mul(self.den)?;
471        Some(left.cmp(&right))
472    }
473
474    /// True when `self` and `other` compare equal.
475    #[must_use]
476    pub fn eq_dim(&self, other: &Self) -> bool {
477        matches!(self.cmp(other), Some(Ordering::Equal))
478    }
479}
480
481fn format_rational(num: i128, den: i128) -> String {
482    if num == 0 {
483        return "0.0".into();
484    }
485    let neg = num < 0;
486    let mut n = num.unsigned_abs();
487    let d = den.unsigned_abs();
488    let ip = n / d;
489    n %= d;
490    let mut s = String::new();
491    if neg {
492        s.push('-');
493    }
494    if n == 0 {
495        s.push_str(&ip.to_string());
496        s.push_str(".0");
497        return s;
498    }
499    s.push_str(&ip.to_string());
500    s.push('.');
501    // Long division, cap 24 digits (plenty for font-unit rationals).
502    for _ in 0..24 {
503        if n == 0 {
504            break;
505        }
506        n *= 10;
507        let digit = n / d;
508        s.push(char::from(b'0' + digit as u8));
509        n %= d;
510    }
511    // Trim trailing zeros but keep one fractional digit.
512    while s.ends_with('0') && !s.ends_with(".0") {
513        s.pop();
514    }
515    s
516}
517
518impl PartialEq for Dim {
519    fn eq(&self, other: &Self) -> bool {
520        self.eq_dim(other)
521    }
522}
523
524impl Eq for Dim {}
525
526impl PartialOrd for Dim {
527    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
528        self.cmp(other)
529    }
530}
531
532impl fmt::Display for Dim {
533    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
534        f.write_str(&self.to_dec_string())
535    }
536}
537
538impl Neg for Dim {
539    type Output = Self;
540
541    fn neg(self) -> Self {
542        if self.nan {
543            return Self::nan();
544        }
545        Self::raw(-self.num, self.den)
546    }
547}
548
549fn add_r(a: i128, b: i128, c: i128, d: i128) -> Option<(i128, i128)> {
550    let ad = a.checked_mul(d)?;
551    let cb = c.checked_mul(b)?;
552    let n = ad.checked_add(cb)?;
553    let den = b.checked_mul(d)?;
554    Some((n, den))
555}
556
557fn sub_r(a: i128, b: i128, c: i128, d: i128) -> Option<(i128, i128)> {
558    let ad = a.checked_mul(d)?;
559    let cb = c.checked_mul(b)?;
560    let n = ad.checked_sub(cb)?;
561    let den = b.checked_mul(d)?;
562    Some((n, den))
563}
564
565fn mul_r(a: i128, b: i128, c: i128, d: i128) -> Option<(i128, i128)> {
566    let n = a.checked_mul(c)?;
567    let den = b.checked_mul(d)?;
568    Some((n, den))
569}
570
571fn div_r(a: i128, b: i128, c: i128, d: i128) -> Option<(i128, i128)> {
572    if c == 0 {
573        return None;
574    }
575    let n = a.checked_mul(d)?;
576    let den = b.checked_mul(c)?;
577    Some((n, den))
578}
579
580macro_rules! impl_dim_op {
581    ($Trait:ident, $method:ident, $helper:ident) => {
582        impl $Trait for Dim {
583            type Output = Dim;
584            fn $method(self, rhs: Dim) -> Dim {
585                Dim::binop(&self, &rhs, $helper)
586            }
587        }
588        impl $Trait<&Dim> for Dim {
589            type Output = Dim;
590            fn $method(self, rhs: &Dim) -> Dim {
591                Dim::binop(&self, rhs, $helper)
592            }
593        }
594        impl $Trait<Dim> for &Dim {
595            type Output = Dim;
596            fn $method(self, rhs: Dim) -> Dim {
597                Dim::binop(self, &rhs, $helper)
598            }
599        }
600        impl $Trait for &Dim {
601            type Output = Dim;
602            fn $method(self, rhs: &Dim) -> Dim {
603                Dim::binop(self, rhs, $helper)
604            }
605        }
606    };
607}
608
609impl_dim_op!(Add, add, add_r);
610impl_dim_op!(Sub, sub, sub_r);
611impl_dim_op!(Mul, mul, mul_r);
612impl_dim_op!(Div, div, div_r);
613
614#[cfg(test)]
615mod tests {
616    use super::Dim;
617
618    #[test]
619    fn half_and_font_units() {
620        assert!(Dim::ratio(1, 2).eq_dim(&(&Dim::one() / &Dim::from_i64(2))));
621        let w = Dim::from_font_units(479, 1000);
622        assert_eq!(w.to_dec_string(), "0.479");
623        assert_eq!((Dim::from_i64(12) * w).to_dec_string(), "5.748");
624    }
625
626    #[test]
627    fn ieee32_one() {
628        let one = Dim::from_ieee32_bits(0x3f80_0000);
629        assert!(one.eq_dim(&Dim::one()));
630        assert_eq!(Dim::one().to_ieee32_bits(), 0x3f80_0000);
631    }
632}