commit f184fc5dbf051c95238762e930bfde3242018f83 Author: Nihil Carcosa Date: Wed Nov 13 22:33:10 2024 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..97439b3 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,92 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "array-init" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb6d71005dc22a708c7496eee5c8dc0300ee47355de6256c3b35b12b5fef596" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "colors-transform" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9226dbc05df4fb986f48d730b001532580883c4c06c5d1c213f4b34c1c157178" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "flate2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "miniz_oxide" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +dependencies = [ + "adler", +] + +[[package]] +name = "png" +version = "0.17.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f0e7f4c94ec26ff209cee506314212639d6c91b80afb82984819fafce9df01c" +dependencies = [ + "bitflags", + "crc32fast", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "qoi-test" +version = "0.1.0" +dependencies = [ + "array-init", + "colors-transform", + "log", + "png", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3c03564 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "qoi-test" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +array-init = "2.0.1" +png = "0.17.6" +log = "0.4.17" +colors-transform = "0.2.11" diff --git a/README.md b/README.md new file mode 100644 index 0000000..6f49673 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# QOI (Quite OK Image Format) Rust De-/Encoder + +This is a (currently not bug free) implementation of the QOI image compression algorithm in Rust. + +Currently supports de- and encoding from and to PNG. + +## To build + +run `cargo build -r` to build a stable version for your rustc toolchain in `./target/release` diff --git a/checkerboard.qoi b/checkerboard.qoi new file mode 100644 index 0000000..f38cec6 Binary files /dev/null and b/checkerboard.qoi differ diff --git a/img.qoi b/img.qoi new file mode 100644 index 0000000..997aed4 Binary files /dev/null and b/img.qoi differ diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..36214f0 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,828 @@ +//! # qoi_img +//! `qoi_img` is a bad, from-scratch implementation of the decoder and encoder for the `.qoi` file format as described as on [qoiformat.org](https://qoiformat.org/qoi-specification.pdf). +//! This crate should not be published as better crates are available, e.g. [rapid-qoi](https://crates.io/crates/rapid-qoi). + +#![allow(dead_code, unused_variables)] +pub mod qoi_img { + + use std::fmt; + use std::io::prelude::*; + use std::fs::File; + use log::{debug,info, Level, Record, SetLoggerError, LevelFilter}; + + use array_init; + + //Custom error for custom error handling + #[derive(Debug, Clone, PartialEq)] + pub enum ImgError { + DataError, + PixelNumberError, + DecodeError, + HeaderError, + } + //inherit from base Error + impl std::error::Error for ImgError {} + + + //Output for error handling + impl fmt::Display for ImgError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ImgError::DataError => write!(f, "invalid number of bytes (must be devisible by 4)"), + ImgError::PixelNumberError => write!(f, "number of pixels does not match height and width params"), + ImgError::DecodeError => write!(f, "decoder failed to construct valid image"), + ImgError::HeaderError => write!(f, "not a valid QOI file header") + } + } + } + + + //boilerplate implementation of the log crate + struct SimpleLogger; + + impl log::Log for SimpleLogger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= Level::Debug + } + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + eprintln!("{} - {}", record.level(), record.args()); + } + } + fn flush(&self) { + + } + } + //logging boilerplate + static LOGGER: SimpleLogger = SimpleLogger; + /// Initialises the logger provided by [log](https://crates.io/crates/log) + /// # Example + /// + /// ``` + /// # use std::error::Error; + /// # use crate::qoi_test::qoi_img::*; + /// # fn main() -> Result<(), Box> { + /// init().expect("Failed to initialize logger"); + /// # + /// # Ok(()) + /// # + /// # } + /// ``` + /// + /// If you want to pass the error on replace the `println!`: + /// + /// ``` + /// # use std::error::Error; + /// # use crate::qoi_test::qoi_img::*; + /// # fn main() -> Result<(), ImgError> { + /// match init() { + /// Ok(()) => (), + /// Err(e) => println!("Logger failed to initialize!") + /// } + /// # + /// # Ok(()) + /// # + /// # } + /// ``` + pub fn init() -> Result<(), SetLoggerError>{ + log::set_logger(&LOGGER).map(|()| log::set_max_level(LevelFilter::Debug)) + } + + + /// Custom image struct, which is used to store decoded data. Used by [encode_from_image] to encode the necessary data in bytes. Requires a Vector over [Pixel] values, `Vec`, + /// which can be generated by [`self::new`] if given byte data. Otherwise, [self.pixels] must be given filled vector. + /// `height` and `width` are given as u32 (note that qoi encoding does not guarantee functionality for images containing over 4000000 pixels.) + /// `channels` specifies the number of channels 3 (RGB) or 4 (RBGA). + /// `colorspace` specifies whether sRGB or all linear channels are used (0,1); + /// # Examples + /// Create a new image via constructor [`Image::new()`]; + /// ```rust + /// # use std::error::Error; + /// # use crate::qoi_test::qoi_img::*; + /// # fn main() -> Result<(), Box> { + /// + /// let pixels: Vec = vec![0;1024*1024*4]; + /// let height: u32 = 1024; + /// let width: u32 = 1024; + /// let channels: u8 = 4; + /// let colorspace: u8 = 0; + /// let img: Image = Image::new(pixels, height, width, channels, colorspace)?; + /// # + /// # Ok(()) + /// # } + /// ``` + /// + /// Alternatively, [`Image::from_pixels()`] can be used to create an image from pixel values. + pub struct Image { + pixels: Vec, + height: u32, + width: u32, + channels: u8, + colorspace: u8, + } + + impl Image { + //Image constructor, expects an array of u8 pixels values in order, left to right, top to bottom. + pub fn new(data: Vec, height: u32, width: u32, channels: u8, colorspace: u8,) -> Result { + let pixels: Vec = match Image::pixels_from_bytes(data) { + Ok(out) => out, + Err(error) => return Err(error), + }; + if pixels.len() == (height * width) as usize { + let out: Image = Image { + pixels, + height, + width, + channels, + colorspace, + }; + Ok(out) + } else { + Err(ImgError::PixelNumberError) + } + + } + + pub fn from_pixels(pixels: Vec, height: u32, width: u32, channels: u8, colorspace: u8) -> Image { + let img = Image { + pixels, + height, + width, + channels, + colorspace + }; + img + } + + fn pixels_from_bytes(data: Vec) -> Result, ImgError> { + let mut pixels: Vec = Vec::with_capacity(data.len()/4); + if data.len() % 4 == 0 { + for i in 0..data.len()/4 { + pixels.push(Pixel { + r: data[i*4], + g: data[i*4+1], + b: data[i*4+2], + a: data[i*4+3], + }); + } + Ok(pixels) + } else { + Err(ImgError::DataError) + } + } + } + + #[derive(Clone,Copy,Debug, PartialEq)] + pub struct Pixel { + r: u8, + g: u8, + b: u8, + a: u8, + } + + #[derive(Debug, PartialEq)] + pub enum ChunkType { + Run, + Index, + Luma, + Diff, + RGB, + RGBA + } + + impl Pixel { + + + pub fn new(r: u8, g: u8, b: u8, a: u8) -> Pixel { + Pixel { + r, + g, + b, + a, + } + } + fn equals(&self, other: &Pixel) -> bool { + if (self.r == other.r) && (self.g == other.g) && (self.b == other.b) && (self.a == other.a) { + true + } else { + false + } + } + + fn equals_rgb(&self, other: &Pixel) -> bool { + if (self.r == other.r) && (self.g == other.g) && (self.b == other.b) { + true + } else { + false + } + } + + + //self = curr pixel, other = prev pixel + pub fn determine_chunk(&self, other: &Pixel, buffer: &Vec) -> (ChunkType, Option<(u8,u8,u8)>){ + + if self.equals(&other) { + return (ChunkType::Run, None); + } + + if self.equals(&buffer[color_hash(&self) as usize]) { + return (ChunkType::Index,Some((color_hash(&self), 0,0 ))); + } + + if self.a != other.a { + return (ChunkType::RGBA, None); + } + + let diff_tuple: (i16,i16,i16) = self.diff(other); + let dr: i16 = diff_tuple.0; + let dg: i16 = diff_tuple.1; + let db: i16 = diff_tuple.2; + + if (dr > -3 && dr < 2) && (dg > -3 && dg < 2) && (db > -3 && db < 2) { + let dr: u8 = (dr + DIFF_BIAS as i16) as u8; + let dg: u8 = (dg + DIFF_BIAS as i16) as u8; + let db: u8 = (db + DIFF_BIAS as i16) as u8; + return (ChunkType::Diff, Some((dr, dg, db))); + } else if (dg > -33 && dg < 32) && ((dr - dg) > -9) && ((dr - dg) < 8) && ((db - dg) > -9) && ((db - dg) < 8) { + let dg_out: u8 = (dg + LUMA_BIAS_G as i16) as u8; + let dr_dg: u8 = (dr - dg + LUMA_BIAS_RB as i16) as u8; + let db_dg: u8 = (db - dg + LUMA_BIAS_RB as i16) as u8; + return (ChunkType::Luma, Some((dg_out, dr_dg, db_dg))); + } else { + return (ChunkType::RGB, None); + } + + } + pub fn diff(&self, other: &Pixel) -> (i16, i16, i16) { + let mut dr: i16; + let dr_inv: i16; + let mut dg: i16; + let dg_inv: i16; + let mut db: i16; + let db_inv: i16; + + dr = self.r.wrapping_sub(other.r) as i16; + dr_inv = other.r.wrapping_sub(self.r) as i16; + + if dr.abs() > dr_inv.abs() { + dr = dr_inv; + dr = -dr; + } + + dg = self.g.wrapping_sub(other.g) as i16; + dg_inv = other.g.wrapping_sub(self.g) as i16; + + if dg.abs() > dg_inv.abs() { + dg = dg_inv; + dg = -dg; + } + + db = self.b.wrapping_sub(other.b) as i16; + db_inv = other.b.wrapping_sub(self.b) as i16; + + if db.abs() > db_inv.abs() { + db = db_inv; + db = -db; + } + + (dr, dg, db) + + } + } + + //Definition of header bytes + struct Header { + magic: [char; 4], //magic bytes "qoif" + width: u32, //image width in pixels (BE) + height: u32, //image height in pixels (BE) + channels: u8, // 3 = RGB, 4 = RBGA + colorspace: u8, // 0 = sRGB with linear alpha, 1 = all channels linear + } + + impl Header { + fn convert_to_bytestream(&self) -> [u8;14] { + let mut out: [u8; 14] = [0;14]; + + //First, set magic bytes + out[0] = self.magic[0] as u8; + out[1] = self.magic[1] as u8; + out[2] = self.magic[2] as u8; + out[3] = self.magic[3] as u8; + + //split width and height into 8-bit chunks + let width_bytes = self.width.to_be_bytes(); + let height_bytes = self.height.to_be_bytes(); + + out[4] = width_bytes[0]; + out[5] = width_bytes[1]; + out[6] = width_bytes[2]; + out[7] = width_bytes[3]; + out[8] = height_bytes[0]; + out[9] = height_bytes[1]; + out[10] = height_bytes[2]; + out[11] = height_bytes[3]; + + //Set information bits + out[12] = self.channels; + out[13] = self.colorspace; + + out + } + } + + //Definition of End of Stream bytes + #[derive(Debug)] + struct End { + bytes: [u8;8] + } + impl End { + fn new() -> End { + End { + bytes: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01] + } + } + } + + //chunks as defined in the QOI spec + const QOI_OP_RGB: u8 = 0b1111_1110; + const QOI_OP_RGBA: u8 = 0b1111_1111; + const QOI_OP_RUN: u8 = 0b1100_0000; + const QOI_OP_INDEX: u8 = 0b0000_0000; + const QOI_OP_DIFF: u8 = 0b0100_0000; + const QOI_OP_LUMA: u8 = 0b1000_0000; + + //Biases as defined in the QOI spec + const RUN_BIAS: u8 = 1; + + const DIFF_BIAS: u8 = 2; + + const LUMA_BIAS_G: u8 = 32; + const LUMA_BIAS_RB: u8 = 8; + + + + //hash function for assigning buffer indices to stored pixels + fn color_hash(pixel: &Pixel) -> u8 { + let store: u32 = pixel.r as u32 * 3 + pixel.g as u32 * 5 + pixel.b as u32 * 7 + pixel.a as u32 * 11; + (store % 64) as u8 + } + + pub fn encode_from_image(img: Image) -> Vec { + + let mut prev_pixel: Pixel = Pixel {r: 0u8, b: 0u8, g: 0u8, a: 255u8}; + + let mut prev_buffer: Vec = Vec::with_capacity(64); + + for i in 0..64 { + let pix: Pixel = Pixel {r:0,g:0,b:0,a:0}; + prev_buffer.push(pix); + } + + let mut encoded_bytes: Vec = Vec::new(); + let mut run: u8 = 0; + + + let head = Header { + magic: ['q', 'o', 'i', 'f'], + width: img.width, + height: img.height, + channels: img.channels, + colorspace: img.colorspace + }; + let head_stream = head.convert_to_bytestream(); + + + for i in head_stream { + encoded_bytes.push(i); + } + + let mut counter: u32 = 0; + + for pixel in img.pixels { + counter += 1; + let chunk: (ChunkType, Option<(u8,u8,u8)>) = pixel.determine_chunk(&prev_pixel, &prev_buffer); + if chunk == (ChunkType::Run, None) { + run += 1; + prev_pixel = pixel.clone(); + continue; + } + if run > 0 { + if run > 62 { + while run > 0 { + if run/62 > 0 { + encoded_bytes.push(QOI_OP_RUN | (62-RUN_BIAS)); + run -= 62; + } else if run%62 > 0 { + encoded_bytes.push(QOI_OP_RUN | (run-RUN_BIAS)); + run = 0; + } else { + break; + } + } + } else { + encoded_bytes.push(QOI_OP_RUN | (run-RUN_BIAS)); + run = 0; + } + } + + match chunk { + (ChunkType::Index, Some((index, irr1, irr2))) => { + encoded_bytes.push(QOI_OP_INDEX | index); + prev_pixel = pixel; + }, + (ChunkType::Diff, Some((dr, dg,db))) => { + let mut out: u8 = 0b0000_0000; + out = out | db; + out = out | (dg << 2); + out = out | (dr << 4); + encoded_bytes.push(QOI_OP_DIFF | out); + prev_pixel = pixel.clone(); + prev_buffer[color_hash(&pixel) as usize] = pixel; + }, + (ChunkType::Luma, Some((dg, dr_dg, db_dg))) => { + let mut out: [u8; 2] = [0b0000_0000;2]; + out[0] |= dg; + out[0] |= QOI_OP_LUMA; + out[1] |= db_dg; + out[1] |= dr_dg << 4; + encoded_bytes.push(out[0]); + encoded_bytes.push(out[1]); + prev_pixel = pixel.clone(); + prev_buffer[color_hash(&pixel) as usize] = pixel; + }, + (ChunkType::RGB, None) => { + encoded_bytes.push(QOI_OP_RGB); + encoded_bytes.push(pixel.r); + encoded_bytes.push(pixel.g); + encoded_bytes.push(pixel.b); + prev_pixel = pixel.clone(); + prev_buffer[color_hash(&pixel) as usize] = pixel; + }, + (ChunkType::RGBA, None) => { + if (pixel.a as i16 - prev_pixel.a as i16) == 0i16 { + //this should never be reached, but it is + encoded_bytes.push(QOI_OP_RGB); + encoded_bytes.push(pixel.r); + encoded_bytes.push(pixel.g); + encoded_bytes.push(pixel.b); + prev_pixel = pixel.clone(); + prev_buffer[color_hash(&pixel) as usize] = pixel; + } else { + encoded_bytes.push(QOI_OP_RGBA); + encoded_bytes.push(pixel.r); + encoded_bytes.push(pixel.g); + encoded_bytes.push(pixel.b); + encoded_bytes.push(pixel.a); + prev_pixel = pixel.clone(); + prev_buffer[color_hash(&pixel) as usize] = pixel; + } + }, + _ => panic!("Critical error at encoding stage: Illegal output from difference function.") + } + + } + + if run > 0 { + if run > 62 { + while run > 0 { + if run/62 > 0 { + encoded_bytes.push(QOI_OP_RUN | (62-RUN_BIAS)); + run -= 62; + } else if run%62 > 0 { + encoded_bytes.push(QOI_OP_RUN | (run-RUN_BIAS)); + run = 0; + } else { + break; + } + } + } else { + encoded_bytes.push(QOI_OP_RUN | (run-RUN_BIAS)); + } + } + + let end_bytes = End::new(); + for i in end_bytes.bytes { + encoded_bytes.push(i) + } + + info!("Number of pixels processed: {}.", counter); + info!("Number of bytes in encoding: {:?}.", encoded_bytes.len()-22); + info!("Compression rate: {:.2}%.", (1.0-(encoded_bytes.len()-22) as f64/(counter*4)as f64)*100.0); + + encoded_bytes + + } + + pub fn write_to_file(bytes: Vec, filename: &str) -> std::io::Result<()>{ + let mut file_path: String = String::from(filename); + file_path.push_str(".qoi"); + + let mut buffer = File::create(file_path)?; + let mut pos = 0; + + while pos < bytes.len() { + let bytes_written = buffer.write(&bytes[pos..])?; + pos += bytes_written; + } + Ok(()) + } + + fn read_header(bytes: &[u8]) -> Result<(u32,u32,u8,u8), ImgError> { + if bytes[0] == 'q' as u8 && bytes[1] == 'o' as u8 && bytes[2] == 'i' as u8 && bytes[3] == 'f' as u8 { + let mut width: u32 = 0b0000_0000_0000_0000_0000_0000_0000_0000; + let mut height: u32 = 0b0000_0000_0000_0000_0000_0000_0000_0000; + width |= ((bytes[4] as u32) << 24) as u32; + width |= ((bytes[5] as u32) << 16) as u32; + width |= ((bytes[6] as u32) << 8) as u32; + width |= (bytes[7]) as u32; + height |= ((bytes[8] as u32) << 24) as u32; + height |= ((bytes[9] as u32) << 16) as u32; + height |= ((bytes[10] as u32) << 8) as u32; + height |= (bytes[11]) as u32; + return Ok((width, height, bytes[12], bytes[13])); + } else { + return Err(ImgError::HeaderError); + } + } + + fn read_tag(tag: u8) -> Result { + if tag == QOI_OP_RGB { + return Ok(ChunkType::RGB); + } + if tag == QOI_OP_RGBA { + return Ok(ChunkType::RGBA); + } + if (tag & 0b1100_0000) == QOI_OP_DIFF{ + return Ok(ChunkType::Diff); + } + if (tag & 0b1100_0000) == QOI_OP_INDEX { + return Ok(ChunkType::Index); + } + if (tag & 0b1100_0000) == QOI_OP_LUMA { + return Ok(ChunkType::Luma); + } + if (tag & 0b1100_0000) == QOI_OP_RUN { + return Ok(ChunkType::Run); + } + return Err(ImgError::DecodeError); + } + + fn dec_rgb(bytes: &[u8], alpha: u8) -> Pixel { + let pixel: Pixel = Pixel::new(bytes[1], bytes[2], bytes[3], alpha); + pixel + } + + fn dec_rgba(bytes: &[u8]) -> Pixel { + let pixel: Pixel = Pixel::new(bytes[1], bytes[2], bytes[3], bytes[4]); + pixel + } + + fn dec_diff(byte: u8, prev_pixel: &Pixel) -> Pixel { + let dr: u8; + let dg: u8; + let db: u8; + + dr = (byte & 0b00110000) >> 4; + dg = (byte & 0b00001100) >> 2; + db = byte & 0b00000011; + + let r: u8 = prev_pixel.r.wrapping_add(dr); + let g: u8 = prev_pixel.g.wrapping_add(dg); + let b: u8 = prev_pixel.b.wrapping_add(db); + + let r: u8 = r.wrapping_sub(DIFF_BIAS); + let b: u8 = b.wrapping_sub(DIFF_BIAS); + let g: u8 = g.wrapping_sub(DIFF_BIAS); + + let pixel: Pixel = Pixel::new(r,g,b, prev_pixel.a); + pixel + } + + fn dec_luma(bytes: &[u8], prev_pixel: &Pixel) -> Pixel { + let dr: u8; + let dr_dg: u8; + let db_dg: u8; + let dg: u8; + let db: u8; + + dg = bytes[0] & 0b00111111; + dr_dg = (bytes[1] & 0b11110000) >> 4; + db_dg = bytes[1] & 0b00001111; + dr = dr_dg + dg; + db = db_dg + dg; + + let r: u8 = prev_pixel.r.wrapping_add(dr); + let g: u8 = prev_pixel.g.wrapping_add(dg); + let b: u8 = prev_pixel.b.wrapping_add(db); + + let r: u8 = r.wrapping_sub(LUMA_BIAS_RB + LUMA_BIAS_G); + let g: u8 = g.wrapping_sub(LUMA_BIAS_G); + let b: u8 = b.wrapping_sub(LUMA_BIAS_RB + LUMA_BIAS_G); + + let pixel: Pixel = Pixel::new(r, g, b, prev_pixel.a); + pixel + } + + fn dec_run() {} + + pub fn decode(mut bytes: Vec) -> Result { + let width: u32; + let height: u32; + let channels: u8; + let colorspace: u8; + + let mut prev_pixel: Pixel = Pixel {r: 0u8, g: 0u8, b: 0u8, a: 255u8}; + + let mut prev_buffer: [Pixel; 64] = array_init::array_init(|_| Pixel::new(0,0,0,0)); + + match read_header(&bytes[0..14]) { + Ok((w, h, ch, c))=> { + width = w; + height = h; + channels = ch; + colorspace = c; + }, + Err(err) => { + return Err(err); + } + } + let mut pixels: Vec = Vec::with_capacity((width*height*4) as usize); + + if bytes[bytes.len()-1] == 1 { + for i in 2..9 { + if bytes[bytes.len()-i] != 0 { + debug!("Ending bytes not present."); + return Err(ImgError::DecodeError); + } + } + for i in 0..8 { + bytes.pop(); + } + } else { + debug!("Ending bytes not present."); + return Err(ImgError::DecodeError); + } + + let mut i: usize = 14; + + while i < bytes.len() { + match read_tag(bytes[i]) { + Ok(tag) => { + match tag { + ChunkType::RGB => { + let dec_pix: Pixel = dec_rgb(&bytes[i..i+4], prev_pixel.a); + prev_pixel = dec_pix.clone(); + prev_buffer[color_hash(&dec_pix) as usize] = dec_pix.clone(); + pixels.push(dec_pix); + i += 3; + }, + ChunkType::RGBA => { + let dec_pix: Pixel = dec_rgba(&bytes[i..i+5]); + prev_pixel = dec_pix.clone(); + prev_buffer[color_hash(&dec_pix) as usize] = dec_pix.clone(); + pixels.push(dec_pix); + i += 4; + }, + ChunkType::Diff => { + let dec_pix: Pixel = dec_diff(bytes[i], &prev_pixel); + prev_pixel = dec_pix.clone(); + prev_buffer[color_hash(&dec_pix) as usize] = dec_pix.clone(); + pixels.push(dec_pix); + }, + ChunkType::Index => { + let dec_pix: Pixel = prev_buffer[bytes[i] as usize]; + prev_pixel = dec_pix.clone(); + prev_buffer[color_hash(&dec_pix) as usize] = dec_pix.clone(); + pixels.push(dec_pix); + }, + ChunkType::Luma => { + let dec_pix: Pixel = dec_luma(&bytes[i..i+2], &prev_pixel); + prev_pixel = dec_pix.clone(); + prev_buffer[color_hash(&dec_pix) as usize] = dec_pix.clone(); + pixels.push(dec_pix); + i += 1; + }, + ChunkType::Run => { + let length: u8 = (bytes[i] & 0b00111111) + RUN_BIAS; + for j in 0..length { + pixels.push(prev_pixel.clone()); + } + prev_buffer[color_hash(&prev_pixel) as usize] = prev_pixel.clone(); + } + } + }, + Err(err) => return Err(err), + } + i += 1; + } + + if pixels.len() as u32 != height*width { + debug!("h*w: {}", height*width); + debug!("n pixels: {}", pixels.len()); + return Err(ImgError::DecodeError); + } + + let img = Image::from_pixels(pixels, height, width, channels, colorspace); + Ok(img) + + } + + #[cfg(test)] +mod tests { + use super::*; + use std::io; + use std::io::{Read, BufReader}; + use std::fs::File; + + #[test] + fn diff_test() { + init().expect("Logger initialisation failed!"); + let pix1: Pixel = Pixel::new( 0, 0, 0,255); + let pix2: Pixel = Pixel::new(255,255,255,255); + + let pix3: Pixel = Pixel::new(155,155,155,255); + let pix4: Pixel = Pixel::new(160,160,160,255); + + assert_eq!(pix1.diff(&pix2), ( 1, 1, 1)); + assert_eq!(pix2.diff(&pix1), (-1,-1,-1)); + assert_eq!(pix4.diff(&pix3), ( 5, 5, 5)); + assert_eq!(pix3.diff(&pix4), (-5,-5,-5)); + } + + #[test] + fn encode_test() -> io::Result<()> { + let f: File = File::open("test.qoi")?; + let mut reader = BufReader::new(f); + let mut bytes: Vec = Vec::new(); + + reader.read_to_end(&mut bytes)?; + + let out_img: super::Image; + + match super::decode(bytes) { + Ok(img) => out_img = img, + Err(err) => panic!("wallah geht nicht :/ {:?}", err) + } + write_to_file(encode_from_image(out_img), "test_dec").expect("wallahi!"); + Ok(()) + } + + #[test] + fn decode_test() -> io::Result<()> { + let f: File = File::open("testcard_rgba.qoi")?; + let mut reader = BufReader::new(f); + let mut bytes: Vec = Vec::new(); + + reader.read_to_end(&mut bytes)?; + + let out_img: super::Image; + + match super::decode(bytes) { + Ok(img) => out_img = img, + Err(err) => panic!("Ja bruder war nicht so erfolgreich ahahahahahha {:?}", err) + } + + write_to_file(encode_from_image(out_img), "testcard_new").expect("Boowomb!"); + + Ok(()) + + } + + #[test] + fn tag_test() { + //init().expect("Logger initialisation failed!"); + let test_rgb: u8 = 0b1111_1110; + let test_rgba: u8 = 0b1111_1111; + let test_luma: u8 = 0b1011_1010; + let test_run: u8 = 0b1110_1101; + let test_diff: u8 = 0b0110_1010; + let test_index: u8 = 0b0010_1010; + + + assert_eq!(Ok(ChunkType::RGB), super::read_tag(test_rgb)); + assert_eq!(Ok(ChunkType::RGBA), super::read_tag(test_rgba)); + assert_eq!(Ok(ChunkType::Luma), super::read_tag(test_luma)); + assert_eq!(Ok(ChunkType::Diff), super::read_tag(test_diff)); + assert_eq!(Ok(ChunkType::Index), super::read_tag(test_index)); + assert_eq!(Ok(ChunkType::Run), super::read_tag(test_run)); + } + + #[test] + fn sub_decoders_test() { + //init().expect("Logger initialisation failed!"); + let pix: Pixel = Pixel { r: 255, g: 255, b: 255, a: 255 }; + let prev: Pixel = Pixel { r: 1, g: 1, b: 1, a: 255 }; + let byte: u8 = 0b01000000; + + assert_eq!(pix,dec_diff(byte, &prev)); + + let pix: Pixel = Pixel { r: 17, g: 22, b: 28, a: 100 }; + let prev: Pixel = Pixel { r: 10, g: 10, b: 10, a: 100 }; + let byte: [u8;2] = [ 0b10101100, 0b00111110 ]; + + assert_eq!(pix, dec_luma(&byte[0..2], &prev)); + } +} + +} + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..849331e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,256 @@ +use std::env; +use std::fs::File; +use std::io; +use std::io::{BufReader, Read}; +use std::time::SystemTime; + +use colors_transform::{Color, Hsl, Rgb}; +use png; +use qoi_test::qoi_img::*; + +fn encode_checkerboard() { + let mut pixels: Vec = Vec::with_capacity(64 * 64); + let red: u8 = 150; + let green: u8 = 0; + let blue: u8 = 150; + //row iterator + for i in 0..64 { + //column iterator + for j in 0..64 { + //if row is 0..16, 32..48 + if (i / 16) == 0 || (i / 16) == 2 { + //if column is 0..16, 32..48 + if (j / 16) == 0 || (j / 16) == 2 { + let push_pix: Pixel = Pixel::new(red, green, blue, 255); + pixels.push(push_pix); + } else { + let push_pix: Pixel = Pixel::new(255, 255, 255, 255); + pixels.push(push_pix); + } + } else { + if (j / 16) == 1 || (j / 16) == 3 { + let push_pix: Pixel = Pixel::new(red, green, blue, 255); + pixels.push(push_pix); + } else { + let push_pix: Pixel = Pixel::new(255, 255, 255, 255); + pixels.push(push_pix); + } + } + } + } + + let img: Image = Image::from_pixels(pixels, 64, 64, 4, 0); + write_to_file(encode_from_image(img), "checkerboard").expect("Error writing file!"); +} + +fn encode_debug() { + let mut img_data: Vec = Vec::new(); + //row iterator + for i in 0..1024 { + //cell iterator + for j in 0..1024 { + //subpixel iterator + for k in 0..4 { + let rgb: Hsl = Hsl::from(0.3515625 * j as f32, 100.0, 50.0); + let rgb: Rgb = rgb.to_rgb(); + let alpha: f64 = -(255.0 / 1024.0) * (i as f64) + 255.0; + match k { + 0 => img_data.push(rgb.get_red() as u8), + 1 => img_data.push(rgb.get_green() as u8), + 2 => img_data.push(rgb.get_blue() as u8), + 3 => img_data.push(alpha as u8), + _ => panic!("unrecoverable for-loop failure"), + } + } + } + } + let img: Image = match Image::new(img_data, 1024, 1024, 4, 0) { + Ok(image) => image, + Err(err) => panic!("Problem generating image: {:?}", err), + }; + let start = SystemTime::now(); + let img_bytes: Vec = encode_from_image(img); + let stop = match start.elapsed() { + Ok(elapsed) => elapsed.as_millis(), + Err(e) => { + println!("Error: {e:?}"); + return (); + } + }; + println!("Encode took: {} ms.", stop); + write_to_file(img_bytes, "test").expect("Error writing file!"); +} + +fn demo() { + let decoder = png::Decoder::new(File::open("testcard_rgba.png").unwrap()); + let mut reader = match decoder.read_info() { + Ok(reader) => reader, + Err(e) => panic!("ERROR: couldn't read file: {e:}"), + }; + let mut buf = vec![0; reader.output_buffer_size()]; + + let info = match reader.next_frame(&mut buf) { + Ok(i) => i, + Err(e) => panic!("ERROR: {e:?}"), + }; + + let bytes = &buf[..info.buffer_size()]; + let byte_vec: Vec = bytes.to_vec(); + + let img: Image = match Image::new(byte_vec, 256, 256, 4, 0) { + Ok(image) => image, + Err(err) => panic!("Problem generating imag: {:?}", err), + }; + let start = SystemTime::now(); + write_to_file(encode_from_image(img), "img").expect("Error writing file!"); + let stop = match start.elapsed() { + Ok(elapsed) => elapsed.as_millis(), + Err(e) => { + println!("Error: {e:?}"); + return (); + } + }; + println!("Encode took: {} ms.", stop); + let start = SystemTime::now(); + encode_checkerboard(); + let stop = match start.elapsed() { + Ok(elapsed) => elapsed.as_millis(), + Err(e) => { + println!("Error: {e:?}"); + return (); + } + }; + println!("Encode took: {} ms.", stop); + encode_debug(); +} + +//Attempts to encode given png image as second argument into qoi +fn encode(args: &Vec) { + //Path is fetched from arguments + let path = &args[2]; + + //Init png decoder, attempt to decode png into bitmap, throw error if unsuccessful + let decoder = png::Decoder::new(File::open(path).unwrap()); + let mut reader = match decoder.read_info() { + Ok(reader) => reader, + Err(e) => panic!("ERROR: couldn't read file: {e:}"), + }; + + //read image metadata + let width: u32 = reader.info().width; + let height: u32 = reader.info().height; + //for now: hardcoded to 4 + let channels: u8 = 4; + + //create buffer matching the size of png-decoder output, writing size to output + let mut buf = vec![0; reader.output_buffer_size()]; + let info = match reader.next_frame(&mut buf) { + Ok(i) => i, + Err(e) => panic!("ERROR: {e:?}"), + }; + + //convert buffer into vector + let bytes = &buf[..info.buffer_size()]; + let byte_vec: Vec = bytes.to_vec(); + + //create bitmap data from raw byte vector + let img: Image = match Image::new(byte_vec, height, width, channels, 0) { + Ok(image) => image, + Err(err) => panic!("Problem generating image: {:?}", err), + }; + + //encode generated bitmap + if args.len() >= 4 { + let filename: &String = &args[3]; + write_to_file(encode_from_image(img), filename).expect("ERROR: Can't write file."); + } else { + let mut filename = path.clone(); + for _i in 0..4 { + filename.pop(); + } + write_to_file(encode_from_image(img), filename.as_str()).expect("ERROR: Can't write file."); + } + println!("Encoding successful!"); +} + +fn decode(args: &Vec) -> io::Result<()> { + let mut path: String = String::new(); + if args.len() > 2 { + path.push_str(args[2].as_str()); + } else { + println!("ERROR: incorrect number of arguments! (specify file to decode!)."); + () + } + + let f: File = match File::open(path.as_str()) { + Ok(f) => f, + Err(e) => panic!("ERROR: {e:?}"), + }; + let mut reader = BufReader::new(f); + let mut bytes: Vec = Vec::new(); + + reader.read_to_end(&mut bytes)?; + + match qoi_test::qoi_img::decode(bytes) { + Ok(_img) => println!("Decoding successful!"), + Err(err) => panic!("ERROR: {err:?}"), + } + Ok(()) +} + +fn bench(args: &Vec) { + if args.len() < 4 { + panic!("ERROR: invalid number of arguments!"); + } + + let start = SystemTime::now(); + encode(args); + match start.elapsed() { + Ok(elapsed) => println!("Encode took {} μs", elapsed.as_micros()), + Err(e) => panic!("ERROR: {e:?}"), + } + let mut new_arg: Vec = Vec::new(); + new_arg.push(String::from("")); + new_arg.push(String::from("")); + let mut to_push: String = args[3].clone(); + to_push.push_str(".qoi"); + new_arg.push(to_push); + let start = SystemTime::now(); + decode(&new_arg).expect( + "ERROR: Unspecified error during io-pipeline. Ensure file path is valid and can be read.", + ); + match start.elapsed() { + Ok(elapsed) => println!("Decode took {} μs", elapsed.as_micros()), + Err(e) => panic!("ERROR: {e:?}"), + } +} + +fn main() { + //Initialize logger + init().expect("Failed to initialize logger."); + + let args: Vec = env::args().collect(); + + if args.len() == 1 { + panic!("ERROR: no arguments send!"); + } + + match args[1].as_str() { + "demo" => { + demo(); + } + //can only handle pngs for now + "encode" => { + encode(&args); + } + "decode" => { + decode(&args).expect("ERROR: Unspecified error during io-pipeline. Ensure file path is valid and can be read."); + } + "bench" => { + bench(&args); + } + _ => { + panic!("Invalid arguments!") + } + } +} diff --git a/test.qoi b/test.qoi new file mode 100644 index 0000000..32db02b Binary files /dev/null and b/test.qoi differ diff --git a/testcard_rgba.png b/testcard_rgba.png new file mode 100644 index 0000000..2454247 Binary files /dev/null and b/testcard_rgba.png differ diff --git a/testcard_rgba.qoi b/testcard_rgba.qoi new file mode 100644 index 0000000..997aed4 Binary files /dev/null and b/testcard_rgba.qoi differ