use std::fs;
use oxideav_core::{Error, PixelFormat, Result, RuntimeContext, SourceOutput};
use oxideav_pdf::read_pdf_to_scene;
use crate::op::ConvertPlan;
use crate::pdf_runner::is_pdf_input;
pub fn run(plan: &ConvertPlan, ctx: &RuntimeContext) -> Result<()> {
if is_pdf_input(&plan.input) {
return ping_pdf(plan);
}
ping_container(plan, ctx)
}
fn ping_pdf(plan: &ConvertPlan) -> Result<()> {
let bytes = fs::read(&plan.input)
.map_err(|e| Error::invalid(format!("convert: failed to read {}: {e}", plan.input)))?;
let file_size = bytes.len();
let scene = read_pdf_to_scene(&bytes)
.map_err(|e| Error::invalid(format!("convert: failed to parse PDF: {e:?}")))?;
let pages = scene.pages.as_deref().unwrap_or(&[]);
if pages.is_empty() {
return Err(Error::invalid("convert: PDF has no pages to ping"));
}
let indices: Vec<usize> = match &plan.input_pages {
Some(sel) => sel
.resolve(pages.len())
.map_err(|e| Error::invalid(format!("convert: input '{}': {e}", plan.input)))?,
None => (0..pages.len()).collect(),
};
for idx in indices {
let page = &pages[idx];
let w = page.width.round() as u32;
let h = page.height.round() as u32;
println!(
"{}[{}] PDF {w}x{h} {w}x{h}+0+0 8-bit sRGB {}B",
plan.input, idx, file_size
);
}
Ok(())
}
fn ping_container(plan: &ConvertPlan, ctx: &RuntimeContext) -> Result<()> {
let raw = match ctx.sources.open(&plan.input)? {
SourceOutput::Bytes(b) => b,
SourceOutput::Packets(_) => {
return Err(Error::unsupported(format!(
"convert -ping: {}: packet-shape source not supported",
plan.input
)));
}
SourceOutput::Frames(_) => {
return Err(Error::unsupported(format!(
"convert -ping: {}: frame-shape source not supported",
plan.input
)));
}
SourceOutput::MultiTitle(_) => {
return Err(Error::unsupported(format!(
"convert -ping: {}: multi-title sources are not supported",
plan.input
)));
}
#[allow(unreachable_patterns)]
_ => {
return Err(Error::unsupported(format!(
"convert -ping: {}: source kind not supported by ping",
plan.input
)));
}
};
let file_size = fs::metadata(&plan.input).map(|m| m.len()).unwrap_or(0);
let mut handle: Box<dyn oxideav_core::ReadSeek> = Box::new(raw);
let ext = ext_from_uri(&plan.input);
let format = ctx
.containers
.probe_input(&mut *handle, ext.as_deref())
.map_err(|e| Error::invalid(format!("convert -ping: {}: {e}", plan.input)))?;
let demuxer = ctx
.containers
.open_demuxer(&format, handle, &ctx.codecs)
.map_err(|e| Error::invalid(format!("convert -ping: {}: {e}", plan.input)))?;
let format_label = format_label_for(&format, demuxer.format_name());
let video_streams: Vec<_> = demuxer
.streams()
.iter()
.filter(|s| matches!(s.params.media_type, oxideav_core::MediaType::Video))
.collect();
if video_streams.is_empty() {
eprintln!(
"convert -ping: {}: no video stream — emitting 0x0 line",
plan.input
);
println!("{} {format_label} 0x0 0x0+0+0 - {file_size}B", plan.input);
return Ok(());
}
for (i, s) in video_streams.iter().enumerate() {
let p = &s.params;
let w = p.width.unwrap_or(0);
let h = p.height.unwrap_or(0);
let (depth, colorspace) = match p.pixel_format {
Some(pf) => describe_pixel_format(pf),
None => (0, "Unknown"),
};
let depth_field = if depth == 0 {
String::from("?-bit")
} else {
format!("{depth}-bit")
};
let label = if video_streams.len() == 1 {
plan.input.clone()
} else {
format!("{}[{i}]", plan.input)
};
println!(
"{label} {format_label} {w}x{h} {w}x{h}+0+0 {depth_field} {colorspace} {file_size}B"
);
}
Ok(())
}
fn describe_pixel_format(pf: PixelFormat) -> (u8, &'static str) {
use PixelFormat::*;
match pf {
Rgb24 | Rgba | Bgr24 | Bgra | Argb | Abgr => (8, "sRGB"),
Rgb48Le | Rgba64Le => (16, "sRGB"),
Yuv420P | Yuv422P | Yuv444P | YuvJ420P | YuvJ422P | YuvJ444P | Nv12 | Nv21 | Yuyv422
| Uyvy422 | Yuva420P => (8, "sRGB"),
Yuv420P10Le | Yuv422P10Le | Yuv444P10Le => (10, "sRGB"),
Yuv420P12Le | Yuv422P12Le | Yuv444P12Le => (12, "sRGB"),
Gray8 | Ya8 => (8, "Gray"),
Gray10Le => (10, "Gray"),
Gray12Le => (12, "Gray"),
Gray16Le => (16, "Gray"),
MonoBlack | MonoWhite => (1, "Gray"),
Pal8 => (8, "sRGB"),
Cmyk => (8, "CMYK"),
_ => (0, "Unknown"),
}
}
fn format_label_for(format: &str, format_name: &str) -> String {
let chosen = if !format_name.is_empty() {
format_name
} else {
format
};
chosen.to_ascii_uppercase()
}
fn ext_from_uri(uri: &str) -> Option<String> {
let last = uri.rsplit('/').next().unwrap_or(uri);
let last = last.split('?').next().unwrap_or(last);
let dot = last.rfind('.')?;
Some(last[dot + 1..].to_ascii_lowercase())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_label_uppercase() {
assert_eq!(format_label_for("png", "png"), "PNG");
assert_eq!(format_label_for("png", ""), "PNG");
assert_eq!(format_label_for("matroska", "MKV"), "MKV");
}
#[test]
fn ext_hint() {
assert_eq!(ext_from_uri("/tmp/a.png").as_deref(), Some("png"));
assert_eq!(
ext_from_uri("https://x/y.JPG?cache=1").as_deref(),
Some("jpg")
);
assert_eq!(ext_from_uri("noext").as_deref(), None);
}
#[test]
fn pixel_format_mapping_covers_common_cases() {
assert_eq!(describe_pixel_format(PixelFormat::Rgb24), (8, "sRGB"));
assert_eq!(describe_pixel_format(PixelFormat::Rgba64Le), (16, "sRGB"));
assert_eq!(describe_pixel_format(PixelFormat::Gray8), (8, "Gray"));
assert_eq!(describe_pixel_format(PixelFormat::Gray16Le), (16, "Gray"));
assert_eq!(
describe_pixel_format(PixelFormat::Yuv420P10Le),
(10, "sRGB")
);
assert_eq!(describe_pixel_format(PixelFormat::Cmyk), (8, "CMYK"));
assert_eq!(describe_pixel_format(PixelFormat::MonoBlack), (1, "Gray"));
}
}