diff --git a/src/codecs/tga/encoder.rs b/src/codecs/tga/encoder.rs index 7cc1c6fa54..f1f3e1325e 100644 --- a/src/codecs/tga/encoder.rs +++ b/src/codecs/tga/encoder.rs @@ -1,5 +1,8 @@ use super::header::Header; -use crate::{error::EncodingError, ColorType, ImageEncoder, ImageError, ImageFormat, ImageResult}; +use crate::{ + codecs::tga::header::ImageType, error::EncodingError, ColorType, ImageEncoder, ImageError, + ImageFormat, ImageResult, +}; use std::{convert::TryFrom, error, fmt, io::Write}; /// Errors that can occur during encoding and saving of a TGA image. @@ -34,12 +37,114 @@ impl error::Error for EncoderError {} /// TGA encoder. pub struct TgaEncoder { writer: W, + + /// Run-length encoding + use_rle: bool, +} + +const MAX_RUN_LENGTH: u8 = 128; + +#[derive(Debug, Eq, PartialEq)] +enum PacketType { + Raw, + Rle, } impl TgaEncoder { /// Create a new encoder that writes its output to ```w```. pub fn new(w: W) -> TgaEncoder { - TgaEncoder { writer: w } + TgaEncoder { + writer: w, + use_rle: true, + } + } + + /// Disables run-length encoding + pub fn disable_rle(mut self) -> TgaEncoder { + self.use_rle = false; + self + } + + /// Writes a raw packet to the writer + fn write_raw_packet(&mut self, pixels: &[u8], counter: u8) -> ImageResult<()> { + // Set high bit = 0 and store counter - 1 (because 0 would be useless) + // The counter fills 7 bits max, so the high bit is set to 0 implicitly + let header = counter - 1; + self.writer.write_all(&[header])?; + self.writer.write_all(pixels)?; + Ok(()) + } + + /// Writes a run-length encoded packet to the writer + fn write_rle_encoded_packet(&mut self, pixel: &[u8], counter: u8) -> ImageResult<()> { + // Set high bit = 1 and store counter - 1 (because 0 would be useless) + let header = 0x80 | (counter - 1); + self.writer.write_all(&[header])?; + self.writer.write_all(pixel)?; + Ok(()) + } + + /// Writes the run-length encoded buffer to the writer + fn run_length_encode(&mut self, image: &[u8], color_type: ColorType) -> ImageResult<()> { + use PacketType::*; + + let bytes_per_pixel = color_type.bytes_per_pixel(); + let capacity_in_bytes = usize::from(MAX_RUN_LENGTH) * usize::from(bytes_per_pixel); + + // Buffer to temporarily store pixels + // so we can choose whether to use RLE or not when we need to + let mut buf = Vec::with_capacity(capacity_in_bytes); + + let mut counter = 0; + let mut prev_pixel = None; + let mut packet_type = Rle; + + for pixel in image.chunks(usize::from(bytes_per_pixel)) { + // Make sure we are not at the first pixel + if let Some(prev) = prev_pixel { + if pixel == prev { + if packet_type == Raw && counter > 0 { + self.write_raw_packet(&buf, counter)?; + counter = 0; + buf.clear(); + } + + packet_type = Rle; + } else if packet_type == Rle && counter > 0 { + self.write_rle_encoded_packet(prev, counter)?; + counter = 0; + packet_type = Raw; + buf.clear(); + } + } + + counter += 1; + buf.extend_from_slice(pixel); + + debug_assert!(buf.len() <= capacity_in_bytes); + + if counter == MAX_RUN_LENGTH { + match packet_type { + Rle => self.write_rle_encoded_packet(prev_pixel.unwrap(), counter), + Raw => self.write_raw_packet(&buf, counter), + }?; + + counter = 0; + packet_type = Rle; + buf.clear(); + } + + prev_pixel = Some(pixel); + } + + if counter > 0 { + match packet_type { + Rle => self.write_rle_encoded_packet(prev_pixel.unwrap(), counter), + Raw => self.write_raw_packet(&buf, counter), + }?; + } + + Ok(()) } /// Encodes the image ```buf``` that has dimensions ```width``` @@ -71,22 +176,48 @@ impl TgaEncoder { .map_err(|_| ImageError::from(EncoderError::HeightInvalid(height)))?; // Write out TGA header. - let header = Header::from_pixel_info(color_type, width, height)?; + let header = Header::from_pixel_info(color_type, width, height, self.use_rle)?; header.write_to(&mut self.writer)?; - // Write out Bgr(a)8 or L(a)8 image data. - match color_type { - ColorType::Rgb8 | ColorType::Rgba8 => { - let mut image = Vec::from(buf); + let image_type = ImageType::new(header.image_type); - for chunk in image.chunks_mut(usize::from(color_type.bytes_per_pixel())) { - chunk.swap(0, 2); - } + match image_type { + //TODO: support RunColorMap, and change match to image_type.is_encoded() + ImageType::RunTrueColor | ImageType::RunGrayScale => { + // Write run-length encoded image data - self.writer.write_all(&image)?; + match color_type { + ColorType::Rgb8 | ColorType::Rgba8 => { + let mut image = Vec::from(buf); + + for pixel in image.chunks_mut(usize::from(color_type.bytes_per_pixel())) { + pixel.swap(0, 2); + } + + self.run_length_encode(&image, color_type)?; + } + _ => { + self.run_length_encode(buf, color_type)?; + } + } } _ => { - self.writer.write_all(buf)?; + // Write uncompressed image data + + match color_type { + ColorType::Rgb8 | ColorType::Rgba8 => { + let mut image = Vec::from(buf); + + for pixel in image.chunks_mut(usize::from(color_type.bytes_per_pixel())) { + pixel.swap(0, 2); + } + + self.writer.write_all(&image)?; + } + _ => { + self.writer.write_all(buf)?; + } + } } } @@ -112,22 +243,6 @@ mod tests { use crate::{codecs::tga::TgaDecoder, ColorType, ImageDecoder, ImageError}; use std::{error::Error, io::Cursor}; - fn round_trip_image(image: &[u8], width: u32, height: u32, c: ColorType) -> Vec { - let mut encoded_data = Vec::new(); - { - let encoder = TgaEncoder::new(&mut encoded_data); - encoder - .encode(image, width, height, c) - .expect("could not encode image"); - } - - let decoder = TgaDecoder::new(Cursor::new(&encoded_data)).expect("failed to decode"); - - let mut buf = vec![0; decoder.total_bytes() as usize]; - decoder.read_image(&mut buf).expect("failed to decode"); - buf - } - #[test] fn test_image_width_too_large() { // TGA cannot encode images larger than 65,535×65,535 @@ -135,10 +250,12 @@ mod tests { let size = usize::from(u16::MAX) + 1; let dimension = size as u32; let img = vec![0u8; size]; + // Try to encode an image that is too large let mut encoded = Vec::new(); let encoder = TgaEncoder::new(&mut encoded); let result = encoder.encode(&img, dimension, 1, ColorType::L8); + match result { Err(ImageError::Encoding(err)) => { let err = err @@ -163,10 +280,12 @@ mod tests { let size = usize::from(u16::MAX) + 1; let dimension = size as u32; let img = vec![0u8; size]; + // Try to encode an image that is too large let mut encoded = Vec::new(); let encoder = TgaEncoder::new(&mut encoded); let result = encoder.encode(&img, 1, dimension, ColorType::L8); + match result { Err(ImageError::Encoding(err)) => { let err = err @@ -185,40 +304,195 @@ mod tests { } #[test] - fn round_trip_single_pixel_rgb() { - let image = [0, 1, 2]; - let decoded = round_trip_image(&image, 1, 1, ColorType::Rgb8); - assert_eq!(decoded.len(), image.len()); - assert_eq!(decoded.as_slice(), image); - } + fn test_compression_diff() { + let image = [0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2]; - #[test] - fn round_trip_single_pixel_rgba() { - let image = [0, 1, 2, 3]; - let decoded = round_trip_image(&image, 1, 1, ColorType::Rgba8); - assert_eq!(decoded.len(), image.len()); - assert_eq!(decoded.as_slice(), image); - } + let uncompressed_bytes = { + let mut encoded_data = Vec::new(); + let encoder = TgaEncoder::new(&mut encoded_data).disable_rle(); + encoder + .encode(&image, 5, 1, ColorType::Rgb8) + .expect("could not encode image"); - #[test] - fn round_trip_gray() { - let image = [0, 1, 2]; - let decoded = round_trip_image(&image, 3, 1, ColorType::L8); - assert_eq!(decoded.len(), image.len()); - assert_eq!(decoded.as_slice(), image); + encoded_data + }; + + let compressed_bytes = { + let mut encoded_data = Vec::new(); + let encoder = TgaEncoder::new(&mut encoded_data); + encoder + .encode(&image, 5, 1, ColorType::Rgb8) + .expect("could not encode image"); + + encoded_data + }; + + assert!(uncompressed_bytes.len() > compressed_bytes.len()); } - #[test] - fn round_trip_graya() { - let image = [0, 1, 2, 3, 4, 5]; - let decoded = round_trip_image(&image, 1, 3, ColorType::La8); - assert_eq!(decoded.len(), image.len()); - assert_eq!(decoded.as_slice(), image); + mod compressed { + use super::*; + + fn round_trip_image(image: &[u8], width: u32, height: u32, c: ColorType) -> Vec { + let mut encoded_data = Vec::new(); + { + let encoder = TgaEncoder::new(&mut encoded_data); + encoder + .encode(image, width, height, c) + .expect("could not encode image"); + } + let decoder = TgaDecoder::new(Cursor::new(&encoded_data)).expect("failed to decode"); + + let mut buf = vec![0; decoder.total_bytes() as usize]; + decoder.read_image(&mut buf).expect("failed to decode"); + buf + } + + #[test] + fn mixed_packets() { + let image = [ + 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, + ]; + let decoded = round_trip_image(&image, 5, 1, ColorType::Rgb8); + assert_eq!(decoded.len(), image.len()); + assert_eq!(decoded.as_slice(), image); + } + + #[test] + fn round_trip_gray() { + let image = [0, 1, 2]; + let decoded = round_trip_image(&image, 3, 1, ColorType::L8); + assert_eq!(decoded.len(), image.len()); + assert_eq!(decoded.as_slice(), image); + } + + #[test] + fn round_trip_graya() { + let image = [0, 1, 2, 3, 4, 5]; + let decoded = round_trip_image(&image, 1, 3, ColorType::La8); + assert_eq!(decoded.len(), image.len()); + assert_eq!(decoded.as_slice(), image); + } + + #[test] + fn round_trip_single_pixel_rgb() { + let image = [0, 1, 2]; + let decoded = round_trip_image(&image, 1, 1, ColorType::Rgb8); + assert_eq!(decoded.len(), image.len()); + assert_eq!(decoded.as_slice(), image); + } + + #[test] + fn round_trip_three_pixel_rgb() { + let image = [0, 1, 2, 0, 1, 2, 0, 1, 2]; + let decoded = round_trip_image(&image, 3, 1, ColorType::Rgb8); + assert_eq!(decoded.len(), image.len()); + assert_eq!(decoded.as_slice(), image); + } + + #[test] + fn round_trip_3px_rgb() { + let image = [0; 3 * 3 * 3]; // 3x3 pixels, 3 bytes per pixel + let decoded = round_trip_image(&image, 3, 3, ColorType::Rgb8); + assert_eq!(decoded.len(), image.len()); + assert_eq!(decoded.as_slice(), image); + } + + #[test] + fn round_trip_different() { + let image = [0, 1, 2, 0, 1, 3, 0, 1, 4]; + let decoded = round_trip_image(&image, 3, 1, ColorType::Rgb8); + assert_eq!(decoded.len(), image.len()); + assert_eq!(decoded.as_slice(), image); + } + + #[test] + fn round_trip_different_2() { + let image = [0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 4]; + let decoded = round_trip_image(&image, 4, 1, ColorType::Rgb8); + assert_eq!(decoded.len(), image.len()); + assert_eq!(decoded.as_slice(), image); + } + + #[test] + fn round_trip_different_3() { + let image = [0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 4, 0, 1, 2]; + let decoded = round_trip_image(&image, 5, 1, ColorType::Rgb8); + assert_eq!(decoded.len(), image.len()); + assert_eq!(decoded.as_slice(), image); + } + + #[test] + fn round_trip_bw() { + // This example demonstrates the run-length counter being saturated + // It should never overflow and can be 128 max + let image = crate::open("tests/images/tga/encoding/black_white.tga").unwrap(); + let (width, height) = (image.width(), image.height()); + let image = image.as_rgb8().unwrap().to_vec(); + + let decoded = round_trip_image(&image, width, height, ColorType::Rgb8); + assert_eq!(decoded.len(), image.len()); + assert_eq!(decoded.as_slice(), image); + } } - #[test] - fn round_trip_3px_rgb() { - let image = [0; 3 * 3 * 3]; // 3x3 pixels, 3 bytes per pixel - let _decoded = round_trip_image(&image, 3, 3, ColorType::Rgb8); + mod uncompressed { + use super::*; + + fn round_trip_image(image: &[u8], width: u32, height: u32, c: ColorType) -> Vec { + let mut encoded_data = Vec::new(); + { + let encoder = TgaEncoder::new(&mut encoded_data).disable_rle(); + encoder + .encode(image, width, height, c) + .expect("could not encode image"); + } + + let decoder = TgaDecoder::new(Cursor::new(&encoded_data)).expect("failed to decode"); + + let mut buf = vec![0; decoder.total_bytes() as usize]; + decoder.read_image(&mut buf).expect("failed to decode"); + buf + } + + #[test] + fn round_trip_single_pixel_rgb() { + let image = [0, 1, 2]; + let decoded = round_trip_image(&image, 1, 1, ColorType::Rgb8); + assert_eq!(decoded.len(), image.len()); + assert_eq!(decoded.as_slice(), image); + } + + #[test] + fn round_trip_single_pixel_rgba() { + let image = [0, 1, 2, 3]; + let decoded = round_trip_image(&image, 1, 1, ColorType::Rgba8); + assert_eq!(decoded.len(), image.len()); + assert_eq!(decoded.as_slice(), image); + } + + #[test] + fn round_trip_gray() { + let image = [0, 1, 2]; + let decoded = round_trip_image(&image, 3, 1, ColorType::L8); + assert_eq!(decoded.len(), image.len()); + assert_eq!(decoded.as_slice(), image); + } + + #[test] + fn round_trip_graya() { + let image = [0, 1, 2, 3, 4, 5]; + let decoded = round_trip_image(&image, 1, 3, ColorType::La8); + assert_eq!(decoded.len(), image.len()); + assert_eq!(decoded.as_slice(), image); + } + + #[test] + fn round_trip_3px_rgb() { + let image = [0; 3 * 3 * 3]; // 3x3 pixels, 3 bytes per pixel + let decoded = round_trip_image(&image, 3, 3, ColorType::Rgb8); + assert_eq!(decoded.len(), image.len()); + assert_eq!(decoded.as_slice(), image); + } } } diff --git a/src/codecs/tga/header.rs b/src/codecs/tga/header.rs index 83ba7a34d3..c73770f24e 100644 --- a/src/codecs/tga/header.rs +++ b/src/codecs/tga/header.rs @@ -83,15 +83,20 @@ impl Header { color_type: ColorType, width: u16, height: u16, + use_rle: bool, ) -> ImageResult { let mut header = Self::default(); if width > 0 && height > 0 { - let (num_alpha_bits, other_channel_bits, image_type) = match color_type { - ColorType::Rgba8 => (8, 24, ImageType::RawTrueColor), - ColorType::Rgb8 => (0, 24, ImageType::RawTrueColor), - ColorType::La8 => (8, 8, ImageType::RawGrayScale), - ColorType::L8 => (0, 8, ImageType::RawGrayScale), + let (num_alpha_bits, other_channel_bits, image_type) = match (color_type, use_rle) { + (ColorType::Rgba8, true) => (8, 24, ImageType::RunTrueColor), + (ColorType::Rgb8, true) => (0, 24, ImageType::RunTrueColor), + (ColorType::La8, true) => (8, 8, ImageType::RunGrayScale), + (ColorType::L8, true) => (0, 8, ImageType::RunGrayScale), + (ColorType::Rgba8, false) => (8, 24, ImageType::RawTrueColor), + (ColorType::Rgb8, false) => (0, 24, ImageType::RawTrueColor), + (ColorType::La8, false) => (8, 8, ImageType::RawGrayScale), + (ColorType::L8, false) => (0, 8, ImageType::RawGrayScale), _ => { return Err(ImageError::Unsupported( UnsupportedError::from_format_and_kind( diff --git a/tests/images/tga/encoding/black_white.tga b/tests/images/tga/encoding/black_white.tga new file mode 100644 index 0000000000..4428df335b Binary files /dev/null and b/tests/images/tga/encoding/black_white.tga differ