mirror of
https://github.com/Dangoware/sqp.git
synced 2025-04-19 07:12:55 -05:00
Added proper encoding and decoding, fixed minor issues
This commit is contained in:
parent
4f2f101c03
commit
e1a52c7077
5 changed files with 155 additions and 93 deletions
|
@ -6,5 +6,6 @@ edition = "2021"
|
|||
[dependencies]
|
||||
byteorder = "1.5.0"
|
||||
image = "0.25.2"
|
||||
integer-encoding = "4.0.0"
|
||||
rayon = "1.10.0"
|
||||
thiserror = "1.0.63"
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
use std::{f32::consts::{PI, SQRT_2}, sync::{Arc, Mutex}};
|
||||
|
||||
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator};
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::header::ColorFormat;
|
||||
|
||||
/// Perform a Discrete Cosine Transform on the input matrix.
|
||||
pub fn dct(input: &[u8], width: usize, height: usize) -> Vec<f32> {
|
||||
if input.len() != width * height {
|
||||
panic!("Input matrix size must be width×height")
|
||||
panic!("Input matrix size must be width * height, got {}", input.len())
|
||||
}
|
||||
|
||||
let sqrt_width_zero = 1.0 / (width as f32).sqrt();
|
||||
|
@ -53,7 +53,7 @@ pub fn dct(input: &[u8], width: usize, height: usize) -> Vec<f32> {
|
|||
/// Perform an inverse Discrete Cosine Transform on the input matrix.
|
||||
pub fn idct(input: &[f32], width: usize, height: usize) -> Vec<u8> {
|
||||
if input.len() != width * height {
|
||||
panic!("Input matrix size must be width×height")
|
||||
panic!("Input matrix size must be width * height, got {}", input.len())
|
||||
}
|
||||
|
||||
let sqrt_width_zero = 1.0 / (width as f32).sqrt();
|
||||
|
@ -127,12 +127,18 @@ pub fn quantization_matrix(quality: u32) -> [u16; 64] {
|
|||
|
||||
/// Quantize an input matrix, returning the result.
|
||||
pub fn quantize(input: &[f32], quant_matrix: [u16; 64]) -> Vec<i16> {
|
||||
input.iter().zip(quant_matrix).map(|(v, q)| (v / q as f32).round() as i16).collect()
|
||||
input.iter()
|
||||
.zip(quant_matrix)
|
||||
.map(|(v, q)| (v / q as f32).round() as i16)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Dequantize an input matrix, returning an approximation of the original.
|
||||
pub fn dequantize(input: &[i16], quant_matrix: [u16; 64]) -> Vec<f32> {
|
||||
input.iter().zip(quant_matrix).map(|(v, q)| (*v as i16 * q as i16) as f32).collect()
|
||||
input.iter()
|
||||
.zip(quant_matrix)
|
||||
.map(|(v, q)| (*v as i16 * q as i16) as f32)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Take in an image encoded in some [`ColorFormat`] and perform DCT on it,
|
||||
|
@ -140,7 +146,7 @@ pub fn dequantize(input: &[i16], quant_matrix: [u16; 64]) -> Vec<f32> {
|
|||
/// to a multiple of 8, which must be reversed when decoding.
|
||||
pub fn dct_compress(input: &[u8], parameters: DctParameters) -> Vec<Vec<i16>> {
|
||||
let new_width = parameters.width + (8 - parameters.width % 8);
|
||||
let new_height = parameters.height + (8 - parameters.width % 8);
|
||||
let new_height = parameters.height + (8 - parameters.height % 8);
|
||||
let quantization_matrix = quantization_matrix(parameters.quality);
|
||||
|
||||
let mut dct_image = Vec::with_capacity(input.len());
|
||||
|
@ -150,7 +156,6 @@ pub fn dct_compress(input: &[u8], parameters: DctParameters) -> Vec<Vec<i16>> {
|
|||
.step_by(parameters.format.channels() as usize)
|
||||
.copied()
|
||||
.collect();
|
||||
println!("Encoding channel {ch}");
|
||||
|
||||
// Create 2d array of the channel for ease of processing
|
||||
let mut img_2d: Vec<Vec<u8>> = channel.windows(parameters.width).step_by(parameters.width).map(|r| r.to_vec()).collect();
|
||||
|
@ -185,20 +190,17 @@ pub fn dct_compress(input: &[u8], parameters: DctParameters) -> Vec<Vec<i16>> {
|
|||
|
||||
/// Take in an image encoded with DCT and quantized and perform IDCT on it,
|
||||
/// returning an approximation of the original data.
|
||||
pub fn dct_decompress(input: &[Vec<i16>], parameters: DctParameters) -> Vec<u8> {
|
||||
pub fn dct_decompress(input: &[i16], parameters: DctParameters) -> Vec<u8> {
|
||||
let new_width = parameters.width + (8 - parameters.width % 8);
|
||||
let new_height = parameters.height + (8 - parameters.width % 8);
|
||||
let new_height = parameters.height + (8 - parameters.height % 8);
|
||||
|
||||
// Precalculate the quantization matrix
|
||||
let quantization_matrix = quantization_matrix(parameters.quality);
|
||||
|
||||
let final_img = Arc::new(Mutex::new(vec![0u8; (new_width * new_height) * parameters.format.channels() as usize]));
|
||||
|
||||
input.par_iter().enumerate().for_each(|(chan_num, channel)| {
|
||||
println!("Decoding channel {chan_num}");
|
||||
|
||||
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.into_par_iter().copied().chunks(64).enumerate().for_each(|(i, chunk)| {
|
||||
channel.par_chunks(64).enumerate().for_each(|(i, chunk)| {
|
||||
let dequantized_dct = dequantize(&chunk, quantization_matrix);
|
||||
let original = idct(&dequantized_dct, 8, 8);
|
||||
|
||||
|
|
|
@ -40,16 +40,20 @@ impl Default for Header {
|
|||
}
|
||||
|
||||
impl Header {
|
||||
pub fn to_bytes(&self) -> [u8; 16] {
|
||||
pub fn to_bytes(&self) -> [u8; 19] {
|
||||
let mut buf = Cursor::new(Vec::new());
|
||||
|
||||
buf.write_all(&self.magic).unwrap();
|
||||
buf.write_u32::<LE>(self.width).unwrap();
|
||||
buf.write_u32::<LE>(self.height).unwrap();
|
||||
|
||||
buf.write_u8(self.compression_type as u8).unwrap();
|
||||
// Write compression info
|
||||
buf.write_u8(self.compression_type.into()).unwrap();
|
||||
buf.write_i8(self.compression_level).unwrap();
|
||||
|
||||
// Write color format
|
||||
buf.write_u8(self.color_format as u8).unwrap();
|
||||
|
||||
buf.into_inner().try_into().unwrap()
|
||||
}
|
||||
|
||||
|
@ -73,6 +77,8 @@ impl Header {
|
|||
}
|
||||
}
|
||||
|
||||
/// The format of bytes in the image.
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ColorFormat {
|
||||
/// RGBA, 8 bits per channel
|
||||
|
@ -127,6 +133,7 @@ impl TryFrom<u8> for ColorFormat {
|
|||
}
|
||||
|
||||
/// The type of compression used in the image
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CompressionType {
|
||||
/// No compression at all, raw bitmap
|
||||
|
@ -151,3 +158,13 @@ impl TryFrom<u8> for CompressionType {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<u8> for CompressionType {
|
||||
fn into(self) -> u8 {
|
||||
match self {
|
||||
CompressionType::None => 0,
|
||||
CompressionType::Lossless => 1,
|
||||
CompressionType::LossyDct => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
106
src/main.rs
106
src/main.rs
|
@ -7,80 +7,66 @@ mod header;
|
|||
mod operations;
|
||||
pub mod picture;
|
||||
|
||||
use std::{fs::File, io::Write, time::Instant};
|
||||
|
||||
use header::ColorFormat;
|
||||
use compression::{dct::{dct_compress, dct_decompress, DctParameters}, lossless};
|
||||
use std::{fs::File, io::{BufReader, BufWriter}, time::Instant};
|
||||
use header::{ColorFormat, CompressionType};
|
||||
use image::RgbaImage;
|
||||
use picture::DangoPicture;
|
||||
|
||||
fn main() {
|
||||
let input = image::open("transparent2.png").unwrap().to_rgba8();
|
||||
input.save("original.png").unwrap();
|
||||
|
||||
let timer = Instant::now();
|
||||
let dct_result = dct_compress(
|
||||
input.as_raw(),
|
||||
DctParameters {
|
||||
quality: 68,
|
||||
format: ColorFormat::Rgba32,
|
||||
width: input.width() as usize,
|
||||
height: input.height() as usize,
|
||||
}
|
||||
);
|
||||
|
||||
let compressed_dct = lossless::compress(&dct_result.concat().iter().flat_map(|x| x.to_le_bytes()).collect::<Vec<u8>>()).unwrap();
|
||||
println!("Encoding took {}ms", timer.elapsed().as_millis());
|
||||
|
||||
let mut dct_output = File::create("test-dct.dpf").unwrap();
|
||||
dct_output.write_all(&compressed_dct.0).unwrap();
|
||||
|
||||
let timer = Instant::now();
|
||||
let decoded_dct = dct_decompress(
|
||||
&dct_result,
|
||||
DctParameters {
|
||||
quality: 68,
|
||||
format: ColorFormat::Rgba32,
|
||||
width: input.width() as usize,
|
||||
height: input.height() as usize
|
||||
}
|
||||
);
|
||||
println!("Decoding took {}ms", timer.elapsed().as_millis());
|
||||
|
||||
image::RgbaImage::from_raw(
|
||||
let dpf_lossy = DangoPicture::from_raw(
|
||||
input.width(),
|
||||
input.height(),
|
||||
decoded_dct
|
||||
).unwrap().save("dct-final.png").unwrap();
|
||||
ColorFormat::Rgba32,
|
||||
CompressionType::LossyDct,
|
||||
Some(10),
|
||||
input.as_raw().clone()
|
||||
);
|
||||
|
||||
/*
|
||||
// Reverse the DCT
|
||||
let idct: Vec<u8> = idct(&dct, 8, 8).iter().map(|c| *c as u8).collect();
|
||||
let dpf_lossless = DangoPicture::from_raw(
|
||||
input.width(),
|
||||
input.height(),
|
||||
ColorFormat::Rgba32,
|
||||
CompressionType::Lossless,
|
||||
None,
|
||||
input.as_raw().clone()
|
||||
);
|
||||
|
||||
let img = GrayImage::from_raw(input.width(), input.height(), idct).unwrap();
|
||||
img.save("test.png").unwrap();
|
||||
*/
|
||||
|
||||
/*
|
||||
let image_data = image::open("bw.jpg").unwrap().to_rgba8();
|
||||
let encoded_dpf = DangoPicture::from_raw(image_data.width(), image_data.height(), &image_data);
|
||||
|
||||
println!("ENCODING ---");
|
||||
println!("\n--- LOSSY ---");
|
||||
println!("Encoding");
|
||||
let timer = Instant::now();
|
||||
let mut outfile = BufWriter::new(File::create("test.dpf").unwrap());
|
||||
encoded_dpf.encode(&mut outfile);
|
||||
let mut outfile = BufWriter::new(std::fs::File::create("test-lossy.dpf").unwrap());
|
||||
dpf_lossy.encode(&mut outfile).unwrap();
|
||||
println!("Encoding took {}ms", timer.elapsed().as_millis());
|
||||
|
||||
println!("DECODING ---");
|
||||
let mut outbuf = Vec::new();
|
||||
dpf_lossy.encode(&mut outbuf).unwrap();
|
||||
println!("Size is {}Mb", (((outbuf.len() as f32 / 1_000_000.0) * 100.0) as u32 as f32) / 100.0);
|
||||
|
||||
println!("Decoding");
|
||||
let timer = Instant::now();
|
||||
let mut infile = BufReader::new(File::open("test.dpf").unwrap());
|
||||
let mut infile = BufReader::new(File::open("test-lossy.dpf").unwrap());
|
||||
let decoded_dpf = DangoPicture::decode(&mut infile).unwrap();
|
||||
RgbaImage::from_raw(decoded_dpf.header.width, decoded_dpf.header.height, decoded_dpf.bitmap.into()).unwrap().save("test-lossy.png").unwrap();
|
||||
println!("Decoding took {}ms", timer.elapsed().as_millis());
|
||||
|
||||
let out_image = RgbaImage::from_raw(
|
||||
decoded_dpf.header.width,
|
||||
decoded_dpf.header.height,
|
||||
decoded_dpf.bitmap.into(),
|
||||
)
|
||||
.unwrap();
|
||||
out_image.save("test.png").unwrap();
|
||||
*/
|
||||
println!("\n--- LOSSLESS ---");
|
||||
println!("Encoding");
|
||||
let timer = Instant::now();
|
||||
let mut outfile = BufWriter::new(std::fs::File::create("test-lossless.dpf").unwrap());
|
||||
dpf_lossless.encode(&mut outfile).unwrap();
|
||||
println!("Encoding took {}ms", timer.elapsed().as_millis());
|
||||
|
||||
let mut outbuf = Vec::new();
|
||||
dpf_lossless.encode(&mut outbuf).unwrap();
|
||||
println!("Size is {}Mb", (((outbuf.len() as f32 / 1_000_000.0) * 100.0) as u32 as f32) / 100.0);
|
||||
|
||||
println!("Decoding");
|
||||
let timer = Instant::now();
|
||||
let mut infile = BufReader::new(File::open("test-lossless.dpf").unwrap());
|
||||
let decoded_dpf = DangoPicture::decode(&mut infile).unwrap();
|
||||
RgbaImage::from_raw(decoded_dpf.header.width, decoded_dpf.header.height, decoded_dpf.bitmap.into()).unwrap().save("test-lossless.png").unwrap();
|
||||
println!("Decoding took {}ms", timer.elapsed().as_millis());
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
use std::io::{self, Read, Write};
|
||||
use std::{fs::File, io::{self, BufWriter, Read, Write}};
|
||||
|
||||
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||
use integer_encoding::VarInt;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
compression::{dct::{dct_compress, DctParameters}, lossless::{compress, decompress, CompressionError, CompressionInfo}},
|
||||
compression::{dct::{dct_compress, dct_decompress, DctParameters},
|
||||
lossless::{compress, decompress, CompressionError, CompressionInfo}},
|
||||
header::{ColorFormat, CompressionType, Header},
|
||||
operations::{diff_line, line_diff},
|
||||
};
|
||||
|
@ -16,7 +18,7 @@ pub struct DangoPicture {
|
|||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("incorrect identifier, got {0:?}")]
|
||||
#[error("incorrect identifier, got {0:02X?}")]
|
||||
InvalidIdentifier([u8; 8]),
|
||||
|
||||
#[error("io operation failed: {0}")]
|
||||
|
@ -27,21 +29,35 @@ pub enum Error {
|
|||
}
|
||||
|
||||
impl DangoPicture {
|
||||
/// Create a DPF
|
||||
pub fn from_raw(
|
||||
width: u32,
|
||||
height: u32,
|
||||
color_format: ColorFormat,
|
||||
compression_type: CompressionType,
|
||||
compression_level: Option<u8>,
|
||||
bitmap: Vec<u8>,
|
||||
) -> 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,
|
||||
};
|
||||
|
||||
let header = Header {
|
||||
magic: *b"dangoimg",
|
||||
|
||||
width,
|
||||
height,
|
||||
|
||||
compression_type,
|
||||
color_format,
|
||||
compression_level,
|
||||
|
||||
..Default::default()
|
||||
color_format,
|
||||
};
|
||||
|
||||
DangoPicture {
|
||||
|
@ -55,9 +71,12 @@ impl DangoPicture {
|
|||
// Write out the header
|
||||
output.write_all(&self.header.to_bytes()).unwrap();
|
||||
|
||||
// Based on the compression type, modify the data accordingly
|
||||
let modified_data = match self.header.compression_type {
|
||||
CompressionType::None => &self.bitmap,
|
||||
CompressionType::Lossless => &diff_line(self.header.width, self.header.height, &self.bitmap),
|
||||
CompressionType::Lossless => {
|
||||
&diff_line(self.header.width, self.header.height, &self.bitmap)
|
||||
},
|
||||
CompressionType::LossyDct => {
|
||||
&dct_compress(
|
||||
&self.bitmap,
|
||||
|
@ -67,11 +86,15 @@ impl DangoPicture {
|
|||
width: self.header.width as usize,
|
||||
height: self.header.height as usize,
|
||||
}
|
||||
).concat().iter().flat_map(|i| i.to_le_bytes()).collect()
|
||||
)
|
||||
.concat()
|
||||
.into_iter()
|
||||
.flat_map(VarInt::encode_var_vec)
|
||||
.collect()
|
||||
},
|
||||
};
|
||||
|
||||
// Compress the image data
|
||||
// Compress the final image data using the basic LZW scheme
|
||||
let (compressed_data, compression_info) = compress(&modified_data)?;
|
||||
|
||||
// Write out compression info
|
||||
|
@ -83,22 +106,55 @@ impl DangoPicture {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Encode and write the image out to a file.
|
||||
pub fn save<P: ?Sized + AsRef<std::path::Path>>(&self, path: &P) -> Result<(), Error> {
|
||||
let mut out_file = BufWriter::new(File::create(path.as_ref())?);
|
||||
|
||||
self.encode(&mut out_file)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decode the image from anything that implements [Read]
|
||||
pub fn decode<I: Read + ReadBytesExt>(mut input: I) -> Result<DangoPicture, Error> {
|
||||
let mut magic = [0u8; 8];
|
||||
input.read_exact(&mut magic).unwrap();
|
||||
|
||||
if magic != *b"dangoimg" {
|
||||
return Err(Error::InvalidIdentifier(magic));
|
||||
}
|
||||
|
||||
let header = Header::read_from(&mut input)?;
|
||||
|
||||
let compression_info = CompressionInfo::read_from(&mut input);
|
||||
|
||||
let preprocessed_bitmap = decompress(&mut input, &compression_info);
|
||||
let pre_bitmap = decompress(&mut input, &compression_info);
|
||||
|
||||
let bitmap = line_diff(header.width, header.height, &preprocessed_bitmap);
|
||||
let bitmap = match header.compression_type {
|
||||
CompressionType::None => pre_bitmap,
|
||||
CompressionType::Lossless => {
|
||||
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,
|
||||
DctParameters {
|
||||
quality: header.compression_level as u32,
|
||||
format: header.color_format,
|
||||
width: header.width as usize,
|
||||
height: header.height as usize,
|
||||
}
|
||||
)
|
||||
},
|
||||
};
|
||||
|
||||
Ok(DangoPicture { header, bitmap })
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue