1use crate::{
4 image_backend::{Decoded, ImageInfo},
5 optimizer, png_pixels, quality,
6};
7use anyhow::{Context, Result, ensure};
8use serde::Serialize;
9
10pub const WEB_MAX_INPUT_BYTES: usize = 16 * 1024 * 1024;
11pub const WEB_MAX_PIXELS: usize = 2 * 1024 * 1024;
12
13#[derive(Debug, Serialize)]
14pub struct PngSummary {
15 pub width: usize,
16 pub height: usize,
17 pub original_bytes: usize,
18 pub optimized_bytes: usize,
19 pub saved_bytes: usize,
20 pub transparent_pixels: usize,
21 pub pixel_equivalent: bool,
22 pub ssimulacra2: f64,
23 pub reductions: bool,
24}
25
26pub struct OptimizedPng {
27 pub bytes: Vec<u8>,
28 pub summary: PngSummary,
29}
30
31pub fn optimize_png(bytes: &[u8], effort: u8, reductions: bool) -> Result<OptimizedPng> {
34 ensure!(
35 bytes.len() <= WEB_MAX_INPUT_BYTES,
36 "PNG exceeds the 16 MiB browser input limit"
37 );
38 ensure!(effort <= 4, "browser effort must be 0..=4");
39 let original = decode_png(bytes, WEB_MAX_PIXELS)?;
40 let candidate = optimizer::optimize(
41 bytes,
42 &optimizer::Policy {
43 png_level: effort,
44 reductions,
45 ..Default::default()
46 },
47 )?;
48 optimizer::verify(bytes, &candidate, reductions)?;
49 let chosen = if candidate.len() < bytes.len() {
50 candidate
51 } else {
52 bytes.to_vec()
53 };
54 let decoded = decode_png(&chosen, WEB_MAX_PIXELS)?;
55 let score = quality::ssimulacra2(&original, &decoded)?;
56 Ok(OptimizedPng {
57 summary: PngSummary {
58 width: original.info.width,
59 height: original.info.height,
60 original_bytes: bytes.len(),
61 optimized_bytes: chosen.len(),
62 saved_bytes: bytes.len().saturating_sub(chosen.len()),
63 transparent_pixels: original.info.transparent_pixels,
64 pixel_equivalent: true,
65 ssimulacra2: score,
66 reductions,
67 },
68 bytes: chosen,
69 })
70}
71
72pub(crate) fn decode_png(bytes: &[u8], max_pixels: usize) -> Result<Decoded> {
73 let limit = max_pixels.checked_mul(16).context("pixel limit overflow")?;
74 let mut reader = png_pixels::reader(bytes, limit)?;
75 let width = reader.info().width as usize;
76 let height = reader.info().height as usize;
77 let count = width
78 .checked_mul(height)
79 .context("PNG dimensions overflow")?;
80 ensure!(
81 count > 0 && count <= max_pixels,
82 "decoded_image_exceeds_max_pixels"
83 );
84 ensure!(
85 reader.info().animation_control.is_none(),
86 "animated PNG is not supported"
87 );
88 let mut pixels = Vec::with_capacity(count * 4);
89 let mut transparent_pixels = 0;
90 while let Some(row) = png_pixels::next_rgba_row(&mut reader)? {
91 for pixel in row.as_chunks::<4>().0 {
92 let alpha = f32::from(pixel[3]) / 65535.0;
93 transparent_pixels += usize::from(pixel[3] != u16::MAX);
94 pixels.extend([
95 f32::from(pixel[0]) / 65535.0 * alpha,
96 f32::from(pixel[1]) / 65535.0 * alpha,
97 f32::from(pixel[2]) / 65535.0 * alpha,
98 alpha,
99 ]);
100 }
101 }
102 ensure!(pixels.len() == count * 4, "incomplete PNG pixels");
103 Ok(Decoded {
104 info: ImageInfo {
105 decoder_type: "png".into(),
106 width,
107 height,
108 frames: 1,
109 bits_per_component: 16,
110 orientation: 1,
111 transparent_pixels,
112 has_transparent_pixels: transparent_pixels > 0,
113 },
114 pixels,
115 })
116}
117
118pub fn score_srgb_rgba(
121 width: usize,
122 height: usize,
123 original: &[u8],
124 candidate: &[u8],
125) -> Result<f64> {
126 let count = width
127 .checked_mul(height)
128 .context("image dimensions overflow")?;
129 ensure!(
130 count > 0 && count <= WEB_MAX_PIXELS,
131 "comparison exceeds browser pixel limit"
132 );
133 ensure!(
134 original.len() == count * 4 && candidate.len() == count * 4,
135 "RGBA buffer length mismatch"
136 );
137 let decode = |bytes: &[u8]| {
138 let transparent_pixels = bytes
139 .as_chunks::<4>()
140 .0
141 .iter()
142 .filter(|p| p[3] != 255)
143 .count();
144 Decoded {
145 info: ImageInfo {
146 decoder_type: "sRGB ImageData".into(),
147 width,
148 height,
149 frames: 1,
150 bits_per_component: 8,
151 orientation: 1,
152 transparent_pixels,
153 has_transparent_pixels: transparent_pixels > 0,
154 },
155 pixels: bytes
156 .as_chunks::<4>()
157 .0
158 .iter()
159 .flat_map(|p| {
160 let a = f32::from(p[3]) / 255.0;
161 [
162 f32::from(p[0]) / 255.0 * a,
163 f32::from(p[1]) / 255.0 * a,
164 f32::from(p[2]) / 255.0 * a,
165 a,
166 ]
167 })
168 .collect(),
169 }
170 };
171 quality::ssimulacra2(&decode(original), &decode(candidate))
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 fn png() -> Vec<u8> {
178 let mut bytes = Vec::new();
179 {
180 let mut encoder = png::Encoder::new(&mut bytes, 64, 64);
181 encoder.set_color(png::ColorType::Rgba);
182 encoder.set_compression(png::Compression::NoCompression);
183 encoder
184 .write_header()
185 .unwrap()
186 .write_image_data(&[40, 80, 120, 128].repeat(64 * 64))
187 .unwrap();
188 }
189 bytes
190 }
191 #[test]
192 fn portable_optimization_preserves_alpha_and_never_grows_output() {
193 let original = png();
194 let first = optimize_png(&original, 2, true).unwrap();
195 assert!(first.bytes.len() < original.len());
196 assert_eq!(first.summary.transparent_pixels, 64 * 64);
197 assert!((first.summary.ssimulacra2 - 100.0).abs() < 0.01);
198 let second = optimize_png(&first.bytes, 2, true).unwrap();
199 assert!(second.bytes.len() <= first.bytes.len());
200 }
201 #[test]
202 fn malformed_inputs_and_rgba_lengths_are_rejected() {
203 assert!(optimize_png(b"invalid", 2, true).is_err());
204 assert!(optimize_png(&png(), 5, true).is_err());
205 assert!(score_srgb_rgba(64, 64, &[0; 4], &[0; 4]).is_err());
206 assert!(score_srgb_rgba(WEB_MAX_PIXELS + 1, 1, &[], &[]).is_err());
207 }
208}