Skip to main content

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}