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}