ppt_rs/elements/
position.rs

1//! Position and size types for PPTX elements
2//!
3//! All measurements are in EMU (English Metric Units).
4//! 1 inch = 914400 EMU
5//! 1 cm = 360000 EMU
6//! 1 pt = 12700 EMU
7
8use crate::core::ToXml;
9
10/// EMU conversion constants
11pub const EMU_PER_INCH: i64 = 914400;
12pub const EMU_PER_CM: i64 = 360000;
13pub const EMU_PER_MM: i64 = 36000;
14pub const EMU_PER_PT: i64 = 12700;
15
16/// Position in EMU
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
18pub struct Position {
19    pub x: i64,
20    pub y: i64,
21}
22
23impl Position {
24    /// Create position from EMU values
25    pub fn new(x: i64, y: i64) -> Self {
26        Self { x, y }
27    }
28
29    /// Create position from inches
30    pub fn from_inches(x: f64, y: f64) -> Self {
31        Self {
32            x: (x * EMU_PER_INCH as f64) as i64,
33            y: (y * EMU_PER_INCH as f64) as i64,
34        }
35    }
36
37    /// Create position from centimeters
38    pub fn from_cm(x: f64, y: f64) -> Self {
39        Self {
40            x: (x * EMU_PER_CM as f64) as i64,
41            y: (y * EMU_PER_CM as f64) as i64,
42        }
43    }
44
45    /// Get X in inches
46    pub fn x_inches(&self) -> f64 {
47        self.x as f64 / EMU_PER_INCH as f64
48    }
49
50    /// Get Y in inches
51    pub fn y_inches(&self) -> f64 {
52        self.y as f64 / EMU_PER_INCH as f64
53    }
54}
55
56/// Size in EMU
57#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
58pub struct Size {
59    pub width: i64,
60    pub height: i64,
61}
62
63impl Size {
64    /// Create size from EMU values
65    pub fn new(width: i64, height: i64) -> Self {
66        Self { width, height }
67    }
68
69    /// Create size from inches
70    pub fn from_inches(width: f64, height: f64) -> Self {
71        Self {
72            width: (width * EMU_PER_INCH as f64) as i64,
73            height: (height * EMU_PER_INCH as f64) as i64,
74        }
75    }
76
77    /// Create size from centimeters
78    pub fn from_cm(width: f64, height: f64) -> Self {
79        Self {
80            width: (width * EMU_PER_CM as f64) as i64,
81            height: (height * EMU_PER_CM as f64) as i64,
82        }
83    }
84
85    /// Get width in inches
86    pub fn width_inches(&self) -> f64 {
87        self.width as f64 / EMU_PER_INCH as f64
88    }
89
90    /// Get height in inches
91    pub fn height_inches(&self) -> f64 {
92        self.height as f64 / EMU_PER_INCH as f64
93    }
94}
95
96/// Transform (position + size) for shapes
97#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
98pub struct Transform {
99    pub position: Position,
100    pub size: Size,
101    pub rotation: i32, // in 60000ths of a degree
102}
103
104impl Transform {
105    /// Create a new transform
106    pub fn new(position: Position, size: Size) -> Self {
107        Self {
108            position,
109            size,
110            rotation: 0,
111        }
112    }
113
114    /// Create from inches
115    pub fn from_inches(x: f64, y: f64, width: f64, height: f64) -> Self {
116        Self {
117            position: Position::from_inches(x, y),
118            size: Size::from_inches(width, height),
119            rotation: 0,
120        }
121    }
122
123    /// Set rotation in degrees
124    pub fn with_rotation(mut self, degrees: f64) -> Self {
125        self.rotation = (degrees * 60000.0) as i32;
126        self
127    }
128}
129
130impl ToXml for Transform {
131    fn to_xml(&self) -> String {
132        let mut xml = String::from("<a:xfrm");
133        if self.rotation != 0 {
134            xml.push_str(&format!(r#" rot="{}""#, self.rotation));
135        }
136        xml.push_str(&format!(
137            r#"><a:off x="{}" y="{}"/><a:ext cx="{}" cy="{}"/></a:xfrm>"#,
138            self.position.x, self.position.y, self.size.width, self.size.height
139        ));
140        xml
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_position_from_inches() {
150        let pos = Position::from_inches(1.0, 2.0);
151        assert_eq!(pos.x, 914400);
152        assert_eq!(pos.y, 1828800);
153    }
154
155    #[test]
156    fn test_size_from_inches() {
157        let size = Size::from_inches(3.0, 2.0);
158        assert_eq!(size.width, 2743200);
159        assert_eq!(size.height, 1828800);
160    }
161
162    #[test]
163    fn test_transform_to_xml() {
164        let transform = Transform::from_inches(1.0, 1.0, 2.0, 1.5);
165        let xml = transform.to_xml();
166        assert!(xml.contains("a:xfrm"));
167        assert!(xml.contains("a:off"));
168        assert!(xml.contains("a:ext"));
169    }
170
171    #[test]
172    fn test_transform_with_rotation() {
173        let transform = Transform::from_inches(0.0, 0.0, 1.0, 1.0)
174            .with_rotation(45.0);
175        let xml = transform.to_xml();
176        assert!(xml.contains("rot=\"2700000\"")); // 45 * 60000
177    }
178
179    #[test]
180    fn test_emu_constants() {
181        assert_eq!(EMU_PER_INCH, 914400);
182        assert_eq!(EMU_PER_CM, 360000);
183        assert_eq!(EMU_PER_PT, 12700);
184    }
185}