Skip to main content

oxideav_cli_convert/
lib.rs

1//! `oxideav convert` engine.
2//!
3//! Translates ImageMagick-style arg syntax into an in-memory
4//! [`oxideav_pipeline::Job`] and runs it through the same executor
5//! the `oxideav run` subcommand uses. One code path serves image,
6//! video, and audio inputs — a PNG → JPG is just a 1-frame stream.
7//!
8//! Entry point: [`run`] takes the slice of arguments that come after
9//! `oxideav convert` plus the caller-supplied registries, and does
10//! everything.
11//!
12//! # Example
13//!
14//! ```no_run
15//! use oxideav_core::RuntimeContext;
16//!
17//! let mut ctx = RuntimeContext::new();
18//! oxideav_source::register(&mut ctx);
19//!
20//! // Equivalent to:
21//! //   oxideav convert in.png -resize 800x600 out.jpg
22//! oxideav_cli_convert::run(
23//!     &[
24//!         "in.png".into(),
25//!         "-resize".into(),
26//!         "800x600".into(),
27//!         "out.jpg".into(),
28//!     ],
29//!     &ctx,
30//! ).unwrap();
31//! ```
32
33pub mod args;
34#[cfg(feature = "ico")]
35pub mod ico_runner;
36#[cfg(feature = "mesh3d")]
37pub mod mesh3d_render;
38#[cfg(feature = "mesh3d")]
39pub mod mesh3d_runner;
40pub mod op;
41pub mod pdf_runner;
42pub mod ping;
43pub mod pixel_xform;
44pub mod plan_to_job;
45pub mod probe;
46pub mod raster_io;
47pub mod suggest;
48
49pub use op::{AlphaOp, ConvertPlan, Dither, Op, PrintfTemplate, ResizeMode};
50
51use oxideav_core::{Error, RuntimeContext};
52
53/// Run convert with a caller-supplied [`RuntimeContext`].
54///
55/// The CLI passes the same context produced by `oxideav::with_all_features()`
56/// it uses for `remux` / `transcode` / `run`; third-party embedders can
57/// pass a narrower set.
58pub fn run(args: &[String], ctx: &RuntimeContext) -> Result<(), Error> {
59    let plan = args::parse(args)?;
60
61    // `-ping` short-circuits everything: print one IM-format header
62    // line per "image" (page / video stream) to stdout and exit
63    // without any pixel decode or output write. Routed BEFORE the PDF
64    // side-channel so the ping module owns the PDF-vs-container split
65    // for itself.
66    if plan.ping {
67        return ping::run(&plan, ctx);
68    }
69
70    // `--probe` is the structural-inspection mode: decode the input
71    // far enough to extract metadata (page count, mesh count, sample
72    // rate, …), print a summary to stdout, and skip any output write.
73    // The args parser already enforced the "no output positional"
74    // rule, so the probe module only deals with one shape (input-only).
75    if plan.probe {
76        return probe::run(&plan, ctx);
77    }
78
79    // Side-channel: PDF inputs go through a Scene-aware runner that
80    // bypasses the regular FrameSource pipeline. PDF pages don't fit
81    // the `Frame::Video` shape `oxideav-pipeline` expects, and the
82    // routing rule (encoder accepts Scene → pass through; otherwise
83    // fan out per page when the filename has a `%d` template) is
84    // specific to `convert`. See `pdf_runner` module docs.
85    if pdf_runner::is_pdf_input(&plan.input) {
86        return pdf_runner::run(&plan);
87    }
88
89    // Side-channel: 3D-asset inputs (STL/OBJ/glTF/GLB/USDZ/MTL) go
90    // through a Mesh3DRegistry-driven decode→encode bridge for
91    // same-class round-trips, OR through the 3D→raster software
92    // renderer when paired with a raster output (.png/.jpg/.bmp/
93    // .webp). 3D scenes don't fit any of the codec/container shapes
94    // the regular pipeline walks, and the dispatch contract is a
95    // separate registry from `RuntimeContext`'s codec/container/
96    // filter/source path. See `mesh3d_runner` and `mesh3d_render`
97    // module docs.
98    #[cfg(feature = "mesh3d")]
99    if mesh3d_runner::is_mesh3d_input(&plan.input) {
100        if plan.output_template.is_some() {
101            return Err(Error::invalid(format!(
102                "convert: output '{}' has a `%d` template but 3D scenes are single-document; remove the template",
103                plan.output
104            )));
105        }
106        if mesh3d_runner::is_mesh3d_output(&plan.output) {
107            if !plan.ops.is_empty() {
108                eprintln!(
109                    "convert: note: raster ops ignored on 3D-asset conversion (stl/obj/gltf/glb/mtl/usdz have no pixel grid)"
110                );
111            }
112            return mesh3d_runner::run(&plan.input, &plan.output, &plan.mesh3d_options);
113        }
114        // Raster output? Route to the 3D→raster software renderer.
115        if matches!(
116            raster_io::classify_output(&plan.output),
117            Ok(raster_io::OutputClass::Raster(_))
118        ) {
119            return mesh3d_render::run(&plan.input, &plan.output, &plan.ops, &plan.mesh3d_options);
120        }
121        // Anything else (e.g. .pdf, .svg, .mp4, …): emit the
122        // pairing-mismatch error with a did-you-mean hint over the
123        // 3D-output set.
124        let out_ext = plan.output.rsplit('.').next().unwrap_or("");
125        let hint = suggest::format_hint(out_ext, &["stl", "obj", "gltf", "glb", "mtl", "usdz"]);
126        return Err(Error::invalid(format!(
127            "convert: 3D input '{}' must pair with a 3D output (.stl/.obj/.gltf/.glb/.mtl/.usdz) OR a raster output (.png/.jpg/.bmp/.webp); got '{}'{hint}",
128            plan.input, plan.output
129        )));
130    }
131
132    // Side-channel: `.ico` output. The ICO container carries one OR
133    // more sub-images at different resolutions, driven on the IM CLI
134    // by `-define icon:auto-resize=W1,W2,…`. The regular pipeline
135    // path's "one Frame::Video per track" shape doesn't fit that
136    // multi-image fan-out, and the `-define` key parsing + per-size
137    // resize is convert-specific. See `ico_runner` module docs.
138    #[cfg(feature = "ico")]
139    if ico_runner::is_ico_output(&plan.output) {
140        return ico_runner::run(&plan);
141    }
142
143    let job = plan_to_job::plan_to_job(&plan, ctx)?;
144    let stats = oxideav_pipeline::Executor::new(&job, ctx).run()?;
145    eprintln!(
146        "convert: {} packet(s) read, {} frame(s) decoded, {} frame(s) written",
147        stats.packets_read, stats.frames_decoded, stats.frames_written
148    );
149    Ok(())
150}