From fdaedd8d1ad09e2f91aa7202b3f9b5fa6308d529 Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 6 Mar 2026 15:07:54 +0200 Subject: [PATCH] Wire progressive JPEG, AVIF speed, and custom per-format quality to encoder - Add EncoderOptions struct with progressive_jpeg and avif_speed fields - Pass encoder options through ProcessingJob to PipelineExecutor - mozjpeg set_progressive_mode() called when progressive JPEG enabled - AVIF encoder speed now configurable (was hardcoded to 6) - run_processing uses CompressConfig::Custom when user overrides preset defaults - Executor properly handles AVIF quality and PNG level in Custom mode --- pixstrip-core/src/encoder.rs | 27 ++++++++++++++++++++++++--- pixstrip-core/src/executor.rs | 18 +++++++++++++----- pixstrip-core/src/pipeline.rs | 4 ++++ pixstrip-core/src/preset.rs | 2 ++ pixstrip-gtk/src/app.rs | 24 +++++++++++++++++++++++- 5 files changed, 66 insertions(+), 9 deletions(-) diff --git a/pixstrip-core/src/encoder.rs b/pixstrip-core/src/encoder.rs index 584205d..acd1cbf 100644 --- a/pixstrip-core/src/encoder.rs +++ b/pixstrip-core/src/encoder.rs @@ -4,11 +4,28 @@ use std::path::Path; use crate::error::{PixstripError, Result}; use crate::types::{ImageFormat, QualityPreset}; -pub struct OutputEncoder; +#[derive(Debug, Clone, Default)] +pub struct EncoderOptions { + pub progressive_jpeg: bool, + pub avif_speed: u8, +} + +pub struct OutputEncoder { + pub options: EncoderOptions, +} impl OutputEncoder { pub fn new() -> Self { - Self + Self { + options: EncoderOptions { + progressive_jpeg: false, + avif_speed: 6, + }, + } + } + + pub fn with_options(options: EncoderOptions) -> Self { + Self { options } } pub fn encode( @@ -60,6 +77,9 @@ impl OutputEncoder { let mut comp = mozjpeg::Compress::new(mozjpeg::ColorSpace::JCS_RGB); comp.set_size(width, height); comp.set_quality(quality as f32); + if self.options.progressive_jpeg { + comp.set_progressive_mode(); + } let mut output = Vec::new(); let mut started = comp.start_compress(&mut output).map_err(|e| PixstripError::Processing { @@ -121,9 +141,10 @@ impl OutputEncoder { let mut buf = Vec::new(); let cursor = Cursor::new(&mut buf); let rgba = img.to_rgba8(); + let speed = self.options.avif_speed.clamp(1, 10); let encoder = image::codecs::avif::AvifEncoder::new_with_speed_quality( cursor, - 6, + speed, quality, ); image::ImageEncoder::write_image( diff --git a/pixstrip-core/src/executor.rs b/pixstrip-core/src/executor.rs index 9e6effc..ba4892d 100644 --- a/pixstrip-core/src/executor.rs +++ b/pixstrip-core/src/executor.rs @@ -4,7 +4,7 @@ use std::time::Instant; use rayon::prelude::*; -use crate::encoder::OutputEncoder; +use crate::encoder::{EncoderOptions, OutputEncoder}; use crate::error::{PixstripError, Result}; use crate::loader::ImageLoader; use crate::operations::adjustments::apply_adjustments; @@ -182,7 +182,10 @@ impl PipelineExecutor { }); let loader = ImageLoader::new(); - let encoder = OutputEncoder::new(); + let encoder = OutputEncoder::with_options(EncoderOptions { + progressive_jpeg: job.progressive_jpeg, + avif_speed: job.avif_speed, + }); match Self::process_single_static(job, source, &loader, &encoder, idx) { Ok((in_size, out_size)) => { @@ -237,7 +240,10 @@ impl PipelineExecutor { let start = Instant::now(); let total = job.sources.len(); let loader = ImageLoader::new(); - let encoder = OutputEncoder::new(); + let encoder = OutputEncoder::with_options(EncoderOptions { + progressive_jpeg: job.progressive_jpeg, + avif_speed: job.avif_speed, + }); let mut result = BatchResult { total, @@ -368,12 +374,14 @@ impl PipelineExecutor { } crate::operations::CompressConfig::Custom { jpeg_quality, - png_level: _, + png_level, webp_quality, - avif_quality: _, + avif_quality, } => match output_format { ImageFormat::Jpeg => jpeg_quality.unwrap_or(85), + ImageFormat::Png => png_level.unwrap_or(6), ImageFormat::WebP => webp_quality.map(|q| q as u8).unwrap_or(80), + ImageFormat::Avif => avif_quality.map(|q| q as u8).unwrap_or(50), _ => 85, }, }); diff --git a/pixstrip-core/src/pipeline.rs b/pixstrip-core/src/pipeline.rs index f316df5..37b2126 100644 --- a/pixstrip-core/src/pipeline.rs +++ b/pixstrip-core/src/pipeline.rs @@ -21,6 +21,8 @@ pub struct ProcessingJob { pub watermark: Option, pub rename: Option, pub preserve_directory_structure: bool, + pub progressive_jpeg: bool, + pub avif_speed: u8, } impl ProcessingJob { @@ -39,6 +41,8 @@ impl ProcessingJob { watermark: None, rename: None, preserve_directory_structure: false, + progressive_jpeg: false, + avif_speed: 6, } } diff --git a/pixstrip-core/src/preset.rs b/pixstrip-core/src/preset.rs index 19b4a38..d65242d 100644 --- a/pixstrip-core/src/preset.rs +++ b/pixstrip-core/src/preset.rs @@ -40,6 +40,8 @@ impl Preset { watermark: self.watermark.clone(), rename: self.rename.clone(), preserve_directory_structure: false, + progressive_jpeg: false, + avif_speed: 6, } } diff --git a/pixstrip-gtk/src/app.rs b/pixstrip-gtk/src/app.rs index eab53e6..a58ca89 100644 --- a/pixstrip-gtk/src/app.rs +++ b/pixstrip-gtk/src/app.rs @@ -1265,9 +1265,31 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) { } if cfg.compress_enabled { - job.compress = Some(pixstrip_core::operations::CompressConfig::Preset(cfg.quality_preset)); + // Check if user has customized per-format quality values beyond the preset defaults + let preset_jpeg = cfg.quality_preset.jpeg_quality(); + let preset_webp = cfg.quality_preset.webp_quality(); + let has_custom = cfg.jpeg_quality != preset_jpeg + || cfg.webp_quality != preset_webp as u8 + || cfg.avif_quality != preset_webp as u8 + || cfg.avif_speed != 6 + || cfg.webp_effort != 4; + + if has_custom { + job.compress = Some(pixstrip_core::operations::CompressConfig::Custom { + jpeg_quality: Some(cfg.jpeg_quality), + png_level: Some(cfg.png_level), + webp_quality: Some(cfg.webp_quality as f32), + avif_quality: Some(cfg.avif_quality as f32), + }); + } else { + job.compress = Some(pixstrip_core::operations::CompressConfig::Preset(cfg.quality_preset)); + } } + // Pass encoder options to the job + job.progressive_jpeg = cfg.progressive_jpeg; + job.avif_speed = cfg.avif_speed; + if cfg.metadata_enabled { job.metadata = Some(match cfg.metadata_mode { MetadataMode::StripAll => pixstrip_core::operations::MetadataConfig::StripAll,