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}
88
89impl Default for ShrinkOpts {
90 fn default() -> Self {
91 Self {
92 goal: SizeGoal::Quality,
93 video_codec: VideoCodec::H264,
94 audio: AudioChoice::Keep,
95 resolution: ResolutionOpt::Auto,
96 fps: FpsOpt::Auto,
97 quality: QualityPreset::Balanced,
98 audio_codec: AudioCodec::Aac,
99 mono: false,
100 sample_rate: None,
101 vbr: false,
102 target_vmaf: None,
103 output: None,
104 two_pass: None,
105 dpi: None,
106 }
107 }
108}
109
110/// Video encoding parameters.
111#[derive(Debug, Clone, PartialEq)]
112pub struct VideoSpec {
113 pub codec: VideoCodec,
114 /// Target average bitrate (bits/s) for two-pass; `None` in CRF/quality mode.
115 pub bitrate_bps: Option<u64>,
116 /// CRF value for quality mode; `None` in bitrate mode.
117 pub crf: Option<u8>,
118 /// Downscale target height; `None` keeps the source resolution.
119 pub height: Option<u32>,
120 /// Frame-rate cap; `None` keeps the source rate.
121 pub fps: Option<u32>,
122 pub preset: QualityPreset,
123}
124
125/// Audio encoding parameters (absent means drop the track).
126#[derive(Debug, Clone, PartialEq)]
127pub struct AudioSpec {
128 pub codec: AudioCodec,
129 pub bitrate_bps: u64,
130 /// Downmix to a single channel.
131 pub mono: bool,
132 /// Resample to this rate (Hz); `None` keeps the source rate.
133 pub sample_rate: Option<u32>,
134 /// Prefer VBR where the codec supports it.
135 pub vbr: bool,
136}
137
138impl AudioSpec {
139 /// A plain CBR/ABR track (used for the audio track inside a video).
140 pub fn cbr(codec: AudioCodec, bitrate_bps: u64) -> Self {
141 Self {
142 codec,
143 bitrate_bps,
144 mono: false,
145 sample_rate: None,
146 vbr: false,
147 }
148 }
149}
150
151/// The full encode recipe.
152#[derive(Debug, Clone, PartialEq)]
153pub struct EncodeSpec {
154 pub video: VideoSpec,
155 /// `None` means `-an` (no audio).
156 pub audio: Option<AudioSpec>,
157 pub faststart: bool,
158 pub two_pass: bool,
159 /// Stream-copy remux only: the source already fits, so never re-encode
160 /// (and never inflate). `video`/`audio` are ignored when set.
161 pub passthrough: bool,
162 /// Pure-audio encode: emit `-vn` and use `audio` only (`video` is ignored).
163 pub audio_only: bool,
164 /// Target DPI for a document's embedded images (see [`ShrinkOpts::dpi`]).
165 /// Document engines only; ignored by the media engine.
166 pub dpi: Option<u32>,
167}
168
169/// The encode plan — the result of [`Engine::plan`]. Usable for `--dry-run`.
170#[derive(Debug, Clone, PartialEq)]
171pub struct EncodePlan {
172 pub input: PathBuf,
173 pub output: PathBuf,
174 /// Human-readable description of the plan (codec, bitrates, passes).
175 pub summary: String,
176 /// Expected final size in bytes, if predictable.
177 pub expected_bytes: Option<u64>,
178 /// The hard size cap, if any (drives the post-encode correction retry).
179 pub target_bytes: Option<u64>,
180 /// Target VMAF for a CRF search, if requested (quality mode only).
181 pub target_vmaf: Option<f64>,
182 /// Source duration in seconds (for progress reporting).
183 pub source_duration_sec: f64,
184 /// Source resolution and frame rate — the reference for VMAF measurement.
185 pub source_width: Option<u32>,
186 pub source_height: Option<u32>,
187 pub source_fps: Option<f64>,
188 pub spec: EncodeSpec,
189}
190
191/// The execution result — the result of [`Engine::run`].
192#[derive(Debug, Clone, PartialEq)]
193pub struct Outcome {
194 pub output: PathBuf,
195 pub final_bytes: u64,
196 /// Measured VMAF of the result vs the source, if a measurement was taken.
197 pub vmaf: Option<f64>,
198}
199
200/// Engine errors.
201#[derive(Debug, Error)]
202pub enum EngineError {
203 #[error("input not supported by this engine: {0}")]
204 Unsupported(String),
205 #[error("cannot reach target size at a reasonable quality")]
206 Infeasible,
207 #[error("not yet implemented: {0}")]
208 NotImplemented(&'static str),
209 #[error(transparent)]
210 Ffmpeg(#[from] deepshrink_ffmpeg::FfmpegError),
211 #[error(transparent)]
212 Io(#[from] std::io::Error),
213}
214
215/// The compression engine contract. The one v0.1 implementation is [`media::MediaEngine`].
216pub trait Engine {
217 /// Whether this engine handles the given file.
218 fn supports(&self, input: &Path) -> bool;
219 /// Read metadata (side effect: run a probe, e.g. ffprobe).
220 fn probe(&self, input: &Path) -> Result<MediaInfo, EngineError>;
221 /// Build the encode plan. Pure function — tested without encoding.
222 fn plan(&self, info: &MediaInfo, opts: &ShrinkOpts) -> Result<EncodePlan, EngineError>;
223 /// Execute the plan (side effect: run the encoder binary).
224 fn run(&self, plan: &EncodePlan) -> Result<Outcome, EngineError>;
225}