1use super::decoder::ColorType;
14use super::filter::{FilterStrategy, FilterType};
15use crate::error::{CodecError, CodecResult};
16use oxiarc_deflate::ZlibStreamEncoder;
17use std::io::Write;
18
19const PNG_SIGNATURE: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10];
21
22#[derive(Debug, Clone)]
24pub struct EncoderConfig {
25 pub compression_level: u32,
27 pub filter_strategy: FilterStrategy,
29 pub interlace: bool,
31 pub gamma: Option<f64>,
33 pub optimize_palette: bool,
35}
36
37impl EncoderConfig {
38 #[must_use]
40 pub fn new() -> Self {
41 Self::default()
42 }
43
44 #[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 #[must_use]
53 pub const fn with_filter_strategy(mut self, strategy: FilterStrategy) -> Self {
54 self.filter_strategy = strategy;
55 self
56 }
57
58 #[must_use]
60 pub const fn with_interlace(mut self, interlace: bool) -> Self {
61 self.interlace = interlace;
62 self
63 }
64
65 #[must_use]
67 pub const fn with_gamma(mut self, gamma: f64) -> Self {
68 self.gamma = Some(gamma);
69 self
70 }
71
72 #[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
92pub struct PngEncoder {
94 config: EncoderConfig,
95}
96
97impl PngEncoder {
98 #[must_use]
100 pub fn new() -> Self {
101 Self {
102 config: EncoderConfig::default(),
103 }
104 }
105
106 #[must_use]
108 pub const fn with_config(config: EncoderConfig) -> Self {
109 Self { config }
110 }
111
112 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 output.extend_from_slice(&PNG_SIGNATURE);
136
137 let (color_type, bit_depth, image_data) = self.optimize_color_type(width, height, data)?;
139
140 self.write_ihdr(&mut output, width, height, bit_depth, color_type)?;
142
143 if let Some(gamma) = self.config.gamma {
145 self.write_gamma(&mut output, gamma)?;
146 }
147
148 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 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 self.write_iend(&mut output)?;
162
163 Ok(output)
164 }
165
166 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 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 #[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 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 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 if is_grayscale && !has_alpha {
284 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 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 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 Ok((ColorType::Rgba, 8, rgba_data.to_vec()))
310 }
311 }
312
313 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 #[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 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 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 #[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 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 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 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); data.push(0); data.push(if self.config.interlace { 1 } else { 0 });
501
502 self.write_chunk(output, b"IHDR", &data)
503 }
504
505 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 fn write_palette(&self, output: &mut Vec<u8>, palette: &[u8]) -> CodecResult<()> {
514 self.write_chunk(output, b"PLTE", palette)
515 }
516
517 fn write_idat(&self, output: &mut Vec<u8>, data: &[u8]) -> CodecResult<()> {
519 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 fn write_iend(&self, output: &mut Vec<u8>) -> CodecResult<()> {
535 self.write_chunk(output, b"IEND", &[])
536 }
537
538 fn write_chunk(
540 &self,
541 output: &mut Vec<u8>,
542 chunk_type: &[u8; 4],
543 data: &[u8],
544 ) -> CodecResult<()> {
545 output.extend_from_slice(&(data.len() as u32).to_be_bytes());
547
548 output.extend_from_slice(chunk_type);
550
551 output.extend_from_slice(data);
553
554 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
568fn 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
588pub enum CompressionLevel {
589 None,
591 Fast,
593 Default,
595 Best,
597}
598
599impl CompressionLevel {
600 #[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
612pub struct EncoderBuilder {
614 config: EncoderConfig,
615}
616
617impl EncoderBuilder {
618 #[must_use]
620 pub fn new() -> Self {
621 Self {
622 config: EncoderConfig::default(),
623 }
624 }
625
626 #[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 #[must_use]
635 pub const fn filter_strategy(mut self, strategy: FilterStrategy) -> Self {
636 self.config.filter_strategy = strategy;
637 self
638 }
639
640 #[must_use]
642 pub const fn interlace(mut self, enable: bool) -> Self {
643 self.config.interlace = enable;
644 self
645 }
646
647 #[must_use]
649 pub const fn gamma(mut self, gamma: f64) -> Self {
650 self.config.gamma = Some(gamma);
651 self
652 }
653
654 #[must_use]
656 pub const fn optimize_palette(mut self, enable: bool) -> Self {
657 self.config.optimize_palette = enable;
658 self
659 }
660
661 #[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#[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#[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
698pub struct PngEncoderExtended {
700 encoder: PngEncoder,
702 chromaticity: Option<super::decoder::Chromaticity>,
704 physical_dimensions: Option<super::decoder::PhysicalDimensions>,
706 #[allow(dead_code)]
708 significant_bits: Option<super::decoder::SignificantBits>,
709 text_chunks: Vec<super::decoder::TextChunk>,
711 background_color: Option<(u16, u16, u16)>,
713}
714
715impl PngEncoderExtended {
716 #[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 #[must_use]
731 pub fn with_chromaticity(mut self, chroma: super::decoder::Chromaticity) -> Self {
732 self.chromaticity = Some(chroma);
733 self
734 }
735
736 #[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 #[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 #[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 #[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 #[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 let (color_type, bit_depth, image_data) =
790 self.encoder.optimize_color_type(width, height, data)?;
791
792 self.encoder
794 .write_ihdr(&mut output, width, height, bit_depth, color_type)?;
795
796 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 for text_chunk in &self.text_chunks {
815 self.write_text_chunk(&mut output, text_chunk)?;
816 }
817
818 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 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 self.encoder.write_iend(&mut output)?;
833
834 Ok(output)
835 }
836
837 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 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 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 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); 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
917pub struct PaletteEntry {
918 pub r: u8,
920 pub g: u8,
922 pub b: u8,
924}
925
926pub struct PaletteOptimizer {
928 colors: std::collections::HashMap<PaletteEntry, u32>,
930 max_size: usize,
932}
933
934impl PaletteOptimizer {
935 #[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 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 #[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)); Some(palette.iter().map(|(entry, _)| **entry).collect())
963 }
964
965 #[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#[derive(Debug, Clone, Default)]
975pub struct EncodingStats {
976 pub uncompressed_size: usize,
978 pub compressed_size: usize,
980 pub filter_distribution: [usize; 5],
982 pub encoding_time_ms: u64,
984 pub compression_ratio: f64,
986}
987
988impl EncodingStats {
989 #[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 pub fn add_filter_usage(&mut self, filter_type: FilterType) {
1009 self.filter_distribution[filter_type.to_u8() as usize] += 1;
1010 }
1011
1012 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1028pub enum EncodingProfile {
1029 Fast,
1031 Balanced,
1033 Best,
1035 Web,
1037 Archive,
1039}
1040
1041impl EncodingProfile {
1042 #[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 #[must_use]
1086 pub fn create_encoder(self) -> PngEncoder {
1087 PngEncoder::with_config(self.to_config())
1088 }
1089}
1090
1091pub struct ParallelPngEncoder {
1093 config: EncoderConfig,
1094}
1095
1096impl ParallelPngEncoder {
1097 #[must_use]
1099 pub const fn new(config: EncoderConfig) -> Self {
1100 Self { config }
1101 }
1102
1103 pub fn encode_rgba(&self, width: u32, height: u32, data: &[u8]) -> CodecResult<Vec<u8>> {
1109 let encoder = PngEncoder::with_config(self.config.clone());
1112 encoder.encode_rgba(width, height, data)
1113 }
1114}
1115
1116#[must_use]
1118pub fn encoder_from_profile(profile: EncodingProfile) -> PngEncoder {
1119 profile.create_encoder()
1120}
1121
1122pub 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}