Skip to main content

ppt_rs/core/
dimension.rs

1//! Flexible dimension types for position and size specification
2//!
3//! Supports multiple units: EMU, inches, centimeters, points, and ratio (0.0–1.0 of slide).
4//!
5//! # Examples
6//! ```
7//! use ppt_rs::core::Dimension;
8//!
9//! // Different ways to express the same position
10//! let d1 = Dimension::Emu(914400);
11//! let d2 = Dimension::Inches(1.0);
12//! let d3 = Dimension::Cm(2.54);
13//! let d4 = Dimension::Ratio(0.1); // 10% of reference (slide width or height)
14//!
15//! assert_eq!(d1.to_emu(9144000), 914400);
16//! assert_eq!(d2.to_emu(9144000), 914400);
17//! assert_eq!(d4.to_emu(9144000), 914400); // 10% of 10 inches
18//! ```
19
20/// Standard slide width in EMU (10 inches)
21pub const SLIDE_WIDTH_EMU: u32 = 9144000;
22/// Standard slide height in EMU (7.5 inches)
23pub const SLIDE_HEIGHT_EMU: u32 = 6858000;
24
25/// EMU per inch
26const EMU_PER_INCH: f64 = 914400.0;
27/// EMU per centimeter
28const EMU_PER_CM: f64 = 360000.0;
29/// EMU per point
30const EMU_PER_PT: f64 = 12700.0;
31
32/// A flexible dimension that can be expressed in multiple units.
33///
34/// All variants resolve to EMU (English Metric Units) at render time.
35/// `Ratio` is relative to a reference dimension (slide width for x/width, slide height for y/height).
36#[derive(Clone, Debug, PartialEq)]
37pub enum Dimension {
38    /// Absolute value in EMU (English Metric Units)
39    Emu(u32),
40    /// Value in inches (1 inch = 914400 EMU)
41    Inches(f64),
42    /// Value in centimeters (1 cm = 360000 EMU)
43    Cm(f64),
44    /// Value in points (1 pt = 12700 EMU)
45    Pt(f64),
46    /// Ratio of reference dimension (0.0–1.0). For x/width, reference is slide width; for y/height, slide height.
47    Ratio(f64),
48}
49
50impl Dimension {
51    /// Resolve to EMU given a reference dimension (used only for `Ratio`).
52    ///
53    /// For absolute units (Emu, Inches, Cm, Pt), `reference_emu` is ignored.
54    pub fn to_emu(&self, reference_emu: u32) -> u32 {
55        match self {
56            Dimension::Emu(v) => *v,
57            Dimension::Inches(v) => (v * EMU_PER_INCH) as u32,
58            Dimension::Cm(v) => (v * EMU_PER_CM) as u32,
59            Dimension::Pt(v) => (v * EMU_PER_PT) as u32,
60            Dimension::Ratio(r) => {
61                (crate::core::clamp_ratio(*r) * reference_emu as f64) as u32
62            }
63        }
64    }
65
66    /// Resolve X position or width to EMU (reference = slide width)
67    pub fn to_emu_x(&self) -> u32 {
68        self.to_emu(SLIDE_WIDTH_EMU)
69    }
70
71    /// Resolve Y position or height to EMU (reference = slide height)
72    pub fn to_emu_y(&self) -> u32 {
73        self.to_emu(SLIDE_HEIGHT_EMU)
74    }
75}
76
77/// Convenience: convert from u32 (treated as EMU)
78impl From<u32> for Dimension {
79    fn from(emu: u32) -> Self {
80        Dimension::Emu(emu)
81    }
82}
83
84/// Convenience: convert from f64 (treated as ratio if 0.0–1.0, else inches)
85/// This is intentionally NOT implemented to avoid ambiguity.
86/// Use the explicit constructors instead.
87
88/// Shorthand constructors for ergonomic API
89impl Dimension {
90    /// Create from inches
91    pub fn inches(v: f64) -> Self {
92        Dimension::Inches(v)
93    }
94    /// Create from centimeters
95    pub fn cm(v: f64) -> Self {
96        Dimension::Cm(v)
97    }
98    /// Create from points
99    pub fn pt(v: f64) -> Self {
100        Dimension::Pt(v)
101    }
102    /// Create from ratio (0.0–1.0 of slide dimension)
103    pub fn ratio(v: f64) -> Self {
104        Dimension::Ratio(v)
105    }
106    /// Create from EMU
107    pub fn emu(v: u32) -> Self {
108        Dimension::Emu(v)
109    }
110    /// Create from percentage (0–100) of slide dimension
111    pub fn percent(v: f64) -> Self {
112        Dimension::Ratio(v / 100.0)
113    }
114}
115
116/// A 2D position expressed in flexible dimensions.
117#[derive(Clone, Debug)]
118pub struct FlexPosition {
119    pub x: Dimension,
120    pub y: Dimension,
121}
122
123impl FlexPosition {
124    pub fn new(x: Dimension, y: Dimension) -> Self {
125        Self { x, y }
126    }
127
128    /// Resolve to (x_emu, y_emu) using standard slide dimensions
129    pub fn to_emu(&self) -> (u32, u32) {
130        (self.x.to_emu_x(), self.y.to_emu_y())
131    }
132
133    /// Resolve to (x_emu, y_emu) using custom slide dimensions
134    pub fn to_emu_with(&self, slide_width: u32, slide_height: u32) -> (u32, u32) {
135        (self.x.to_emu(slide_width), self.y.to_emu(slide_height))
136    }
137}
138
139/// A 2D size expressed in flexible dimensions.
140#[derive(Clone, Debug)]
141pub struct FlexSize {
142    pub width: Dimension,
143    pub height: Dimension,
144}
145
146impl FlexSize {
147    pub fn new(width: Dimension, height: Dimension) -> Self {
148        Self { width, height }
149    }
150
151    /// Resolve to (width_emu, height_emu) using standard slide dimensions
152    pub fn to_emu(&self) -> (u32, u32) {
153        (self.width.to_emu_x(), self.height.to_emu_y())
154    }
155
156    /// Resolve to (width_emu, height_emu) using custom slide dimensions
157    pub fn to_emu_with(&self, slide_width: u32, slide_height: u32) -> (u32, u32) {
158        (
159            self.width.to_emu(slide_width),
160            self.height.to_emu(slide_height),
161        )
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_emu_passthrough() {
171        assert_eq!(Dimension::Emu(914400).to_emu(0), 914400);
172    }
173
174    #[test]
175    fn test_inches_to_emu() {
176        assert_eq!(Dimension::Inches(1.0).to_emu(0), 914400);
177        assert_eq!(Dimension::Inches(0.5).to_emu(0), 457200);
178        assert_eq!(Dimension::Inches(10.0).to_emu(0), 9144000);
179    }
180
181    #[test]
182    fn test_cm_to_emu() {
183        assert_eq!(Dimension::Cm(2.54).to_emu(0), 914400);
184        assert_eq!(Dimension::Cm(1.0).to_emu(0), 360000);
185    }
186
187    #[test]
188    fn test_pt_to_emu() {
189        assert_eq!(Dimension::Pt(72.0).to_emu(0), 914400); // 72pt = 1 inch
190        assert_eq!(Dimension::Pt(1.0).to_emu(0), 12700);
191    }
192
193    #[test]
194    fn test_ratio_to_emu() {
195        // 10% of slide width (10 inches = 9144000 EMU) = 1 inch
196        assert_eq!(Dimension::Ratio(0.1).to_emu(SLIDE_WIDTH_EMU), 914400);
197        // 50% of slide width = 5 inches
198        assert_eq!(Dimension::Ratio(0.5).to_emu(SLIDE_WIDTH_EMU), 4572000);
199        // 100% of slide width = 10 inches
200        assert_eq!(Dimension::Ratio(1.0).to_emu(SLIDE_WIDTH_EMU), 9144000);
201        // 0% = 0
202        assert_eq!(Dimension::Ratio(0.0).to_emu(SLIDE_WIDTH_EMU), 0);
203    }
204
205    #[test]
206    fn test_ratio_clamped() {
207        // Values > 1.0 clamped to 1.0
208        assert_eq!(Dimension::Ratio(1.5).to_emu(SLIDE_WIDTH_EMU), 9144000);
209        // Values < 0.0 clamped to 0.0
210        assert_eq!(Dimension::Ratio(-0.5).to_emu(SLIDE_WIDTH_EMU), 0);
211    }
212
213    #[test]
214    fn test_percent() {
215        assert_eq!(Dimension::percent(50.0).to_emu(SLIDE_WIDTH_EMU), 4572000);
216        assert_eq!(Dimension::percent(10.0).to_emu(SLIDE_WIDTH_EMU), 914400);
217    }
218
219    #[test]
220    fn test_to_emu_x_y() {
221        let x = Dimension::Ratio(0.5);
222        let y = Dimension::Ratio(0.5);
223        assert_eq!(x.to_emu_x(), SLIDE_WIDTH_EMU / 2);
224        assert_eq!(y.to_emu_y(), SLIDE_HEIGHT_EMU / 2);
225    }
226
227    #[test]
228    fn test_flex_position() {
229        let pos = FlexPosition::new(Dimension::Inches(1.0), Dimension::Ratio(0.5));
230        let (x, y) = pos.to_emu();
231        assert_eq!(x, 914400);
232        assert_eq!(y, SLIDE_HEIGHT_EMU / 2);
233    }
234
235    #[test]
236    fn test_flex_size() {
237        let size = FlexSize::new(Dimension::Ratio(0.8), Dimension::Inches(2.0));
238        let (w, h) = size.to_emu();
239        assert_eq!(w, (SLIDE_WIDTH_EMU as f64 * 0.8) as u32);
240        assert_eq!(h, 914400 * 2);
241    }
242
243    #[test]
244    fn test_flex_position_custom_slide() {
245        let custom_w = 12192000_u32; // 13.33 inches (widescreen)
246        let custom_h = 6858000_u32;
247        let pos = FlexPosition::new(Dimension::Ratio(0.5), Dimension::Ratio(0.5));
248        let (x, y) = pos.to_emu_with(custom_w, custom_h);
249        assert_eq!(x, custom_w / 2);
250        assert_eq!(y, custom_h / 2);
251    }
252
253    #[test]
254    fn test_from_u32() {
255        let d: Dimension = 914400_u32.into();
256        assert_eq!(d, Dimension::Emu(914400));
257    }
258
259    #[test]
260    fn test_shorthand_constructors() {
261        assert_eq!(Dimension::inches(1.0), Dimension::Inches(1.0));
262        assert_eq!(Dimension::cm(2.54), Dimension::Cm(2.54));
263        assert_eq!(Dimension::pt(72.0), Dimension::Pt(72.0));
264        assert_eq!(Dimension::ratio(0.5), Dimension::Ratio(0.5));
265        assert_eq!(Dimension::emu(914400), Dimension::Emu(914400));
266    }
267}