Skip to main content

oximedia_codec/png/
decoder.rs

1//! PNG decoder implementation.
2//!
3//! Implements a complete PNG 1.2 specification decoder 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//! - All filter types (0-4)
8//! - Ancillary chunks (tRNS, gAMA, cHRM, etc.)
9//! - CRC validation
10
11use super::filter::{unfilter, FilterType};
12use crate::error::{CodecError, CodecResult};
13use bytes::Bytes;
14use flate2::read::ZlibDecoder;
15use std::io::Read;
16
17/// PNG signature bytes.
18const PNG_SIGNATURE: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10];
19
20/// PNG color types.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ColorType {
23    /// Grayscale.
24    Grayscale = 0,
25    /// RGB truecolor.
26    Rgb = 2,
27    /// Indexed color (palette).
28    Palette = 3,
29    /// Grayscale with alpha.
30    GrayscaleAlpha = 4,
31    /// RGB with alpha.
32    Rgba = 6,
33}
34
35impl ColorType {
36    /// Create color type from byte.
37    ///
38    /// # Errors
39    ///
40    /// Returns error if color type is invalid.
41    pub fn from_u8(value: u8) -> CodecResult<Self> {
42        match value {
43            0 => Ok(Self::Grayscale),
44            2 => Ok(Self::Rgb),
45            3 => Ok(Self::Palette),
46            4 => Ok(Self::GrayscaleAlpha),
47            6 => Ok(Self::Rgba),
48            _ => Err(CodecError::InvalidData(format!(
49                "Invalid color type: {value}"
50            ))),
51        }
52    }
53
54    /// Get number of samples per pixel.
55    #[must_use]
56    pub const fn samples_per_pixel(self) -> usize {
57        match self {
58            Self::Grayscale => 1,
59            Self::Rgb => 3,
60            Self::Palette => 1,
61            Self::GrayscaleAlpha => 2,
62            Self::Rgba => 4,
63        }
64    }
65
66    /// Check if color type has alpha channel.
67    #[must_use]
68    pub const fn has_alpha(self) -> bool {
69        matches!(self, Self::GrayscaleAlpha | Self::Rgba)
70    }
71}
72
73/// PNG image header (IHDR chunk).
74#[derive(Debug, Clone)]
75pub struct ImageHeader {
76    /// Image width in pixels.
77    pub width: u32,
78    /// Image height in pixels.
79    pub height: u32,
80    /// Bit depth.
81    pub bit_depth: u8,
82    /// Color type.
83    pub color_type: ColorType,
84    /// Compression method (always 0 for PNG).
85    pub compression: u8,
86    /// Filter method (always 0 for PNG).
87    pub filter_method: u8,
88    /// Interlace method (0 = none, 1 = Adam7).
89    pub interlace: u8,
90}
91
92impl ImageHeader {
93    /// Parse IHDR chunk data.
94    ///
95    /// # Errors
96    ///
97    /// Returns error if IHDR data is invalid.
98    pub fn parse(data: &[u8]) -> CodecResult<Self> {
99        if data.len() < 13 {
100            return Err(CodecError::InvalidData("IHDR too short".into()));
101        }
102
103        let width = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
104        let height = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
105        let bit_depth = data[8];
106        let color_type = ColorType::from_u8(data[9])?;
107        let compression = data[10];
108        let filter_method = data[11];
109        let interlace = data[12];
110
111        if width == 0 || height == 0 {
112            return Err(CodecError::InvalidData("Invalid dimensions".into()));
113        }
114
115        if compression != 0 {
116            return Err(CodecError::UnsupportedFeature(format!(
117                "Unsupported compression method: {compression}"
118            )));
119        }
120
121        if filter_method != 0 {
122            return Err(CodecError::UnsupportedFeature(format!(
123                "Unsupported filter method: {filter_method}"
124            )));
125        }
126
127        if interlace > 1 {
128            return Err(CodecError::InvalidData(format!(
129                "Invalid interlace method: {interlace}"
130            )));
131        }
132
133        // Validate bit depth for color type
134        let valid_depths = match color_type {
135            ColorType::Grayscale => &[1, 2, 4, 8, 16][..],
136            ColorType::Rgb => &[8, 16][..],
137            ColorType::Palette => &[1, 2, 4, 8][..],
138            ColorType::GrayscaleAlpha => &[8, 16][..],
139            ColorType::Rgba => &[8, 16][..],
140        };
141
142        if !valid_depths.contains(&bit_depth) {
143            return Err(CodecError::InvalidData(format!(
144                "Invalid bit depth {bit_depth} for color type {color_type:?}"
145            )));
146        }
147
148        Ok(Self {
149            width,
150            height,
151            bit_depth,
152            color_type,
153            compression,
154            filter_method,
155            interlace,
156        })
157    }
158
159    /// Get bytes per pixel (rounded up).
160    #[must_use]
161    pub fn bytes_per_pixel(&self) -> usize {
162        let bits_per_pixel = self.color_type.samples_per_pixel() * self.bit_depth as usize;
163        (bits_per_pixel + 7) / 8
164    }
165
166    /// Get scanline length in bytes.
167    #[must_use]
168    pub fn scanline_length(&self) -> usize {
169        let bits_per_scanline =
170            self.width as usize * self.color_type.samples_per_pixel() * self.bit_depth as usize;
171        (bits_per_scanline + 7) / 8
172    }
173}
174
175/// PNG chunk.
176#[derive(Debug)]
177struct Chunk {
178    /// Chunk type (4 bytes).
179    chunk_type: [u8; 4],
180    /// Chunk data.
181    data: Vec<u8>,
182}
183
184impl Chunk {
185    /// Read a chunk from data.
186    fn read(data: &[u8], offset: &mut usize) -> CodecResult<Self> {
187        if *offset + 12 > data.len() {
188            return Err(CodecError::InvalidData("Incomplete chunk".into()));
189        }
190
191        let length = u32::from_be_bytes([
192            data[*offset],
193            data[*offset + 1],
194            data[*offset + 2],
195            data[*offset + 3],
196        ]) as usize;
197        *offset += 4;
198
199        let chunk_type = [
200            data[*offset],
201            data[*offset + 1],
202            data[*offset + 2],
203            data[*offset + 3],
204        ];
205        *offset += 4;
206
207        if *offset + length + 4 > data.len() {
208            return Err(CodecError::InvalidData("Incomplete chunk data".into()));
209        }
210
211        let chunk_data = data[*offset..*offset + length].to_vec();
212        *offset += length;
213
214        let expected_crc = u32::from_be_bytes([
215            data[*offset],
216            data[*offset + 1],
217            data[*offset + 2],
218            data[*offset + 3],
219        ]);
220        *offset += 4;
221
222        // Validate CRC
223        let actual_crc = crc32(&chunk_type, &chunk_data);
224        if actual_crc != expected_crc {
225            return Err(CodecError::InvalidData(format!(
226                "CRC mismatch for chunk {:?}",
227                std::str::from_utf8(&chunk_type).unwrap_or("???")
228            )));
229        }
230
231        Ok(Self {
232            chunk_type,
233            data: chunk_data,
234        })
235    }
236
237    /// Get chunk type as string.
238    fn type_str(&self) -> &str {
239        std::str::from_utf8(&self.chunk_type).unwrap_or("????")
240    }
241}
242
243/// Calculate CRC32 for PNG chunk.
244fn crc32(chunk_type: &[u8; 4], data: &[u8]) -> u32 {
245    let mut crc = !0u32;
246
247    for &byte in chunk_type.iter().chain(data.iter()) {
248        crc ^= u32::from(byte);
249        for _ in 0..8 {
250            crc = if crc & 1 != 0 {
251                0xedb8_8320 ^ (crc >> 1)
252            } else {
253                crc >> 1
254            };
255        }
256    }
257
258    !crc
259}
260
261/// Adam7 interlacing pass information.
262struct Adam7Pass {
263    x_start: u32,
264    y_start: u32,
265    x_step: u32,
266    y_step: u32,
267}
268
269const ADAM7_PASSES: [Adam7Pass; 7] = [
270    Adam7Pass {
271        x_start: 0,
272        y_start: 0,
273        x_step: 8,
274        y_step: 8,
275    },
276    Adam7Pass {
277        x_start: 4,
278        y_start: 0,
279        x_step: 8,
280        y_step: 8,
281    },
282    Adam7Pass {
283        x_start: 0,
284        y_start: 4,
285        x_step: 4,
286        y_step: 8,
287    },
288    Adam7Pass {
289        x_start: 2,
290        y_start: 0,
291        x_step: 4,
292        y_step: 4,
293    },
294    Adam7Pass {
295        x_start: 0,
296        y_start: 2,
297        x_step: 2,
298        y_step: 4,
299    },
300    Adam7Pass {
301        x_start: 1,
302        y_start: 0,
303        x_step: 2,
304        y_step: 2,
305    },
306    Adam7Pass {
307        x_start: 0,
308        y_start: 1,
309        x_step: 1,
310        y_step: 2,
311    },
312];
313
314/// PNG decoder.
315pub struct PngDecoder {
316    header: ImageHeader,
317    palette: Option<Vec<u8>>,
318    transparency: Option<Vec<u8>>,
319    #[allow(dead_code)]
320    gamma: Option<f64>,
321    image_data: Vec<u8>,
322}
323
324impl PngDecoder {
325    /// Create a new PNG decoder.
326    ///
327    /// # Errors
328    ///
329    /// Returns error if PNG data is invalid.
330    pub fn new(data: &[u8]) -> CodecResult<Self> {
331        if data.len() < 8 {
332            return Err(CodecError::InvalidData("PNG data too short".into()));
333        }
334
335        // Validate PNG signature
336        if &data[0..8] != PNG_SIGNATURE {
337            return Err(CodecError::InvalidData("Invalid PNG signature".into()));
338        }
339
340        let mut offset = 8;
341        let mut header: Option<ImageHeader> = None;
342        let mut palette: Option<Vec<u8>> = None;
343        let mut transparency: Option<Vec<u8>> = None;
344        let mut gamma: Option<f64> = None;
345        let mut idat_chunks = Vec::new();
346
347        // Parse chunks
348        while offset < data.len() {
349            let chunk = Chunk::read(data, &mut offset)?;
350
351            match chunk.type_str() {
352                "IHDR" => {
353                    if header.is_some() {
354                        return Err(CodecError::InvalidData("Multiple IHDR chunks".into()));
355                    }
356                    header = Some(ImageHeader::parse(&chunk.data)?);
357                }
358                "PLTE" => {
359                    if palette.is_some() {
360                        return Err(CodecError::InvalidData("Multiple PLTE chunks".into()));
361                    }
362                    if chunk.data.len() % 3 != 0 || chunk.data.is_empty() {
363                        return Err(CodecError::InvalidData("Invalid PLTE chunk".into()));
364                    }
365                    palette = Some(chunk.data);
366                }
367                "IDAT" => {
368                    idat_chunks.push(chunk.data);
369                }
370                "IEND" => {
371                    break;
372                }
373                "tRNS" => {
374                    transparency = Some(chunk.data);
375                }
376                "gAMA" => {
377                    if chunk.data.len() == 4 {
378                        let gamma_int = u32::from_be_bytes([
379                            chunk.data[0],
380                            chunk.data[1],
381                            chunk.data[2],
382                            chunk.data[3],
383                        ]);
384                        gamma = Some(f64::from(gamma_int) / 100_000.0);
385                    }
386                }
387                _ => {
388                    // Skip unknown ancillary chunks
389                }
390            }
391        }
392
393        let header = header.ok_or_else(|| CodecError::InvalidData("Missing IHDR chunk".into()))?;
394
395        if header.color_type == ColorType::Palette && palette.is_none() {
396            return Err(CodecError::InvalidData(
397                "Palette color type requires PLTE chunk".into(),
398            ));
399        }
400
401        // Decompress IDAT data
402        let compressed_data: Vec<u8> = idat_chunks.into_iter().flatten().collect();
403        let mut decoder = ZlibDecoder::new(&compressed_data[..]);
404        let mut image_data = Vec::new();
405        decoder
406            .read_to_end(&mut image_data)
407            .map_err(|e| CodecError::DecoderError(format!("DEFLATE decompression failed: {e}")))?;
408
409        Ok(Self {
410            header,
411            palette,
412            transparency,
413            gamma,
414            image_data,
415        })
416    }
417
418    /// Get image width.
419    #[must_use]
420    pub const fn width(&self) -> u32 {
421        self.header.width
422    }
423
424    /// Get image height.
425    #[must_use]
426    pub const fn height(&self) -> u32 {
427        self.header.height
428    }
429
430    /// Get color type.
431    #[must_use]
432    pub const fn color_type(&self) -> ColorType {
433        self.header.color_type
434    }
435
436    /// Get bit depth.
437    #[must_use]
438    pub const fn bit_depth(&self) -> u8 {
439        self.header.bit_depth
440    }
441
442    /// Check if image is interlaced.
443    #[must_use]
444    pub const fn is_interlaced(&self) -> bool {
445        self.header.interlace == 1
446    }
447
448    /// Decode the PNG image.
449    ///
450    /// # Errors
451    ///
452    /// Returns error if decoding fails.
453    pub fn decode(&self) -> CodecResult<DecodedImage> {
454        let raw_data = if self.is_interlaced() {
455            self.decode_interlaced()?
456        } else {
457            self.decode_sequential()?
458        };
459
460        let rgba_data = self.convert_to_rgba(&raw_data)?;
461
462        Ok(DecodedImage {
463            width: self.header.width,
464            height: self.header.height,
465            data: Bytes::from(rgba_data),
466        })
467    }
468
469    /// Decode sequential (non-interlaced) image.
470    fn decode_sequential(&self) -> CodecResult<Vec<u8>> {
471        let scanline_len = self.header.scanline_length();
472        let bytes_per_pixel = self.header.bytes_per_pixel();
473        let expected_len = (scanline_len + 1) * self.header.height as usize;
474
475        if self.image_data.len() < expected_len {
476            return Err(CodecError::InvalidData("Insufficient image data".into()));
477        }
478
479        let mut output = Vec::with_capacity(scanline_len * self.header.height as usize);
480        let mut prev_scanline: Option<Vec<u8>> = None;
481
482        for y in 0..self.header.height as usize {
483            let offset = y * (scanline_len + 1);
484            let filter_type = FilterType::from_u8(self.image_data[offset])?;
485            let filtered = &self.image_data[offset + 1..offset + 1 + scanline_len];
486
487            let scanline = unfilter(
488                filter_type,
489                filtered,
490                prev_scanline.as_deref(),
491                bytes_per_pixel,
492            )?;
493
494            output.extend_from_slice(&scanline);
495            prev_scanline = Some(scanline);
496        }
497
498        Ok(output)
499    }
500
501    /// Decode interlaced (Adam7) image.
502    #[allow(clippy::similar_names)]
503    fn decode_interlaced(&self) -> CodecResult<Vec<u8>> {
504        let bytes_per_pixel = self.header.bytes_per_pixel();
505        let bits_per_pixel =
506            self.header.color_type.samples_per_pixel() * self.header.bit_depth as usize;
507        let bytes_per_sample = (self.header.bit_depth as usize + 7) / 8;
508
509        let total_pixels =
510            self.header.width as usize * self.header.height as usize * bytes_per_pixel;
511        let mut output = vec![0u8; total_pixels];
512
513        let mut data_offset = 0;
514
515        for pass in &ADAM7_PASSES {
516            let pass_width =
517                (self.header.width.saturating_sub(pass.x_start) + pass.x_step - 1) / pass.x_step;
518            let pass_height =
519                (self.header.height.saturating_sub(pass.y_start) + pass.y_step - 1) / pass.y_step;
520
521            if pass_width == 0 || pass_height == 0 {
522                continue;
523            }
524
525            let scanline_bits = pass_width as usize * bits_per_pixel;
526            let scanline_len = (scanline_bits + 7) / 8;
527
528            let mut prev_scanline: Option<Vec<u8>> = None;
529
530            for py in 0..pass_height {
531                if data_offset >= self.image_data.len() {
532                    return Err(CodecError::InvalidData(
533                        "Insufficient data for interlaced image".into(),
534                    ));
535                }
536
537                let filter_type = FilterType::from_u8(self.image_data[data_offset])?;
538                data_offset += 1;
539
540                if data_offset + scanline_len > self.image_data.len() {
541                    return Err(CodecError::InvalidData(
542                        "Insufficient data for scanline".into(),
543                    ));
544                }
545
546                let filtered = &self.image_data[data_offset..data_offset + scanline_len];
547                data_offset += scanline_len;
548
549                let scanline = unfilter(
550                    filter_type,
551                    filtered,
552                    prev_scanline.as_deref(),
553                    bytes_per_pixel,
554                )?;
555
556                // Copy pixels to output
557                let y = (pass.y_start + py * pass.y_step) as usize;
558                for px in 0..pass_width as usize {
559                    let x = pass.x_start as usize + px * pass.x_step as usize;
560                    let src_offset = px * bytes_per_pixel;
561                    let dst_offset = (y * self.header.width as usize + x) * bytes_per_pixel;
562
563                    if src_offset + bytes_per_pixel <= scanline.len()
564                        && dst_offset + bytes_per_pixel <= output.len()
565                    {
566                        output[dst_offset..dst_offset + bytes_per_pixel]
567                            .copy_from_slice(&scanline[src_offset..src_offset + bytes_per_pixel]);
568                    }
569                }
570
571                prev_scanline = Some(scanline);
572            }
573        }
574
575        Ok(output)
576    }
577
578    /// Convert raw image data to RGBA format.
579    #[allow(clippy::too_many_lines)]
580    fn convert_to_rgba(&self, raw_data: &[u8]) -> CodecResult<Vec<u8>> {
581        let pixel_count = (self.header.width * self.header.height) as usize;
582        let mut rgba = vec![255u8; pixel_count * 4];
583
584        match self.header.color_type {
585            ColorType::Grayscale => {
586                if self.header.bit_depth == 8 {
587                    for i in 0..pixel_count {
588                        let gray = raw_data[i];
589                        rgba[i * 4] = gray;
590                        rgba[i * 4 + 1] = gray;
591                        rgba[i * 4 + 2] = gray;
592                        rgba[i * 4 + 3] = 255;
593                    }
594                } else if self.header.bit_depth == 16 {
595                    for i in 0..pixel_count {
596                        let gray = raw_data[i * 2];
597                        rgba[i * 4] = gray;
598                        rgba[i * 4 + 1] = gray;
599                        rgba[i * 4 + 2] = gray;
600                        rgba[i * 4 + 3] = 255;
601                    }
602                } else {
603                    // Expand low bit depths
604                    self.expand_grayscale(raw_data, &mut rgba)?;
605                }
606            }
607            ColorType::Rgb => {
608                if self.header.bit_depth == 8 {
609                    for i in 0..pixel_count {
610                        rgba[i * 4] = raw_data[i * 3];
611                        rgba[i * 4 + 1] = raw_data[i * 3 + 1];
612                        rgba[i * 4 + 2] = raw_data[i * 3 + 2];
613                        rgba[i * 4 + 3] = 255;
614                    }
615                } else if self.header.bit_depth == 16 {
616                    for i in 0..pixel_count {
617                        rgba[i * 4] = raw_data[i * 6];
618                        rgba[i * 4 + 1] = raw_data[i * 6 + 2];
619                        rgba[i * 4 + 2] = raw_data[i * 6 + 4];
620                        rgba[i * 4 + 3] = 255;
621                    }
622                }
623            }
624            ColorType::Palette => {
625                let palette = self
626                    .palette
627                    .as_ref()
628                    .ok_or_else(|| CodecError::InvalidData("Missing palette".into()))?;
629                self.expand_palette(raw_data, palette, &mut rgba)?;
630            }
631            ColorType::GrayscaleAlpha => {
632                if self.header.bit_depth == 8 {
633                    for i in 0..pixel_count {
634                        let gray = raw_data[i * 2];
635                        let alpha = raw_data[i * 2 + 1];
636                        rgba[i * 4] = gray;
637                        rgba[i * 4 + 1] = gray;
638                        rgba[i * 4 + 2] = gray;
639                        rgba[i * 4 + 3] = alpha;
640                    }
641                } else if self.header.bit_depth == 16 {
642                    for i in 0..pixel_count {
643                        let gray = raw_data[i * 4];
644                        let alpha = raw_data[i * 4 + 2];
645                        rgba[i * 4] = gray;
646                        rgba[i * 4 + 1] = gray;
647                        rgba[i * 4 + 2] = gray;
648                        rgba[i * 4 + 3] = alpha;
649                    }
650                }
651            }
652            ColorType::Rgba => {
653                if self.header.bit_depth == 8 {
654                    rgba.copy_from_slice(raw_data);
655                } else if self.header.bit_depth == 16 {
656                    for i in 0..pixel_count {
657                        rgba[i * 4] = raw_data[i * 8];
658                        rgba[i * 4 + 1] = raw_data[i * 8 + 2];
659                        rgba[i * 4 + 2] = raw_data[i * 8 + 4];
660                        rgba[i * 4 + 3] = raw_data[i * 8 + 6];
661                    }
662                }
663            }
664        }
665
666        // Apply transparency if present
667        if let Some(trns) = &self.transparency {
668            self.apply_transparency(&mut rgba, trns)?;
669        }
670
671        Ok(rgba)
672    }
673
674    /// Expand low bit-depth grayscale to 8-bit.
675    fn expand_grayscale(&self, raw_data: &[u8], rgba: &mut [u8]) -> CodecResult<()> {
676        let pixel_count = (self.header.width * self.header.height) as usize;
677        let scale = 255 / ((1 << self.header.bit_depth) - 1);
678
679        let mut bit_pos = 0;
680        for i in 0..pixel_count {
681            let byte_pos = bit_pos / 8;
682            let shift = 8 - (bit_pos % 8) - self.header.bit_depth as usize;
683            let mask = (1 << self.header.bit_depth) - 1;
684
685            if byte_pos >= raw_data.len() {
686                return Err(CodecError::InvalidData(
687                    "Insufficient grayscale data".into(),
688                ));
689            }
690
691            let value = (raw_data[byte_pos] >> shift) & mask;
692            let gray = value * scale;
693
694            rgba[i * 4] = gray;
695            rgba[i * 4 + 1] = gray;
696            rgba[i * 4 + 2] = gray;
697            rgba[i * 4 + 3] = 255;
698
699            bit_pos += self.header.bit_depth as usize;
700        }
701
702        Ok(())
703    }
704
705    /// Expand palette indices to RGBA.
706    fn expand_palette(&self, raw_data: &[u8], palette: &[u8], rgba: &mut [u8]) -> CodecResult<()> {
707        let pixel_count = (self.header.width * self.header.height) as usize;
708        let mut bit_pos = 0;
709
710        for i in 0..pixel_count {
711            let byte_pos = bit_pos / 8;
712            let shift = 8 - (bit_pos % 8) - self.header.bit_depth as usize;
713            let mask = (1 << self.header.bit_depth) - 1;
714
715            if byte_pos >= raw_data.len() {
716                return Err(CodecError::InvalidData("Insufficient palette data".into()));
717            }
718
719            let index = ((raw_data[byte_pos] >> shift) & mask) as usize;
720            let palette_offset = index * 3;
721
722            if palette_offset + 3 > palette.len() {
723                return Err(CodecError::InvalidData(format!(
724                    "Invalid palette index: {index}"
725                )));
726            }
727
728            rgba[i * 4] = palette[palette_offset];
729            rgba[i * 4 + 1] = palette[palette_offset + 1];
730            rgba[i * 4 + 2] = palette[palette_offset + 2];
731            rgba[i * 4 + 3] = 255;
732
733            bit_pos += self.header.bit_depth as usize;
734        }
735
736        Ok(())
737    }
738
739    /// Apply transparency data.
740    fn apply_transparency(&self, rgba: &mut [u8], trns: &[u8]) -> CodecResult<()> {
741        match self.header.color_type {
742            ColorType::Grayscale => {
743                if trns.len() < 2 {
744                    return Ok(());
745                }
746                let transparent_gray = u16::from_be_bytes([trns[0], trns[1]]);
747                let transparent_gray_8 = if self.header.bit_depth == 16 {
748                    (transparent_gray >> 8) as u8
749                } else {
750                    transparent_gray as u8
751                };
752
753                for i in 0..rgba.len() / 4 {
754                    if rgba[i * 4] == transparent_gray_8 {
755                        rgba[i * 4 + 3] = 0;
756                    }
757                }
758            }
759            ColorType::Palette => {
760                for i in 0..rgba.len() / 4 {
761                    if i < trns.len() {
762                        rgba[i * 4 + 3] = trns[i];
763                    }
764                }
765            }
766            ColorType::Rgb => {
767                if trns.len() < 6 {
768                    return Ok(());
769                }
770                let tr = u16::from_be_bytes([trns[0], trns[1]]);
771                let tg = u16::from_be_bytes([trns[2], trns[3]]);
772                let tb = u16::from_be_bytes([trns[4], trns[5]]);
773
774                for i in 0..rgba.len() / 4 {
775                    let r = u16::from(rgba[i * 4]);
776                    let g = u16::from(rgba[i * 4 + 1]);
777                    let b = u16::from(rgba[i * 4 + 2]);
778
779                    if r == tr && g == tg && b == tb {
780                        rgba[i * 4 + 3] = 0;
781                    }
782                }
783            }
784            _ => {}
785        }
786
787        Ok(())
788    }
789}
790
791/// Decoded PNG image.
792#[derive(Debug, Clone)]
793pub struct DecodedImage {
794    /// Image width.
795    pub width: u32,
796    /// Image height.
797    pub height: u32,
798    /// RGBA pixel data.
799    pub data: Bytes,
800}
801
802/// Chromaticity coordinates (cHRM chunk).
803#[derive(Debug, Clone, Copy)]
804pub struct Chromaticity {
805    /// White point X.
806    pub white_x: f64,
807    /// White point Y.
808    pub white_y: f64,
809    /// Red X.
810    pub red_x: f64,
811    /// Red Y.
812    pub red_y: f64,
813    /// Green X.
814    pub green_x: f64,
815    /// Green Y.
816    pub green_y: f64,
817    /// Blue X.
818    pub blue_x: f64,
819    /// Blue Y.
820    pub blue_y: f64,
821}
822
823/// Physical pixel dimensions (pHYs chunk).
824#[derive(Debug, Clone, Copy)]
825pub struct PhysicalDimensions {
826    /// Pixels per unit, X axis.
827    pub x: u32,
828    /// Pixels per unit, Y axis.
829    pub y: u32,
830    /// Unit specifier (0 = unknown, 1 = meter).
831    pub unit: u8,
832}
833
834/// Significant bits (sBIT chunk).
835#[derive(Debug, Clone)]
836pub struct SignificantBits {
837    /// Significant bits for each channel.
838    pub bits: Vec<u8>,
839}
840
841/// Text metadata (tEXt chunk).
842#[derive(Debug, Clone)]
843pub struct TextChunk {
844    /// Keyword.
845    pub keyword: String,
846    /// Text value.
847    pub text: String,
848}
849
850/// PNG metadata.
851#[derive(Debug, Clone, Default)]
852pub struct PngMetadata {
853    /// Gamma value.
854    pub gamma: Option<f64>,
855    /// Chromaticity coordinates.
856    pub chromaticity: Option<Chromaticity>,
857    /// Physical dimensions.
858    pub physical_dimensions: Option<PhysicalDimensions>,
859    /// Significant bits.
860    pub significant_bits: Option<SignificantBits>,
861    /// Text chunks.
862    pub text_chunks: Vec<TextChunk>,
863    /// Background color.
864    pub background_color: Option<(u16, u16, u16)>,
865    /// Suggested palette.
866    pub suggested_palette: Option<Vec<u8>>,
867}
868
869impl Chromaticity {
870    /// Parse from cHRM chunk data.
871    ///
872    /// # Errors
873    ///
874    /// Returns error if chunk data is invalid.
875    pub fn parse(data: &[u8]) -> CodecResult<Self> {
876        if data.len() < 32 {
877            return Err(CodecError::InvalidData("cHRM chunk too short".into()));
878        }
879
880        let white_x = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
881        let white_y = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
882        let red_x = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
883        let red_y = u32::from_be_bytes([data[12], data[13], data[14], data[15]]);
884        let green_x = u32::from_be_bytes([data[16], data[17], data[18], data[19]]);
885        let green_y = u32::from_be_bytes([data[20], data[21], data[22], data[23]]);
886        let blue_x = u32::from_be_bytes([data[24], data[25], data[26], data[27]]);
887        let blue_y = u32::from_be_bytes([data[28], data[29], data[30], data[31]]);
888
889        Ok(Self {
890            white_x: f64::from(white_x) / 100_000.0,
891            white_y: f64::from(white_y) / 100_000.0,
892            red_x: f64::from(red_x) / 100_000.0,
893            red_y: f64::from(red_y) / 100_000.0,
894            green_x: f64::from(green_x) / 100_000.0,
895            green_y: f64::from(green_y) / 100_000.0,
896            blue_x: f64::from(blue_x) / 100_000.0,
897            blue_y: f64::from(blue_y) / 100_000.0,
898        })
899    }
900
901    /// Get sRGB chromaticity.
902    #[must_use]
903    pub fn srgb() -> Self {
904        Self {
905            white_x: 0.3127,
906            white_y: 0.329,
907            red_x: 0.64,
908            red_y: 0.33,
909            green_x: 0.3,
910            green_y: 0.6,
911            blue_x: 0.15,
912            blue_y: 0.06,
913        }
914    }
915}
916
917impl PhysicalDimensions {
918    /// Parse from pHYs chunk data.
919    ///
920    /// # Errors
921    ///
922    /// Returns error if chunk data is invalid.
923    pub fn parse(data: &[u8]) -> CodecResult<Self> {
924        if data.len() < 9 {
925            return Err(CodecError::InvalidData("pHYs chunk too short".into()));
926        }
927
928        let x = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
929        let y = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
930        let unit = data[8];
931
932        Ok(Self { x, y, unit })
933    }
934
935    /// Get DPI if unit is meter.
936    #[must_use]
937    pub fn dpi(&self) -> Option<(f64, f64)> {
938        if self.unit == 1 {
939            const METERS_PER_INCH: f64 = 0.0254;
940            Some((
941                f64::from(self.x) * METERS_PER_INCH,
942                f64::from(self.y) * METERS_PER_INCH,
943            ))
944        } else {
945            None
946        }
947    }
948}
949
950impl SignificantBits {
951    /// Parse from sBIT chunk data.
952    ///
953    /// # Errors
954    ///
955    /// Returns error if chunk data is invalid.
956    pub fn parse(data: &[u8]) -> CodecResult<Self> {
957        if data.is_empty() {
958            return Err(CodecError::InvalidData("sBIT chunk is empty".into()));
959        }
960
961        Ok(Self {
962            bits: data.to_vec(),
963        })
964    }
965}
966
967impl TextChunk {
968    /// Parse from tEXt chunk data.
969    ///
970    /// # Errors
971    ///
972    /// Returns error if chunk data is invalid.
973    pub fn parse(data: &[u8]) -> CodecResult<Self> {
974        // Find null separator
975        let null_pos = data
976            .iter()
977            .position(|&b| b == 0)
978            .ok_or_else(|| CodecError::InvalidData("tEXt chunk missing null separator".into()))?;
979
980        let keyword = String::from_utf8_lossy(&data[..null_pos]).into_owned();
981        let text = String::from_utf8_lossy(&data[null_pos + 1..]).into_owned();
982
983        Ok(Self { keyword, text })
984    }
985}
986
987/// Extended PNG decoder with metadata support.
988pub struct PngDecoderExtended {
989    /// Base decoder.
990    pub decoder: PngDecoder,
991    /// Metadata.
992    pub metadata: PngMetadata,
993}
994
995impl PngDecoderExtended {
996    /// Create a new extended PNG decoder.
997    ///
998    /// # Errors
999    ///
1000    /// Returns error if PNG data is invalid.
1001    #[allow(clippy::too_many_lines)]
1002    pub fn new(data: &[u8]) -> CodecResult<Self> {
1003        if data.len() < 8 {
1004            return Err(CodecError::InvalidData("PNG data too short".into()));
1005        }
1006
1007        if &data[0..8] != PNG_SIGNATURE {
1008            return Err(CodecError::InvalidData("Invalid PNG signature".into()));
1009        }
1010
1011        let mut offset = 8;
1012        let mut header: Option<ImageHeader> = None;
1013        let mut palette: Option<Vec<u8>> = None;
1014        let mut transparency: Option<Vec<u8>> = None;
1015        let mut metadata = PngMetadata::default();
1016        let mut idat_chunks = Vec::new();
1017
1018        while offset < data.len() {
1019            let chunk = Chunk::read(data, &mut offset)?;
1020
1021            match chunk.type_str() {
1022                "IHDR" => {
1023                    if header.is_some() {
1024                        return Err(CodecError::InvalidData("Multiple IHDR chunks".into()));
1025                    }
1026                    header = Some(ImageHeader::parse(&chunk.data)?);
1027                }
1028                "PLTE" => {
1029                    if palette.is_some() {
1030                        return Err(CodecError::InvalidData("Multiple PLTE chunks".into()));
1031                    }
1032                    if chunk.data.len() % 3 != 0 || chunk.data.is_empty() {
1033                        return Err(CodecError::InvalidData("Invalid PLTE chunk".into()));
1034                    }
1035                    palette = Some(chunk.data);
1036                }
1037                "IDAT" => {
1038                    idat_chunks.push(chunk.data);
1039                }
1040                "IEND" => {
1041                    break;
1042                }
1043                "tRNS" => {
1044                    transparency = Some(chunk.data);
1045                }
1046                "gAMA" => {
1047                    if chunk.data.len() == 4 {
1048                        let gamma_int = u32::from_be_bytes([
1049                            chunk.data[0],
1050                            chunk.data[1],
1051                            chunk.data[2],
1052                            chunk.data[3],
1053                        ]);
1054                        metadata.gamma = Some(f64::from(gamma_int) / 100_000.0);
1055                    }
1056                }
1057                "cHRM" => {
1058                    metadata.chromaticity = Some(Chromaticity::parse(&chunk.data)?);
1059                }
1060                "pHYs" => {
1061                    metadata.physical_dimensions = Some(PhysicalDimensions::parse(&chunk.data)?);
1062                }
1063                "sBIT" => {
1064                    metadata.significant_bits = Some(SignificantBits::parse(&chunk.data)?);
1065                }
1066                "tEXt" => {
1067                    if let Ok(text_chunk) = TextChunk::parse(&chunk.data) {
1068                        metadata.text_chunks.push(text_chunk);
1069                    }
1070                }
1071                "bKGD" => {
1072                    if chunk.data.len() >= 6 {
1073                        let r = u16::from_be_bytes([chunk.data[0], chunk.data[1]]);
1074                        let g = u16::from_be_bytes([chunk.data[2], chunk.data[3]]);
1075                        let b = u16::from_be_bytes([chunk.data[4], chunk.data[5]]);
1076                        metadata.background_color = Some((r, g, b));
1077                    }
1078                }
1079                "sPLT" => {
1080                    metadata.suggested_palette = Some(chunk.data);
1081                }
1082                _ => {
1083                    // Skip unknown ancillary chunks
1084                }
1085            }
1086        }
1087
1088        let header = header.ok_or_else(|| CodecError::InvalidData("Missing IHDR chunk".into()))?;
1089
1090        if header.color_type == ColorType::Palette && palette.is_none() {
1091            return Err(CodecError::InvalidData(
1092                "Palette color type requires PLTE chunk".into(),
1093            ));
1094        }
1095
1096        let compressed_data: Vec<u8> = idat_chunks.into_iter().flatten().collect();
1097        let mut zlib_decoder = ZlibDecoder::new(&compressed_data[..]);
1098        let mut image_data = Vec::new();
1099        zlib_decoder
1100            .read_to_end(&mut image_data)
1101            .map_err(|e| CodecError::DecoderError(format!("DEFLATE decompression failed: {e}")))?;
1102
1103        let decoder = PngDecoder {
1104            header,
1105            palette,
1106            transparency,
1107            gamma: metadata.gamma,
1108            image_data,
1109        };
1110
1111        Ok(Self { decoder, metadata })
1112    }
1113
1114    /// Decode the PNG image.
1115    ///
1116    /// # Errors
1117    ///
1118    /// Returns error if decoding fails.
1119    pub fn decode(&self) -> CodecResult<DecodedImage> {
1120        self.decoder.decode()
1121    }
1122}