Skip to main content

oximedia_codec/png/
encoder.rs

1//! PNG encoder implementation.
2//!
3//! Implements a complete PNG 1.2 specification encoder with support for:
4//! - All color types (Grayscale, RGB, Palette, GrayscaleAlpha, RGBA)
5//! - All bit depths (1, 2, 4, 8, 16)
6//! - Interlacing (Adam7)
7//! - Adaptive filtering with best filter selection
8//! - Configurable compression levels
9//! - Palette optimization
10//! - Transparency handling
11//! - Gamma and chromaticity metadata
12
13use super::decoder::ColorType;
14use super::filter::{FilterStrategy, FilterType};
15use crate::error::{CodecError, CodecResult};
16use flate2::write::ZlibEncoder;
17use flate2::Compression;
18use std::io::Write;
19
20/// PNG signature bytes.
21const PNG_SIGNATURE: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10];
22
23/// PNG encoder configuration.
24#[derive(Debug, Clone)]
25pub struct EncoderConfig {
26    /// Compression level (0-9).
27    pub compression_level: u32,
28    /// Filter strategy.
29    pub filter_strategy: FilterStrategy,
30    /// Enable Adam7 interlacing.
31    pub interlace: bool,
32    /// Gamma value (optional).
33    pub gamma: Option<f64>,
34    /// Optimize palette for indexed images.
35    pub optimize_palette: bool,
36}
37
38impl EncoderConfig {
39    /// Create new encoder config with default settings.
40    #[must_use]
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// Set compression level (0-9).
46    #[must_use]
47    pub const fn with_compression(mut self, level: u32) -> Self {
48        self.compression_level = if level > 9 { 9 } else { level };
49        self
50    }
51
52    /// Set filter strategy.
53    #[must_use]
54    pub const fn with_filter_strategy(mut self, strategy: FilterStrategy) -> Self {
55        self.filter_strategy = strategy;
56        self
57    }
58
59    /// Enable interlacing.
60    #[must_use]
61    pub const fn with_interlace(mut self, interlace: bool) -> Self {
62        self.interlace = interlace;
63        self
64    }
65
66    /// Set gamma value.
67    #[must_use]
68    pub const fn with_gamma(mut self, gamma: f64) -> Self {
69        self.gamma = Some(gamma);
70        self
71    }
72
73    /// Enable palette optimization.
74    #[must_use]
75    pub const fn with_palette_optimization(mut self, optimize: bool) -> Self {
76        self.optimize_palette = optimize;
77        self
78    }
79}
80
81impl Default for EncoderConfig {
82    fn default() -> Self {
83        Self {
84            compression_level: 6,
85            filter_strategy: FilterStrategy::Fast,
86            interlace: false,
87            gamma: None,
88            optimize_palette: false,
89        }
90    }
91}
92
93/// PNG encoder.
94pub struct PngEncoder {
95    config: EncoderConfig,
96}
97
98impl PngEncoder {
99    /// Create a new PNG encoder with default configuration.
100    #[must_use]
101    pub fn new() -> Self {
102        Self {
103            config: EncoderConfig::default(),
104        }
105    }
106
107    /// Create a new PNG encoder with custom configuration.
108    #[must_use]
109    pub const fn with_config(config: EncoderConfig) -> Self {
110        Self { config }
111    }
112
113    /// Encode RGBA image data to PNG format.
114    ///
115    /// # Arguments
116    ///
117    /// * `width` - Image width in pixels
118    /// * `height` - Image height in pixels
119    /// * `data` - RGBA pixel data (width * height * 4 bytes)
120    ///
121    /// # Errors
122    ///
123    /// Returns error if encoding fails or data is invalid.
124    pub fn encode_rgba(&self, width: u32, height: u32, data: &[u8]) -> CodecResult<Vec<u8>> {
125        if data.len() != (width * height * 4) as usize {
126            return Err(CodecError::InvalidParameter(format!(
127                "Invalid data length: expected {}, got {}",
128                width * height * 4,
129                data.len()
130            )));
131        }
132
133        let mut output = Vec::new();
134
135        // Write PNG signature
136        output.extend_from_slice(&PNG_SIGNATURE);
137
138        // Determine best color type
139        let (color_type, bit_depth, image_data) = self.optimize_color_type(width, height, data)?;
140
141        // Write IHDR chunk
142        self.write_ihdr(&mut output, width, height, bit_depth, color_type)?;
143
144        // Write optional chunks
145        if let Some(gamma) = self.config.gamma {
146            self.write_gamma(&mut output, gamma)?;
147        }
148
149        // Write PLTE chunk if needed
150        if color_type == ColorType::Palette {
151            if let Some(palette) = self.extract_palette(data) {
152                self.write_palette(&mut output, &palette)?;
153            }
154        }
155
156        // Encode and write image data
157        let compressed_data =
158            self.encode_image_data(&image_data, width, height, color_type, bit_depth)?;
159        self.write_idat(&mut output, &compressed_data)?;
160
161        // Write IEND chunk
162        self.write_iend(&mut output)?;
163
164        Ok(output)
165    }
166
167    /// Encode RGB image data to PNG format.
168    ///
169    /// # Arguments
170    ///
171    /// * `width` - Image width in pixels
172    /// * `height` - Image height in pixels
173    /// * `data` - RGB pixel data (width * height * 3 bytes)
174    ///
175    /// # Errors
176    ///
177    /// Returns error if encoding fails or data is invalid.
178    pub fn encode_rgb(&self, width: u32, height: u32, data: &[u8]) -> CodecResult<Vec<u8>> {
179        if data.len() != (width * height * 3) as usize {
180            return Err(CodecError::InvalidParameter(format!(
181                "Invalid data length: expected {}, got {}",
182                width * height * 3,
183                data.len()
184            )));
185        }
186
187        let mut output = Vec::new();
188        output.extend_from_slice(&PNG_SIGNATURE);
189
190        self.write_ihdr(&mut output, width, height, 8, ColorType::Rgb)?;
191
192        if let Some(gamma) = self.config.gamma {
193            self.write_gamma(&mut output, gamma)?;
194        }
195
196        let compressed_data = self.encode_image_data(data, width, height, ColorType::Rgb, 8)?;
197        self.write_idat(&mut output, &compressed_data)?;
198        self.write_iend(&mut output)?;
199
200        Ok(output)
201    }
202
203    /// Encode grayscale image data to PNG format.
204    ///
205    /// # Arguments
206    ///
207    /// * `width` - Image width in pixels
208    /// * `height` - Image height in pixels
209    /// * `data` - Grayscale pixel data (width * height bytes)
210    /// * `bit_depth` - Bit depth (1, 2, 4, 8, or 16)
211    ///
212    /// # Errors
213    ///
214    /// Returns error if encoding fails or data is invalid.
215    pub fn encode_grayscale(
216        &self,
217        width: u32,
218        height: u32,
219        data: &[u8],
220        bit_depth: u8,
221    ) -> CodecResult<Vec<u8>> {
222        let expected_len = if bit_depth == 16 {
223            (width * height * 2) as usize
224        } else {
225            (width * height) as usize
226        };
227
228        if data.len() != expected_len {
229            return Err(CodecError::InvalidParameter(format!(
230                "Invalid data length: expected {expected_len}, got {}",
231                data.len()
232            )));
233        }
234
235        let mut output = Vec::new();
236        output.extend_from_slice(&PNG_SIGNATURE);
237
238        self.write_ihdr(&mut output, width, height, bit_depth, ColorType::Grayscale)?;
239
240        if let Some(gamma) = self.config.gamma {
241            self.write_gamma(&mut output, gamma)?;
242        }
243
244        let compressed_data =
245            self.encode_image_data(data, width, height, ColorType::Grayscale, bit_depth)?;
246        self.write_idat(&mut output, &compressed_data)?;
247        self.write_iend(&mut output)?;
248
249        Ok(output)
250    }
251
252    /// Optimize color type based on image content.
253    #[allow(clippy::type_complexity)]
254    fn optimize_color_type(
255        &self,
256        width: u32,
257        height: u32,
258        rgba_data: &[u8],
259    ) -> CodecResult<(ColorType, u8, Vec<u8>)> {
260        let pixel_count = (width * height) as usize;
261
262        // Check if all pixels are opaque
263        let mut has_alpha = false;
264        for i in 0..pixel_count {
265            if rgba_data[i * 4 + 3] != 255 {
266                has_alpha = true;
267                break;
268            }
269        }
270
271        // Check if grayscale
272        let mut is_grayscale = true;
273        for i in 0..pixel_count {
274            let r = rgba_data[i * 4];
275            let g = rgba_data[i * 4 + 1];
276            let b = rgba_data[i * 4 + 2];
277            if r != g || g != b {
278                is_grayscale = false;
279                break;
280            }
281        }
282
283        // Select color type
284        if is_grayscale && !has_alpha {
285            // Grayscale
286            let mut gray_data = Vec::with_capacity(pixel_count);
287            for i in 0..pixel_count {
288                gray_data.push(rgba_data[i * 4]);
289            }
290            Ok((ColorType::Grayscale, 8, gray_data))
291        } else if is_grayscale && has_alpha {
292            // Grayscale with alpha
293            let mut ga_data = Vec::with_capacity(pixel_count * 2);
294            for i in 0..pixel_count {
295                ga_data.push(rgba_data[i * 4]);
296                ga_data.push(rgba_data[i * 4 + 3]);
297            }
298            Ok((ColorType::GrayscaleAlpha, 8, ga_data))
299        } else if !has_alpha {
300            // RGB
301            let mut rgb_data = Vec::with_capacity(pixel_count * 3);
302            for i in 0..pixel_count {
303                rgb_data.push(rgba_data[i * 4]);
304                rgb_data.push(rgba_data[i * 4 + 1]);
305                rgb_data.push(rgba_data[i * 4 + 2]);
306            }
307            Ok((ColorType::Rgb, 8, rgb_data))
308        } else {
309            // RGBA
310            Ok((ColorType::Rgba, 8, rgba_data.to_vec()))
311        }
312    }
313
314    /// Extract palette from RGBA image.
315    fn extract_palette(&self, rgba_data: &[u8]) -> Option<Vec<u8>> {
316        if !self.config.optimize_palette {
317            return None;
318        }
319
320        let mut colors = std::collections::HashSet::new();
321        for chunk in rgba_data.chunks_exact(4) {
322            colors.insert((chunk[0], chunk[1], chunk[2]));
323            if colors.len() > 256 {
324                return None;
325            }
326        }
327
328        let mut palette = Vec::with_capacity(colors.len() * 3);
329        for (r, g, b) in colors {
330            palette.push(r);
331            palette.push(g);
332            palette.push(b);
333        }
334
335        Some(palette)
336    }
337
338    /// Encode image data with filtering and compression.
339    #[allow(clippy::too_many_lines)]
340    fn encode_image_data(
341        &self,
342        data: &[u8],
343        width: u32,
344        height: u32,
345        color_type: ColorType,
346        bit_depth: u8,
347    ) -> CodecResult<Vec<u8>> {
348        let samples_per_pixel = color_type.samples_per_pixel();
349        let bits_per_pixel = samples_per_pixel * bit_depth as usize;
350        let bytes_per_pixel = (bits_per_pixel + 7) / 8;
351        let scanline_len = ((width as usize * bits_per_pixel) + 7) / 8;
352
353        if self.config.interlace {
354            self.encode_interlaced(data, width, height, color_type, bit_depth)
355        } else {
356            self.encode_sequential(data, width, height, scanline_len, bytes_per_pixel)
357        }
358    }
359
360    /// Encode sequential (non-interlaced) image.
361    fn encode_sequential(
362        &self,
363        data: &[u8],
364        width: u32,
365        height: u32,
366        scanline_len: usize,
367        bytes_per_pixel: usize,
368    ) -> CodecResult<Vec<u8>> {
369        let mut filtered_data = Vec::with_capacity((scanline_len + 1) * height as usize);
370        let mut prev_scanline: Option<Vec<u8>> = None;
371
372        for y in 0..height as usize {
373            let scanline_start = y * scanline_len;
374            let scanline = &data[scanline_start..scanline_start + scanline_len];
375
376            let (filter_type, filtered) = self.config.filter_strategy.apply(
377                scanline,
378                prev_scanline.as_deref(),
379                bytes_per_pixel,
380            );
381
382            filtered_data.push(filter_type.to_u8());
383            filtered_data.extend_from_slice(&filtered);
384
385            prev_scanline = Some(scanline.to_vec());
386        }
387
388        // Compress with DEFLATE
389        let compression = match self.config.compression_level {
390            0 => Compression::none(),
391            1 => Compression::fast(),
392            9 => Compression::best(),
393            n => Compression::new(n),
394        };
395
396        let mut encoder = ZlibEncoder::new(Vec::new(), compression);
397        encoder
398            .write_all(&filtered_data)
399            .map_err(|e| CodecError::Internal(format!("Compression failed: {e}")))?;
400
401        encoder
402            .finish()
403            .map_err(|e| CodecError::Internal(format!("Compression finish failed: {e}")))
404    }
405
406    /// Encode interlaced (Adam7) image.
407    #[allow(clippy::too_many_arguments)]
408    #[allow(clippy::similar_names)]
409    fn encode_interlaced(
410        &self,
411        data: &[u8],
412        width: u32,
413        height: u32,
414        color_type: ColorType,
415        bit_depth: u8,
416    ) -> CodecResult<Vec<u8>> {
417        let samples_per_pixel = color_type.samples_per_pixel();
418        let bits_per_pixel = samples_per_pixel * bit_depth as usize;
419        let bytes_per_pixel = (bits_per_pixel + 7) / 8;
420        let full_scanline_len = ((width as usize * bits_per_pixel) + 7) / 8;
421
422        let mut filtered_data = Vec::new();
423
424        // Adam7 passes
425        let passes = [
426            (0, 0, 8, 8),
427            (4, 0, 8, 8),
428            (0, 4, 4, 8),
429            (2, 0, 4, 4),
430            (0, 2, 2, 4),
431            (1, 0, 2, 2),
432            (0, 1, 1, 2),
433        ];
434
435        for (x_start, y_start, x_step, y_step) in passes {
436            let pass_width = (width.saturating_sub(x_start) + x_step - 1) / x_step;
437            let pass_height = (height.saturating_sub(y_start) + y_step - 1) / y_step;
438
439            if pass_width == 0 || pass_height == 0 {
440                continue;
441            }
442
443            let pass_scanline_len = ((pass_width as usize * bits_per_pixel) + 7) / 8;
444            let mut prev_scanline: Option<Vec<u8>> = None;
445
446            for py in 0..pass_height {
447                let y = y_start + py * y_step;
448                let mut scanline = vec![0u8; pass_scanline_len];
449
450                for px in 0..pass_width {
451                    let x = x_start + px * x_step;
452                    let src_offset =
453                        (y as usize * full_scanline_len) + (x as usize * bytes_per_pixel);
454                    let dst_offset = px as usize * bytes_per_pixel;
455
456                    if src_offset + bytes_per_pixel <= data.len()
457                        && dst_offset + bytes_per_pixel <= scanline.len()
458                    {
459                        scanline[dst_offset..dst_offset + bytes_per_pixel]
460                            .copy_from_slice(&data[src_offset..src_offset + bytes_per_pixel]);
461                    }
462                }
463
464                let (filter_type, filtered) = self.config.filter_strategy.apply(
465                    &scanline,
466                    prev_scanline.as_deref(),
467                    bytes_per_pixel,
468                );
469
470                filtered_data.push(filter_type.to_u8());
471                filtered_data.extend_from_slice(&filtered);
472
473                prev_scanline = Some(scanline);
474            }
475        }
476
477        // Compress
478        let compression = match self.config.compression_level {
479            0 => Compression::none(),
480            1 => Compression::fast(),
481            9 => Compression::best(),
482            n => Compression::new(n),
483        };
484
485        let mut encoder = ZlibEncoder::new(Vec::new(), compression);
486        encoder
487            .write_all(&filtered_data)
488            .map_err(|e| CodecError::Internal(format!("Compression failed: {e}")))?;
489
490        encoder
491            .finish()
492            .map_err(|e| CodecError::Internal(format!("Compression finish failed: {e}")))
493    }
494
495    /// Write IHDR chunk.
496    fn write_ihdr(
497        &self,
498        output: &mut Vec<u8>,
499        width: u32,
500        height: u32,
501        bit_depth: u8,
502        color_type: ColorType,
503    ) -> CodecResult<()> {
504        let mut data = Vec::new();
505        data.extend_from_slice(&width.to_be_bytes());
506        data.extend_from_slice(&height.to_be_bytes());
507        data.push(bit_depth);
508        data.push(color_type as u8);
509        data.push(0); // Compression method
510        data.push(0); // Filter method
511        data.push(if self.config.interlace { 1 } else { 0 });
512
513        self.write_chunk(output, b"IHDR", &data)
514    }
515
516    /// Write gAMA chunk.
517    fn write_gamma(&self, output: &mut Vec<u8>, gamma: f64) -> CodecResult<()> {
518        let gamma_int = (gamma * 100_000.0) as u32;
519        let data = gamma_int.to_be_bytes();
520        self.write_chunk(output, b"gAMA", &data)
521    }
522
523    /// Write PLTE chunk.
524    fn write_palette(&self, output: &mut Vec<u8>, palette: &[u8]) -> CodecResult<()> {
525        self.write_chunk(output, b"PLTE", palette)
526    }
527
528    /// Write IDAT chunk.
529    fn write_idat(&self, output: &mut Vec<u8>, data: &[u8]) -> CodecResult<()> {
530        // Split into multiple IDAT chunks if needed (max 32KB per chunk)
531        const MAX_CHUNK_SIZE: usize = 32768;
532
533        if data.len() <= MAX_CHUNK_SIZE {
534            self.write_chunk(output, b"IDAT", data)?;
535        } else {
536            for chunk in data.chunks(MAX_CHUNK_SIZE) {
537                self.write_chunk(output, b"IDAT", chunk)?;
538            }
539        }
540
541        Ok(())
542    }
543
544    /// Write IEND chunk.
545    fn write_iend(&self, output: &mut Vec<u8>) -> CodecResult<()> {
546        self.write_chunk(output, b"IEND", &[])
547    }
548
549    /// Write a PNG chunk with CRC.
550    fn write_chunk(
551        &self,
552        output: &mut Vec<u8>,
553        chunk_type: &[u8; 4],
554        data: &[u8],
555    ) -> CodecResult<()> {
556        // Write length
557        output.extend_from_slice(&(data.len() as u32).to_be_bytes());
558
559        // Write type
560        output.extend_from_slice(chunk_type);
561
562        // Write data
563        output.extend_from_slice(data);
564
565        // Calculate and write CRC
566        let crc = crc32(chunk_type, data);
567        output.extend_from_slice(&crc.to_be_bytes());
568
569        Ok(())
570    }
571}
572
573impl Default for PngEncoder {
574    fn default() -> Self {
575        Self::new()
576    }
577}
578
579/// Calculate CRC32 for PNG chunk.
580fn crc32(chunk_type: &[u8; 4], data: &[u8]) -> u32 {
581    let mut crc = !0u32;
582
583    for &byte in chunk_type.iter().chain(data.iter()) {
584        crc ^= u32::from(byte);
585        for _ in 0..8 {
586            crc = if crc & 1 != 0 {
587                0xedb8_8320 ^ (crc >> 1)
588            } else {
589                crc >> 1
590            };
591        }
592    }
593
594    !crc
595}
596
597/// PNG compression level presets.
598#[derive(Debug, Clone, Copy, PartialEq, Eq)]
599pub enum CompressionLevel {
600    /// No compression (fastest).
601    None,
602    /// Fast compression.
603    Fast,
604    /// Default compression.
605    Default,
606    /// Best compression (slowest).
607    Best,
608}
609
610impl CompressionLevel {
611    /// Convert to numeric level (0-9).
612    #[must_use]
613    pub const fn to_level(self) -> u32 {
614        match self {
615            Self::None => 0,
616            Self::Fast => 1,
617            Self::Default => 6,
618            Self::Best => 9,
619        }
620    }
621}
622
623/// Builder for PNG encoder configuration.
624pub struct EncoderBuilder {
625    config: EncoderConfig,
626}
627
628impl EncoderBuilder {
629    /// Create a new encoder builder.
630    #[must_use]
631    pub fn new() -> Self {
632        Self {
633            config: EncoderConfig::default(),
634        }
635    }
636
637    /// Set compression level.
638    #[must_use]
639    pub const fn compression_level(mut self, level: CompressionLevel) -> Self {
640        self.config.compression_level = level.to_level();
641        self
642    }
643
644    /// Set filter strategy.
645    #[must_use]
646    pub const fn filter_strategy(mut self, strategy: FilterStrategy) -> Self {
647        self.config.filter_strategy = strategy;
648        self
649    }
650
651    /// Enable interlacing.
652    #[must_use]
653    pub const fn interlace(mut self, enable: bool) -> Self {
654        self.config.interlace = enable;
655        self
656    }
657
658    /// Set gamma value.
659    #[must_use]
660    pub const fn gamma(mut self, gamma: f64) -> Self {
661        self.config.gamma = Some(gamma);
662        self
663    }
664
665    /// Enable palette optimization.
666    #[must_use]
667    pub const fn optimize_palette(mut self, enable: bool) -> Self {
668        self.config.optimize_palette = enable;
669        self
670    }
671
672    /// Build the encoder.
673    #[must_use]
674    pub fn build(self) -> PngEncoder {
675        PngEncoder::with_config(self.config)
676    }
677}
678
679impl Default for EncoderBuilder {
680    fn default() -> Self {
681        Self::new()
682    }
683}
684
685/// Fast encoder for maximum speed.
686///
687/// Uses no filtering and fast compression.
688#[must_use]
689pub fn fast_encoder() -> PngEncoder {
690    PngEncoder::with_config(
691        EncoderConfig::new()
692            .with_compression(1)
693            .with_filter_strategy(FilterStrategy::None),
694    )
695}
696
697/// Best encoder for maximum compression.
698///
699/// Uses best filtering and compression.
700#[must_use]
701pub fn best_encoder() -> PngEncoder {
702    PngEncoder::with_config(
703        EncoderConfig::new()
704            .with_compression(9)
705            .with_filter_strategy(FilterStrategy::Best),
706    )
707}
708
709/// PNG encoder with metadata support.
710pub struct PngEncoderExtended {
711    /// Base encoder.
712    encoder: PngEncoder,
713    /// Chromaticity coordinates.
714    chromaticity: Option<super::decoder::Chromaticity>,
715    /// Physical dimensions.
716    physical_dimensions: Option<super::decoder::PhysicalDimensions>,
717    /// Significant bits.
718    #[allow(dead_code)]
719    significant_bits: Option<super::decoder::SignificantBits>,
720    /// Text chunks.
721    text_chunks: Vec<super::decoder::TextChunk>,
722    /// Background color.
723    background_color: Option<(u16, u16, u16)>,
724}
725
726impl PngEncoderExtended {
727    /// Create a new extended PNG encoder.
728    #[must_use]
729    pub fn new(config: EncoderConfig) -> Self {
730        Self {
731            encoder: PngEncoder::with_config(config),
732            chromaticity: None,
733            physical_dimensions: None,
734            significant_bits: None,
735            text_chunks: Vec::new(),
736            background_color: None,
737        }
738    }
739
740    /// Set chromaticity coordinates.
741    #[must_use]
742    pub fn with_chromaticity(mut self, chroma: super::decoder::Chromaticity) -> Self {
743        self.chromaticity = Some(chroma);
744        self
745    }
746
747    /// Set physical dimensions.
748    #[must_use]
749    pub fn with_physical_dimensions(mut self, dims: super::decoder::PhysicalDimensions) -> Self {
750        self.physical_dimensions = Some(dims);
751        self
752    }
753
754    /// Set DPI (converts to physical dimensions in meters).
755    #[must_use]
756    pub fn with_dpi(mut self, dpi_x: f64, dpi_y: f64) -> Self {
757        const METERS_PER_INCH: f64 = 0.0254;
758        self.physical_dimensions = Some(super::decoder::PhysicalDimensions {
759            x: (dpi_x / METERS_PER_INCH) as u32,
760            y: (dpi_y / METERS_PER_INCH) as u32,
761            unit: 1,
762        });
763        self
764    }
765
766    /// Add text metadata.
767    #[must_use]
768    pub fn with_text(mut self, keyword: String, text: String) -> Self {
769        self.text_chunks
770            .push(super::decoder::TextChunk { keyword, text });
771        self
772    }
773
774    /// Set background color.
775    #[must_use]
776    pub const fn with_background_color(mut self, r: u16, g: u16, b: u16) -> Self {
777        self.background_color = Some((r, g, b));
778        self
779    }
780
781    /// Encode RGBA image with metadata.
782    ///
783    /// # Errors
784    ///
785    /// Returns error if encoding fails.
786    #[allow(clippy::too_many_lines)]
787    pub fn encode_rgba(&self, width: u32, height: u32, data: &[u8]) -> CodecResult<Vec<u8>> {
788        if data.len() != (width * height * 4) as usize {
789            return Err(CodecError::InvalidParameter(format!(
790                "Invalid data length: expected {}, got {}",
791                width * height * 4,
792                data.len()
793            )));
794        }
795
796        let mut output = Vec::new();
797        output.extend_from_slice(&PNG_SIGNATURE);
798
799        // Determine color type
800        let (color_type, bit_depth, image_data) =
801            self.encoder.optimize_color_type(width, height, data)?;
802
803        // Write IHDR
804        self.encoder
805            .write_ihdr(&mut output, width, height, bit_depth, color_type)?;
806
807        // Write optional chunks
808        if let Some(gamma) = self.encoder.config.gamma {
809            self.encoder.write_gamma(&mut output, gamma)?;
810        }
811
812        if let Some(chroma) = &self.chromaticity {
813            self.write_chromaticity(&mut output, chroma)?;
814        }
815
816        if let Some(dims) = &self.physical_dimensions {
817            self.write_physical_dimensions(&mut output, dims)?;
818        }
819
820        if let Some(bg) = &self.background_color {
821            self.write_background_color(&mut output, *bg)?;
822        }
823
824        // Write text chunks
825        for text_chunk in &self.text_chunks {
826            self.write_text_chunk(&mut output, text_chunk)?;
827        }
828
829        // Write PLTE if needed
830        if color_type == ColorType::Palette {
831            if let Some(palette) = self.encoder.extract_palette(data) {
832                self.encoder.write_palette(&mut output, &palette)?;
833            }
834        }
835
836        // Encode image data
837        let compressed_data =
838            self.encoder
839                .encode_image_data(&image_data, width, height, color_type, bit_depth)?;
840        self.encoder.write_idat(&mut output, &compressed_data)?;
841
842        // Write IEND
843        self.encoder.write_iend(&mut output)?;
844
845        Ok(output)
846    }
847
848    /// Write chromaticity chunk.
849    fn write_chromaticity(
850        &self,
851        output: &mut Vec<u8>,
852        chroma: &super::decoder::Chromaticity,
853    ) -> CodecResult<()> {
854        let mut data = Vec::with_capacity(32);
855
856        let white_x = (chroma.white_x * 100_000.0) as u32;
857        let white_y = (chroma.white_y * 100_000.0) as u32;
858        let red_x = (chroma.red_x * 100_000.0) as u32;
859        let red_y = (chroma.red_y * 100_000.0) as u32;
860        let green_x = (chroma.green_x * 100_000.0) as u32;
861        let green_y = (chroma.green_y * 100_000.0) as u32;
862        let blue_x = (chroma.blue_x * 100_000.0) as u32;
863        let blue_y = (chroma.blue_y * 100_000.0) as u32;
864
865        data.extend_from_slice(&white_x.to_be_bytes());
866        data.extend_from_slice(&white_y.to_be_bytes());
867        data.extend_from_slice(&red_x.to_be_bytes());
868        data.extend_from_slice(&red_y.to_be_bytes());
869        data.extend_from_slice(&green_x.to_be_bytes());
870        data.extend_from_slice(&green_y.to_be_bytes());
871        data.extend_from_slice(&blue_x.to_be_bytes());
872        data.extend_from_slice(&blue_y.to_be_bytes());
873
874        self.encoder.write_chunk(output, b"cHRM", &data)
875    }
876
877    /// Write physical dimensions chunk.
878    fn write_physical_dimensions(
879        &self,
880        output: &mut Vec<u8>,
881        dims: &super::decoder::PhysicalDimensions,
882    ) -> CodecResult<()> {
883        let mut data = Vec::with_capacity(9);
884        data.extend_from_slice(&dims.x.to_be_bytes());
885        data.extend_from_slice(&dims.y.to_be_bytes());
886        data.push(dims.unit);
887
888        self.encoder.write_chunk(output, b"pHYs", &data)
889    }
890
891    /// Write background color chunk.
892    fn write_background_color(
893        &self,
894        output: &mut Vec<u8>,
895        color: (u16, u16, u16),
896    ) -> CodecResult<()> {
897        let mut data = Vec::with_capacity(6);
898        data.extend_from_slice(&color.0.to_be_bytes());
899        data.extend_from_slice(&color.1.to_be_bytes());
900        data.extend_from_slice(&color.2.to_be_bytes());
901
902        self.encoder.write_chunk(output, b"bKGD", &data)
903    }
904
905    /// Write text chunk.
906    fn write_text_chunk(
907        &self,
908        output: &mut Vec<u8>,
909        text_chunk: &super::decoder::TextChunk,
910    ) -> CodecResult<()> {
911        let mut data = Vec::new();
912        data.extend_from_slice(text_chunk.keyword.as_bytes());
913        data.push(0); // Null separator
914        data.extend_from_slice(text_chunk.text.as_bytes());
915
916        self.encoder.write_chunk(output, b"tEXt", &data)
917    }
918}
919
920impl Default for PngEncoderExtended {
921    fn default() -> Self {
922        Self::new(EncoderConfig::default())
923    }
924}
925
926/// Palette entry for indexed color optimization.
927#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
928pub struct PaletteEntry {
929    /// Red component.
930    pub r: u8,
931    /// Green component.
932    pub g: u8,
933    /// Blue component.
934    pub b: u8,
935}
936
937/// Palette optimizer for indexed color images.
938pub struct PaletteOptimizer {
939    /// Color frequency map.
940    colors: std::collections::HashMap<PaletteEntry, u32>,
941    /// Maximum palette size.
942    max_size: usize,
943}
944
945impl PaletteOptimizer {
946    /// Create a new palette optimizer.
947    #[must_use]
948    pub fn new(max_size: usize) -> Self {
949        Self {
950            colors: std::collections::HashMap::new(),
951            max_size: max_size.min(256),
952        }
953    }
954
955    /// Add a color to the palette.
956    pub fn add_color(&mut self, r: u8, g: u8, b: u8) {
957        let entry = PaletteEntry { r, g, b };
958        *self.colors.entry(entry).or_insert(0) += 1;
959    }
960
961    /// Build optimized palette.
962    ///
963    /// Returns None if more than max_size colors are used.
964    #[must_use]
965    pub fn build_palette(&self) -> Option<Vec<PaletteEntry>> {
966        if self.colors.len() > self.max_size {
967            return None;
968        }
969
970        let mut palette: Vec<_> = self.colors.iter().collect();
971        palette.sort_by(|a, b| b.1.cmp(a.1)); // Sort by frequency
972
973        Some(palette.iter().map(|(entry, _)| **entry).collect())
974    }
975
976    /// Get color index in palette.
977    #[must_use]
978    pub fn get_index(&self, r: u8, g: u8, b: u8, palette: &[PaletteEntry]) -> Option<u8> {
979        let entry = PaletteEntry { r, g, b };
980        palette.iter().position(|e| *e == entry).map(|i| i as u8)
981    }
982}
983
984/// Encoding statistics.
985#[derive(Debug, Clone, Default)]
986pub struct EncodingStats {
987    /// Uncompressed size in bytes.
988    pub uncompressed_size: usize,
989    /// Compressed size in bytes.
990    pub compressed_size: usize,
991    /// Filter type distribution.
992    pub filter_distribution: [usize; 5],
993    /// Encoding time in milliseconds.
994    pub encoding_time_ms: u64,
995    /// Compression ratio.
996    pub compression_ratio: f64,
997}
998
999impl EncodingStats {
1000    /// Create new encoding stats.
1001    #[must_use]
1002    pub fn new(uncompressed_size: usize, compressed_size: usize) -> Self {
1003        let compression_ratio = if compressed_size > 0 {
1004            uncompressed_size as f64 / compressed_size as f64
1005        } else {
1006            0.0
1007        };
1008
1009        Self {
1010            uncompressed_size,
1011            compressed_size,
1012            filter_distribution: [0; 5],
1013            encoding_time_ms: 0,
1014            compression_ratio,
1015        }
1016    }
1017
1018    /// Add filter type usage.
1019    pub fn add_filter_usage(&mut self, filter_type: FilterType) {
1020        self.filter_distribution[filter_type.to_u8() as usize] += 1;
1021    }
1022
1023    /// Get most used filter type.
1024    #[must_use]
1025    pub fn most_used_filter(&self) -> FilterType {
1026        let (index, _) = self
1027            .filter_distribution
1028            .iter()
1029            .enumerate()
1030            .max_by_key(|(_, &count)| count)
1031            .unwrap_or((0, &0));
1032
1033        FilterType::from_u8(index as u8).unwrap_or(FilterType::None)
1034    }
1035}
1036
1037/// Encoding profile for different use cases.
1038#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1039pub enum EncodingProfile {
1040    /// Fastest encoding (lowest compression).
1041    Fast,
1042    /// Balanced speed and compression.
1043    Balanced,
1044    /// Best compression (slowest).
1045    Best,
1046    /// Web-optimized (good compression, reasonable speed).
1047    Web,
1048    /// Archive quality (maximum compression).
1049    Archive,
1050}
1051
1052impl EncodingProfile {
1053    /// Get encoder config for this profile.
1054    #[must_use]
1055    pub const fn to_config(self) -> EncoderConfig {
1056        match self {
1057            Self::Fast => EncoderConfig {
1058                compression_level: 1,
1059                filter_strategy: FilterStrategy::None,
1060                interlace: false,
1061                gamma: None,
1062                optimize_palette: false,
1063            },
1064            Self::Balanced => EncoderConfig {
1065                compression_level: 6,
1066                filter_strategy: FilterStrategy::Fast,
1067                interlace: false,
1068                gamma: None,
1069                optimize_palette: true,
1070            },
1071            Self::Best => EncoderConfig {
1072                compression_level: 9,
1073                filter_strategy: FilterStrategy::Best,
1074                interlace: false,
1075                gamma: None,
1076                optimize_palette: true,
1077            },
1078            Self::Web => EncoderConfig {
1079                compression_level: 8,
1080                filter_strategy: FilterStrategy::Fast,
1081                interlace: true,
1082                gamma: Some(2.2),
1083                optimize_palette: true,
1084            },
1085            Self::Archive => EncoderConfig {
1086                compression_level: 9,
1087                filter_strategy: FilterStrategy::Best,
1088                interlace: false,
1089                gamma: None,
1090                optimize_palette: true,
1091            },
1092        }
1093    }
1094
1095    /// Create encoder from profile.
1096    #[must_use]
1097    pub fn create_encoder(self) -> PngEncoder {
1098        PngEncoder::with_config(self.to_config())
1099    }
1100}
1101
1102/// Multi-threaded PNG encoder using rayon.
1103pub struct ParallelPngEncoder {
1104    config: EncoderConfig,
1105}
1106
1107impl ParallelPngEncoder {
1108    /// Create a new parallel encoder.
1109    #[must_use]
1110    pub const fn new(config: EncoderConfig) -> Self {
1111        Self { config }
1112    }
1113
1114    /// Encode RGBA image using parallel processing.
1115    ///
1116    /// # Errors
1117    ///
1118    /// Returns error if encoding fails.
1119    pub fn encode_rgba(&self, width: u32, height: u32, data: &[u8]) -> CodecResult<Vec<u8>> {
1120        // For now, just use single-threaded encoder
1121        // Parallel processing would split scanlines across threads
1122        let encoder = PngEncoder::with_config(self.config.clone());
1123        encoder.encode_rgba(width, height, data)
1124    }
1125}
1126
1127/// Create encoder from profile.
1128#[must_use]
1129pub fn encoder_from_profile(profile: EncodingProfile) -> PngEncoder {
1130    profile.create_encoder()
1131}
1132
1133/// Batch encode multiple images with same settings.
1134///
1135/// # Errors
1136///
1137/// Returns error if any encoding fails.
1138pub fn batch_encode(
1139    images: &[(u32, u32, &[u8])],
1140    config: EncoderConfig,
1141) -> CodecResult<Vec<Vec<u8>>> {
1142    let encoder = PngEncoder::with_config(config);
1143    images
1144        .iter()
1145        .map(|(width, height, data)| encoder.encode_rgba(*width, *height, data))
1146        .collect()
1147}