From 450bd3012c8ae83bfcb4fc19926e8a92c9b0cdbf Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Thu, 23 Mar 2023 17:04:56 +0100 Subject: [PATCH 01/46] Mark old functions as deprecated --- src/decoders.rs | 1 + src/encoders.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/decoders.rs b/src/decoders.rs index 47a24a91..7e9e7d76 100644 --- a/src/decoders.rs +++ b/src/decoders.rs @@ -14,6 +14,7 @@ use rgb::{FromSlice, RGBA8}; /// This function will panic if file has no extension /// /// TODO: Return error if file has no extension +#[deprecated(since = "0.2.0", note = "use the Decoder struct")] pub fn decode_image(path: &path::PathBuf) -> Result<(Vec, usize, usize), Box> { let input_format = path.extension().unwrap(); let decoded = match input_format.to_str() { diff --git a/src/encoders.rs b/src/encoders.rs index 410e7baa..0a99bfe0 100644 --- a/src/encoders.rs +++ b/src/encoders.rs @@ -18,6 +18,7 @@ use rgb::{ComponentBytes, RGBA8}; /// This function will panic if Error occurs in encode functions /// /// TODO: Return error if inner functions returns error +#[deprecated(since = "0.2.0", note = "use the Encoder struct")] pub fn encode_image( path: &path::PathBuf, pixels: &[RGBA8], From 913f549df893ed12dca70ba9a6aec33393297133 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Thu, 23 Mar 2023 17:05:17 +0100 Subject: [PATCH 02/46] Added base decoding functions --- CHANGELOG.md | 7 ++ src/lib.rs | 228 +++++++++++++++++++++++++++++++++++++++++++++++++-- src/main.rs | 20 ++++- 3 files changed, 249 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab7ee5a1..a7a6d887 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## v0.2.0 + +- [Added] struct `ImageData` for storing images +- [Added] struct `DecodingError` to process errors +- [Added] struct `Decoder` to decode images +- [Changed] `decoders::decode_image` and `encoders::encode_image` now deprecated, use `Decoder` and `Encoder` structs instead + ## v0.1.3 - [Bugfix] Fixed long processing of png images diff --git a/src/lib.rs b/src/lib.rs index fc322f07..db9bf2da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,17 @@ //! `rimage` is CLI tool that compress multiple images at once. //! Also it provides lib crate with functions to decode and encode images -use clap::Parser; +use std::{ + fmt, fs, io, panic, + path::{self, PathBuf}, +}; + +use clap::{Parser, Subcommand}; + +/// Decoders for images +pub mod decoders; +/// Encoders for images +pub mod encoders; /// Config from command line input #[derive(Parser, Debug)] @@ -18,9 +28,217 @@ pub struct Config { /// Format of output images #[arg(short, long, default_value_t = String::from("jpg"))] pub output_format: String, + + #[command(subcommand)] + pub command: Commands, } -/// Decoders for images -pub mod decoders; -/// Encoders for images -pub mod encoders; +#[derive(Subcommand, Debug)] +pub enum Commands { + Info { input: Vec }, +} + +pub struct ImageData { + color_space: mozjpeg::ColorSpace, + width: usize, + height: usize, + data: Vec, +} + +pub struct Decoder<'a> { + path: &'a path::PathBuf, + raw_data: Vec, +} + +#[derive(Debug)] +pub enum DecodingError { + IoError(io::Error), + Format(String), + Config(ConfigError), + Parsing(String), +} + +#[derive(Debug)] +pub enum ConfigError { + QualityOutOfBounds, + WidthIsZero, + HeightIsZero, + SizeIsZero, + FormatNotSupported(String), +} + +impl<'a> Decoder<'a> { + pub fn build(path: &'a PathBuf) -> Result { + let raw_data = fs::read(path)?; + Ok(Decoder { path, raw_data }) + } + + pub fn decode(&self) -> Result { + let extension = match self.path.extension() { + Some(ext) => ext, + None => return Err(DecodingError::Format("None".to_string())), + }; + + match extension.to_str() { + Some("jpg") | Some("jpeg") => self.decode_jpeg(), + Some("png") => self.decode_png(), + Some(ext) => return Err(DecodingError::Format(ext.to_string())), + None => { + return Err(DecodingError::Parsing( + "Extension is not valid unicode".to_string(), + )) + } + } + } + + fn decode_jpeg(&self) -> Result { + panic::catch_unwind(|| -> Result { + let d = mozjpeg::Decompress::new_mem(&self.raw_data)?; + let color_space = d.color_space(); + let mut image = match color_space { + mozjpeg::ColorSpace::JCS_UNKNOWN => { + return Err(DecodingError::Parsing("Unknown color space".to_string())) + } + mozjpeg::ColorSpace::JCS_GRAYSCALE => d.grayscale(), + mozjpeg::ColorSpace::JCS_EXT_RGBA => d.rgba(), + mozjpeg::ColorSpace::JCS_EXT_BGRA => d.rgba(), + mozjpeg::ColorSpace::JCS_EXT_ABGR => d.rgba(), + mozjpeg::ColorSpace::JCS_EXT_ARGB => d.rgba(), + _ => d.rgb(), + }?; + + let width = image.width(); + let height = image.height(); + + Ok(ImageData { + color_space, + width, + height, + data: image.read_scanlines_flat().unwrap(), + }) + }) + .unwrap_or(Err(DecodingError::Parsing( + "Failed to decode jpeg".to_string(), + ))) + } + + fn decode_png(&self) -> Result { + let d = png::Decoder::new(fs::File::open(self.path)?); + let mut reader = d.read_info()?; + let mut buf = vec![0; reader.output_buffer_size()]; + let info = reader.next_frame(&mut buf)?; + let data = buf[..info.buffer_size()].to_vec(); + let color_space = match info.color_type { + png::ColorType::Grayscale => mozjpeg::ColorSpace::JCS_GRAYSCALE, + png::ColorType::Rgb => mozjpeg::ColorSpace::JCS_RGB, + png::ColorType::Indexed => mozjpeg::ColorSpace::JCS_UNKNOWN, + png::ColorType::GrayscaleAlpha => mozjpeg::ColorSpace::JCS_GRAYSCALE, + png::ColorType::Rgba => mozjpeg::ColorSpace::JCS_EXT_RGBA, + }; + Ok(ImageData { + color_space: color_space, + width: info.width as usize, + height: info.height as usize, + data, + }) + } +} + +impl ImageData { + pub fn size(&self) -> (usize, usize) { + (self.width, self.height) + } + pub fn color_space(&self) -> mozjpeg::ColorSpace { + self.color_space + } +} + +impl From for DecodingError { + fn from(err: io::Error) -> Self { + DecodingError::IoError(err) + } +} + +impl From for DecodingError { + fn from(err: png::DecodingError) -> Self { + match err { + png::DecodingError::IoError(io_err) => DecodingError::IoError(io_err), + png::DecodingError::Format(f_err) => DecodingError::Format(f_err.to_string()), + png::DecodingError::Parameter(p_err) => DecodingError::Parsing(p_err.to_string()), + png::DecodingError::LimitsExceeded => { + DecodingError::Parsing("Png limits exceeded".to_string()) + } + } + } +} + +impl fmt::Display for DecodingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DecodingError::IoError(io_err) => f.write_fmt(format_args!("{}", io_err)), + DecodingError::Format(fmt_err) => f.write_str(fmt_err), + DecodingError::Config(cfg_err) => f.write_fmt(format_args!("{}", cfg_err)), + DecodingError::Parsing(prs_err) => f.write_str(prs_err), + } + } +} + +impl fmt::Display for ConfigError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ConfigError::QualityOutOfBounds => f.write_str("Quality is out of bounds"), + ConfigError::WidthIsZero => f.write_str("Width is cannot be zero"), + ConfigError::HeightIsZero => f.write_str("Height is cannot be zero"), + ConfigError::SizeIsZero => f.write_str("Size is cannot be zero"), + ConfigError::FormatNotSupported(ext) => { + f.write_fmt(format_args!("{} is not supported", ext)) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn display_decoder_error() { + assert_eq!( + DecodingError::IoError(io::Error::new(io::ErrorKind::NotFound, "path not found")) + .to_string(), + "path not found" + ); + assert_eq!( + DecodingError::Format("WebP not supported".to_string()).to_string(), + "WebP not supported" + ); + assert_eq!( + DecodingError::Config(ConfigError::QualityOutOfBounds).to_string(), + "Quality is out of bounds" + ) + } + + #[test] + fn display_config_error() { + assert_eq!( + ConfigError::QualityOutOfBounds.to_string(), + "Quality is out of bounds" + ); + assert_eq!( + ConfigError::WidthIsZero.to_string(), + "Width is cannot be zero" + ); + assert_eq!( + ConfigError::HeightIsZero.to_string(), + "Height is cannot be zero" + ); + assert_eq!( + ConfigError::SizeIsZero.to_string(), + "Size is cannot be zero" + ); + assert_eq!( + ConfigError::FormatNotSupported("webp".to_string()).to_string(), + "webp is not supported" + ) + } +} diff --git a/src/main.rs b/src/main.rs index 2fa4c1e7..b189ef2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,13 @@ +use std::process; + use clap::Parser; use indicatif::{ProgressBar, ProgressStyle}; -use rimage::{decoders, encoders, Config}; +use rimage::{decoders, encoders, Commands, Config, Decoder}; fn main() { let conf = Config::parse_from(wild::args_os()); let pb = ProgressBar::new(conf.input.len() as u64); + pb.set_style( ProgressStyle::with_template("{bar:40.green/blue} {pos}/{len} {wide_msg}") .unwrap() @@ -12,6 +15,21 @@ fn main() { ); pb.set_position(0); + match conf.command { + Commands::Info { input } => { + for path in input { + let d = Decoder::build(&path).unwrap(); + let img_data = d.decode().unwrap(); + + println!("Path: {:?}", path); + println!("WxH: {:?}", img_data.size()); + println!("Color space: {:?}", img_data.color_space()); + println!(""); + } + process::exit(0); + } + } + for path in conf.input { pb.set_message(path.file_name().unwrap().to_str().unwrap().to_owned()); pb.inc(1); From fbecc356e9c49d90ae885ffd3f1b133f2306546c Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Thu, 23 Mar 2023 17:06:02 +0100 Subject: [PATCH 03/46] Update tests folder --- test/test.bmp | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test/test.bmp diff --git a/test/test.bmp b/test/test.bmp deleted file mode 100644 index e69de29b..00000000 From 3ae5109eab536688fe710c18461199abda2ce904 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Thu, 23 Mar 2023 17:29:29 +0100 Subject: [PATCH 04/46] Added custom color scheme --- src/lib.rs | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index db9bf2da..27eb0d13 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,7 +39,7 @@ pub enum Commands { } pub struct ImageData { - color_space: mozjpeg::ColorSpace, + color_space: ColorScheme, width: usize, height: usize, data: Vec, @@ -67,6 +67,15 @@ pub enum ConfigError { FormatNotSupported(String), } +#[derive(Debug)] +pub enum ColorScheme { + RGB, + RGBA, + Indexed, + Grayscale, + GrayscaleAlpha, +} + impl<'a> Decoder<'a> { pub fn build(path: &'a PathBuf) -> Result { let raw_data = fs::read(path)?; @@ -94,8 +103,15 @@ impl<'a> Decoder<'a> { fn decode_jpeg(&self) -> Result { panic::catch_unwind(|| -> Result { let d = mozjpeg::Decompress::new_mem(&self.raw_data)?; - let color_space = d.color_space(); - let mut image = match color_space { + let color_space = match d.color_space() { + mozjpeg::ColorSpace::JCS_GRAYSCALE => ColorScheme::Grayscale, + mozjpeg::ColorSpace::JCS_EXT_RGBA => ColorScheme::RGBA, + mozjpeg::ColorSpace::JCS_EXT_BGRA => ColorScheme::RGBA, + mozjpeg::ColorSpace::JCS_EXT_ABGR => ColorScheme::RGBA, + mozjpeg::ColorSpace::JCS_EXT_ARGB => ColorScheme::RGBA, + _ => ColorScheme::RGB, + }; + let mut image = match d.color_space() { mozjpeg::ColorSpace::JCS_UNKNOWN => { return Err(DecodingError::Parsing("Unknown color space".to_string())) } @@ -129,14 +145,14 @@ impl<'a> Decoder<'a> { let info = reader.next_frame(&mut buf)?; let data = buf[..info.buffer_size()].to_vec(); let color_space = match info.color_type { - png::ColorType::Grayscale => mozjpeg::ColorSpace::JCS_GRAYSCALE, - png::ColorType::Rgb => mozjpeg::ColorSpace::JCS_RGB, - png::ColorType::Indexed => mozjpeg::ColorSpace::JCS_UNKNOWN, - png::ColorType::GrayscaleAlpha => mozjpeg::ColorSpace::JCS_GRAYSCALE, - png::ColorType::Rgba => mozjpeg::ColorSpace::JCS_EXT_RGBA, + png::ColorType::Grayscale => ColorScheme::Grayscale, + png::ColorType::Rgb => ColorScheme::RGB, + png::ColorType::Indexed => ColorScheme::Indexed, + png::ColorType::GrayscaleAlpha => ColorScheme::GrayscaleAlpha, + png::ColorType::Rgba => ColorScheme::RGBA, }; Ok(ImageData { - color_space: color_space, + color_space, width: info.width as usize, height: info.height as usize, data, @@ -148,8 +164,11 @@ impl ImageData { pub fn size(&self) -> (usize, usize) { (self.width, self.height) } - pub fn color_space(&self) -> mozjpeg::ColorSpace { - self.color_space + pub fn color_space(&self) -> &ColorScheme { + &self.color_space + } + pub fn data_len(&self) -> usize { + self.data.len() } } From 3562eb79027e32d39639a4aaf6cb902d7526fda9 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Thu, 23 Mar 2023 17:30:07 +0100 Subject: [PATCH 05/46] Added info command error handling --- src/main.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index b189ef2f..052a436e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,12 +18,25 @@ fn main() { match conf.command { Commands::Info { input } => { for path in input { - let d = Decoder::build(&path).unwrap(); - let img_data = d.decode().unwrap(); + let d = match Decoder::build(&path) { + Ok(dec) => dec, + Err(e) => { + println!("Error: {e}"); + continue; + } + }; + let img_data = match d.decode() { + Ok(img_d) => img_d, + Err(e) => { + println!("Error: {e}"); + continue; + } + }; println!("Path: {:?}", path); println!("WxH: {:?}", img_data.size()); println!("Color space: {:?}", img_data.color_space()); + println!("Bytes: {}", img_data.data_len()); println!(""); } process::exit(0); From 9eb29261f9ce5f46c6b5496f0918735a15e1d389 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Thu, 23 Mar 2023 17:30:22 +0100 Subject: [PATCH 06/46] Added png test cases From 5730b4e37ff915da28d44600d1cd084517737c43 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Thu, 23 Mar 2023 23:46:27 +0100 Subject: [PATCH 07/46] Added test cases for Decoder --- Cargo.lock | 18 +++++ Cargo.toml | 3 +- src/decoders.rs | 21 ++---- src/encoders.rs | 12 ++-- src/lib.rs | 153 ++++++++++++++++++++++++++++++++++++++++--- src/main.rs | 49 +++++--------- tests/files/test.bmp | 0 7 files changed, 190 insertions(+), 66 deletions(-) create mode 100644 tests/files/test.bmp diff --git a/Cargo.lock b/Cargo.lock index b4939684..b2b7165a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + [[package]] name = "anes" version = "0.1.6" @@ -548,6 +557,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + [[package]] name = "memoffset" version = "0.8.0" @@ -815,6 +830,8 @@ version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cce168fea28d3e05f158bda4576cf0c844d5045bc2cc3620fa0292ed5bb5814c" dependencies = [ + "aho-corasick", + "memchr", "regex-syntax", ] @@ -844,6 +861,7 @@ dependencies = [ "mozjpeg", "oxipng", "png", + "regex", "rgb", "wild", ] diff --git a/Cargo.toml b/Cargo.toml index 9ee53a2a..ab941d8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ wild = "2.1.0" [dev-dependencies] criterion = { version = "0.4.0", features = ["html_reports"] } +regex = "1.7.2" [[bench]] name = "decode_large_jpg" @@ -43,4 +44,4 @@ harness = false [[bench]] name = "encode_large_png" -harness = false \ No newline at end of file +harness = false diff --git a/src/decoders.rs b/src/decoders.rs index 7e9e7d76..09ba4317 100644 --- a/src/decoders.rs +++ b/src/decoders.rs @@ -92,57 +92,50 @@ mod tests { #[test] fn test_decode_image_jpg() { - let file_path = PathBuf::from("test/test.jpg"); + let file_path = PathBuf::from("tests/files/test.jpg"); let result = decode_image(&file_path); - println!("{result:?}"); assert!(result.is_ok()); } #[test] fn test_decode_image_png() { - let file_path = PathBuf::from("test/test.png"); + let file_path = PathBuf::from("tests/files/test.png"); let result = decode_image(&file_path); - println!("{result:?}"); assert!(result.is_ok()); } #[test] fn test_decode_image_unsupported() { - let file_path = PathBuf::from("test/test.bmp"); + let file_path = PathBuf::from("tests/files/test.bmp"); let result = decode_image(&file_path); - println!("{result:?}"); assert!(result.is_err()); } #[test] fn test_decode_jpeg() { - let file_path = PathBuf::from("test/test.jpg"); + let file_path = PathBuf::from("tests/files/test.jpg"); let result = decode_jpeg(&file_path); - println!("{result:?}"); assert!(result.is_ok()); } #[test] fn test_decode_jpeg_invalid() { - let file_path = PathBuf::from("test/invalid.jpg"); + let file_path = PathBuf::from("tests/files/invalid.jpg"); let result = decode_jpeg(&file_path); - println!("{result:?}"); assert!(result.is_err()); } #[test] fn test_decode_png() { - let file_path = PathBuf::from("test/test.png"); + let file_path = PathBuf::from("tests/files/test.png"); let result = decode_png(&file_path); - println!("{result:?}"); assert!(result.is_ok()); } #[test] fn test_decode_png_invalid() { - let file_path = PathBuf::from("test/invalid.png"); + let file_path = PathBuf::from("tests/files/invalid.png"); let result = decode_png(&file_path); - println!("{result:?}"); assert!(result.is_err()); } } diff --git a/src/encoders.rs b/src/encoders.rs index 0a99bfe0..5a448e46 100644 --- a/src/encoders.rs +++ b/src/encoders.rs @@ -93,7 +93,7 @@ mod tests { #[test] fn test_encode_jpeg() { let (pixels, width, height) = - decoders::decode_image(&PathBuf::from("test/encode_test.png")).unwrap(); + decoders::decode_image(&PathBuf::from("tests/files/encode_test.png")).unwrap(); let quality = 0.8; let result = encode_jpeg(&pixels, width, height, quality); @@ -106,10 +106,9 @@ mod tests { #[test] fn test_encode_png() { let (pixels, width, height) = - decoders::decode_image(&PathBuf::from("test/encode_test.png")).unwrap(); + decoders::decode_image(&PathBuf::from("tests/files/encode_test.png")).unwrap(); let result = encode_png(&pixels, width, height); - println!("{result:?}"); assert!(result.is_ok()); let encoded_bytes = result.unwrap(); @@ -120,10 +119,9 @@ mod tests { #[ignore = "Too long"] fn test_encode_transparent_png() { let (pixels, width, height) = - decoders::decode_image(&PathBuf::from("test/test_transparent.png")).unwrap(); + decoders::decode_image(&PathBuf::from("tests/files/test_transparent.png")).unwrap(); let result = encode_png(&pixels, width, height); - println!("{result:?}"); assert!(result.is_ok()); let encoded_bytes = result.unwrap(); @@ -133,18 +131,16 @@ mod tests { #[test] fn test_encode_image() { let (pixels, width, height) = - decoders::decode_image(&PathBuf::from("test/encode_test.png")).unwrap(); + decoders::decode_image(&PathBuf::from("tests/files/encode_test.png")).unwrap(); let quality = 0.8; let path = PathBuf::from("test.jpg"); let result = encode_image(&path, &pixels, "jpg", width, height, quality); - println!("{result:?}"); assert!(result.is_ok()); assert!(fs::remove_file(path).is_ok()); let path = PathBuf::from("test.png"); let result = encode_image(&path, &pixels, "png", width, height, quality); - println!("{result:?}"); assert!(result.is_ok()); assert!(fs::remove_file(path).is_ok()); } diff --git a/src/lib.rs b/src/lib.rs index 27eb0d13..8331f82f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ use std::{ path::{self, PathBuf}, }; -use clap::{Parser, Subcommand}; +use clap::Parser; /// Decoders for images pub mod decoders; @@ -28,14 +28,6 @@ pub struct Config { /// Format of output images #[arg(short, long, default_value_t = String::from("jpg"))] pub output_format: String, - - #[command(subcommand)] - pub command: Commands, -} - -#[derive(Subcommand, Debug)] -pub enum Commands { - Info { input: Vec }, } pub struct ImageData { @@ -67,7 +59,7 @@ pub enum ConfigError { FormatNotSupported(String), } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum ColorScheme { RGB, RGBA, @@ -218,6 +210,8 @@ impl fmt::Display for ConfigError { #[cfg(test)] mod tests { + use regex::Regex; + use super::*; #[test] @@ -260,4 +254,143 @@ mod tests { "webp is not supported" ) } + + #[test] + fn decode_grayscale() { + let files: Vec = fs::read_dir("tests/files/") + .unwrap() + .map(|entry| { + let entry = entry.unwrap(); + entry.path() + }) + .filter(|path| { + let re = Regex::new(r"^tests/files/[^x].+0g\d\d\.png$").unwrap(); + re.is_match(path.to_str().unwrap_or("")) + }) + .collect(); + + files.iter().for_each(|path| { + let image = Decoder::build(path).unwrap().decode().unwrap(); + + assert_eq!(image.color_space(), &ColorScheme::Grayscale); + assert_ne!(image.data_len(), 0); + assert_ne!(image.size(), (0, 0)); + }) + } + + #[test] + fn decode_grayscale_alpha() { + let files: Vec = fs::read_dir("tests/files/") + .unwrap() + .map(|entry| { + let entry = entry.unwrap(); + entry.path() + }) + .filter(|path| { + let re = Regex::new(r"^tests/files/[^x].+4a\d\d\.png$").unwrap(); + re.is_match(path.to_str().unwrap_or("")) + }) + .collect(); + + files.iter().for_each(|path| { + let image = Decoder::build(path).unwrap().decode().unwrap(); + + assert_eq!(image.color_space(), &ColorScheme::GrayscaleAlpha); + assert_ne!(image.data_len(), 0); + assert_ne!(image.size(), (0, 0)); + }) + } + + #[test] + fn decode_rgb() { + let files: Vec = fs::read_dir("tests/files/") + .unwrap() + .map(|entry| { + let entry = entry.unwrap(); + entry.path() + }) + .filter(|path| { + let re = Regex::new(r"^tests/files/[^x].+2c\d\d\.png$").unwrap(); + re.is_match(path.to_str().unwrap_or("")) + }) + .collect(); + + files.iter().for_each(|path| { + let image = Decoder::build(path).unwrap().decode().unwrap(); + + assert_eq!(image.color_space(), &ColorScheme::RGB); + assert_ne!(image.data_len(), 0); + assert_ne!(image.size(), (0, 0)); + }) + } + + #[test] + fn decode_rgba() { + let files: Vec = fs::read_dir("tests/files/") + .unwrap() + .map(|entry| { + let entry = entry.unwrap(); + entry.path() + }) + .filter(|path| { + let re = Regex::new(r"^tests/files/[^x].+6a\d\d\.png$").unwrap(); + re.is_match(path.to_str().unwrap_or("")) + }) + .collect(); + + files.iter().for_each(|path| { + let image = Decoder::build(path).unwrap().decode().unwrap(); + + assert_eq!(image.color_space(), &ColorScheme::RGBA); + assert_ne!(image.data_len(), 0); + assert_ne!(image.size(), (0, 0)); + }) + } + + #[test] + fn decode_indexed() { + let files: Vec = fs::read_dir("tests/files/") + .unwrap() + .map(|entry| { + let entry = entry.unwrap(); + entry.path() + }) + .filter(|path| { + let re = Regex::new(r"^tests/files/[^x].+3p\d\d\.png$").unwrap(); + re.is_match(path.to_str().unwrap_or("")) + }) + .collect(); + + files.iter().for_each(|path| { + let image = Decoder::build(path).unwrap().decode().unwrap(); + + assert_eq!(image.color_space(), &ColorScheme::Indexed); + assert_ne!(image.data_len(), 0); + assert_ne!(image.size(), (0, 0)); + }) + } + + #[test] + #[should_panic] + fn decode_corrupted() { + let files: Vec = fs::read_dir("tests/files/") + .unwrap() + .map(|entry| { + let entry = entry.unwrap(); + entry.path() + }) + .filter(|path| { + let re = Regex::new(r"^tests/files/x.+0g\d\d\.png$").unwrap(); + re.is_match(path.to_str().unwrap_or("")) + }) + .collect(); + + files.iter().for_each(|path| { + let image = Decoder::build(path).unwrap().decode().unwrap(); + + assert_eq!(image.color_space(), &ColorScheme::Grayscale); + assert_ne!(image.data_len(), 0); + assert_ne!(image.size(), (0, 0)); + }) + } } diff --git a/src/main.rs b/src/main.rs index 052a436e..be885ab2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use std::process; use clap::Parser; use indicatif::{ProgressBar, ProgressStyle}; -use rimage::{decoders, encoders, Commands, Config, Decoder}; +use rimage::{decoders, encoders, Config}; fn main() { let conf = Config::parse_from(wild::args_os()); @@ -15,49 +15,32 @@ fn main() { ); pb.set_position(0); - match conf.command { - Commands::Info { input } => { - for path in input { - let d = match Decoder::build(&path) { - Ok(dec) => dec, - Err(e) => { - println!("Error: {e}"); - continue; - } - }; - let img_data = match d.decode() { - Ok(img_d) => img_d, - Err(e) => { - println!("Error: {e}"); - continue; - } - }; - - println!("Path: {:?}", path); - println!("WxH: {:?}", img_data.size()); - println!("Color space: {:?}", img_data.color_space()); - println!("Bytes: {}", img_data.data_len()); - println!(""); - } - process::exit(0); - } - } - for path in conf.input { pb.set_message(path.file_name().unwrap().to_str().unwrap().to_owned()); pb.inc(1); - let (pixels, width, height) = decoders::decode_image(&path).unwrap(); + let (pixels, width, height) = match decoders::decode_image(&path) { + Ok(data) => data, + Err(e) => { + println!("{e}"); + continue; + } + }; - encoders::encode_image( + match encoders::encode_image( &path, &pixels, &conf.output_format, width, height, conf.quality, - ) - .unwrap(); + ) { + Ok(()) => (), + Err(e) => { + println!("{e}"); + continue; + } + }; } pb.finish(); } diff --git a/tests/files/test.bmp b/tests/files/test.bmp new file mode 100644 index 00000000..e69de29b From d82ea0deb8ac061701e8ed98a2a23a1ecdae3dfa Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Thu, 23 Mar 2023 23:52:22 +0100 Subject: [PATCH 08/46] Fixed warns --- src/decoders.rs | 3 +++ src/encoders.rs | 6 ++++++ src/lib.rs | 4 ++-- src/main.rs | 2 -- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/decoders.rs b/src/decoders.rs index 09ba4317..7178136a 100644 --- a/src/decoders.rs +++ b/src/decoders.rs @@ -93,6 +93,7 @@ mod tests { #[test] fn test_decode_image_jpg() { let file_path = PathBuf::from("tests/files/test.jpg"); + #[allow(deprecated)] let result = decode_image(&file_path); assert!(result.is_ok()); } @@ -100,6 +101,7 @@ mod tests { #[test] fn test_decode_image_png() { let file_path = PathBuf::from("tests/files/test.png"); + #[allow(deprecated)] let result = decode_image(&file_path); assert!(result.is_ok()); } @@ -107,6 +109,7 @@ mod tests { #[test] fn test_decode_image_unsupported() { let file_path = PathBuf::from("tests/files/test.bmp"); + #[allow(deprecated)] let result = decode_image(&file_path); assert!(result.is_err()); } diff --git a/src/encoders.rs b/src/encoders.rs index 5a448e46..2532352d 100644 --- a/src/encoders.rs +++ b/src/encoders.rs @@ -92,6 +92,7 @@ mod tests { #[test] fn test_encode_jpeg() { + #[allow(deprecated)] let (pixels, width, height) = decoders::decode_image(&PathBuf::from("tests/files/encode_test.png")).unwrap(); let quality = 0.8; @@ -105,6 +106,7 @@ mod tests { #[test] fn test_encode_png() { + #[allow(deprecated)] let (pixels, width, height) = decoders::decode_image(&PathBuf::from("tests/files/encode_test.png")).unwrap(); @@ -118,6 +120,7 @@ mod tests { #[test] #[ignore = "Too long"] fn test_encode_transparent_png() { + #[allow(deprecated)] let (pixels, width, height) = decoders::decode_image(&PathBuf::from("tests/files/test_transparent.png")).unwrap(); @@ -130,16 +133,19 @@ mod tests { #[test] fn test_encode_image() { + #[allow(deprecated)] let (pixels, width, height) = decoders::decode_image(&PathBuf::from("tests/files/encode_test.png")).unwrap(); let quality = 0.8; let path = PathBuf::from("test.jpg"); + #[allow(deprecated)] let result = encode_image(&path, &pixels, "jpg", width, height, quality); assert!(result.is_ok()); assert!(fs::remove_file(path).is_ok()); let path = PathBuf::from("test.png"); + #[allow(deprecated)] let result = encode_image(&path, &pixels, "png", width, height, quality); assert!(result.is_ok()); assert!(fs::remove_file(path).is_ok()); diff --git a/src/lib.rs b/src/lib.rs index 8331f82f..7d263345 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,9 +83,9 @@ impl<'a> Decoder<'a> { match extension.to_str() { Some("jpg") | Some("jpeg") => self.decode_jpeg(), Some("png") => self.decode_png(), - Some(ext) => return Err(DecodingError::Format(ext.to_string())), + Some(ext) => Err(DecodingError::Format(ext.to_string())), None => { - return Err(DecodingError::Parsing( + Err(DecodingError::Parsing( "Extension is not valid unicode".to_string(), )) } diff --git a/src/main.rs b/src/main.rs index be885ab2..2a09d3d2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,3 @@ -use std::process; - use clap::Parser; use indicatif::{ProgressBar, ProgressStyle}; use rimage::{decoders, encoders, Config}; From 4d1cf19062ae9c3bdc0a66d8b9f2f094095fd554 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Fri, 24 Mar 2023 15:19:24 +0100 Subject: [PATCH 09/46] Removed tests from deprecated modules --- src/decoders.rs | 58 ----------------------------------------- src/encoders.rs | 68 ------------------------------------------------- 2 files changed, 126 deletions(-) diff --git a/src/decoders.rs b/src/decoders.rs index 7178136a..ea181b10 100644 --- a/src/decoders.rs +++ b/src/decoders.rs @@ -84,61 +84,3 @@ fn decode_png(path: &path::PathBuf) -> Result<(Vec, usize, usize), io::Er }; Ok((pixels, info.width as usize, info.height as usize)) } - -#[cfg(test)] -mod tests { - use super::*; - use std::path::PathBuf; - - #[test] - fn test_decode_image_jpg() { - let file_path = PathBuf::from("tests/files/test.jpg"); - #[allow(deprecated)] - let result = decode_image(&file_path); - assert!(result.is_ok()); - } - - #[test] - fn test_decode_image_png() { - let file_path = PathBuf::from("tests/files/test.png"); - #[allow(deprecated)] - let result = decode_image(&file_path); - assert!(result.is_ok()); - } - - #[test] - fn test_decode_image_unsupported() { - let file_path = PathBuf::from("tests/files/test.bmp"); - #[allow(deprecated)] - let result = decode_image(&file_path); - assert!(result.is_err()); - } - - #[test] - fn test_decode_jpeg() { - let file_path = PathBuf::from("tests/files/test.jpg"); - let result = decode_jpeg(&file_path); - assert!(result.is_ok()); - } - - #[test] - fn test_decode_jpeg_invalid() { - let file_path = PathBuf::from("tests/files/invalid.jpg"); - let result = decode_jpeg(&file_path); - assert!(result.is_err()); - } - - #[test] - fn test_decode_png() { - let file_path = PathBuf::from("tests/files/test.png"); - let result = decode_png(&file_path); - assert!(result.is_ok()); - } - - #[test] - fn test_decode_png_invalid() { - let file_path = PathBuf::from("tests/files/invalid.png"); - let result = decode_png(&file_path); - assert!(result.is_err()); - } -} diff --git a/src/encoders.rs b/src/encoders.rs index 2532352d..82740f42 100644 --- a/src/encoders.rs +++ b/src/encoders.rs @@ -83,71 +83,3 @@ fn encode_png( let opts = oxipng::Options::from_preset(2); Ok(oxipng::optimize_from_memory(buf.get_ref(), &opts)?) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::decoders; - use std::path::PathBuf; - - #[test] - fn test_encode_jpeg() { - #[allow(deprecated)] - let (pixels, width, height) = - decoders::decode_image(&PathBuf::from("tests/files/encode_test.png")).unwrap(); - let quality = 0.8; - - let result = encode_jpeg(&pixels, width, height, quality); - assert!(result.is_ok()); - - let encoded_bytes = result.unwrap(); - assert!(!encoded_bytes.is_empty()); - } - - #[test] - fn test_encode_png() { - #[allow(deprecated)] - let (pixels, width, height) = - decoders::decode_image(&PathBuf::from("tests/files/encode_test.png")).unwrap(); - - let result = encode_png(&pixels, width, height); - assert!(result.is_ok()); - - let encoded_bytes = result.unwrap(); - assert!(!encoded_bytes.is_empty()); - } - - #[test] - #[ignore = "Too long"] - fn test_encode_transparent_png() { - #[allow(deprecated)] - let (pixels, width, height) = - decoders::decode_image(&PathBuf::from("tests/files/test_transparent.png")).unwrap(); - - let result = encode_png(&pixels, width, height); - assert!(result.is_ok()); - - let encoded_bytes = result.unwrap(); - assert!(!encoded_bytes.is_empty()); - } - - #[test] - fn test_encode_image() { - #[allow(deprecated)] - let (pixels, width, height) = - decoders::decode_image(&PathBuf::from("tests/files/encode_test.png")).unwrap(); - let quality = 0.8; - - let path = PathBuf::from("test.jpg"); - #[allow(deprecated)] - let result = encode_image(&path, &pixels, "jpg", width, height, quality); - assert!(result.is_ok()); - assert!(fs::remove_file(path).is_ok()); - - let path = PathBuf::from("test.png"); - #[allow(deprecated)] - let result = encode_image(&path, &pixels, "png", width, height, quality); - assert!(result.is_ok()); - assert!(fs::remove_file(path).is_ok()); - } -} From ef3933c357f611549481cbbcd3124707ed93a7e3 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Fri, 24 Mar 2023 15:19:54 +0100 Subject: [PATCH 10/46] Added more test cases From 8c1bb5caa57380e2a4a7a038ae1d0f4688936b44 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Fri, 24 Mar 2023 15:20:13 +0100 Subject: [PATCH 11/46] Added: BitDepth, more color spaces, test cases for jpeg, info option, image processing from stdio Changed: rewrite decode jpeg function, rewrite decode png function --- src/lib.rs | 103 +++++++++++++++++++++++++++++++--------------------- src/main.rs | 37 ++++++++++++++++--- 2 files changed, 94 insertions(+), 46 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7d263345..f6f7982a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,10 +28,15 @@ pub struct Config { /// Format of output images #[arg(short, long, default_value_t = String::from("jpg"))] pub output_format: String, + + /// Outputs info about image + #[arg(short, long, default_value_t = false)] + pub info: bool, } pub struct ImageData { color_space: ColorScheme, + bit_depth: BitDepth, width: usize, height: usize, data: Vec, @@ -61,13 +66,24 @@ pub enum ConfigError { #[derive(Debug, PartialEq, Eq)] pub enum ColorScheme { - RGB, - RGBA, + Rgb, + Rgba, + Cmyk, Indexed, Grayscale, GrayscaleAlpha, } +#[derive(Debug)] +#[repr(u8)] +pub enum BitDepth { + One = 1, + Two = 2, + Four = 4, + Eight = 8, + Sixteen = 16, +} + impl<'a> Decoder<'a> { pub fn build(path: &'a PathBuf) -> Result { let raw_data = fs::read(path)?; @@ -84,11 +100,9 @@ impl<'a> Decoder<'a> { Some("jpg") | Some("jpeg") => self.decode_jpeg(), Some("png") => self.decode_png(), Some(ext) => Err(DecodingError::Format(ext.to_string())), - None => { - Err(DecodingError::Parsing( - "Extension is not valid unicode".to_string(), - )) - } + None => Err(DecodingError::Parsing( + "Extension is not valid unicode".to_string(), + )), } } @@ -97,31 +111,21 @@ impl<'a> Decoder<'a> { let d = mozjpeg::Decompress::new_mem(&self.raw_data)?; let color_space = match d.color_space() { mozjpeg::ColorSpace::JCS_GRAYSCALE => ColorScheme::Grayscale, - mozjpeg::ColorSpace::JCS_EXT_RGBA => ColorScheme::RGBA, - mozjpeg::ColorSpace::JCS_EXT_BGRA => ColorScheme::RGBA, - mozjpeg::ColorSpace::JCS_EXT_ABGR => ColorScheme::RGBA, - mozjpeg::ColorSpace::JCS_EXT_ARGB => ColorScheme::RGBA, - _ => ColorScheme::RGB, + mozjpeg::ColorSpace::JCS_CMYK => ColorScheme::Cmyk, + mozjpeg::ColorSpace::JCS_RGB => ColorScheme::Rgb, + _ => ColorScheme::Rgb, + }; + let mut image = match d.image()? { + mozjpeg::Format::RGB(img) => img, + mozjpeg::Format::Gray(img) => img, + mozjpeg::Format::CMYK(img) => img, }; - let mut image = match d.color_space() { - mozjpeg::ColorSpace::JCS_UNKNOWN => { - return Err(DecodingError::Parsing("Unknown color space".to_string())) - } - mozjpeg::ColorSpace::JCS_GRAYSCALE => d.grayscale(), - mozjpeg::ColorSpace::JCS_EXT_RGBA => d.rgba(), - mozjpeg::ColorSpace::JCS_EXT_BGRA => d.rgba(), - mozjpeg::ColorSpace::JCS_EXT_ABGR => d.rgba(), - mozjpeg::ColorSpace::JCS_EXT_ARGB => d.rgba(), - _ => d.rgb(), - }?; - - let width = image.width(); - let height = image.height(); Ok(ImageData { color_space, - width, - height, + bit_depth: BitDepth::Eight, + width: image.width(), + height: image.height(), data: image.read_scanlines_flat().unwrap(), }) }) @@ -138,13 +142,14 @@ impl<'a> Decoder<'a> { let data = buf[..info.buffer_size()].to_vec(); let color_space = match info.color_type { png::ColorType::Grayscale => ColorScheme::Grayscale, - png::ColorType::Rgb => ColorScheme::RGB, + png::ColorType::Rgb => ColorScheme::Rgb, png::ColorType::Indexed => ColorScheme::Indexed, png::ColorType::GrayscaleAlpha => ColorScheme::GrayscaleAlpha, - png::ColorType::Rgba => ColorScheme::RGBA, + png::ColorType::Rgba => ColorScheme::Rgba, }; Ok(ImageData { color_space, + bit_depth: info.bit_depth.into(), width: info.width as usize, height: info.height as usize, data, @@ -162,6 +167,9 @@ impl ImageData { pub fn data_len(&self) -> usize { self.data.len() } + pub fn bit_depth(&self) -> &BitDepth { + &self.bit_depth + } } impl From for DecodingError { @@ -183,6 +191,18 @@ impl From for DecodingError { } } +impl From for BitDepth { + fn from(bit_depth: png::BitDepth) -> Self { + match bit_depth { + png::BitDepth::One => BitDepth::One, + png::BitDepth::Two => BitDepth::Two, + png::BitDepth::Four => BitDepth::Four, + png::BitDepth::Eight => BitDepth::Eight, + png::BitDepth::Sixteen => BitDepth::Sixteen, + } + } +} + impl fmt::Display for DecodingError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -264,7 +284,7 @@ mod tests { entry.path() }) .filter(|path| { - let re = Regex::new(r"^tests/files/[^x].+0g\d\d\.png$").unwrap(); + let re = Regex::new(r"^tests/files/[^x].+0g\d\d((\.png)|(\.jpg))").unwrap(); re.is_match(path.to_str().unwrap_or("")) }) .collect(); @@ -272,6 +292,8 @@ mod tests { files.iter().for_each(|path| { let image = Decoder::build(path).unwrap().decode().unwrap(); + println!("{path:?}"); + assert_eq!(image.color_space(), &ColorScheme::Grayscale); assert_ne!(image.data_len(), 0); assert_ne!(image.size(), (0, 0)); @@ -287,7 +309,7 @@ mod tests { entry.path() }) .filter(|path| { - let re = Regex::new(r"^tests/files/[^x].+4a\d\d\.png$").unwrap(); + let re = Regex::new(r"^tests/files/[^x].+4a\d\d\.png").unwrap(); re.is_match(path.to_str().unwrap_or("")) }) .collect(); @@ -310,7 +332,7 @@ mod tests { entry.path() }) .filter(|path| { - let re = Regex::new(r"^tests/files/[^x].+2c\d\d\.png$").unwrap(); + let re = Regex::new(r"^^tests/files/[^x].+2c\d\d((\.png)|(\.jpg))").unwrap(); re.is_match(path.to_str().unwrap_or("")) }) .collect(); @@ -318,7 +340,7 @@ mod tests { files.iter().for_each(|path| { let image = Decoder::build(path).unwrap().decode().unwrap(); - assert_eq!(image.color_space(), &ColorScheme::RGB); + assert_eq!(image.color_space(), &ColorScheme::Rgb); assert_ne!(image.data_len(), 0); assert_ne!(image.size(), (0, 0)); }) @@ -341,7 +363,7 @@ mod tests { files.iter().for_each(|path| { let image = Decoder::build(path).unwrap().decode().unwrap(); - assert_eq!(image.color_space(), &ColorScheme::RGBA); + assert_eq!(image.color_space(), &ColorScheme::Rgba); assert_ne!(image.data_len(), 0); assert_ne!(image.size(), (0, 0)); }) @@ -371,7 +393,6 @@ mod tests { } #[test] - #[should_panic] fn decode_corrupted() { let files: Vec = fs::read_dir("tests/files/") .unwrap() @@ -380,17 +401,17 @@ mod tests { entry.path() }) .filter(|path| { - let re = Regex::new(r"^tests/files/x.+0g\d\d\.png$").unwrap(); + let re = Regex::new(r"^tests/files/x.+\d\d\.png$").unwrap(); re.is_match(path.to_str().unwrap_or("")) }) .collect(); files.iter().for_each(|path| { - let image = Decoder::build(path).unwrap().decode().unwrap(); + let d = Decoder::build(path); + assert!(d.is_ok()); - assert_eq!(image.color_space(), &ColorScheme::Grayscale); - assert_ne!(image.data_len(), 0); - assert_ne!(image.size(), (0, 0)); + let img = d.unwrap().decode(); + assert!(img.is_err()); }) } } diff --git a/src/main.rs b/src/main.rs index 2a09d3d2..465350b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,23 @@ +use std::{io, path, process}; + use clap::Parser; use indicatif::{ProgressBar, ProgressStyle}; -use rimage::{decoders, encoders, Config}; +use rimage::{decoders, encoders, Config, Decoder}; fn main() { - let conf = Config::parse_from(wild::args_os()); + let mut conf = Config::parse_from(wild::args_os()); let pb = ProgressBar::new(conf.input.len() as u64); + if conf.input.len() == 0 { + conf.input = io::stdin() + .lines() + .map(|res| { + let input_file = res.unwrap(); + path::PathBuf::from(input_file.trim()) + }) + .collect(); + } + pb.set_style( ProgressStyle::with_template("{bar:40.green/blue} {pos}/{len} {wide_msg}") .unwrap() @@ -13,11 +25,26 @@ fn main() { ); pb.set_position(0); - for path in conf.input { + if conf.info { + for path in &conf.input { + let d = Decoder::build(path).unwrap(); + let img = d.decode().unwrap(); + + println!("{:?}", path.file_name().unwrap()); + println!("Color Space: {:?}", img.color_space()); + println!("Bit Depth: {:?}", img.bit_depth()); + println!("Size: {:?}", img.size()); + println!("Data length: {:?}", img.data_len()); + println!(""); + } + process::exit(0); + } + + for path in &conf.input { pb.set_message(path.file_name().unwrap().to_str().unwrap().to_owned()); pb.inc(1); - let (pixels, width, height) = match decoders::decode_image(&path) { + let (pixels, width, height) = match decoders::decode_image(path) { Ok(data) => data, Err(e) => { println!("{e}"); @@ -26,7 +53,7 @@ fn main() { }; match encoders::encode_image( - &path, + path, &pixels, &conf.output_format, width, From 2906a3789c5e8695920216c0e736d61fd09819a7 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Fri, 24 Mar 2023 15:28:04 +0100 Subject: [PATCH 12/46] Added: From ColorType for ColorScheme Changed: Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ src/lib.rs | 21 +++++++++++++-------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7a6d887..ed4e0aff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ - [Added] struct `ImageData` for storing images - [Added] struct `DecodingError` to process errors - [Added] struct `Decoder` to decode images + +- [Added] image processing from stdio +- [Added] info option + - [Changed] `decoders::decode_image` and `encoders::encode_image` now deprecated, use `Decoder` and `Encoder` structs instead ## v0.1.3 diff --git a/src/lib.rs b/src/lib.rs index f6f7982a..5020e049 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -140,15 +140,8 @@ impl<'a> Decoder<'a> { let mut buf = vec![0; reader.output_buffer_size()]; let info = reader.next_frame(&mut buf)?; let data = buf[..info.buffer_size()].to_vec(); - let color_space = match info.color_type { - png::ColorType::Grayscale => ColorScheme::Grayscale, - png::ColorType::Rgb => ColorScheme::Rgb, - png::ColorType::Indexed => ColorScheme::Indexed, - png::ColorType::GrayscaleAlpha => ColorScheme::GrayscaleAlpha, - png::ColorType::Rgba => ColorScheme::Rgba, - }; Ok(ImageData { - color_space, + color_space: info.color_type.into(), bit_depth: info.bit_depth.into(), width: info.width as usize, height: info.height as usize, @@ -191,6 +184,18 @@ impl From for DecodingError { } } +impl From for ColorScheme { + fn from(color_type: png::ColorType) -> Self { + match color_type { + png::ColorType::Grayscale => ColorScheme::Grayscale, + png::ColorType::Rgb => ColorScheme::Rgb, + png::ColorType::Indexed => ColorScheme::Indexed, + png::ColorType::GrayscaleAlpha => ColorScheme::GrayscaleAlpha, + png::ColorType::Rgba => ColorScheme::Rgba, + } + } +} + impl From for BitDepth { fn from(bit_depth: png::BitDepth) -> Self { match bit_depth { From 4cc5768d50aacac94a7c878f0670a2e195d1ceca Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Fri, 24 Mar 2023 15:34:34 +0100 Subject: [PATCH 13/46] Fixed rgb image to grey From 2564a4232712f8a0f6cee0c77ffde24ac0cd5a5a Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Fri, 24 Mar 2023 15:35:26 +0100 Subject: [PATCH 14/46] Removed unused tests images --- tests/files/test.bmp | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/files/test.bmp diff --git a/tests/files/test.bmp b/tests/files/test.bmp deleted file mode 100644 index e69de29b..00000000 From ec2f7d8b8bfbae456d6dd47b2a9479d33f5256a1 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Sat, 25 Mar 2023 00:05:04 +0100 Subject: [PATCH 15/46] Remove deprecated flag from already deprecated module --- src/decoders.rs | 1 - src/encoders.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/decoders.rs b/src/decoders.rs index ea181b10..330a2677 100644 --- a/src/decoders.rs +++ b/src/decoders.rs @@ -14,7 +14,6 @@ use rgb::{FromSlice, RGBA8}; /// This function will panic if file has no extension /// /// TODO: Return error if file has no extension -#[deprecated(since = "0.2.0", note = "use the Decoder struct")] pub fn decode_image(path: &path::PathBuf) -> Result<(Vec, usize, usize), Box> { let input_format = path.extension().unwrap(); let decoded = match input_format.to_str() { diff --git a/src/encoders.rs b/src/encoders.rs index 82740f42..6958a288 100644 --- a/src/encoders.rs +++ b/src/encoders.rs @@ -18,7 +18,6 @@ use rgb::{ComponentBytes, RGBA8}; /// This function will panic if Error occurs in encode functions /// /// TODO: Return error if inner functions returns error -#[deprecated(since = "0.2.0", note = "use the Encoder struct")] pub fn encode_image( path: &path::PathBuf, pixels: &[RGBA8], From 2b3af34a676ef993a96fa17d474458a558c29a16 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Sat, 25 Mar 2023 00:05:37 +0100 Subject: [PATCH 16/46] Added documentation and changed some names --- src/lib.rs | 202 ++++++++++++++++++++++++++++++++++++++++++++-------- src/main.rs | 2 +- 2 files changed, 173 insertions(+), 31 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5020e049..4aa72853 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,66 @@ -//! # Rimage -//! `rimage` is CLI tool that compress multiple images at once. -//! Also it provides lib crate with functions to decode and encode images +/*! +This crate provides a cli tool and library for image processing. +Similar to [squoosh!](https://squoosh.app/) using same codecs, +but fully written on rust and with bulk processing support. + +Current features: +- Decoding jpeg and png +- Encoding with optimizations +- Get image information + +# Usage + +First add this crate to your dependencies: +```text +cargo add rimage +``` + +or add this to Cargo.toml: +```toml +[dependencies] +rimage = "0.2" +``` + +Next import Decoder: +```text +use rimage::Decoder; +``` + +After that you can use this crate: + +## Decoding + +``` +# use rimage::Decoder; +# let path = std::path::PathBuf::from("tests/files/basi0g01.jpg"); +// Build decoder from file path +let decoder = match Decoder::build(&path) { + Ok(d) => d, + Err(e) => { + eprintln!("Oh no there is error! {e}"); + std::process::exit(1); + } +}; + +// Decode image to image data +let image = match decoder.decode() { + Ok(img) => img, + Err(e) => { + eprintln!("Oh no there is another error! {e}"); + std::process::exit(1); + } +}; + +// Get image data +println!("Color Space: {:?}", image.color_space()); +println!("Bit Depth: {:?}", image.bit_depth()); +println!("Size: {:?}", image.size()); +println!("Data length: {:?}", image.data().len()); + +// Do something with image... +``` +*/ +#![warn(missing_docs)] use std::{ fmt, fs, io, panic, @@ -10,8 +70,10 @@ use std::{ use clap::Parser; /// Decoders for images +#[deprecated(since = "0.2.0", note = "use the Decoder struct instead")] pub mod decoders; /// Encoders for images +#[deprecated(since = "0.2.0", note = "use the Encoder struct instead")] pub mod encoders; /// Config from command line input @@ -29,67 +91,139 @@ pub struct Config { #[arg(short, long, default_value_t = String::from("jpg"))] pub output_format: String, - /// Outputs info about image + /// Outputs info about images #[arg(short, long, default_value_t = false)] pub info: bool, } +/// Image data from decoder +/// +/// # Examples +/// +/// ``` +/// # use rimage::{Decoder, DecodingError}; +/// # use std::path; +/// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); +/// let d = Decoder::build(&path)?; +/// +/// let image = d.decode()?; +/// +/// // Get something from image data +/// println!("Color Space: {:?}", image.color_space()); +/// println!("Bit Depth: {:?}", image.bit_depth()); +/// println!("Size: {:?}", image.size()); +/// println!("Data length: {:?}", image.data().len()); +/// # Ok::<(), DecodingError>(()) +/// ``` pub struct ImageData { - color_space: ColorScheme, + color_space: ColorSpace, bit_depth: BitDepth, width: usize, height: usize, data: Vec, } +/// Decoder used to get image data from file pub struct Decoder<'a> { path: &'a path::PathBuf, raw_data: Vec, } +/// Decoding error #[derive(Debug)] pub enum DecodingError { + /// Error that occurs if file is missing, failed to open or other [`io::Error`] IoError(io::Error), + /// Error that occurs if format of file is not supported Format(String), + /// Error that occurs if [`Config`] is invalid Config(ConfigError), + /// Error that occurs if file failed to decode Parsing(String), } +/// Configuration error #[derive(Debug)] pub enum ConfigError { + /// Error that occurs if quality is less than 0 or greater than 1 QualityOutOfBounds, + /// Error that occurs if width is 0 WidthIsZero, + /// Error that occurs if height is 0 HeightIsZero, + /// Error that occurs if size is 0 SizeIsZero, + /// Error that occurs if output format is not supported FormatNotSupported(String), } +/// Color space of image #[derive(Debug, PartialEq, Eq)] -pub enum ColorScheme { +pub enum ColorSpace { + /// **R**ed/**G**reen/**B**lue Rgb, + /// **R**ed/**G**reen/**B**lue/**A**lpha Rgba, + /// **C**yan/**M**agenta/**Y**ellow/Blac**K** Cmyk, + /// Indexed color palette Indexed, + /// Grayscale Grayscale, + /// Grayscale/Alpha GrayscaleAlpha, } +/// Bit depth of image per pixel #[derive(Debug)] #[repr(u8)] pub enum BitDepth { + /// One bit per pixel One = 1, + /// Two bits per pixel Two = 2, + /// Four bits per pixel Four = 4, + /// Eight bits per pixel Eight = 8, + /// Sixteen bits per pixel Sixteen = 16, } impl<'a> Decoder<'a> { + /// Builds decoder from path + /// + /// # Result + /// + /// - [`Decoder`] if Ok + /// - [`DecodingError`] if + /// - File not found or other `io::Error` + /// - Config is invalid pub fn build(path: &'a PathBuf) -> Result { let raw_data = fs::read(path)?; + Ok(Decoder { path, raw_data }) } + /// Decodes file to ImageData + /// + /// # Result + /// + /// - [`ImageData`] if Ok + /// - [`DecodingError`] if + /// - File is not a image + /// - File extension not supported + /// - File corrupted or in unknown color space + /// + /// # Examples + /// ``` + /// # use rimage::{Decoder, DecodingError}; + /// # use std::path; + /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); + /// let d = Decoder::build(&path)?; + /// let img = d.decode()?; + /// # Ok::<(), DecodingError>(()) + /// ``` pub fn decode(&self) -> Result { let extension = match self.path.extension() { Some(ext) => ext, @@ -110,10 +244,10 @@ impl<'a> Decoder<'a> { panic::catch_unwind(|| -> Result { let d = mozjpeg::Decompress::new_mem(&self.raw_data)?; let color_space = match d.color_space() { - mozjpeg::ColorSpace::JCS_GRAYSCALE => ColorScheme::Grayscale, - mozjpeg::ColorSpace::JCS_CMYK => ColorScheme::Cmyk, - mozjpeg::ColorSpace::JCS_RGB => ColorScheme::Rgb, - _ => ColorScheme::Rgb, + mozjpeg::ColorSpace::JCS_GRAYSCALE => ColorSpace::Grayscale, + mozjpeg::ColorSpace::JCS_CMYK => ColorSpace::Cmyk, + mozjpeg::ColorSpace::JCS_RGB => ColorSpace::Rgb, + _ => ColorSpace::Rgb, }; let mut image = match d.image()? { mozjpeg::Format::RGB(img) => img, @@ -121,12 +255,16 @@ impl<'a> Decoder<'a> { mozjpeg::Format::CMYK(img) => img, }; + let data = image.read_scanlines_flat().ok_or(DecodingError::Parsing( + "Cannot read jpeg scanlines".to_string(), + ))?; + Ok(ImageData { color_space, bit_depth: BitDepth::Eight, width: image.width(), height: image.height(), - data: image.read_scanlines_flat().unwrap(), + data, }) }) .unwrap_or(Err(DecodingError::Parsing( @@ -151,15 +289,19 @@ impl<'a> Decoder<'a> { } impl ImageData { + /// Returns size of image (Width, Height) pub fn size(&self) -> (usize, usize) { (self.width, self.height) } - pub fn color_space(&self) -> &ColorScheme { + /// Returns a ref to color space of image [`ColorSpace`] + pub fn color_space(&self) -> &ColorSpace { &self.color_space } - pub fn data_len(&self) -> usize { - self.data.len() + /// Returns a ref to array of bytes in image + pub fn data(&self) -> &[u8] { + &self.data } + /// Returns a ref to bit depth of image [`BitDepth`] pub fn bit_depth(&self) -> &BitDepth { &self.bit_depth } @@ -184,14 +326,14 @@ impl From for DecodingError { } } -impl From for ColorScheme { +impl From for ColorSpace { fn from(color_type: png::ColorType) -> Self { match color_type { - png::ColorType::Grayscale => ColorScheme::Grayscale, - png::ColorType::Rgb => ColorScheme::Rgb, - png::ColorType::Indexed => ColorScheme::Indexed, - png::ColorType::GrayscaleAlpha => ColorScheme::GrayscaleAlpha, - png::ColorType::Rgba => ColorScheme::Rgba, + png::ColorType::Grayscale => ColorSpace::Grayscale, + png::ColorType::Rgb => ColorSpace::Rgb, + png::ColorType::Indexed => ColorSpace::Indexed, + png::ColorType::GrayscaleAlpha => ColorSpace::GrayscaleAlpha, + png::ColorType::Rgba => ColorSpace::Rgba, } } } @@ -299,8 +441,8 @@ mod tests { println!("{path:?}"); - assert_eq!(image.color_space(), &ColorScheme::Grayscale); - assert_ne!(image.data_len(), 0); + assert_eq!(image.color_space(), &ColorSpace::Grayscale); + assert_ne!(image.data().len(), 0); assert_ne!(image.size(), (0, 0)); }) } @@ -322,8 +464,8 @@ mod tests { files.iter().for_each(|path| { let image = Decoder::build(path).unwrap().decode().unwrap(); - assert_eq!(image.color_space(), &ColorScheme::GrayscaleAlpha); - assert_ne!(image.data_len(), 0); + assert_eq!(image.color_space(), &ColorSpace::GrayscaleAlpha); + assert_ne!(image.data().len(), 0); assert_ne!(image.size(), (0, 0)); }) } @@ -345,8 +487,8 @@ mod tests { files.iter().for_each(|path| { let image = Decoder::build(path).unwrap().decode().unwrap(); - assert_eq!(image.color_space(), &ColorScheme::Rgb); - assert_ne!(image.data_len(), 0); + assert_eq!(image.color_space(), &ColorSpace::Rgb); + assert_ne!(image.data().len(), 0); assert_ne!(image.size(), (0, 0)); }) } @@ -368,8 +510,8 @@ mod tests { files.iter().for_each(|path| { let image = Decoder::build(path).unwrap().decode().unwrap(); - assert_eq!(image.color_space(), &ColorScheme::Rgba); - assert_ne!(image.data_len(), 0); + assert_eq!(image.color_space(), &ColorSpace::Rgba); + assert_ne!(image.data().len(), 0); assert_ne!(image.size(), (0, 0)); }) } @@ -391,8 +533,8 @@ mod tests { files.iter().for_each(|path| { let image = Decoder::build(path).unwrap().decode().unwrap(); - assert_eq!(image.color_space(), &ColorScheme::Indexed); - assert_ne!(image.data_len(), 0); + assert_eq!(image.color_space(), &ColorSpace::Indexed); + assert_ne!(image.data().len(), 0); assert_ne!(image.size(), (0, 0)); }) } diff --git a/src/main.rs b/src/main.rs index 465350b0..8ca5684b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,7 +34,7 @@ fn main() { println!("Color Space: {:?}", img.color_space()); println!("Bit Depth: {:?}", img.bit_depth()); println!("Size: {:?}", img.size()); - println!("Data length: {:?}", img.data_len()); + println!("Data length: {:?}", img.data().len()); println!(""); } process::exit(0); From 69f471418bec81ffddfa35063a1adf191288f4d3 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Sat, 25 Mar 2023 00:06:43 +0100 Subject: [PATCH 17/46] Fixed clippy warn --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 8ca5684b..195de12b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ fn main() { let mut conf = Config::parse_from(wild::args_os()); let pb = ProgressBar::new(conf.input.len() as u64); - if conf.input.len() == 0 { + if conf.input.is_empty() { conf.input = io::stdin() .lines() .map(|res| { From dd18f16fc2a5f6ce395c3e8a289168949666b71a Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Sat, 25 Mar 2023 00:09:27 +0100 Subject: [PATCH 18/46] Updated version to 0.2.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b2b7165a..6525f58d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -852,7 +852,7 @@ dependencies = [ [[package]] name = "rimage" -version = "0.1.3" +version = "0.2.0" dependencies = [ "bytemuck", "clap 4.1.11", diff --git a/Cargo.toml b/Cargo.toml index ab941d8e..657ee581 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ exclude = ["test/*", "images/", ".*"] repository = "https://github.com/SalOne22/rimage" license = "MIT OR Apache-2.0" -version = "0.1.3" +version = "0.2.0" edition = "2021" [profile.release] From 2d3556c328ea3088388d0d5a3af959206f8a3f2f Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Sat, 25 Mar 2023 00:10:04 +0100 Subject: [PATCH 19/46] Updated dependencies --- Cargo.lock | 69 +++++++++++++----------------------------------------- 1 file changed, 16 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6525f58d..1796591f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,12 +52,6 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "bitflags" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1" - [[package]] name = "bitvec" version = "1.0.1" @@ -143,7 +137,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ "atty", - "bitflags 1.3.2", + "bitflags", "clap_lex 0.2.4", "indexmap", "strsim", @@ -153,11 +147,11 @@ dependencies = [ [[package]] name = "clap" -version = "4.1.11" +version = "4.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42dfd32784433290c51d92c438bb72ea5063797fc3cc9a21a8c4346bebbb2098" +checksum = "3c911b090850d79fc64fe9ea01e28e465f65e821e08813ced95bced72f7a8a9b" dependencies = [ - "bitflags 2.0.2", + "bitflags", "clap_derive", "clap_lex 0.3.3", "is-terminal", @@ -168,15 +162,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.1.9" +version = "4.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fddf67631444a3a3e3e5ac51c36a5e01335302de677bd78759eaa90ab1f46644" +checksum = "9a932373bab67b984c790ddf2c9ca295d8e3af3b7ef92de5a5bacdccdee4b09b" dependencies = [ "heck", - "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.9", ] [[package]] @@ -733,7 +726,7 @@ version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" dependencies = [ - "bitflags 1.3.2", + "bitflags", "crc32fast", "flate2", "miniz_oxide", @@ -745,35 +738,11 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b" -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro2" -version = "1.0.52" +version = "1.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" +checksum = "ba466839c78239c09faf015484e5cc04860f88242cff4d03eb038f04b4699b73" dependencies = [ "unicode-ident", ] @@ -821,7 +790,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags 1.3.2", + "bitflags", ] [[package]] @@ -855,7 +824,7 @@ name = "rimage" version = "0.2.0" dependencies = [ "bytemuck", - "clap 4.1.11", + "clap 4.1.13", "criterion", "indicatif", "mozjpeg", @@ -887,7 +856,7 @@ version = "0.36.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db4165c9963ab29e422d6c26fbc1d37f15bace6b2810221f9d925023480fcf0e" dependencies = [ - "bitflags 1.3.2", + "bitflags", "errno", "io-lifetimes", "libc", @@ -939,7 +908,7 @@ checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad" dependencies = [ "proc-macro2", "quote", - "syn 2.0.5", + "syn 2.0.9", ] [[package]] @@ -990,9 +959,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.5" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89c2d1c76a26822187a1fbb5964e3fff108bc208f02e820ab9dac1234f6b388a" +checksum = "0da4a3c17e109f700685ec577c0f85efd9b19bcf15c913985f14dc1ac01775aa" dependencies = [ "proc-macro2", "quote", @@ -1058,12 +1027,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - [[package]] name = "walkdir" version = "2.3.3" From 3d6858dc05da7fd1c2a07cb9fac55fbc34870bcc Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Sat, 25 Mar 2023 00:49:18 +0100 Subject: [PATCH 20/46] Rewrite error docs and remove unneeded uses --- src/lib.rs | 30 ++++++++++++++---------------- src/main.rs | 2 +- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4aa72853..d7117fce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,10 +62,7 @@ println!("Data length: {:?}", image.data().len()); */ #![warn(missing_docs)] -use std::{ - fmt, fs, io, panic, - path::{self, PathBuf}, -}; +use std::{fmt, fs, io, panic, path}; use clap::Parser; @@ -129,31 +126,32 @@ pub struct Decoder<'a> { raw_data: Vec, } -/// Decoding error +/// An error that occurred during decoding a image #[derive(Debug)] pub enum DecodingError { - /// Error that occurs if file is missing, failed to open or other [`io::Error`] + /// A [`io::Error`] if file failed to read, find, etc. IoError(io::Error), - /// Error that occurs if format of file is not supported + /// The format of file is not supported Format(String), - /// Error that occurs if [`Config`] is invalid + /// A configuration error, see [`ConfigError`] Config(ConfigError), - /// Error that occurs if file failed to decode + /// A decoding error, file is not a image, unsupported color space, etc. Parsing(String), } -/// Configuration error +/// An error that occurred if configuration is invalid #[derive(Debug)] +#[non_exhaustive] pub enum ConfigError { - /// Error that occurs if quality is less than 0 or greater than 1 + /// Quality is less than 0 or greater than 1 QualityOutOfBounds, - /// Error that occurs if width is 0 + /// Width is 0 WidthIsZero, - /// Error that occurs if height is 0 + /// Height is 0 HeightIsZero, - /// Error that occurs if size is 0 + /// Size is 0 SizeIsZero, - /// Error that occurs if output format is not supported + /// Output format is not supported FormatNotSupported(String), } @@ -199,7 +197,7 @@ impl<'a> Decoder<'a> { /// - [`DecodingError`] if /// - File not found or other `io::Error` /// - Config is invalid - pub fn build(path: &'a PathBuf) -> Result { + pub fn build(path: &'a path::PathBuf) -> Result { let raw_data = fs::read(path)?; Ok(Decoder { path, raw_data }) diff --git a/src/main.rs b/src/main.rs index 195de12b..eb9ceb15 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,7 +35,7 @@ fn main() { println!("Bit Depth: {:?}", img.bit_depth()); println!("Size: {:?}", img.size()); println!("Data length: {:?}", img.data().len()); - println!(""); + println!(); } process::exit(0); } From f088570962b1869f8a0e86267accc37b119dc6d8 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Sat, 25 Mar 2023 17:08:33 +0100 Subject: [PATCH 21/46] Added more documentation and rewrite some code --- src/lib.rs | 419 ++++++++++++++++++++++++++++++++++++++++++---------- src/main.rs | 42 ++++-- 2 files changed, 375 insertions(+), 86 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d7117fce..0079cdc8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,9 +62,7 @@ println!("Data length: {:?}", image.data().len()); */ #![warn(missing_docs)] -use std::{fmt, fs, io, panic, path}; - -use clap::Parser; +use std::{borrow::Cow, fmt, fs, io, panic, path}; /// Decoders for images #[deprecated(since = "0.2.0", note = "use the Decoder struct instead")] @@ -74,23 +72,63 @@ pub mod decoders; pub mod encoders; /// Config from command line input -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -pub struct Config { - /// Input files to be compressed - pub input: Vec, - - /// Quality of output images from 0 to 1 - #[arg(short, long, default_value_t = 0.75)] - pub quality: f32, - - /// Format of output images - #[arg(short, long, default_value_t = String::from("jpg"))] - pub output_format: String, - - /// Outputs info about images - #[arg(short, long, default_value_t = false)] - pub info: bool, +#[derive(Debug)] +pub struct Config<'a> { + input: &'a [path::PathBuf], + quality: f32, + output_format: OutputFormat, +} + +impl<'a> Config<'a> { + /// Builds config from parameters + /// + /// # Result + /// + /// - [`Config`] if Ok + /// - [`ConfigError`] if + /// - Quality under 0 or greater than 1 + /// - input is empty + pub fn build( + input: &'a [path::PathBuf], + quality: f32, + output_format: OutputFormat, + ) -> Result { + if quality < 0.0 || quality > 1.0 { + return Err(ConfigError::QualityOutOfBounds); + } + + if input.is_empty() { + return Err(ConfigError::InputIsEmpty); + } + + Ok(Config { + input, + quality, + output_format, + }) + } + /// Gets input array of paths from config + pub fn input(&self) -> &[path::PathBuf] { + &self.input + } + /// Gets quality of output images from config + pub fn quality(&self) -> f32 { + self.quality + } + /// Gets format of output images from config + pub fn output_format(&self) -> &OutputFormat { + &self.output_format + } +} + +impl<'a> Default for Config<'a> { + fn default() -> Self { + Self { + input: &[], + quality: 0.75, + output_format: OutputFormat::MozJpeg, + } + } } /// Image data from decoder @@ -112,12 +150,12 @@ pub struct Config { /// println!("Data length: {:?}", image.data().len()); /// # Ok::<(), DecodingError>(()) /// ``` -pub struct ImageData { +pub struct ImageData<'a> { color_space: ColorSpace, bit_depth: BitDepth, width: usize, height: usize, - data: Vec, + data: Cow<'a, [u8]>, } /// Decoder used to get image data from file @@ -126,20 +164,106 @@ pub struct Decoder<'a> { raw_data: Vec, } +// Write encoder struct +/// Encoder used to encode image data to file +pub struct Encoder<'a> { + path: &'a path::PathBuf, + image_data: ImageData<'a>, + quality: f32, +} + +/// Supported output format +/// +/// # Examples +/// ``` +/// # use rimage::OutputFormat; +/// # use std::str::FromStr; +/// let format = OutputFormat::from_str("mozjpeg").unwrap(); +/// println!("Format: {}", format); +/// ``` +#[derive(Debug, Clone, Copy)] +pub enum OutputFormat { + /// MozJpeg image + MozJpeg, + /// Browser Png image + Png, + /// OxiPng image + Oxipng, +} + +impl std::str::FromStr for OutputFormat { + type Err = Cow<'static, str>; + + fn from_str(s: &str) -> Result { + match s { + "mozjpeg" | "jpg" | "jpeg" => Ok(OutputFormat::MozJpeg), + "png" => Ok(OutputFormat::Png), + "oxipng" => Ok(OutputFormat::Oxipng), + _ => Err(format!("{} is not a valid output format", s).into()), + } + } +} + +impl fmt::Display for OutputFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + OutputFormat::MozJpeg => write!(f, "jpeg"), + OutputFormat::Png => write!(f, "png"), + OutputFormat::Oxipng => write!(f, "oxipng"), + } + } +} + /// An error that occurred during decoding a image +/// +/// # Examples +/// ``` +/// # use rimage::{Decoder, DecodingError}; +/// # use std::path; +/// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); +/// let d = Decoder::build(&path)?; +/// let image = d.decode(); +/// match image { +/// Ok(_) => println!("Image decoded"), +/// Err(e) => println!("Error: {}", e), +/// } +/// # Ok::<(), DecodingError>(()) +/// ``` #[derive(Debug)] pub enum DecodingError { /// A [`io::Error`] if file failed to read, find, etc. IoError(io::Error), /// The format of file is not supported Format(String), - /// A configuration error, see [`ConfigError`] - Config(ConfigError), /// A decoding error, file is not a image, unsupported color space, etc. Parsing(String), } /// An error that occurred if configuration is invalid +/// +/// # Examples +/// ``` +/// # use rimage::{Config, ConfigError}; +/// let config = Config::build(&[], 1.1, rimage::OutputFormat::MozJpeg); +/// match config { +/// Ok(_) => println!("Config is valid"), +/// Err(e) => println!("Error: {}", e), +/// } +/// ``` +/// +/// # Errors +/// +/// - [`ConfigError::QualityOutOfBounds`] if quality is less than 0 or greater than 1 +/// - [`ConfigError::InputIsEmpty`] if input is empty +/// - [`ConfigError::WidthIsZero`] if width is 0 +/// - [`ConfigError::HeightIsZero`] if height is 0 +/// - [`ConfigError::SizeIsZero`] if size is 0 +/// +/// [`ConfigError::QualityOutOfBounds`]: enum.ConfigError.html#variant.QualityOutOfBounds +/// [`ConfigError::InputIsEmpty`]: enum.ConfigError.html#variant.InputIsEmpty +/// [`ConfigError::WidthIsZero`]: enum.ConfigError.html#variant.WidthIsZero +/// [`ConfigError::HeightIsZero`]: enum.ConfigError.html#variant.HeightIsZero +/// [`ConfigError::SizeIsZero`]: enum.ConfigError.html#variant.SizeIsZero #[derive(Debug)] #[non_exhaustive] pub enum ConfigError { @@ -151,11 +275,25 @@ pub enum ConfigError { HeightIsZero, /// Size is 0 SizeIsZero, - /// Output format is not supported - FormatNotSupported(String), + /// Input is empty + InputIsEmpty, } /// Color space of image +/// +/// # Examples +/// ``` +/// # use rimage::ColorSpace; +/// # use std::str::FromStr; +/// let color_space = ColorSpace::from_str("rgb").unwrap(); +/// println!("Color Space: {}", color_space); +/// ``` +/// +/// # Errors +/// +/// - [`ColorSpace::from_str`] if color space is not supported +/// +/// [`ColorSpace::from_str`]: enum.ColorSpace.html#method.from_str #[derive(Debug, PartialEq, Eq)] pub enum ColorSpace { /// **R**ed/**G**reen/**B**lue @@ -172,7 +310,50 @@ pub enum ColorSpace { GrayscaleAlpha, } +impl std::str::FromStr for ColorSpace { + type Err = Cow<'static, str>; + + fn from_str(s: &str) -> Result { + match s { + "rgb" => Ok(ColorSpace::Rgb), + "rgba" => Ok(ColorSpace::Rgba), + "cmyk" => Ok(ColorSpace::Cmyk), + "indexed" => Ok(ColorSpace::Indexed), + "grayscale" => Ok(ColorSpace::Grayscale), + "grayscale_alpha" => Ok(ColorSpace::GrayscaleAlpha), + _ => Err(format!("{} is not a valid color space", s).into()), + } + } +} + +impl fmt::Display for ColorSpace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ColorSpace::Rgb => write!(f, "rgb"), + ColorSpace::Rgba => write!(f, "rgba"), + ColorSpace::Cmyk => write!(f, "cmyk"), + ColorSpace::Indexed => write!(f, "indexed"), + ColorSpace::Grayscale => write!(f, "grayscale"), + ColorSpace::GrayscaleAlpha => write!(f, "grayscale_alpha"), + } + } +} + /// Bit depth of image per pixel +/// +/// # Examples +/// ``` +/// # use rimage::BitDepth; +/// # use std::str::FromStr; +/// let bit_depth = BitDepth::from_str("8").unwrap(); +/// println!("Bit Depth: {}", bit_depth); +/// ``` +/// +/// # Errors +/// +/// - [`BitDepth::from_str`] if bit depth is not supported +/// +/// [`BitDepth::from_str`]: enum.BitDepth.html#method.from_str #[derive(Debug)] #[repr(u8)] pub enum BitDepth { @@ -188,15 +369,50 @@ pub enum BitDepth { Sixteen = 16, } +impl std::str::FromStr for BitDepth { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "1" => Ok(BitDepth::One), + "2" => Ok(BitDepth::Two), + "4" => Ok(BitDepth::Four), + "8" => Ok(BitDepth::Eight), + "16" => Ok(BitDepth::Sixteen), + _ => Err(format!("{} is not a valid bit depth", s)), + } + } +} + +impl fmt::Display for BitDepth { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BitDepth::One => write!(f, "1"), + BitDepth::Two => write!(f, "2"), + BitDepth::Four => write!(f, "4"), + BitDepth::Eight => write!(f, "8"), + BitDepth::Sixteen => write!(f, "16"), + } + } +} + impl<'a> Decoder<'a> { /// Builds decoder from path /// - /// # Result + /// # Examples + /// ``` + /// # use rimage::Decoder; + /// # use std::path; + /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); + /// let d = Decoder::build(&path); + /// ``` + /// + /// # Errors /// - /// - [`Decoder`] if Ok - /// - [`DecodingError`] if - /// - File not found or other `io::Error` - /// - Config is invalid + /// - [`io::Error`] if file failed to read, find, etc. + /// + /// [`Decoder`]: struct.Decoder.html + /// [`io::Error`]: https://doc.rust-lang.org/std/io/struct.Error.html pub fn build(path: &'a path::PathBuf) -> Result { let raw_data = fs::read(path)?; @@ -205,23 +421,26 @@ impl<'a> Decoder<'a> { /// Decodes file to ImageData /// - /// # Result - /// - /// - [`ImageData`] if Ok - /// - [`DecodingError`] if - /// - File is not a image - /// - File extension not supported - /// - File corrupted or in unknown color space - /// /// # Examples /// ``` - /// # use rimage::{Decoder, DecodingError}; + /// # use rimage::{Decoder, ImageData}; /// # use std::path; /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); - /// let d = Decoder::build(&path)?; - /// let img = d.decode()?; - /// # Ok::<(), DecodingError>(()) + /// # let d = Decoder::build(&path).unwrap(); + /// let image = d.decode(); + /// match image { + /// Ok(_) => println!("Image decoded"), + /// Err(e) => println!("Error: {}", e), + /// } /// ``` + /// + /// # Errors + /// + /// - [`DecodingError::Format`] if format is not supported + /// - [`DecodingError::Parsing`] if file is not a image, unsupported color space, etc. + /// + /// [`DecodingError::Format`]: enum.DecodingError.html#variant.Format + /// [`DecodingError::Parsing`]: enum.DecodingError.html#variant.Parsing pub fn decode(&self) -> Result { let extension = match self.path.extension() { Some(ext) => ext, @@ -253,9 +472,12 @@ impl<'a> Decoder<'a> { mozjpeg::Format::CMYK(img) => img, }; - let data = image.read_scanlines_flat().ok_or(DecodingError::Parsing( - "Cannot read jpeg scanlines".to_string(), - ))?; + let data = Cow::Borrowed(image + .read_scanlines_flat() + .ok_or(DecodingError::Parsing( + "Cannot read jpeg scanlines".to_string(), + ))? + .as_slice()); Ok(ImageData { color_space, @@ -275,7 +497,7 @@ impl<'a> Decoder<'a> { let mut reader = d.read_info()?; let mut buf = vec![0; reader.output_buffer_size()]; let info = reader.next_frame(&mut buf)?; - let data = buf[..info.buffer_size()].to_vec(); + let data = buf[..info.buffer_size()].into(); Ok(ImageData { color_space: info.color_type.into(), bit_depth: info.bit_depth.into(), @@ -286,20 +508,78 @@ impl<'a> Decoder<'a> { } } -impl ImageData { +impl<'a> ImageData<'a> { /// Returns size of image (Width, Height) + /// + /// # Examples + /// ``` + /// # use rimage::{Decoder, ImageData}; + /// # use std::path; + /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); + /// # let d = Decoder::build(&path).unwrap(); + /// # let image = d.decode().unwrap(); + /// let (width, height) = image.size(); + /// ``` pub fn size(&self) -> (usize, usize) { (self.width, self.height) } /// Returns a ref to color space of image [`ColorSpace`] + /// + /// # Examples + /// ``` + /// # use rimage::{Decoder, ImageData, ColorSpace}; + /// # use std::path; + /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); + /// # let d = Decoder::build(&path).unwrap(); + /// # let image = d.decode().unwrap(); + /// let color_space = image.color_space(); + /// match color_space { + /// ColorSpace::Grayscale => println!("Grayscale"), + /// ColorSpace::Rgb => println!("RGB"), + /// ColorSpace::Cmyk => println!("CMYK"), + /// ColorSpace::Rgba => println!("RGBA"), + /// ColorSpace::Indexed => println!("Indexed"), + /// ColorSpace::GrayscaleAlpha => println!("Grayscale Alpha"), + /// } + /// ``` + /// [`ColorSpace`]: enum.ColorSpace.html pub fn color_space(&self) -> &ColorSpace { &self.color_space } /// Returns a ref to array of bytes in image + /// + /// # Examples + /// ``` + /// # use rimage::{Decoder, ImageData}; + /// # use std::path; + /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); + /// # let d = Decoder::build(&path).unwrap(); + /// # let image = d.decode().unwrap(); + /// let data = image.data(); + /// ``` pub fn data(&self) -> &[u8] { &self.data } /// Returns a ref to bit depth of image [`BitDepth`] + /// + /// # Examples + /// ``` + /// # use rimage::{Decoder, ImageData, BitDepth}; + /// # use std::path; + /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); + /// # let d = Decoder::build(&path).unwrap(); + /// # let image = d.decode().unwrap(); + /// let bit_depth = image.bit_depth(); + /// match bit_depth { + /// BitDepth::One => println!("1 bit"), + /// BitDepth::Two => println!("2 bits"), + /// BitDepth::Four => println!("4 bits"), + /// BitDepth::Eight => println!("8 bits"), + /// BitDepth::Sixteen => println!("16 bits"), + /// } + /// ``` + /// + /// [`BitDepth`]: enum.BitDepth.html pub fn bit_depth(&self) -> &BitDepth { &self.bit_depth } @@ -351,10 +631,9 @@ impl From for BitDepth { impl fmt::Display for DecodingError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - DecodingError::IoError(io_err) => f.write_fmt(format_args!("{}", io_err)), - DecodingError::Format(fmt_err) => f.write_str(fmt_err), - DecodingError::Config(cfg_err) => f.write_fmt(format_args!("{}", cfg_err)), - DecodingError::Parsing(prs_err) => f.write_str(prs_err), + DecodingError::IoError(io_err) => write!(f, "IO Error: {}", io_err), + DecodingError::Format(fmt_err) => write!(f, "Format Error: {}", fmt_err), + DecodingError::Parsing(prs_err) => write!(f, "Parsing Error: {}", prs_err), } } } @@ -362,13 +641,11 @@ impl fmt::Display for DecodingError { impl fmt::Display for ConfigError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ConfigError::QualityOutOfBounds => f.write_str("Quality is out of bounds"), - ConfigError::WidthIsZero => f.write_str("Width is cannot be zero"), - ConfigError::HeightIsZero => f.write_str("Height is cannot be zero"), - ConfigError::SizeIsZero => f.write_str("Size is cannot be zero"), - ConfigError::FormatNotSupported(ext) => { - f.write_fmt(format_args!("{} is not supported", ext)) - } + ConfigError::QualityOutOfBounds => write!(f, "Quality is out of bounds"), + ConfigError::WidthIsZero => write!(f, "Width cannot be zero"), + ConfigError::HeightIsZero => write!(f, "Height cannot be zero"), + ConfigError::SizeIsZero => write!(f, "Size cannot be zero"), + ConfigError::InputIsEmpty => write!(f, "Input cannot be zero"), } } } @@ -384,16 +661,12 @@ mod tests { assert_eq!( DecodingError::IoError(io::Error::new(io::ErrorKind::NotFound, "path not found")) .to_string(), - "path not found" + "IO Error: path not found" ); assert_eq!( DecodingError::Format("WebP not supported".to_string()).to_string(), - "WebP not supported" + "Format Error: WebP not supported" ); - assert_eq!( - DecodingError::Config(ConfigError::QualityOutOfBounds).to_string(), - "Quality is out of bounds" - ) } #[test] @@ -402,21 +675,15 @@ mod tests { ConfigError::QualityOutOfBounds.to_string(), "Quality is out of bounds" ); - assert_eq!( - ConfigError::WidthIsZero.to_string(), - "Width is cannot be zero" - ); + assert_eq!(ConfigError::WidthIsZero.to_string(), "Width cannot be zero"); assert_eq!( ConfigError::HeightIsZero.to_string(), - "Height is cannot be zero" - ); - assert_eq!( - ConfigError::SizeIsZero.to_string(), - "Size is cannot be zero" + "Height cannot be zero" ); + assert_eq!(ConfigError::SizeIsZero.to_string(), "Size cannot be zero"); assert_eq!( - ConfigError::FormatNotSupported("webp".to_string()).to_string(), - "webp is not supported" + ConfigError::InputIsEmpty.to_string(), + "Input cannot be zero" ) } diff --git a/src/main.rs b/src/main.rs index eb9ceb15..87e5e173 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,14 +2,36 @@ use std::{io, path, process}; use clap::Parser; use indicatif::{ProgressBar, ProgressStyle}; -use rimage::{decoders, encoders, Config, Decoder}; +use rimage::{decoders, encoders, Config, Decoder, OutputFormat}; + +#[derive(Parser)] +#[command(author, about, version, long_about = None)] +struct Args { + /// Input file(s) + input: Vec, + /// Quality of the output image (0-100) + #[arg(short, long, default_value = "75")] + quality: f32, + /// Output format of the output image + #[arg(short, long, default_value = "jpg")] + output_format: OutputFormat, + /// Print image info + #[arg(short, long)] + info: bool, +} fn main() { - let mut conf = Config::parse_from(wild::args_os()); - let pb = ProgressBar::new(conf.input.len() as u64); + let mut args = Args::parse_from(wild::args_os()); + let conf = if let Ok(conf) = Config::build(&args.input, args.quality, args.output_format) { + conf + } else { + eprintln!("Error: Invalid configuration."); + process::exit(1); + }; + let pb = ProgressBar::new(args.input.len() as u64); - if conf.input.is_empty() { - conf.input = io::stdin() + if args.input.is_empty() { + args.input = io::stdin() .lines() .map(|res| { let input_file = res.unwrap(); @@ -25,8 +47,8 @@ fn main() { ); pb.set_position(0); - if conf.info { - for path in &conf.input { + if args.info { + for path in &args.input { let d = Decoder::build(path).unwrap(); let img = d.decode().unwrap(); @@ -40,7 +62,7 @@ fn main() { process::exit(0); } - for path in &conf.input { + for path in &args.input { pb.set_message(path.file_name().unwrap().to_str().unwrap().to_owned()); pb.inc(1); @@ -55,10 +77,10 @@ fn main() { match encoders::encode_image( path, &pixels, - &conf.output_format, + &args.output_format.to_string(), width, height, - conf.quality, + args.quality, ) { Ok(()) => (), Err(e) => { From 9490fdac6b25cba916a4305e9603b09377134afe Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Sat, 25 Mar 2023 18:22:39 +0100 Subject: [PATCH 22/46] Added profiling profile --- Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 657ee581..899c6d73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,11 @@ edition = "2021" [profile.release] lto = true +codegen-units = 1 + +[profile.profiling] +inherits = "release" +debug = 1 [dependencies] bytemuck = "1.13.1" From b53ab8d1486be2eabeeb5caee5409294922d7bc3 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Sat, 25 Mar 2023 18:22:57 +0100 Subject: [PATCH 23/46] Added error handling in main --- src/main.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index eb9ceb15..195b0c05 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,8 +27,21 @@ fn main() { if conf.info { for path in &conf.input { - let d = Decoder::build(path).unwrap(); - let img = d.decode().unwrap(); + let d = match Decoder::build(path) { + Ok(d) => d, + Err(e) => { + eprintln!("{} Error: {e}", path.file_name().unwrap().to_str().unwrap()); + continue; + } + }; + + let img = match d.decode() { + Ok(img) => img, + Err(e) => { + eprintln!("{} Error: {e}", path.file_name().unwrap().to_str().unwrap()); + continue; + } + }; println!("{:?}", path.file_name().unwrap()); println!("Color Space: {:?}", img.color_space()); From 638a2e8b18cdac7a69a51b5e967b5876d7f4ee73 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Sat, 25 Mar 2023 21:55:31 +0100 Subject: [PATCH 24/46] Added profilers files to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 81cf4658..f57694cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target /.vscode +cachegrind.* +dhat.* \ No newline at end of file From 8f5fda58f5548c775551cef8d33070878c8afac2 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Sat, 25 Mar 2023 21:56:09 +0100 Subject: [PATCH 25/46] Rewrite benchmarks with new API --- Cargo.toml | 8 +++---- benches/decode_jpg.rs | 25 +++++++++++++++++++++ benches/decode_large_jpg.rs | 23 ------------------- benches/decode_large_png.rs | 23 ------------------- benches/decode_png.rs | 25 +++++++++++++++++++++ benches/encode_jpg.rs | 26 +++++++++++++++++++++ benches/encode_large_jpg.rs | 45 ------------------------------------- benches/encode_large_png.rs | 45 ------------------------------------- benches/encode_png.rs | 26 +++++++++++++++++++++ 9 files changed, 106 insertions(+), 140 deletions(-) create mode 100644 benches/decode_jpg.rs delete mode 100644 benches/decode_large_jpg.rs delete mode 100644 benches/decode_large_png.rs create mode 100644 benches/decode_png.rs create mode 100644 benches/encode_jpg.rs delete mode 100644 benches/encode_large_jpg.rs delete mode 100644 benches/encode_large_png.rs create mode 100644 benches/encode_png.rs diff --git a/Cargo.toml b/Cargo.toml index 899c6d73..a08a47a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,17 +36,17 @@ criterion = { version = "0.4.0", features = ["html_reports"] } regex = "1.7.2" [[bench]] -name = "decode_large_jpg" +name = "decode_jpg" harness = false [[bench]] -name = "decode_large_png" +name = "decode_png" harness = false [[bench]] -name = "encode_large_jpg" +name = "encode_jpg" harness = false [[bench]] -name = "encode_large_png" +name = "encode_png" harness = false diff --git a/benches/decode_jpg.rs b/benches/decode_jpg.rs new file mode 100644 index 00000000..8ff98fe7 --- /dev/null +++ b/benches/decode_jpg.rs @@ -0,0 +1,25 @@ +use std::path::PathBuf; + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +#[allow(deprecated)] +use rimage::decoders::decode_image; +use rimage::Decoder; + +#[allow(deprecated)] +fn bench_decode_jpg(c: &mut Criterion) { + let mut group = c.benchmark_group("decode_jpg"); + group.bench_function("decoders", |b| { + b.iter(|| decode_image(black_box(&PathBuf::from("tests/files/basi6a08.jpg")))) + }); + group.bench_function("Decoder", |b| { + b.iter(|| { + Decoder::build(black_box(&PathBuf::from("tests/files/basi6a08.jpg"))) + .unwrap() + .decode() + }) + }); + group.finish(); +} + +criterion_group!(benches, bench_decode_jpg); +criterion_main!(benches); diff --git a/benches/decode_large_jpg.rs b/benches/decode_large_jpg.rs deleted file mode 100644 index 20c552e0..00000000 --- a/benches/decode_large_jpg.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::{path::PathBuf, time::Duration}; - -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use rimage::decoders::decode_image; - -fn bench_decode_jpg_1(c: &mut Criterion) { - c.bench_function("di lt1jpg", |b| { - b.iter(|| decode_image(black_box(&PathBuf::from("test/large_test1.jpg")))) - }); -} - -fn bench_decode_jpg_2(c: &mut Criterion) { - c.bench_function("di lt2jpg", |b| { - b.iter(|| decode_image(black_box(&PathBuf::from("test/large_test2.jpg")))) - }); -} - -criterion_group!( - name = benches; - config = Criterion::default().sample_size(20).measurement_time(Duration::from_secs(25)); - targets = bench_decode_jpg_1, bench_decode_jpg_2 -); -criterion_main!(benches); diff --git a/benches/decode_large_png.rs b/benches/decode_large_png.rs deleted file mode 100644 index fc2a0e49..00000000 --- a/benches/decode_large_png.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::{path::PathBuf, time::Duration}; - -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use rimage::decoders::decode_image; - -fn bench_decode_png_1(c: &mut Criterion) { - c.bench_function("di lt1png", |b| { - b.iter(|| decode_image(black_box(&PathBuf::from("test/large_test1.png")))) - }); -} - -fn bench_decode_png_2(c: &mut Criterion) { - c.bench_function("di lt2png", |b| { - b.iter(|| decode_image(black_box(&PathBuf::from("test/large_test2.png")))) - }); -} - -criterion_group!( - name = benches; - config = Criterion::default().sample_size(20).measurement_time(Duration::from_secs(25)); - targets = bench_decode_png_1, bench_decode_png_2 -); -criterion_main!(benches); diff --git a/benches/decode_png.rs b/benches/decode_png.rs new file mode 100644 index 00000000..a4a356da --- /dev/null +++ b/benches/decode_png.rs @@ -0,0 +1,25 @@ +use std::path::PathBuf; + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +#[allow(deprecated)] +use rimage::decoders::decode_image; +use rimage::Decoder; + +#[allow(deprecated)] +fn bench_decode_png(c: &mut Criterion) { + let mut group = c.benchmark_group("decode_png"); + group.bench_function("decoders", |b| { + b.iter(|| decode_image(black_box(&PathBuf::from("tests/files/basi6a08.png")))) + }); + group.bench_function("Decoder", |b| { + b.iter(|| { + Decoder::build(black_box(&PathBuf::from("tests/files/basi6a08.png"))) + .unwrap() + .decode() + }) + }); + group.finish(); +} + +criterion_group!(benches, bench_decode_png); +criterion_main!(benches); diff --git a/benches/encode_jpg.rs b/benches/encode_jpg.rs new file mode 100644 index 00000000..b7b6a667 --- /dev/null +++ b/benches/encode_jpg.rs @@ -0,0 +1,26 @@ +use std::{fs, path::PathBuf}; + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +#[allow(deprecated)] +use rimage::{decoders::decode_image, encoders::encode_image}; + +#[allow(deprecated)] +fn bench_encode_jpg(c: &mut Criterion) { + let (pixels, width, height) = decode_image(&PathBuf::from("tests/files/basi6a08.jpg")).unwrap(); + c.bench_function("en jpg", |b| { + b.iter(|| { + encode_image( + black_box(&PathBuf::from("en")), + black_box(&pixels), + black_box("jpg"), + black_box(width), + black_box(height), + black_box(0.75), + ) + }) + }); + fs::remove_file("en.jpg").unwrap(); +} + +criterion_group!(benches, bench_encode_jpg); +criterion_main!(benches); diff --git a/benches/encode_large_jpg.rs b/benches/encode_large_jpg.rs deleted file mode 100644 index 31b098aa..00000000 --- a/benches/encode_large_jpg.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::{fs, path::PathBuf, time::Duration}; - -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use rimage::{decoders::decode_image, encoders::encode_image}; - -fn bench_encode_jpg_1(c: &mut Criterion) { - let (pixels, width, height) = decode_image(&PathBuf::from("test/large_test1.jpg")).unwrap(); - c.bench_function("en lt1jpg", |b| { - b.iter(|| { - encode_image( - black_box(&PathBuf::from("en_lt1")), - black_box(&pixels), - black_box("jpg"), - black_box(width), - black_box(height), - black_box(0.75), - ) - }) - }); - fs::remove_file("en_lt1.jpg").unwrap(); -} - -fn bench_encode_jpg_2(c: &mut Criterion) { - let (pixels, width, height) = decode_image(&PathBuf::from("test/large_test2.jpg")).unwrap(); - c.bench_function("en lt2jpg", |b| { - b.iter(|| { - encode_image( - black_box(&PathBuf::from("en_lt2")), - black_box(&pixels), - black_box("jpg"), - black_box(width), - black_box(height), - black_box(0.75), - ) - }) - }); - fs::remove_file("en_lt2.jpg").unwrap(); -} - -criterion_group!( - name = benches; - config = Criterion::default().sample_size(10).measurement_time(Duration::from_secs(30)); - targets = bench_encode_jpg_1, bench_encode_jpg_2 -); -criterion_main!(benches); diff --git a/benches/encode_large_png.rs b/benches/encode_large_png.rs deleted file mode 100644 index 97ad179f..00000000 --- a/benches/encode_large_png.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::{fs, path::PathBuf, time::Duration}; - -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use rimage::{decoders::decode_image, encoders::encode_image}; - -fn bench_encode_png_1(c: &mut Criterion) { - let (pixels, width, height) = decode_image(&PathBuf::from("test/large_test1.jpg")).unwrap(); - c.bench_function("en lt1png", |b| { - b.iter(|| { - encode_image( - black_box(&PathBuf::from("en_lt1")), - black_box(&pixels), - black_box("png"), - black_box(width), - black_box(height), - black_box(0.75), - ) - }) - }); - fs::remove_file("en_lt1.png").unwrap(); -} - -fn bench_encode_png_2(c: &mut Criterion) { - let (pixels, width, height) = decode_image(&PathBuf::from("test/large_test2.jpg")).unwrap(); - c.bench_function("en lt2png", |b| { - b.iter(|| { - encode_image( - black_box(&PathBuf::from("en_lt2")), - black_box(&pixels), - black_box("png"), - black_box(width), - black_box(height), - black_box(0.75), - ) - }) - }); - fs::remove_file("en_lt2.png").unwrap(); -} - -criterion_group!( - name = benches; - config = Criterion::default().sample_size(10).measurement_time(Duration::from_secs(300)); - targets = bench_encode_png_1, bench_encode_png_2 -); -criterion_main!(benches); diff --git a/benches/encode_png.rs b/benches/encode_png.rs new file mode 100644 index 00000000..8975320c --- /dev/null +++ b/benches/encode_png.rs @@ -0,0 +1,26 @@ +use std::{fs, path::PathBuf}; + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +#[allow(deprecated)] +use rimage::{decoders::decode_image, encoders::encode_image}; + +#[allow(deprecated)] +fn bench_encode_png(c: &mut Criterion) { + let (pixels, width, height) = decode_image(&PathBuf::from("tests/files/basi6a08.png")).unwrap(); + c.bench_function("en png", |b| { + b.iter(|| { + encode_image( + black_box(&PathBuf::from("en")), + black_box(&pixels), + black_box("png"), + black_box(width), + black_box(height), + black_box(0.75), + ) + }) + }); + fs::remove_file("en.png").unwrap(); +} + +criterion_group!(benches, bench_encode_png); +criterion_main!(benches); From 0cc3810c390e35df9871753b6a641beb6b3cb10b Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Sat, 25 Mar 2023 21:58:15 +0100 Subject: [PATCH 26/46] Refactor code to be more maintainable --- src/error.rs | 143 ++++++++++++++ src/image.rs | 314 +++++++++++++++++++++++++++++++ src/lib.rs | 512 ++++++--------------------------------------------- 3 files changed, 510 insertions(+), 459 deletions(-) create mode 100644 src/error.rs create mode 100644 src/image.rs diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 00000000..e6a932cc --- /dev/null +++ b/src/error.rs @@ -0,0 +1,143 @@ +use std::{fmt, io}; + +/// An error that occurred if configuration is invalid +/// +/// # Examples +/// ``` +/// # use rimage::{Config, ConfigError}; +/// let config = Config::build(&[], 1.1, rimage::OutputFormat::MozJpeg); +/// match config { +/// Ok(_) => println!("Config is valid"), +/// Err(e) => println!("Error: {}", e), +/// } +/// ``` +/// +/// # Errors +/// +/// - [`ConfigError::QualityOutOfBounds`] if quality is less than 0 or greater than 1 +/// - [`ConfigError::InputIsEmpty`] if input is empty +/// - [`ConfigError::WidthIsZero`] if width is 0 +/// - [`ConfigError::HeightIsZero`] if height is 0 +/// - [`ConfigError::SizeIsZero`] if size is 0 +/// +/// [`ConfigError::QualityOutOfBounds`]: enum.ConfigError.html#variant.QualityOutOfBounds +/// [`ConfigError::InputIsEmpty`]: enum.ConfigError.html#variant.InputIsEmpty +/// [`ConfigError::WidthIsZero`]: enum.ConfigError.html#variant.WidthIsZero +/// [`ConfigError::HeightIsZero`]: enum.ConfigError.html#variant.HeightIsZero +/// [`ConfigError::SizeIsZero`]: enum.ConfigError.html#variant.SizeIsZero +#[derive(Debug)] +pub enum ConfigError { + /// Quality is less than 0 or greater than 1 + QualityOutOfBounds, + /// Width is 0 + WidthIsZero, + /// Height is 0 + HeightIsZero, + /// Size is 0 + SizeIsZero, + /// Input is empty + InputIsEmpty, +} + +/// An error that occurred during decoding a image +/// +/// # Examples +/// ``` +/// # use rimage::{Decoder, DecodingError}; +/// # use std::path; +/// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); +/// let d = Decoder::build(&path)?; +/// let image = d.decode(); +/// match image { +/// Ok(_) => println!("Image decoded"), +/// Err(e) => println!("Error: {}", e), +/// } +/// # Ok::<(), DecodingError>(()) +/// ``` +#[derive(Debug)] +pub enum DecodingError { + /// A [`io::Error`] if file failed to read, find, etc. + IoError(io::Error), + /// The format of file is not supported + Format(String), + /// A decoding error, file is not a image, unsupported color space, etc. + Parsing(String), +} + +impl From for DecodingError { + #[inline] + fn from(err: io::Error) -> Self { + DecodingError::IoError(err) + } +} + +impl From for DecodingError { + fn from(err: png::DecodingError) -> Self { + match err { + png::DecodingError::IoError(io_err) => DecodingError::IoError(io_err), + png::DecodingError::Format(f_err) => DecodingError::Format(f_err.to_string()), + png::DecodingError::Parameter(p_err) => DecodingError::Parsing(p_err.to_string()), + png::DecodingError::LimitsExceeded => { + DecodingError::Parsing("Png limits exceeded".to_string()) + } + } + } +} + +impl fmt::Display for DecodingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DecodingError::IoError(io_err) => write!(f, "IO Error: {}", io_err), + DecodingError::Format(fmt_err) => write!(f, "Format Error: {}", fmt_err), + DecodingError::Parsing(prs_err) => write!(f, "Parsing Error: {}", prs_err), + } + } +} + +impl fmt::Display for ConfigError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ConfigError::QualityOutOfBounds => write!(f, "Quality is out of bounds"), + ConfigError::WidthIsZero => write!(f, "Width cannot be zero"), + ConfigError::HeightIsZero => write!(f, "Height cannot be zero"), + ConfigError::SizeIsZero => write!(f, "Size cannot be zero"), + ConfigError::InputIsEmpty => write!(f, "Input cannot be zero"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn display_decoder_error() { + assert_eq!( + DecodingError::IoError(io::Error::new(io::ErrorKind::NotFound, "path not found")) + .to_string(), + "IO Error: path not found" + ); + assert_eq!( + DecodingError::Format("WebP not supported".to_string()).to_string(), + "Format Error: WebP not supported" + ); + } + + #[test] + fn display_config_error() { + assert_eq!( + ConfigError::QualityOutOfBounds.to_string(), + "Quality is out of bounds" + ); + assert_eq!(ConfigError::WidthIsZero.to_string(), "Width cannot be zero"); + assert_eq!( + ConfigError::HeightIsZero.to_string(), + "Height cannot be zero" + ); + assert_eq!(ConfigError::SizeIsZero.to_string(), "Size cannot be zero"); + assert_eq!( + ConfigError::InputIsEmpty.to_string(), + "Input cannot be zero" + ) + } +} diff --git a/src/image.rs b/src/image.rs new file mode 100644 index 00000000..84de5128 --- /dev/null +++ b/src/image.rs @@ -0,0 +1,314 @@ +use std::{borrow::Cow, fmt}; + +/// Image data from decoder +/// +/// # Examples +/// +/// ``` +/// # use rimage::{Decoder, DecodingError}; +/// # use std::path; +/// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); +/// let d = Decoder::build(&path)?; +/// +/// let image = d.decode()?; +/// +/// // Get something from image data +/// println!("Color Space: {:?}", image.color_space()); +/// println!("Bit Depth: {:?}", image.bit_depth()); +/// println!("Size: {:?}", image.size()); +/// println!("Data length: {:?}", image.data().len()); +/// # Ok::<(), DecodingError>(()) +/// ``` +pub struct ImageData { + color_space: ColorSpace, + bit_depth: BitDepth, + width: usize, + height: usize, + data: Vec, +} + +impl ImageData { + /// Creates a new [`ImageData`] + /// + /// # Examples + /// ``` + /// # use rimage::{ImageData, ColorSpace, BitDepth}; + /// let image = ImageData::new(ColorSpace::Gray, BitDepth::Eight, 100, 100, vec![0; 100 * 100]); + /// ``` + pub fn new( + color_space: ColorSpace, + bit_depth: BitDepth, + width: usize, + height: usize, + data: Vec, + ) -> Self { + Self { + color_space, + bit_depth, + width, + height, + data, + } + } + /// Returns size of image (Width, Height) + /// + /// # Examples + /// ``` + /// # use rimage::{Decoder, ImageData}; + /// # use std::path; + /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); + /// # let d = Decoder::build(&path).unwrap(); + /// # let image = d.decode().unwrap(); + /// let (width, height) = image.size(); + /// ``` + #[inline] + pub fn size(&self) -> (usize, usize) { + (self.width, self.height) + } + /// Returns a ref to color space of image [`ColorSpace`] + /// + /// # Examples + /// ``` + /// # use rimage::{Decoder, ImageData, ColorSpace}; + /// # use std::path; + /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); + /// # let d = Decoder::build(&path).unwrap(); + /// # let image = d.decode().unwrap(); + /// let color_space = image.color_space(); + /// match color_space { + /// ColorSpace::Gray => println!("Grayscale"), + /// ColorSpace::Rgb => println!("RGB"), + /// ColorSpace::Cmyk => println!("CMYK"), + /// ColorSpace::Rgba => println!("RGBA"), + /// ColorSpace::Indexed => println!("Indexed"), + /// ColorSpace::GrayAlpha => println!("Grayscale Alpha"), + /// } + /// ``` + /// [`ColorSpace`]: enum.ColorSpace.html + #[inline] + pub fn color_space(&self) -> &ColorSpace { + &self.color_space + } + /// Returns a ref to array of bytes in image + /// + /// # Examples + /// ``` + /// # use rimage::{Decoder, ImageData}; + /// # use std::path; + /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); + /// # let d = Decoder::build(&path).unwrap(); + /// # let image = d.decode().unwrap(); + /// let data = image.data(); + /// ``` + #[inline] + pub fn data(&self) -> &[u8] { + &self.data + } + /// Returns a ref to bit depth of image [`BitDepth`] + /// + /// # Examples + /// ``` + /// # use rimage::{Decoder, ImageData, BitDepth}; + /// # use std::path; + /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); + /// # let d = Decoder::build(&path).unwrap(); + /// # let image = d.decode().unwrap(); + /// let bit_depth = image.bit_depth(); + /// match bit_depth { + /// BitDepth::One => println!("1 bit"), + /// BitDepth::Two => println!("2 bits"), + /// BitDepth::Four => println!("4 bits"), + /// BitDepth::Eight => println!("8 bits"), + /// BitDepth::Sixteen => println!("16 bits"), + /// } + /// ``` + /// + /// [`BitDepth`]: enum.BitDepth.html + #[inline] + pub fn bit_depth(&self) -> &BitDepth { + &self.bit_depth + } +} + +/// Supported output format +/// +/// # Examples +/// ``` +/// # use rimage::OutputFormat; +/// # use std::str::FromStr; +/// let format = OutputFormat::from_str("mozjpeg").unwrap(); +/// println!("Format: {}", format); +/// ``` +#[derive(Debug, Clone, Copy)] +pub enum OutputFormat { + /// MozJpeg image + MozJpeg, + /// Browser Png image + Png, + /// OxiPng image + Oxipng, +} + +impl std::str::FromStr for OutputFormat { + type Err = Cow<'static, str>; + + fn from_str(s: &str) -> Result { + match s { + "mozjpeg" | "jpg" | "jpeg" => Ok(OutputFormat::MozJpeg), + "png" => Ok(OutputFormat::Png), + "oxipng" => Ok(OutputFormat::Oxipng), + _ => Err(format!("{} is not a valid output format", s).into()), + } + } +} + +impl fmt::Display for OutputFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + OutputFormat::MozJpeg => write!(f, "jpeg"), + OutputFormat::Png => write!(f, "png"), + OutputFormat::Oxipng => write!(f, "oxipng"), + } + } +} + +/// Color space of image +/// +/// # Examples +/// ``` +/// # use rimage::ColorSpace; +/// # use std::str::FromStr; +/// let color_space = ColorSpace::from_str("rgb").unwrap(); +/// println!("Color Space: {}", color_space); +/// ``` +/// +/// # Errors +/// +/// - [`ColorSpace::from_str`] if color space is not supported +/// +/// [`ColorSpace::from_str`]: enum.ColorSpace.html#method.from_str +#[derive(Debug, PartialEq, Eq)] +pub enum ColorSpace { + /// **R**ed/**G**reen/**B**lue + Rgb, + /// **R**ed/**G**reen/**B**lue/**A**lpha + Rgba, + /// **C**yan/**M**agenta/**Y**ellow/Blac**K** + Cmyk, + /// Indexed color palette + Indexed, + /// Grayscale + Gray, + /// Grayscale/Alpha + GrayAlpha, +} + +impl From for ColorSpace { + fn from(color_type: png::ColorType) -> Self { + match color_type { + png::ColorType::Grayscale => ColorSpace::Gray, + png::ColorType::Rgb => ColorSpace::Rgb, + png::ColorType::Indexed => ColorSpace::Indexed, + png::ColorType::GrayscaleAlpha => ColorSpace::GrayAlpha, + png::ColorType::Rgba => ColorSpace::Rgba, + } + } +} + +impl std::str::FromStr for ColorSpace { + type Err = Cow<'static, str>; + + fn from_str(s: &str) -> Result { + match s { + "rgb" => Ok(ColorSpace::Rgb), + "rgba" => Ok(ColorSpace::Rgba), + "cmyk" => Ok(ColorSpace::Cmyk), + "indexed" => Ok(ColorSpace::Indexed), + "grayscale" => Ok(ColorSpace::Gray), + "grayscale_alpha" => Ok(ColorSpace::GrayAlpha), + _ => Err(format!("{} is not a valid color space", s).into()), + } + } +} + +impl fmt::Display for ColorSpace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ColorSpace::Rgb => write!(f, "rgb"), + ColorSpace::Rgba => write!(f, "rgba"), + ColorSpace::Cmyk => write!(f, "cmyk"), + ColorSpace::Indexed => write!(f, "indexed"), + ColorSpace::Gray => write!(f, "grayscale"), + ColorSpace::GrayAlpha => write!(f, "grayscale_alpha"), + } + } +} + +/// Bit depth of image per pixel +/// +/// # Examples +/// ``` +/// # use rimage::BitDepth; +/// # use std::str::FromStr; +/// let bit_depth = BitDepth::from_str("8").unwrap(); +/// println!("Bit Depth: {}", bit_depth); +/// ``` +/// +/// # Errors +/// +/// - [`BitDepth::from_str`] if bit depth is not supported +/// +/// [`BitDepth::from_str`]: enum.BitDepth.html#method.from_str +#[derive(Debug)] +#[repr(u8)] +pub enum BitDepth { + /// One bit per pixel + One = 1, + /// Two bits per pixel + Two = 2, + /// Four bits per pixel + Four = 4, + /// Eight bits per pixel + Eight = 8, + /// Sixteen bits per pixel + Sixteen = 16, +} + +impl From for BitDepth { + fn from(bit_depth: png::BitDepth) -> Self { + match bit_depth { + png::BitDepth::One => BitDepth::One, + png::BitDepth::Two => BitDepth::Two, + png::BitDepth::Four => BitDepth::Four, + png::BitDepth::Eight => BitDepth::Eight, + png::BitDepth::Sixteen => BitDepth::Sixteen, + } + } +} + +impl std::str::FromStr for BitDepth { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "1" => Ok(BitDepth::One), + "2" => Ok(BitDepth::Two), + "4" => Ok(BitDepth::Four), + "8" => Ok(BitDepth::Eight), + "16" => Ok(BitDepth::Sixteen), + _ => Err(format!("{} is not a valid bit depth", s)), + } + } +} + +impl fmt::Display for BitDepth { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BitDepth::One => write!(f, "1"), + BitDepth::Two => write!(f, "2"), + BitDepth::Four => write!(f, "4"), + BitDepth::Eight => write!(f, "8"), + BitDepth::Sixteen => write!(f, "16"), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 0079cdc8..fdf9b368 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,7 +62,10 @@ println!("Data length: {:?}", image.data().len()); */ #![warn(missing_docs)] -use std::{borrow::Cow, fmt, fs, io, panic, path}; +use std::{fs, panic, path}; + +pub use error::{ConfigError, DecodingError}; +pub use image::{BitDepth, ColorSpace, ImageData, OutputFormat}; /// Decoders for images #[deprecated(since = "0.2.0", note = "use the Decoder struct instead")] @@ -71,7 +74,13 @@ pub mod decoders; #[deprecated(since = "0.2.0", note = "use the Encoder struct instead")] pub mod encoders; -/// Config from command line input +/// All errors that can occur +pub mod error; + +/// Image data +pub mod image; + +/// Config for encoder #[derive(Debug)] pub struct Config<'a> { input: &'a [path::PathBuf], @@ -82,18 +91,26 @@ pub struct Config<'a> { impl<'a> Config<'a> { /// Builds config from parameters /// - /// # Result + /// # Examples + /// + /// ``` + /// # use rimage::{Config, OutputFormat}; + /// # use std::path; + /// let input = &[path::PathBuf::from("tests/files/basi0g01.jpg")]; + /// let quality = 100.0; + /// let output_format = OutputFormat::MozJpeg; + /// let config = Config::build(input, quality, output_format).unwrap(); + /// ``` /// - /// - [`Config`] if Ok - /// - [`ConfigError`] if - /// - Quality under 0 or greater than 1 - /// - input is empty + /// # Errors + /// - [`ConfigError::QualityOutOfBounds`] if quality is not in range 0.0 - 100.0 + /// - [`ConfigError::InputIsEmpty`] if input is empty pub fn build( input: &'a [path::PathBuf], quality: f32, output_format: OutputFormat, ) -> Result { - if quality < 0.0 || quality > 1.0 { + if quality < 0.0 || quality > 100.0 { return Err(ConfigError::QualityOutOfBounds); } @@ -107,14 +124,17 @@ impl<'a> Config<'a> { output_format, }) } + #[inline] /// Gets input array of paths from config pub fn input(&self) -> &[path::PathBuf] { &self.input } + #[inline] /// Gets quality of output images from config pub fn quality(&self) -> f32 { self.quality } + #[inline] /// Gets format of output images from config pub fn output_format(&self) -> &OutputFormat { &self.output_format @@ -122,280 +142,22 @@ impl<'a> Config<'a> { } impl<'a> Default for Config<'a> { + #[inline] fn default() -> Self { Self { input: &[], - quality: 0.75, + quality: 75.0, output_format: OutputFormat::MozJpeg, } } } -/// Image data from decoder -/// -/// # Examples -/// -/// ``` -/// # use rimage::{Decoder, DecodingError}; -/// # use std::path; -/// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); -/// let d = Decoder::build(&path)?; -/// -/// let image = d.decode()?; -/// -/// // Get something from image data -/// println!("Color Space: {:?}", image.color_space()); -/// println!("Bit Depth: {:?}", image.bit_depth()); -/// println!("Size: {:?}", image.size()); -/// println!("Data length: {:?}", image.data().len()); -/// # Ok::<(), DecodingError>(()) -/// ``` -pub struct ImageData<'a> { - color_space: ColorSpace, - bit_depth: BitDepth, - width: usize, - height: usize, - data: Cow<'a, [u8]>, -} - /// Decoder used to get image data from file pub struct Decoder<'a> { path: &'a path::PathBuf, raw_data: Vec, } -// Write encoder struct -/// Encoder used to encode image data to file -pub struct Encoder<'a> { - path: &'a path::PathBuf, - image_data: ImageData<'a>, - quality: f32, -} - -/// Supported output format -/// -/// # Examples -/// ``` -/// # use rimage::OutputFormat; -/// # use std::str::FromStr; -/// let format = OutputFormat::from_str("mozjpeg").unwrap(); -/// println!("Format: {}", format); -/// ``` -#[derive(Debug, Clone, Copy)] -pub enum OutputFormat { - /// MozJpeg image - MozJpeg, - /// Browser Png image - Png, - /// OxiPng image - Oxipng, -} - -impl std::str::FromStr for OutputFormat { - type Err = Cow<'static, str>; - - fn from_str(s: &str) -> Result { - match s { - "mozjpeg" | "jpg" | "jpeg" => Ok(OutputFormat::MozJpeg), - "png" => Ok(OutputFormat::Png), - "oxipng" => Ok(OutputFormat::Oxipng), - _ => Err(format!("{} is not a valid output format", s).into()), - } - } -} - -impl fmt::Display for OutputFormat { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - OutputFormat::MozJpeg => write!(f, "jpeg"), - OutputFormat::Png => write!(f, "png"), - OutputFormat::Oxipng => write!(f, "oxipng"), - } - } -} - -/// An error that occurred during decoding a image -/// -/// # Examples -/// ``` -/// # use rimage::{Decoder, DecodingError}; -/// # use std::path; -/// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); -/// let d = Decoder::build(&path)?; -/// let image = d.decode(); -/// match image { -/// Ok(_) => println!("Image decoded"), -/// Err(e) => println!("Error: {}", e), -/// } -/// # Ok::<(), DecodingError>(()) -/// ``` -#[derive(Debug)] -pub enum DecodingError { - /// A [`io::Error`] if file failed to read, find, etc. - IoError(io::Error), - /// The format of file is not supported - Format(String), - /// A decoding error, file is not a image, unsupported color space, etc. - Parsing(String), -} - -/// An error that occurred if configuration is invalid -/// -/// # Examples -/// ``` -/// # use rimage::{Config, ConfigError}; -/// let config = Config::build(&[], 1.1, rimage::OutputFormat::MozJpeg); -/// match config { -/// Ok(_) => println!("Config is valid"), -/// Err(e) => println!("Error: {}", e), -/// } -/// ``` -/// -/// # Errors -/// -/// - [`ConfigError::QualityOutOfBounds`] if quality is less than 0 or greater than 1 -/// - [`ConfigError::InputIsEmpty`] if input is empty -/// - [`ConfigError::WidthIsZero`] if width is 0 -/// - [`ConfigError::HeightIsZero`] if height is 0 -/// - [`ConfigError::SizeIsZero`] if size is 0 -/// -/// [`ConfigError::QualityOutOfBounds`]: enum.ConfigError.html#variant.QualityOutOfBounds -/// [`ConfigError::InputIsEmpty`]: enum.ConfigError.html#variant.InputIsEmpty -/// [`ConfigError::WidthIsZero`]: enum.ConfigError.html#variant.WidthIsZero -/// [`ConfigError::HeightIsZero`]: enum.ConfigError.html#variant.HeightIsZero -/// [`ConfigError::SizeIsZero`]: enum.ConfigError.html#variant.SizeIsZero -#[derive(Debug)] -#[non_exhaustive] -pub enum ConfigError { - /// Quality is less than 0 or greater than 1 - QualityOutOfBounds, - /// Width is 0 - WidthIsZero, - /// Height is 0 - HeightIsZero, - /// Size is 0 - SizeIsZero, - /// Input is empty - InputIsEmpty, -} - -/// Color space of image -/// -/// # Examples -/// ``` -/// # use rimage::ColorSpace; -/// # use std::str::FromStr; -/// let color_space = ColorSpace::from_str("rgb").unwrap(); -/// println!("Color Space: {}", color_space); -/// ``` -/// -/// # Errors -/// -/// - [`ColorSpace::from_str`] if color space is not supported -/// -/// [`ColorSpace::from_str`]: enum.ColorSpace.html#method.from_str -#[derive(Debug, PartialEq, Eq)] -pub enum ColorSpace { - /// **R**ed/**G**reen/**B**lue - Rgb, - /// **R**ed/**G**reen/**B**lue/**A**lpha - Rgba, - /// **C**yan/**M**agenta/**Y**ellow/Blac**K** - Cmyk, - /// Indexed color palette - Indexed, - /// Grayscale - Grayscale, - /// Grayscale/Alpha - GrayscaleAlpha, -} - -impl std::str::FromStr for ColorSpace { - type Err = Cow<'static, str>; - - fn from_str(s: &str) -> Result { - match s { - "rgb" => Ok(ColorSpace::Rgb), - "rgba" => Ok(ColorSpace::Rgba), - "cmyk" => Ok(ColorSpace::Cmyk), - "indexed" => Ok(ColorSpace::Indexed), - "grayscale" => Ok(ColorSpace::Grayscale), - "grayscale_alpha" => Ok(ColorSpace::GrayscaleAlpha), - _ => Err(format!("{} is not a valid color space", s).into()), - } - } -} - -impl fmt::Display for ColorSpace { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ColorSpace::Rgb => write!(f, "rgb"), - ColorSpace::Rgba => write!(f, "rgba"), - ColorSpace::Cmyk => write!(f, "cmyk"), - ColorSpace::Indexed => write!(f, "indexed"), - ColorSpace::Grayscale => write!(f, "grayscale"), - ColorSpace::GrayscaleAlpha => write!(f, "grayscale_alpha"), - } - } -} - -/// Bit depth of image per pixel -/// -/// # Examples -/// ``` -/// # use rimage::BitDepth; -/// # use std::str::FromStr; -/// let bit_depth = BitDepth::from_str("8").unwrap(); -/// println!("Bit Depth: {}", bit_depth); -/// ``` -/// -/// # Errors -/// -/// - [`BitDepth::from_str`] if bit depth is not supported -/// -/// [`BitDepth::from_str`]: enum.BitDepth.html#method.from_str -#[derive(Debug)] -#[repr(u8)] -pub enum BitDepth { - /// One bit per pixel - One = 1, - /// Two bits per pixel - Two = 2, - /// Four bits per pixel - Four = 4, - /// Eight bits per pixel - Eight = 8, - /// Sixteen bits per pixel - Sixteen = 16, -} - -impl std::str::FromStr for BitDepth { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "1" => Ok(BitDepth::One), - "2" => Ok(BitDepth::Two), - "4" => Ok(BitDepth::Four), - "8" => Ok(BitDepth::Eight), - "16" => Ok(BitDepth::Sixteen), - _ => Err(format!("{} is not a valid bit depth", s)), - } - } -} - -impl fmt::Display for BitDepth { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - BitDepth::One => write!(f, "1"), - BitDepth::Two => write!(f, "2"), - BitDepth::Four => write!(f, "4"), - BitDepth::Eight => write!(f, "8"), - BitDepth::Sixteen => write!(f, "16"), - } - } -} - impl<'a> Decoder<'a> { /// Builds decoder from path /// @@ -413,6 +175,7 @@ impl<'a> Decoder<'a> { /// /// [`Decoder`]: struct.Decoder.html /// [`io::Error`]: https://doc.rust-lang.org/std/io/struct.Error.html + #[inline] pub fn build(path: &'a path::PathBuf) -> Result { let raw_data = fs::read(path)?; @@ -461,7 +224,7 @@ impl<'a> Decoder<'a> { panic::catch_unwind(|| -> Result { let d = mozjpeg::Decompress::new_mem(&self.raw_data)?; let color_space = match d.color_space() { - mozjpeg::ColorSpace::JCS_GRAYSCALE => ColorSpace::Grayscale, + mozjpeg::ColorSpace::JCS_GRAYSCALE => ColorSpace::Gray, mozjpeg::ColorSpace::JCS_CMYK => ColorSpace::Cmyk, mozjpeg::ColorSpace::JCS_RGB => ColorSpace::Rgb, _ => ColorSpace::Rgb, @@ -472,20 +235,17 @@ impl<'a> Decoder<'a> { mozjpeg::Format::CMYK(img) => img, }; - let data = Cow::Borrowed(image - .read_scanlines_flat() - .ok_or(DecodingError::Parsing( - "Cannot read jpeg scanlines".to_string(), - ))? - .as_slice()); + let data = image.read_scanlines_flat().ok_or(DecodingError::Parsing( + "Cannot read jpeg scanlines".to_string(), + ))?; - Ok(ImageData { + Ok(ImageData::new( color_space, - bit_depth: BitDepth::Eight, - width: image.width(), - height: image.height(), + BitDepth::Eight, + image.width() as usize, + image.height() as usize, data, - }) + )) }) .unwrap_or(Err(DecodingError::Parsing( "Failed to decode jpeg".to_string(), @@ -498,156 +258,21 @@ impl<'a> Decoder<'a> { let mut buf = vec![0; reader.output_buffer_size()]; let info = reader.next_frame(&mut buf)?; let data = buf[..info.buffer_size()].into(); - Ok(ImageData { - color_space: info.color_type.into(), - bit_depth: info.bit_depth.into(), - width: info.width as usize, - height: info.height as usize, + Ok(ImageData::new( + info.color_type.into(), + info.bit_depth.into(), + info.width as usize, + info.height as usize, data, - }) + )) } } -impl<'a> ImageData<'a> { - /// Returns size of image (Width, Height) - /// - /// # Examples - /// ``` - /// # use rimage::{Decoder, ImageData}; - /// # use std::path; - /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); - /// # let d = Decoder::build(&path).unwrap(); - /// # let image = d.decode().unwrap(); - /// let (width, height) = image.size(); - /// ``` - pub fn size(&self) -> (usize, usize) { - (self.width, self.height) - } - /// Returns a ref to color space of image [`ColorSpace`] - /// - /// # Examples - /// ``` - /// # use rimage::{Decoder, ImageData, ColorSpace}; - /// # use std::path; - /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); - /// # let d = Decoder::build(&path).unwrap(); - /// # let image = d.decode().unwrap(); - /// let color_space = image.color_space(); - /// match color_space { - /// ColorSpace::Grayscale => println!("Grayscale"), - /// ColorSpace::Rgb => println!("RGB"), - /// ColorSpace::Cmyk => println!("CMYK"), - /// ColorSpace::Rgba => println!("RGBA"), - /// ColorSpace::Indexed => println!("Indexed"), - /// ColorSpace::GrayscaleAlpha => println!("Grayscale Alpha"), - /// } - /// ``` - /// [`ColorSpace`]: enum.ColorSpace.html - pub fn color_space(&self) -> &ColorSpace { - &self.color_space - } - /// Returns a ref to array of bytes in image - /// - /// # Examples - /// ``` - /// # use rimage::{Decoder, ImageData}; - /// # use std::path; - /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); - /// # let d = Decoder::build(&path).unwrap(); - /// # let image = d.decode().unwrap(); - /// let data = image.data(); - /// ``` - pub fn data(&self) -> &[u8] { - &self.data - } - /// Returns a ref to bit depth of image [`BitDepth`] - /// - /// # Examples - /// ``` - /// # use rimage::{Decoder, ImageData, BitDepth}; - /// # use std::path; - /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); - /// # let d = Decoder::build(&path).unwrap(); - /// # let image = d.decode().unwrap(); - /// let bit_depth = image.bit_depth(); - /// match bit_depth { - /// BitDepth::One => println!("1 bit"), - /// BitDepth::Two => println!("2 bits"), - /// BitDepth::Four => println!("4 bits"), - /// BitDepth::Eight => println!("8 bits"), - /// BitDepth::Sixteen => println!("16 bits"), - /// } - /// ``` - /// - /// [`BitDepth`]: enum.BitDepth.html - pub fn bit_depth(&self) -> &BitDepth { - &self.bit_depth - } -} - -impl From for DecodingError { - fn from(err: io::Error) -> Self { - DecodingError::IoError(err) - } -} - -impl From for DecodingError { - fn from(err: png::DecodingError) -> Self { - match err { - png::DecodingError::IoError(io_err) => DecodingError::IoError(io_err), - png::DecodingError::Format(f_err) => DecodingError::Format(f_err.to_string()), - png::DecodingError::Parameter(p_err) => DecodingError::Parsing(p_err.to_string()), - png::DecodingError::LimitsExceeded => { - DecodingError::Parsing("Png limits exceeded".to_string()) - } - } - } -} - -impl From for ColorSpace { - fn from(color_type: png::ColorType) -> Self { - match color_type { - png::ColorType::Grayscale => ColorSpace::Grayscale, - png::ColorType::Rgb => ColorSpace::Rgb, - png::ColorType::Indexed => ColorSpace::Indexed, - png::ColorType::GrayscaleAlpha => ColorSpace::GrayscaleAlpha, - png::ColorType::Rgba => ColorSpace::Rgba, - } - } -} - -impl From for BitDepth { - fn from(bit_depth: png::BitDepth) -> Self { - match bit_depth { - png::BitDepth::One => BitDepth::One, - png::BitDepth::Two => BitDepth::Two, - png::BitDepth::Four => BitDepth::Four, - png::BitDepth::Eight => BitDepth::Eight, - png::BitDepth::Sixteen => BitDepth::Sixteen, - } - } -} - -impl fmt::Display for DecodingError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - DecodingError::IoError(io_err) => write!(f, "IO Error: {}", io_err), - DecodingError::Format(fmt_err) => write!(f, "Format Error: {}", fmt_err), - DecodingError::Parsing(prs_err) => write!(f, "Parsing Error: {}", prs_err), - } - } -} - -impl fmt::Display for ConfigError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ConfigError::QualityOutOfBounds => write!(f, "Quality is out of bounds"), - ConfigError::WidthIsZero => write!(f, "Width cannot be zero"), - ConfigError::HeightIsZero => write!(f, "Height cannot be zero"), - ConfigError::SizeIsZero => write!(f, "Size cannot be zero"), - ConfigError::InputIsEmpty => write!(f, "Input cannot be zero"), - } - } +/// Encoder used to encode image data to file +pub struct Encoder<'a> { + path: &'a path::PathBuf, + image_data: ImageData, + quality: f32, } #[cfg(test)] @@ -656,37 +281,6 @@ mod tests { use super::*; - #[test] - fn display_decoder_error() { - assert_eq!( - DecodingError::IoError(io::Error::new(io::ErrorKind::NotFound, "path not found")) - .to_string(), - "IO Error: path not found" - ); - assert_eq!( - DecodingError::Format("WebP not supported".to_string()).to_string(), - "Format Error: WebP not supported" - ); - } - - #[test] - fn display_config_error() { - assert_eq!( - ConfigError::QualityOutOfBounds.to_string(), - "Quality is out of bounds" - ); - assert_eq!(ConfigError::WidthIsZero.to_string(), "Width cannot be zero"); - assert_eq!( - ConfigError::HeightIsZero.to_string(), - "Height cannot be zero" - ); - assert_eq!(ConfigError::SizeIsZero.to_string(), "Size cannot be zero"); - assert_eq!( - ConfigError::InputIsEmpty.to_string(), - "Input cannot be zero" - ) - } - #[test] fn decode_grayscale() { let files: Vec = fs::read_dir("tests/files/") @@ -706,7 +300,7 @@ mod tests { println!("{path:?}"); - assert_eq!(image.color_space(), &ColorSpace::Grayscale); + assert_eq!(image.color_space(), &ColorSpace::Gray); assert_ne!(image.data().len(), 0); assert_ne!(image.size(), (0, 0)); }) @@ -729,7 +323,7 @@ mod tests { files.iter().for_each(|path| { let image = Decoder::build(path).unwrap().decode().unwrap(); - assert_eq!(image.color_space(), &ColorSpace::GrayscaleAlpha); + assert_eq!(image.color_space(), &ColorSpace::GrayAlpha); assert_ne!(image.data().len(), 0); assert_ne!(image.size(), (0, 0)); }) From 166c27ea303746402f20c58fd7749bc5c9afe4dc Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Sat, 25 Mar 2023 22:02:42 +0100 Subject: [PATCH 27/46] Refactor re exports --- src/error.rs | 4 ++-- src/image.rs | 2 +- src/lib.rs | 5 +++-- src/main.rs | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/error.rs b/src/error.rs index e6a932cc..b05491c9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,7 +4,7 @@ use std::{fmt, io}; /// /// # Examples /// ``` -/// # use rimage::{Config, ConfigError}; +/// # use rimage::{Config, error::ConfigError}; /// let config = Config::build(&[], 1.1, rimage::OutputFormat::MozJpeg); /// match config { /// Ok(_) => println!("Config is valid"), @@ -43,7 +43,7 @@ pub enum ConfigError { /// /// # Examples /// ``` -/// # use rimage::{Decoder, DecodingError}; +/// # use rimage::{Decoder, error::DecodingError}; /// # use std::path; /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); /// let d = Decoder::build(&path)?; diff --git a/src/image.rs b/src/image.rs index 84de5128..e9d669de 100644 --- a/src/image.rs +++ b/src/image.rs @@ -5,7 +5,7 @@ use std::{borrow::Cow, fmt}; /// # Examples /// /// ``` -/// # use rimage::{Decoder, DecodingError}; +/// # use rimage::{Decoder, error::DecodingError}; /// # use std::path; /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); /// let d = Decoder::build(&path)?; diff --git a/src/lib.rs b/src/lib.rs index fdf9b368..8e423144 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,10 +62,11 @@ println!("Data length: {:?}", image.data().len()); */ #![warn(missing_docs)] +use error::{ConfigError, DecodingError}; use std::{fs, panic, path}; -pub use error::{ConfigError, DecodingError}; -pub use image::{BitDepth, ColorSpace, ImageData, OutputFormat}; +pub use image::ImageData; +use image::{BitDepth, ColorSpace, OutputFormat}; /// Decoders for images #[deprecated(since = "0.2.0", note = "use the Decoder struct instead")] diff --git a/src/main.rs b/src/main.rs index 15b51ecf..c3597376 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use std::{io, path, process}; use clap::Parser; use indicatif::{ProgressBar, ProgressStyle}; -use rimage::{decoders, encoders, Config, Decoder, OutputFormat}; +use rimage::{decoders, encoders, image::OutputFormat, Config, Decoder}; #[derive(Parser)] #[command(author, about, version, long_about = None)] From 3ff1e751ea7134dfa182ed38967e7ab9fcfde351 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Sun, 26 Mar 2023 12:51:33 +0200 Subject: [PATCH 28/46] Rewrite errors --- src/error.rs | 87 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 8 deletions(-) diff --git a/src/error.rs b/src/error.rs index b05491c9..4b0371ce 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -use std::{fmt, io}; +use std::{error::Error, fmt, io}; /// An error that occurred if configuration is invalid /// @@ -39,6 +39,20 @@ pub enum ConfigError { InputIsEmpty, } +impl Error for ConfigError {} + +impl fmt::Display for ConfigError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ConfigError::QualityOutOfBounds => write!(f, "Quality is out of bounds"), + ConfigError::WidthIsZero => write!(f, "Width cannot be zero"), + ConfigError::HeightIsZero => write!(f, "Height cannot be zero"), + ConfigError::SizeIsZero => write!(f, "Size cannot be zero"), + ConfigError::InputIsEmpty => write!(f, "Input cannot be zero"), + } + } +} + /// An error that occurred during decoding a image /// /// # Examples @@ -64,6 +78,8 @@ pub enum DecodingError { Parsing(String), } +impl Error for DecodingError {} + impl From for DecodingError { #[inline] fn from(err: io::Error) -> Self { @@ -94,14 +110,69 @@ impl fmt::Display for DecodingError { } } -impl fmt::Display for ConfigError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +/// An error that occurred during encoding a image +#[derive(Debug)] +pub enum EncodingError { + /// A [`io::Error`] if file failed to write, find, etc. + IoError(io::Error), + /// The format of file is not supported + Format(String), + /// A encoding error, data is invalid, unsupported color space, etc. + Encoding(String), +} + +impl Error for EncodingError {} + +impl From for EncodingError { + #[inline] + fn from(err: io::Error) -> Self { + EncodingError::IoError(err) + } +} + +impl From for EncodingError { + fn from(err: png::EncodingError) -> Self { + match err { + png::EncodingError::IoError(io_err) => EncodingError::IoError(io_err), + png::EncodingError::Format(f_err) => EncodingError::Format(f_err.to_string()), + png::EncodingError::Parameter(p_err) => EncodingError::Encoding(p_err.to_string()), + png::EncodingError::LimitsExceeded => { + EncodingError::Encoding("Png limits exceeded".to_string()) + } + } + } +} + +impl From for EncodingError { + fn from(err: oxipng::PngError) -> Self { + match err { + oxipng::PngError::DeflatedDataTooLong(_) => { + EncodingError::Encoding("Deflated data too long".to_string()) + } + oxipng::PngError::TimedOut => EncodingError::Encoding("Timed out".to_string()), + oxipng::PngError::NotPNG => EncodingError::Encoding("Not a PNG".to_string()), + oxipng::PngError::APNGNotSupported => { + EncodingError::Encoding("APNG not supported".to_string()) + } + oxipng::PngError::InvalidData => EncodingError::Encoding("Invalid data".to_string()), + oxipng::PngError::TruncatedData => { + EncodingError::Encoding("Truncated data".to_string()) + } + oxipng::PngError::ChunkMissing(_) => { + EncodingError::Encoding("Chunk missing".to_string()) + } + oxipng::PngError::Other(err) => EncodingError::Encoding(err.into_string()), + _ => EncodingError::Encoding("Unknown error".to_string()), + } + } +} + +impl fmt::Display for EncodingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - ConfigError::QualityOutOfBounds => write!(f, "Quality is out of bounds"), - ConfigError::WidthIsZero => write!(f, "Width cannot be zero"), - ConfigError::HeightIsZero => write!(f, "Height cannot be zero"), - ConfigError::SizeIsZero => write!(f, "Size cannot be zero"), - ConfigError::InputIsEmpty => write!(f, "Input cannot be zero"), + EncodingError::IoError(io_err) => write!(f, "IO Error: {}", io_err), + EncodingError::Format(fmt_err) => write!(f, "Format Error: {}", fmt_err), + EncodingError::Encoding(enc_err) => write!(f, "Encoding Error: {}", enc_err), } } } From 288675770b7e3ea0bac2bd340ffa41e9db60acd1 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Sun, 26 Mar 2023 12:52:01 +0200 Subject: [PATCH 29/46] Remove unused config from info command --- src/main.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index c3597376..79fa7435 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,12 +22,6 @@ struct Args { fn main() { let mut args = Args::parse_from(wild::args_os()); - let conf = if let Ok(conf) = Config::build(&args.input, args.quality, args.output_format) { - conf - } else { - eprintln!("Error: Invalid configuration."); - process::exit(1); - }; let pb = ProgressBar::new(args.input.len() as u64); if args.input.is_empty() { From f46f258ba32911afca6ac3af3b101d592e0c7b28 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Sun, 26 Mar 2023 12:52:21 +0200 Subject: [PATCH 30/46] Added imagequant to dependencies --- Cargo.lock | 25 +++++++++++++++++++++++++ Cargo.toml | 1 + 2 files changed, 26 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 1796591f..89f21d51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -426,6 +426,21 @@ dependencies = [ "png", ] +[[package]] +name = "imagequant" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f332f82fc531b53cffa3181c14f7beb5b6c33932d68bb0c2fa4fd583553fca64" +dependencies = [ + "arrayvec", + "noisy_float", + "num_cpus", + "once_cell", + "rayon", + "rgb", + "thread_local", +] + [[package]] name = "indexmap" version = "1.9.2" @@ -604,6 +619,15 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce095842aee9aa3ecbda7a5d2a4df680375fd128a8596b6b56f8e497e231f483" +[[package]] +name = "noisy_float" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978fe6e6ebc0bf53de533cd456ca2d9de13de13856eda1518a285d7705a213af" +dependencies = [ + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -826,6 +850,7 @@ dependencies = [ "bytemuck", "clap 4.1.13", "criterion", + "imagequant", "indicatif", "mozjpeg", "oxipng", diff --git a/Cargo.toml b/Cargo.toml index a08a47a7..33acdeb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ debug = 1 [dependencies] bytemuck = "1.13.1" clap = { version = "4.1.11", features = ["derive"] } +imagequant = "4.1.1" indicatif = "0.17.3" mozjpeg = "0.9.4" oxipng = "8.0.0" From 4c937ebc739479e305c5c08996ea5400d2e8bde7 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Sun, 26 Mar 2023 12:53:19 +0200 Subject: [PATCH 31/46] Rewrite crate structure, add bit depth convert fn --- src/image.rs | 383 +++++++++++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 205 ++++++++++++++++++++++----- 2 files changed, 547 insertions(+), 41 deletions(-) diff --git a/src/image.rs b/src/image.rs index e9d669de..c7e4ad35 100644 --- a/src/image.rs +++ b/src/image.rs @@ -19,6 +19,7 @@ use std::{borrow::Cow, fmt}; /// println!("Data length: {:?}", image.data().len()); /// # Ok::<(), DecodingError>(()) /// ``` +#[derive(Debug)] pub struct ImageData { color_space: ColorSpace, bit_depth: BitDepth, @@ -32,8 +33,8 @@ impl ImageData { /// /// # Examples /// ``` - /// # use rimage::{ImageData, ColorSpace, BitDepth}; - /// let image = ImageData::new(ColorSpace::Gray, BitDepth::Eight, 100, 100, vec![0; 100 * 100]); + /// # use rimage::{ImageData, image}; + /// let image = ImageData::new(image::ColorSpace::Gray, image::BitDepth::Eight, 100, 100, vec![0; 100 * 100]); /// ``` pub fn new( color_space: ColorSpace, @@ -69,7 +70,7 @@ impl ImageData { /// /// # Examples /// ``` - /// # use rimage::{Decoder, ImageData, ColorSpace}; + /// # use rimage::{Decoder, ImageData, image::ColorSpace}; /// # use std::path; /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); /// # let d = Decoder::build(&path).unwrap(); @@ -108,7 +109,7 @@ impl ImageData { /// /// # Examples /// ``` - /// # use rimage::{Decoder, ImageData, BitDepth}; + /// # use rimage::{Decoder, ImageData, image::BitDepth}; /// # use std::path; /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); /// # let d = Decoder::build(&path).unwrap(); @@ -128,6 +129,297 @@ impl ImageData { pub fn bit_depth(&self) -> &BitDepth { &self.bit_depth } + + pub fn convert_bit_depth(self, new_bit_depth: BitDepth) -> Self { + if self.bit_depth == new_bit_depth { + return self; + } + + match self.bit_depth { + BitDepth::One => match new_bit_depth { + BitDepth::One => return self, + BitDepth::Two => { + let mut new_data = Vec::with_capacity(self.data.len() * 2); + for byte in self.data.iter() { + for i in 0..2 { + let bit = (byte >> i) & 0b11; + new_data.push(bit); + } + } + return Self { + data: new_data, + bit_depth: new_bit_depth, + ..self + }; + } + BitDepth::Four => { + let mut new_data = Vec::with_capacity(self.data.len() * 4); + for byte in self.data.iter() { + for i in 0..4 { + let bit = (byte >> i) & 0b1111; + new_data.push(bit); + } + } + return Self { + data: new_data, + bit_depth: new_bit_depth, + ..self + }; + } + BitDepth::Eight => { + let mut new_data = Vec::with_capacity(self.data.len() * 8); + for byte in self.data.iter() { + for i in 0..8 { + let bit = (byte >> i) & 0b1111_1111; + new_data.push(bit); + } + } + return Self { + data: new_data, + bit_depth: new_bit_depth, + ..self + }; + } + BitDepth::Sixteen => { + let mut new_data = Vec::with_capacity(self.data.len() * 16); + for byte in self.data.iter() { + for i in 0..16 { + let bit = (byte >> i) & 0b1111_1111; + new_data.push(bit); + new_data.push(bit); + } + } + return Self { + data: new_data, + bit_depth: new_bit_depth, + ..self + }; + } + }, + BitDepth::Two => match new_bit_depth { + BitDepth::One => { + let mut new_data = Vec::with_capacity(self.data.len() / 2); + for byte in self.data.iter() { + let bit = (byte >> 6) & 0b11; + new_data.push(bit); + } + return Self { + data: new_data, + bit_depth: new_bit_depth, + ..self + }; + } + BitDepth::Two => return self, + BitDepth::Four => { + let mut new_data = Vec::with_capacity(self.data.len() * 2); + for byte in self.data.iter() { + for i in 0..2 { + let bit = (byte >> i) & 0b11; + new_data.push(bit); + } + } + return Self { + data: new_data, + bit_depth: new_bit_depth, + ..self + }; + } + BitDepth::Eight => { + let mut new_data = Vec::with_capacity(self.data.len() * 4); + for byte in self.data.iter() { + for i in 0..4 { + let bit = (byte >> i) & 0b11; + new_data.push(bit); + new_data.push(bit); + } + } + return Self { + data: new_data, + bit_depth: new_bit_depth, + ..self + }; + } + BitDepth::Sixteen => { + let mut new_data = Vec::with_capacity(self.data.len() * 8); + for byte in self.data.iter() { + for i in 0..8 { + let bit = (byte >> i) & 0b11; + new_data.push(bit); + new_data.push(bit); + new_data.push(bit); + new_data.push(bit); + } + } + return Self { + data: new_data, + bit_depth: new_bit_depth, + ..self + }; + } + }, + BitDepth::Four => match new_bit_depth { + BitDepth::One => { + let mut new_data = Vec::with_capacity(self.data.len() / 4); + for byte in self.data.iter() { + let bit = (byte >> 4) & 0b1111; + new_data.push(bit); + } + return Self { + data: new_data, + bit_depth: new_bit_depth, + ..self + }; + } + BitDepth::Two => { + let mut new_data = Vec::with_capacity(self.data.len() / 2); + for byte in self.data.iter() { + let bit = (byte >> 4) & 0b11; + new_data.push(bit); + } + return Self { + data: new_data, + bit_depth: new_bit_depth, + ..self + }; + } + BitDepth::Four => return self, + BitDepth::Eight => { + let mut new_data = Vec::with_capacity(self.data.len() * 2); + for byte in self.data.iter() { + for i in 0..2 { + let bit = (byte >> i) & 0b1111; + new_data.push(bit); + } + } + return Self { + data: new_data, + bit_depth: new_bit_depth, + ..self + }; + } + BitDepth::Sixteen => { + let mut new_data = Vec::with_capacity(self.data.len() * 4); + for byte in self.data.iter() { + for i in 0..4 { + let bit = (byte >> i) & 0b1111; + new_data.push(bit); + new_data.push(bit); + } + } + return Self { + data: new_data, + bit_depth: new_bit_depth, + ..self + }; + } + }, + BitDepth::Eight => match new_bit_depth { + BitDepth::One => { + let mut new_data = Vec::with_capacity(self.data.len() / 8); + for byte in self.data.iter() { + let bit = (byte >> 8) & 0b1111_1111; + new_data.push(bit); + } + return Self { + data: new_data, + bit_depth: new_bit_depth, + ..self + }; + } + BitDepth::Two => { + let mut new_data = Vec::with_capacity(self.data.len() / 4); + for byte in self.data.iter() { + let bit = (byte >> 8) & 0b11; + new_data.push(bit); + } + return Self { + data: new_data, + bit_depth: new_bit_depth, + ..self + }; + } + BitDepth::Four => { + let mut new_data = Vec::with_capacity(self.data.len() / 2); + for byte in self.data.iter() { + let bit = (byte >> 8) & 0b1111; + new_data.push(bit); + } + return Self { + data: new_data, + bit_depth: new_bit_depth, + ..self + }; + } + BitDepth::Eight => return self, + BitDepth::Sixteen => { + let mut new_data = Vec::with_capacity(self.data.len() * 2); + for byte in self.data.iter() { + for i in 0..2 { + let bit = (byte >> i) & 0b1111_1111; + new_data.push(bit); + new_data.push(bit); + } + } + return Self { + data: new_data, + bit_depth: new_bit_depth, + ..self + }; + } + }, + BitDepth::Sixteen => match new_bit_depth { + BitDepth::One => { + let mut new_data = Vec::with_capacity(self.data.len() / 16); + for byte in self.data.iter() { + let bit = (byte >> 16) & 0b1111_1111; + new_data.push(bit); + } + return Self { + data: new_data, + bit_depth: new_bit_depth, + ..self + }; + } + BitDepth::Two => { + let mut new_data = Vec::with_capacity(self.data.len() / 8); + for byte in self.data.iter() { + let bit = (byte >> 16) & 0b11; + new_data.push(bit); + } + return Self { + data: new_data, + bit_depth: new_bit_depth, + ..self + }; + } + BitDepth::Four => { + let mut new_data = Vec::with_capacity(self.data.len() / 4); + for byte in self.data.iter() { + let bit = (byte >> 16) & 0b1111; + new_data.push(bit); + } + return Self { + data: new_data, + bit_depth: new_bit_depth, + ..self + }; + } + BitDepth::Eight => { + let mut new_data = Vec::with_capacity(self.data.len() / 2); + for byte in self.data.iter() { + let bit = (byte >> 16) & 0b1111_1111; + new_data.push(bit); + } + return Self { + data: new_data, + bit_depth: new_bit_depth, + ..self + }; + } + BitDepth::Sixteen => return self, + }, + } + } } /// Supported output format @@ -176,7 +468,7 @@ impl fmt::Display for OutputFormat { /// /// # Examples /// ``` -/// # use rimage::ColorSpace; +/// # use rimage::image::ColorSpace; /// # use std::str::FromStr; /// let color_space = ColorSpace::from_str("rgb").unwrap(); /// println!("Color Space: {}", color_space); @@ -187,7 +479,7 @@ impl fmt::Display for OutputFormat { /// - [`ColorSpace::from_str`] if color space is not supported /// /// [`ColorSpace::from_str`]: enum.ColorSpace.html#method.from_str -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ColorSpace { /// **R**ed/**G**reen/**B**lue Rgb, @@ -215,6 +507,43 @@ impl From for ColorSpace { } } +impl From for ColorSpace { + fn from(color_space: mozjpeg::ColorSpace) -> Self { + match color_space { + mozjpeg::ColorSpace::JCS_GRAYSCALE => ColorSpace::Gray, + mozjpeg::ColorSpace::JCS_CMYK => ColorSpace::Cmyk, + mozjpeg::ColorSpace::JCS_RGB => ColorSpace::Rgb, + _ => ColorSpace::Rgb, + } + } +} + +impl Into for ColorSpace { + fn into(self) -> mozjpeg::ColorSpace { + match self { + ColorSpace::Rgb => mozjpeg::ColorSpace::JCS_RGB, + ColorSpace::Rgba => mozjpeg::ColorSpace::JCS_RGB, + ColorSpace::Cmyk => mozjpeg::ColorSpace::JCS_CMYK, + ColorSpace::Indexed => mozjpeg::ColorSpace::JCS_RGB, + ColorSpace::Gray => mozjpeg::ColorSpace::JCS_GRAYSCALE, + ColorSpace::GrayAlpha => mozjpeg::ColorSpace::JCS_GRAYSCALE, + } + } +} + +impl Into for ColorSpace { + fn into(self) -> png::ColorType { + match self { + ColorSpace::Rgb => png::ColorType::Rgb, + ColorSpace::Rgba => png::ColorType::Rgba, + ColorSpace::Cmyk => png::ColorType::Rgb, + ColorSpace::Indexed => png::ColorType::Indexed, + ColorSpace::Gray => png::ColorType::Grayscale, + ColorSpace::GrayAlpha => png::ColorType::GrayscaleAlpha, + } + } +} + impl std::str::FromStr for ColorSpace { type Err = Cow<'static, str>; @@ -248,7 +577,7 @@ impl fmt::Display for ColorSpace { /// /// # Examples /// ``` -/// # use rimage::BitDepth; +/// # use rimage::image::BitDepth; /// # use std::str::FromStr; /// let bit_depth = BitDepth::from_str("8").unwrap(); /// println!("Bit Depth: {}", bit_depth); @@ -259,7 +588,7 @@ impl fmt::Display for ColorSpace { /// - [`BitDepth::from_str`] if bit depth is not supported /// /// [`BitDepth::from_str`]: enum.BitDepth.html#method.from_str -#[derive(Debug)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum BitDepth { /// One bit per pixel @@ -286,6 +615,18 @@ impl From for BitDepth { } } +impl Into for BitDepth { + fn into(self) -> png::BitDepth { + match self { + BitDepth::One => png::BitDepth::One, + BitDepth::Two => png::BitDepth::Two, + BitDepth::Four => png::BitDepth::Four, + BitDepth::Eight => png::BitDepth::Eight, + BitDepth::Sixteen => png::BitDepth::Sixteen, + } + } +} + impl std::str::FromStr for BitDepth { type Err = String; @@ -312,3 +653,29 @@ impl fmt::Display for BitDepth { } } } + +#[cfg(test)] +mod tests { + use std::path; + + use crate::Decoder; + + use super::*; + + #[test] + fn test_convert_bit_depth() { + let image = Decoder::build(&path::PathBuf::from("tests/files/basi0g01.png")) + .unwrap() + .decode() + .unwrap(); + + assert_eq!(image.bit_depth(), &BitDepth::One); + assert_eq!(image.data().len(), 128); + let image = image.convert_bit_depth(BitDepth::Eight); + assert_eq!(image.bit_depth(), &BitDepth::Eight); + assert_eq!(image.data().len(), 1024); + let image = image.convert_bit_depth(BitDepth::Sixteen); + assert_eq!(image.bit_depth(), &BitDepth::Sixteen); + assert_eq!(image.data().len(), 4096); + } +} diff --git a/src/lib.rs b/src/lib.rs index 8e423144..900b675a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,11 +62,11 @@ println!("Data length: {:?}", image.data().len()); */ #![warn(missing_docs)] -use error::{ConfigError, DecodingError}; +use error::{ConfigError, DecodingError, EncodingError}; use std::{fs, panic, path}; -pub use image::ImageData; -use image::{BitDepth, ColorSpace, OutputFormat}; +use image::BitDepth; +pub use image::{ImageData, OutputFormat}; /// Decoders for images #[deprecated(since = "0.2.0", note = "use the Decoder struct instead")] @@ -83,13 +83,13 @@ pub mod image; /// Config for encoder #[derive(Debug)] -pub struct Config<'a> { - input: &'a [path::PathBuf], +pub struct Config { + output: path::PathBuf, quality: f32, output_format: OutputFormat, } -impl<'a> Config<'a> { +impl Config { /// Builds config from parameters /// /// # Examples @@ -107,7 +107,7 @@ impl<'a> Config<'a> { /// - [`ConfigError::QualityOutOfBounds`] if quality is not in range 0.0 - 100.0 /// - [`ConfigError::InputIsEmpty`] if input is empty pub fn build( - input: &'a [path::PathBuf], + output: path::PathBuf, quality: f32, output_format: OutputFormat, ) -> Result { @@ -115,20 +115,20 @@ impl<'a> Config<'a> { return Err(ConfigError::QualityOutOfBounds); } - if input.is_empty() { + if output.to_str().unwrap().is_empty() { return Err(ConfigError::InputIsEmpty); } Ok(Config { - input, + output, quality, output_format, }) } #[inline] /// Gets input array of paths from config - pub fn input(&self) -> &[path::PathBuf] { - &self.input + pub fn input(&self) -> &path::PathBuf { + &self.output } #[inline] /// Gets quality of output images from config @@ -142,11 +142,11 @@ impl<'a> Config<'a> { } } -impl<'a> Default for Config<'a> { +impl Default for Config { #[inline] fn default() -> Self { Self { - input: &[], + output: path::PathBuf::from(""), quality: 75.0, output_format: OutputFormat::MozJpeg, } @@ -224,12 +224,7 @@ impl<'a> Decoder<'a> { fn decode_jpeg(&self) -> Result { panic::catch_unwind(|| -> Result { let d = mozjpeg::Decompress::new_mem(&self.raw_data)?; - let color_space = match d.color_space() { - mozjpeg::ColorSpace::JCS_GRAYSCALE => ColorSpace::Gray, - mozjpeg::ColorSpace::JCS_CMYK => ColorSpace::Cmyk, - mozjpeg::ColorSpace::JCS_RGB => ColorSpace::Rgb, - _ => ColorSpace::Rgb, - }; + let color_space = d.color_space().into(); let mut image = match d.image()? { mozjpeg::Format::RGB(img) => img, mozjpeg::Format::Gray(img) => img, @@ -258,7 +253,7 @@ impl<'a> Decoder<'a> { let mut reader = d.read_info()?; let mut buf = vec![0; reader.output_buffer_size()]; let info = reader.next_frame(&mut buf)?; - let data = buf[..info.buffer_size()].into(); + let data = buf[..info.buffer_size()].to_vec(); Ok(ImageData::new( info.color_type.into(), info.bit_depth.into(), @@ -271,30 +266,141 @@ impl<'a> Decoder<'a> { /// Encoder used to encode image data to file pub struct Encoder<'a> { - path: &'a path::PathBuf, image_data: ImageData, - quality: f32, + config: &'a Config, +} + +// Write Encoder encode method to encode image data to file in different formats from config +// Write Encoder build method to build encoder from Config and ImageData +// Write Encoder encode_png method to encode image data to png file +// Write Encoder encode_jpeg method to encode image data to jpeg file +impl<'a> Encoder<'a> { + /// Builds encoder from config and image data + /// + /// # Examples + /// ``` + /// # use rimage::{Encoder, Config, ImageData, image}; + /// let config = Config::default(); + /// let image_data = ImageData::new(image::ColorSpace::Rgb, image::BitDepth::Eight, 8, 8, vec![0; 192]); + /// let encoder = Encoder::new(&config, image_data); + /// ``` + pub fn new(conf: &'a Config, image_data: ImageData) -> Self { + Encoder { + image_data, + config: conf, + } + } + + /// Encodes image data to file + /// + /// # Examples + /// ``` + /// # use rimage::{Encoder, Config, ImageData, image}; + /// # let config = Config::default(); + /// # let image_data = ImageData::new(image::ColorSpace::Rgb, image::BitDepth::Eight, 8, 8, vec![0; 192]); + /// # let encoder = Encoder::new(&config, image_data); + /// encoder.encode(); + /// ``` + /// + /// # Errors + /// + /// - [`EncodingError::Encoding`] if encoding failed + /// - [`EncodingError::IoError`] if IO error occurred + pub fn encode(self) -> Result<(), EncodingError> { + match self.config.output_format { + OutputFormat::Png => self.encode_png(), + OutputFormat::Oxipng => self.encode_oxipng(), + OutputFormat::MozJpeg => self.encode_mozjpeg(), + } + } + + fn encode_mozjpeg(self) -> Result<(), EncodingError> { + let image_data = self.image_data.convert_bit_depth(BitDepth::Eight); + panic::catch_unwind(|| -> Result<(), EncodingError> { + let mut encoder = mozjpeg::Compress::new((*image_data.color_space()).into()); + println!("ImgData: {:?}", image_data); + println!("Config: {:?}", self.config); + println!("Data len: {}", image_data.data().len()); + + encoder.set_size(image_data.size().0, image_data.size().1); + encoder.set_quality(self.config.quality); + encoder.set_progressive_mode(); + encoder.set_mem_dest(); + encoder.start_compress(); + encoder.write_scanlines(&image_data.data()); + encoder.finish_compress(); + + let data = encoder.data_as_mut_slice().unwrap(); + + fs::write(&self.config.output, data)?; + + Ok(()) + }) + .unwrap_or(Err(EncodingError::Encoding( + "Failed to encode jpeg".to_string(), + ))) + } + + fn encode_png(&self) -> Result<(), EncodingError> { + let mut encoder = png::Encoder::new( + fs::File::create(&self.config.output)?, + self.image_data.size().0 as u32, + self.image_data.size().1 as u32, + ); + + encoder.set_color((*self.image_data.color_space()).into()); + encoder.set_depth((*self.image_data.bit_depth()).into()); + let mut writer = encoder.write_header()?; + writer.write_image_data(&self.image_data.data())?; + + Ok(()) + } + + fn encode_oxipng(&self) -> Result<(), EncodingError> { + let mut file = fs::File::create(&self.config.output)?; + let mut encoder = png::Encoder::new( + &mut file, + self.image_data.size().0 as u32, + self.image_data.size().1 as u32, + ); + encoder.set_color((*self.image_data.color_space()).into()); + encoder.set_depth((*self.image_data.bit_depth()).into()); + let mut writer = encoder.write_header()?; + writer.write_image_data(&self.image_data.data())?; + writer.finish()?; + + oxipng::optimize( + &oxipng::InFile::from(&self.config.output), + &oxipng::OutFile::Path(Some(self.config.output.clone())), + &oxipng::Options::default(), + )?; + + Ok(()) + } } #[cfg(test)] mod tests { use regex::Regex; + use crate::image::ColorSpace; + use super::*; #[test] fn decode_grayscale() { - let files: Vec = fs::read_dir("tests/files/") - .unwrap() - .map(|entry| { - let entry = entry.unwrap(); - entry.path() - }) - .filter(|path| { - let re = Regex::new(r"^tests/files/[^x].+0g\d\d((\.png)|(\.jpg))").unwrap(); - re.is_match(path.to_str().unwrap_or("")) - }) - .collect(); + // let files: Vec = fs::read_dir("tests/files/") + // .unwrap() + // .map(|entry| { + // let entry = entry.unwrap(); + // entry.path() + // }) + // .filter(|path| { + // let re = Regex::new(r"^tests/files/[^x].+0g\d\d((\.png)|(\.jpg))").unwrap(); + // re.is_match(path.to_str().unwrap_or("")) + // }) + // .collect(); + let files = [path::PathBuf::from("tests/files/basi0g08.png")]; files.iter().for_each(|path| { let image = Decoder::build(path).unwrap().decode().unwrap(); @@ -421,4 +527,37 @@ mod tests { assert!(img.is_err()); }) } + + #[test] + fn encode_grayscale_jpeg() { + let files: Vec = fs::read_dir("tests/files/") + .unwrap() + .map(|entry| { + let entry = entry.unwrap(); + entry.path() + }) + .filter(|path| { + let re = Regex::new(r"^tests/files/[^x].+0g\d\d((\.png)|(\.jpg))").unwrap(); + re.is_match(path.to_str().unwrap_or("")) + }) + .collect(); + + files.iter().for_each(|path| { + let image = Decoder::build(path).unwrap().decode().unwrap(); + + let out_path = path.with_extension("out.jpg"); + + let conf = Config::build(out_path.clone(), 75.0, OutputFormat::MozJpeg).unwrap(); + + let encoder = Encoder::new(&conf, image); + let result = encoder.encode(); + println!("{path:?}: {result:?}"); + + assert!(result.is_ok()); + assert!(out_path.exists()); + assert!(out_path.is_file()); + assert!(out_path.metadata().unwrap().len() > 0); + // assert!(fs::remove_file(out_path).is_ok()); + }) + } } From 01d82bfedc264729f6b90cb03f4a1c31af39bbbf Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Sun, 26 Mar 2023 22:04:29 +0200 Subject: [PATCH 32/46] Removed convert bit depth method --- src/image.rs | 317 --------------------------------------------------- src/lib.rs | 11 +- 2 files changed, 5 insertions(+), 323 deletions(-) diff --git a/src/image.rs b/src/image.rs index c7e4ad35..8e247d2e 100644 --- a/src/image.rs +++ b/src/image.rs @@ -129,297 +129,6 @@ impl ImageData { pub fn bit_depth(&self) -> &BitDepth { &self.bit_depth } - - pub fn convert_bit_depth(self, new_bit_depth: BitDepth) -> Self { - if self.bit_depth == new_bit_depth { - return self; - } - - match self.bit_depth { - BitDepth::One => match new_bit_depth { - BitDepth::One => return self, - BitDepth::Two => { - let mut new_data = Vec::with_capacity(self.data.len() * 2); - for byte in self.data.iter() { - for i in 0..2 { - let bit = (byte >> i) & 0b11; - new_data.push(bit); - } - } - return Self { - data: new_data, - bit_depth: new_bit_depth, - ..self - }; - } - BitDepth::Four => { - let mut new_data = Vec::with_capacity(self.data.len() * 4); - for byte in self.data.iter() { - for i in 0..4 { - let bit = (byte >> i) & 0b1111; - new_data.push(bit); - } - } - return Self { - data: new_data, - bit_depth: new_bit_depth, - ..self - }; - } - BitDepth::Eight => { - let mut new_data = Vec::with_capacity(self.data.len() * 8); - for byte in self.data.iter() { - for i in 0..8 { - let bit = (byte >> i) & 0b1111_1111; - new_data.push(bit); - } - } - return Self { - data: new_data, - bit_depth: new_bit_depth, - ..self - }; - } - BitDepth::Sixteen => { - let mut new_data = Vec::with_capacity(self.data.len() * 16); - for byte in self.data.iter() { - for i in 0..16 { - let bit = (byte >> i) & 0b1111_1111; - new_data.push(bit); - new_data.push(bit); - } - } - return Self { - data: new_data, - bit_depth: new_bit_depth, - ..self - }; - } - }, - BitDepth::Two => match new_bit_depth { - BitDepth::One => { - let mut new_data = Vec::with_capacity(self.data.len() / 2); - for byte in self.data.iter() { - let bit = (byte >> 6) & 0b11; - new_data.push(bit); - } - return Self { - data: new_data, - bit_depth: new_bit_depth, - ..self - }; - } - BitDepth::Two => return self, - BitDepth::Four => { - let mut new_data = Vec::with_capacity(self.data.len() * 2); - for byte in self.data.iter() { - for i in 0..2 { - let bit = (byte >> i) & 0b11; - new_data.push(bit); - } - } - return Self { - data: new_data, - bit_depth: new_bit_depth, - ..self - }; - } - BitDepth::Eight => { - let mut new_data = Vec::with_capacity(self.data.len() * 4); - for byte in self.data.iter() { - for i in 0..4 { - let bit = (byte >> i) & 0b11; - new_data.push(bit); - new_data.push(bit); - } - } - return Self { - data: new_data, - bit_depth: new_bit_depth, - ..self - }; - } - BitDepth::Sixteen => { - let mut new_data = Vec::with_capacity(self.data.len() * 8); - for byte in self.data.iter() { - for i in 0..8 { - let bit = (byte >> i) & 0b11; - new_data.push(bit); - new_data.push(bit); - new_data.push(bit); - new_data.push(bit); - } - } - return Self { - data: new_data, - bit_depth: new_bit_depth, - ..self - }; - } - }, - BitDepth::Four => match new_bit_depth { - BitDepth::One => { - let mut new_data = Vec::with_capacity(self.data.len() / 4); - for byte in self.data.iter() { - let bit = (byte >> 4) & 0b1111; - new_data.push(bit); - } - return Self { - data: new_data, - bit_depth: new_bit_depth, - ..self - }; - } - BitDepth::Two => { - let mut new_data = Vec::with_capacity(self.data.len() / 2); - for byte in self.data.iter() { - let bit = (byte >> 4) & 0b11; - new_data.push(bit); - } - return Self { - data: new_data, - bit_depth: new_bit_depth, - ..self - }; - } - BitDepth::Four => return self, - BitDepth::Eight => { - let mut new_data = Vec::with_capacity(self.data.len() * 2); - for byte in self.data.iter() { - for i in 0..2 { - let bit = (byte >> i) & 0b1111; - new_data.push(bit); - } - } - return Self { - data: new_data, - bit_depth: new_bit_depth, - ..self - }; - } - BitDepth::Sixteen => { - let mut new_data = Vec::with_capacity(self.data.len() * 4); - for byte in self.data.iter() { - for i in 0..4 { - let bit = (byte >> i) & 0b1111; - new_data.push(bit); - new_data.push(bit); - } - } - return Self { - data: new_data, - bit_depth: new_bit_depth, - ..self - }; - } - }, - BitDepth::Eight => match new_bit_depth { - BitDepth::One => { - let mut new_data = Vec::with_capacity(self.data.len() / 8); - for byte in self.data.iter() { - let bit = (byte >> 8) & 0b1111_1111; - new_data.push(bit); - } - return Self { - data: new_data, - bit_depth: new_bit_depth, - ..self - }; - } - BitDepth::Two => { - let mut new_data = Vec::with_capacity(self.data.len() / 4); - for byte in self.data.iter() { - let bit = (byte >> 8) & 0b11; - new_data.push(bit); - } - return Self { - data: new_data, - bit_depth: new_bit_depth, - ..self - }; - } - BitDepth::Four => { - let mut new_data = Vec::with_capacity(self.data.len() / 2); - for byte in self.data.iter() { - let bit = (byte >> 8) & 0b1111; - new_data.push(bit); - } - return Self { - data: new_data, - bit_depth: new_bit_depth, - ..self - }; - } - BitDepth::Eight => return self, - BitDepth::Sixteen => { - let mut new_data = Vec::with_capacity(self.data.len() * 2); - for byte in self.data.iter() { - for i in 0..2 { - let bit = (byte >> i) & 0b1111_1111; - new_data.push(bit); - new_data.push(bit); - } - } - return Self { - data: new_data, - bit_depth: new_bit_depth, - ..self - }; - } - }, - BitDepth::Sixteen => match new_bit_depth { - BitDepth::One => { - let mut new_data = Vec::with_capacity(self.data.len() / 16); - for byte in self.data.iter() { - let bit = (byte >> 16) & 0b1111_1111; - new_data.push(bit); - } - return Self { - data: new_data, - bit_depth: new_bit_depth, - ..self - }; - } - BitDepth::Two => { - let mut new_data = Vec::with_capacity(self.data.len() / 8); - for byte in self.data.iter() { - let bit = (byte >> 16) & 0b11; - new_data.push(bit); - } - return Self { - data: new_data, - bit_depth: new_bit_depth, - ..self - }; - } - BitDepth::Four => { - let mut new_data = Vec::with_capacity(self.data.len() / 4); - for byte in self.data.iter() { - let bit = (byte >> 16) & 0b1111; - new_data.push(bit); - } - return Self { - data: new_data, - bit_depth: new_bit_depth, - ..self - }; - } - BitDepth::Eight => { - let mut new_data = Vec::with_capacity(self.data.len() / 2); - for byte in self.data.iter() { - let bit = (byte >> 16) & 0b1111_1111; - new_data.push(bit); - } - return Self { - data: new_data, - bit_depth: new_bit_depth, - ..self - }; - } - BitDepth::Sixteen => return self, - }, - } - } } /// Supported output format @@ -653,29 +362,3 @@ impl fmt::Display for BitDepth { } } } - -#[cfg(test)] -mod tests { - use std::path; - - use crate::Decoder; - - use super::*; - - #[test] - fn test_convert_bit_depth() { - let image = Decoder::build(&path::PathBuf::from("tests/files/basi0g01.png")) - .unwrap() - .decode() - .unwrap(); - - assert_eq!(image.bit_depth(), &BitDepth::One); - assert_eq!(image.data().len(), 128); - let image = image.convert_bit_depth(BitDepth::Eight); - assert_eq!(image.bit_depth(), &BitDepth::Eight); - assert_eq!(image.data().len(), 1024); - let image = image.convert_bit_depth(BitDepth::Sixteen); - assert_eq!(image.bit_depth(), &BitDepth::Sixteen); - assert_eq!(image.data().len(), 4096); - } -} diff --git a/src/lib.rs b/src/lib.rs index 900b675a..1bea6ee5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -315,19 +315,18 @@ impl<'a> Encoder<'a> { } fn encode_mozjpeg(self) -> Result<(), EncodingError> { - let image_data = self.image_data.convert_bit_depth(BitDepth::Eight); panic::catch_unwind(|| -> Result<(), EncodingError> { - let mut encoder = mozjpeg::Compress::new((*image_data.color_space()).into()); - println!("ImgData: {:?}", image_data); + let mut encoder = mozjpeg::Compress::new((*self.image_data.color_space()).into()); + println!("ImgData: {:?}", self.image_data); println!("Config: {:?}", self.config); - println!("Data len: {}", image_data.data().len()); + println!("Data len: {}", self.image_data.data().len()); - encoder.set_size(image_data.size().0, image_data.size().1); + encoder.set_size(self.image_data.size().0, self.image_data.size().1); encoder.set_quality(self.config.quality); encoder.set_progressive_mode(); encoder.set_mem_dest(); encoder.start_compress(); - encoder.write_scanlines(&image_data.data()); + encoder.write_scanlines(&self.image_data.data()); encoder.finish_compress(); let data = encoder.data_as_mut_slice().unwrap(); From b4ca29660fd8f5d27c6a57166b0a17ae3f66004a Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Mon, 27 Mar 2023 00:20:52 +0200 Subject: [PATCH 33/46] Rewrite docs, removed BitDepth, refactor code --- src/error.rs | 38 -------------- src/image.rs | 138 ++++++--------------------------------------------- src/lib.rs | 122 ++++++++++++++++++++++----------------------- src/main.rs | 14 ++---- 4 files changed, 77 insertions(+), 235 deletions(-) diff --git a/src/error.rs b/src/error.rs index 4b0371ce..695e56c8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,30 +1,6 @@ use std::{error::Error, fmt, io}; /// An error that occurred if configuration is invalid -/// -/// # Examples -/// ``` -/// # use rimage::{Config, error::ConfigError}; -/// let config = Config::build(&[], 1.1, rimage::OutputFormat::MozJpeg); -/// match config { -/// Ok(_) => println!("Config is valid"), -/// Err(e) => println!("Error: {}", e), -/// } -/// ``` -/// -/// # Errors -/// -/// - [`ConfigError::QualityOutOfBounds`] if quality is less than 0 or greater than 1 -/// - [`ConfigError::InputIsEmpty`] if input is empty -/// - [`ConfigError::WidthIsZero`] if width is 0 -/// - [`ConfigError::HeightIsZero`] if height is 0 -/// - [`ConfigError::SizeIsZero`] if size is 0 -/// -/// [`ConfigError::QualityOutOfBounds`]: enum.ConfigError.html#variant.QualityOutOfBounds -/// [`ConfigError::InputIsEmpty`]: enum.ConfigError.html#variant.InputIsEmpty -/// [`ConfigError::WidthIsZero`]: enum.ConfigError.html#variant.WidthIsZero -/// [`ConfigError::HeightIsZero`]: enum.ConfigError.html#variant.HeightIsZero -/// [`ConfigError::SizeIsZero`]: enum.ConfigError.html#variant.SizeIsZero #[derive(Debug)] pub enum ConfigError { /// Quality is less than 0 or greater than 1 @@ -54,20 +30,6 @@ impl fmt::Display for ConfigError { } /// An error that occurred during decoding a image -/// -/// # Examples -/// ``` -/// # use rimage::{Decoder, error::DecodingError}; -/// # use std::path; -/// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); -/// let d = Decoder::build(&path)?; -/// let image = d.decode(); -/// match image { -/// Ok(_) => println!("Image decoded"), -/// Err(e) => println!("Error: {}", e), -/// } -/// # Ok::<(), DecodingError>(()) -/// ``` #[derive(Debug)] pub enum DecodingError { /// A [`io::Error`] if file failed to read, find, etc. diff --git a/src/image.rs b/src/image.rs index 8e247d2e..56a426f9 100644 --- a/src/image.rs +++ b/src/image.rs @@ -6,15 +6,15 @@ use std::{borrow::Cow, fmt}; /// /// ``` /// # use rimage::{Decoder, error::DecodingError}; -/// # use std::path; +/// # use std::{path, fs}; /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); -/// let d = Decoder::build(&path)?; +/// let data = fs::read(&path)?; +/// let d = Decoder::new(&path, &data); /// /// let image = d.decode()?; /// /// // Get something from image data /// println!("Color Space: {:?}", image.color_space()); -/// println!("Bit Depth: {:?}", image.bit_depth()); /// println!("Size: {:?}", image.size()); /// println!("Data length: {:?}", image.data().len()); /// # Ok::<(), DecodingError>(()) @@ -22,7 +22,6 @@ use std::{borrow::Cow, fmt}; #[derive(Debug)] pub struct ImageData { color_space: ColorSpace, - bit_depth: BitDepth, width: usize, height: usize, data: Vec, @@ -34,18 +33,11 @@ impl ImageData { /// # Examples /// ``` /// # use rimage::{ImageData, image}; - /// let image = ImageData::new(image::ColorSpace::Gray, image::BitDepth::Eight, 100, 100, vec![0; 100 * 100]); + /// let image = ImageData::new(image::ColorSpace::Gray, 100, 100, vec![0; 100 * 100]); /// ``` - pub fn new( - color_space: ColorSpace, - bit_depth: BitDepth, - width: usize, - height: usize, - data: Vec, - ) -> Self { + pub fn new(color_space: ColorSpace, width: usize, height: usize, data: Vec) -> Self { Self { color_space, - bit_depth, width, height, data, @@ -57,8 +49,9 @@ impl ImageData { /// ``` /// # use rimage::{Decoder, ImageData}; /// # use std::path; - /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); - /// # let d = Decoder::build(&path).unwrap(); + /// # let path = path::Path::new("tests/files/basi0g01.jpg"); + /// # let data = std::fs::read(&path).unwrap(); + /// # let d = Decoder::new(&path, &data); /// # let image = d.decode().unwrap(); /// let (width, height) = image.size(); /// ``` @@ -72,8 +65,9 @@ impl ImageData { /// ``` /// # use rimage::{Decoder, ImageData, image::ColorSpace}; /// # use std::path; - /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); - /// # let d = Decoder::build(&path).unwrap(); + /// # let path = path::Path::new("tests/files/basi0g01.jpg"); + /// # let data = std::fs::read(&path).unwrap(); + /// # let d = Decoder::new(&path, &data); /// # let image = d.decode().unwrap(); /// let color_space = image.color_space(); /// match color_space { @@ -96,8 +90,9 @@ impl ImageData { /// ``` /// # use rimage::{Decoder, ImageData}; /// # use std::path; - /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); - /// # let d = Decoder::build(&path).unwrap(); + /// # let path = path::Path::new("tests/files/basi0g01.jpg"); + /// # let data = std::fs::read(&path).unwrap(); + /// # let d = Decoder::new(&path, &data); /// # let image = d.decode().unwrap(); /// let data = image.data(); /// ``` @@ -105,30 +100,6 @@ impl ImageData { pub fn data(&self) -> &[u8] { &self.data } - /// Returns a ref to bit depth of image [`BitDepth`] - /// - /// # Examples - /// ``` - /// # use rimage::{Decoder, ImageData, image::BitDepth}; - /// # use std::path; - /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); - /// # let d = Decoder::build(&path).unwrap(); - /// # let image = d.decode().unwrap(); - /// let bit_depth = image.bit_depth(); - /// match bit_depth { - /// BitDepth::One => println!("1 bit"), - /// BitDepth::Two => println!("2 bits"), - /// BitDepth::Four => println!("4 bits"), - /// BitDepth::Eight => println!("8 bits"), - /// BitDepth::Sixteen => println!("16 bits"), - /// } - /// ``` - /// - /// [`BitDepth`]: enum.BitDepth.html - #[inline] - pub fn bit_depth(&self) -> &BitDepth { - &self.bit_depth - } } /// Supported output format @@ -281,84 +252,3 @@ impl fmt::Display for ColorSpace { } } } - -/// Bit depth of image per pixel -/// -/// # Examples -/// ``` -/// # use rimage::image::BitDepth; -/// # use std::str::FromStr; -/// let bit_depth = BitDepth::from_str("8").unwrap(); -/// println!("Bit Depth: {}", bit_depth); -/// ``` -/// -/// # Errors -/// -/// - [`BitDepth::from_str`] if bit depth is not supported -/// -/// [`BitDepth::from_str`]: enum.BitDepth.html#method.from_str -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u8)] -pub enum BitDepth { - /// One bit per pixel - One = 1, - /// Two bits per pixel - Two = 2, - /// Four bits per pixel - Four = 4, - /// Eight bits per pixel - Eight = 8, - /// Sixteen bits per pixel - Sixteen = 16, -} - -impl From for BitDepth { - fn from(bit_depth: png::BitDepth) -> Self { - match bit_depth { - png::BitDepth::One => BitDepth::One, - png::BitDepth::Two => BitDepth::Two, - png::BitDepth::Four => BitDepth::Four, - png::BitDepth::Eight => BitDepth::Eight, - png::BitDepth::Sixteen => BitDepth::Sixteen, - } - } -} - -impl Into for BitDepth { - fn into(self) -> png::BitDepth { - match self { - BitDepth::One => png::BitDepth::One, - BitDepth::Two => png::BitDepth::Two, - BitDepth::Four => png::BitDepth::Four, - BitDepth::Eight => png::BitDepth::Eight, - BitDepth::Sixteen => png::BitDepth::Sixteen, - } - } -} - -impl std::str::FromStr for BitDepth { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "1" => Ok(BitDepth::One), - "2" => Ok(BitDepth::Two), - "4" => Ok(BitDepth::Four), - "8" => Ok(BitDepth::Eight), - "16" => Ok(BitDepth::Sixteen), - _ => Err(format!("{} is not a valid bit depth", s)), - } - } -} - -impl fmt::Display for BitDepth { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - BitDepth::One => write!(f, "1"), - BitDepth::Two => write!(f, "2"), - BitDepth::Four => write!(f, "4"), - BitDepth::Eight => write!(f, "8"), - BitDepth::Sixteen => write!(f, "16"), - } - } -} diff --git a/src/lib.rs b/src/lib.rs index 1bea6ee5..d8e1fbde 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,14 +33,9 @@ After that you can use this crate: ``` # use rimage::Decoder; # let path = std::path::PathBuf::from("tests/files/basi0g01.jpg"); -// Build decoder from file path -let decoder = match Decoder::build(&path) { - Ok(d) => d, - Err(e) => { - eprintln!("Oh no there is error! {e}"); - std::process::exit(1); - } -}; +// Create decoder from file path and data +let data = std::fs::read(&path).unwrap(); +let decoder = Decoder::new(&path, &data); // Decode image to image data let image = match decoder.decode() { @@ -53,7 +48,6 @@ let image = match decoder.decode() { // Get image data println!("Color Space: {:?}", image.color_space()); -println!("Bit Depth: {:?}", image.bit_depth()); println!("Size: {:?}", image.size()); println!("Data length: {:?}", image.data().len()); @@ -65,7 +59,6 @@ println!("Data length: {:?}", image.data().len()); use error::{ConfigError, DecodingError, EncodingError}; use std::{fs, panic, path}; -use image::BitDepth; pub use image::{ImageData, OutputFormat}; /// Decoders for images @@ -97,7 +90,7 @@ impl Config { /// ``` /// # use rimage::{Config, OutputFormat}; /// # use std::path; - /// let input = &[path::PathBuf::from("tests/files/basi0g01.jpg")]; + /// let input = path::PathBuf::from("tests/files/basi0g01.jpg"); /// let quality = 100.0; /// let output_format = OutputFormat::MozJpeg; /// let config = Config::build(input, quality, output_format).unwrap(); @@ -155,32 +148,24 @@ impl Default for Config { /// Decoder used to get image data from file pub struct Decoder<'a> { - path: &'a path::PathBuf, - raw_data: Vec, + path: &'a path::Path, + raw_data: &'a [u8], } impl<'a> Decoder<'a> { - /// Builds decoder from path + /// Creates new decoder from path and raw data /// /// # Examples /// ``` /// # use rimage::Decoder; /// # use std::path; /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); - /// let d = Decoder::build(&path); + /// let data = std::fs::read(&path).unwrap(); + /// Decoder::new(&path, &data); /// ``` - /// - /// # Errors - /// - /// - [`io::Error`] if file failed to read, find, etc. - /// - /// [`Decoder`]: struct.Decoder.html - /// [`io::Error`]: https://doc.rust-lang.org/std/io/struct.Error.html #[inline] - pub fn build(path: &'a path::PathBuf) -> Result { - let raw_data = fs::read(path)?; - - Ok(Decoder { path, raw_data }) + pub fn new(path: &'a path::Path, raw_data: &'a [u8]) -> Self { + Decoder { path, raw_data } } /// Decodes file to ImageData @@ -189,8 +174,9 @@ impl<'a> Decoder<'a> { /// ``` /// # use rimage::{Decoder, ImageData}; /// # use std::path; - /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); - /// # let d = Decoder::build(&path).unwrap(); + /// # let path = path::Path::new("tests/files/basi0g01.jpg"); + /// let data = std::fs::read(&path).unwrap(); + /// let d = Decoder::new(&path, &data); /// let image = d.decode(); /// match image { /// Ok(_) => println!("Image decoded"), @@ -237,7 +223,6 @@ impl<'a> Decoder<'a> { Ok(ImageData::new( color_space, - BitDepth::Eight, image.width() as usize, image.height() as usize, data, @@ -249,17 +234,20 @@ impl<'a> Decoder<'a> { } fn decode_png(&self) -> Result { - let d = png::Decoder::new(fs::File::open(self.path)?); + let mut d = png::Decoder::new(&self.raw_data[..]); + d.set_transformations(png::Transformations::normalize_to_color8()); + let mut reader = d.read_info()?; + let mut buf = vec![0; reader.output_buffer_size()]; + let info = reader.next_frame(&mut buf)?; - let data = buf[..info.buffer_size()].to_vec(); + Ok(ImageData::new( info.color_type.into(), - info.bit_depth.into(), info.width as usize, info.height as usize, - data, + buf, )) } } @@ -281,7 +269,7 @@ impl<'a> Encoder<'a> { /// ``` /// # use rimage::{Encoder, Config, ImageData, image}; /// let config = Config::default(); - /// let image_data = ImageData::new(image::ColorSpace::Rgb, image::BitDepth::Eight, 8, 8, vec![0; 192]); + /// let image_data = ImageData::new(image::ColorSpace::Rgb, 8, 8, vec![0; 192]); /// let encoder = Encoder::new(&config, image_data); /// ``` pub fn new(conf: &'a Config, image_data: ImageData) -> Self { @@ -297,7 +285,7 @@ impl<'a> Encoder<'a> { /// ``` /// # use rimage::{Encoder, Config, ImageData, image}; /// # let config = Config::default(); - /// # let image_data = ImageData::new(image::ColorSpace::Rgb, image::BitDepth::Eight, 8, 8, vec![0; 192]); + /// # let image_data = ImageData::new(image::ColorSpace::Rgb, 8, 8, vec![0; 192]); /// # let encoder = Encoder::new(&config, image_data); /// encoder.encode(); /// ``` @@ -348,7 +336,6 @@ impl<'a> Encoder<'a> { ); encoder.set_color((*self.image_data.color_space()).into()); - encoder.set_depth((*self.image_data.bit_depth()).into()); let mut writer = encoder.write_header()?; writer.write_image_data(&self.image_data.data())?; @@ -363,7 +350,6 @@ impl<'a> Encoder<'a> { self.image_data.size().1 as u32, ); encoder.set_color((*self.image_data.color_space()).into()); - encoder.set_depth((*self.image_data.bit_depth()).into()); let mut writer = encoder.write_header()?; writer.write_image_data(&self.image_data.data())?; writer.finish()?; @@ -388,23 +374,22 @@ mod tests { #[test] fn decode_grayscale() { - // let files: Vec = fs::read_dir("tests/files/") - // .unwrap() - // .map(|entry| { - // let entry = entry.unwrap(); - // entry.path() - // }) - // .filter(|path| { - // let re = Regex::new(r"^tests/files/[^x].+0g\d\d((\.png)|(\.jpg))").unwrap(); - // re.is_match(path.to_str().unwrap_or("")) - // }) - // .collect(); - let files = [path::PathBuf::from("tests/files/basi0g08.png")]; + let files: Vec = fs::read_dir("tests/files/") + .unwrap() + .map(|entry| { + let entry = entry.unwrap(); + entry.path() + }) + .filter(|path| { + let re = Regex::new(r"^tests/files/[^x]&[^t].+0g\d\d((\.png)|(\.jpg))").unwrap(); + re.is_match(path.to_str().unwrap_or("")) + }) + .collect(); files.iter().for_each(|path| { - let image = Decoder::build(path).unwrap().decode().unwrap(); - println!("{path:?}"); + let data = fs::read(path).unwrap(); + let image = Decoder::new(path, &data).decode().unwrap(); assert_eq!(image.color_space(), &ColorSpace::Gray); assert_ne!(image.data().len(), 0); @@ -427,7 +412,9 @@ mod tests { .collect(); files.iter().for_each(|path| { - let image = Decoder::build(path).unwrap().decode().unwrap(); + println!("{path:?}"); + let data = fs::read(path).unwrap(); + let image = Decoder::new(path, &data).decode().unwrap(); assert_eq!(image.color_space(), &ColorSpace::GrayAlpha); assert_ne!(image.data().len(), 0); @@ -444,13 +431,15 @@ mod tests { entry.path() }) .filter(|path| { - let re = Regex::new(r"^^tests/files/[^x].+2c\d\d((\.png)|(\.jpg))").unwrap(); + let re = Regex::new(r"^^tests/files/[^x]&[^t].+2c\d\d((\.png)|(\.jpg))").unwrap(); re.is_match(path.to_str().unwrap_or("")) }) .collect(); files.iter().for_each(|path| { - let image = Decoder::build(path).unwrap().decode().unwrap(); + println!("{path:?}"); + let data = fs::read(path).unwrap(); + let image = Decoder::new(path, &data).decode().unwrap(); assert_eq!(image.color_space(), &ColorSpace::Rgb); assert_ne!(image.data().len(), 0); @@ -473,7 +462,9 @@ mod tests { .collect(); files.iter().for_each(|path| { - let image = Decoder::build(path).unwrap().decode().unwrap(); + println!("{path:?}"); + let data = fs::read(path).unwrap(); + let image = Decoder::new(path, &data).decode().unwrap(); assert_eq!(image.color_space(), &ColorSpace::Rgba); assert_ne!(image.data().len(), 0); @@ -490,15 +481,17 @@ mod tests { entry.path() }) .filter(|path| { - let re = Regex::new(r"^tests/files/[^x].+3p\d\d\.png$").unwrap(); + let re = Regex::new(r"^tests/files/[^x]&[^t].+3p\d\d\.png$").unwrap(); re.is_match(path.to_str().unwrap_or("")) }) .collect(); files.iter().for_each(|path| { - let image = Decoder::build(path).unwrap().decode().unwrap(); + println!("{path:?}"); + let data = fs::read(path).unwrap(); + let image = Decoder::new(path, &data).decode().unwrap(); - assert_eq!(image.color_space(), &ColorSpace::Indexed); + assert_eq!(image.color_space(), &ColorSpace::Rgb); assert_ne!(image.data().len(), 0); assert_ne!(image.size(), (0, 0)); }) @@ -519,15 +512,17 @@ mod tests { .collect(); files.iter().for_each(|path| { - let d = Decoder::build(path); - assert!(d.is_ok()); + println!("{path:?}"); + let data = fs::read(path).unwrap(); + let d = Decoder::new(path, &data); - let img = d.unwrap().decode(); + let img = d.decode(); assert!(img.is_err()); }) } #[test] + #[ignore] fn encode_grayscale_jpeg() { let files: Vec = fs::read_dir("tests/files/") .unwrap() @@ -542,7 +537,9 @@ mod tests { .collect(); files.iter().for_each(|path| { - let image = Decoder::build(path).unwrap().decode().unwrap(); + println!("{path:?}"); + let data = fs::read(path).unwrap(); + let image = Decoder::new(path, &data).decode().unwrap(); let out_path = path.with_extension("out.jpg"); @@ -550,7 +547,6 @@ mod tests { let encoder = Encoder::new(&conf, image); let result = encoder.encode(); - println!("{path:?}: {result:?}"); assert!(result.is_ok()); assert!(out_path.exists()); diff --git a/src/main.rs b/src/main.rs index 79fa7435..dd6ef3ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,8 @@ -use std::{io, path, process}; +use std::{fs, io, path, process}; use clap::Parser; use indicatif::{ProgressBar, ProgressStyle}; -use rimage::{decoders, encoders, image::OutputFormat, Config, Decoder}; +use rimage::{decoders, encoders, image::OutputFormat, Decoder}; #[derive(Parser)] #[command(author, about, version, long_about = None)] @@ -43,13 +43,8 @@ fn main() { if args.info { for path in &args.input { - let d = match Decoder::build(path) { - Ok(d) => d, - Err(e) => { - eprintln!("{} Error: {e}", path.file_name().unwrap().to_str().unwrap()); - continue; - } - }; + let data = fs::read(path).unwrap(); + let d = Decoder::new(path, &data); let img = match d.decode() { Ok(img) => img, @@ -61,7 +56,6 @@ fn main() { println!("{:?}", path.file_name().unwrap()); println!("Color Space: {:?}", img.color_space()); - println!("Bit Depth: {:?}", img.bit_depth()); println!("Size: {:?}", img.size()); println!("Data length: {:?}", img.data().len()); println!(); From cb7d6d7cc5dff0257e6230262e3be3056f2f5f88 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Mon, 27 Mar 2023 00:21:45 +0200 Subject: [PATCH 34/46] Removed ignore from encoder test --- src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index d8e1fbde..c5ac9e0e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -522,7 +522,6 @@ mod tests { } #[test] - #[ignore] fn encode_grayscale_jpeg() { let files: Vec = fs::read_dir("tests/files/") .unwrap() From 5b32c02265a453e32d86816ecd8a86569c00a74e Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Mon, 27 Mar 2023 01:01:55 +0200 Subject: [PATCH 35/46] Fixed benchmarks --- benches/decode_jpg.rs | 10 ++++++---- benches/decode_png.rs | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/benches/decode_jpg.rs b/benches/decode_jpg.rs index 8ff98fe7..227bedf4 100644 --- a/benches/decode_jpg.rs +++ b/benches/decode_jpg.rs @@ -1,4 +1,7 @@ -use std::path::PathBuf; +use std::{ + fs, + path::{Path, PathBuf}, +}; use criterion::{black_box, criterion_group, criterion_main, Criterion}; #[allow(deprecated)] @@ -13,9 +16,8 @@ fn bench_decode_jpg(c: &mut Criterion) { }); group.bench_function("Decoder", |b| { b.iter(|| { - Decoder::build(black_box(&PathBuf::from("tests/files/basi6a08.jpg"))) - .unwrap() - .decode() + let data = fs::read(&Path::new("tests/files/basi6a08.jpg")).unwrap(); + Decoder::new(black_box(&Path::new("tests/files/basi6a08.jpg")), &data).decode() }) }); group.finish(); diff --git a/benches/decode_png.rs b/benches/decode_png.rs index a4a356da..40b43dcd 100644 --- a/benches/decode_png.rs +++ b/benches/decode_png.rs @@ -1,4 +1,7 @@ -use std::path::PathBuf; +use std::{ + fs, + path::{Path, PathBuf}, +}; use criterion::{black_box, criterion_group, criterion_main, Criterion}; #[allow(deprecated)] @@ -13,9 +16,8 @@ fn bench_decode_png(c: &mut Criterion) { }); group.bench_function("Decoder", |b| { b.iter(|| { - Decoder::build(black_box(&PathBuf::from("tests/files/basi6a08.png"))) - .unwrap() - .decode() + let data = fs::read(&Path::new("tests/files/basi6a08.png")).unwrap(); + Decoder::new(black_box(&Path::new("tests/files/basi6a08.png")), &data).decode() }) }); group.finish(); From 70363172be278835ebe7607cc6acc9826330bd81 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Mon, 27 Mar 2023 01:09:58 +0200 Subject: [PATCH 36/46] Fixed encoding bugs --- src/image.rs | 6 ++++- src/lib.rs | 65 +++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/image.rs b/src/image.rs index 56a426f9..b5439561 100644 --- a/src/image.rs +++ b/src/image.rs @@ -100,6 +100,10 @@ impl ImageData { pub fn data(&self) -> &[u8] { &self.data } + #[inline] + pub(crate) fn data_mut(&mut self) -> &mut Vec { + &mut self.data + } } /// Supported output format @@ -202,7 +206,7 @@ impl Into for ColorSpace { fn into(self) -> mozjpeg::ColorSpace { match self { ColorSpace::Rgb => mozjpeg::ColorSpace::JCS_RGB, - ColorSpace::Rgba => mozjpeg::ColorSpace::JCS_RGB, + ColorSpace::Rgba => mozjpeg::ColorSpace::JCS_EXT_RGBA, ColorSpace::Cmyk => mozjpeg::ColorSpace::JCS_CMYK, ColorSpace::Indexed => mozjpeg::ColorSpace::JCS_RGB, ColorSpace::Gray => mozjpeg::ColorSpace::JCS_GRAYSCALE, diff --git a/src/lib.rs b/src/lib.rs index c5ac9e0e..cebd3784 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -302,12 +302,15 @@ impl<'a> Encoder<'a> { } } - fn encode_mozjpeg(self) -> Result<(), EncodingError> { + fn encode_mozjpeg(mut self) -> Result<(), EncodingError> { + if self.image_data.color_space() == &image::ColorSpace::GrayAlpha { + for i in 0..self.image_data.size().0 * self.image_data.size().1 { + self.image_data.data_mut()[i] = self.image_data.data()[i * 2]; + } + } + panic::catch_unwind(|| -> Result<(), EncodingError> { let mut encoder = mozjpeg::Compress::new((*self.image_data.color_space()).into()); - println!("ImgData: {:?}", self.image_data); - println!("Config: {:?}", self.config); - println!("Data len: {}", self.image_data.data().len()); encoder.set_size(self.image_data.size().0, self.image_data.size().1); encoder.set_quality(self.config.quality); @@ -447,6 +450,31 @@ mod tests { }) } + #[test] + fn decode_rgb_transparent() { + let files: Vec = fs::read_dir("tests/files/") + .unwrap() + .map(|entry| { + let entry = entry.unwrap(); + entry.path() + }) + .filter(|path| { + let re = Regex::new(r"^^tests/files/[^x]&[t].+2c\d\d((\.png)|(\.jpg))").unwrap(); + re.is_match(path.to_str().unwrap_or("")) + }) + .collect(); + + files.iter().for_each(|path| { + println!("{path:?}"); + let data = fs::read(path).unwrap(); + let image = Decoder::new(path, &data).decode().unwrap(); + + assert_eq!(image.color_space(), &ColorSpace::Rgba); + assert_ne!(image.data().len(), 0); + assert_ne!(image.size(), (0, 0)); + }) + } + #[test] fn decode_rgba() { let files: Vec = fs::read_dir("tests/files/") @@ -497,6 +525,31 @@ mod tests { }) } + #[test] + fn decode_indexed_transparent() { + let files: Vec = fs::read_dir("tests/files/") + .unwrap() + .map(|entry| { + let entry = entry.unwrap(); + entry.path() + }) + .filter(|path| { + let re = Regex::new(r"^tests/files/[^x]&[t].+3p\d\d\.png$").unwrap(); + re.is_match(path.to_str().unwrap_or("")) + }) + .collect(); + + files.iter().for_each(|path| { + println!("{path:?}"); + let data = fs::read(path).unwrap(); + let image = Decoder::new(path, &data).decode().unwrap(); + + assert_eq!(image.color_space(), &ColorSpace::Rgba); + assert_ne!(image.data().len(), 0); + assert_ne!(image.size(), (0, 0)); + }) + } + #[test] fn decode_corrupted() { let files: Vec = fs::read_dir("tests/files/") @@ -522,7 +575,7 @@ mod tests { } #[test] - fn encode_grayscale_jpeg() { + fn encode_jpeg() { let files: Vec = fs::read_dir("tests/files/") .unwrap() .map(|entry| { @@ -530,7 +583,7 @@ mod tests { entry.path() }) .filter(|path| { - let re = Regex::new(r"^tests/files/[^x].+0g\d\d((\.png)|(\.jpg))").unwrap(); + let re = Regex::new(r"^tests/files/[^x]").unwrap(); re.is_match(path.to_str().unwrap_or("")) }) .collect(); From 2dfe51cb0871ea919b8b1b088b1ee6646d80b013 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Mon, 27 Mar 2023 01:11:26 +0200 Subject: [PATCH 37/46] Removed files after test --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index cebd3784..7a8a0030 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -604,7 +604,7 @@ mod tests { assert!(out_path.exists()); assert!(out_path.is_file()); assert!(out_path.metadata().unwrap().len() > 0); - // assert!(fs::remove_file(out_path).is_ok()); + assert!(fs::remove_file(out_path).is_ok()); }) } } From b4afb58d68b53925557ca429eb13565bde9e5e11 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Mon, 27 Mar 2023 01:17:04 +0200 Subject: [PATCH 38/46] Added test cases for png encoding --- src/lib.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 7a8a0030..e5b99b0f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -607,4 +607,72 @@ mod tests { assert!(fs::remove_file(out_path).is_ok()); }) } + + #[test] + fn encode_png() { + let files: Vec = fs::read_dir("tests/files/") + .unwrap() + .map(|entry| { + let entry = entry.unwrap(); + entry.path() + }) + .filter(|path| { + let re = Regex::new(r"^tests/files/[^x]").unwrap(); + re.is_match(path.to_str().unwrap_or("")) + }) + .collect(); + + files.iter().for_each(|path| { + println!("{path:?}"); + let data = fs::read(path).unwrap(); + let image = Decoder::new(path, &data).decode().unwrap(); + + let out_path = path.with_extension("out.png"); + + let conf = Config::build(out_path.clone(), 75.0, OutputFormat::Png).unwrap(); + + let encoder = Encoder::new(&conf, image); + let result = encoder.encode(); + + assert!(result.is_ok()); + assert!(out_path.exists()); + assert!(out_path.is_file()); + assert!(out_path.metadata().unwrap().len() > 0); + assert!(fs::remove_file(out_path).is_ok()); + }) + } + + #[test] + fn encode_oxipng() { + let files: Vec = fs::read_dir("tests/files/") + .unwrap() + .map(|entry| { + let entry = entry.unwrap(); + entry.path() + }) + .filter(|path| { + let re = Regex::new(r"^tests/files/[^x]").unwrap(); + re.is_match(path.to_str().unwrap_or("")) + }) + .collect(); + + files.iter().for_each(|path| { + println!("{path:?}"); + let data = fs::read(path).unwrap(); + let image = Decoder::new(path, &data).decode().unwrap(); + + let out_path = path.with_extension("out.oxi.png"); + + let conf = Config::build(out_path.clone(), 75.0, OutputFormat::Oxipng).unwrap(); + + let encoder = Encoder::new(&conf, image); + let result = encoder.encode(); + + assert!(result.is_ok()); + assert!(out_path.exists()); + assert!(out_path.is_file()); + assert!(out_path.metadata().unwrap().len() > 0); + assert!(fs::remove_file(out_path).is_ok()); + }) + } } From dbd7d09194a5aa5845527152564b08c98c88424d Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Mon, 27 Mar 2023 01:46:00 +0200 Subject: [PATCH 39/46] Updated encode benches --- benches/encode_jpg.rs | 36 +++++++++++++++++++++++++++++++++--- benches/encode_png.rs | 36 +++++++++++++++++++++++++++++++++--- src/image.rs | 2 +- 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/benches/encode_jpg.rs b/benches/encode_jpg.rs index b7b6a667..4395c302 100644 --- a/benches/encode_jpg.rs +++ b/benches/encode_jpg.rs @@ -1,13 +1,25 @@ -use std::{fs, path::PathBuf}; +use std::{ + fs, + path::{Path, PathBuf}, +}; use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use rimage::Decoder; #[allow(deprecated)] use rimage::{decoders::decode_image, encoders::encode_image}; #[allow(deprecated)] fn bench_encode_jpg(c: &mut Criterion) { + let mut group = c.benchmark_group("encode_jpg"); + let (pixels, width, height) = decode_image(&PathBuf::from("tests/files/basi6a08.jpg")).unwrap(); - c.bench_function("en jpg", |b| { + + let data = fs::read(&Path::new("tests/files/basi6a08.jpg")).unwrap(); + let image = Decoder::new(&Path::new("tests/files/basi6a08.jpg"), &data) + .decode() + .unwrap(); + + group.bench_function("encoders", |b| { b.iter(|| { encode_image( black_box(&PathBuf::from("en")), @@ -19,7 +31,25 @@ fn bench_encode_jpg(c: &mut Criterion) { ) }) }); - fs::remove_file("en.jpg").unwrap(); + group.bench_function("Encoder", |b| { + b.iter(|| { + rimage::Encoder::new( + black_box( + &rimage::Config::build( + PathBuf::from("en_u.jpg"), + 75.0, + rimage::OutputFormat::MozJpeg, + ) + .unwrap(), + ), + black_box(image.clone()), + ) + .encode() + .unwrap(); + }) + }); + // fs::remove_file("en.jpg").unwrap(); + // fs::remove_file("en_u.jpg").unwrap(); } criterion_group!(benches, bench_encode_jpg); diff --git a/benches/encode_png.rs b/benches/encode_png.rs index 8975320c..6bac609f 100644 --- a/benches/encode_png.rs +++ b/benches/encode_png.rs @@ -1,13 +1,25 @@ -use std::{fs, path::PathBuf}; +use std::{ + fs, + path::{Path, PathBuf}, +}; use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use rimage::Decoder; #[allow(deprecated)] use rimage::{decoders::decode_image, encoders::encode_image}; #[allow(deprecated)] fn bench_encode_png(c: &mut Criterion) { + let mut group = c.benchmark_group("encode_png"); + let (pixels, width, height) = decode_image(&PathBuf::from("tests/files/basi6a08.png")).unwrap(); - c.bench_function("en png", |b| { + + let data = fs::read(&Path::new("tests/files/basi6a08.jpg")).unwrap(); + let image = Decoder::new(&Path::new("tests/files/basi6a08.jpg"), &data) + .decode() + .unwrap(); + + group.bench_function("encoders", |b| { b.iter(|| { encode_image( black_box(&PathBuf::from("en")), @@ -19,7 +31,25 @@ fn bench_encode_png(c: &mut Criterion) { ) }) }); - fs::remove_file("en.png").unwrap(); + group.bench_function("Encoder", |b| { + b.iter(|| { + rimage::Encoder::new( + black_box( + &rimage::Config::build( + PathBuf::from("en_u.png"), + 75.0, + rimage::OutputFormat::Oxipng, + ) + .unwrap(), + ), + black_box(image.clone()), + ) + .encode() + .unwrap(); + }) + }); + // fs::remove_file("en.png").unwrap(); + // fs::remove_file("en_u.png").unwrap(); } criterion_group!(benches, bench_encode_png); diff --git a/src/image.rs b/src/image.rs index b5439561..7d9d1fdd 100644 --- a/src/image.rs +++ b/src/image.rs @@ -19,7 +19,7 @@ use std::{borrow::Cow, fmt}; /// println!("Data length: {:?}", image.data().len()); /// # Ok::<(), DecodingError>(()) /// ``` -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ImageData { color_space: ColorSpace, width: usize, From 9d89971cf9f2e9b097f46aae275247f7f34b31c5 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Mon, 27 Mar 2023 14:34:40 +0200 Subject: [PATCH 40/46] Rewrite documentation and main function --- src/error.rs | 2 +- src/image.rs | 200 +---------------------------- src/lib.rs | 348 ++++++++++++++++++++++----------------------------- src/main.rs | 39 ++---- 4 files changed, 168 insertions(+), 421 deletions(-) diff --git a/src/error.rs b/src/error.rs index 695e56c8..1bfc8641 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,7 +3,7 @@ use std::{error::Error, fmt, io}; /// An error that occurred if configuration is invalid #[derive(Debug)] pub enum ConfigError { - /// Quality is less than 0 or greater than 1 + /// Quality is less than 0 or greater than 100 QualityOutOfBounds, /// Width is 0 WidthIsZero, diff --git a/src/image.rs b/src/image.rs index 7d9d1fdd..feea385c 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,27 +1,8 @@ use std::{borrow::Cow, fmt}; -/// Image data from decoder -/// -/// # Examples -/// -/// ``` -/// # use rimage::{Decoder, error::DecodingError}; -/// # use std::{path, fs}; -/// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); -/// let data = fs::read(&path)?; -/// let d = Decoder::new(&path, &data); -/// -/// let image = d.decode()?; -/// -/// // Get something from image data -/// println!("Color Space: {:?}", image.color_space()); -/// println!("Size: {:?}", image.size()); -/// println!("Data length: {:?}", image.data().len()); -/// # Ok::<(), DecodingError>(()) -/// ``` +/// Image data #[derive(Debug, Clone)] pub struct ImageData { - color_space: ColorSpace, width: usize, height: usize, data: Vec, @@ -33,88 +14,28 @@ impl ImageData { /// # Examples /// ``` /// # use rimage::{ImageData, image}; - /// let image = ImageData::new(image::ColorSpace::Gray, 100, 100, vec![0; 100 * 100]); + /// let image = ImageData::new(100, 100, vec![0; 100 * 100]); /// ``` - pub fn new(color_space: ColorSpace, width: usize, height: usize, data: Vec) -> Self { + pub fn new(width: usize, height: usize, data: Vec) -> Self { Self { - color_space, width, height, data, } } - /// Returns size of image (Width, Height) - /// - /// # Examples - /// ``` - /// # use rimage::{Decoder, ImageData}; - /// # use std::path; - /// # let path = path::Path::new("tests/files/basi0g01.jpg"); - /// # let data = std::fs::read(&path).unwrap(); - /// # let d = Decoder::new(&path, &data); - /// # let image = d.decode().unwrap(); - /// let (width, height) = image.size(); - /// ``` + /// Get the width and height of the image #[inline] pub fn size(&self) -> (usize, usize) { (self.width, self.height) } - /// Returns a ref to color space of image [`ColorSpace`] - /// - /// # Examples - /// ``` - /// # use rimage::{Decoder, ImageData, image::ColorSpace}; - /// # use std::path; - /// # let path = path::Path::new("tests/files/basi0g01.jpg"); - /// # let data = std::fs::read(&path).unwrap(); - /// # let d = Decoder::new(&path, &data); - /// # let image = d.decode().unwrap(); - /// let color_space = image.color_space(); - /// match color_space { - /// ColorSpace::Gray => println!("Grayscale"), - /// ColorSpace::Rgb => println!("RGB"), - /// ColorSpace::Cmyk => println!("CMYK"), - /// ColorSpace::Rgba => println!("RGBA"), - /// ColorSpace::Indexed => println!("Indexed"), - /// ColorSpace::GrayAlpha => println!("Grayscale Alpha"), - /// } - /// ``` - /// [`ColorSpace`]: enum.ColorSpace.html - #[inline] - pub fn color_space(&self) -> &ColorSpace { - &self.color_space - } - /// Returns a ref to array of bytes in image - /// - /// # Examples - /// ``` - /// # use rimage::{Decoder, ImageData}; - /// # use std::path; - /// # let path = path::Path::new("tests/files/basi0g01.jpg"); - /// # let data = std::fs::read(&path).unwrap(); - /// # let d = Decoder::new(&path, &data); - /// # let image = d.decode().unwrap(); - /// let data = image.data(); - /// ``` + /// Get image data #[inline] pub fn data(&self) -> &[u8] { &self.data } - #[inline] - pub(crate) fn data_mut(&mut self) -> &mut Vec { - &mut self.data - } } -/// Supported output format -/// -/// # Examples -/// ``` -/// # use rimage::OutputFormat; -/// # use std::str::FromStr; -/// let format = OutputFormat::from_str("mozjpeg").unwrap(); -/// println!("Format: {}", format); -/// ``` +/// Image format to encode #[derive(Debug, Clone, Copy)] pub enum OutputFormat { /// MozJpeg image @@ -147,112 +68,3 @@ impl fmt::Display for OutputFormat { } } } - -/// Color space of image -/// -/// # Examples -/// ``` -/// # use rimage::image::ColorSpace; -/// # use std::str::FromStr; -/// let color_space = ColorSpace::from_str("rgb").unwrap(); -/// println!("Color Space: {}", color_space); -/// ``` -/// -/// # Errors -/// -/// - [`ColorSpace::from_str`] if color space is not supported -/// -/// [`ColorSpace::from_str`]: enum.ColorSpace.html#method.from_str -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ColorSpace { - /// **R**ed/**G**reen/**B**lue - Rgb, - /// **R**ed/**G**reen/**B**lue/**A**lpha - Rgba, - /// **C**yan/**M**agenta/**Y**ellow/Blac**K** - Cmyk, - /// Indexed color palette - Indexed, - /// Grayscale - Gray, - /// Grayscale/Alpha - GrayAlpha, -} - -impl From for ColorSpace { - fn from(color_type: png::ColorType) -> Self { - match color_type { - png::ColorType::Grayscale => ColorSpace::Gray, - png::ColorType::Rgb => ColorSpace::Rgb, - png::ColorType::Indexed => ColorSpace::Indexed, - png::ColorType::GrayscaleAlpha => ColorSpace::GrayAlpha, - png::ColorType::Rgba => ColorSpace::Rgba, - } - } -} - -impl From for ColorSpace { - fn from(color_space: mozjpeg::ColorSpace) -> Self { - match color_space { - mozjpeg::ColorSpace::JCS_GRAYSCALE => ColorSpace::Gray, - mozjpeg::ColorSpace::JCS_CMYK => ColorSpace::Cmyk, - mozjpeg::ColorSpace::JCS_RGB => ColorSpace::Rgb, - _ => ColorSpace::Rgb, - } - } -} - -impl Into for ColorSpace { - fn into(self) -> mozjpeg::ColorSpace { - match self { - ColorSpace::Rgb => mozjpeg::ColorSpace::JCS_RGB, - ColorSpace::Rgba => mozjpeg::ColorSpace::JCS_EXT_RGBA, - ColorSpace::Cmyk => mozjpeg::ColorSpace::JCS_CMYK, - ColorSpace::Indexed => mozjpeg::ColorSpace::JCS_RGB, - ColorSpace::Gray => mozjpeg::ColorSpace::JCS_GRAYSCALE, - ColorSpace::GrayAlpha => mozjpeg::ColorSpace::JCS_GRAYSCALE, - } - } -} - -impl Into for ColorSpace { - fn into(self) -> png::ColorType { - match self { - ColorSpace::Rgb => png::ColorType::Rgb, - ColorSpace::Rgba => png::ColorType::Rgba, - ColorSpace::Cmyk => png::ColorType::Rgb, - ColorSpace::Indexed => png::ColorType::Indexed, - ColorSpace::Gray => png::ColorType::Grayscale, - ColorSpace::GrayAlpha => png::ColorType::GrayscaleAlpha, - } - } -} - -impl std::str::FromStr for ColorSpace { - type Err = Cow<'static, str>; - - fn from_str(s: &str) -> Result { - match s { - "rgb" => Ok(ColorSpace::Rgb), - "rgba" => Ok(ColorSpace::Rgba), - "cmyk" => Ok(ColorSpace::Cmyk), - "indexed" => Ok(ColorSpace::Indexed), - "grayscale" => Ok(ColorSpace::Gray), - "grayscale_alpha" => Ok(ColorSpace::GrayAlpha), - _ => Err(format!("{} is not a valid color space", s).into()), - } - } -} - -impl fmt::Display for ColorSpace { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ColorSpace::Rgb => write!(f, "rgb"), - ColorSpace::Rgba => write!(f, "rgba"), - ColorSpace::Cmyk => write!(f, "cmyk"), - ColorSpace::Indexed => write!(f, "indexed"), - ColorSpace::Gray => write!(f, "grayscale"), - ColorSpace::GrayAlpha => write!(f, "grayscale_alpha"), - } - } -} diff --git a/src/lib.rs b/src/lib.rs index e5b99b0f..5fb810d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,18 +21,13 @@ or add this to Cargo.toml: rimage = "0.2" ``` -Next import Decoder: -```text -use rimage::Decoder; -``` - After that you can use this crate: ## Decoding - ``` -# use rimage::Decoder; +use rimage::Decoder; # let path = std::path::PathBuf::from("tests/files/basi0g01.jpg"); + // Create decoder from file path and data let data = std::fs::read(&path).unwrap(); let decoder = Decoder::new(&path, &data); @@ -47,17 +42,56 @@ let image = match decoder.decode() { }; // Get image data -println!("Color Space: {:?}", image.color_space()); println!("Size: {:?}", image.size()); println!("Data length: {:?}", image.data().len()); // Do something with image... ``` + +## Encoding + +``` +# use rimage::Decoder; +use rimage::{Config, Encoder, OutputFormat}; +# let path = std::path::PathBuf::from("tests/files/basi0g01.jpg"); +# let data = std::fs::read(&path).unwrap(); +# let decoder = Decoder::new(&path, &data); +# let image = decoder.decode().unwrap(); + +// Encode image to file +let config = match Config::build( + 75.0, + OutputFormat::MozJpeg, +) { + Ok(config) => config, + Err(e) => { + eprintln!("Oh no there is error! {e}"); + std::process::exit(1); + } +}; + +let encoder = Encoder::new(&config, image); + +let data = match encoder.encode() { + Ok(data) => data, + Err(e) => { + eprintln!("Oh no there is error! {e}"); + std::process::exit(1); + } +}; + +// Write image to file +std::fs::write("output.jpg", data); +``` */ #![warn(missing_docs)] use error::{ConfigError, DecodingError, EncodingError}; -use std::{fs, panic, path}; +use rgb::{ + alt::{GRAY8, GRAYA8}, + AsPixels, FromSlice, RGB8, RGBA8, +}; +use std::{panic, path}; pub use image::{ImageData, OutputFormat}; @@ -68,68 +102,38 @@ pub mod decoders; #[deprecated(since = "0.2.0", note = "use the Encoder struct instead")] pub mod encoders; -/// All errors that can occur +/// Errors that can occur during image processing pub mod error; -/// Image data +/// Image data structs pub mod image; -/// Config for encoder +/// Config for image encoding #[derive(Debug)] pub struct Config { - output: path::PathBuf, quality: f32, output_format: OutputFormat, } impl Config { - /// Builds config from parameters - /// - /// # Examples - /// - /// ``` - /// # use rimage::{Config, OutputFormat}; - /// # use std::path; - /// let input = path::PathBuf::from("tests/files/basi0g01.jpg"); - /// let quality = 100.0; - /// let output_format = OutputFormat::MozJpeg; - /// let config = Config::build(input, quality, output_format).unwrap(); - /// ``` - /// - /// # Errors - /// - [`ConfigError::QualityOutOfBounds`] if quality is not in range 0.0 - 100.0 - /// - [`ConfigError::InputIsEmpty`] if input is empty - pub fn build( - output: path::PathBuf, - quality: f32, - output_format: OutputFormat, - ) -> Result { + /// Create new config + pub fn build(quality: f32, output_format: OutputFormat) -> Result { if quality < 0.0 || quality > 100.0 { return Err(ConfigError::QualityOutOfBounds); } - if output.to_str().unwrap().is_empty() { - return Err(ConfigError::InputIsEmpty); - } - Ok(Config { - output, quality, output_format, }) } + /// Get quality #[inline] - /// Gets input array of paths from config - pub fn input(&self) -> &path::PathBuf { - &self.output - } - #[inline] - /// Gets quality of output images from config pub fn quality(&self) -> f32 { self.quality } + /// Get output format #[inline] - /// Gets format of output images from config pub fn output_format(&self) -> &OutputFormat { &self.output_format } @@ -139,58 +143,26 @@ impl Default for Config { #[inline] fn default() -> Self { Self { - output: path::PathBuf::from(""), quality: 75.0, output_format: OutputFormat::MozJpeg, } } } -/// Decoder used to get image data from file +/// Decoder for images pub struct Decoder<'a> { path: &'a path::Path, raw_data: &'a [u8], } impl<'a> Decoder<'a> { - /// Creates new decoder from path and raw data - /// - /// # Examples - /// ``` - /// # use rimage::Decoder; - /// # use std::path; - /// # let path = path::PathBuf::from("tests/files/basi0g01.jpg"); - /// let data = std::fs::read(&path).unwrap(); - /// Decoder::new(&path, &data); - /// ``` + /// Create new decoder #[inline] pub fn new(path: &'a path::Path, raw_data: &'a [u8]) -> Self { Decoder { path, raw_data } } - /// Decodes file to ImageData - /// - /// # Examples - /// ``` - /// # use rimage::{Decoder, ImageData}; - /// # use std::path; - /// # let path = path::Path::new("tests/files/basi0g01.jpg"); - /// let data = std::fs::read(&path).unwrap(); - /// let d = Decoder::new(&path, &data); - /// let image = d.decode(); - /// match image { - /// Ok(_) => println!("Image decoded"), - /// Err(e) => println!("Error: {}", e), - /// } - /// ``` - /// - /// # Errors - /// - /// - [`DecodingError::Format`] if format is not supported - /// - [`DecodingError::Parsing`] if file is not a image, unsupported color space, etc. - /// - /// [`DecodingError::Format`]: enum.DecodingError.html#variant.Format - /// [`DecodingError::Parsing`]: enum.DecodingError.html#variant.Parsing + /// Decode image pub fn decode(&self) -> Result { let extension = match self.path.extension() { Some(ext) => ext, @@ -210,19 +182,13 @@ impl<'a> Decoder<'a> { fn decode_jpeg(&self) -> Result { panic::catch_unwind(|| -> Result { let d = mozjpeg::Decompress::new_mem(&self.raw_data)?; - let color_space = d.color_space().into(); - let mut image = match d.image()? { - mozjpeg::Format::RGB(img) => img, - mozjpeg::Format::Gray(img) => img, - mozjpeg::Format::CMYK(img) => img, - }; - - let data = image.read_scanlines_flat().ok_or(DecodingError::Parsing( - "Cannot read jpeg scanlines".to_string(), + let mut image = d.rgba()?; + + let data = image.read_scanlines().ok_or(DecodingError::Parsing( + "Failed to read scanlines".to_string(), ))?; Ok(ImageData::new( - color_space, image.width() as usize, image.height() as usize, data, @@ -233,68 +199,65 @@ impl<'a> Decoder<'a> { ))) } + fn expand_pixels(buf: &mut [u8], to_rgba: impl Fn(T) -> RGBA8) + where + [u8]: AsPixels + FromSlice, + { + assert!(std::mem::size_of::() <= std::mem::size_of::()); + for i in (0..buf.len() / 4).rev() { + let src_pix = buf.as_pixels()[i]; + buf.as_rgba_mut()[i] = to_rgba(src_pix); + } + } + fn decode_png(&self) -> Result { let mut d = png::Decoder::new(&self.raw_data[..]); d.set_transformations(png::Transformations::normalize_to_color8()); let mut reader = d.read_info()?; + let width = reader.info().width; + let height = reader.info().height; + + let buf_size = width as usize * height as usize * 4; - let mut buf = vec![0; reader.output_buffer_size()]; + let mut buf = vec![0; buf_size]; let info = reader.next_frame(&mut buf)?; - Ok(ImageData::new( - info.color_type.into(), - info.width as usize, - info.height as usize, - buf, - )) + match info.color_type { + png::ColorType::Grayscale => { + Self::expand_pixels(&mut buf, |gray: GRAY8| GRAY8::from(gray).into()) + } + png::ColorType::GrayscaleAlpha => Self::expand_pixels(&mut buf, GRAYA8::into), + png::ColorType::Rgb => Self::expand_pixels(&mut buf, RGB8::into), + png::ColorType::Rgba => {} + png::ColorType::Indexed => { + return Err(DecodingError::Parsing( + "Indexed color must be expanded to RGB".to_string(), + )) + } + } + + Ok(ImageData::new(width as usize, height as usize, buf)) } } -/// Encoder used to encode image data to file +/// Encoder for images pub struct Encoder<'a> { image_data: ImageData, config: &'a Config, } -// Write Encoder encode method to encode image data to file in different formats from config -// Write Encoder build method to build encoder from Config and ImageData -// Write Encoder encode_png method to encode image data to png file -// Write Encoder encode_jpeg method to encode image data to jpeg file impl<'a> Encoder<'a> { - /// Builds encoder from config and image data - /// - /// # Examples - /// ``` - /// # use rimage::{Encoder, Config, ImageData, image}; - /// let config = Config::default(); - /// let image_data = ImageData::new(image::ColorSpace::Rgb, 8, 8, vec![0; 192]); - /// let encoder = Encoder::new(&config, image_data); - /// ``` + /// Create new encoder pub fn new(conf: &'a Config, image_data: ImageData) -> Self { Encoder { image_data, config: conf, } } - - /// Encodes image data to file - /// - /// # Examples - /// ``` - /// # use rimage::{Encoder, Config, ImageData, image}; - /// # let config = Config::default(); - /// # let image_data = ImageData::new(image::ColorSpace::Rgb, 8, 8, vec![0; 192]); - /// # let encoder = Encoder::new(&config, image_data); - /// encoder.encode(); - /// ``` - /// - /// # Errors - /// - /// - [`EncodingError::Encoding`] if encoding failed - /// - [`EncodingError::IoError`] if IO error occurred - pub fn encode(self) -> Result<(), EncodingError> { + /// Encode image + pub fn encode(self) -> Result, EncodingError> { match self.config.output_format { OutputFormat::Png => self.encode_png(), OutputFormat::Oxipng => self.encode_oxipng(), @@ -302,15 +265,9 @@ impl<'a> Encoder<'a> { } } - fn encode_mozjpeg(mut self) -> Result<(), EncodingError> { - if self.image_data.color_space() == &image::ColorSpace::GrayAlpha { - for i in 0..self.image_data.size().0 * self.image_data.size().1 { - self.image_data.data_mut()[i] = self.image_data.data()[i * 2]; - } - } - - panic::catch_unwind(|| -> Result<(), EncodingError> { - let mut encoder = mozjpeg::Compress::new((*self.image_data.color_space()).into()); + fn encode_mozjpeg(self) -> Result, EncodingError> { + panic::catch_unwind(|| -> Result, EncodingError> { + let mut encoder = mozjpeg::Compress::new(mozjpeg::ColorSpace::JCS_EXT_RGBA); encoder.set_size(self.image_data.size().0, self.image_data.size().1); encoder.set_quality(self.config.quality); @@ -320,58 +277,64 @@ impl<'a> Encoder<'a> { encoder.write_scanlines(&self.image_data.data()); encoder.finish_compress(); - let data = encoder.data_as_mut_slice().unwrap(); - - fs::write(&self.config.output, data)?; - - Ok(()) + encoder.data_to_vec().map_err(|_| { + EncodingError::Encoding("Failed to convert data to vector".to_string()) + }) }) .unwrap_or(Err(EncodingError::Encoding( "Failed to encode jpeg".to_string(), ))) } - fn encode_png(&self) -> Result<(), EncodingError> { - let mut encoder = png::Encoder::new( - fs::File::create(&self.config.output)?, - self.image_data.size().0 as u32, - self.image_data.size().1 as u32, - ); + fn encode_png(&self) -> Result, EncodingError> { + let mut buf = Vec::new(); - encoder.set_color((*self.image_data.color_space()).into()); - let mut writer = encoder.write_header()?; - writer.write_image_data(&self.image_data.data())?; + { + let mut encoder = png::Encoder::new( + &mut buf, + self.image_data.size().0 as u32, + self.image_data.size().1 as u32, + ); + + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + + let mut writer = encoder.write_header()?; + writer.write_image_data(self.image_data.data())?; + writer.finish()?; + } - Ok(()) + Ok(buf) } - fn encode_oxipng(&self) -> Result<(), EncodingError> { - let mut file = fs::File::create(&self.config.output)?; - let mut encoder = png::Encoder::new( - &mut file, - self.image_data.size().0 as u32, - self.image_data.size().1 as u32, - ); - encoder.set_color((*self.image_data.color_space()).into()); - let mut writer = encoder.write_header()?; - writer.write_image_data(&self.image_data.data())?; - writer.finish()?; - - oxipng::optimize( - &oxipng::InFile::from(&self.config.output), - &oxipng::OutFile::Path(Some(self.config.output.clone())), - &oxipng::Options::default(), - )?; - - Ok(()) + fn encode_oxipng(&self) -> Result, EncodingError> { + let mut buf = Vec::new(); + + { + let mut encoder = png::Encoder::new( + &mut buf, + self.image_data.size().0 as u32, + self.image_data.size().1 as u32, + ); + + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + + let mut writer = encoder.write_header()?; + writer.write_image_data(self.image_data.data())?; + writer.finish()?; + } + + oxipng::optimize_from_memory(&buf, &oxipng::Options::default()) + .map_err(|e| EncodingError::Encoding(e.to_string())) } } #[cfg(test)] mod tests { - use regex::Regex; + use std::fs; - use crate::image::ColorSpace; + use regex::Regex; use super::*; @@ -394,7 +357,6 @@ mod tests { let data = fs::read(path).unwrap(); let image = Decoder::new(path, &data).decode().unwrap(); - assert_eq!(image.color_space(), &ColorSpace::Gray); assert_ne!(image.data().len(), 0); assert_ne!(image.size(), (0, 0)); }) @@ -419,7 +381,6 @@ mod tests { let data = fs::read(path).unwrap(); let image = Decoder::new(path, &data).decode().unwrap(); - assert_eq!(image.color_space(), &ColorSpace::GrayAlpha); assert_ne!(image.data().len(), 0); assert_ne!(image.size(), (0, 0)); }) @@ -444,7 +405,6 @@ mod tests { let data = fs::read(path).unwrap(); let image = Decoder::new(path, &data).decode().unwrap(); - assert_eq!(image.color_space(), &ColorSpace::Rgb); assert_ne!(image.data().len(), 0); assert_ne!(image.size(), (0, 0)); }) @@ -469,7 +429,6 @@ mod tests { let data = fs::read(path).unwrap(); let image = Decoder::new(path, &data).decode().unwrap(); - assert_eq!(image.color_space(), &ColorSpace::Rgba); assert_ne!(image.data().len(), 0); assert_ne!(image.size(), (0, 0)); }) @@ -494,7 +453,6 @@ mod tests { let data = fs::read(path).unwrap(); let image = Decoder::new(path, &data).decode().unwrap(); - assert_eq!(image.color_space(), &ColorSpace::Rgba); assert_ne!(image.data().len(), 0); assert_ne!(image.size(), (0, 0)); }) @@ -519,7 +477,6 @@ mod tests { let data = fs::read(path).unwrap(); let image = Decoder::new(path, &data).decode().unwrap(); - assert_eq!(image.color_space(), &ColorSpace::Rgb); assert_ne!(image.data().len(), 0); assert_ne!(image.size(), (0, 0)); }) @@ -544,7 +501,6 @@ mod tests { let data = fs::read(path).unwrap(); let image = Decoder::new(path, &data).decode().unwrap(); - assert_eq!(image.color_space(), &ColorSpace::Rgba); assert_ne!(image.data().len(), 0); assert_ne!(image.size(), (0, 0)); }) @@ -583,7 +539,7 @@ mod tests { entry.path() }) .filter(|path| { - let re = Regex::new(r"^tests/files/[^x]").unwrap(); + let re = Regex::new(r"^tests/files/[^x].+\.png").unwrap(); re.is_match(path.to_str().unwrap_or("")) }) .collect(); @@ -595,16 +551,14 @@ mod tests { let out_path = path.with_extension("out.jpg"); - let conf = Config::build(out_path.clone(), 75.0, OutputFormat::MozJpeg).unwrap(); + let conf = Config::build(75.0, OutputFormat::MozJpeg).unwrap(); let encoder = Encoder::new(&conf, image); let result = encoder.encode(); assert!(result.is_ok()); - assert!(out_path.exists()); - assert!(out_path.is_file()); - assert!(out_path.metadata().unwrap().len() > 0); - assert!(fs::remove_file(out_path).is_ok()); + let result = result.unwrap(); + assert!(!result.is_empty()); }) } @@ -617,7 +571,7 @@ mod tests { entry.path() }) .filter(|path| { - let re = Regex::new(r"^tests/files/[^x]").unwrap(); + let re = Regex::new(r"^tests/files/[^x].+\.png").unwrap(); re.is_match(path.to_str().unwrap_or("")) }) .collect(); @@ -629,16 +583,14 @@ mod tests { let out_path = path.with_extension("out.png"); - let conf = Config::build(out_path.clone(), 75.0, OutputFormat::Png).unwrap(); + let conf = Config::build(75.0, OutputFormat::Png).unwrap(); let encoder = Encoder::new(&conf, image); let result = encoder.encode(); assert!(result.is_ok()); - assert!(out_path.exists()); - assert!(out_path.is_file()); - assert!(out_path.metadata().unwrap().len() > 0); - assert!(fs::remove_file(out_path).is_ok()); + let result = result.unwrap(); + assert!(!result.is_empty()); }) } @@ -651,7 +603,7 @@ mod tests { entry.path() }) .filter(|path| { - let re = Regex::new(r"^tests/files/[^x]").unwrap(); + let re = Regex::new(r"^tests/files/[^x].+\.png").unwrap(); re.is_match(path.to_str().unwrap_or("")) }) .collect(); @@ -663,16 +615,14 @@ mod tests { let out_path = path.with_extension("out.oxi.png"); - let conf = Config::build(out_path.clone(), 75.0, OutputFormat::Oxipng).unwrap(); + let conf = Config::build(75.0, OutputFormat::Oxipng).unwrap(); let encoder = Encoder::new(&conf, image); let result = encoder.encode(); assert!(result.is_ok()); - assert!(out_path.exists()); - assert!(out_path.is_file()); - assert!(out_path.metadata().unwrap().len() > 0); - assert!(fs::remove_file(out_path).is_ok()); + let result = result.unwrap(); + assert!(!result.is_empty()); }) } } diff --git a/src/main.rs b/src/main.rs index dd6ef3ac..ab8e603e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,8 @@ -use std::{fs, io, path, process}; +use std::{error::Error, fs, io, path, process}; use clap::Parser; use indicatif::{ProgressBar, ProgressStyle}; -use rimage::{decoders, encoders, image::OutputFormat, Decoder}; +use rimage::{decoders, encoders, image::OutputFormat, Config, Decoder, Encoder}; #[derive(Parser)] #[command(author, about, version, long_about = None)] @@ -20,7 +20,7 @@ struct Args { info: bool, } -fn main() { +fn main() -> Result<(), Box> { let mut args = Args::parse_from(wild::args_os()); let pb = ProgressBar::new(args.input.len() as u64); @@ -34,16 +34,18 @@ fn main() { .collect(); } + let conf = Config::build(args.quality, args.output_format)?; + pb.set_style( ProgressStyle::with_template("{bar:40.green/blue} {pos}/{len} {wide_msg}") .unwrap() - .progress_chars("##*"), + .progress_chars("##-"), ); pb.set_position(0); if args.info { for path in &args.input { - let data = fs::read(path).unwrap(); + let data = fs::read(path)?; let d = Decoder::new(path, &data); let img = match d.decode() { @@ -55,7 +57,6 @@ fn main() { }; println!("{:?}", path.file_name().unwrap()); - println!("Color Space: {:?}", img.color_space()); println!("Size: {:?}", img.size()); println!("Data length: {:?}", img.data().len()); println!(); @@ -67,28 +68,12 @@ fn main() { pb.set_message(path.file_name().unwrap().to_str().unwrap().to_owned()); pb.inc(1); - let (pixels, width, height) = match decoders::decode_image(path) { - Ok(data) => data, - Err(e) => { - println!("{e}"); - continue; - } - }; + let data = fs::read(&path)?; - match encoders::encode_image( - path, - &pixels, - &args.output_format.to_string(), - width, - height, - args.quality, - ) { - Ok(()) => (), - Err(e) => { - println!("{e}"); - continue; - } - }; + let d = Decoder::new(&path, &data); + let e = Encoder::new(&conf, d.decode()?); + fs::write(path, e.encode()?)? } pb.finish(); + Ok(()) } From d05c218b05bd49e3c56e5fd4100aaedf66e22e1b Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Mon, 27 Mar 2023 15:29:02 +0200 Subject: [PATCH 41/46] Rewrite benches --- benches/encode_jpg.rs | 16 +++++----------- benches/encode_png.rs | 20 +++++++------------- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/benches/encode_jpg.rs b/benches/encode_jpg.rs index 4395c302..5387c436 100644 --- a/benches/encode_jpg.rs +++ b/benches/encode_jpg.rs @@ -33,23 +33,17 @@ fn bench_encode_jpg(c: &mut Criterion) { }); group.bench_function("Encoder", |b| { b.iter(|| { - rimage::Encoder::new( - black_box( - &rimage::Config::build( - PathBuf::from("en_u.jpg"), - 75.0, - rimage::OutputFormat::MozJpeg, - ) - .unwrap(), - ), + let data = rimage::Encoder::new( + black_box(&rimage::Config::build(75.0, rimage::OutputFormat::MozJpeg).unwrap()), black_box(image.clone()), ) .encode() .unwrap(); + fs::write("en_u.jpg", data).unwrap(); }) }); - // fs::remove_file("en.jpg").unwrap(); - // fs::remove_file("en_u.jpg").unwrap(); + fs::remove_file("en.jpg").unwrap(); + fs::remove_file("en_u.jpg").unwrap(); } criterion_group!(benches, bench_encode_jpg); diff --git a/benches/encode_png.rs b/benches/encode_png.rs index 6bac609f..aaf01a91 100644 --- a/benches/encode_png.rs +++ b/benches/encode_png.rs @@ -14,8 +14,8 @@ fn bench_encode_png(c: &mut Criterion) { let (pixels, width, height) = decode_image(&PathBuf::from("tests/files/basi6a08.png")).unwrap(); - let data = fs::read(&Path::new("tests/files/basi6a08.jpg")).unwrap(); - let image = Decoder::new(&Path::new("tests/files/basi6a08.jpg"), &data) + let data = fs::read(&Path::new("tests/files/basi6a08.png")).unwrap(); + let image = Decoder::new(&Path::new("tests/files/basi6a08.png"), &data) .decode() .unwrap(); @@ -33,23 +33,17 @@ fn bench_encode_png(c: &mut Criterion) { }); group.bench_function("Encoder", |b| { b.iter(|| { - rimage::Encoder::new( - black_box( - &rimage::Config::build( - PathBuf::from("en_u.png"), - 75.0, - rimage::OutputFormat::Oxipng, - ) - .unwrap(), - ), + let data = rimage::Encoder::new( + black_box(&rimage::Config::build(75.0, rimage::OutputFormat::Oxipng).unwrap()), black_box(image.clone()), ) .encode() .unwrap(); + fs::write("en_u.png", data).unwrap(); }) }); - // fs::remove_file("en.png").unwrap(); - // fs::remove_file("en_u.png").unwrap(); + fs::remove_file("en.png").unwrap(); + fs::remove_file("en_u.png").unwrap(); } criterion_group!(benches, bench_encode_png); From 2bb71ccb28767becf9dca7a7329f42d0563686ea Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Mon, 27 Mar 2023 15:29:15 +0200 Subject: [PATCH 42/46] Added new api to main --- src/image.rs | 4 ++-- src/lib.rs | 6 +++--- src/main.rs | 21 ++++++++++++++++++--- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/image.rs b/src/image.rs index feea385c..477de6c0 100644 --- a/src/image.rs +++ b/src/image.rs @@ -62,9 +62,9 @@ impl std::str::FromStr for OutputFormat { impl fmt::Display for OutputFormat { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - OutputFormat::MozJpeg => write!(f, "jpeg"), + OutputFormat::MozJpeg => write!(f, "jpg"), OutputFormat::Png => write!(f, "png"), - OutputFormat::Oxipng => write!(f, "oxipng"), + OutputFormat::Oxipng => write!(f, "png"), } } } diff --git a/src/lib.rs b/src/lib.rs index 5fb810d3..5dc56be4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,7 +89,7 @@ std::fs::write("output.jpg", data); use error::{ConfigError, DecodingError, EncodingError}; use rgb::{ alt::{GRAY8, GRAYA8}, - AsPixels, FromSlice, RGB8, RGBA8, + AsPixels, ComponentBytes, FromSlice, RGB8, RGBA8, }; use std::{panic, path}; @@ -184,14 +184,14 @@ impl<'a> Decoder<'a> { let d = mozjpeg::Decompress::new_mem(&self.raw_data)?; let mut image = d.rgba()?; - let data = image.read_scanlines().ok_or(DecodingError::Parsing( + let data: Vec = image.read_scanlines().ok_or(DecodingError::Parsing( "Failed to read scanlines".to_string(), ))?; Ok(ImageData::new( image.width() as usize, image.height() as usize, - data, + data.as_bytes().to_owned(), )) }) .unwrap_or(Err(DecodingError::Parsing( diff --git a/src/main.rs b/src/main.rs index ab8e603e..4593a18d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use std::{error::Error, fs, io, path, process}; use clap::Parser; use indicatif::{ProgressBar, ProgressStyle}; -use rimage::{decoders, encoders, image::OutputFormat, Config, Decoder, Encoder}; +use rimage::{image::OutputFormat, Config, Decoder, Encoder}; #[derive(Parser)] #[command(author, about, version, long_about = None)] @@ -18,6 +18,9 @@ struct Args { /// Print image info #[arg(short, long)] info: bool, + /// Prefix of the output file + #[arg(short, long)] + suffix: Option, } fn main() -> Result<(), Box> { @@ -66,13 +69,25 @@ fn main() -> Result<(), Box> { for path in &args.input { pb.set_message(path.file_name().unwrap().to_str().unwrap().to_owned()); - pb.inc(1); let data = fs::read(&path)?; let d = Decoder::new(&path, &data); let e = Encoder::new(&conf, d.decode()?); - fs::write(path, e.encode()?)? + + let mut new_path = path.clone(); + let ext = args.output_format.to_string(); + let suffix = args.suffix.clone().unwrap_or_default(); + + new_path.set_file_name(format!( + "{}{}", + path.file_stem().unwrap().to_str().unwrap(), + suffix, + )); + new_path.set_extension(ext); + + fs::write(new_path, e.encode()?)?; + pb.inc(1); } pb.finish(); Ok(()) From 3e94daafe7e6aa58660b98c1d4f7a10f9011da0d Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Mon, 27 Mar 2023 15:32:33 +0200 Subject: [PATCH 43/46] Fixed clippy errors --- src/lib.rs | 20 +++++++------------- src/main.rs | 4 ++-- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5dc56be4..6e4c9230 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -118,7 +118,7 @@ pub struct Config { impl Config { /// Create new config pub fn build(quality: f32, output_format: OutputFormat) -> Result { - if quality < 0.0 || quality > 100.0 { + if !(0.0..=100.0).contains(&quality) { return Err(ConfigError::QualityOutOfBounds); } @@ -181,7 +181,7 @@ impl<'a> Decoder<'a> { fn decode_jpeg(&self) -> Result { panic::catch_unwind(|| -> Result { - let d = mozjpeg::Decompress::new_mem(&self.raw_data)?; + let d = mozjpeg::Decompress::new_mem(self.raw_data)?; let mut image = d.rgba()?; let data: Vec = image.read_scanlines().ok_or(DecodingError::Parsing( @@ -189,8 +189,8 @@ impl<'a> Decoder<'a> { ))?; Ok(ImageData::new( - image.width() as usize, - image.height() as usize, + image.width(), + image.height(), data.as_bytes().to_owned(), )) }) @@ -211,7 +211,7 @@ impl<'a> Decoder<'a> { } fn decode_png(&self) -> Result { - let mut d = png::Decoder::new(&self.raw_data[..]); + let mut d = png::Decoder::new(self.raw_data); d.set_transformations(png::Transformations::normalize_to_color8()); let mut reader = d.read_info()?; @@ -226,7 +226,7 @@ impl<'a> Decoder<'a> { match info.color_type { png::ColorType::Grayscale => { - Self::expand_pixels(&mut buf, |gray: GRAY8| GRAY8::from(gray).into()) + Self::expand_pixels(&mut buf, |gray: GRAY8| gray.into()) } png::ColorType::GrayscaleAlpha => Self::expand_pixels(&mut buf, GRAYA8::into), png::ColorType::Rgb => Self::expand_pixels(&mut buf, RGB8::into), @@ -274,7 +274,7 @@ impl<'a> Encoder<'a> { encoder.set_progressive_mode(); encoder.set_mem_dest(); encoder.start_compress(); - encoder.write_scanlines(&self.image_data.data()); + encoder.write_scanlines(self.image_data.data()); encoder.finish_compress(); encoder.data_to_vec().map_err(|_| { @@ -549,8 +549,6 @@ mod tests { let data = fs::read(path).unwrap(); let image = Decoder::new(path, &data).decode().unwrap(); - let out_path = path.with_extension("out.jpg"); - let conf = Config::build(75.0, OutputFormat::MozJpeg).unwrap(); let encoder = Encoder::new(&conf, image); @@ -581,8 +579,6 @@ mod tests { let data = fs::read(path).unwrap(); let image = Decoder::new(path, &data).decode().unwrap(); - let out_path = path.with_extension("out.png"); - let conf = Config::build(75.0, OutputFormat::Png).unwrap(); let encoder = Encoder::new(&conf, image); @@ -613,8 +609,6 @@ mod tests { let data = fs::read(path).unwrap(); let image = Decoder::new(path, &data).decode().unwrap(); - let out_path = path.with_extension("out.oxi.png"); - let conf = Config::build(75.0, OutputFormat::Oxipng).unwrap(); let encoder = Encoder::new(&conf, image); diff --git a/src/main.rs b/src/main.rs index 4593a18d..4114ca82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -70,9 +70,9 @@ fn main() -> Result<(), Box> { for path in &args.input { pb.set_message(path.file_name().unwrap().to_str().unwrap().to_owned()); - let data = fs::read(&path)?; + let data = fs::read(path)?; - let d = Decoder::new(&path, &data); + let d = Decoder::new(path, &data); let e = Encoder::new(&conf, d.decode()?); let mut new_path = path.clone(); From af89c082469e4bba19b66c586fb99b85bd3c705a Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Mon, 27 Mar 2023 16:08:41 +0200 Subject: [PATCH 44/46] Added some tests for docs --- tests/files/test.bmp | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/files/test.bmp diff --git a/tests/files/test.bmp b/tests/files/test.bmp new file mode 100644 index 00000000..e69de29b From c859d3ca63694fcc1d589053874d8052ce786b08 Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Mon, 27 Mar 2023 16:08:56 +0200 Subject: [PATCH 45/46] Update CHANGELOG --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed4e0aff..e6b81096 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,16 @@ ## v0.2.0 -- [Added] struct `ImageData` for storing images -- [Added] struct `DecodingError` to process errors +- [Added] struct `ImageData` for storing images data - [Added] struct `Decoder` to decode images +- [Added] struct `Encoder` to encode images +- [Added] structs for errors in `rimage::errors` - [Added] image processing from stdio - [Added] info option +- [Added] suffix option - [Changed] `decoders::decode_image` and `encoders::encode_image` now deprecated, use `Decoder` and `Encoder` structs instead +- [Improvement] Added documentation to almost all functions and structs with examples ## v0.1.3 From caa6939d54ede29a024c18d18743b35aa264f2db Mon Sep 17 00:00:00 2001 From: SalOne22 <111443297+SalOne22@users.noreply.github.com> Date: Mon, 27 Mar 2023 16:09:04 +0200 Subject: [PATCH 46/46] Rewrite docs --- src/image.rs | 8 ++- src/lib.rs | 185 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 181 insertions(+), 12 deletions(-) diff --git a/src/image.rs b/src/image.rs index 477de6c0..6781b8d9 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,6 +1,8 @@ use std::{borrow::Cow, fmt}; /// Image data +/// +/// Used to store dimensions and data of an image #[derive(Debug, Clone)] pub struct ImageData { width: usize, @@ -14,7 +16,7 @@ impl ImageData { /// # Examples /// ``` /// # use rimage::{ImageData, image}; - /// let image = ImageData::new(100, 100, vec![0; 100 * 100]); + /// let image = ImageData::new(100, 100, vec![0; 100 * 100 * 4]); // 100x100 RGBA image /// ``` pub fn new(width: usize, height: usize, data: Vec) -> Self { Self { @@ -35,8 +37,8 @@ impl ImageData { } } -/// Image format to encode -#[derive(Debug, Clone, Copy)] +/// Image format for output +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum OutputFormat { /// MozJpeg image MozJpeg, diff --git a/src/lib.rs b/src/lib.rs index 6e4c9230..5ba293ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,9 +26,9 @@ After that you can use this crate: ## Decoding ``` use rimage::Decoder; -# let path = std::path::PathBuf::from("tests/files/basi0g01.jpg"); // Create decoder from file path and data +let path = std::path::PathBuf::from("tests/files/basi0g01.jpg"); // Or any other image let data = std::fs::read(&path).unwrap(); let decoder = Decoder::new(&path, &data); @@ -36,7 +36,7 @@ let decoder = Decoder::new(&path, &data); let image = match decoder.decode() { Ok(img) => img, Err(e) => { - eprintln!("Oh no there is another error! {e}"); + eprintln!("Oh no, there is error! {e}"); std::process::exit(1); } }; @@ -58,24 +58,25 @@ use rimage::{Config, Encoder, OutputFormat}; # let decoder = Decoder::new(&path, &data); # let image = decoder.decode().unwrap(); -// Encode image to file +// Build config for encoding let config = match Config::build( 75.0, OutputFormat::MozJpeg, ) { Ok(config) => config, Err(e) => { - eprintln!("Oh no there is error! {e}"); + eprintln!("Oh no, there is error! {e}"); std::process::exit(1); } }; -let encoder = Encoder::new(&config, image); +let encoder = Encoder::new(&config, image); // where image is image::ImageData +// Get encoded image data from encoder let data = match encoder.encode() { Ok(data) => data, Err(e) => { - eprintln!("Oh no there is error! {e}"); + eprintln!("Oh no, there is error! {e}"); std::process::exit(1); } }; @@ -109,6 +110,22 @@ pub mod error; pub mod image; /// Config for image encoding +/// +/// # Example +/// ``` +/// use rimage::{Config, OutputFormat}; +/// +/// let config = Config::build(75.0, OutputFormat::MozJpeg).unwrap(); +/// ``` +/// +/// # Default +/// ``` +/// use rimage::{Config, OutputFormat}; +/// +/// let config = Config::default(); +/// assert_eq!(config.quality(), 75.0); +/// assert_eq!(config.output_format(), &OutputFormat::MozJpeg); +/// ``` #[derive(Debug)] pub struct Config { quality: f32, @@ -117,6 +134,24 @@ pub struct Config { impl Config { /// Create new config + /// + /// # Example + /// ``` + /// use rimage::{Config, OutputFormat}; + /// + /// let config = Config::build(75.0, OutputFormat::MozJpeg).unwrap(); + /// ``` + /// + /// # Errors + /// + /// If quality is not in range 0.0..=100.0 + /// + /// ``` + /// use rimage::{Config, OutputFormat}; + /// + /// let config = Config::build(200.0, OutputFormat::MozJpeg); + /// assert!(config.is_err()); + /// ``` pub fn build(quality: f32, output_format: OutputFormat) -> Result { if !(0.0..=100.0).contains(&quality) { return Err(ConfigError::QualityOutOfBounds); @@ -128,11 +163,27 @@ impl Config { }) } /// Get quality + /// + /// # Example + /// ``` + /// use rimage::{Config, OutputFormat}; + /// + /// let config = Config::build(75.0, OutputFormat::MozJpeg).unwrap(); + /// assert_eq!(config.quality(), 75.0); + /// ``` #[inline] pub fn quality(&self) -> f32 { self.quality } /// Get output format + /// + /// # Example + /// ``` + /// use rimage::{Config, OutputFormat}; + /// + /// let config = Config::build(75.0, OutputFormat::MozJpeg).unwrap(); + /// assert_eq!(config.output_format(), &OutputFormat::MozJpeg); + /// ``` #[inline] pub fn output_format(&self) -> &OutputFormat { &self.output_format @@ -150,6 +201,24 @@ impl Default for Config { } /// Decoder for images +/// +/// # Example +/// ``` +/// # use rimage::Decoder; +/// # let path = std::path::PathBuf::from("tests/files/basi0g01.jpg"); +/// let data = std::fs::read(&path).unwrap(); +/// +/// let decoder = Decoder::new(&path, &data); +/// +/// // Decode image to image data +/// let image = match decoder.decode() { +/// Ok(img) => img, +/// Err(e) => { +/// eprintln!("Oh no there is error! {e}"); +/// std::process::exit(1); +/// } +/// }; +/// ``` pub struct Decoder<'a> { path: &'a path::Path, raw_data: &'a [u8], @@ -157,12 +226,69 @@ pub struct Decoder<'a> { impl<'a> Decoder<'a> { /// Create new decoder + /// + /// # Example + /// ``` + /// # use rimage::Decoder; + /// # let path = std::path::PathBuf::from("tests/files/basi0g01.jpg"); + /// let data = std::fs::read(&path).unwrap(); + /// + /// let decoder = Decoder::new(&path, &data); + /// ``` #[inline] pub fn new(path: &'a path::Path, raw_data: &'a [u8]) -> Self { Decoder { path, raw_data } } /// Decode image + /// + /// # Example + /// ``` + /// # use rimage::Decoder; + /// # let path = std::path::PathBuf::from("tests/files/basi0g01.jpg"); + /// # let data = std::fs::read(&path).unwrap(); + /// # let decoder = Decoder::new(&path, &data); + /// // Decode image to image data + /// let image = match decoder.decode() { + /// Ok(img) => img, + /// Err(e) => { + /// eprintln!("Oh no there is error! {e}"); + /// std::process::exit(1); + /// } + /// }; + /// + /// // Do something with image data... + /// ``` + /// + /// # Errors + /// + /// If image format is not supported + /// + /// ``` + /// # use rimage::Decoder; + /// let path = std::path::PathBuf::from("tests/files/test.bmp"); + /// let data = std::fs::read(&path).unwrap(); + /// let decoder = Decoder::new(&path, &data); + /// + /// let result = decoder.decode(); + /// + /// assert!(result.is_err()); + /// assert_eq!(result.unwrap_err().to_string(), "Format Error: bmp"); + /// ``` + /// + /// If image format is supported but there is error during decoding + /// + /// ``` + /// # use rimage::Decoder; + /// let path = std::path::PathBuf::from("tests/files/test_corrupted.jpg"); + /// let data = std::fs::read(&path).unwrap(); + /// let decoder = Decoder::new(&path, &data); + /// + /// let result = decoder.decode(); + /// + /// assert!(result.is_err()); + /// assert_eq!(result.unwrap_err().to_string(), "Parsing Error: Failed to decode jpeg"); + /// ``` pub fn decode(&self) -> Result { let extension = match self.path.extension() { Some(ext) => ext, @@ -225,9 +351,7 @@ impl<'a> Decoder<'a> { let info = reader.next_frame(&mut buf)?; match info.color_type { - png::ColorType::Grayscale => { - Self::expand_pixels(&mut buf, |gray: GRAY8| gray.into()) - } + png::ColorType::Grayscale => Self::expand_pixels(&mut buf, |gray: GRAY8| gray.into()), png::ColorType::GrayscaleAlpha => Self::expand_pixels(&mut buf, GRAYA8::into), png::ColorType::Rgb => Self::expand_pixels(&mut buf, RGB8::into), png::ColorType::Rgba => {} @@ -243,6 +367,21 @@ impl<'a> Decoder<'a> { } /// Encoder for images +/// +/// # Example +/// ``` +/// # use rimage::{Encoder, Config, ImageData, OutputFormat}; +/// # let path = std::path::PathBuf::from("tests/files/basi0g01.jpg"); +/// # let data = std::fs::read(&path).unwrap(); +/// # let decoder = rimage::Decoder::new(&path, &data); +/// # let image = decoder.decode().unwrap(); +/// let config = Config::default(); +/// +/// // image is ImageData +/// let encoder = Encoder::new(&config, image); +/// let result = encoder.encode(); +/// assert!(result.is_ok()); +/// ``` pub struct Encoder<'a> { image_data: ImageData, config: &'a Config, @@ -250,6 +389,17 @@ pub struct Encoder<'a> { impl<'a> Encoder<'a> { /// Create new encoder + /// + /// # Example + /// ``` + /// # use rimage::{Encoder, Config, ImageData, OutputFormat}; + /// # let path = std::path::PathBuf::from("tests/files/basi0g01.jpg"); + /// # let data = std::fs::read(&path).unwrap(); + /// # let decoder = rimage::Decoder::new(&path, &data); + /// # let image = decoder.decode().unwrap(); + /// let config = Config::default(); + /// let encoder = Encoder::new(&config, image); // where image is ImageData + /// ``` pub fn new(conf: &'a Config, image_data: ImageData) -> Self { Encoder { image_data, @@ -257,6 +407,23 @@ impl<'a> Encoder<'a> { } } /// Encode image + /// + /// # Example + /// ``` + /// # use rimage::{Encoder, Config, ImageData, OutputFormat}; + /// # let path = std::path::PathBuf::from("tests/files/basi0g01.jpg"); + /// # let data = std::fs::read(&path).unwrap(); + /// # let decoder = rimage::Decoder::new(&path, &data); + /// # let image = decoder.decode().unwrap(); + /// let config = Config::default(); + /// let encoder = Encoder::new(&config, image); // where image is ImageData + /// let result = encoder.encode(); + /// assert!(result.is_ok()); + /// ``` + /// + /// # Errors + /// + /// Returns [`EncodingError`] if encoding failed pub fn encode(self) -> Result, EncodingError> { match self.config.output_format { OutputFormat::Png => self.encode_png(),