Skip to main content

easyofd_core/model/
image_object.rs

1//! 图片对象。
2
3use crate::model::image_format::ImageFormat;
4
5/// 带位置和尺寸的图片对象。
6#[derive(Debug, Clone)]
7pub struct ImageObject {
8    /// 距左边缘的 X 位置(mm)。
9    pub x: f64,
10    /// 距顶部的 Y 位置(mm)。
11    pub y: f64,
12    /// 宽度(mm)。
13    pub width: f64,
14    /// 高度(mm)。
15    pub height: f64,
16    /// 图片数据(原始字节)。
17    pub data: Vec<u8>,
18    /// 图片格式。
19    pub format: ImageFormat,
20    /// 原始资源路径(相对文档目录,如 `"Res/qrcode.png"`)。
21    ///
22    /// 读取 OFD 文件时保留,写入器优先使用该路径而非自动命名,
23    /// 从而在 roundtrip 时保持图片资源名与原始文件一致。
24    pub res_name: Option<String>,
25}
26
27impl ImageObject {
28    /// 创建新的图片对象。
29    #[must_use]
30    pub fn new(
31        x: f64,
32        y: f64,
33        width: f64,
34        height: f64,
35        data: Vec<u8>,
36        format: ImageFormat,
37    ) -> Self {
38        Self {
39            x,
40            y,
41            width,
42            height,
43            data,
44            format,
45            res_name: None,
46        }
47    }
48
49    /// 设置原始资源路径(roundtrip 保留图片名时使用)。
50    #[must_use]
51    pub fn with_res_name(mut self, res_name: impl Into<String>) -> Self {
52        self.res_name = Some(res_name.into());
53        self
54    }
55
56    /// 创建 JPEG 图片对象。
57    #[must_use]
58    pub fn jpeg(x: f64, y: f64, width: f64, height: f64, data: Vec<u8>) -> Self {
59        Self::new(x, y, width, height, data, ImageFormat::Jpeg)
60    }
61
62    /// 创建 PNG 图片对象。
63    #[must_use]
64    pub fn png(x: f64, y: f64, width: f64, height: f64, data: Vec<u8>) -> Self {
65        Self::new(x, y, width, height, data, ImageFormat::Png)
66    }
67
68    /// 从文件路径创建图片对象,自动检测格式。
69    ///
70    /// 从以下信息检测格式:
71    /// - 文件扩展名(.jpg/.jpeg → Jpeg, .png → Png, .bmp → Bmp, .tiff/.tif → Tiff)
72    /// - 魔术字节(扩展名不明确时的回退方案)
73    ///
74    /// # 错误
75    ///
76    /// 文件无法读取或格式不支持时返回错误。
77    pub fn from_file(
78        x: f64,
79        y: f64,
80        width: f64,
81        height: f64,
82        path: impl AsRef<std::path::Path>,
83    ) -> crate::OfdResult<Self> {
84        let data = std::fs::read(path.as_ref()).map_err(crate::OfdError::Io)?;
85        let format = detect_image_format(path.as_ref(), &data);
86        Ok(Self::new(x, y, width, height, data, format))
87    }
88}
89
90/// 从文件扩展名和/或魔术字节检测图片格式。
91fn detect_image_format(path: &std::path::Path, data: &[u8]) -> ImageFormat {
92    // 首先检查扩展名
93    if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
94        match ext.to_lowercase().as_str() {
95            "jpg" | "jpeg" => return ImageFormat::Jpeg,
96            "png" => return ImageFormat::Png,
97            "bmp" => return ImageFormat::Bmp,
98            "tiff" | "tif" => return ImageFormat::Tiff,
99            _ => {}
100        }
101    }
102    // 回退: 魔术字节
103    if data.len() >= 2 && data[0] == 0xFF && data[1] == 0xD8 {
104        return ImageFormat::Jpeg;
105    }
106    if data.len() >= 4 && data[0] == 0x89 && data[1] == b'P' && data[2] == b'N' && data[3] == b'G' {
107        return ImageFormat::Png;
108    }
109    if data.len() >= 2 && data[0] == b'B' && data[1] == b'M' {
110        return ImageFormat::Bmp;
111    }
112    if data.len() >= 4
113        && ((data[0] == b'I' && data[1] == b'I') || (data[0] == b'M' && data[1] == b'M'))
114        && data[2] == 0x00
115        && data[3] == 0x2A
116    {
117        return ImageFormat::Tiff;
118    }
119    // 默认 JPEG
120    ImageFormat::Jpeg
121}