ooxml_core/units.rs
1//! 单位、基本类型与颜色。
2//!
3//! 本模块集中管理 `ooxml-core` 中所有"非 OOXML 强类型但又频繁使用"的小型数据载体。
4//! 它是 `crate::lib` 中"高阶 API 层"与"OPC / OOXML 模型层"之间的语义桥梁。
5//!
6//! # PowerPoint 的几何单位
7//!
8//! OOXML 中所有几何属性(`<a:off>` / `<a:ext>` / 行高 / 列宽 / 边框 / 缩进)都是
9//! **EMU(English Metric Unit)** 整数。换算关系:
10//!
11//! | 单位 | EMU | 备注 |
12//! | --------- | -------------- | ------------------------------ |
13//! | 1 inch | 914 400 EMU | 1 in = 914 400 |
14//! | 1 cm | 360 000 EMU | 1 cm = 360 000 |
15//! | 1 pt | 12 700 EMU | 1 pt = 12 700 |
16//! | 1 sp | 6 350 EMU | 1 pt = 20 sp(OpenType 子像素)|
17//!
18//! 库内统一以 `i64` 形式的 EMU 作内部表示,理由:
19//!
20//! 1. **无浮点漂移**:在缩放 / 旋转 / 多次叠加时仍能保持整型精度。
21//! 2. **互操作零成本**:与 python-pptx / Open XML SDK / LibreOffice 完全一致。
22//! 3. **可读性**:用户阅读 `Emu(914400)` 与 `Inches(1.0)` 时是等价的。
23//!
24//! # 类型分层
25//!
26//! - [`Emu`]:绝对内部单位,整数 NewType。
27//! - [`Pt`] / [`Inches`] / [`Cm`]:外部友好单位,浮点 NewType。
28//! - [`EmuPoint`]:2D 点(x, y),以 EMU 整数存储,主要给连接器/手绘 freeform 用。
29//! - [`EmuExt`]:扩展 trait,统一从 `f64` / `f32` / `i32` / `i64` / `Pt` / `Inches` / `Cm` 转换到 `Emu`。
30//! - [`RGBColor`]:sRGB 颜色三字节 0-255,对应 OOXML `<a:srgbClr val="RRGGBB"/>`。
31//!
32//! # 设计哲学
33//!
34//! - **不暴露 `f64` 形参**:高阶 API 的"位置 / 大小"参数应接受 NewType 或显式单位
35//! 转换。例如 `add_textbox(Inches(1.0), Inches(1.0), Inches(8.0), Inches(1.0))`,
36//! 让"单位混用"在编译期就被拒绝。
37//! - **浮点语义**:`f64` 在 `EmuExt` 中默认解释为"英寸"——这是最常用的隐含单位;
38//! 若需其它语义请显式包装 `Pt(...)` / `Cm(...)`。
39//! - **颜色不归此模块管**:主题色(schemeClr)、系统色(sysClr)、preset 颜色
40//! 见 [`crate::oxml::color`]。
41//!
42//! # 与 python-pptx 的对应
43//!
44//! - `pptx.util.Pt` / `pptx.util.Inches` / `pptx.util.Cm` / `pptx.util.Emu` ←→ 同名 NewType。
45//! - `pptx.util.Length` 的 `.x` / `.y` 抽象 → 本库通过 [`EmuPoint`] 暴露。
46//! - `pptx.dml.color.RGBColor` ←→ [`RGBColor`](构造时 `(r, g, b)` 元组也支持)。
47//!
48//! # 示例
49//!
50//! ```
51//! use ooxml_core::{Emu, Inches, Pt, Cm, EmuExt, RGBColor};
52//!
53//! // 隐式单位转换
54//! let w: Emu = Inches(8.5).emu();
55//! assert_eq!(w.value(), (8.5 * 914_400.0) as i64);
56//!
57//! // 跨单位混算
58//! let h = Pt(72.0).emu() + Inches(1.0).emu();
59//!
60//! // 颜色
61//! let red: RGBColor = (255, 0, 0).into();
62//! assert_eq!(red, RGBColor::RED);
63//! ```
64
65use std::fmt;
66
67/// EMU(English Metric Unit),所有内部几何计算都使用 `i64` 形式的 EMU。
68///
69/// EMU 在 OOXML 中是绝对整数,可保证精度无损;库内任何"宽 / 高 / 偏移 / 行高 / 列宽"
70/// 字段均以本类型为内部表示。NewType 包装的 `i64` 同时:
71///
72/// - 提供 `value()` / `new()` 与 f64 单位的双向换算;
73/// - 暴露 `+` / `-` / `*` 等运算符便于几何计算;
74/// - 实现 `Copy + Default + Ord`,可直接作为 `BTreeMap` key。
75#[derive(Copy, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
76pub struct Emu(
77 /// EMU 数值(可为负)。
78 pub i64,
79);
80
81impl Emu {
82 /// 0 EMU。用于默认值与"无偏移"语义。
83 pub const ZERO: Emu = Emu(0);
84
85 /// 直接以 i64 构造。
86 ///
87 /// # 参数
88 /// - `v`:EMU 数值(可为负)。
89 ///
90 /// # 示例
91 /// ```
92 /// use ooxml_core::Emu;
93 /// let off = Emu::new(914_400);
94 /// assert_eq!(off.value(), 914_400);
95 /// ```
96 #[inline]
97 pub const fn new(v: i64) -> Self {
98 Emu(v)
99 }
100
101 /// 取内部 i64 值。
102 #[inline]
103 pub const fn value(self) -> i64 {
104 self.0
105 }
106
107 /// 转为英寸(`f64`)。
108 #[inline]
109 pub const fn inches(self) -> f64 {
110 self.0 as f64 / 914_400.0
111 }
112
113 /// 转为磅(`f64`)。
114 #[inline]
115 pub const fn pt(self) -> f64 {
116 self.0 as f64 / 12_700.0
117 }
118
119 /// 转为厘米(`f64`)。
120 #[inline]
121 pub const fn cm(self) -> f64 {
122 self.0 as f64 / 360_000.0
123 }
124}
125
126impl fmt::Debug for Emu {
127 /// Debug 输出同时给出原始 EMU 与换算后的英寸,便于日志阅读。
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 write!(f, "Emu({} = {} in)", self.0, self.inches())
130 }
131}
132
133impl fmt::Display for Emu {
134 /// Display 形式为 `"<n> EMU"`,便于错误消息中嵌入。
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 write!(f, "{} EMU", self.0)
137 }
138}
139
140// 算术运算符:让 Emu 参与几何计算时无需 .0 拆箱。
141impl std::ops::Add for Emu {
142 type Output = Emu;
143 #[inline]
144 fn add(self, rhs: Emu) -> Emu {
145 Emu(self.0 + rhs.0)
146 }
147}
148impl std::ops::Sub for Emu {
149 type Output = Emu;
150 #[inline]
151 fn sub(self, rhs: Emu) -> Emu {
152 Emu(self.0 - rhs.0)
153 }
154}
155impl std::ops::Neg for Emu {
156 type Output = Emu;
157 #[inline]
158 fn neg(self) -> Emu {
159 Emu(-self.0)
160 }
161}
162impl std::ops::Mul<i64> for Emu {
163 type Output = Emu;
164 #[inline]
165 fn mul(self, rhs: i64) -> Emu {
166 Emu(self.0 * rhs)
167 }
168}
169
170/// 单位转换 trait,封装"任意类型 → [`Emu`]"的语义。
171///
172/// 通过 `EmuExt`,任意实现了该 trait 的类型都能 `.emu()` 转为 `Emu`。
173/// 这样 `Inches(1.0).emu() + Pt(72.0).emu()` 这类混合表达式可以一行写完,
174/// 而不必每次手写 `Emu::new(...)`。
175///
176/// 各类实现的隐含语义:
177///
178/// - `Emu` / `i64` / `i32` / `u32`:原值就是 EMU;
179/// - `f64` / `f32`:解释为"英寸"(最常用的隐含单位);
180/// - `Pt`:磅(1 pt = 12 700 EMU);
181/// - `Inches`:英寸(1 in = 914 400 EMU);
182/// - `Cm`:厘米(1 cm = 360 000 EMU)。
183pub trait EmuExt: Copy {
184 /// 转为 [`Emu`]。具体换算由实现决定。
185 fn emu(self) -> Emu;
186}
187
188impl EmuExt for Emu {
189 /// 恒等映射。
190 #[inline]
191 fn emu(self) -> Emu {
192 self
193 }
194}
195
196impl EmuExt for i64 {
197 /// 原值视为 EMU。
198 #[inline]
199 fn emu(self) -> Emu {
200 Emu(self)
201 }
202}
203
204impl EmuExt for i32 {
205 /// 原值视为 EMU(提升到 i64)。
206 #[inline]
207 fn emu(self) -> Emu {
208 Emu(self as i64)
209 }
210}
211
212impl EmuExt for u32 {
213 /// 原值视为 EMU(提升到 i64)。
214 #[inline]
215 fn emu(self) -> Emu {
216 Emu(self as i64)
217 }
218}
219
220impl EmuExt for f64 {
221 /// `f64` 当作**英寸**处理。
222 #[inline]
223 fn emu(self) -> Emu {
224 Emu((self * 914_400.0) as i64)
225 }
226}
227
228impl EmuExt for f32 {
229 /// `f32` 当作**英寸**处理。
230 #[inline]
231 fn emu(self) -> Emu {
232 Emu((self as f64 * 914_400.0) as i64)
233 }
234}
235
236/// 磅:1 pt = 12700 EMU。
237///
238/// 文本大小(`sz`)、行距(`lnSpc`)、缩进(`indent`)等文本相关属性
239/// 优先以 `Pt` 为单位。包装为 NewType 后编译器可阻止与 `Inches` 误混。
240#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd)]
241pub struct Pt(
242 /// 磅值(f64)。
243 pub f64,
244);
245
246impl Pt {
247 /// 构造磅值。
248 #[inline]
249 pub const fn new(v: f64) -> Self {
250 Pt(v)
251 }
252
253 /// 取内部 f64。
254 #[inline]
255 pub const fn value(self) -> f64 {
256 self.0
257 }
258}
259
260impl EmuExt for Pt {
261 /// 1 pt = 12 700 EMU。
262 #[inline]
263 fn emu(self) -> Emu {
264 Emu((self.0 * 12_700.0) as i64)
265 }
266}
267
268/// 英寸:1 inch = 914400 EMU。
269///
270/// 形状的 `cx` / `cy` / `x` / `y` 等几何属性最常用的语义单位。
271#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd)]
272pub struct Inches(
273 /// 英寸值(f64)。
274 pub f64,
275);
276
277impl Inches {
278 /// 构造英寸值。
279 #[inline]
280 pub const fn new(v: f64) -> Self {
281 Inches(v)
282 }
283
284 /// 取内部 f64。
285 #[inline]
286 pub const fn value(self) -> f64 {
287 self.0
288 }
289}
290
291impl EmuExt for Inches {
292 /// 1 in = 914 400 EMU。
293 #[inline]
294 fn emu(self) -> Emu {
295 Emu((self.0 * 914_400.0) as i64)
296 }
297}
298
299/// 厘米:1 cm = 360000 EMU。
300///
301/// 仅作为欧式使用习惯的可选入口,与 Inches / Pt 完全等价(换算即可)。
302#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd)]
303pub struct Cm(
304 /// 厘米值(f64)。
305 pub f64,
306);
307
308impl Cm {
309 /// 构造厘米值。
310 #[inline]
311 pub const fn new(v: f64) -> Self {
312 Cm(v)
313 }
314
315 /// 取内部 f64。
316 #[inline]
317 pub const fn value(self) -> f64 {
318 self.0
319 }
320}
321
322impl EmuExt for Cm {
323 /// 1 cm = 360 000 EMU。
324 #[inline]
325 fn emu(self) -> Emu {
326 Emu((self.0 * 360_000.0) as i64)
327 }
328}
329
330/// 2D 几何点(EMU 整数坐标)。
331///
332/// 主要给以下场景使用:
333/// - **连接器**的 begin / end;
334/// - **freeform** 的内部路径点(`a:cubicBezTo` / `a:lineTo` / `a:moveTo`);
335/// - **占位符锚点**(`a:off x="" y=""`)。
336///
337/// # 与 python-pptx 的对应
338///
339/// python-pptx 中没有 `EmuPoint` 这样的直接类型;最接近的是
340/// `(x: int, y: int)` 元组。本库用 NewType 是为了类型安全与可读性。
341#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
342pub struct EmuPoint(
343 /// x 坐标(EMU)。
344 pub i64,
345 /// y 坐标(EMU)。
346 pub i64,
347);
348
349impl EmuPoint {
350 /// 构造新点。
351 #[inline]
352 pub const fn new(x: i64, y: i64) -> Self {
353 EmuPoint(x, y)
354 }
355 /// 取 x 坐标。
356 #[inline]
357 pub const fn x(self) -> i64 {
358 self.0
359 }
360 /// 取 y 坐标。
361 #[inline]
362 pub const fn y(self) -> i64 {
363 self.1
364 }
365 /// 转为 `(x, y)` 元组。
366 #[inline]
367 pub const fn as_tuple(self) -> (i64, i64) {
368 (self.0, self.1)
369 }
370}
371
372impl From<(i64, i64)> for EmuPoint {
373 #[inline]
374 fn from((x, y): (i64, i64)) -> Self {
375 EmuPoint(x, y)
376 }
377}
378impl From<EmuPoint> for (i64, i64) {
379 #[inline]
380 fn from(p: EmuPoint) -> Self {
381 (p.0, p.1)
382 }
383}
384
385/// sRGB 颜色(三字节 0-255)。
386///
387/// 对应 OOXML 中的 `<a:srgbClr val="RRGGBB"/>` 元素。
388/// 注意:本类型仅表达"绝对 RGB 颜色",**不**含透明度(透明度由
389/// `<a:alpha val="..."/>` 子元素承载,见 [`crate::oxml::color`])。
390#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
391pub struct RGBColor(
392 /// 红色分量(0-255)。
393 pub u8,
394 /// 绿色分量(0-255)。
395 pub u8,
396 /// 蓝色分量(0-255)。
397 pub u8,
398);
399
400impl RGBColor {
401 /// 黑色 `#000000`。
402 pub const BLACK: RGBColor = RGBColor(0, 0, 0);
403 /// 白色 `#FFFFFF`。
404 pub const WHITE: RGBColor = RGBColor(255, 255, 255);
405 /// 红色 `#FF0000`。
406 pub const RED: RGBColor = RGBColor(255, 0, 0);
407 /// 绿色 `#00FF00`。
408 pub const GREEN: RGBColor = RGBColor(0, 255, 0);
409 /// 蓝色 `#0000FF`。
410 pub const BLUE: RGBColor = RGBColor(0, 0, 255);
411}
412
413/// 允许 `(r, g, b)` 三元组直接 `into()` 转为 [`RGBColor`],方便函数签名紧凑。
414impl From<(u8, u8, u8)> for RGBColor {
415 #[inline]
416 fn from((r, g, b): (u8, u8, u8)) -> Self {
417 RGBColor(r, g, b)
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424
425 /// 主要单位换算 round-trip 测试。
426 #[test]
427 fn unit_conversion() {
428 assert_eq!(Pt(72.0).emu().value(), 72 * 12_700);
429 assert_eq!(Inches(1.0).emu().value(), 914_400);
430 assert_eq!(Cm(1.0).emu().value(), 360_000);
431 assert!((Emu(914_400).inches() - 1.0).abs() < 1e-9);
432 assert!((Emu(12_700).pt() - 1.0).abs() < 1e-9);
433 }
434
435 /// RGBColor 基本转换测试。
436 #[test]
437 fn rgb() {
438 let c: RGBColor = (10, 20, 30).into();
439 assert_eq!(c, RGBColor(10, 20, 30));
440 }
441}