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#[doc(hidden)]
35pub mod frame_tap;
36#[cfg(feature = "ico")]
37#[doc(hidden)]
38pub mod ico_runner;
39#[doc(hidden)]
40pub mod image_sink;
41#[cfg(feature = "mesh3d")]
42#[doc(hidden)]
43pub mod mesh3d_render;
44#[cfg(feature = "mesh3d")]
45#[doc(hidden)]
46pub mod mesh3d_runner;
47pub mod op;
48#[doc(hidden)]
49pub mod pdf_runner;
50#[doc(hidden)]
51pub mod ping;
52#[doc(hidden)]
53pub mod pixel_xform;
54pub mod plan_to_job;
55#[doc(hidden)]
56pub mod probe;
57#[doc(hidden)]
58pub mod raster_io;
59pub mod route;
60#[doc(hidden)]
61pub mod suggest;
62
63/// `OXIDEAV_CONVERT_TIMING=1` stage spans for the pipeline route.
64mod timing {
65    use std::time::Duration;
66
67    /// True when `OXIDEAV_CONVERT_TIMING` is set to a non-empty value
68    /// other than `0`.
69    pub fn enabled() -> bool {
70        std::env::var_os("OXIDEAV_CONVERT_TIMING").is_some_and(|v| !v.is_empty() && v != "0")
71    }
72
73    /// Print one `convert: timing: <stage> <ms>` line per span.
74    pub fn report(spans: &[(&str, Duration)]) {
75        for (stage, d) in spans {
76            eprintln!(
77                "convert: timing: {stage:<44} {:>9.1} ms",
78                d.as_secs_f64() * 1000.0
79            );
80        }
81    }
82}
83
84pub use op::{AlphaOp, ConvertPlan, Dither, Op, PrintfTemplate, ResizeMode};
85pub use route::Route;
86
87use oxideav_core::{Error, RuntimeContext};
88
89/// Run convert with a caller-supplied [`RuntimeContext`].
90///
91/// The CLI passes the same context produced by `oxideav::with_all_features()`
92/// it uses for `remux` / `transcode` / `run`; third-party embedders can
93/// pass a narrower set.
94pub fn run(args: &[String], ctx: &RuntimeContext) -> Result<(), Error> {
95    let plan = args::parse(args)?;
96
97    // All routing decisions live in `route::decide` (pure, matrix-
98    // tested); this function only performs the dispatched work. See
99    // the `route` module docs for the precedence order and why input
100    // classification outranks output classification.
101    match route::decide(&plan)? {
102        // `--help`: print the usage synopsis (generated from the
103        // parser's own flag table) and do nothing else.
104        Route::Help => {
105            print!("{}", args::usage());
106            Ok(())
107        }
108
109        // `-ping`: one IM-format header line per "image" (page /
110        // video stream) to stdout, no pixel decode, no output write.
111        // The ping module owns the PDF-vs-container split for itself.
112        Route::Ping => ping::run(&plan, ctx),
113
114        // `--probe`: decode the input far enough to extract metadata
115        // (page count, mesh count, sample rate, …), print a summary
116        // to stdout, and skip any output write. The args parser
117        // already enforced the "no output positional" rule, so the
118        // probe module only deals with one shape (input-only).
119        Route::Probe => probe::run(&plan, ctx),
120
121        // PDF inputs go through a Scene-aware runner that bypasses
122        // the regular FrameSource pipeline. PDF pages don't fit the
123        // `Frame::Video` shape `oxideav-pipeline` expects, and the
124        // routing rule (encoder accepts Scene → pass through;
125        // otherwise fan out per page when the filename has a `%d`
126        // template) is specific to `convert`. See `pdf_runner`.
127        Route::PdfSideChannel => pdf_runner::run(&plan),
128
129        // 3D-asset same-class round-trip: decode → re-encode through
130        // the `Mesh3DRegistry`. 3D scenes don't fit any of the
131        // codec/container shapes the regular pipeline walks. See
132        // `mesh3d_runner` module docs.
133        #[cfg(feature = "mesh3d")]
134        Route::Mesh3dToMesh3d => {
135            if !plan.ops.is_empty() {
136                eprintln!(
137                    "convert: note: raster ops ignored on 3D-asset conversion (stl/obj/gltf/glb/mtl/usdz have no pixel grid)"
138                );
139            }
140            mesh3d_runner::run(&plan.input, &plan.output, &plan.mesh3d_options)
141        }
142
143        // 3D-asset input paired with a raster output: software-render
144        // the scene to RGBA, then apply raster ops and encode. See
145        // `mesh3d_render` module docs.
146        #[cfg(feature = "mesh3d")]
147        Route::Mesh3dToRaster => {
148            mesh3d_render::run(&plan.input, &plan.output, &plan.ops, &plan.mesh3d_options)
149        }
150
151        // `.ico` output: the ICO container carries one OR more
152        // sub-images at different resolutions, driven on the IM CLI
153        // by `-define icon:auto-resize=W1,W2,…`. The regular pipeline
154        // path's "one Frame::Video per track" shape doesn't fit that
155        // multi-image fan-out. See `ico_runner` module docs.
156        #[cfg(feature = "ico")]
157        Route::IcoOutput => ico_runner::run(&plan),
158
159        Route::Pipeline => {
160            let timing = timing::enabled();
161            let t0 = std::time::Instant::now();
162            let planned = plan_to_job::plan_pipeline(&plan, ctx)?;
163            let t_plan = t0.elapsed();
164            for note in &planned.notes {
165                eprintln!("{note}");
166            }
167            match planned.tap {
168                None => {
169                    let t1 = std::time::Instant::now();
170                    let stats = oxideav_pipeline::Executor::new(&planned.job, ctx).run()?;
171                    eprintln!(
172                        "convert: {} packet(s) read, {} frame(s) decoded, {} frame(s) written",
173                        stats.packets_read, stats.frames_decoded, stats.frames_written
174                    );
175                    if timing {
176                        timing::report(&[
177                            ("plan (incl. input header probe)", t_plan),
178                            ("pipeline (demux+decode+convert+encode+mux)", t1.elapsed()),
179                            ("total", t0.elapsed()),
180                        ]);
181                    }
182                }
183                Some(tap) => {
184                    let t1 = std::time::Instant::now();
185                    let (stats, written) = frame_tap::run(&planned.job, ctx, &tap)?;
186                    eprintln!(
187                        "convert: {} packet(s) read, {} frame(s) decoded, {} file(s) written",
188                        stats.packets_read, stats.frames_decoded, written.files
189                    );
190                    if timing {
191                        let run = t1.elapsed();
192                        let first = written.first_frame.unwrap_or_default();
193                        timing::report(&[
194                            ("plan (incl. input header probe)", t_plan),
195                            ("first frame (demux+decode+filters+convert)", first),
196                            ("encode", written.encode),
197                            ("mux + write", written.mux),
198                            ("run", run),
199                            ("total", t0.elapsed()),
200                        ]);
201                    }
202                }
203            }
204            Ok(())
205        }
206    }
207}