const EMU_PER_INCH: u64 = 914_400;
const EMU_PER_PT: u64 = 12_700;
const EMU_PER_CM: u64 = 360_000;
const EMU_PER_MM: u64 = 36_000;
const EMU_PER_PX: u64 = 9_525;
const EMU_PER_TWIP: u64 = 635;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Length(u64);
impl Length {
#[must_use]
pub const fn from_emu(emu: u64) -> Self {
Self(emu)
}
#[must_use]
pub const fn from_px(px: u32) -> Self {
Self(px as u64 * EMU_PER_PX)
}
#[must_use]
pub const fn from_pt(pt: f64) -> Self {
Self(round_to_emu(pt, EMU_PER_PT))
}
#[must_use]
pub const fn from_inches(inches: f64) -> Self {
Self(round_to_emu(inches, EMU_PER_INCH))
}
#[must_use]
pub const fn from_cm(cm: f64) -> Self {
Self(round_to_emu(cm, EMU_PER_CM))
}
#[must_use]
pub const fn from_mm(mm: f64) -> Self {
Self(round_to_emu(mm, EMU_PER_MM))
}
#[must_use]
pub const fn as_emu(self) -> u64 {
self.0
}
pub(crate) const fn as_twips(self) -> u64 {
let twips = self.0 / EMU_PER_TWIP;
if self.0 % EMU_PER_TWIP > EMU_PER_TWIP / 2 {
twips + 1
} else {
twips
}
}
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
reason = "float-to-int casts saturate as documented; the unit factors and \
every truncated value below 2^53 convert to f64 exactly"
)]
const fn round_to_emu(value: f64, emu_per_unit: u64) -> u64 {
let emu = value * emu_per_unit as f64;
let whole = emu as u64;
if emu - whole as f64 >= 0.5 {
whole.saturating_add(1)
} else {
whole
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn units_convert_to_their_exact_emu_values() {
assert_eq!(Length::from_emu(42).as_emu(), 42);
assert_eq!(Length::from_px(1).as_emu(), 9_525);
assert_eq!(Length::from_pt(1.0).as_emu(), 12_700);
assert_eq!(Length::from_inches(1.0).as_emu(), 914_400);
assert_eq!(Length::from_cm(1.0).as_emu(), 360_000);
assert_eq!(Length::from_mm(1.0).as_emu(), 36_000);
}
#[test]
fn px_are_measured_at_96_dpi() {
assert_eq!(Length::from_px(96), Length::from_inches(1.0));
assert_eq!(Length::from_px(4), Length::from_pt(3.0));
}
#[test]
fn equivalent_lengths_in_different_units_are_equal() {
assert_eq!(Length::from_cm(2.54), Length::from_inches(1.0));
assert_eq!(Length::from_mm(25.4), Length::from_inches(1.0));
assert_eq!(Length::from_pt(72.0), Length::from_inches(1.0));
assert_eq!(Length::from_mm(10.0), Length::from_cm(1.0));
}
#[test]
fn fractional_values_round_to_the_nearest_emu() {
assert_eq!(Length::from_cm(0.7).as_emu(), 252_000);
assert_eq!(Length::from_mm(0.07).as_emu(), 2_520);
assert_eq!(Length::from_pt(1.15).as_emu(), 14_605);
assert_eq!(Length::from_pt(0.5).as_emu(), 6_350);
assert_eq!(Length::from_inches(1e-6).as_emu(), 1);
assert_eq!(Length::from_inches(4e-7).as_emu(), 0);
assert_eq!(Length::from_pt(0.5 / 12_700.0).as_emu(), 1);
assert_eq!(Length::from_pt(1.5 / 12_700.0).as_emu(), 2);
}
#[test]
fn negative_and_nan_lengths_become_zero() {
for length in [
Length::from_pt(-1.0),
Length::from_inches(-0.0),
Length::from_cm(f64::NAN),
Length::from_mm(-f64::NAN),
Length::from_inches(f64::NEG_INFINITY),
Length::from_cm(f64::MIN),
Length::from_mm(-1e-300),
] {
assert_eq!(length, Length::from_emu(0));
}
}
#[test]
fn huge_and_infinite_lengths_saturate() {
for length in [
Length::from_pt(f64::INFINITY),
Length::from_inches(f64::MAX),
Length::from_cm(1e30),
Length::from_mm(5.2e14),
] {
assert_eq!(length.as_emu(), u64::MAX);
}
assert_eq!(
Length::from_px(u32::MAX).as_emu(),
u64::from(u32::MAX) * 9_525
);
}
#[test]
fn large_whole_values_convert_exactly() {
assert_eq!(Length::from_cm(1e12).as_emu(), 360_000_000_000_000_000);
assert_eq!(Length::from_emu(u64::MAX).as_emu(), u64::MAX);
}
#[test]
fn constructors_work_in_const_context() {
const MARGIN: Length = Length::from_cm(2.0);
assert_eq!(MARGIN.as_emu(), 720_000);
}
#[test]
fn as_twips_rounds_to_the_nearest_twip() {
let twips = |emu| Length::from_emu(emu).as_twips();
assert_eq!(twips(0), 0);
assert_eq!(twips(317), 0);
assert_eq!(twips(318), 1);
assert_eq!(twips(635), 1);
assert_eq!(twips(952), 1);
assert_eq!(twips(953), 2);
assert_eq!(Length::from_pt(1.0).as_twips(), 20);
assert_eq!(Length::from_inches(1.0).as_twips(), 1_440);
assert_eq!(Length::from_cm(1.0).as_twips(), 567);
assert_eq!(twips(u64::MAX), 29_049_990_667_259_136);
assert_eq!(twips(u64::MAX - 256), 29_049_990_667_259_136);
}
#[test]
fn lengths_order_by_size() {
assert_eq!(Length::default(), Length::from_emu(0));
assert!(Length::from_cm(1.0) < Length::from_inches(1.0));
assert!(Length::from_pt(1.0) > Length::from_px(1));
}
}