Skip to main content

oxml_core/
units.rs

1//! Unit types for OOXML measurements.
2
3/// Twips — 1/20 of a point, or 1/1440 of an inch.
4/// Used for page dimensions, margins, spacing, indentation.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
6pub struct Twips(pub i32);
7
8impl Twips {
9    pub fn from_inches(inches: f64) -> Self {
10        Twips((inches * 1440.0) as i32)
11    }
12
13    pub fn from_cm(cm: f64) -> Self {
14        Twips((cm * 567.0) as i32)
15    }
16
17    pub fn from_pt(pt: f64) -> Self {
18        Twips((pt * 20.0) as i32)
19    }
20
21    pub fn to_inches(self) -> f64 {
22        self.0 as f64 / 1440.0
23    }
24
25    pub fn to_cm(self) -> f64 {
26        self.0 as f64 / 567.0
27    }
28
29    pub fn to_pt(self) -> f64 {
30        self.0 as f64 / 20.0
31    }
32
33    pub fn to_emu(self) -> Emu {
34        Emu(self.0 as i64 * 635)
35    }
36}
37
38/// English Metric Units — 1/914400 of an inch.
39/// Used for drawing coordinates and image sizing.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
41pub struct Emu(pub i64);
42
43impl Emu {
44    pub fn from_inches(inches: f64) -> Self {
45        Emu((inches * 914400.0) as i64)
46    }
47
48    pub fn from_cm(cm: f64) -> Self {
49        Emu((cm * 360000.0) as i64)
50    }
51
52    pub fn from_pt(pt: f64) -> Self {
53        Emu((pt * 12700.0) as i64)
54    }
55
56    pub fn to_inches(self) -> f64 {
57        self.0 as f64 / 914400.0
58    }
59
60    pub fn to_cm(self) -> f64 {
61        self.0 as f64 / 360000.0
62    }
63
64    pub fn to_pt(self) -> f64 {
65        self.0 as f64 / 12700.0
66    }
67
68    pub fn to_twips(self) -> Twips {
69        Twips((self.0 / 635) as i32)
70    }
71}
72
73/// Half-points — 1/2 of a point, or 1/144 of an inch.
74/// Used for font sizes (e.g., 24 half-points = 12pt).
75#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
76pub struct HalfPoint(pub u32);
77
78impl HalfPoint {
79    pub fn from_pt(pt: f64) -> Self {
80        HalfPoint((pt * 2.0) as u32)
81    }
82
83    pub fn to_pt(self) -> f64 {
84        self.0 as f64 / 2.0
85    }
86}
87
88/// Centipoints, 1/100 of a point.
89/// Used for DrawingML font sizes.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
91pub struct Centipoints(pub i32);
92
93impl Centipoints {
94    pub fn from_pt(pt: f64) -> Self {
95        Centipoints((pt * 100.0) as i32)
96    }
97
98    pub fn to_pt(self) -> f64 {
99        self.0 as f64 / 100.0
100    }
101}
102
103/// Angles stored in 60,000ths of a degree.
104/// Used for DrawingML rotation and gradient angles.
105#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
106pub struct Angle(pub i32);
107
108impl Angle {
109    pub fn from_degrees(degrees: f64) -> Self {
110        Angle((degrees * 60_000.0) as i32)
111    }
112
113    pub fn to_degrees(self) -> f64 {
114        self.0 as f64 / 60_000.0
115    }
116}
117
118/// Percentages stored in thousandths of a percent.
119/// A stored value of 75,000 represents 75 percent.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
121pub struct Percent1000(pub i32);
122
123impl Percent1000 {
124    pub fn from_percent(percent: f64) -> Self {
125        Percent1000((percent * 1000.0) as i32)
126    }
127
128    pub fn to_fraction(self) -> f64 {
129        self.0 as f64 / 100_000.0
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn twips_conversions() {
139        let t = Twips::from_inches(1.0);
140        assert_eq!(t.0, 1440);
141        assert!((t.to_inches() - 1.0).abs() < 0.001);
142        assert!((t.to_pt() - 72.0).abs() < 0.001);
143    }
144
145    #[test]
146    fn emu_conversions() {
147        let e = Emu::from_inches(1.0);
148        assert_eq!(e.0, 914400);
149        assert!((e.to_inches() - 1.0).abs() < 0.001);
150    }
151
152    #[test]
153    fn half_point_conversion() {
154        let hp = HalfPoint::from_pt(12.0);
155        assert_eq!(hp.0, 24);
156        assert!((hp.to_pt() - 12.0).abs() < 0.001);
157    }
158
159    #[test]
160    fn twips_to_emu_round_trip() {
161        let t = Twips(1440);
162        let e = t.to_emu();
163        let t2 = e.to_twips();
164        assert_eq!(t, t2);
165    }
166
167    #[test]
168    fn twips_float_constructors_truncate_toward_zero() {
169        assert_eq!(Twips::from_inches(1.75 / 1440.0).0, 1);
170        assert_eq!(Twips::from_inches(-1.75 / 1440.0).0, -1);
171        assert_eq!(Twips::from_cm(1.75 / 567.0).0, 1);
172        assert_eq!(Twips::from_cm(-1.75 / 567.0).0, -1);
173        assert_eq!(Twips::from_pt(1.75 / 20.0).0, 1);
174        assert_eq!(Twips::from_pt(-1.75 / 20.0).0, -1);
175    }
176
177    #[test]
178    fn emu_float_constructors_truncate_toward_zero() {
179        assert_eq!(Emu::from_inches(1.75 / 914400.0).0, 1);
180        assert_eq!(Emu::from_inches(-1.75 / 914400.0).0, -1);
181        assert_eq!(Emu::from_cm(1.75 / 360000.0).0, 1);
182        assert_eq!(Emu::from_cm(-1.75 / 360000.0).0, -1);
183        assert_eq!(Emu::from_pt(1.75 / 12700.0).0, 1);
184        assert_eq!(Emu::from_pt(-1.75 / 12700.0).0, -1);
185    }
186
187    #[test]
188    fn centipoints_round_trip_points() {
189        let value = Centipoints::from_pt(18.0);
190        assert_eq!(value.0, 1800);
191        assert_eq!(value.to_pt(), 18.0);
192    }
193
194    #[test]
195    fn angle_round_trip_degrees() {
196        let value = Angle::from_degrees(90.0);
197        assert_eq!(value.0, 5_400_000);
198        assert_eq!(value.to_degrees(), 90.0);
199    }
200
201    #[test]
202    fn percent1000_round_trip_percent() {
203        let value = Percent1000::from_percent(75.0);
204        assert_eq!(value.0, 75_000);
205        assert_eq!(value.to_fraction(), 0.75);
206    }
207
208    #[test]
209    fn new_unit_float_constructors_truncate_toward_zero() {
210        assert_eq!(Centipoints::from_pt(1.75 / 100.0).0, 1);
211        assert_eq!(Centipoints::from_pt(-1.75 / 100.0).0, -1);
212        assert_eq!(Angle::from_degrees(1.75 / 60_000.0).0, 1);
213        assert_eq!(Angle::from_degrees(-1.75 / 60_000.0).0, -1);
214        assert_eq!(Percent1000::from_percent(1.75 / 1000.0).0, 1);
215        assert_eq!(Percent1000::from_percent(-1.75 / 1000.0).0, -1);
216    }
217}