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, QuantTableConfig,
6 XybSubsampling, ZeroBiasConfig,
7};
8use crate::error::Result;
9use crate::types::EdgePaddingConfig;
10
11/// JPEG encoder configuration. Dimension-independent, reusable across images.
12#[derive(Clone, Debug)]
13pub struct EncoderConfig {
14 pub(crate) quality: Quality,
15 pub(crate) quant_tables: QuantTableConfig,
16 pub(crate) zero_bias: ZeroBiasConfig,
17 pub(crate) progressive: bool,
18 pub(crate) optimize_huffman: bool,
19 pub(crate) color_mode: ColorMode,
20 pub(crate) downsampling_method: DownsamplingMethod,
21 pub(crate) restart_interval: u16,
22 pub(crate) icc_profile: Option<Vec<u8>>,
23 pub(crate) exif_data: Option<super::exif::Exif>,
24 pub(crate) xmp_data: Option<Vec<u8>>,
25 pub(crate) edge_padding: EdgePaddingConfig,
26 /// Parallel encoding configuration (requires `parallel` feature)
27 #[cfg(feature = "parallel")]
28 pub(crate) parallel: Option<super::encoder_types::ParallelEncoding>,
29 /// Hybrid quantization configuration (requires `experimental-hybrid-trellis` feature)
30 #[cfg(feature = "experimental-hybrid-trellis")]
31 pub(crate) hybrid_config: crate::hybrid::config::HybridConfig,
32}
33
34// Note: No Default impl - quality and subsampling are required via new()
35
36impl EncoderConfig {
37 /// Create a new encoder configuration with required quality and chroma subsampling.
38 ///
39 /// # Arguments
40 /// - `quality`: Quality level (0-100 for jpegli scale, or use `Quality::*` variants)
41 /// - `subsampling`: Chroma subsampling mode
42 /// - `ChromaSubsampling::Quarter` (4:2:0) - good compression, smaller files
43 /// - `ChromaSubsampling::None` (4:4:4) - best quality, larger files
44 ///
45 /// # Example
46 /// ```ignore
47 /// use jpegli::encoder::{EncoderConfig, ChromaSubsampling};
48 ///
49 /// let config = EncoderConfig::new(85.0, ChromaSubsampling::Quarter)
50 /// .progressive(true);
51 /// ```
52 #[must_use]
53 pub fn new(quality: impl Into<Quality>, subsampling: ChromaSubsampling) -> Self {
54 Self {
55 quality: quality.into(),
56 color_mode: ColorMode::YCbCr { subsampling },
57 ..Self::default_internal()
58 }
59 }
60
61 /// Internal default for non-required fields only.
62 fn default_internal() -> Self {
63 Self {
64 quality: Quality::default(),
65 quant_tables: QuantTableConfig::default(),
66 zero_bias: ZeroBiasConfig::default(),
67 progressive: false,
68 optimize_huffman: true,
69 color_mode: ColorMode::default(),
70 downsampling_method: DownsamplingMethod::default(),
71 restart_interval: 0,
72 icc_profile: None,
73 exif_data: None,
74 xmp_data: None,
75 edge_padding: EdgePaddingConfig::default(),
76 #[cfg(feature = "parallel")]
77 parallel: None,
78 #[cfg(feature = "experimental-hybrid-trellis")]
79 hybrid_config: crate::hybrid::config::HybridConfig::default(),
80 }
81 }
82
83 // === Quality & Quantization ===
84
85 /// Override the quality level.
86 ///
87 /// Accepts any type that converts to `Quality`:
88 /// - `f32` or `u8` for ApproxJpegli scale
89 /// - `Quality::ApproxMozjpeg(u8)` for mozjpeg-like quality
90 /// - `Quality::ApproxSsim2(f32)` for SSIMULACRA2 target
91 /// - `Quality::ApproxButteraugli(f32)` for Butteraugli target
92 #[must_use]
93 pub fn quality(mut self, q: impl Into<Quality>) -> Self {
94 self.quality = q.into();
95 self
96 }
97
98 /// Set custom quantization tables.
99 #[must_use]
100 pub fn quant_tables(mut self, config: QuantTableConfig) -> Self {
101 self.quant_tables = config;
102 self
103 }
104
105 /// Set zero-bias configuration.
106 ///
107 /// Zero-bias controls how DCT coefficients are rounded toward zero during
108 /// quantization. The default `Perceptual` mode uses jpegli's quality-adaptive
109 /// tables which are optimized for visual quality.
110 ///
111 /// # Options
112 ///
113 /// - `ZeroBiasConfig::Perceptual` (default) - quality-adaptive tables
114 /// - `ZeroBiasConfig::Disabled` - no zero-bias (standard JPEG behavior)
115 /// - `ZeroBiasConfig::Custom { .. }` - provide custom per-component tables
116 ///
117 /// # Example
118 ///
119 /// ```ignore
120 /// use jpegli::encoder::{EncoderConfig, ZeroBiasConfig};
121 ///
122 /// // Disable zero-bias for standard JPEG behavior
123 /// let config = EncoderConfig::new(85, ChromaSubsampling::None)
124 /// .zero_bias(ZeroBiasConfig::Disabled);
125 /// ```
126 #[must_use]
127 pub fn zero_bias(mut self, config: ZeroBiasConfig) -> Self {
128 self.zero_bias = config;
129 self
130 }
131
132 // === Encoding Mode ===
133
134 /// Enable or disable progressive encoding.
135 ///
136 /// Progressive encoding produces multiple scans for incremental display.
137 /// Automatically enables optimized Huffman tables (required for progressive).
138 #[must_use]
139 pub fn progressive(mut self, enable: bool) -> Self {
140 self.progressive = enable;
141 if enable {
142 self.optimize_huffman = true;
143 }
144 self
145 }
146
147 /// Enable or disable Huffman table optimization.
148 ///
149 /// When enabled (default), computes optimal Huffman tables from image data.
150 /// When disabled, uses standard JPEG Huffman tables (faster but larger files).
151 ///
152 /// Note: Progressive mode requires optimized Huffman tables.
153 #[must_use]
154 pub fn optimize_huffman(mut self, enable: bool) -> Self {
155 self.optimize_huffman = enable;
156 self
157 }
158
159 /// Set the restart interval (MCUs between restart markers).
160 ///
161 /// Restart markers allow partial decoding and error recovery.
162 /// Set to 0 to disable restart markers (default).
163 #[must_use]
164 pub fn restart_interval(mut self, interval: u16) -> Self {
165 self.restart_interval = interval;
166 self
167 }
168
169 /// Enable parallel encoding for improved throughput on multi-core systems.
170 ///
171 /// When enabled, the encoder uses multiple threads for:
172 /// - DCT computation (block transforms)
173 /// - Entropy/Huffman encoding (via restart markers)
174 ///
175 /// # Restart Marker Behavior
176 ///
177 /// Parallel entropy encoding requires restart markers between segments.
178 /// When parallel encoding is enabled:
179 /// - If `restart_interval` is 0 or too small, it will be **increased** to an
180 /// optimal value based on thread count and image size
181 /// - User-specified `restart_interval` values are respected as a minimum
182 /// (the encoder may increase but will not decrease them)
183 ///
184 /// # Performance
185 ///
186 /// - 2 threads: ~1.2-1.6x speedup
187 /// - 4 threads: ~1.3-1.7x speedup
188 /// - Minimum useful size: ~512x512 (smaller images have too much overhead)
189 ///
190 /// # Example
191 ///
192 /// ```ignore
193 /// use jpegli::{EncoderConfig, ParallelEncoding};
194 ///
195 /// let config = EncoderConfig::new()
196 /// .quality(85)
197 /// .parallel(ParallelEncoding::Auto);
198 /// ```
199 ///
200 /// Requires the `parallel` feature flag.
201 #[cfg(feature = "parallel")]
202 #[must_use]
203 pub fn parallel(mut self, mode: super::encoder_types::ParallelEncoding) -> Self {
204 self.parallel = Some(mode);
205 self
206 }
207
208 /// Configure hybrid quantization (jpegli AQ + mozjpeg trellis).
209 ///
210 /// Allows fine-tuning all hybrid AQ+trellis parameters.
211 /// See [`HybridConfig`](crate::hybrid::config::HybridConfig) for available options.
212 ///
213 /// Requires the `experimental-hybrid-trellis` feature.
214 #[cfg(feature = "experimental-hybrid-trellis")]
215 #[must_use]
216 pub fn hybrid_config(mut self, config: crate::hybrid::config::HybridConfig) -> Self {
217 self.hybrid_config = config;
218 self
219 }
220
221 // === ICC Profile ===
222
223 /// Attach an ICC color profile to the output JPEG.
224 ///
225 /// The profile will be written as APP2 marker segments with the standard
226 /// "ICC_PROFILE" signature. Large profiles are automatically chunked
227 /// (max 65519 bytes per segment) as required by the ICC profile embedding spec.
228 ///
229 /// Common profiles:
230 /// - sRGB IEC61966-2.1 (~3KB)
231 /// - Display P3 (~0.5KB)
232 /// - Adobe RGB 1998 (~0.5KB)
233 ///
234 /// # Example
235 /// ```ignore
236 /// let srgb_profile = std::fs::read("sRGB.icc")?;
237 /// let config = EncoderConfig::new()
238 /// .quality(85)
239 /// .icc_profile(srgb_profile);
240 /// ```
241 #[must_use]
242 pub fn icc_profile(mut self, profile: impl Into<Vec<u8>>) -> Self {
243 self.icc_profile = Some(profile.into());
244 self
245 }
246
247 // === EXIF/XMP Metadata ===
248
249 /// Attach EXIF metadata to the output JPEG.
250 ///
251 /// Use [`Exif::raw`][super::exif::Exif::raw] for raw EXIF bytes, or
252 /// [`Exif::build`][super::exif::Exif::build] to construct from common fields.
253 ///
254 /// The two modes are mutually exclusive at compile time - you cannot
255 /// mix raw bytes with field-based building.
256 ///
257 /// # Examples
258 ///
259 /// Build from fields (orientation and copyright):
260 /// ```ignore
261 /// use jpegli::encoder::{EncoderConfig, ChromaSubsampling, Exif, Orientation};
262 ///
263 /// let config = EncoderConfig::new(85, ChromaSubsampling::Quarter)
264 /// .exif(Exif::build()
265 /// .orientation(Orientation::Rotate90)
266 /// .copyright("© 2024 Example Corp"));
267 /// ```
268 ///
269 /// Use raw EXIF bytes:
270 /// ```ignore
271 /// use jpegli::encoder::{EncoderConfig, ChromaSubsampling, Exif};
272 ///
273 /// let config = EncoderConfig::new(85, ChromaSubsampling::Quarter)
274 /// .exif(Exif::raw(my_exif_bytes));
275 /// ```
276 ///
277 /// # Notes
278 ///
279 /// - EXIF is placed immediately after SOI, before any other markers
280 /// - Raw bytes should be TIFF data without the "Exif\0\0" prefix (added automatically)
281 /// - Maximum size: 65527 bytes (larger data will be truncated)
282 #[must_use]
283 pub fn exif(mut self, exif: impl Into<super::exif::Exif>) -> Self {
284 self.exif_data = Some(exif.into());
285 self
286 }
287
288 /// Attach XMP metadata to the output JPEG.
289 ///
290 /// The data will be written as an APP1 marker segment with the standard
291 /// Adobe XMP namespace signature. The provided bytes should be the raw XMP
292 /// XML data without the APP1 marker or namespace prefix.
293 ///
294 /// XMP is placed after EXIF (if present) but before ICC profile.
295 ///
296 /// # Maximum Size
297 /// Standard XMP is limited to 65502 bytes (65535 - 2 length - 29 namespace - 2 padding).
298 /// For larger XMP data, use Extended XMP (not yet supported).
299 #[must_use]
300 pub fn xmp(mut self, data: impl Into<Vec<u8>>) -> Self {
301 self.xmp_data = Some(data.into());
302 self
303 }
304
305 // === Color Mode ===
306
307 /// Set the output color mode.
308 #[must_use]
309 pub fn color_mode(mut self, mode: ColorMode) -> Self {
310 self.color_mode = mode;
311 self
312 }
313
314 /// Set the chroma downsampling method.
315 ///
316 /// Only affects RGB/RGBX input with chroma subsampling enabled.
317 /// Ignored for grayscale, YCbCr input, or 4:4:4 subsampling.
318 #[must_use]
319 pub fn downsampling_method(mut self, method: DownsamplingMethod) -> Self {
320 self.downsampling_method = method;
321 self
322 }
323
324 /// Internal: Set edge padding strategy for partial MCU blocks.
325 #[doc(hidden)]
326 #[must_use]
327 pub fn edge_padding_internal(mut self, config: EdgePaddingConfig) -> Self {
328 self.edge_padding = config;
329 self
330 }
331
332 // === Convenience Shortcuts ===
333
334 /// Set YCbCr color mode with specified chroma subsampling.
335 ///
336 /// Common values:
337 /// - `ChromaSubsampling::None` (4:4:4) - default, best quality
338 /// - `ChromaSubsampling::Quarter` (4:2:0) - good compression, smaller files
339 /// - `ChromaSubsampling::HalfHorizontal` (4:2:2) - horizontal subsampling only
340 #[must_use]
341 pub fn ycbcr(self, subsampling: ChromaSubsampling) -> Self {
342 self.color_mode(ColorMode::YCbCr { subsampling })
343 }
344
345 /// Set XYB color mode with B-quarter subsampling (default, perceptually optimized).
346 ///
347 /// XYB is a perceptual color space that can achieve better quality at the same
348 /// file size for some images. Requires linear RGB input (f32 or u16).
349 #[must_use]
350 pub fn xyb(self) -> Self {
351 self.color_mode(ColorMode::Xyb {
352 subsampling: XybSubsampling::BQuarter,
353 })
354 }
355
356 /// Set XYB color mode with full resolution (no subsampling).
357 #[must_use]
358 pub fn xyb_full(self) -> Self {
359 self.color_mode(ColorMode::Xyb {
360 subsampling: XybSubsampling::Full,
361 })
362 }
363
364 /// Set grayscale output mode.
365 ///
366 /// Only the luminance channel is encoded. Works with any input format.
367 #[must_use]
368 pub fn grayscale(self) -> Self {
369 self.color_mode(ColorMode::Grayscale)
370 }
371
372 /// Enable or disable SharpYUV (GammaAwareIterative) downsampling.
373 ///
374 /// SharpYUV produces better color preservation on edges and thin lines,
375 /// at the cost of ~3x slower encoding.
376 #[must_use]
377 pub fn sharp_yuv(self, enable: bool) -> Self {
378 self.downsampling_method(if enable {
379 DownsamplingMethod::GammaAwareIterative
380 } else {
381 DownsamplingMethod::Box
382 })
383 }
384
385 // === Validation ===
386
387 /// Validate the configuration, returning an error for invalid combinations.
388 ///
389 /// Invalid combinations:
390 /// - Progressive mode with disabled Huffman optimization
391 pub fn validate(&self) -> Result<()> {
392 if self.progressive && !self.optimize_huffman {
393 return Err(crate::error::Error::invalid_config(
394 "progressive mode requires optimized Huffman tables".into(),
395 ));
396 }
397 Ok(())
398 }
399
400 // === Encoder Creation ===
401
402 /// Create an encoder from raw bytes with explicit pixel layout.
403 ///
404 /// Use this when working with raw byte buffers and you know the pixel layout.
405 ///
406 /// # Arguments
407 /// - `width`: Image width in pixels
408 /// - `height`: Image height in pixels
409 /// - `layout`: Pixel data layout (channel order, depth, color space)
410 ///
411 /// # Example
412 /// ```ignore
413 /// let config = EncoderConfig::new().quality(85);
414 /// let mut enc = config.encode_from_bytes(1920, 1080, PixelLayout::Rgb8Srgb)?;
415 /// enc.push_packed(&rgb_bytes, Unstoppable)?;
416 /// let jpeg = enc.finish()?;
417 /// ```
418 pub fn encode_from_bytes(
419 &self,
420 width: u32,
421 height: u32,
422 layout: PixelLayout,
423 ) -> Result<BytesEncoder> {
424 self.validate()?;
425 BytesEncoder::new(self.clone(), width, height, layout)
426 }
427
428 /// Create an encoder from rgb crate pixel types.
429 ///
430 /// Layout is inferred from the type parameter. For RGBA/BGRA types,
431 /// the 4th channel is ignored.
432 ///
433 /// # Type Parameter
434 /// - `P`: Pixel type from the `rgb` crate (e.g., `RGB<u8>`, `RGBA<f32>`)
435 ///
436 /// # Example
437 /// ```ignore
438 /// use rgb::RGB;
439 ///
440 /// let config = EncoderConfig::new().quality(85);
441 /// let mut enc = config.encode_from_rgb::<RGB<u8>>(1920, 1080)?;
442 /// enc.push_packed(&pixels, Unstoppable)?;
443 /// let jpeg = enc.finish()?;
444 /// ```
445 pub fn encode_from_rgb<P: super::byte_encoders::Pixel>(
446 &self,
447 width: u32,
448 height: u32,
449 ) -> Result<RgbEncoder<P>> {
450 self.validate()?;
451 RgbEncoder::new(self.clone(), width, height)
452 }
453
454 /// Create an encoder from planar YCbCr data.
455 ///
456 /// Use this when you have pre-converted YCbCr from video decoders, etc.
457 /// Skips RGB->YCbCr conversion entirely.
458 ///
459 /// Only valid with `ColorMode::YCbCr`. XYB mode requires RGB input.
460 ///
461 /// # Example
462 /// ```ignore
463 /// let config = EncoderConfig::new()
464 /// .quality(85)
465 /// .ycbcr(ChromaSubsampling::Quarter);
466 ///
467 /// let mut enc = config.encode_from_ycbcr_planar(1920, 1080)?;
468 /// enc.push(&planes, height, Unstoppable)?;
469 /// let jpeg = enc.finish()?;
470 /// ```
471 pub fn encode_from_ycbcr_planar(&self, width: u32, height: u32) -> Result<YCbCrPlanarEncoder> {
472 self.validate()?;
473
474 // Validate color mode
475 if !matches!(self.color_mode, ColorMode::YCbCr { .. }) {
476 return Err(crate::error::Error::invalid_config(
477 "planar YCbCr input requires YCbCr color mode".into(),
478 ));
479 }
480
481 YCbCrPlanarEncoder::new(self.clone(), width, height)
482 }
483
484 // === Resource Estimation ===
485
486 /// Estimate peak memory usage for encoding an image of the given dimensions.
487 ///
488 /// Returns estimated bytes based on color mode, subsampling, and dimensions.
489 /// Delegates to the streaming encoder's estimate which accounts for all
490 /// internal buffers.
491 #[must_use]
492 #[allow(deprecated)]
493 pub fn estimate_memory(&self, width: u32, height: u32) -> usize {
494 use crate::encode::streaming::StreamingEncoder;
495
496 let subsampling = match self.color_mode {
497 ColorMode::YCbCr { subsampling } => subsampling.to_legacy(),
498 ColorMode::Xyb { .. } => crate::types::Subsampling::S444,
499 ColorMode::Grayscale => crate::types::Subsampling::S444,
500 };
501
502 StreamingEncoder::new(width, height)
503 .subsampling(subsampling)
504 .optimize_huffman(self.optimize_huffman)
505 .estimate_memory_usage()
506 }
507
508 /// Returns an absolute ceiling on memory usage.
509 ///
510 /// Unlike `estimate_memory`, this returns a **guaranteed upper bound**
511 /// that actual peak memory will never exceed. Use this for resource reservation
512 /// when you need certainty rather than a close estimate.
513 ///
514 /// The ceiling accounts for:
515 /// - Worst-case token counts per block (high-frequency content)
516 /// - Maximum output buffer size (incompressible images)
517 /// - Vec capacity overhead (allocator rounding)
518 /// - All intermediate buffers at their maximum sizes
519 ///
520 /// # Example
521 ///
522 /// ```rust,ignore
523 /// use jpegli::encoder::EncoderConfig;
524 ///
525 /// let config = EncoderConfig::new().quality(85);
526 /// let ceiling = config.estimate_memory_ceiling(1920, 1080);
527 ///
528 /// // Reserve this much memory - actual usage guaranteed to be less
529 /// let buffer = Vec::with_capacity(ceiling);
530 /// ```
531 #[must_use]
532 #[allow(deprecated)]
533 pub fn estimate_memory_ceiling(&self, width: u32, height: u32) -> usize {
534 use crate::encode::streaming::StreamingEncoder;
535
536 let subsampling = match self.color_mode {
537 ColorMode::YCbCr { subsampling } => subsampling.to_legacy(),
538 ColorMode::Xyb { .. } => crate::types::Subsampling::S444,
539 ColorMode::Grayscale => crate::types::Subsampling::S444,
540 };
541
542 StreamingEncoder::new(width, height)
543 .subsampling(subsampling)
544 .estimate_memory_ceiling()
545 }
546
547 // === Accessors ===
548
549 /// Get the configured quality.
550 #[must_use]
551 pub fn get_quality(&self) -> Quality {
552 self.quality
553 }
554
555 /// Get the configured color mode.
556 #[must_use]
557 pub fn get_color_mode(&self) -> ColorMode {
558 self.color_mode
559 }
560
561 /// Check if progressive mode is enabled.
562 #[must_use]
563 pub fn is_progressive(&self) -> bool {
564 self.progressive
565 }
566
567 /// Check if Huffman optimization is enabled.
568 #[must_use]
569 pub fn is_optimize_huffman(&self) -> bool {
570 self.optimize_huffman
571 }
572
573 /// Get the ICC profile, if set.
574 #[must_use]
575 pub fn get_icc_profile(&self) -> Option<&[u8]> {
576 self.icc_profile.as_deref()
577 }
578
579 /// Get the EXIF data, if set.
580 #[must_use]
581 pub fn get_exif(&self) -> Option<&super::exif::Exif> {
582 self.exif_data.as_ref()
583 }
584
585 /// Get the XMP data, if set.
586 #[must_use]
587 pub fn get_xmp(&self) -> Option<&[u8]> {
588 self.xmp_data.as_deref()
589 }
590
591 /// Internal: Get the configured edge padding.
592 #[doc(hidden)]
593 #[must_use]
594 pub fn get_edge_padding(&self) -> EdgePaddingConfig {
595 self.edge_padding
596 }
597}
598
599#[cfg(test)]
600mod tests {
601 use super::*;
602
603 #[test]
604 fn test_default_config() {
605 let config = EncoderConfig::new(90.0, ChromaSubsampling::None);
606 assert!(matches!(config.quality, Quality::ApproxJpegli(90.0)));
607 assert!(!config.progressive);
608 assert!(config.optimize_huffman);
609 assert!(matches!(
610 config.color_mode,
611 ColorMode::YCbCr {
612 subsampling: ChromaSubsampling::None
613 }
614 ));
615 }
616
617 #[test]
618 fn test_builder_pattern() {
619 let config = EncoderConfig::new(85, ChromaSubsampling::None)
620 .progressive(true)
621 .sharp_yuv(true);
622
623 assert!(matches!(config.quality, Quality::ApproxJpegli(85.0)));
624 assert!(config.progressive);
625 assert!(config.optimize_huffman); // auto-enabled by progressive
626 assert!(matches!(
627 config.color_mode,
628 ColorMode::YCbCr {
629 subsampling: ChromaSubsampling::None
630 }
631 ));
632 assert!(matches!(
633 config.downsampling_method,
634 DownsamplingMethod::GammaAwareIterative
635 ));
636 }
637
638 #[test]
639 fn test_progressive_enables_huffman() {
640 let config = EncoderConfig::new(90.0, ChromaSubsampling::None)
641 .optimize_huffman(false)
642 .progressive(true);
643
644 assert!(config.optimize_huffman);
645 }
646
647 #[test]
648 fn test_validation_progressive_huffman() {
649 let mut config = EncoderConfig::new(90.0, ChromaSubsampling::None);
650 config.progressive = true;
651 config.optimize_huffman = false;
652
653 assert!(config.validate().is_err());
654 }
655
656 #[test]
657 fn test_xyb_shortcuts() {
658 let config = EncoderConfig::new(90.0, ChromaSubsampling::None).xyb();
659 assert!(matches!(
660 config.color_mode,
661 ColorMode::Xyb {
662 subsampling: XybSubsampling::BQuarter
663 }
664 ));
665
666 let config = EncoderConfig::new(90.0, ChromaSubsampling::None).xyb_full();
667 assert!(matches!(
668 config.color_mode,
669 ColorMode::Xyb {
670 subsampling: XybSubsampling::Full
671 }
672 ));
673 }
674}