oxideav_cli_convert/
ping.rs1use std::fs;
16
17use oxideav_core::{Error, PixelFormat, Result, RuntimeContext, SourceOutput};
18use oxideav_pdf::read_pdf_to_scene;
19
20use crate::op::ConvertPlan;
21use crate::pdf_runner::is_pdf_input;
22
23pub fn run(plan: &ConvertPlan, ctx: &RuntimeContext) -> Result<()> {
25 if is_pdf_input(&plan.input) {
26 return ping_pdf(plan);
27 }
28 ping_container(plan, ctx)
29}
30
31fn ping_pdf(plan: &ConvertPlan) -> Result<()> {
32 let bytes = fs::read(&plan.input)
33 .map_err(|e| Error::invalid(format!("convert: failed to read {}: {e}", plan.input)))?;
34 let file_size = bytes.len();
35 let scene = read_pdf_to_scene(&bytes)
36 .map_err(|e| Error::invalid(format!("convert: failed to parse PDF: {e:?}")))?;
37 let pages = scene.pages.as_deref().unwrap_or(&[]);
38 if pages.is_empty() {
39 return Err(Error::invalid("convert: PDF has no pages to ping"));
40 }
41 let indices: Vec<usize> = match &plan.input_pages {
42 Some(sel) => sel
43 .resolve(pages.len())
44 .map_err(|e| Error::invalid(format!("convert: input '{}': {e}", plan.input)))?,
45 None => (0..pages.len()).collect(),
46 };
47 for idx in indices {
48 let page = &pages[idx];
49 let w = page.width.round() as u32;
53 let h = page.height.round() as u32;
54 println!(
55 "{}[{}] PDF {w}x{h} {w}x{h}+0+0 8-bit sRGB {}B",
56 plan.input, idx, file_size
57 );
58 }
59 Ok(())
60}
61
62fn ping_container(plan: &ConvertPlan, ctx: &RuntimeContext) -> Result<()> {
63 let raw = match ctx.sources.open(&plan.input)? {
64 SourceOutput::Bytes(b) => b,
65 SourceOutput::Packets(_) => {
66 return Err(Error::unsupported(format!(
67 "convert -ping: {}: packet-shape source not supported",
68 plan.input
69 )));
70 }
71 SourceOutput::Frames(_) => {
72 return Err(Error::unsupported(format!(
73 "convert -ping: {}: frame-shape source not supported",
74 plan.input
75 )));
76 }
77 SourceOutput::MultiTitle(_) => {
81 return Err(Error::unsupported(format!(
82 "convert -ping: {}: multi-title sources are not supported",
83 plan.input
84 )));
85 }
86 #[allow(unreachable_patterns)]
91 _ => {
92 return Err(Error::unsupported(format!(
93 "convert -ping: {}: source kind not supported by ping",
94 plan.input
95 )));
96 }
97 };
98 let file_size = fs::metadata(&plan.input).map(|m| m.len()).unwrap_or(0);
99 let mut handle: Box<dyn oxideav_core::ReadSeek> = Box::new(raw);
101 let ext = ext_from_uri(&plan.input);
102 let format = ctx
103 .containers
104 .probe_input(&mut *handle, ext.as_deref())
105 .map_err(|e| Error::invalid(format!("convert -ping: {}: {e}", plan.input)))?;
106 let demuxer = ctx
107 .containers
108 .open_demuxer(&format, handle, &ctx.codecs)
109 .map_err(|e| Error::invalid(format!("convert -ping: {}: {e}", plan.input)))?;
110
111 let format_label = format_label_for(&format, demuxer.format_name());
112 let video_streams: Vec<_> = demuxer
113 .streams()
114 .iter()
115 .filter(|s| matches!(s.params.media_type, oxideav_core::MediaType::Video))
116 .collect();
117
118 if video_streams.is_empty() {
119 eprintln!(
122 "convert -ping: {}: no video stream — emitting 0x0 line",
123 plan.input
124 );
125 println!("{} {format_label} 0x0 0x0+0+0 - {file_size}B", plan.input);
126 return Ok(());
127 }
128
129 for (i, s) in video_streams.iter().enumerate() {
130 let p = &s.params;
131 let w = p.width.unwrap_or(0);
132 let h = p.height.unwrap_or(0);
133 let (depth, colorspace) = match p.pixel_format {
134 Some(pf) => describe_pixel_format(pf),
135 None => (0, "Unknown"),
136 };
137 let depth_field = if depth == 0 {
138 String::from("?-bit")
139 } else {
140 format!("{depth}-bit")
141 };
142 let label = if video_streams.len() == 1 {
145 plan.input.clone()
146 } else {
147 format!("{}[{i}]", plan.input)
148 };
149 println!(
150 "{label} {format_label} {w}x{h} {w}x{h}+0+0 {depth_field} {colorspace} {file_size}B"
151 );
152 }
153 Ok(())
154}
155
156fn describe_pixel_format(pf: PixelFormat) -> (u8, &'static str) {
160 use PixelFormat::*;
161 match pf {
162 Rgb24 | Rgba | Bgr24 | Bgra | Argb | Abgr => (8, "sRGB"),
164 Rgb48Le | Rgba64Le => (16, "sRGB"),
166 Yuv420P | Yuv422P | Yuv444P | YuvJ420P | YuvJ422P | YuvJ444P | Nv12 | Nv21 | Yuyv422
168 | Uyvy422 | Yuva420P => (8, "sRGB"),
169 Yuv420P10Le | Yuv422P10Le | Yuv444P10Le => (10, "sRGB"),
170 Yuv420P12Le | Yuv422P12Le | Yuv444P12Le => (12, "sRGB"),
171 Gray8 | Ya8 => (8, "Gray"),
173 Gray10Le => (10, "Gray"),
174 Gray12Le => (12, "Gray"),
175 Gray16Le => (16, "Gray"),
176 MonoBlack | MonoWhite => (1, "Gray"),
177 Pal8 => (8, "sRGB"),
179 Cmyk => (8, "CMYK"),
181 _ => (0, "Unknown"),
183 }
184}
185
186fn format_label_for(format: &str, format_name: &str) -> String {
193 let chosen = if !format_name.is_empty() {
194 format_name
195 } else {
196 format
197 };
198 chosen.to_ascii_uppercase()
199}
200
201fn ext_from_uri(uri: &str) -> Option<String> {
205 let last = uri.rsplit('/').next().unwrap_or(uri);
206 let last = last.split('?').next().unwrap_or(last);
207 let dot = last.rfind('.')?;
208 Some(last[dot + 1..].to_ascii_lowercase())
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn format_label_uppercase() {
217 assert_eq!(format_label_for("png", "png"), "PNG");
218 assert_eq!(format_label_for("png", ""), "PNG");
219 assert_eq!(format_label_for("matroska", "MKV"), "MKV");
220 }
221
222 #[test]
223 fn ext_hint() {
224 assert_eq!(ext_from_uri("/tmp/a.png").as_deref(), Some("png"));
225 assert_eq!(
226 ext_from_uri("https://x/y.JPG?cache=1").as_deref(),
227 Some("jpg")
228 );
229 assert_eq!(ext_from_uri("noext").as_deref(), None);
230 }
231
232 #[test]
233 fn pixel_format_mapping_covers_common_cases() {
234 assert_eq!(describe_pixel_format(PixelFormat::Rgb24), (8, "sRGB"));
235 assert_eq!(describe_pixel_format(PixelFormat::Rgba64Le), (16, "sRGB"));
236 assert_eq!(describe_pixel_format(PixelFormat::Gray8), (8, "Gray"));
237 assert_eq!(describe_pixel_format(PixelFormat::Gray16Le), (16, "Gray"));
238 assert_eq!(
239 describe_pixel_format(PixelFormat::Yuv420P10Le),
240 (10, "sRGB")
241 );
242 assert_eq!(describe_pixel_format(PixelFormat::Cmyk), (8, "CMYK"));
243 assert_eq!(describe_pixel_format(PixelFormat::MonoBlack), (1, "Gray"));
244 }
245}