1use super::decoder::ColorType;
14use super::filter::{FilterStrategy, FilterType};
15use crate::error::{CodecError, CodecResult};
16use flate2::write::ZlibEncoder;
17use flate2::Compression;
18use std::io::Write;
19
20const PNG_SIGNATURE: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10];
22
23#[derive(Debug, Clone)]
25pub struct EncoderConfig {
26 pub compression_level: u32,
28 pub filter_strategy: FilterStrategy,
30 pub interlace: bool,
32 pub gamma: Option<f64>,
34 pub optimize_palette: bool,
36}
37
38impl EncoderConfig {
39 #[must_use]
41 pub fn new() -> Self {
42 Self::default()
43 }
44
45 #[must_use]
47 pub const fn with_compression(mut self, level: u32) -> Self {
48 self.compression_level = if level > 9 { 9 } else { level };
49 self
50 }
51
52 #[must_use]
54 pub const fn with_filter_strategy(mut self, strategy: FilterStrategy) -> Self {
55 self.filter_strategy = strategy;
56 self
57 }
58
59 #[must_use]
61 pub const fn with_interlace(mut self, interlace: bool) -> Self {
62 self.interlace = interlace;
63 self
64 }
65
66 #[must_use]
68 pub const fn with_gamma(mut self, gamma: f64) -> Self {
69 self.gamma = Some(gamma);
70 self
71 }
72
73 #[must_use]
75 pub const fn with_palette_optimization(mut self, optimize: bool) -> Self {
76 self.optimize_palette = optimize;
77 self
78 }
79}
80
81impl Default for EncoderConfig {
82 fn default() -> Self {
83 Self {
84 compression_level: 6,
85 filter_strategy: FilterStrategy::Fast,
86 interlace: false,
87 gamma: None,
88 optimize_palette: false,
89 }
90 }
91}
92
93pub struct PngEncoder {
95 config: EncoderConfig,
96}
97
98impl PngEncoder {
99 #[must_use]
101 pub fn new() -> Self {
102 Self {
103 config: EncoderConfig::default(),
104 }
105 }
106
107 #[must_use]
109 pub const fn with_config(config: EncoderConfig) -> Self {
110 Self { config }
111 }
112
113 pub fn encode_rgba(&self, width: u32, height: u32, data: &[u8]) -> CodecResult<Vec<u8>> {
125 if data.len() != (width * height * 4) as usize {
126 return Err(CodecError::InvalidParameter(format!(
127 "Invalid data length: expected {}, got {}",
128 width * height * 4,
129 data.len()
130 )));
131 }
132
133 let mut output = Vec::new();
134
135 output.extend_from_slice(&PNG_SIGNATURE);
137
138 let (color_type, bit_depth, image_data) = self.optimize_color_type(width, height, data)?;
140
141 self.write_ihdr(&mut output, width, height, bit_depth, color_type)?;
143
144 if let Some(gamma) = self.config.gamma {
146 self.write_gamma(&mut output, gamma)?;
147 }
148
149 if color_type == ColorType::Palette {
151 if let Some(palette) = self.extract_palette(data) {
152 self.write_palette(&mut output, &palette)?;
153 }
154 }
155
156 let compressed_data =
158 self.encode_image_data(&image_data, width, height, color_type, bit_depth)?;
159 self.write_idat(&mut output, &compressed_data)?;
160
161 self.write_iend(&mut output)?;
163
164 Ok(output)
165 }
166
167 pub fn encode_rgb(&self, width: u32, height: u32, data: &[u8]) -> CodecResult<Vec<u8>> {
179 if data.len() != (width * height * 3) as usize {
180 return Err(CodecError::InvalidParameter(format!(
181 "Invalid data length: expected {}, got {}",
182 width * height * 3,
183 data.len()
184 )));
185 }
186
187 let mut output = Vec::new();
188 output.extend_from_slice(&PNG_SIGNATURE);
189
190 self.write_ihdr(&mut output, width, height, 8, ColorType::Rgb)?;
191
192 if let Some(gamma) = self.config.gamma {
193 self.write_gamma(&mut output, gamma)?;
194 }
195
196 let compressed_data = self.encode_image_data(data, width, height, ColorType::Rgb, 8)?;
197 self.write_idat(&mut output, &compressed_data)?;
198 self.write_iend(&mut output)?;
199
200 Ok(output)
201 }
202
203 pub fn encode_grayscale(
216 &self,
217 width: u32,
218 height: u32,
219 data: &[u8],
220 bit_depth: u8,
221 ) -> CodecResult<Vec<u8>> {
222 let expected_len = if bit_depth == 16 {
223 (width * height * 2) as usize
224 } else {
225 (width * height) as usize
226 };
227
228 if data.len() != expected_len {
229 return Err(CodecError::InvalidParameter(format!(
230 "Invalid data length: expected {expected_len}, got {}",
231 data.len()
232 )));
233 }
234
235 let mut output = Vec::new();
236 output.extend_from_slice(&PNG_SIGNATURE);
237
238 self.write_ihdr(&mut output, width, height, bit_depth, ColorType::Grayscale)?;
239
240 if let Some(gamma) = self.config.gamma {
241 self.write_gamma(&mut output, gamma)?;
242 }
243
244 let compressed_data =
245 self.encode_image_data(data, width, height, ColorType::Grayscale, bit_depth)?;
246 self.write_idat(&mut output, &compressed_data)?;
247 self.write_iend(&mut output)?;
248
249 Ok(output)
250 }
251
252 #[allow(clippy::type_complexity)]
254 fn optimize_color_type(
255 &self,
256 width: u32,
257 height: u32,
258 rgba_data: &[u8],
259 ) -> CodecResult<(ColorType, u8, Vec<u8>)> {
260 let pixel_count = (width * height) as usize;
261
262 let mut has_alpha = false;
264 for i in 0..pixel_count {
265 if rgba_data[i * 4 + 3] != 255 {
266 has_alpha = true;
267 break;
268 }
269 }
270
271 let mut is_grayscale = true;
273 for i in 0..pixel_count {
274 let r = rgba_data[i * 4];
275 let g = rgba_data[i * 4 + 1];
276 let b = rgba_data[i * 4 + 2];
277 if r != g || g != b {
278 is_grayscale = false;
279 break;
280 }
281 }
282
283 if is_grayscale && !has_alpha {
285 let mut gray_data = Vec::with_capacity(pixel_count);
287 for i in 0..pixel_count {
288 gray_data.push(rgba_data[i * 4]);
289 }
290 Ok((ColorType::Grayscale, 8, gray_data))
291 } else if is_grayscale && has_alpha {
292 let mut ga_data = Vec::with_capacity(pixel_count * 2);
294 for i in 0..pixel_count {
295 ga_data.push(rgba_data[i * 4]);
296 ga_data.push(rgba_data[i * 4 + 3]);
297 }
298 Ok((ColorType::GrayscaleAlpha, 8, ga_data))
299 } else if !has_alpha {
300 let mut rgb_data = Vec::with_capacity(pixel_count * 3);
302 for i in 0..pixel_count {
303 rgb_data.push(rgba_data[i * 4]);
304 rgb_data.push(rgba_data[i * 4 + 1]);
305 rgb_data.push(rgba_data[i * 4 + 2]);
306 }
307 Ok((ColorType::Rgb, 8, rgb_data))
308 } else {
309 Ok((ColorType::Rgba, 8, rgba_data.to_vec()))
311 }
312 }
313
314 fn extract_palette(&self, rgba_data: &[u8]) -> Option<Vec<u8>> {
316 if !self.config.optimize_palette {
317 return None;
318 }
319
320 let mut colors = std::collections::HashSet::new();
321 for chunk in rgba_data.chunks_exact(4) {
322 colors.insert((chunk[0], chunk[1], chunk[2]));
323 if colors.len() > 256 {
324 return None;
325 }
326 }
327
328 let mut palette = Vec::with_capacity(colors.len() * 3);
329 for (r, g, b) in colors {
330 palette.push(r);
331 palette.push(g);
332 palette.push(b);
333 }
334
335 Some(palette)
336 }
337
338 #[allow(clippy::too_many_lines)]
340 fn encode_image_data(
341 &self,
342 data: &[u8],
343 width: u32,
344 height: u32,
345 color_type: ColorType,
346 bit_depth: u8,
347 ) -> CodecResult<Vec<u8>> {
348 let samples_per_pixel = color_type.samples_per_pixel();
349 let bits_per_pixel = samples_per_pixel * bit_depth as usize;
350 let bytes_per_pixel = (bits_per_pixel + 7) / 8;
351 let scanline_len = ((width as usize * bits_per_pixel) + 7) / 8;
352
353 if self.config.interlace {
354 self.encode_interlaced(data, width, height, color_type, bit_depth)
355 } else {
356 self.encode_sequential(data, width, height, scanline_len, bytes_per_pixel)
357 }
358 }
359
360 fn encode_sequential(
362 &self,
363 data: &[u8],
364 width: u32,
365 height: u32,
366 scanline_len: usize,
367 bytes_per_pixel: usize,
368 ) -> CodecResult<Vec<u8>> {
369 let mut filtered_data = Vec::with_capacity((scanline_len + 1) * height as usize);
370 let mut prev_scanline: Option<Vec<u8>> = None;
371
372 for y in 0..height as usize {
373 let scanline_start = y * scanline_len;
374 let scanline = &data[scanline_start..scanline_start + scanline_len];
375
376 let (filter_type, filtered) = self.config.filter_strategy.apply(
377 scanline,
378 prev_scanline.as_deref(),
379 bytes_per_pixel,
380 );
381
382 filtered_data.push(filter_type.to_u8());
383 filtered_data.extend_from_slice(&filtered);
384
385 prev_scanline = Some(scanline.to_vec());
386 }
387
388 let compression = match self.config.compression_level {
390 0 => Compression::none(),
391 1 => Compression::fast(),
392 9 => Compression::best(),
393 n => Compression::new(n),
394 };
395
396 let mut encoder = ZlibEncoder::new(Vec::new(), compression);
397 encoder
398 .write_all(&filtered_data)
399 .map_err(|e| CodecError::Internal(format!("Compression failed: {e}")))?;
400
401 encoder
402 .finish()
403 .map_err(|e| CodecError::Internal(format!("Compression finish failed: {e}")))
404 }
405
406 #[allow(clippy::too_many_arguments)]
408 #[allow(clippy::similar_names)]
409 fn encode_interlaced(
410 &self,
411 data: &[u8],
412 width: u32,
413 height: u32,
414 color_type: ColorType,
415 bit_depth: u8,
416 ) -> CodecResult<Vec<u8>> {
417 let samples_per_pixel = color_type.samples_per_pixel();
418 let bits_per_pixel = samples_per_pixel * bit_depth as usize;
419 let bytes_per_pixel = (bits_per_pixel + 7) / 8;
420 let full_scanline_len = ((width as usize * bits_per_pixel) + 7) / 8;
421
422 let mut filtered_data = Vec::new();
423
424 let passes = [
426 (0, 0, 8, 8),
427 (4, 0, 8, 8),
428 (0, 4, 4, 8),
429 (2, 0, 4, 4),
430 (0, 2, 2, 4),
431 (1, 0, 2, 2),
432 (0, 1, 1, 2),
433 ];
434
435 for (x_start, y_start, x_step, y_step) in passes {
436 let pass_width = (width.saturating_sub(x_start) + x_step - 1) / x_step;
437 let pass_height = (height.saturating_sub(y_start) + y_step - 1) / y_step;
438
439 if pass_width == 0 || pass_height == 0 {
440 continue;
441 }
442
443 let pass_scanline_len = ((pass_width as usize * bits_per_pixel) + 7) / 8;
444 let mut prev_scanline: Option<Vec<u8>> = None;
445
446 for py in 0..pass_height {
447 let y = y_start + py * y_step;
448 let mut scanline = vec![0u8; pass_scanline_len];
449
450 for px in 0..pass_width {
451 let x = x_start + px * x_step;
452 let src_offset =
453 (y as usize * full_scanline_len) + (x as usize * bytes_per_pixel);
454 let dst_offset = px as usize * bytes_per_pixel;
455
456 if src_offset + bytes_per_pixel <= data.len()
457 && dst_offset + bytes_per_pixel <= scanline.len()
458 {
459 scanline[dst_offset..dst_offset + bytes_per_pixel]
460 .copy_from_slice(&data[src_offset..src_offset + bytes_per_pixel]);
461 }
462 }
463
464 let (filter_type, filtered) = self.config.filter_strategy.apply(
465 &scanline,
466 prev_scanline.as_deref(),
467 bytes_per_pixel,
468 );
469
470 filtered_data.push(filter_type.to_u8());
471 filtered_data.extend_from_slice(&filtered);
472
473 prev_scanline = Some(scanline);
474 }
475 }
476
477 let compression = match self.config.compression_level {
479 0 => Compression::none(),
480 1 => Compression::fast(),
481 9 => Compression::best(),
482 n => Compression::new(n),
483 };
484
485 let mut encoder = ZlibEncoder::new(Vec::new(), compression);
486 encoder
487 .write_all(&filtered_data)
488 .map_err(|e| CodecError::Internal(format!("Compression failed: {e}")))?;
489
490 encoder
491 .finish()
492 .map_err(|e| CodecError::Internal(format!("Compression finish failed: {e}")))
493 }
494
495 fn write_ihdr(
497 &self,
498 output: &mut Vec<u8>,
499 width: u32,
500 height: u32,
501 bit_depth: u8,
502 color_type: ColorType,
503 ) -> CodecResult<()> {
504 let mut data = Vec::new();
505 data.extend_from_slice(&width.to_be_bytes());
506 data.extend_from_slice(&height.to_be_bytes());
507 data.push(bit_depth);
508 data.push(color_type as u8);
509 data.push(0); data.push(0); data.push(if self.config.interlace { 1 } else { 0 });
512
513 self.write_chunk(output, b"IHDR", &data)
514 }
515
516 fn write_gamma(&self, output: &mut Vec<u8>, gamma: f64) -> CodecResult<()> {
518 let gamma_int = (gamma * 100_000.0) as u32;
519 let data = gamma_int.to_be_bytes();
520 self.write_chunk(output, b"gAMA", &data)
521 }
522
523 fn write_palette(&self, output: &mut Vec<u8>, palette: &[u8]) -> CodecResult<()> {
525 self.write_chunk(output, b"PLTE", palette)
526 }
527
528 fn write_idat(&self, output: &mut Vec<u8>, data: &[u8]) -> CodecResult<()> {
530 const MAX_CHUNK_SIZE: usize = 32768;
532
533 if data.len() <= MAX_CHUNK_SIZE {
534 self.write_chunk(output, b"IDAT", data)?;
535 } else {
536 for chunk in data.chunks(MAX_CHUNK_SIZE) {
537 self.write_chunk(output, b"IDAT", chunk)?;
538 }
539 }
540
541 Ok(())
542 }
543
544 fn write_iend(&self, output: &mut Vec<u8>) -> CodecResult<()> {
546 self.write_chunk(output, b"IEND", &[])
547 }
548
549 fn write_chunk(
551 &self,
552 output: &mut Vec<u8>,
553 chunk_type: &[u8; 4],
554 data: &[u8],
555 ) -> CodecResult<()> {
556 output.extend_from_slice(&(data.len() as u32).to_be_bytes());
558
559 output.extend_from_slice(chunk_type);
561
562 output.extend_from_slice(data);
564
565 let crc = crc32(chunk_type, data);
567 output.extend_from_slice(&crc.to_be_bytes());
568
569 Ok(())
570 }
571}
572
573impl Default for PngEncoder {
574 fn default() -> Self {
575 Self::new()
576 }
577}
578
579fn crc32(chunk_type: &[u8; 4], data: &[u8]) -> u32 {
581 let mut crc = !0u32;
582
583 for &byte in chunk_type.iter().chain(data.iter()) {
584 crc ^= u32::from(byte);
585 for _ in 0..8 {
586 crc = if crc & 1 != 0 {
587 0xedb8_8320 ^ (crc >> 1)
588 } else {
589 crc >> 1
590 };
591 }
592 }
593
594 !crc
595}
596
597#[derive(Debug, Clone, Copy, PartialEq, Eq)]
599pub enum CompressionLevel {
600 None,
602 Fast,
604 Default,
606 Best,
608}
609
610impl CompressionLevel {
611 #[must_use]
613 pub const fn to_level(self) -> u32 {
614 match self {
615 Self::None => 0,
616 Self::Fast => 1,
617 Self::Default => 6,
618 Self::Best => 9,
619 }
620 }
621}
622
623pub struct EncoderBuilder {
625 config: EncoderConfig,
626}
627
628impl EncoderBuilder {
629 #[must_use]
631 pub fn new() -> Self {
632 Self {
633 config: EncoderConfig::default(),
634 }
635 }
636
637 #[must_use]
639 pub const fn compression_level(mut self, level: CompressionLevel) -> Self {
640 self.config.compression_level = level.to_level();
641 self
642 }
643
644 #[must_use]
646 pub const fn filter_strategy(mut self, strategy: FilterStrategy) -> Self {
647 self.config.filter_strategy = strategy;
648 self
649 }
650
651 #[must_use]
653 pub const fn interlace(mut self, enable: bool) -> Self {
654 self.config.interlace = enable;
655 self
656 }
657
658 #[must_use]
660 pub const fn gamma(mut self, gamma: f64) -> Self {
661 self.config.gamma = Some(gamma);
662 self
663 }
664
665 #[must_use]
667 pub const fn optimize_palette(mut self, enable: bool) -> Self {
668 self.config.optimize_palette = enable;
669 self
670 }
671
672 #[must_use]
674 pub fn build(self) -> PngEncoder {
675 PngEncoder::with_config(self.config)
676 }
677}
678
679impl Default for EncoderBuilder {
680 fn default() -> Self {
681 Self::new()
682 }
683}
684
685#[must_use]
689pub fn fast_encoder() -> PngEncoder {
690 PngEncoder::with_config(
691 EncoderConfig::new()
692 .with_compression(1)
693 .with_filter_strategy(FilterStrategy::None),
694 )
695}
696
697#[must_use]
701pub fn best_encoder() -> PngEncoder {
702 PngEncoder::with_config(
703 EncoderConfig::new()
704 .with_compression(9)
705 .with_filter_strategy(FilterStrategy::Best),
706 )
707}
708
709pub struct PngEncoderExtended {
711 encoder: PngEncoder,
713 chromaticity: Option<super::decoder::Chromaticity>,
715 physical_dimensions: Option<super::decoder::PhysicalDimensions>,
717 #[allow(dead_code)]
719 significant_bits: Option<super::decoder::SignificantBits>,
720 text_chunks: Vec<super::decoder::TextChunk>,
722 background_color: Option<(u16, u16, u16)>,
724}
725
726impl PngEncoderExtended {
727 #[must_use]
729 pub fn new(config: EncoderConfig) -> Self {
730 Self {
731 encoder: PngEncoder::with_config(config),
732 chromaticity: None,
733 physical_dimensions: None,
734 significant_bits: None,
735 text_chunks: Vec::new(),
736 background_color: None,
737 }
738 }
739
740 #[must_use]
742 pub fn with_chromaticity(mut self, chroma: super::decoder::Chromaticity) -> Self {
743 self.chromaticity = Some(chroma);
744 self
745 }
746
747 #[must_use]
749 pub fn with_physical_dimensions(mut self, dims: super::decoder::PhysicalDimensions) -> Self {
750 self.physical_dimensions = Some(dims);
751 self
752 }
753
754 #[must_use]
756 pub fn with_dpi(mut self, dpi_x: f64, dpi_y: f64) -> Self {
757 const METERS_PER_INCH: f64 = 0.0254;
758 self.physical_dimensions = Some(super::decoder::PhysicalDimensions {
759 x: (dpi_x / METERS_PER_INCH) as u32,
760 y: (dpi_y / METERS_PER_INCH) as u32,
761 unit: 1,
762 });
763 self
764 }
765
766 #[must_use]
768 pub fn with_text(mut self, keyword: String, text: String) -> Self {
769 self.text_chunks
770 .push(super::decoder::TextChunk { keyword, text });
771 self
772 }
773
774 #[must_use]
776 pub const fn with_background_color(mut self, r: u16, g: u16, b: u16) -> Self {
777 self.background_color = Some((r, g, b));
778 self
779 }
780
781 #[allow(clippy::too_many_lines)]
787 pub fn encode_rgba(&self, width: u32, height: u32, data: &[u8]) -> CodecResult<Vec<u8>> {
788 if data.len() != (width * height * 4) as usize {
789 return Err(CodecError::InvalidParameter(format!(
790 "Invalid data length: expected {}, got {}",
791 width * height * 4,
792 data.len()
793 )));
794 }
795
796 let mut output = Vec::new();
797 output.extend_from_slice(&PNG_SIGNATURE);
798
799 let (color_type, bit_depth, image_data) =
801 self.encoder.optimize_color_type(width, height, data)?;
802
803 self.encoder
805 .write_ihdr(&mut output, width, height, bit_depth, color_type)?;
806
807 if let Some(gamma) = self.encoder.config.gamma {
809 self.encoder.write_gamma(&mut output, gamma)?;
810 }
811
812 if let Some(chroma) = &self.chromaticity {
813 self.write_chromaticity(&mut output, chroma)?;
814 }
815
816 if let Some(dims) = &self.physical_dimensions {
817 self.write_physical_dimensions(&mut output, dims)?;
818 }
819
820 if let Some(bg) = &self.background_color {
821 self.write_background_color(&mut output, *bg)?;
822 }
823
824 for text_chunk in &self.text_chunks {
826 self.write_text_chunk(&mut output, text_chunk)?;
827 }
828
829 if color_type == ColorType::Palette {
831 if let Some(palette) = self.encoder.extract_palette(data) {
832 self.encoder.write_palette(&mut output, &palette)?;
833 }
834 }
835
836 let compressed_data =
838 self.encoder
839 .encode_image_data(&image_data, width, height, color_type, bit_depth)?;
840 self.encoder.write_idat(&mut output, &compressed_data)?;
841
842 self.encoder.write_iend(&mut output)?;
844
845 Ok(output)
846 }
847
848 fn write_chromaticity(
850 &self,
851 output: &mut Vec<u8>,
852 chroma: &super::decoder::Chromaticity,
853 ) -> CodecResult<()> {
854 let mut data = Vec::with_capacity(32);
855
856 let white_x = (chroma.white_x * 100_000.0) as u32;
857 let white_y = (chroma.white_y * 100_000.0) as u32;
858 let red_x = (chroma.red_x * 100_000.0) as u32;
859 let red_y = (chroma.red_y * 100_000.0) as u32;
860 let green_x = (chroma.green_x * 100_000.0) as u32;
861 let green_y = (chroma.green_y * 100_000.0) as u32;
862 let blue_x = (chroma.blue_x * 100_000.0) as u32;
863 let blue_y = (chroma.blue_y * 100_000.0) as u32;
864
865 data.extend_from_slice(&white_x.to_be_bytes());
866 data.extend_from_slice(&white_y.to_be_bytes());
867 data.extend_from_slice(&red_x.to_be_bytes());
868 data.extend_from_slice(&red_y.to_be_bytes());
869 data.extend_from_slice(&green_x.to_be_bytes());
870 data.extend_from_slice(&green_y.to_be_bytes());
871 data.extend_from_slice(&blue_x.to_be_bytes());
872 data.extend_from_slice(&blue_y.to_be_bytes());
873
874 self.encoder.write_chunk(output, b"cHRM", &data)
875 }
876
877 fn write_physical_dimensions(
879 &self,
880 output: &mut Vec<u8>,
881 dims: &super::decoder::PhysicalDimensions,
882 ) -> CodecResult<()> {
883 let mut data = Vec::with_capacity(9);
884 data.extend_from_slice(&dims.x.to_be_bytes());
885 data.extend_from_slice(&dims.y.to_be_bytes());
886 data.push(dims.unit);
887
888 self.encoder.write_chunk(output, b"pHYs", &data)
889 }
890
891 fn write_background_color(
893 &self,
894 output: &mut Vec<u8>,
895 color: (u16, u16, u16),
896 ) -> CodecResult<()> {
897 let mut data = Vec::with_capacity(6);
898 data.extend_from_slice(&color.0.to_be_bytes());
899 data.extend_from_slice(&color.1.to_be_bytes());
900 data.extend_from_slice(&color.2.to_be_bytes());
901
902 self.encoder.write_chunk(output, b"bKGD", &data)
903 }
904
905 fn write_text_chunk(
907 &self,
908 output: &mut Vec<u8>,
909 text_chunk: &super::decoder::TextChunk,
910 ) -> CodecResult<()> {
911 let mut data = Vec::new();
912 data.extend_from_slice(text_chunk.keyword.as_bytes());
913 data.push(0); data.extend_from_slice(text_chunk.text.as_bytes());
915
916 self.encoder.write_chunk(output, b"tEXt", &data)
917 }
918}
919
920impl Default for PngEncoderExtended {
921 fn default() -> Self {
922 Self::new(EncoderConfig::default())
923 }
924}
925
926#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
928pub struct PaletteEntry {
929 pub r: u8,
931 pub g: u8,
933 pub b: u8,
935}
936
937pub struct PaletteOptimizer {
939 colors: std::collections::HashMap<PaletteEntry, u32>,
941 max_size: usize,
943}
944
945impl PaletteOptimizer {
946 #[must_use]
948 pub fn new(max_size: usize) -> Self {
949 Self {
950 colors: std::collections::HashMap::new(),
951 max_size: max_size.min(256),
952 }
953 }
954
955 pub fn add_color(&mut self, r: u8, g: u8, b: u8) {
957 let entry = PaletteEntry { r, g, b };
958 *self.colors.entry(entry).or_insert(0) += 1;
959 }
960
961 #[must_use]
965 pub fn build_palette(&self) -> Option<Vec<PaletteEntry>> {
966 if self.colors.len() > self.max_size {
967 return None;
968 }
969
970 let mut palette: Vec<_> = self.colors.iter().collect();
971 palette.sort_by(|a, b| b.1.cmp(a.1)); Some(palette.iter().map(|(entry, _)| **entry).collect())
974 }
975
976 #[must_use]
978 pub fn get_index(&self, r: u8, g: u8, b: u8, palette: &[PaletteEntry]) -> Option<u8> {
979 let entry = PaletteEntry { r, g, b };
980 palette.iter().position(|e| *e == entry).map(|i| i as u8)
981 }
982}
983
984#[derive(Debug, Clone, Default)]
986pub struct EncodingStats {
987 pub uncompressed_size: usize,
989 pub compressed_size: usize,
991 pub filter_distribution: [usize; 5],
993 pub encoding_time_ms: u64,
995 pub compression_ratio: f64,
997}
998
999impl EncodingStats {
1000 #[must_use]
1002 pub fn new(uncompressed_size: usize, compressed_size: usize) -> Self {
1003 let compression_ratio = if compressed_size > 0 {
1004 uncompressed_size as f64 / compressed_size as f64
1005 } else {
1006 0.0
1007 };
1008
1009 Self {
1010 uncompressed_size,
1011 compressed_size,
1012 filter_distribution: [0; 5],
1013 encoding_time_ms: 0,
1014 compression_ratio,
1015 }
1016 }
1017
1018 pub fn add_filter_usage(&mut self, filter_type: FilterType) {
1020 self.filter_distribution[filter_type.to_u8() as usize] += 1;
1021 }
1022
1023 #[must_use]
1025 pub fn most_used_filter(&self) -> FilterType {
1026 let (index, _) = self
1027 .filter_distribution
1028 .iter()
1029 .enumerate()
1030 .max_by_key(|(_, &count)| count)
1031 .unwrap_or((0, &0));
1032
1033 FilterType::from_u8(index as u8).unwrap_or(FilterType::None)
1034 }
1035}
1036
1037#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1039pub enum EncodingProfile {
1040 Fast,
1042 Balanced,
1044 Best,
1046 Web,
1048 Archive,
1050}
1051
1052impl EncodingProfile {
1053 #[must_use]
1055 pub const fn to_config(self) -> EncoderConfig {
1056 match self {
1057 Self::Fast => EncoderConfig {
1058 compression_level: 1,
1059 filter_strategy: FilterStrategy::None,
1060 interlace: false,
1061 gamma: None,
1062 optimize_palette: false,
1063 },
1064 Self::Balanced => EncoderConfig {
1065 compression_level: 6,
1066 filter_strategy: FilterStrategy::Fast,
1067 interlace: false,
1068 gamma: None,
1069 optimize_palette: true,
1070 },
1071 Self::Best => EncoderConfig {
1072 compression_level: 9,
1073 filter_strategy: FilterStrategy::Best,
1074 interlace: false,
1075 gamma: None,
1076 optimize_palette: true,
1077 },
1078 Self::Web => EncoderConfig {
1079 compression_level: 8,
1080 filter_strategy: FilterStrategy::Fast,
1081 interlace: true,
1082 gamma: Some(2.2),
1083 optimize_palette: true,
1084 },
1085 Self::Archive => EncoderConfig {
1086 compression_level: 9,
1087 filter_strategy: FilterStrategy::Best,
1088 interlace: false,
1089 gamma: None,
1090 optimize_palette: true,
1091 },
1092 }
1093 }
1094
1095 #[must_use]
1097 pub fn create_encoder(self) -> PngEncoder {
1098 PngEncoder::with_config(self.to_config())
1099 }
1100}
1101
1102pub struct ParallelPngEncoder {
1104 config: EncoderConfig,
1105}
1106
1107impl ParallelPngEncoder {
1108 #[must_use]
1110 pub const fn new(config: EncoderConfig) -> Self {
1111 Self { config }
1112 }
1113
1114 pub fn encode_rgba(&self, width: u32, height: u32, data: &[u8]) -> CodecResult<Vec<u8>> {
1120 let encoder = PngEncoder::with_config(self.config.clone());
1123 encoder.encode_rgba(width, height, data)
1124 }
1125}
1126
1127#[must_use]
1129pub fn encoder_from_profile(profile: EncodingProfile) -> PngEncoder {
1130 profile.create_encoder()
1131}
1132
1133pub fn batch_encode(
1139 images: &[(u32, u32, &[u8])],
1140 config: EncoderConfig,
1141) -> CodecResult<Vec<Vec<u8>>> {
1142 let encoder = PngEncoder::with_config(config);
1143 images
1144 .iter()
1145 .map(|(width, height, data)| encoder.encode_rgba(*width, *height, data))
1146 .collect()
1147}