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