Skip to main content

media_decode/
meta.rs

1//! 媒体元数据探测:只读文件头/容器元信息,不解码像素。
2//!
3//! 供索引等批量场景低成本采集"分辨率"与"时长":图片/RAW/PSD 头部毫秒级,
4//! 视频走 ffmpeg 容器探测(数十毫秒)。读不到的格式返回 None,调用方按
5//! "该文件无此属性"处理,不应重试或回退到全量解码。
6
7use std::path::Path;
8
9/// 媒体元数据;字段为 None 表示该文件类型无此属性或读取失败
10#[derive(Debug, Clone, Copy, Default, PartialEq)]
11pub struct MediaMeta {
12    /// 像素宽度(视频为编码分辨率,未处理旋转元数据)
13    pub width: Option<u32>,
14    /// 像素高度
15    pub height: Option<u32>,
16    /// 时长(秒);视频取容器 duration,音频取流属性
17    pub duration_secs: Option<f64>,
18}
19
20impl MediaMeta {
21    /// 宽高是否构成有效的"分辨率"展示值
22    pub fn dimensions(&self) -> Option<(u32, u32)> {
23        match (self.width, self.height) {
24            (Some(w), Some(h)) if w > 0 && h > 0 => Some((w, h)),
25            _ => None,
26        }
27    }
28}
29
30/// 按扩展名分派探测媒体元数据;无法识别的扩展名返回 None
31pub fn probe_media_meta(path: &Path) -> Option<MediaMeta> {
32    let ext = path
33        .extension()
34        .and_then(|e| e.to_str())
35        .map(|s| s.to_ascii_lowercase())?;
36
37    #[cfg(feature = "video")]
38    if crate::mime_resolve::is_video_ext(&ext) {
39        return probe_video(path);
40    }
41
42    #[cfg(feature = "audio")]
43    if crate::mime_resolve::is_audio_ext(&ext) {
44        return probe_audio(path);
45    }
46
47    // PSD/PSB 头部结构相同(仅 version 不同),直接解析文件头
48    if matches!(ext.as_str(), "psd" | "psb") {
49        return probe_psd_header(path);
50    }
51
52    #[cfg(feature = "pdf")]
53    if ext == "ai" {
54        return probe_ai_page_size(path);
55    }
56
57    // RAW 与普通图片统一走头部尺寸读取:TIFF 系 RAW(DNG/CR2/NEF 等)可读,
58    // 私有容器(CRW/X3F 等)读不到即 None;SVG 无固定像素尺寸也会在此返回 None
59    probe_image_dimensions(path)
60}
61
62/// 视频:ffmpeg 容器探测宽高与时长;宽高拿不到时整体视为不可解析
63#[cfg(feature = "video")]
64fn probe_video(path: &Path) -> Option<MediaMeta> {
65    let probe = crate::decode::ffmpeg_probe::probe_options_for_video(path);
66    let (dimensions, duration_secs) = crate::decode::ffmpeg_decode::probe_video_meta(path, probe);
67    let (width, height) = dimensions?;
68    Some(MediaMeta {
69        width: Some(width),
70        height: Some(height),
71        duration_secs,
72    })
73}
74
75/// 音频:lofty 只读标签与流属性即可拿到时长
76#[cfg(feature = "audio")]
77fn probe_audio(path: &Path) -> Option<MediaMeta> {
78    use lofty::file::AudioFile;
79
80    let tagged = lofty::read_from_path(path).ok()?;
81    let duration = tagged.properties().duration();
82    if duration.is_zero() {
83        return None;
84    }
85    Some(MediaMeta {
86        width: None,
87        height: None,
88        duration_secs: Some(duration.as_secs_f64()),
89    })
90}
91
92/// PSD/PSB 文件头:签名 "8BPS",通道数之后紧跟大端 height/width,
93/// 只读 22 字节,比库全量解析便宜几个量级
94fn probe_psd_header(path: &Path) -> Option<MediaMeta> {
95    use std::io::Read;
96
97    let mut file = std::fs::File::open(path).ok()?;
98    let mut header = [0u8; 22];
99    file.read_exact(&mut header).ok()?;
100    if &header[0..4] != b"8BPS" {
101        return None;
102    }
103    // 头部布局:signature(0-3) version(4-5) reserved(6-11) channels(12-13)
104    // height(14-17) width(18-21),均为大端
105    let height = u32::from_be_bytes(header[14..18].try_into().ok()?);
106    let width = u32::from_be_bytes(header[18..22].try_into().ok()?);
107    Some(MediaMeta {
108        width: Some(width),
109        height: Some(height),
110        duration_secs: None,
111    })
112}
113
114/// AI 文件多为 PDF 兼容封装,经 pdfium 读首页画板尺寸;私有二进制打不开即 None
115#[cfg(feature = "pdf")]
116fn probe_ai_page_size(path: &Path) -> Option<MediaMeta> {
117    let (width, height) = crate::thumbs::pdf::probe_page_size(path)?;
118    Some(MediaMeta {
119        width: Some(width),
120        height: Some(height),
121        duration_secs: None,
122    })
123}
124
125/// 只读头部拿像素尺寸;image crate 不认识的格式(含各 RAW 私有容器)返回 None
126fn probe_image_dimensions(path: &Path) -> Option<MediaMeta> {
127    let (width, height) = image::ImageReader::open(path)
128        .ok()?
129        .into_dimensions()
130        .ok()?;
131    Some(MediaMeta {
132        width: Some(width),
133        height: Some(height),
134        duration_secs: None,
135    })
136}