diff --git a/src/binio.rs b/src/binio.rs index 459f1cb..8038506 100644 --- a/src/binio.rs +++ b/src/binio.rs @@ -42,11 +42,7 @@ impl<'a, O: Write + WriteBytesExt> BitWriter<'a, O> { /// Check if the stream is aligned to a byte. pub fn aligned(&self) -> bool { - if self.bit_offset() == 0 { - true - } else { - false - } + self.bit_offset() == 0 } /// Align the writer to the nearest byte by padding with zero bits. diff --git a/src/compression/dct.rs b/src/compression/dct.rs index 92af693..260e004 100644 --- a/src/compression/dct.rs +++ b/src/compression/dct.rs @@ -81,7 +81,7 @@ pub fn idct(input: &[f32], width: usize, height: usize) -> Vec { sqrt_height }; - let idct = input[u * width + v] as f32 * + let idct = input[u * width + v] * f32::cos((2.0 * x as f32 + 1.0) * u as f32 * PI / (2.0 * width as f32)) * f32::cos((2.0 * y as f32 + 1.0) * v as f32 * PI / (2.0 * height as f32)); @@ -137,7 +137,7 @@ pub fn quantize(input: &[f32], quant_matrix: [u16; 64]) -> Vec { pub fn dequantize(input: &[i16], quant_matrix: [u16; 64]) -> Vec { input.iter() .zip(quant_matrix) - .map(|(v, q)| (*v as i16 * q as i16) as f32) + .map(|(v, q)| (*v * q as i16) as f32) .collect() } @@ -158,7 +158,12 @@ pub fn dct_compress(input: &[u8], parameters: DctParameters) -> Vec> { .collect(); // Create 2d array of the channel for ease of processing - let mut img_2d: Vec> = channel.windows(parameters.width).step_by(parameters.width).map(|r| r.to_vec()).collect(); + let mut img_2d: Vec> = + channel.windows(parameters.width) + .step_by(parameters.width) + .map(|r| r.to_vec()) + .collect(); + img_2d.iter_mut().for_each(|r| r.resize(new_width, 0)); img_2d.resize(new_height, vec![0u8; new_width]); @@ -170,7 +175,7 @@ pub fn dct_compress(input: &[u8], parameters: DctParameters) -> Vec> { let mut chunk = Vec::new(); for i in 0..8 { let row = &img_2d[(h * 8) + i][w * 8..(w * 8) + 8]; - chunk.extend_from_slice(&row); + chunk.extend_from_slice(row); } // Perform the DCT on the image section @@ -201,7 +206,7 @@ pub fn dct_decompress(input: &[i16], parameters: DctParameters) -> Vec { input.par_chunks(new_width * new_height).enumerate().for_each(|(chan_num, channel)| { let decoded_image = Arc::new(Mutex::new(vec![0u8; parameters.width * parameters.height])); channel.par_chunks(64).enumerate().for_each(|(i, chunk)| { - let dequantized_dct = dequantize(&chunk, quantization_matrix); + let dequantized_dct = dequantize(chunk, quantization_matrix); let original = idct(&dequantized_dct, 8, 8); // Write rows of blocks @@ -261,25 +266,13 @@ impl Default for DctParameters { fn default() -> Self { Self { quality: 80, - format: ColorFormat::Rgba32, + format: ColorFormat::Rgba8, width: 0, height: 0, } } } -/// The results of DCT compression -pub struct DctImage { - /// The DCT encoded version of each channel. - pub channels: Vec>, - - /// New width after padding. - pub width: u32, - - /// New height after padding. - pub height: u32, -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/compression/lossless.rs b/src/compression/lossless.rs index 80aef9d..8fe70be 100644 --- a/src/compression/lossless.rs +++ b/src/compression/lossless.rs @@ -111,7 +111,7 @@ pub fn compress(data: &[u8]) -> Result<(Vec, CompressionInfo), CompressionEr fn compress_lzw(data: &[u8], last: Vec) -> (usize, Vec, Vec) { let mut count = 0; - let mut dictionary: HashMap, u64> = HashMap::from_iter((0..=255).into_iter().map(|i| (vec![i], i as u64))); + let mut dictionary: HashMap, u64> = HashMap::from_iter((0..=255).map(|i| (vec![i], i as u64))); let mut dictionary_count = (dictionary.len() + 1) as u64; let mut element = Vec::new(); @@ -233,7 +233,7 @@ fn decompress_lzw(input_data: &[u8], size: usize) -> Result, Compression let data_size = input_data.len(); let mut bit_io = BitReader::new(&mut data); - let mut w = dictionary.get(0).unwrap().clone(); + let mut w = dictionary.first().unwrap().clone(); let mut element; loop { diff --git a/src/header.rs b/src/header.rs index 9bdfefc..3508a5f 100644 --- a/src/header.rs +++ b/src/header.rs @@ -19,8 +19,8 @@ pub struct Header { pub compression_type: CompressionType, /// Level of compression. Only applies in Lossy mode, otherwise this value - /// should be set to -1. - pub compression_level: i8, + /// should be set to 0, and ignored. + pub quality: u8, /// Format of color data in the image. pub color_format: ColorFormat, @@ -33,8 +33,8 @@ impl Default for Header { width: 0, height: 0, compression_type: CompressionType::Lossless, - compression_level: -1, - color_format: ColorFormat::Rgba32, + quality: 0, + color_format: ColorFormat::Rgba8, } } } @@ -49,7 +49,7 @@ impl Header { // Write compression info buf.write_u8(self.compression_type.into()).unwrap(); - buf.write_i8(self.compression_level).unwrap(); + buf.write_u8(self.quality).unwrap(); // Write color format buf.write_u8(self.color_format as u8).unwrap(); @@ -57,10 +57,13 @@ impl Header { buf.into_inner().try_into().unwrap() } + /// Length of the header in bytes. + #[allow(clippy::len_without_is_empty)] pub fn len(&self) -> usize { 19 } + /// Create a header from something implementing [`Read`]. pub fn read_from(input: &mut T) -> Result { let mut magic = [0u8; 8]; input.read_exact(&mut magic).unwrap(); @@ -75,7 +78,7 @@ impl Header { height: input.read_u32::()?, compression_type: input.read_u8()?.try_into().unwrap(), - compression_level: input.read_i8()?, + quality: input.read_u8()?, color_format: input.read_u8()?.try_into().unwrap(), }) } @@ -86,10 +89,10 @@ impl Header { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ColorFormat { /// RGBA, 8 bits per channel - Rgba32 = 0, + Rgba8 = 0, /// RGB, 8 bits per channel - Rgb24 = 1, + Rgb8 = 1, } impl ColorFormat { @@ -98,8 +101,8 @@ impl ColorFormat { /// Ex. Rgba32 has `8bpc` pub fn bpc(&self) -> u8 { match self { - ColorFormat::Rgba32 => 8, - ColorFormat::Rgb24 => 8, + ColorFormat::Rgba8 => 8, + ColorFormat::Rgb8 => 8, } } @@ -108,8 +111,8 @@ impl ColorFormat { /// Ex. Rgba32 has `32bpp` pub fn bpp(&self) -> u16 { match self { - ColorFormat::Rgba32 => 32, - ColorFormat::Rgb24 => 24, + ColorFormat::Rgba8 => 32, + ColorFormat::Rgb8 => 24, } } @@ -118,8 +121,8 @@ impl ColorFormat { /// Ex. Rgba32 has `4` channels pub fn channels(self) -> u16 { match self { - ColorFormat::Rgba32 => 4, - ColorFormat::Rgb24 => 3, + ColorFormat::Rgba8 => 4, + ColorFormat::Rgb8 => 3, } } } @@ -129,8 +132,8 @@ impl TryFrom for ColorFormat { fn try_from(value: u8) -> Result { Ok(match value { - 0 => Self::Rgba32, - 1 => Self::Rgb24, + 0 => Self::Rgba8, + 1 => Self::Rgb8, v => return Err(format!("invalid color format {v}")), }) } @@ -163,9 +166,9 @@ impl TryFrom for CompressionType { } } -impl Into for CompressionType { - fn into(self) -> u8 { - match self { +impl From for u8 { + fn from(val: CompressionType) -> Self { + match val { CompressionType::None => 0, CompressionType::Lossless => 1, CompressionType::LossyDct => 2, diff --git a/src/lib.rs b/src/lib.rs index a7a1429..59c3fca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,5 @@ //! SQP (SQuishy Picture Format) is an image format. It can be used to store -//! image data in lossless or lossy compressed form, while remaining relatively -//! simple. +//! image data in lossless or lossy compressed form. mod compression { pub mod dct; diff --git a/src/operations.rs b/src/operations.rs index 6072f61..602ecfb 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -77,7 +77,7 @@ pub fn diff_line(width: u32, height: u32, input: &[u8]) -> Vec { .copied() .collect(); - if y % block_height as u32 != 0 { + if y % block_height != 0 { for x in 0..width as usize * 3 { curr_line[x] = curr_line[x].wrapping_sub(prev_line[x]); prev_line[x] = prev_line[x].wrapping_add(curr_line[x]); diff --git a/src/picture.rs b/src/picture.rs index e86e914..6b4264b 100644 --- a/src/picture.rs +++ b/src/picture.rs @@ -31,6 +31,9 @@ pub enum Error { impl DangoPicture { /// Create a DPF from raw bytes in a particular [`ColorFormat`]. /// + /// The quality parameter does nothing if the compression type is not + /// lossy, so it should be set to None. + /// /// ## Example /// ``` /// let dpf_lossy = DangoPicture::from_raw( @@ -47,18 +50,12 @@ impl DangoPicture { height: u32, color_format: ColorFormat, compression_type: CompressionType, - compression_level: Option, + quality: Option, bitmap: Vec, ) -> Self { - let compression_level = match compression_level { - Some(level) => { - if level < 1 || level > 100 { - panic!("Compression level out of range 1..100") - } - level as i8 - }, - None => -1, - }; + if quality.is_none() && compression_type == CompressionType::LossyDct { + panic!("compression level must not be `None` when compression type is lossy") + } let header = Header { magic: *b"dangoimg", @@ -67,7 +64,10 @@ impl DangoPicture { height, compression_type, - compression_level, + quality: match quality { + Some(level) => level.clamp(1, 100), + None => 0, + }, color_format, }; @@ -78,6 +78,42 @@ impl DangoPicture { } } + /// Convenience method over [`DangoPicture::from_raw`] which creates a + /// lossy image with a given quality. + pub fn from_raw_lossy( + width: u32, + height: u32, + color_format: ColorFormat, + quality: u8, + bitmap: Vec, + ) -> Self { + Self::from_raw( + width, + height, + color_format, + CompressionType::LossyDct, + Some(quality), + bitmap, + ) + } + + + pub fn from_raw_lossless( + width: u32, + height: u32, + color_format: ColorFormat, + bitmap: Vec, + ) -> Self { + Self::from_raw( + width, + height, + color_format, + CompressionType::Lossless, + None, + bitmap, + ) + } + /// Encode the image into anything that implements [Write]. Returns the /// number of bytes written. pub fn encode(&self, mut output: O) -> Result { @@ -97,7 +133,7 @@ impl DangoPicture { &dct_compress( &self.bitmap, DctParameters { - quality: self.header.compression_level as u32, + quality: self.header.quality as u32, format: self.header.color_format, width: self.header.width as usize, height: self.header.height as usize, @@ -111,7 +147,7 @@ impl DangoPicture { }; // Compress the final image data using the basic LZW scheme - let (compressed_data, compression_info) = compress(&modified_data)?; + let (compressed_data, compression_info) = compress(modified_data)?; // Write out compression info count += compression_info.write_into(&mut output).unwrap(); @@ -146,25 +182,10 @@ impl DangoPicture { line_diff(header.width, header.height, &pre_bitmap) }, CompressionType::LossyDct => { - let mut decoded = Vec::new(); - let mut offset = 0; - loop { - if offset > pre_bitmap.len() { - break; - } - - if let Some(num) = i16::decode_var(&pre_bitmap[offset..]) { - offset += num.1; - decoded.push(num.0 as i16); - } else { - break; - } - } - dct_decompress( - &decoded, + &decode_varint_stream(&pre_bitmap), DctParameters { - quality: header.compression_level as u32, + quality: header.quality as u32, format: header.color_format, width: header.width as usize, height: header.height as usize, @@ -176,3 +197,15 @@ impl DangoPicture { Ok(DangoPicture { header, bitmap }) } } + +fn decode_varint_stream(stream: &[u8]) -> Vec { + let mut output = Vec::new(); + let mut offset = 0; + + while let Some(num) = i16::decode_var(&stream[offset..]) { + offset += num.1; + output.push(num.0); + } + + output +}