Skip to main content

auto_thumbnail/decode/
mod.rs

1//! 通用图像解码:ImageReader → ICO 宽松 → 色调映射。
2//!
3//! 不含平台 Shell/GDI;应用层(cherry-box)负责最终兜底。
4
5pub(crate) mod ffmpeg_decode;
6mod ffmpeg_image;
7pub(crate) mod ffmpeg_log;
8pub(crate) mod ffmpeg_probe;
9mod ico;
10mod j2k;
11mod jxl;
12mod mng;
13mod reader;
14pub mod tone_map;
15
16use std::path::Path;
17use std::sync::Once;
18
19use image::DynamicImage;
20use thiserror::Error;
21
22static REGISTER_EXTRAS: Once = Once::new();
23
24fn ensure_extras_registered() {
25    REGISTER_EXTRAS.call_once(|| {
26        image_extras::register();
27    });
28}
29
30#[derive(Error, Debug)]
31pub enum DecodeError {
32    #[error("无法解码图像")]
33    Unsupported,
34}
35
36/// 尽力解码任意支持的图像文件
37pub fn decode_image(path: &Path) -> Result<DynamicImage, DecodeError> {
38    ensure_extras_registered();
39    #[cfg(feature = "video")]
40    ffmpeg_log::init_ffmpeg_logging();
41    reader::try_decode_reader(path)
42        .or_else(|| jxl::try_decode_jxl(path))
43        .or_else(|| j2k::try_decode_j2k(path))
44        .or_else(|| j2k::try_decode_j2k_rgba(path))
45        .or_else(|| j2k::try_decode_j2k_via_ffmpeg(path))
46        .or_else(|| ico::try_decode_ico(path))
47        .or_else(|| mng::try_decode_mng(path))
48        .or_else(|| ffmpeg_image::try_decode_via_ffmpeg(path))
49        .ok_or(DecodeError::Unsupported)
50}
51
52/// 解码并缩放到 max_dim 边长内
53pub fn decode_and_thumbnail(path: &Path, max_dim: u32) -> Result<DynamicImage, DecodeError> {
54    let img = decode_image(path)?;
55    Ok(if img.width() > max_dim || img.height() > max_dim {
56        img.thumbnail(max_dim, max_dim)
57    } else {
58        img
59    })
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use std::path::Path;
66
67    #[test]
68    fn decode_reported_hdr_samples() {
69        let samples = [
70            r"D:\xsmspace\data\1\file\图像\photo_hdr.hdr",
71            r"D:\xsmspace\data\2\file\图像\sample.hdr",
72        ];
73        for sample in samples {
74            let path = Path::new(sample);
75            if !path.exists() {
76                eprintln!("跳过不存在样本: {sample}");
77                continue;
78            }
79            let img = decode_image(path).unwrap_or_else(|_| panic!("无法解码 HDR: {sample}"));
80            assert!(img.width() > 0 && img.height() > 0);
81        }
82    }
83
84    /// ravif 暂不支持 HDR AVIF(10-bit);此类样本由应用层 Shell 兜底
85    #[test]
86    fn decode_reported_avif_best_effort() {
87        let samples = [
88            r"D:\xsmspace\data\2\_clone_sample-files\images\sample.avif",
89            r"D:\xsmspace\data\1\file\图像\hdr_cosmos.avif",
90            r"D:\xsmspace\data\2\file\图像\sample.avif",
91        ];
92        for sample in samples {
93            let path = Path::new(sample);
94            if !path.exists() {
95                continue;
96            }
97            match decode_image(path) {
98                Ok(img) => assert!(img.width() > 0 && img.height() > 0),
99                Err(_) => eprintln!("AVIF 纯 Rust 解码跳过(可能为 HDR AVIF): {sample}"),
100            }
101        }
102    }
103
104    /// header-only JP2 与 raw codestream FFmpeg 兜底
105    #[test]
106    fn decode_j2k_ffmpeg_fallback_samples() {
107        let samples = [
108            r"D:\xsmspace\data\1\file\图像\d2_colr.j2c",
109            r"D:\xsmspace\data\1\file\图像\imagery.jpc",
110            r"D:\xsmspace\data\1\file\图像\balloon.jp2",
111        ];
112        for sample in samples {
113            let path = Path::new(sample);
114            if !path.exists() {
115                eprintln!("跳过不存在: {sample}");
116                continue;
117            }
118            let img = decode_image(path).unwrap_or_else(|_| panic!("必须能解码: {sample}"));
119            assert!(img.width() > 0 && img.height() > 0, "{sample}");
120            eprintln!("OK: {sample} {}x{}", img.width(), img.height());
121        }
122    }
123
124    #[test]
125    fn decode_logged_failure_samples_best_effort() {
126        let required = [r"D:\xsmspace\data\1\file\图像\animated.mng"];
127        for sample in required {
128            let path = Path::new(sample);
129            if !path.exists() {
130                eprintln!("跳过不存在样本: {sample}");
131                continue;
132            }
133            let img = decode_image(path).unwrap_or_else(|_| panic!("必须能解码: {sample}"));
134            assert!(img.width() > 0 && img.height() > 0, "{sample}");
135        }
136
137        let samples = [
138            r"D:\xsmspace\data\1\file\图像\BLOOD02.pcx",
139            r"D:\xsmspace\data\1\file\图像\balloon.jp2",
140            r"D:\xsmspace\data\1\file\图像\imagery.jpc",
141            r"D:\xsmspace\data\1\file\图像\cropped_16bit.j2k",
142            r"D:\xsmspace\data\1\file\图像\d2_colr.j2c",
143            r"D:\xsmspace\data\1\file\图像\balloon.jpf",
144            r"D:\xsmspace\data\1\file\图像\deerstalker.cur",
145            r"D:\xsmspace\data\1\file\图像\hdr_cosmos.avif",
146            r"D:\xsmspace\data\1\file\图像\g4-multi.tiff",
147            r"D:\xsmspace\data\1\file\图像\dscf0013.tif",
148        ];
149        let mut ok_count = 0;
150        for sample in samples {
151            let path = Path::new(sample);
152            if !path.exists() {
153                continue;
154            }
155            if decode_image(path).is_ok() {
156                ok_count += 1;
157            }
158        }
159        assert!(
160            ok_count >= 8,
161            "auto-thumbnail 解码样本成功率过低: {ok_count}/{}",
162            samples.len()
163        );
164    }
165}