1use otf_pixels_core::{
26 EncodeOptions, Encoder, ImageDescriptor, PixelFormat, PixelsError, Result, Sink,
27};
28
29use crate::format::{ColorType, Filter, SIGNATURE, apply_filter, write_chunk};
30use otf_pixels_compress::{Level, build_palette, build_palette_rgba, quantize_with, zlib_compress};
31use otf_pixels_core::{ChannelLayout, SampleKind};
32
33#[derive(Debug)]
35pub struct PngEncoder {
36 level: Level,
37 palette: Option<(usize, f32)>,
39 state: Option<State>,
41 icc: Option<Vec<u8>>,
43 exif: Option<Vec<u8>>,
45}
46
47#[derive(Debug)]
49struct State {
50 descriptor: ImageDescriptor,
51 bit_depth: u8,
52 stride: usize,
54 filtered: Vec<u8>,
56 previous: Vec<u8>,
58 swapped: Vec<u8>,
60 true_colour: Option<Vec<u8>>,
63 rows_written: u32,
64}
65
66impl PngEncoder {
67 #[must_use]
69 pub const fn new() -> Self {
70 Self::with_level(Level::DEFAULT)
71 }
72
73 #[must_use]
75 pub const fn with_level(level: Level) -> Self {
76 Self {
77 level,
78 palette: None,
79 state: None,
80 icc: None,
81 exif: None,
82 }
83 }
84
85 pub fn with_palette(mut self, colours: usize, dither: f32) -> Result<Self> {
93 if !(2..=256).contains(&colours) {
94 return Err(PixelsError::invalid_argument(
95 "colours",
96 format!("palette size must be in 2..=256, got {colours}"),
97 ));
98 }
99 if !(0.0..=1.0).contains(&dither) {
100 return Err(PixelsError::invalid_argument(
101 "dither",
102 format!("must be in 0..=1, got {dither}"),
103 ));
104 }
105 self.palette = Some((colours, dither));
106 Ok(self)
107 }
108
109 #[must_use]
120 pub fn from_options(options: &EncodeOptions) -> Self {
121 let level = options.compression_level.unwrap_or_else(|| {
122 let quality = u32::from(options.quality.clamp(1, 100));
123 ((quality - 1) * 8 / 99 + 1) as u8
126 });
127 let mut encoder = Self::with_level(Level::new(level).unwrap_or(Level::DEFAULT));
128 if options.wants_palette() {
129 let colours = options.colours.map_or(256, usize::from).clamp(2, 256);
130 let dither = options.dither.unwrap_or(1.0).clamp(0.0, 1.0);
131 encoder.palette = Some((colours, dither));
132 }
133 encoder
134 }
135
136 fn png_type_of(format: PixelFormat) -> Result<(ColorType, u8)> {
138 match format {
139 PixelFormat::Gray8 => Ok((ColorType::Grayscale, 8)),
140 PixelFormat::Gray16 => Ok((ColorType::Grayscale, 16)),
141 PixelFormat::GrayA8 => Ok((ColorType::GrayscaleAlpha, 8)),
142 PixelFormat::Rgb8 => Ok((ColorType::Rgb, 8)),
143 PixelFormat::Rgb16 => Ok((ColorType::Rgb, 16)),
144 PixelFormat::Rgba8 => Ok((ColorType::Rgba, 8)),
145 PixelFormat::Rgba16 => Ok((ColorType::Rgba, 16)),
146 other => Err(PixelsError::unsupported(format!(
148 "PNG cannot represent {other}; convert to an integer format first"
149 ))),
150 }
151 }
152}
153
154impl Default for PngEncoder {
155 fn default() -> Self {
156 Self::new()
157 }
158}
159
160impl State {
161 fn append_filtered(&mut self, row: &[u8]) {
168 let candidates: &[Filter] = &[
169 Filter::None,
170 Filter::Sub,
171 Filter::Up,
172 Filter::Average,
173 Filter::Paeth,
174 ];
175
176 let mut best = Filter::None;
177 let mut best_score = u64::MAX;
178 let mut scratch = Vec::with_capacity(row.len());
179 for &filter in candidates {
180 scratch.clear();
181 apply_filter(filter, row, &self.previous, self.stride, &mut scratch);
182 let score: u64 = scratch
183 .iter()
184 .map(|&byte| u64::from((byte as i8).unsigned_abs()))
185 .sum();
186 if score < best_score {
187 best_score = score;
188 best = filter;
189 }
190 }
191
192 self.filtered.push(best.to_byte());
193 apply_filter(best, row, &self.previous, self.stride, &mut self.filtered);
194 self.previous.clear();
195 self.previous.extend_from_slice(row);
196 }
197}
198
199fn to_big_endian_16(row: &[u8], out: &mut Vec<u8>) {
204 out.clear();
205 for pair in row.chunks_exact(2) {
206 let value = u16::from_ne_bytes([
207 pair.first().copied().unwrap_or(0),
208 pair.get(1).copied().unwrap_or(0),
209 ]);
210 out.extend_from_slice(&value.to_be_bytes());
211 }
212}
213
214fn append_8_bit(row: &[u8], format: PixelFormat, out: &mut Vec<u8>) {
216 let wide = format.sample_kind() == SampleKind::U16;
217 let sample = |pixel: &[u8], index: usize| -> u8 {
218 if wide {
219 let at = index * 2;
220 let v = u16::from_ne_bytes([
221 pixel.get(at).copied().unwrap_or(0),
222 pixel.get(at + 1).copied().unwrap_or(0),
223 ]);
224 ((u32::from(v) * 255 + 32_767) / 65_535) as u8
226 } else {
227 pixel.get(index).copied().unwrap_or(0)
228 }
229 };
230 for pixel in row.chunks_exact(format.bytes_per_pixel()) {
231 match format.layout() {
232 ChannelLayout::Gray => {
233 let g = sample(pixel, 0);
234 out.extend_from_slice(&[g, g, g]);
235 }
236 ChannelLayout::GrayAlpha => {
237 let g = sample(pixel, 0);
238 out.extend_from_slice(&[g, g, g, sample(pixel, 1)]);
239 }
240 ChannelLayout::Rgb => out.extend((0..3).map(|i| sample(pixel, i))),
241 ChannelLayout::Rgba => out.extend((0..4).map(|i| sample(pixel, i))),
242 }
243 }
244}
245
246fn encode_palette(
249 descriptor: &ImageDescriptor,
250 pixels: &[u8],
251 colours: usize,
252 dither: f32,
253 level: Level,
254 icc: Option<&[u8]>,
255 exif: Option<&[u8]>,
256) -> Result<Vec<u8>> {
257 let width = descriptor.width as usize;
258 let (entries, indices): (Vec<[u8; 4]>, Vec<u8>) = if descriptor.pixel.layout().has_alpha() {
259 let palette = build_palette_rgba(pixels, colours);
260 let indices = quantize_with(pixels, width, &palette, dither);
261 (palette.entries().to_vec(), indices)
262 } else {
263 let palette = build_palette(pixels, colours);
264 let indices = quantize_with(pixels, width, &palette, dither);
265 let entries = palette
266 .entries()
267 .iter()
268 .map(|&[r, g, b]| [r, g, b, 255])
269 .collect();
270 (entries, indices)
271 };
272
273 let bit_depth: u8 = match entries.len() {
275 0..=2 => 1,
276 3..=4 => 2,
277 5..=16 => 4,
278 _ => 8,
279 };
280 let per_byte = 8 / usize::from(bit_depth);
281 let row_bytes = width.div_ceil(per_byte);
282 let mut scanlines = Vec::with_capacity((row_bytes + 1) * descriptor.height as usize);
283 for row in indices.chunks_exact(width) {
284 scanlines.push(Filter::None.to_byte());
285 for group in row.chunks(per_byte) {
286 let mut byte = 0_u8;
287 for (i, &index) in group.iter().enumerate() {
288 byte |= index << (8 - usize::from(bit_depth) * (i + 1));
290 }
291 scanlines.push(byte);
292 }
293 }
294
295 let mut out = SIGNATURE.to_vec();
296 let mut ihdr = Vec::with_capacity(13);
297 ihdr.extend_from_slice(&descriptor.width.to_be_bytes());
298 ihdr.extend_from_slice(&descriptor.height.to_be_bytes());
299 ihdr.extend_from_slice(&[bit_depth, ColorType::Palette.to_byte(), 0, 0, 0]);
300 write_chunk(&mut out, b"IHDR", &ihdr);
301 if let Some(profile) = icc {
302 let mut iccp = b"ICC Profile\0\0".to_vec();
303 iccp.extend(zlib_compress(profile, level).map_err(crate::compress_error)?);
304 write_chunk(&mut out, b"iCCP", &iccp);
305 }
306 if let Some(exif) = exif {
307 write_chunk(&mut out, b"eXIf", exif);
308 }
309 let plte: Vec<u8> = entries.iter().flat_map(|e| [e[0], e[1], e[2]]).collect();
310 write_chunk(&mut out, b"PLTE", &plte);
311 if let Some(last) = entries.iter().rposition(|e| e[3] != 255) {
314 let alphas: Vec<u8> = entries.iter().take(last + 1).map(|e| e[3]).collect();
315 write_chunk(&mut out, b"tRNS", &alphas);
316 }
317 let compressed = zlib_compress(&scanlines, level).map_err(crate::compress_error)?;
318 write_chunk(&mut out, b"IDAT", &compressed);
319 write_chunk(&mut out, b"IEND", &[]);
320 Ok(out)
321}
322
323impl Encoder for PngEncoder {
324 fn set_icc_profile(&mut self, profile: Option<&[u8]>) -> Result<()> {
325 if self.state.is_some() {
326 return Err(PixelsError::invalid_argument(
327 "profile",
328 "the ICC profile must be set before write_header",
329 ));
330 }
331 self.icc = profile.map(<[u8]>::to_vec);
332 Ok(())
333 }
334
335 fn set_exif(&mut self, exif: Option<&[u8]>) -> Result<()> {
336 if self.state.is_some() {
337 return Err(PixelsError::invalid_argument(
338 "exif",
339 "the EXIF block must be set before write_header",
340 ));
341 }
342 self.exif = exif.map(<[u8]>::to_vec);
343 Ok(())
344 }
345
346 fn write_header(&mut self, desc: &ImageDescriptor, sink: &mut dyn Sink) -> Result<()> {
347 if self.state.is_some() {
348 return Err(PixelsError::invalid_argument(
349 "descriptor",
350 "write_header called more than once",
351 ));
352 }
353 if desc.width == 0 || desc.height == 0 {
354 return Err(PixelsError::invalid_argument(
355 "descriptor",
356 format!(
357 "PNG dimensions must be non-zero, got {}x{}",
358 desc.width, desc.height
359 ),
360 ));
361 }
362 let (color_type, bit_depth) = Self::png_type_of(desc.pixel)?;
363
364 if self.palette.is_some() {
365 let channels = if desc.pixel.layout().has_alpha() {
368 4
369 } else {
370 3
371 };
372 self.state = Some(State {
373 descriptor: *desc,
374 bit_depth: 8,
375 stride: 1,
376 filtered: Vec::new(),
377 previous: Vec::new(),
378 swapped: Vec::new(),
379 true_colour: Some(Vec::with_capacity(
380 desc.width as usize * desc.height as usize * channels,
381 )),
382 rows_written: 0,
383 });
384 return Ok(());
385 }
386
387 sink.write_all(&SIGNATURE)?;
388 let mut ihdr = Vec::with_capacity(13);
389 ihdr.extend_from_slice(&desc.width.to_be_bytes());
390 ihdr.extend_from_slice(&desc.height.to_be_bytes());
391 ihdr.push(bit_depth);
392 ihdr.push(color_type.to_byte());
393 ihdr.extend_from_slice(&[0, 0, 0]);
396 let mut chunk = Vec::new();
397 write_chunk(&mut chunk, b"IHDR", &ihdr);
398 if let Some(profile) = &self.icc {
399 let mut iccp = b"ICC Profile\0\0".to_vec();
401 iccp.extend(zlib_compress(profile, self.level).map_err(crate::compress_error)?);
402 write_chunk(&mut chunk, b"iCCP", &iccp);
403 }
404 if let Some(exif) = &self.exif {
405 write_chunk(&mut chunk, b"eXIf", exif);
407 }
408 sink.write_all(&chunk)?;
409
410 let row_bytes = desc.row_bytes();
411 let stride = (color_type.channels() * bit_depth as usize).div_ceil(8);
412 self.state = Some(State {
413 descriptor: *desc,
414 bit_depth,
415 stride,
416 filtered: Vec::with_capacity((row_bytes + 1) * desc.height as usize),
418 previous: vec![0_u8; row_bytes],
419 swapped: Vec::new(),
420 true_colour: None,
421 rows_written: 0,
422 });
423 Ok(())
424 }
425
426 fn write_row(&mut self, row: &[u8], _sink: &mut dyn Sink) -> Result<()> {
427 let Some(state) = self.state.as_mut() else {
428 return Err(PixelsError::invalid_argument(
429 "row",
430 "write_row called before write_header",
431 ));
432 };
433 let expected = state.descriptor.row_bytes();
434 if row.len() != expected {
435 return Err(PixelsError::invalid_argument(
436 "row",
437 format!("row is {} bytes, expected {expected}", row.len()),
438 ));
439 }
440 if state.rows_written >= state.descriptor.height {
441 return Err(PixelsError::invalid_argument(
442 "row",
443 format!("more than {} rows written", state.descriptor.height),
444 ));
445 }
446
447 if let Some(pixels) = state.true_colour.as_mut() {
448 append_8_bit(row, state.descriptor.pixel, pixels);
449 } else if state.bit_depth == 16 {
450 let mut swapped = std::mem::take(&mut state.swapped);
451 to_big_endian_16(row, &mut swapped);
452 state.append_filtered(&swapped);
453 state.swapped = swapped;
454 } else {
455 state.append_filtered(row);
456 }
457 state.rows_written += 1;
458 Ok(())
459 }
460
461 fn finish(&mut self, sink: &mut dyn Sink) -> Result<()> {
462 let Some(state) = self.state.as_mut() else {
463 return Err(PixelsError::malformed(
464 "png",
465 "finish called before write_header",
466 ));
467 };
468 if state.rows_written != state.descriptor.height {
469 return Err(PixelsError::malformed(
470 "png",
471 format!(
472 "{} of {} rows written; a partial image is never emitted",
473 state.rows_written, state.descriptor.height
474 ),
475 ));
476 }
477
478 if let (Some(pixels), Some((colours, dither))) = (state.true_colour.take(), self.palette) {
479 let file = encode_palette(
480 &state.descriptor,
481 &pixels,
482 colours,
483 dither,
484 self.level,
485 self.icc.as_deref(),
486 self.exif.as_deref(),
487 )?;
488 sink.write_all(&file)?;
489 return sink.flush();
490 }
491
492 let compressed =
493 zlib_compress(&state.filtered, self.level).map_err(crate::compress_error)?;
494 let mut chunk = Vec::with_capacity(compressed.len() + 12);
495 write_chunk(&mut chunk, b"IDAT", &compressed);
496 write_chunk(&mut chunk, b"IEND", &[]);
497 sink.write_all(&chunk)?;
498 sink.flush()?;
499
500 state.filtered = Vec::new();
503 state.previous = Vec::new();
504 state.swapped = Vec::new();
505 Ok(())
506 }
507}
508
509#[cfg(test)]
510#[allow(
511 clippy::unwrap_used,
512 clippy::expect_used,
513 clippy::indexing_slicing,
514 clippy::panic,
515 reason = "tests operate on known-good values and assert shapes directly"
516)]
517mod tests {
518 use super::*;
519 use crate::decoder::PngDecoder;
520 use otf_pixels_core::{Decoder, Limits};
521
522 fn encode(desc: &ImageDescriptor, raster: &[u8], level: Level) -> Result<Vec<u8>> {
524 let mut encoder = PngEncoder::with_level(level);
525 let mut out: Vec<u8> = Vec::new();
526 encoder.write_header(desc, &mut out)?;
527 for row in raster.chunks_exact(desc.row_bytes()) {
528 encoder.write_row(row, &mut out)?;
529 }
530 encoder.finish(&mut out)?;
531 Ok(out)
532 }
533
534 fn decode(bytes: &[u8]) -> Result<(ImageDescriptor, Vec<u8>)> {
536 let mut decoder = PngDecoder::new(bytes, Limits::default())?;
537 let mut row = vec![0_u8; decoder.descriptor().row_bytes()];
538 let mut raster = Vec::new();
539 for _ in 0..decoder.descriptor().height {
540 decoder.read_row(&mut row)?;
541 raster.extend_from_slice(&row);
542 }
543 Ok((decoder.descriptor(), raster))
544 }
545
546 fn sample(desc: &ImageDescriptor) -> Vec<u8> {
549 let mut raster = vec![0_u8; desc.byte_len().unwrap()];
550 for (index, byte) in raster.iter_mut().enumerate() {
551 let value = if index % 97 < 40 {
552 (index % 251) as u8
553 } else {
554 ((index * 7) % 13) as u8
555 };
556 *byte = value;
557 }
558 raster
559 }
560
561 #[test]
562 fn every_supported_format_round_trips() {
563 for format in [
564 PixelFormat::Gray8,
565 PixelFormat::Gray16,
566 PixelFormat::GrayA8,
567 PixelFormat::Rgb8,
568 PixelFormat::Rgb16,
569 PixelFormat::Rgba8,
570 PixelFormat::Rgba16,
571 ] {
572 let desc = ImageDescriptor::new(23, 17, format).unwrap();
573 let raster = sample(&desc);
574 let bytes = encode(&desc, &raster, Level::DEFAULT)
575 .unwrap_or_else(|e| panic!("encoding {format}: {e}"));
576 let (decoded_desc, decoded) =
577 decode(&bytes).unwrap_or_else(|e| panic!("decoding {format}: {e}"));
578 assert_eq!(decoded_desc.pixel, format, "{format} changed format");
579 assert_eq!(
580 (decoded_desc.width, decoded_desc.height),
581 (23, 17),
582 "{format}"
583 );
584 assert_eq!(decoded, raster, "{format} did not round-trip");
585 }
586 }
587
588 #[test]
589 fn every_level_round_trips_to_the_same_pixels() {
590 let desc = ImageDescriptor::new(19, 11, PixelFormat::Rgba8).unwrap();
591 let raster = sample(&desc);
592 for level in 0..=9 {
593 let level = Level::new(level).unwrap();
594 let bytes = encode(&desc, &raster, level).unwrap();
595 let (_, decoded) = decode(&bytes).unwrap();
596 assert_eq!(decoded, raster, "level {} did not round-trip", level.get());
597 }
598 }
599
600 #[test]
601 fn the_output_starts_with_the_signature_and_ihdr() {
602 let desc = ImageDescriptor::new(4, 4, PixelFormat::Rgb8).unwrap();
603 let bytes = encode(&desc, &sample(&desc), Level::FAST).unwrap();
604 assert_eq!(&bytes[..8], &SIGNATURE, "signature");
605 assert_eq!(&bytes[12..16], b"IHDR", "first chunk");
606 assert_eq!(
607 &bytes[bytes.len() - 8..bytes.len() - 4],
608 b"IEND",
609 "last chunk"
610 );
611 }
612
613 #[test]
614 fn sixteen_bit_samples_are_written_big_endian() {
615 let mut out = Vec::new();
620 to_big_endian_16(&0x1234_u16.to_ne_bytes(), &mut out);
621 assert_eq!(out, vec![0x12, 0x34]);
622 }
623
624 #[test]
625 fn compression_shrinks_a_compressible_image() {
626 let desc = ImageDescriptor::new(64, 64, PixelFormat::Rgb8).unwrap();
629 let raster = vec![7_u8; desc.byte_len().unwrap()];
630 let stored = encode(&desc, &raster, Level::NONE).unwrap();
631 let packed = encode(&desc, &raster, Level::BEST).unwrap();
632 assert!(
633 packed.len() * 10 < stored.len(),
634 "level 9 produced {} bytes against level 0's {}",
635 packed.len(),
636 stored.len()
637 );
638 assert_eq!(decode(&packed).unwrap().1, raster);
639 }
640
641 #[test]
642 fn a_float_format_is_unsupported_not_a_panic() {
643 let desc = ImageDescriptor::new(2, 2, PixelFormat::RgbaF32).unwrap();
644 let mut encoder = PngEncoder::new();
645 let mut out = Vec::new();
646 let error = encoder.write_header(&desc, &mut out).unwrap_err();
647 assert!(matches!(error, PixelsError::Unsupported { .. }), "{error}");
648 assert!(
649 out.is_empty(),
650 "nothing should be written for a rejected format"
651 );
652 }
653
654 #[test]
655 fn a_short_image_is_an_error_not_a_truncated_png() {
656 let desc = ImageDescriptor::new(4, 4, PixelFormat::Gray8).unwrap();
657 let mut encoder = PngEncoder::new();
658 let mut out = Vec::new();
659 encoder.write_header(&desc, &mut out).unwrap();
660 encoder.write_row(&[0; 4], &mut out).unwrap();
661 let error = encoder.finish(&mut out).unwrap_err();
662 assert!(matches!(error, PixelsError::Malformed { .. }), "{error}");
663 }
664
665 #[test]
666 fn extra_rows_and_wrong_row_lengths_are_errors() {
667 let desc = ImageDescriptor::new(4, 1, PixelFormat::Gray8).unwrap();
668 let mut encoder = PngEncoder::new();
669 let mut out = Vec::new();
670 encoder.write_header(&desc, &mut out).unwrap();
671 assert!(encoder.write_row(&[0; 3], &mut out).is_err(), "short row");
672 assert!(encoder.write_row(&[0; 5], &mut out).is_err(), "long row");
673 encoder.write_row(&[0; 4], &mut out).unwrap();
674 assert!(encoder.write_row(&[0; 4], &mut out).is_err(), "extra row");
675 }
676
677 #[test]
678 fn rows_before_the_header_are_an_error() {
679 let mut encoder = PngEncoder::new();
680 let mut out = Vec::new();
681 assert!(encoder.write_row(&[0; 4], &mut out).is_err());
682 assert!(encoder.finish(&mut out).is_err());
683 }
684
685 #[test]
686 fn a_second_header_is_an_error() {
687 let desc = ImageDescriptor::new(2, 2, PixelFormat::Gray8).unwrap();
688 let mut encoder = PngEncoder::new();
689 let mut out = Vec::new();
690 encoder.write_header(&desc, &mut out).unwrap();
691 assert!(encoder.write_header(&desc, &mut out).is_err());
692 }
693
694 #[test]
695 fn zero_dimensions_never_reach_the_encoder() {
696 assert!(ImageDescriptor::new(0, 4, PixelFormat::Gray8).is_err());
701 assert!(ImageDescriptor::new(4, 0, PixelFormat::Gray8).is_err());
702 }
703
704 #[test]
705 fn options_map_quality_onto_compression_effort() {
706 assert_eq!(
707 PngEncoder::from_options(&EncodeOptions::default())
708 .level
709 .get(),
710 7
711 );
712 let lowest = EncodeOptions::with_quality(1).unwrap();
713 assert_eq!(PngEncoder::from_options(&lowest).level.get(), 1);
714 let highest = EncodeOptions::with_quality(100).unwrap();
715 assert_eq!(PngEncoder::from_options(&highest).level.get(), 9);
716 }
717
718 #[test]
719 fn a_single_pixel_image_round_trips() {
720 let desc = ImageDescriptor::new(1, 1, PixelFormat::Rgba8).unwrap();
722 let raster = vec![1, 2, 3, 4];
723 let bytes = encode(&desc, &raster, Level::BEST).unwrap();
724 assert_eq!(decode(&bytes).unwrap().1, raster);
725 }
726
727 fn encode_with(desc: &ImageDescriptor, raster: &[u8], mut encoder: PngEncoder) -> Vec<u8> {
729 let mut out: Vec<u8> = Vec::new();
730 encoder.write_header(desc, &mut out).unwrap();
731 for row in raster.chunks_exact(desc.row_bytes()) {
732 encoder.write_row(row, &mut out).unwrap();
733 }
734 encoder.finish(&mut out).unwrap();
735 out
736 }
737
738 fn palette(colours: usize, dither: f32) -> PngEncoder {
739 PngEncoder::new().with_palette(colours, dither).unwrap()
740 }
741
742 fn header(bytes: &[u8]) -> (u8, u8) {
744 (bytes[24], bytes[25])
745 }
746
747 fn has_chunk(bytes: &[u8], kind: &[u8; 4]) -> bool {
748 bytes.windows(4).any(|w| w == kind)
749 }
750
751 #[test]
752 fn a_palette_image_with_few_colours_round_trips_exactly() {
753 let desc = ImageDescriptor::new(13, 7, PixelFormat::Rgba8).unwrap();
754 let colours = [
755 [255, 0, 0, 255],
756 [0, 255, 0, 128],
757 [0, 0, 255, 0],
758 [9, 9, 9, 255],
759 ];
760 let raster: Vec<u8> = (0..13 * 7).flat_map(|i| colours[i % 4]).collect();
761 let bytes = encode_with(&desc, &raster, palette(256, 1.0));
762 assert_eq!(header(&bytes), (2, 3), "four colours fit two bits");
763 assert!(has_chunk(&bytes, b"PLTE") && has_chunk(&bytes, b"tRNS"));
764 let (out, decoded) = decode(&bytes).unwrap();
765 assert_eq!(out.pixel, PixelFormat::Rgba8);
766 let visible: Vec<u8> = raster
768 .chunks_exact(4)
769 .flat_map(|p| {
770 if p[3] == 0 {
771 [0; 4]
772 } else {
773 [p[0], p[1], p[2], p[3]]
774 }
775 })
776 .collect();
777 assert_eq!(decoded, visible);
778 }
779
780 #[test]
781 fn the_bit_depth_is_the_smallest_that_holds_the_palette() {
782 for (count, depth) in [
783 (2_usize, 1_u8),
784 (3, 2),
785 (4, 2),
786 (5, 4),
787 (16, 4),
788 (17, 8),
789 (200, 8),
790 ] {
791 let desc = ImageDescriptor::new(11, 3, PixelFormat::Rgb8).unwrap();
793 let raster: Vec<u8> = (0..33)
794 .flat_map(|i| {
795 let v = (i % count) as u8;
796 [v, v.wrapping_mul(3), 255 - v]
797 })
798 .collect();
799 let bytes = encode_with(&desc, &raster, palette(256, 1.0));
800 assert_eq!(header(&bytes), (depth, 3), "{count} colours");
801 assert!(!has_chunk(&bytes, b"tRNS"), "an opaque image needs no tRNS");
802 let (out, decoded) = decode(&bytes).unwrap();
803 assert_eq!(out.pixel, PixelFormat::Rgb8);
804 assert_eq!(decoded, raster, "{count} colours");
805 }
806 }
807
808 #[test]
809 fn a_photograph_is_reduced_to_the_palette_size() {
810 let desc = ImageDescriptor::new(64, 64, PixelFormat::Rgb8).unwrap();
811 let raster: Vec<u8> = (0..64 * 64 * 3)
812 .map(|i| ((i * 37 + i / 191) % 256) as u8)
813 .collect();
814 for dither in [0.0, 0.5, 1.0] {
815 let bytes = encode_with(&desc, &raster, palette(16, dither));
816 let plte = bytes.windows(4).position(|w| w == b"PLTE").unwrap();
817 let length = u32::from_be_bytes(bytes[plte - 4..plte].try_into().unwrap());
818 assert!(length <= 16 * 3, "{length} bytes of palette");
819 let (_, decoded) = decode(&bytes).unwrap();
820 let mut distinct: Vec<&[u8]> = decoded.chunks_exact(3).collect();
821 distinct.sort_unstable();
822 distinct.dedup();
823 assert!(distinct.len() <= 16);
824 }
825 let mut seed = 12_345_u32;
828 let noisy: Vec<u8> = (0..64 * 64 * 3)
829 .map(|_| {
830 seed = seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
831 (seed >> 16) as u8
832 })
833 .collect();
834 let true_colour = encode(&desc, &noisy, Level::DEFAULT).unwrap();
835 let reduced = encode_with(&desc, &noisy, palette(16, 1.0));
836 assert!(
837 reduced.len() * 2 < true_colour.len(),
838 "{} against {}",
839 reduced.len(),
840 true_colour.len()
841 );
842 }
843
844 #[test]
845 fn grey_and_wide_images_take_a_palette_too() {
846 let desc = ImageDescriptor::new(4, 1, PixelFormat::Gray16).unwrap();
847 let raster: Vec<u8> = [0_u16, 65535, 0, 65535]
848 .iter()
849 .flat_map(|v| v.to_ne_bytes())
850 .collect();
851 let bytes = encode_with(&desc, &raster, palette(256, 1.0));
852 assert_eq!(header(&bytes), (1, 3));
853 assert_eq!(
854 decode(&bytes).unwrap().1,
855 [0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255]
856 );
857 }
858
859 #[test]
860 fn options_choose_the_level_and_the_palette() {
861 let desc = ImageDescriptor::new(32, 32, PixelFormat::Rgb8).unwrap();
862 let raster: Vec<u8> = (0..32 * 32 * 3).map(|i| (i / 7 % 256) as u8).collect();
863 let with = |options: EncodeOptions| {
864 encode_with(&desc, &raster, PngEncoder::from_options(&options))
865 };
866 let stored = with(EncodeOptions::default().with_compression_level(0).unwrap());
867 let best = with(EncodeOptions::default().with_compression_level(9).unwrap());
868 assert!(best.len() < stored.len());
869 assert_eq!(
871 with(EncodeOptions::default()),
872 encode(&desc, &raster, Level::new(7).unwrap()).unwrap()
873 );
874 for options in [
875 EncodeOptions::default().with_palette(true),
876 EncodeOptions::default().with_colours(64).unwrap(),
877 EncodeOptions::default().with_dither(0.0).unwrap(),
878 ] {
879 assert_eq!(header(&with(options)).1, 3, "{options:?}");
880 }
881 assert_eq!(header(&with(EncodeOptions::default())).1, 2);
882 }
883
884 #[test]
885 fn palette_parameters_are_validated() {
886 assert!(PngEncoder::new().with_palette(1, 1.0).is_err());
887 assert!(PngEncoder::new().with_palette(257, 1.0).is_err());
888 assert!(PngEncoder::new().with_palette(2, 1.5).is_err());
889 }
890}