1use otf_pixels_core::{
19 EncodeOptions, Encoder, ImageDescriptor, PixelFormat, PixelsError, Result, Sink,
20};
21
22use crate::format::{ColorType, Filter, SIGNATURE, apply_filter, write_chunk};
23use otf_pixels_compress::{Level, zlib_compress};
24
25#[derive(Debug)]
27pub struct PngEncoder {
28 level: Level,
29 state: Option<State>,
31 icc: Option<Vec<u8>>,
33}
34
35#[derive(Debug)]
37struct State {
38 descriptor: ImageDescriptor,
39 bit_depth: u8,
40 stride: usize,
42 filtered: Vec<u8>,
44 previous: Vec<u8>,
46 swapped: Vec<u8>,
48 rows_written: u32,
49}
50
51impl PngEncoder {
52 #[must_use]
54 pub const fn new() -> Self {
55 Self {
56 level: Level::DEFAULT,
57 state: None,
58 icc: None,
59 }
60 }
61
62 #[must_use]
64 pub const fn with_level(level: Level) -> Self {
65 Self {
66 level,
67 state: None,
68 icc: None,
69 }
70 }
71
72 #[must_use]
78 pub fn from_options(options: &EncodeOptions) -> Self {
79 let quality = u32::from(options.quality.clamp(1, 100));
80 let level = ((quality - 1) * 8 / 99 + 1) as u8;
83 Self::with_level(Level::new(level).unwrap_or(Level::DEFAULT))
84 }
85
86 fn png_type_of(format: PixelFormat) -> Result<(ColorType, u8)> {
88 match format {
89 PixelFormat::Gray8 => Ok((ColorType::Grayscale, 8)),
90 PixelFormat::Gray16 => Ok((ColorType::Grayscale, 16)),
91 PixelFormat::GrayA8 => Ok((ColorType::GrayscaleAlpha, 8)),
92 PixelFormat::Rgb8 => Ok((ColorType::Rgb, 8)),
93 PixelFormat::Rgb16 => Ok((ColorType::Rgb, 16)),
94 PixelFormat::Rgba8 => Ok((ColorType::Rgba, 8)),
95 PixelFormat::Rgba16 => Ok((ColorType::Rgba, 16)),
96 other => Err(PixelsError::unsupported(format!(
98 "PNG cannot represent {other}; convert to an integer format first"
99 ))),
100 }
101 }
102}
103
104impl Default for PngEncoder {
105 fn default() -> Self {
106 Self::new()
107 }
108}
109
110impl State {
111 fn append_filtered(&mut self, row: &[u8]) {
118 let candidates: &[Filter] = &[
119 Filter::None,
120 Filter::Sub,
121 Filter::Up,
122 Filter::Average,
123 Filter::Paeth,
124 ];
125
126 let mut best = Filter::None;
127 let mut best_score = u64::MAX;
128 let mut scratch = Vec::with_capacity(row.len());
129 for &filter in candidates {
130 scratch.clear();
131 apply_filter(filter, row, &self.previous, self.stride, &mut scratch);
132 let score: u64 = scratch
133 .iter()
134 .map(|&byte| u64::from((byte as i8).unsigned_abs()))
135 .sum();
136 if score < best_score {
137 best_score = score;
138 best = filter;
139 }
140 }
141
142 self.filtered.push(best.to_byte());
143 apply_filter(best, row, &self.previous, self.stride, &mut self.filtered);
144 self.previous.clear();
145 self.previous.extend_from_slice(row);
146 }
147}
148
149fn to_big_endian_16(row: &[u8], out: &mut Vec<u8>) {
154 out.clear();
155 for pair in row.chunks_exact(2) {
156 let value = u16::from_ne_bytes([
157 pair.first().copied().unwrap_or(0),
158 pair.get(1).copied().unwrap_or(0),
159 ]);
160 out.extend_from_slice(&value.to_be_bytes());
161 }
162}
163
164impl Encoder for PngEncoder {
165 fn set_icc_profile(&mut self, profile: Option<&[u8]>) -> Result<()> {
166 if self.state.is_some() {
167 return Err(PixelsError::invalid_argument(
168 "profile",
169 "the ICC profile must be set before write_header",
170 ));
171 }
172 self.icc = profile.map(<[u8]>::to_vec);
173 Ok(())
174 }
175
176 fn write_header(&mut self, desc: &ImageDescriptor, sink: &mut dyn Sink) -> Result<()> {
177 if self.state.is_some() {
178 return Err(PixelsError::invalid_argument(
179 "descriptor",
180 "write_header called more than once",
181 ));
182 }
183 if desc.width == 0 || desc.height == 0 {
184 return Err(PixelsError::invalid_argument(
185 "descriptor",
186 format!(
187 "PNG dimensions must be non-zero, got {}x{}",
188 desc.width, desc.height
189 ),
190 ));
191 }
192 let (color_type, bit_depth) = Self::png_type_of(desc.pixel)?;
193
194 sink.write_all(&SIGNATURE)?;
195 let mut ihdr = Vec::with_capacity(13);
196 ihdr.extend_from_slice(&desc.width.to_be_bytes());
197 ihdr.extend_from_slice(&desc.height.to_be_bytes());
198 ihdr.push(bit_depth);
199 ihdr.push(color_type.to_byte());
200 ihdr.extend_from_slice(&[0, 0, 0]);
203 let mut chunk = Vec::new();
204 write_chunk(&mut chunk, b"IHDR", &ihdr);
205 if let Some(profile) = &self.icc {
206 let mut iccp = b"ICC Profile\0\0".to_vec();
208 iccp.extend(zlib_compress(profile, self.level).map_err(crate::compress_error)?);
209 write_chunk(&mut chunk, b"iCCP", &iccp);
210 }
211 sink.write_all(&chunk)?;
212
213 let row_bytes = desc.row_bytes();
214 let stride = (color_type.channels() * bit_depth as usize).div_ceil(8);
215 self.state = Some(State {
216 descriptor: *desc,
217 bit_depth,
218 stride,
219 filtered: Vec::with_capacity((row_bytes + 1) * desc.height as usize),
221 previous: vec![0_u8; row_bytes],
222 swapped: Vec::new(),
223 rows_written: 0,
224 });
225 Ok(())
226 }
227
228 fn write_row(&mut self, row: &[u8], _sink: &mut dyn Sink) -> Result<()> {
229 let Some(state) = self.state.as_mut() else {
230 return Err(PixelsError::invalid_argument(
231 "row",
232 "write_row called before write_header",
233 ));
234 };
235 let expected = state.descriptor.row_bytes();
236 if row.len() != expected {
237 return Err(PixelsError::invalid_argument(
238 "row",
239 format!("row is {} bytes, expected {expected}", row.len()),
240 ));
241 }
242 if state.rows_written >= state.descriptor.height {
243 return Err(PixelsError::invalid_argument(
244 "row",
245 format!("more than {} rows written", state.descriptor.height),
246 ));
247 }
248
249 if state.bit_depth == 16 {
250 let mut swapped = std::mem::take(&mut state.swapped);
251 to_big_endian_16(row, &mut swapped);
252 state.append_filtered(&swapped);
253 state.swapped = swapped;
254 } else {
255 state.append_filtered(row);
256 }
257 state.rows_written += 1;
258 Ok(())
259 }
260
261 fn finish(&mut self, sink: &mut dyn Sink) -> Result<()> {
262 let Some(state) = self.state.as_mut() else {
263 return Err(PixelsError::malformed(
264 "png",
265 "finish called before write_header",
266 ));
267 };
268 if state.rows_written != state.descriptor.height {
269 return Err(PixelsError::malformed(
270 "png",
271 format!(
272 "{} of {} rows written; a partial image is never emitted",
273 state.rows_written, state.descriptor.height
274 ),
275 ));
276 }
277
278 let compressed =
279 zlib_compress(&state.filtered, self.level).map_err(crate::compress_error)?;
280 let mut chunk = Vec::with_capacity(compressed.len() + 12);
281 write_chunk(&mut chunk, b"IDAT", &compressed);
282 write_chunk(&mut chunk, b"IEND", &[]);
283 sink.write_all(&chunk)?;
284 sink.flush()?;
285
286 state.filtered = Vec::new();
289 state.previous = Vec::new();
290 state.swapped = Vec::new();
291 Ok(())
292 }
293}
294
295#[cfg(test)]
296#[allow(
297 clippy::unwrap_used,
298 clippy::expect_used,
299 clippy::indexing_slicing,
300 clippy::panic,
301 reason = "tests operate on known-good values and assert shapes directly"
302)]
303mod tests {
304 use super::*;
305 use crate::decoder::PngDecoder;
306 use otf_pixels_core::{Decoder, Limits};
307
308 fn encode(desc: &ImageDescriptor, raster: &[u8], level: Level) -> Result<Vec<u8>> {
310 let mut encoder = PngEncoder::with_level(level);
311 let mut out: Vec<u8> = Vec::new();
312 encoder.write_header(desc, &mut out)?;
313 for row in raster.chunks_exact(desc.row_bytes()) {
314 encoder.write_row(row, &mut out)?;
315 }
316 encoder.finish(&mut out)?;
317 Ok(out)
318 }
319
320 fn decode(bytes: &[u8]) -> Result<(ImageDescriptor, Vec<u8>)> {
322 let mut decoder = PngDecoder::new(bytes, Limits::default())?;
323 let mut row = vec![0_u8; decoder.descriptor().row_bytes()];
324 let mut raster = Vec::new();
325 for _ in 0..decoder.descriptor().height {
326 decoder.read_row(&mut row)?;
327 raster.extend_from_slice(&row);
328 }
329 Ok((decoder.descriptor(), raster))
330 }
331
332 fn sample(desc: &ImageDescriptor) -> Vec<u8> {
335 let mut raster = vec![0_u8; desc.byte_len().unwrap()];
336 for (index, byte) in raster.iter_mut().enumerate() {
337 let value = if index % 97 < 40 {
338 (index % 251) as u8
339 } else {
340 ((index * 7) % 13) as u8
341 };
342 *byte = value;
343 }
344 raster
345 }
346
347 #[test]
348 fn every_supported_format_round_trips() {
349 for format in [
350 PixelFormat::Gray8,
351 PixelFormat::Gray16,
352 PixelFormat::GrayA8,
353 PixelFormat::Rgb8,
354 PixelFormat::Rgb16,
355 PixelFormat::Rgba8,
356 PixelFormat::Rgba16,
357 ] {
358 let desc = ImageDescriptor::new(23, 17, format).unwrap();
359 let raster = sample(&desc);
360 let bytes = encode(&desc, &raster, Level::DEFAULT)
361 .unwrap_or_else(|e| panic!("encoding {format}: {e}"));
362 let (decoded_desc, decoded) =
363 decode(&bytes).unwrap_or_else(|e| panic!("decoding {format}: {e}"));
364 assert_eq!(decoded_desc.pixel, format, "{format} changed format");
365 assert_eq!(
366 (decoded_desc.width, decoded_desc.height),
367 (23, 17),
368 "{format}"
369 );
370 assert_eq!(decoded, raster, "{format} did not round-trip");
371 }
372 }
373
374 #[test]
375 fn every_level_round_trips_to_the_same_pixels() {
376 let desc = ImageDescriptor::new(19, 11, PixelFormat::Rgba8).unwrap();
377 let raster = sample(&desc);
378 for level in 0..=9 {
379 let level = Level::new(level).unwrap();
380 let bytes = encode(&desc, &raster, level).unwrap();
381 let (_, decoded) = decode(&bytes).unwrap();
382 assert_eq!(decoded, raster, "level {} did not round-trip", level.get());
383 }
384 }
385
386 #[test]
387 fn the_output_starts_with_the_signature_and_ihdr() {
388 let desc = ImageDescriptor::new(4, 4, PixelFormat::Rgb8).unwrap();
389 let bytes = encode(&desc, &sample(&desc), Level::FAST).unwrap();
390 assert_eq!(&bytes[..8], &SIGNATURE, "signature");
391 assert_eq!(&bytes[12..16], b"IHDR", "first chunk");
392 assert_eq!(
393 &bytes[bytes.len() - 8..bytes.len() - 4],
394 b"IEND",
395 "last chunk"
396 );
397 }
398
399 #[test]
400 fn sixteen_bit_samples_are_written_big_endian() {
401 let mut out = Vec::new();
406 to_big_endian_16(&0x1234_u16.to_ne_bytes(), &mut out);
407 assert_eq!(out, vec![0x12, 0x34]);
408 }
409
410 #[test]
411 fn compression_shrinks_a_compressible_image() {
412 let desc = ImageDescriptor::new(64, 64, PixelFormat::Rgb8).unwrap();
415 let raster = vec![7_u8; desc.byte_len().unwrap()];
416 let stored = encode(&desc, &raster, Level::NONE).unwrap();
417 let packed = encode(&desc, &raster, Level::BEST).unwrap();
418 assert!(
419 packed.len() * 10 < stored.len(),
420 "level 9 produced {} bytes against level 0's {}",
421 packed.len(),
422 stored.len()
423 );
424 assert_eq!(decode(&packed).unwrap().1, raster);
425 }
426
427 #[test]
428 fn a_float_format_is_unsupported_not_a_panic() {
429 let desc = ImageDescriptor::new(2, 2, PixelFormat::RgbaF32).unwrap();
430 let mut encoder = PngEncoder::new();
431 let mut out = Vec::new();
432 let error = encoder.write_header(&desc, &mut out).unwrap_err();
433 assert!(matches!(error, PixelsError::Unsupported { .. }), "{error}");
434 assert!(
435 out.is_empty(),
436 "nothing should be written for a rejected format"
437 );
438 }
439
440 #[test]
441 fn a_short_image_is_an_error_not_a_truncated_png() {
442 let desc = ImageDescriptor::new(4, 4, PixelFormat::Gray8).unwrap();
443 let mut encoder = PngEncoder::new();
444 let mut out = Vec::new();
445 encoder.write_header(&desc, &mut out).unwrap();
446 encoder.write_row(&[0; 4], &mut out).unwrap();
447 let error = encoder.finish(&mut out).unwrap_err();
448 assert!(matches!(error, PixelsError::Malformed { .. }), "{error}");
449 }
450
451 #[test]
452 fn extra_rows_and_wrong_row_lengths_are_errors() {
453 let desc = ImageDescriptor::new(4, 1, PixelFormat::Gray8).unwrap();
454 let mut encoder = PngEncoder::new();
455 let mut out = Vec::new();
456 encoder.write_header(&desc, &mut out).unwrap();
457 assert!(encoder.write_row(&[0; 3], &mut out).is_err(), "short row");
458 assert!(encoder.write_row(&[0; 5], &mut out).is_err(), "long row");
459 encoder.write_row(&[0; 4], &mut out).unwrap();
460 assert!(encoder.write_row(&[0; 4], &mut out).is_err(), "extra row");
461 }
462
463 #[test]
464 fn rows_before_the_header_are_an_error() {
465 let mut encoder = PngEncoder::new();
466 let mut out = Vec::new();
467 assert!(encoder.write_row(&[0; 4], &mut out).is_err());
468 assert!(encoder.finish(&mut out).is_err());
469 }
470
471 #[test]
472 fn a_second_header_is_an_error() {
473 let desc = ImageDescriptor::new(2, 2, PixelFormat::Gray8).unwrap();
474 let mut encoder = PngEncoder::new();
475 let mut out = Vec::new();
476 encoder.write_header(&desc, &mut out).unwrap();
477 assert!(encoder.write_header(&desc, &mut out).is_err());
478 }
479
480 #[test]
481 fn zero_dimensions_never_reach_the_encoder() {
482 assert!(ImageDescriptor::new(0, 4, PixelFormat::Gray8).is_err());
487 assert!(ImageDescriptor::new(4, 0, PixelFormat::Gray8).is_err());
488 }
489
490 #[test]
491 fn options_map_quality_onto_compression_effort() {
492 assert_eq!(
493 PngEncoder::from_options(&EncodeOptions::default())
494 .level
495 .get(),
496 7
497 );
498 let lowest = EncodeOptions::with_quality(1).unwrap();
499 assert_eq!(PngEncoder::from_options(&lowest).level.get(), 1);
500 let highest = EncodeOptions::with_quality(100).unwrap();
501 assert_eq!(PngEncoder::from_options(&highest).level.get(), 9);
502 }
503
504 #[test]
505 fn a_single_pixel_image_round_trips() {
506 let desc = ImageDescriptor::new(1, 1, PixelFormat::Rgba8).unwrap();
508 let raster = vec![1, 2, 3, 4];
509 let bytes = encode(&desc, &raster, Level::BEST).unwrap();
510 assert_eq!(decode(&bytes).unwrap().1, raster);
511 }
512}