diff --git a/Cargo.lock b/Cargo.lock index e704976..136f8b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,87 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "justerror" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "befab2078d3ff679889e32730a1e9107b06a60a18ed7dfa3384f66ebe5e1062a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "tinyrender" version = "0.1.0" +dependencies = [ + "justerror", + "thiserror", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" diff --git a/Cargo.toml b/Cargo.toml index 38d6838..cb8924f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,5 @@ version = "0.1.0" edition = "2024" [dependencies] +justerror = "1.1.0" +thiserror = "2.0.16" diff --git a/src/main.rs b/src/main.rs index e7a11a9..cc736a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,6 @@ -fn main() { - println!("Hello, world!"); -} +#[macro_use] +extern crate justerror; + +mod tga; + +fn main() {} diff --git a/src/tga.rs b/src/tga.rs index 0421018..f8f909a 100644 --- a/src/tga.rs +++ b/src/tga.rs @@ -1,9 +1,11 @@ use std::{ fs::File, - io::Read, + io::{Read, Write}, ops::{Deref, DerefMut}, }; +const HEADER_SIZE: usize = 18; + #[derive(Debug, Default, Clone, PartialEq, Eq)] #[repr(C, packed)] pub struct TGAHeader { @@ -63,24 +65,70 @@ impl DerefMut for TGAColor { } } +impl From<[u8; 4]> for TGAColor { + fn from(value: [u8; 4]) -> Self { + Self { bgra: value } + } +} + impl TGAColor { pub fn new() -> Self { Self { bgra: [0; 4] } } + + pub fn b(&mut self) -> &mut u8 { + &mut self[0] + } + + pub fn g(&mut self) -> &mut u8 { + &mut self[1] + } + + pub fn r(&mut self) -> &mut u8 { + &mut self[2] + } + + pub fn a(&mut self) -> &mut u8 { + &mut self[3] + } } #[derive(Debug, Default, Clone, PartialEq, Eq)] struct TGAImage { pub format: Format, - pub width: u32, - pub height: u32, + pub width: u16, + pub height: u16, data: Vec, } -pub type IoResult = std::io::Result<()>; +pub type IoResult = std::result::Result<(), TGAError>; + +#[Error] +pub enum TGAError { + IoError(std::io::Error), + Other(String), +} + +impl From for TGAError { + fn from(value: String) -> Self { + Self::Other(value) + } +} + +impl From<&str> for TGAError { + fn from(value: &str) -> Self { + Self::Other(value.to_string()) + } +} + +impl From for TGAError { + fn from(value: std::io::Error) -> Self { + Self::IoError(value) + } +} impl TGAImage { - pub fn new(width: u32, height: u32, format: Format) -> Self { + pub fn new(width: u16, height: u16, format: Format) -> Self { Self { format, width, @@ -89,20 +137,20 @@ impl TGAImage { } } - pub fn bytes_per_pixel(&self) -> u8 { - self.format as u8 + pub fn from_file(file: &str) -> Result { + let mut img = TGAImage::default(); + img.read_file(file)?; + Ok(img) } pub fn read_file(&mut self, file: &str) -> IoResult { let mut file = std::fs::File::open(file)?; - let header_size = size_of::(); - assert_eq!(header_size, 18); - let mut bytes = [0u8; header_size]; - file.read_exact(&mut bytes)?; - let header: TGAHeader = unsafe { std::mem::transmute(bytes) }; + let mut header_bytes = [0u8; HEADER_SIZE]; + file.read_exact(&mut header_bytes)?; + let header: TGAHeader = unsafe { std::mem::transmute(header_bytes) }; if header.width < 1 || header.height < 1 { - return Err("bad image metadata".into()); + return Err("bad image metadata".to_string().into()); } self.format = (header.bits_per_pixel >> 3).try_into()?; @@ -123,10 +171,53 @@ impl TGAImage { return Err("invalid TGA file".into()); } } + Ok(()) } - pub fn write_file(&self, file: &str) -> IoResult { - todo!() + pub fn write_file(&mut self, file: &str, do_vflip: bool, do_rle: bool) -> IoResult { + let mut developer_area = [0u8; 4]; + let mut extension_area = [0u8; 4]; + let mut footer: Vec = vec![ + 'T', 'R', 'U', 'E', 'V', 'I', 'S', 'I', 'O', 'N', '-', 'X', 'F', 'I', 'L', 'E', '.', + '\0', + ] + .into_iter() + .map(|b| b as u8) + .collect(); + + let mut file = std::fs::File::create(file)?; + + let mut header = TGAHeader::default(); + header.bits_per_pixel = self.bytes_per_pixel() << 3; + header.width = self.width; + header.height = self.height; + + header.datatype_code = match self.format { + Format::Grayscale if do_rle => 11, + Format::Grayscale => 3, + _ if do_rle => 10, + _ => 2, + }; + + header.image_descriptor = if do_vflip { 0x00 } else { 0x20 }; + + let mut header: [u8; HEADER_SIZE] = unsafe { std::mem::transmute(header) }; + + file.write_all(&mut header)?; + + if do_rle { + self.write_rle_file(&mut file)?; + } else { + file.write_all(&mut self.data)?; + } + + file.write_all(&mut developer_area)?; + file.write_all(&mut extension_area)?; + file.write_all(&mut footer)?; + + file.flush()?; + + Ok(()) } pub fn flip_horizontal(&mut self) { @@ -145,7 +236,100 @@ impl TGAImage { todo!() } + pub fn bytes_per_pixel(&self) -> u8 { + self.format as u8 + } + fn read_rle_file(&mut self, file: &mut File) -> IoResult { - todo!() + let num_pixels = (self.width * self.height) as usize; + let bpp = self.bytes_per_pixel() as usize; + self.data.resize(num_pixels * bpp, 0); + + let mut current_pixel = 0; + let mut current_byte = 0; + let mut buffer = vec![0u8; bpp]; + while current_pixel < num_pixels { + let mut chunk_header = [0u8; 1]; + file.read_exact(&mut chunk_header)?; + let [mut chunk_header] = chunk_header; + if chunk_header < 128 { + chunk_header += 1; + for _ in 0..chunk_header { + file.read_exact(&mut buffer)?; + for &b in buffer.iter() { + self.data[current_byte] = b; + current_byte += 1; + } + current_pixel += 1; + if current_pixel > num_pixels { + return Err("too many pixels read".into()); + } + } + } else { + chunk_header -= 127; + file.read_exact(&mut buffer)?; + for _ in 0..chunk_header { + for &b in buffer.iter() { + self.data[current_byte] = b; + current_byte += 1; + } + current_pixel += 1; + if current_pixel > num_pixels { + return Err("too many pixels read".into()); + } + } + } + } + + Ok(()) + } + + fn write_rle_file(&mut self, file: &mut File) -> IoResult { + let max_chunk_len = 128; + let num_pixels = (self.width * self.height) as usize; + let mut current_pixel = 0; + + let bpp = self.bytes_per_pixel() as usize; + + while current_pixel < num_pixels { + let chunk_start = current_pixel * bpp; + let mut current_byte = current_pixel * bpp; + + let mut run_length = 1; + let mut raw = true; + + while (current_pixel + run_length) < num_pixels && run_length < max_chunk_len { + let mut succ_eq = true; + let mut t = 0; + while succ_eq && t < bpp { + succ_eq = self.data[current_byte + t] == self.data[current_byte + t + bpp]; + t += 1; + } + current_byte += bpp; + if 1 == run_length { + raw = !succ_eq; + } + if raw && succ_eq { + run_length -= 1; + break; + } + if !(raw || succ_eq) { + break; + } + run_length += 1; + } + current_pixel += run_length; + let mut out = if raw { + [run_length as u8 - 1] + } else { + [run_length as u8 + 127] + }; + + file.write_all(&mut out)?; + let chunk_end = if raw { run_length * bpp } else { bpp }; + file.write_all(&mut self.data[chunk_start..chunk_end])?; + } + + Ok(()) } }