Skip to main content

fastpack_compress/backends/
png.rs

1use std::io::Cursor;
2
3use fastpack_core::types::config::PackMode;
4use image::ImageFormat;
5
6use crate::{
7    compressor::{CompressInput, CompressOutput, Compressor},
8    error::CompressError,
9};
10
11/// Lossless PNG encoder backed by the `image` crate with optional oxipng optimization.
12///
13/// `PackMode::Fast` encodes only (no post-processing).
14/// `PackMode::Good` runs oxipng at preset level 3.
15/// `PackMode::Best` runs oxipng at preset level 6 (maximum libdeflater compression).
16pub struct PngCompressor;
17
18impl Compressor for PngCompressor {
19    fn compress(&self, input: &CompressInput<'_>) -> Result<CompressOutput, CompressError> {
20        compress_png(input)
21    }
22
23    fn format_id(&self) -> &'static str {
24        "png"
25    }
26
27    fn file_extension(&self) -> &'static str {
28        "png"
29    }
30}
31
32fn compress_png(input: &CompressInput<'_>) -> Result<CompressOutput, CompressError> {
33    let mut buf = Cursor::new(Vec::new());
34    input.image.write_to(&mut buf, ImageFormat::Png)?;
35    let png_bytes = buf.into_inner();
36
37    #[cfg(feature = "png")]
38    match input.pack_mode {
39        PackMode::Fast => {}
40        PackMode::Good => {
41            let opts = oxipng::Options::from_preset(3);
42            let optimized = oxipng::optimize_from_memory(&png_bytes, &opts)?;
43            return Ok(CompressOutput { data: optimized });
44        }
45        PackMode::Best => {
46            let opts = oxipng::Options::from_preset(6);
47            let optimized = oxipng::optimize_from_memory(&png_bytes, &opts)?;
48            return Ok(CompressOutput { data: optimized });
49        }
50    }
51
52    Ok(CompressOutput { data: png_bytes })
53}
54
55/// Lossy PNG encoder using imagequant palette reduction followed by oxipng compression.
56///
57/// `CompressInput::quality` (0–100) controls colour fidelity; lower values produce
58/// smaller files. After palette reduction the result is passed through oxipng at
59/// the same effort level as `PngCompressor`.
60///
61/// Requires the `png` cargo feature (enabled by default).
62#[cfg(feature = "png")]
63pub struct LossyPngCompressor;
64
65#[cfg(feature = "png")]
66impl Compressor for LossyPngCompressor {
67    fn compress(&self, input: &CompressInput<'_>) -> Result<CompressOutput, CompressError> {
68        compress_lossy_png(input)
69    }
70
71    fn format_id(&self) -> &'static str {
72        "png"
73    }
74
75    fn file_extension(&self) -> &'static str {
76        "png"
77    }
78}
79
80#[cfg(feature = "png")]
81fn compress_lossy_png(input: &CompressInput<'_>) -> Result<CompressOutput, CompressError> {
82    let rgba = input.image.to_rgba8();
83    let (w, h) = rgba.dimensions();
84    let quality = input.quality;
85
86    let pixels: Vec<imagequant::RGBA> = rgba
87        .pixels()
88        .map(|p| imagequant::RGBA {
89            r: p[0],
90            g: p[1],
91            b: p[2],
92            a: p[3],
93        })
94        .collect();
95
96    let mut liq = imagequant::new();
97    liq.set_quality(0, quality)
98        .map_err(|e| CompressError::Other(e.to_string()))?;
99    let mut img = liq
100        .new_image_borrowed(&pixels, w as usize, h as usize, 0.0)
101        .map_err(|e| CompressError::Other(e.to_string()))?;
102    let mut res = liq
103        .quantize(&mut img)
104        .map_err(|e| CompressError::Other(e.to_string()))?;
105    res.set_dithering_level(1.0)
106        .map_err(|e| CompressError::Other(e.to_string()))?;
107    let (palette, indexed) = res
108        .remapped(&mut img)
109        .map_err(|e| CompressError::Other(e.to_string()))?;
110
111    let mut png_bytes = Vec::new();
112    {
113        let mut enc = png::Encoder::new(&mut png_bytes, w, h);
114        enc.set_color(png::ColorType::Indexed);
115        enc.set_depth(png::BitDepth::Eight);
116        let pal: Vec<u8> = palette.iter().flat_map(|c| [c.r, c.g, c.b]).collect();
117        enc.set_palette(pal);
118        let trns: Vec<u8> = palette.iter().map(|c| c.a).collect();
119        enc.set_trns(trns);
120        let mut writer = enc
121            .write_header()
122            .map_err(|e| CompressError::Other(e.to_string()))?;
123        writer
124            .write_image_data(&indexed)
125            .map_err(|e| CompressError::Other(e.to_string()))?;
126    }
127
128    match input.pack_mode {
129        PackMode::Fast => {}
130        PackMode::Good => {
131            let opts = oxipng::Options::from_preset(3);
132            let optimized = oxipng::optimize_from_memory(&png_bytes, &opts)?;
133            return Ok(CompressOutput { data: optimized });
134        }
135        PackMode::Best => {
136            let opts = oxipng::Options::from_preset(6);
137            let optimized = oxipng::optimize_from_memory(&png_bytes, &opts)?;
138            return Ok(CompressOutput { data: optimized });
139        }
140    }
141
142    Ok(CompressOutput { data: png_bytes })
143}