Skip to main content

easypdf_core/model/
image_data.rs

1//! PDF 图片元数据与格式。
2
3/// 从 PDF 中识别的图片信息。
4///
5/// `bytes` 为 `None` 时表示仅存在引用(未提取原始数据)。
6///
7/// # Examples
8///
9/// ```
10/// use easypdf_core::{ImageData, ImageFormat};
11///
12/// let data = ImageData::new(ImageFormat::Png)
13///     .with_caption("Logo")
14///     .with_dimensions(200.0, 100.0);
15/// assert_eq!(data.format(), ImageFormat::Png);
16/// assert_eq!(data.width_pt(), Some(200.0));
17/// ```
18#[derive(Clone, Debug, PartialEq)]
19pub struct ImageData {
20    /// 图片格式。
21    format: ImageFormat,
22    /// 图片原始字节;`None` 表示未提取。
23    bytes: Option<Vec<u8>>,
24    /// 图片标题。
25    caption: Option<String>,
26    /// 替代文本。
27    alt_text: Option<String>,
28    /// 宽度(PDF points)。
29    width_pt: Option<f64>,
30    /// 高度(PDF points)。
31    height_pt: Option<f64>,
32}
33
34impl ImageData {
35    /// 创建指定格式的图片数据。
36    #[must_use]
37    pub const fn new(format: ImageFormat) -> Self {
38        Self {
39            format,
40            bytes: None,
41            caption: None,
42            alt_text: None,
43            width_pt: None,
44            height_pt: None,
45        }
46    }
47
48    /// 设置原始字节。
49    #[must_use]
50    pub fn with_bytes(mut self, bytes: Vec<u8>) -> Self {
51        self.bytes = Some(bytes);
52        self
53    }
54
55    /// 设置标题。
56    #[must_use]
57    pub fn with_caption(mut self, caption: impl Into<String>) -> Self {
58        self.caption = Some(caption.into());
59        self
60    }
61
62    /// 设置替代文本。
63    #[must_use]
64    pub fn with_alt_text(mut self, alt: impl Into<String>) -> Self {
65        self.alt_text = Some(alt.into());
66        self
67    }
68
69    /// 设置图片尺寸(PDF points)。
70    #[must_use]
71    pub const fn with_dimensions(mut self, width_pt: f64, height_pt: f64) -> Self {
72        self.width_pt = Some(width_pt);
73        self.height_pt = Some(height_pt);
74        self
75    }
76
77    /// 返回图片格式。
78    #[must_use]
79    pub const fn format(&self) -> ImageFormat {
80        self.format
81    }
82
83    /// 返回原始字节。
84    #[must_use]
85    pub fn bytes(&self) -> Option<&[u8]> {
86        self.bytes.as_deref()
87    }
88
89    /// 返回标题。
90    #[must_use]
91    pub fn caption(&self) -> Option<&str> {
92        self.caption.as_deref()
93    }
94
95    /// 返回替代文本。
96    #[must_use]
97    pub fn alt_text(&self) -> Option<&str> {
98        self.alt_text.as_deref()
99    }
100
101    /// 返回宽度(PDF points)。
102    #[must_use]
103    pub const fn width_pt(&self) -> Option<f64> {
104        self.width_pt
105    }
106
107    /// 返回高度(PDF points)。
108    #[must_use]
109    pub const fn height_pt(&self) -> Option<f64> {
110        self.height_pt
111    }
112}
113
114/// 图片格式枚举。
115#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
116#[non_exhaustive]
117pub enum ImageFormat {
118    /// PNG 格式。
119    Png,
120    /// JPEG 格式。
121    Jpeg,
122    /// GIF 格式。
123    Gif,
124    /// BMP 格式。
125    Bmp,
126    /// TIFF 格式。
127    Tiff,
128    /// SVG 格式。
129    Svg,
130    /// 未知格式。
131    Unknown,
132}
133
134impl ImageFormat {
135    /// 返回格式的典型文件扩展名。
136    #[must_use]
137    pub const fn extension(self) -> &'static str {
138        match self {
139            Self::Png => "png",
140            Self::Jpeg => "jpg",
141            Self::Gif => "gif",
142            Self::Bmp => "bmp",
143            Self::Tiff => "tiff",
144            Self::Svg => "svg",
145            Self::Unknown => "bin",
146        }
147    }
148}
149
150#[cfg(test)]
151#[allow(clippy::uninlined_format_args, clippy::float_cmp)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn new_creates_with_format_only() {
157        let data = ImageData::new(ImageFormat::Png);
158        assert_eq!(data.format(), ImageFormat::Png);
159        assert!(data.bytes().is_none());
160        assert!(data.caption().is_none());
161        assert!(data.alt_text().is_none());
162        assert!(data.width_pt().is_none());
163        assert!(data.height_pt().is_none());
164    }
165
166    #[test]
167    fn with_bytes_sets_bytes() {
168        let data = ImageData::new(ImageFormat::Jpeg).with_bytes(vec![0xFF, 0xD8, 0xFF]);
169        assert_eq!(data.bytes(), Some([0xFF, 0xD8, 0xFF].as_slice()));
170    }
171
172    #[test]
173    fn with_caption_sets_caption() {
174        let data = ImageData::new(ImageFormat::Png).with_caption("Logo");
175        assert_eq!(data.caption(), Some("Logo"));
176    }
177
178    #[test]
179    fn with_alt_text_sets_alt_text() {
180        let data = ImageData::new(ImageFormat::Gif).with_alt_text("A cat");
181        assert_eq!(data.alt_text(), Some("A cat"));
182    }
183
184    #[test]
185    fn with_dimensions_sets_both() {
186        let data = ImageData::new(ImageFormat::Bmp).with_dimensions(200.0, 100.0);
187        assert_eq!(data.width_pt(), Some(200.0));
188        assert_eq!(data.height_pt(), Some(100.0));
189    }
190
191    #[test]
192    fn builder_chaining_sets_all_fields() {
193        let data = ImageData::new(ImageFormat::Svg)
194            .with_bytes(vec![1, 2, 3])
195            .with_caption("SVG Icon")
196            .with_alt_text("icon")
197            .with_dimensions(64.0, 64.0);
198        assert_eq!(data.format(), ImageFormat::Svg);
199        assert_eq!(data.bytes(), Some([1, 2, 3].as_slice()));
200        assert_eq!(data.caption(), Some("SVG Icon"));
201        assert_eq!(data.alt_text(), Some("icon"));
202        assert_eq!(data.width_pt(), Some(64.0));
203        assert_eq!(data.height_pt(), Some(64.0));
204    }
205
206    #[test]
207    fn clone_preserves_all_fields() {
208        let data = ImageData::new(ImageFormat::Tiff)
209            .with_bytes(vec![10, 20])
210            .with_caption("Photo")
211            .with_dimensions(800.0, 600.0);
212        let cloned = data.clone();
213        assert_eq!(data, cloned);
214    }
215
216    #[test]
217    fn partial_eq_works() {
218        let a = ImageData::new(ImageFormat::Png).with_caption("A");
219        let b = ImageData::new(ImageFormat::Png).with_caption("A");
220        let c = ImageData::new(ImageFormat::Png).with_caption("B");
221        assert_eq!(a, b);
222        assert_ne!(a, c);
223    }
224
225    #[test]
226    fn extension_png() {
227        assert_eq!(ImageFormat::Png.extension(), "png");
228    }
229
230    #[test]
231    fn extension_jpeg() {
232        assert_eq!(ImageFormat::Jpeg.extension(), "jpg");
233    }
234
235    #[test]
236    fn extension_gif() {
237        assert_eq!(ImageFormat::Gif.extension(), "gif");
238    }
239
240    #[test]
241    fn extension_bmp() {
242        assert_eq!(ImageFormat::Bmp.extension(), "bmp");
243    }
244
245    #[test]
246    fn extension_tiff() {
247        assert_eq!(ImageFormat::Tiff.extension(), "tiff");
248    }
249
250    #[test]
251    fn extension_svg() {
252        assert_eq!(ImageFormat::Svg.extension(), "svg");
253    }
254
255    #[test]
256    fn extension_unknown() {
257        assert_eq!(ImageFormat::Unknown.extension(), "bin");
258    }
259
260    #[test]
261    fn image_format_eq() {
262        assert_eq!(ImageFormat::Png, ImageFormat::Png);
263        assert_ne!(ImageFormat::Png, ImageFormat::Jpeg);
264    }
265}