jpegli/encode/
encoder_config.rs

1//! Encoder configuration for v2 API.
2
3use super::byte_encoders::{BytesEncoder, RgbEncoder, YCbCrPlanarEncoder};
4use super::encoder_types::{
5    ChromaSubsampling, ColorMode, DownsamplingMethod, PixelLayout, Quality, XybSubsampling,
6};
7#[cfg(feature = "experimental-hybrid-trellis")]
8use super::mozjpeg_compat::TrellisConfig;
9use super::tuning::EncodingTables;
10use crate::error::Result;
11use crate::types::EdgePaddingConfig;
12
13/// JPEG encoder configuration. Dimension-independent, reusable across images.
14#[derive(Clone, Debug)]
15pub struct EncoderConfig {
16    pub(crate) quality: Quality,
17    /// Custom encoding tables (quantization + zero-bias).
18    /// `None` means use perceptual defaults based on color mode and quality.
19    pub(crate) tables: Option<Box<EncodingTables>>,
20    pub(crate) progressive: bool,
21    pub(crate) optimize_huffman: bool,
22    pub(crate) color_mode: ColorMode,
23    pub(crate) downsampling_method: DownsamplingMethod,
24    pub(crate) restart_interval: u16,
25    pub(crate) icc_profile: Option<Vec<u8>>,
26    pub(crate) exif_data: Option<super::exif::Exif>,
27    pub(crate) xmp_data: Option<Vec<u8>>,
28    pub(crate) edge_padding: EdgePaddingConfig,
29    /// Parallel encoding configuration (requires `parallel` feature)
30    #[cfg(feature = "parallel")]
31    pub(crate) parallel: Option<super::encoder_types::ParallelEncoding>,
32    /// Hybrid quantization configuration (requires `experimental-hybrid-trellis` feature)
33    #[cfg(feature = "experimental-hybrid-trellis")]
34    pub(crate) hybrid_config: crate::hybrid::config::HybridConfig,
35    /// Enable overshoot deringing (on by default).
36    pub(crate) deringing: bool,
37    /// Allow 16-bit quantization tables (extended JPEG, SOF1).
38    /// When false, quant values are clamped to 255 for baseline compatibility.
39    pub(crate) allow_16bit_quant_tables: bool,
40    /// Use separate quantization tables for Cb and Cr (3 tables total).
41    /// When false, Cb and Cr share the same table (2 tables total).
42    /// Default is true (3 tables), matching C++ jpegli's `jpegli_set_distance()`.
43    /// Set to false for compatibility with `jpeg_set_quality()` behavior.
44    pub(crate) separate_chroma_tables: bool,
45    /// Trellis quantization configuration (mozjpeg-compatible API).
46    /// When Some, enables trellis quantization for rate-distortion optimization.
47    #[cfg(feature = "experimental-hybrid-trellis")]
48    pub(crate) trellis: Option<TrellisConfig>,
49    /// Prepared segments for injection (EXIF, XMP, ICC, etc.) and MPF secondary images.
50    pub(crate) segments: Option<super::extras::EncoderSegments>,
51}
52
53// Note: No Default impl - quality and color mode are required via constructors
54
55impl EncoderConfig {
56    /// Create a YCbCr encoder configuration.
57    ///
58    /// YCbCr is the standard JPEG color space, compatible with all decoders.
59    ///
60    /// # Arguments
61    /// - `quality`: Quality level (0-100 for jpegli scale, or use `Quality::*` variants)
62    /// - `subsampling`: Chroma subsampling mode
63    ///   - `ChromaSubsampling::None` (4:4:4) - best quality, larger files
64    ///   - `ChromaSubsampling::Quarter` (4:2:0) - good compression, smaller files
65    ///   - `ChromaSubsampling::HalfHorizontal` (4:2:2) - horizontal only
66    ///   - `ChromaSubsampling::HalfVertical` (4:4:0) - vertical only
67    ///
68    /// # Example
69    /// ```ignore
70    /// use jpegli::encoder::{EncoderConfig, ChromaSubsampling};
71    ///
72    /// let config = EncoderConfig::ycbcr(85, ChromaSubsampling::Quarter)
73    ///     .progressive(true);
74    /// ```
75    #[must_use]
76    pub fn ycbcr(quality: impl Into<Quality>, subsampling: ChromaSubsampling) -> Self {
77        Self {
78            quality: quality.into(),
79            color_mode: ColorMode::YCbCr { subsampling },
80            ..Self::default_internal()
81        }
82    }
83
84    /// Create an XYB encoder configuration.
85    ///
86    /// XYB is a perceptual color space that can achieve better quality at the same
87    /// file size for some images. The B (blue-yellow) channel can optionally be
88    /// subsampled since it's less perceptually important.
89    ///
90    /// # Arguments
91    /// - `quality`: Quality level (0-100 for jpegli scale, or use `Quality::*` variants)
92    /// - `b_subsampling`: B channel subsampling
93    ///   - `XybSubsampling::Full` - all channels at full resolution
94    ///   - `XybSubsampling::BQuarter` - B channel at quarter resolution (default, recommended)
95    ///
96    /// # Notes
97    /// - Requires linear RGB input (f32 or u16 pixel formats)
98    /// - Embeds an ICC profile for proper color reproduction
99    /// - Not all decoders support XYB JPEGs correctly
100    ///
101    /// # Example
102    /// ```ignore
103    /// use jpegli::encoder::{EncoderConfig, XybSubsampling};
104    ///
105    /// let config = EncoderConfig::xyb(85, XybSubsampling::BQuarter)
106    ///     .progressive(true);
107    /// ```
108    #[must_use]
109    pub fn xyb(quality: impl Into<Quality>, b_subsampling: XybSubsampling) -> Self {
110        Self {
111            quality: quality.into(),
112            color_mode: ColorMode::Xyb {
113                subsampling: b_subsampling,
114            },
115            ..Self::default_internal()
116        }
117    }
118
119    /// Create a grayscale encoder configuration.
120    ///
121    /// Only the luminance channel is encoded. Works with any input format;
122    /// color inputs are converted to grayscale.
123    ///
124    /// # Arguments
125    /// - `quality`: Quality level (0-100 for jpegli scale, or use `Quality::*` variants)
126    ///
127    /// # Example
128    /// ```ignore
129    /// use jpegli::encoder::EncoderConfig;
130    ///
131    /// let config = EncoderConfig::grayscale(85)
132    ///     .progressive(true);
133    /// ```
134    #[must_use]
135    pub fn grayscale(quality: impl Into<Quality>) -> Self {
136        Self {
137            quality: quality.into(),
138            color_mode: ColorMode::Grayscale,
139            ..Self::default_internal()
140        }
141    }
142
143    /// Internal default for non-required fields only.
144    fn default_internal() -> Self {
145        Self {
146            quality: Quality::default(),
147            tables: None, // Use perceptual defaults
148            progressive: false,
149            optimize_huffman: true,
150            color_mode: ColorMode::default(),
151            downsampling_method: DownsamplingMethod::default(),
152            restart_interval: 0,
153            icc_profile: None,
154            exif_data: None,
155            xmp_data: None,
156            edge_padding: EdgePaddingConfig::default(),
157            #[cfg(feature = "parallel")]
158            parallel: None,
159            #[cfg(feature = "experimental-hybrid-trellis")]
160            hybrid_config: crate::hybrid::config::HybridConfig::default(),
161            deringing: true,
162            allow_16bit_quant_tables: true,
163            separate_chroma_tables: true, // 3 tables (matches jpegli_set_distance)
164            #[cfg(feature = "experimental-hybrid-trellis")]
165            trellis: None,
166            segments: None,
167        }
168    }
169
170    // === Quality & Quantization ===
171
172    /// Override the quality level.
173    ///
174    /// Accepts any type that converts to `Quality`:
175    /// - `f32` or `u8` for ApproxJpegli scale
176    /// - `Quality::ApproxMozjpeg(u8)` for mozjpeg-like quality
177    /// - `Quality::ApproxSsim2(f32)` for SSIMULACRA2 target
178    /// - `Quality::ApproxButteraugli(f32)` for Butteraugli target
179    #[must_use]
180    pub fn quality(mut self, q: impl Into<Quality>) -> Self {
181        self.quality = q.into();
182        self
183    }
184
185    // === Encoding Mode ===
186
187    /// Enable or disable progressive encoding.
188    ///
189    /// Progressive encoding produces multiple scans for incremental display.
190    /// Automatically enables optimized Huffman tables (required for progressive).
191    #[must_use]
192    pub fn progressive(mut self, enable: bool) -> Self {
193        self.progressive = enable;
194        if enable {
195            self.optimize_huffman = true;
196        }
197        self
198    }
199
200    /// Enable or disable Huffman table optimization.
201    ///
202    /// When enabled (default), computes optimal Huffman tables from image data.
203    /// When disabled, uses standard JPEG Huffman tables (faster but larger files).
204    ///
205    /// Note: Progressive mode requires optimized Huffman tables.
206    #[must_use]
207    pub fn optimize_huffman(mut self, enable: bool) -> Self {
208        self.optimize_huffman = enable;
209        self
210    }
211
212    /// Allow 16-bit quantization tables (extended sequential JPEG, SOF1).
213    ///
214    /// When enabled (default), quantization values can exceed 255, producing
215    /// extended sequential JPEGs (SOF1 marker) for better low-quality precision.
216    ///
217    /// When disabled, quantization values are clamped to 255, producing
218    /// baseline-compatible JPEGs (SOF0 marker) that work with all decoders.
219    ///
220    /// Most modern decoders support 16-bit quant tables. Only disable this
221    /// for maximum compatibility with legacy software.
222    #[must_use]
223    pub fn allow_16bit_quant_tables(mut self, enable: bool) -> Self {
224        self.allow_16bit_quant_tables = enable;
225        self
226    }
227
228    /// Use separate quantization tables for Cb and Cr components.
229    ///
230    /// When enabled (default), uses 3 quantization tables:
231    /// - Table 0: Y (luma)
232    /// - Table 1: Cb (blue chroma)
233    /// - Table 2: Cr (red chroma)
234    ///
235    /// When disabled, uses 2 quantization tables:
236    /// - Table 0: Y (luma)
237    /// - Table 1: Cb and Cr (shared chroma)
238    ///
239    /// # Compatibility
240    ///
241    /// - 3 tables (default): Matches C++ jpegli's `jpegli_set_distance()` behavior
242    /// - 2 tables: Matches C++ jpegli's `jpeg_set_quality()` behavior
243    ///
244    /// Use 2 tables when you need exact output parity with tools that use
245    /// `jpeg_set_quality()` (most libjpeg-based encoders).
246    ///
247    /// # Example
248    ///
249    /// ```ignore
250    /// // Match jpeg_set_quality() behavior (2 tables)
251    /// let config = EncoderConfig::ycbcr(85, ChromaSubsampling::Quarter)
252    ///     .separate_chroma_tables(false);
253    /// ```
254    #[must_use]
255    pub fn separate_chroma_tables(mut self, enable: bool) -> Self {
256        self.separate_chroma_tables = enable;
257        self
258    }
259
260    /// Force baseline JPEG compatibility.
261    ///
262    /// This is a convenience method equivalent to:
263    /// ```ignore
264    /// config.progressive(false).allow_16bit_quant_tables(false)
265    /// ```
266    ///
267    /// Baseline JPEGs (SOF0) are the most compatible format, supported by
268    /// all JPEG decoders. Use this when targeting legacy software or when
269    /// maximum compatibility is required.
270    #[must_use]
271    pub fn force_baseline(self) -> Self {
272        self.progressive(false).allow_16bit_quant_tables(false)
273    }
274
275    /// Set the restart interval (MCUs between restart markers).
276    ///
277    /// Restart markers allow partial decoding and error recovery.
278    /// Set to 0 to disable restart markers (default).
279    #[must_use]
280    pub fn restart_interval(mut self, interval: u16) -> Self {
281        self.restart_interval = interval;
282        self
283    }
284
285    /// Enable parallel encoding for improved throughput on multi-core systems.
286    ///
287    /// When enabled, the encoder uses multiple threads for:
288    /// - DCT computation (block transforms)
289    /// - Entropy/Huffman encoding (via restart markers)
290    ///
291    /// # Restart Marker Behavior
292    ///
293    /// Parallel entropy encoding requires restart markers between segments.
294    /// When parallel encoding is enabled:
295    /// - If `restart_interval` is 0 or too small, it will be **increased** to an
296    ///   optimal value based on thread count and image size
297    /// - User-specified `restart_interval` values are respected as a minimum
298    ///   (the encoder may increase but will not decrease them)
299    ///
300    /// # Performance
301    ///
302    /// - 2 threads: ~1.2-1.6x speedup
303    /// - 4 threads: ~1.3-1.7x speedup
304    /// - Minimum useful size: ~512x512 (smaller images have too much overhead)
305    ///
306    /// # Example
307    ///
308    /// ```ignore
309    /// use jpegli::{EncoderConfig, ChromaSubsampling, ParallelEncoding};
310    ///
311    /// let config = EncoderConfig::ycbcr(85.0, ChromaSubsampling::Quarter)
312    ///     .parallel(ParallelEncoding::Auto);
313    /// ```
314    ///
315    /// Requires the `parallel` feature flag.
316    #[cfg(feature = "parallel")]
317    #[must_use]
318    pub fn parallel(mut self, mode: super::encoder_types::ParallelEncoding) -> Self {
319        self.parallel = Some(mode);
320        self
321    }
322
323    /// Configure hybrid quantization (jpegli AQ + mozjpeg trellis).
324    ///
325    /// Allows fine-tuning all hybrid AQ+trellis parameters.
326    /// See [`HybridConfig`](crate::hybrid::config::HybridConfig) for available options.
327    ///
328    /// Requires the `experimental-hybrid-trellis` feature.
329    #[cfg(feature = "experimental-hybrid-trellis")]
330    #[must_use]
331    pub fn hybrid_config(mut self, config: crate::hybrid::config::HybridConfig) -> Self {
332        self.hybrid_config = config;
333        self
334    }
335
336    // === Trellis Quantization ===
337
338    /// Configure trellis quantization (mozjpeg-compatible API).
339    ///
340    /// Trellis quantization uses rate-distortion optimization to find the best
341    /// quantization decisions, typically producing 10-15% smaller files at the
342    /// same visual quality.
343    ///
344    /// This uses the same algorithm as mozjpeg and provides a compatible API.
345    /// For advanced users who want to combine trellis with jpegli's adaptive
346    /// quantization, see the `hybrid_config()` method.
347    ///
348    /// Requires the `experimental-hybrid-trellis` feature.
349    ///
350    /// # Example
351    ///
352    /// ```rust,ignore
353    /// use jpegli::encode::{EncoderConfig, ChromaSubsampling, TrellisConfig};
354    ///
355    /// // Enable trellis with default settings
356    /// let config = EncoderConfig::ycbcr(85, ChromaSubsampling::Quarter)
357    ///     .trellis(TrellisConfig::default());
358    ///
359    /// // Fine-tune trellis parameters
360    /// let config = EncoderConfig::ycbcr(85, ChromaSubsampling::Quarter)
361    ///     .trellis(TrellisConfig::default()
362    ///         .ac_trellis(true)
363    ///         .dc_trellis(true)
364    ///         .speed_mode(TrellisSpeedMode::Level(5))
365    ///         .rd_factor(0.8));
366    ///
367    /// // Disable trellis for fastest encoding
368    /// let config = EncoderConfig::ycbcr(85, ChromaSubsampling::Quarter)
369    ///     .trellis(TrellisConfig::disabled());
370    /// ```
371    #[cfg(feature = "experimental-hybrid-trellis")]
372    #[must_use]
373    pub fn trellis(mut self, config: TrellisConfig) -> Self {
374        self.trellis = Some(config);
375        self
376    }
377
378    /// Get the trellis configuration, if set.
379    ///
380    /// Requires the `experimental-hybrid-trellis` feature.
381    #[cfg(feature = "experimental-hybrid-trellis")]
382    #[must_use]
383    pub fn get_trellis(&self) -> Option<&TrellisConfig> {
384        self.trellis.as_ref()
385    }
386
387    // === ICC Profile ===
388
389    /// Attach an ICC color profile to the output JPEG.
390    ///
391    /// The profile will be written as APP2 marker segments with the standard
392    /// "ICC_PROFILE" signature. Large profiles are automatically chunked
393    /// (max 65519 bytes per segment) as required by the ICC profile embedding spec.
394    ///
395    /// Common profiles:
396    /// - sRGB IEC61966-2.1 (~3KB)
397    /// - Display P3 (~0.5KB)
398    /// - Adobe RGB 1998 (~0.5KB)
399    ///
400    /// # Example
401    /// ```ignore
402    /// use jpegli::{EncoderConfig, ChromaSubsampling};
403    /// let srgb_profile = std::fs::read("sRGB.icc")?;
404    /// let config = EncoderConfig::ycbcr(85.0, ChromaSubsampling::Quarter)
405    ///     .icc_profile(srgb_profile);
406    /// ```
407    #[must_use]
408    pub fn icc_profile(mut self, profile: impl Into<Vec<u8>>) -> Self {
409        self.icc_profile = Some(profile.into());
410        self
411    }
412
413    // === EXIF/XMP Metadata ===
414
415    /// Attach EXIF metadata to the output JPEG.
416    ///
417    /// Use [`Exif::raw`][super::exif::Exif::raw] for raw EXIF bytes, or
418    /// [`Exif::build`][super::exif::Exif::build] to construct from common fields.
419    ///
420    /// The two modes are mutually exclusive at compile time - you cannot
421    /// mix raw bytes with field-based building.
422    ///
423    /// # Examples
424    ///
425    /// Build from fields (orientation and copyright):
426    /// ```ignore
427    /// use jpegli::encoder::{EncoderConfig, ChromaSubsampling, Exif, Orientation};
428    ///
429    /// let config = EncoderConfig::ycbcr(85, ChromaSubsampling::Quarter)
430    ///     .exif(Exif::build()
431    ///         .orientation(Orientation::Rotate90)
432    ///         .copyright("© 2024 Example Corp"));
433    /// ```
434    ///
435    /// Use raw EXIF bytes:
436    /// ```ignore
437    /// use jpegli::encoder::{EncoderConfig, ChromaSubsampling, Exif};
438    ///
439    /// let config = EncoderConfig::ycbcr(85, ChromaSubsampling::Quarter)
440    ///     .exif(Exif::raw(my_exif_bytes));
441    /// ```
442    ///
443    /// # Notes
444    ///
445    /// - EXIF is placed immediately after SOI, before any other markers
446    /// - Raw bytes should be TIFF data without the "Exif\0\0" prefix (added automatically)
447    /// - Maximum size: 65527 bytes (larger data will be truncated)
448    #[must_use]
449    pub fn exif(mut self, exif: impl Into<super::exif::Exif>) -> Self {
450        self.exif_data = Some(exif.into());
451        self
452    }
453
454    /// Attach XMP metadata to the output JPEG.
455    ///
456    /// The data will be written as an APP1 marker segment with the standard
457    /// Adobe XMP namespace signature. The provided bytes should be the raw XMP
458    /// XML data without the APP1 marker or namespace prefix.
459    ///
460    /// XMP is placed after EXIF (if present) but before ICC profile.
461    ///
462    /// # Maximum Size
463    /// Standard XMP is limited to 65502 bytes (65535 - 2 length - 29 namespace - 2 padding).
464    /// For larger XMP data, use Extended XMP (not yet supported).
465    #[must_use]
466    pub fn xmp(mut self, data: impl Into<Vec<u8>>) -> Self {
467        self.xmp_data = Some(data.into());
468        self
469    }
470
471    // === Color Mode ===
472
473    /// Set the output color mode.
474    #[must_use]
475    pub fn color_mode(mut self, mode: ColorMode) -> Self {
476        self.color_mode = mode;
477        self
478    }
479
480    /// Set the chroma downsampling method.
481    ///
482    /// Only affects RGB/RGBX input with chroma subsampling enabled.
483    /// Ignored for grayscale, YCbCr input, or 4:4:4 subsampling.
484    #[must_use]
485    pub fn downsampling_method(mut self, method: DownsamplingMethod) -> Self {
486        self.downsampling_method = method;
487        self
488    }
489
490    /// Internal: Set edge padding strategy for partial MCU blocks.
491    #[doc(hidden)]
492    #[must_use]
493    pub fn edge_padding_internal(mut self, config: EdgePaddingConfig) -> Self {
494        self.edge_padding = config;
495        self
496    }
497
498    // === Tuning API (doc hidden) ===
499
500    /// Apply custom encoding tables for experimentation.
501    ///
502    /// This replaces both quantization tables and zero-bias configuration
503    /// with values from the provided `EncodingTables`.
504    ///
505    /// Takes `Box<EncodingTables>` since custom tables are rarely used and
506    /// the struct is ~1.5KB. This keeps `EncoderConfig` small by default.
507    ///
508    /// # Notes
509    /// - Tables must match the color mode (YCbCr or XYB)
510    /// - When using `ScalingParams::Exact`, quality scaling is bypassed
511    /// - When using `ScalingParams::Scaled`, tables are scaled by quality
512    ///
513    /// # Example
514    /// ```
515    /// use jpegli::encode::{EncoderConfig, ChromaSubsampling};
516    /// use jpegli::encode::tuning::EncodingTables;
517    ///
518    /// let mut tables = EncodingTables::default_ycbcr();
519    /// tables.scale_quant(0, 0, 0.8);  // Reduce DC quantization
520    ///
521    /// let config = EncoderConfig::ycbcr(85, ChromaSubsampling::Quarter)
522    ///     .tables(Box::new(tables));
523    /// ```
524    #[must_use]
525    pub fn tables(mut self, tables: Box<EncodingTables>) -> Self {
526        self.tables = Some(tables);
527        self
528    }
529
530    /// Enable or disable SharpYUV (GammaAwareIterative) downsampling.
531    ///
532    /// SharpYUV produces better color preservation on edges and thin lines,
533    /// at the cost of ~3x slower encoding.
534    #[must_use]
535    pub fn sharp_yuv(self, enable: bool) -> Self {
536        self.downsampling_method(if enable {
537            DownsamplingMethod::GammaAwareIterative
538        } else {
539            DownsamplingMethod::Box
540        })
541    }
542
543    /// Enable or disable overshoot deringing (enabled by default).
544    ///
545    /// Deringing reduces ringing artifacts on white backgrounds by smoothing hard
546    /// edges. It allows pixel values to "overshoot" beyond the displayable range.
547    /// Since JPEG decoders clamp values to 0-255, the overshoot is invisible but
548    /// the smoother curve compresses better with fewer artifacts.
549    ///
550    /// This technique was pioneered by [@kornel](https://github.com/kornelski) in
551    /// [mozjpeg](https://github.com/mozilla/mozjpeg) and significantly improves
552    /// quality for documents, graphics, and text without degrading photographic
553    /// content.
554    ///
555    /// Particularly effective for:
556    /// - Documents and screenshots with white backgrounds
557    /// - Text and graphics with hard edges
558    /// - Any image with saturated regions (pixels at 0 or 255)
559    ///
560    /// There is no quality downside to leaving this enabled for photos.
561    #[must_use]
562    pub fn deringing(mut self, enable: bool) -> Self {
563        self.deringing = enable;
564        self
565    }
566
567    // === Validation ===
568
569    /// Validate the configuration, returning an error for invalid combinations.
570    ///
571    /// Invalid combinations:
572    /// - Progressive mode with disabled Huffman optimization
573    pub fn validate(&self) -> Result<()> {
574        if self.progressive && !self.optimize_huffman {
575            return Err(crate::error::Error::invalid_config(
576                "progressive mode requires optimized Huffman tables".into(),
577            ));
578        }
579        Ok(())
580    }
581
582    // === Encoder Creation ===
583
584    /// Create an encoder from raw bytes with explicit pixel layout.
585    ///
586    /// Use this when working with raw byte buffers and you know the pixel layout.
587    ///
588    /// # Arguments
589    /// - `width`: Image width in pixels
590    /// - `height`: Image height in pixels
591    /// - `layout`: Pixel data layout (channel order, depth, color space)
592    ///
593    /// # Example
594    /// ```ignore
595    /// use jpegli::{EncoderConfig, ChromaSubsampling, PixelLayout, Unstoppable};
596    /// let config = EncoderConfig::ycbcr(85.0, ChromaSubsampling::Quarter);
597    /// let mut enc = config.encode_from_bytes(1920, 1080, PixelLayout::Rgb8Srgb)?;
598    /// enc.push_packed(&rgb_bytes, Unstoppable)?;
599    /// let jpeg = enc.finish()?;
600    /// ```
601    pub fn encode_from_bytes(
602        &self,
603        width: u32,
604        height: u32,
605        layout: PixelLayout,
606    ) -> Result<BytesEncoder> {
607        self.validate()?;
608        BytesEncoder::new(self.clone(), width, height, layout)
609    }
610
611    /// Create an encoder from rgb crate pixel types.
612    ///
613    /// Layout is inferred from the type parameter. For RGBA/BGRA types,
614    /// the 4th channel is ignored.
615    ///
616    /// # Type Parameter
617    /// - `P`: Pixel type from the `rgb` crate (e.g., `RGB<u8>`, `RGBA<f32>`)
618    ///
619    /// # Example
620    /// ```ignore
621    /// use rgb::RGB;
622    /// use jpegli::{EncoderConfig, ChromaSubsampling, Unstoppable};
623    ///
624    /// let config = EncoderConfig::ycbcr(85.0, ChromaSubsampling::Quarter);
625    /// let mut enc = config.encode_from_rgb::<RGB<u8>>(1920, 1080)?;
626    /// enc.push_packed(&pixels, Unstoppable)?;
627    /// let jpeg = enc.finish()?;
628    /// ```
629    pub fn encode_from_rgb<P: super::byte_encoders::Pixel>(
630        &self,
631        width: u32,
632        height: u32,
633    ) -> Result<RgbEncoder<P>> {
634        self.validate()?;
635        RgbEncoder::new(self.clone(), width, height)
636    }
637
638    /// Create an encoder from planar YCbCr data.
639    ///
640    /// Use this when you have pre-converted YCbCr from video decoders, etc.
641    /// Skips RGB->YCbCr conversion entirely.
642    ///
643    /// Only valid with `ColorMode::YCbCr`. XYB mode requires RGB input.
644    ///
645    /// # Example
646    /// ```ignore
647    /// use jpegli::{EncoderConfig, ChromaSubsampling, Unstoppable};
648    ///
649    /// let config = EncoderConfig::ycbcr(85.0, ChromaSubsampling::Quarter);
650    /// let mut enc = config.encode_from_ycbcr_planar(1920, 1080)?;
651    /// enc.push(&planes, height, Unstoppable)?;
652    /// let jpeg = enc.finish()?;
653    /// ```
654    pub fn encode_from_ycbcr_planar(&self, width: u32, height: u32) -> Result<YCbCrPlanarEncoder> {
655        self.validate()?;
656
657        // Validate color mode
658        if !matches!(self.color_mode, ColorMode::YCbCr { .. }) {
659            return Err(crate::error::Error::invalid_config(
660                "planar YCbCr input requires YCbCr color mode".into(),
661            ));
662        }
663
664        YCbCrPlanarEncoder::new(self.clone(), width, height)
665    }
666
667    // === Resource Estimation ===
668
669    /// Estimate peak memory usage for encoding an image of the given dimensions.
670    ///
671    /// Returns estimated bytes based on color mode, subsampling, and dimensions.
672    /// Delegates to the streaming encoder's estimate which accounts for all
673    /// internal buffers.
674    #[must_use]
675    pub fn estimate_memory(&self, width: u32, height: u32) -> usize {
676        use crate::encode::streaming::StreamingEncoder;
677
678        let subsampling = match self.color_mode {
679            ColorMode::YCbCr { subsampling } => subsampling.into(),
680            ColorMode::Xyb { .. } => crate::types::Subsampling::S444,
681            ColorMode::Grayscale => crate::types::Subsampling::S444,
682        };
683
684        StreamingEncoder::new(width, height)
685            .subsampling(subsampling)
686            .optimize_huffman(self.optimize_huffman)
687            .estimate_memory_usage()
688    }
689
690    /// Returns an absolute ceiling on memory usage.
691    ///
692    /// Unlike `estimate_memory`, this returns a **guaranteed upper bound**
693    /// that actual peak memory will never exceed. Use this for resource reservation
694    /// when you need certainty rather than a close estimate.
695    ///
696    /// The ceiling accounts for:
697    /// - Worst-case token counts per block (high-frequency content)
698    /// - Maximum output buffer size (incompressible images)
699    /// - Vec capacity overhead (allocator rounding)
700    /// - All intermediate buffers at their maximum sizes
701    ///
702    /// # Example
703    ///
704    /// ```rust,ignore
705    /// use jpegli::encoder::{EncoderConfig, ChromaSubsampling};
706    ///
707    /// let config = EncoderConfig::ycbcr(85.0, ChromaSubsampling::Quarter);
708    /// let ceiling = config.estimate_memory_ceiling(1920, 1080);
709    ///
710    /// // Reserve this much memory - actual usage guaranteed to be less
711    /// let buffer = Vec::with_capacity(ceiling);
712    /// ```
713    #[must_use]
714    pub fn estimate_memory_ceiling(&self, width: u32, height: u32) -> usize {
715        use crate::encode::streaming::StreamingEncoder;
716
717        let subsampling = match self.color_mode {
718            ColorMode::YCbCr { subsampling } => subsampling.into(),
719            ColorMode::Xyb { .. } => crate::types::Subsampling::S444,
720            ColorMode::Grayscale => crate::types::Subsampling::S444,
721        };
722
723        StreamingEncoder::new(width, height)
724            .subsampling(subsampling)
725            .estimate_memory_ceiling()
726    }
727
728    // === Accessors ===
729
730    /// Get the configured quality.
731    #[must_use]
732    pub fn get_quality(&self) -> Quality {
733        self.quality
734    }
735
736    /// Get the configured color mode.
737    #[must_use]
738    pub fn get_color_mode(&self) -> ColorMode {
739        self.color_mode
740    }
741
742    /// Check if progressive mode is enabled.
743    #[must_use]
744    pub fn is_progressive(&self) -> bool {
745        self.progressive
746    }
747
748    /// Check if Huffman optimization is enabled.
749    #[must_use]
750    pub fn is_optimize_huffman(&self) -> bool {
751        self.optimize_huffman
752    }
753
754    /// Check if 16-bit quantization tables are allowed.
755    #[must_use]
756    pub fn is_allow_16bit_quant_tables(&self) -> bool {
757        self.allow_16bit_quant_tables
758    }
759
760    /// Check if separate chroma tables are enabled (3 tables vs 2).
761    #[must_use]
762    pub fn is_separate_chroma_tables(&self) -> bool {
763        self.separate_chroma_tables
764    }
765
766    /// Get the ICC profile, if set.
767    #[must_use]
768    pub fn get_icc_profile(&self) -> Option<&[u8]> {
769        self.icc_profile.as_deref()
770    }
771
772    /// Get the EXIF data, if set.
773    #[must_use]
774    pub fn get_exif(&self) -> Option<&super::exif::Exif> {
775        self.exif_data.as_ref()
776    }
777
778    /// Get the XMP data, if set.
779    #[must_use]
780    pub fn get_xmp(&self) -> Option<&[u8]> {
781        self.xmp_data.as_deref()
782    }
783
784    /// Internal: Get the configured edge padding.
785    #[doc(hidden)]
786    #[must_use]
787    pub fn get_edge_padding(&self) -> EdgePaddingConfig {
788        self.edge_padding
789    }
790
791    // === Segment Injection ===
792
793    /// Add prepared segments for injection into output.
794    ///
795    /// Use this to preserve metadata during round-trip encoding or to inject
796    /// custom metadata and MPF secondary images.
797    ///
798    /// # Example
799    ///
800    /// ```rust,ignore
801    /// use jpegli::decoder::Decoder;
802    /// use jpegli::encoder::{EncoderConfig, ChromaSubsampling};
803    ///
804    /// // Decode with metadata preservation
805    /// let decoded = Decoder::new().decode(&original)?;
806    /// let extras = decoded.extras().unwrap();
807    ///
808    /// // Re-encode with same metadata
809    /// let config = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter)
810    ///     .with_segments(extras.to_encoder_segments());
811    /// ```
812    #[must_use]
813    pub fn with_segments(mut self, segments: super::extras::EncoderSegments) -> Self {
814        self.segments = Some(segments);
815        self
816    }
817
818    /// Add a single segment (convenience method).
819    ///
820    /// The segment type is inferred from the marker and data.
821    #[must_use]
822    pub fn add_segment(mut self, marker: u8, data: Vec<u8>) -> Self {
823        use super::extras::EncoderSegments;
824        self.segments
825            .get_or_insert_with(EncoderSegments::new)
826            .add_raw_mut(marker, data);
827        self
828    }
829
830    /// Add an MPF secondary image (gain map, depth map, etc.).
831    ///
832    /// The image data must be a complete JPEG file. An MPF directory
833    /// will be automatically generated during encoding.
834    ///
835    /// # Example
836    ///
837    /// ```rust,ignore
838    /// use jpegli::encoder::{EncoderConfig, ChromaSubsampling, MpfImageType};
839    ///
840    /// let config = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter)
841    ///     .add_mpf_image(gainmap_jpeg, MpfImageType::Undefined);
842    /// ```
843    #[must_use]
844    pub fn add_mpf_image(mut self, jpeg: Vec<u8>, typ: super::extras::MpfImageType) -> Self {
845        use super::extras::EncoderSegments;
846        self.segments
847            .get_or_insert_with(EncoderSegments::new)
848            .add_mpf_image_mut(jpeg, typ);
849        self
850    }
851
852    /// Add a gain map (convenience for `MpfImageType::Undefined`).
853    ///
854    /// Gain maps are used by UltraHDR for HDR rendering. The image data
855    /// must be a complete JPEG file (typically grayscale).
856    #[must_use]
857    pub fn add_gainmap(self, jpeg: Vec<u8>) -> Self {
858        self.add_mpf_image(jpeg, super::extras::MpfImageType::Undefined)
859    }
860
861    /// Get the configured segments, if any.
862    #[must_use]
863    pub fn get_segments(&self) -> Option<&super::extras::EncoderSegments> {
864        self.segments.as_ref()
865    }
866}
867
868#[cfg(test)]
869mod tests {
870    use super::*;
871    #[cfg(feature = "experimental-hybrid-trellis")]
872    use crate::encode::mozjpeg_compat::TrellisSpeedMode;
873
874    #[test]
875    fn test_ycbcr_config() {
876        let config = EncoderConfig::ycbcr(90.0, ChromaSubsampling::None);
877        assert!(matches!(config.quality, Quality::ApproxJpegli(90.0)));
878        assert!(!config.progressive);
879        assert!(config.optimize_huffman);
880        assert!(matches!(
881            config.color_mode,
882            ColorMode::YCbCr {
883                subsampling: ChromaSubsampling::None
884            }
885        ));
886    }
887
888    #[test]
889    fn test_xyb_config() {
890        let config = EncoderConfig::xyb(90.0, XybSubsampling::BQuarter);
891        assert!(matches!(config.quality, Quality::ApproxJpegli(90.0)));
892        assert!(matches!(
893            config.color_mode,
894            ColorMode::Xyb {
895                subsampling: XybSubsampling::BQuarter
896            }
897        ));
898
899        let config = EncoderConfig::xyb(90.0, XybSubsampling::Full);
900        assert!(matches!(
901            config.color_mode,
902            ColorMode::Xyb {
903                subsampling: XybSubsampling::Full
904            }
905        ));
906    }
907
908    #[test]
909    fn test_grayscale_config() {
910        let config = EncoderConfig::grayscale(85);
911        assert!(matches!(config.quality, Quality::ApproxJpegli(85.0)));
912        assert!(matches!(config.color_mode, ColorMode::Grayscale));
913    }
914
915    #[test]
916    fn test_builder_pattern() {
917        let config = EncoderConfig::ycbcr(85, ChromaSubsampling::None)
918            .progressive(true)
919            .sharp_yuv(true);
920
921        assert!(matches!(config.quality, Quality::ApproxJpegli(85.0)));
922        assert!(config.progressive);
923        assert!(config.optimize_huffman); // auto-enabled by progressive
924        assert!(matches!(
925            config.color_mode,
926            ColorMode::YCbCr {
927                subsampling: ChromaSubsampling::None
928            }
929        ));
930        assert!(matches!(
931            config.downsampling_method,
932            DownsamplingMethod::GammaAwareIterative
933        ));
934    }
935
936    #[test]
937    fn test_progressive_enables_huffman() {
938        let config = EncoderConfig::ycbcr(90.0, ChromaSubsampling::None)
939            .optimize_huffman(false)
940            .progressive(true);
941
942        assert!(config.optimize_huffman);
943    }
944
945    #[test]
946    fn test_validation_progressive_huffman() {
947        let mut config = EncoderConfig::ycbcr(90.0, ChromaSubsampling::None);
948        config.progressive = true;
949        config.optimize_huffman = false;
950
951        assert!(config.validate().is_err());
952    }
953
954    #[test]
955    fn test_deprecated_new_still_works() {
956        // Ensure backward compatibility during migration
957        let config = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter);
958        assert!(matches!(config.quality, Quality::ApproxJpegli(90.0)));
959        assert!(matches!(
960            config.color_mode,
961            ColorMode::YCbCr {
962                subsampling: ChromaSubsampling::Quarter
963            }
964        ));
965    }
966
967    #[test]
968    #[cfg(feature = "experimental-hybrid-trellis")]
969    fn test_trellis_config() {
970        // Default config has no trellis
971        let config = EncoderConfig::ycbcr(85, ChromaSubsampling::Quarter);
972        assert!(config.trellis.is_none());
973        assert!(config.get_trellis().is_none());
974
975        // Enable trellis with defaults
976        let config =
977            EncoderConfig::ycbcr(85, ChromaSubsampling::Quarter).trellis(TrellisConfig::default());
978        assert!(config.trellis.is_some());
979        let trellis = config.get_trellis().unwrap();
980        assert!(trellis.is_ac_enabled());
981        assert!(trellis.is_dc_enabled());
982        assert_eq!(trellis.get_speed_mode(), TrellisSpeedMode::Adaptive);
983    }
984
985    #[test]
986    #[cfg(feature = "experimental-hybrid-trellis")]
987    fn test_trellis_config_builder() {
988        let config = EncoderConfig::ycbcr(85, ChromaSubsampling::Quarter).trellis(
989            TrellisConfig::default()
990                .ac_trellis(true)
991                .dc_trellis(false)
992                .speed_mode(TrellisSpeedMode::Level(5))
993                .rd_factor(0.8),
994        );
995
996        let trellis = config.get_trellis().unwrap();
997        assert!(trellis.is_ac_enabled());
998        assert!(!trellis.is_dc_enabled());
999        assert_eq!(trellis.get_speed_mode(), TrellisSpeedMode::Level(5));
1000    }
1001
1002    #[test]
1003    #[cfg(feature = "experimental-hybrid-trellis")]
1004    fn test_trellis_disabled() {
1005        let config =
1006            EncoderConfig::ycbcr(85, ChromaSubsampling::Quarter).trellis(TrellisConfig::disabled());
1007
1008        let trellis = config.get_trellis().unwrap();
1009        assert!(!trellis.is_enabled());
1010        assert!(!trellis.is_ac_enabled());
1011        assert!(!trellis.is_dc_enabled());
1012    }
1013}