Skip to main content

easyofd_reader/tools/
image_utils.rs

1//! 图片处理工具。
2//!
3//! 对应 Java: org.ofdrw.reader.tools.ImageUtils
4//!
5//! Java 版依赖 Apache PDFBox JBIG2 解码器和 AWT `BufferedImage`。
6//! Rust 版提供纯数据层面的图片处理辅助函数,不依赖图形库。
7
8/// 图片格式。
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum ImageFormat {
11    /// PNG 格式。
12    Png,
13    /// JPEG 格式。
14    Jpeg,
15    /// BMP 格式。
16    Bmp,
17    /// TIFF 格式。
18    Tiff,
19    /// JB2 (JBIG2) 格式。
20    Jb2,
21    /// 未知格式。
22    Unknown,
23}
24
25impl ImageFormat {
26    /// 从文件扩展名推断图片格式。
27    #[must_use]
28    pub fn from_extension(ext: &str) -> Self {
29        match ext.to_lowercase().as_str() {
30            "png" => Self::Png,
31            "jpg" | "jpeg" => Self::Jpeg,
32            "bmp" => Self::Bmp,
33            "tiff" | "tif" => Self::Tiff,
34            "jb2" | "gbig2" => Self::Jb2,
35            _ => Self::Unknown,
36        }
37    }
38
39    /// 从文件路径推断图片格式。
40    #[must_use]
41    pub fn from_path(path: &str) -> Self {
42        let ext = path.rsplit('.').next().unwrap_or("");
43        Self::from_extension(ext)
44    }
45
46    /// 返回格式的 MIME 类型字符串。
47    #[must_use]
48    pub fn mime_type(&self) -> &'static str {
49        match self {
50            Self::Png => "image/png",
51            Self::Jpeg => "image/jpeg",
52            Self::Bmp => "image/bmp",
53            Self::Tiff => "image/tiff",
54            Self::Jb2 => "image/x-jbig2",
55            Self::Unknown => "application/octet-stream",
56        }
57    }
58
59    /// 返回格式的常用文件扩展名。
60    #[must_use]
61    pub fn extension(&self) -> &'static str {
62        match self {
63            Self::Png => "png",
64            Self::Jpeg => "jpeg",
65            Self::Bmp => "bmp",
66            Self::Tiff => "tiff",
67            Self::Jb2 => "jb2",
68            Self::Unknown => "bin",
69        }
70    }
71}
72
73/// 检查图片数据是否具有有效的文件头签名。
74///
75/// 对应 Java: `ImageUtils` 中的格式判断逻辑
76#[must_use]
77pub fn detect_format(data: &[u8]) -> ImageFormat {
78    if data.len() < 4 {
79        return ImageFormat::Unknown;
80    }
81    // PNG: 89 50 4E 47
82    if data.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
83        return ImageFormat::Png;
84    }
85    // JPEG: FF D8 FF
86    if data.starts_with(&[0xFF, 0xD8, 0xFF]) {
87        return ImageFormat::Jpeg;
88    }
89    // BMP: 42 4D
90    if data.starts_with(&[0x42, 0x4D]) {
91        return ImageFormat::Bmp;
92    }
93    // TIFF: 49 49 2A 00 (little-endian) or 4D 4D 00 2A (big-endian)
94    if data.starts_with(&[0x49, 0x49, 0x2A, 0x00]) || data.starts_with(&[0x4D, 0x4D, 0x00, 0x2A]) {
95        return ImageFormat::Tiff;
96    }
97    // JBIG2: 97 4A 42 32
98    if data.starts_with(&[0x97, 0x4A, 0x42, 0x32]) {
99        return ImageFormat::Jb2;
100    }
101    ImageFormat::Unknown
102}
103
104/// 计算灰度值。
105///
106/// 对应 Java: `ImageUtils.gray(int r, int g, int b)`
107#[must_use]
108pub fn gray(r: u32, g: u32, b: u32) -> u32 {
109    (r * 19_595 + g * 38_469 + b * 7_472) >> 16
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_image_format_from_extension() {
118        assert_eq!(ImageFormat::from_extension("png"), ImageFormat::Png);
119        assert_eq!(ImageFormat::from_extension("JPEG"), ImageFormat::Jpeg);
120        assert_eq!(ImageFormat::from_extension("jpg"), ImageFormat::Jpeg);
121        assert_eq!(ImageFormat::from_extension("bmp"), ImageFormat::Bmp);
122        assert_eq!(ImageFormat::from_extension("tiff"), ImageFormat::Tiff);
123        assert_eq!(ImageFormat::from_extension("tif"), ImageFormat::Tiff);
124        assert_eq!(ImageFormat::from_extension("jb2"), ImageFormat::Jb2);
125        assert_eq!(ImageFormat::from_extension("xyz"), ImageFormat::Unknown);
126    }
127
128    #[test]
129    fn test_image_format_from_path() {
130        assert_eq!(
131            ImageFormat::from_path("/doc/res/photo.jpg"),
132            ImageFormat::Jpeg
133        );
134        assert_eq!(ImageFormat::from_path("image.PNG"), ImageFormat::Png);
135    }
136
137    #[test]
138    fn test_image_format_mime_type() {
139        assert_eq!(ImageFormat::Png.mime_type(), "image/png");
140        assert_eq!(ImageFormat::Jpeg.mime_type(), "image/jpeg");
141    }
142
143    #[test]
144    fn test_image_format_extension() {
145        assert_eq!(ImageFormat::Png.extension(), "png");
146        assert_eq!(ImageFormat::Jpeg.extension(), "jpeg");
147    }
148
149    #[test]
150    fn test_detect_format_png() {
151        let data = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
152        assert_eq!(detect_format(&data), ImageFormat::Png);
153    }
154
155    #[test]
156    fn test_detect_format_jpeg() {
157        let data = [0xFF, 0xD8, 0xFF, 0xE0];
158        assert_eq!(detect_format(&data), ImageFormat::Jpeg);
159    }
160
161    #[test]
162    fn test_detect_format_bmp() {
163        let data = [0x42, 0x4D, 0x00, 0x00];
164        assert_eq!(detect_format(&data), ImageFormat::Bmp);
165    }
166
167    #[test]
168    fn test_detect_format_tiff_le() {
169        let data = [0x49, 0x49, 0x2A, 0x00];
170        assert_eq!(detect_format(&data), ImageFormat::Tiff);
171    }
172
173    #[test]
174    fn test_detect_format_tiff_be() {
175        let data = [0x4D, 0x4D, 0x00, 0x2A];
176        assert_eq!(detect_format(&data), ImageFormat::Tiff);
177    }
178
179    #[test]
180    fn test_detect_format_jb2() {
181        let data = [0x97, 0x4A, 0x42, 0x32];
182        assert_eq!(detect_format(&data), ImageFormat::Jb2);
183    }
184
185    #[test]
186    fn test_detect_format_unknown() {
187        let data = [0x00, 0x01, 0x02, 0x03];
188        assert_eq!(detect_format(&data), ImageFormat::Unknown);
189    }
190
191    #[test]
192    fn test_detect_format_short_data() {
193        assert_eq!(detect_format(&[0x89]), ImageFormat::Unknown);
194        assert_eq!(detect_format(&[]), ImageFormat::Unknown);
195    }
196
197    #[test]
198    fn test_gray() {
199        // 纯白应返回 255
200        let white = gray(255, 255, 255);
201        assert_eq!(white, 255);
202        // 纯黑应返回 0
203        let black = gray(0, 0, 0);
204        assert_eq!(black, 0);
205    }
206}