Skip to main content

deepshrink_core/engine/
mod.rs

1//! The compression engine contract and shared types.
2//!
3//! Key architectural decision: `core` is designed around the [`Engine`]
4//! interface even while there is exactly one engine (media/ffmpeg). This lets us
5//! plug in images/PDF/office later without rewriting the skeleton.
6//!
7//! Principle: [`Engine::plan`] is a pure, testable function (bitrate math);
8//! side effects are isolated in [`Engine::run`].
9
10pub mod media;
11
12use std::path::{Path, PathBuf};
13use thiserror::Error;
14
15use crate::detect::MediaKind;
16use crate::options::{AudioChoice, AudioCodec, FpsOpt, QualityPreset, ResolutionOpt, VideoCodec};
17use crate::size::Preset;
18
19/// Source file metadata — the result of [`Engine::probe`].
20#[derive(Debug, Clone, PartialEq)]
21pub struct MediaInfo {
22    pub path: PathBuf,
23    pub kind: MediaKind,
24    pub duration_sec: f64,
25    pub size_bytes: u64,
26    pub width: Option<u32>,
27    pub height: Option<u32>,
28    pub fps: Option<f64>,
29    pub video_codec: Option<String>,
30    pub audio_codec: Option<String>,
31    pub audio_channels: Option<u32>,
32}
33
34impl MediaInfo {
35    /// Whether the source carries an audio track.
36    pub fn has_audio(&self) -> bool {
37        self.audio_codec.is_some()
38    }
39}
40
41/// What the user wants in terms of size.
42#[derive(Debug, Clone, PartialEq)]
43pub enum SizeGoal {
44    /// Fit into an absolute size (bytes).
45    Target(u64),
46    /// Reduce by a fraction of the original (0.70 = "by 70%").
47    Reduce(f64),
48    /// Platform preset (sets the target size).
49    Preset(Preset),
50    /// Smart, quality-preserving shrink without a hard limit.
51    Quality,
52}
53
54/// Compression options passed to the engine — the decoded form of the flags.
55#[derive(Debug, Clone, PartialEq)]
56pub struct ShrinkOpts {
57    pub goal: SizeGoal,
58    pub video_codec: VideoCodec,
59    /// What to do with the audio track *inside a video*.
60    pub audio: AudioChoice,
61    pub resolution: ResolutionOpt,
62    pub fps: FpsOpt,
63    pub quality: QualityPreset,
64    /// Codec for a *pure-audio* input.
65    pub audio_codec: AudioCodec,
66    /// Downmix pure audio to mono.
67    pub mono: bool,
68    /// Force a sample rate (Hz) for pure audio; `None` keeps the source rate.
69    pub sample_rate: Option<u32>,
70    /// Prefer VBR where the codec supports it.
71    pub vbr: bool,
72    /// Target VMAF: in quality mode, search CRF for the smallest output that
73    /// still scores at least this. `None` disables VMAF-aware encoding.
74    pub target_vmaf: Option<f64>,
75    /// Explicit output path; when `None` the engine derives one.
76    pub output: Option<PathBuf>,
77    /// Force two-pass on or off for a size-targeted video encode. `None` lets
78    /// the engine decide (two-pass whenever it encodes to a bitrate budget).
79    /// `Some(true)` is only meaningful in bitrate mode — a CRF encode has no
80    /// budget for a first pass to measure.
81    pub two_pass: Option<bool>,
82    /// Target resolution (dots per inch) for raster images *embedded in a
83    /// document*, measured at the size they are placed on the page. Only the
84    /// document engines act on it — the media engine ignores it, as pixels in a
85    /// video have no physical size.
86    pub dpi: Option<u32>,
87    /// Allow a quality-mode result that is not smaller than the source.
88    /// Off by default: re-encoding an already-compact lossy file only loses
89    /// quality, so the engine keeps the original instead (sample-predicted for
90    /// video, bitrate-compared for audio, and checked after every encode).
91    pub allow_larger: bool,
92}
93
94impl Default for ShrinkOpts {
95    fn default() -> Self {
96        Self {
97            goal: SizeGoal::Quality,
98            video_codec: VideoCodec::H264,
99            audio: AudioChoice::Keep,
100            resolution: ResolutionOpt::Auto,
101            fps: FpsOpt::Auto,
102            quality: QualityPreset::Balanced,
103            audio_codec: AudioCodec::Aac,
104            mono: false,
105            sample_rate: None,
106            vbr: false,
107            target_vmaf: None,
108            output: None,
109            two_pass: None,
110            dpi: None,
111            allow_larger: false,
112        }
113    }
114}
115
116/// Video encoding parameters.
117#[derive(Debug, Clone, PartialEq)]
118pub struct VideoSpec {
119    pub codec: VideoCodec,
120    /// Target average bitrate (bits/s) for two-pass; `None` in CRF/quality mode.
121    pub bitrate_bps: Option<u64>,
122    /// CRF value for quality mode; `None` in bitrate mode.
123    pub crf: Option<u8>,
124    /// Downscale target height; `None` keeps the source resolution.
125    pub height: Option<u32>,
126    /// Frame-rate cap; `None` keeps the source rate.
127    pub fps: Option<u32>,
128    pub preset: QualityPreset,
129}
130
131/// Audio encoding parameters (absent means drop the track).
132#[derive(Debug, Clone, PartialEq)]
133pub struct AudioSpec {
134    pub codec: AudioCodec,
135    pub bitrate_bps: u64,
136    /// Downmix to a single channel.
137    pub mono: bool,
138    /// Resample to this rate (Hz); `None` keeps the source rate.
139    pub sample_rate: Option<u32>,
140    /// Prefer VBR where the codec supports it.
141    pub vbr: bool,
142}
143
144impl AudioSpec {
145    /// A plain CBR/ABR track (used for the audio track inside a video).
146    pub fn cbr(codec: AudioCodec, bitrate_bps: u64) -> Self {
147        Self {
148            codec,
149            bitrate_bps,
150            mono: false,
151            sample_rate: None,
152            vbr: false,
153        }
154    }
155}
156
157/// The full encode recipe.
158#[derive(Debug, Clone, PartialEq)]
159pub struct EncodeSpec {
160    pub video: VideoSpec,
161    /// `None` means `-an` (no audio).
162    pub audio: Option<AudioSpec>,
163    pub faststart: bool,
164    pub two_pass: bool,
165    /// Stream-copy remux only: the source already fits, so never re-encode
166    /// (and never inflate). `video`/`audio` are ignored when set.
167    pub passthrough: bool,
168    /// Pure-audio encode: emit `-vn` and use `audio` only (`video` is ignored).
169    pub audio_only: bool,
170    /// Target DPI for a document's embedded images (see [`ShrinkOpts::dpi`]).
171    /// Document engines only; ignored by the media engine.
172    pub dpi: Option<u32>,
173}
174
175/// The encode plan — the result of [`Engine::plan`]. Usable for `--dry-run`.
176#[derive(Debug, Clone, PartialEq)]
177pub struct EncodePlan {
178    pub input: PathBuf,
179    pub output: PathBuf,
180    /// Human-readable description of the plan (codec, bitrates, passes).
181    pub summary: String,
182    /// Expected final size in bytes, if predictable.
183    pub expected_bytes: Option<u64>,
184    /// The hard size cap, if any (drives the post-encode correction retry).
185    pub target_bytes: Option<u64>,
186    /// Target VMAF for a CRF search, if requested (quality mode only).
187    pub target_vmaf: Option<f64>,
188    /// Source duration in seconds (for progress reporting).
189    pub source_duration_sec: f64,
190    /// Source resolution and frame rate — the reference for VMAF measurement.
191    pub source_width: Option<u32>,
192    pub source_height: Option<u32>,
193    pub source_fps: Option<f64>,
194    pub spec: EncodeSpec,
195    /// Keep the original instead of producing a result that isn't smaller
196    /// (see [`ShrinkOpts::allow_larger`]). Engines without a guard set `false`.
197    pub guard_larger: bool,
198}
199
200/// The execution result — the result of [`Engine::run`].
201#[derive(Debug, Clone, PartialEq)]
202pub struct Outcome {
203    pub output: PathBuf,
204    pub final_bytes: u64,
205    /// Measured VMAF of the result vs the source, if a measurement was taken.
206    pub vmaf: Option<f64>,
207    /// The output is the source copied as-is (stream copy): it already fit the
208    /// target, or re-encoding it would not have made it smaller.
209    pub already_compact: bool,
210}
211
212/// Engine errors.
213#[derive(Debug, Error)]
214pub enum EngineError {
215    #[error("input not supported by this engine: {0}")]
216    Unsupported(String),
217    #[error("cannot reach target size at a reasonable quality")]
218    Infeasible,
219    #[error("not yet implemented: {0}")]
220    NotImplemented(&'static str),
221    #[error(transparent)]
222    Ffmpeg(#[from] deepshrink_ffmpeg::FfmpegError),
223    #[error(transparent)]
224    Io(#[from] std::io::Error),
225}
226
227/// The compression engine contract. The one v0.1 implementation is [`media::MediaEngine`].
228pub trait Engine {
229    /// Whether this engine handles the given file.
230    fn supports(&self, input: &Path) -> bool;
231    /// Read metadata (side effect: run a probe, e.g. ffprobe).
232    fn probe(&self, input: &Path) -> Result<MediaInfo, EngineError>;
233    /// Build the encode plan. Pure function — tested without encoding.
234    fn plan(&self, info: &MediaInfo, opts: &ShrinkOpts) -> Result<EncodePlan, EngineError>;
235    /// Execute the plan (side effect: run the encoder binary).
236    fn run(&self, plan: &EncodePlan) -> Result<Outcome, EngineError>;
237}