diff --git a/Cargo.lock b/Cargo.lock index 4757729..445c458 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -133,6 +133,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "0.6.4" @@ -652,6 +658,12 @@ version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cbc" version = "0.1.2" @@ -725,6 +737,33 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half 1.8.2", +] + [[package]] name = "cipher" version = "0.4.4" @@ -983,6 +1022,42 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + [[package]] name = "crossbeam-channel" version = "0.5.8" @@ -1457,6 +1532,7 @@ version = "0.1.0" dependencies = [ "camino", "chrono", + "criterion", "image", "sanitize-filename", "serde", @@ -1493,6 +1569,7 @@ dependencies = [ "glob", "thiserror", "tracing", + "zip", ] [[package]] @@ -1505,6 +1582,7 @@ dependencies = [ "thiserror", "tracing", "tracing-subscriber", + "zip", ] [[package]] @@ -1733,7 +1811,7 @@ checksum = "832a761f35ab3e6664babfbdc6cef35a4860e816ec3916dcfd0882954e98a8a8" dependencies = [ "bit_field", "flume", - "half", + "half 2.2.1", "lebe", "miniz_oxide", "rayon-core", @@ -2400,6 +2478,12 @@ dependencies = [ "svg_fmt", ] +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + [[package]] name = "half" version = "2.2.1" @@ -2598,7 +2682,7 @@ dependencies = [ "bitflags 1.3.2", "bytemuck", "glam", - "half", + "half 2.2.1", "iced_core", "image", "kamadak-exif", @@ -2847,6 +2931,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "is-terminal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" +dependencies = [ + "hermit-abi", + "rustix 0.38.24", + "windows-sys 0.52.0", +] + [[package]] name = "istring" version = "0.3.4" @@ -3627,6 +3722,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + [[package]] name = "orbclient" version = "0.3.47" @@ -4013,6 +4114,34 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" + +[[package]] +name = "plotters-svg" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +dependencies = [ + "plotters-backend", +] + [[package]] name = "png" version = "0.17.10" @@ -5168,6 +5297,16 @@ dependencies = [ "strict-num", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -6083,6 +6222,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -6113,6 +6261,21 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + [[package]] name = "windows-tokens" version = "0.44.0" @@ -6131,6 +6294,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -6143,6 +6312,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -6155,6 +6330,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -6167,6 +6348,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -6179,6 +6366,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -6191,6 +6384,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -6203,6 +6402,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winit" version = "0.28.7" diff --git a/Cargo.toml b/Cargo.toml index bd45353..6076dd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ camino = "1.1.4" clap = { version = "4.3.5", features = ["derive"] } cli-table = "0.4.7" chrono = "0.4.31" +criterion = { version = "0.5.1", features = ["html_reports"] } dark-light = "1.0.0" dialoguer = "0.10.4" dioxus = "0.4.0" diff --git a/eco-cbz/Cargo.toml b/eco-cbz/Cargo.toml index 0f9975a..5692b77 100644 --- a/eco-cbz/Cargo.toml +++ b/eco-cbz/Cargo.toml @@ -16,6 +16,13 @@ thiserror.workspace = true tracing.workspace = true zip.workspace = true +[dev-dependencies] +criterion.workspace = true + [features] default = [] metadata = ["dep:chrono", "dep:serde", "dep:serde_json", "dep:serde_repr"] + +[[bench]] +name = "image" +harness = false diff --git a/eco-cbz/benches/image.rs b/eco-cbz/benches/image.rs new file mode 100644 index 0000000..7942be8 --- /dev/null +++ b/eco-cbz/benches/image.rs @@ -0,0 +1,57 @@ +use std::{fs::File, io::Read}; + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use eco_cbz::Image; +use zip::{read::ZipFile, ZipArchive}; + +static INDEX: usize = 1; + +fn try_from_bytes(bytes: &[u8]) { + Image::try_from_bytes(bytes).unwrap(); +} + +fn try_from_zip_file(file: ZipFile) { + Image::try_from_zip_file(file).unwrap(); +} + +fn try_into_bytes(bytes: &[u8]) { + let img = Image::try_from_bytes(bytes).unwrap(); + img.try_into_bytes().unwrap(); +} + +fn criterion_benchmark(c: &mut Criterion) { + let test_file_path = std::env::current_dir().unwrap().join("../test.cbz"); + if !test_file_path.exists() { + panic!("a test.cbz file must be present at the root of this project"); + } + + let mut zip = ZipArchive::new(File::open(test_file_path).unwrap()).unwrap(); + let bytes = { + let mut file = zip.by_index(INDEX).unwrap(); + let mut bytes = Vec::with_capacity(file.size() as usize); + file.read_to_end(&mut bytes).unwrap(); + bytes + }; + + c.bench_function(&format!("try_from_bytes {INDEX}"), |b| { + b.iter(|| { + try_from_bytes(black_box(&bytes)); + }) + }); + + c.bench_function(&format!("try_from_zip_file {INDEX}"), |b| { + b.iter(|| { + let file = zip.by_index(INDEX).unwrap(); + try_from_zip_file(black_box(file)); + }) + }); + + c.bench_function(&format!("try_into_bytes {INDEX}"), |b| { + b.iter(|| { + try_into_bytes(black_box(&bytes)); + }) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/eco-cbz/src/cbz.rs b/eco-cbz/src/cbz.rs index acda2b4..284dabc 100644 --- a/eco-cbz/src/cbz.rs +++ b/eco-cbz/src/cbz.rs @@ -1,6 +1,6 @@ use std::{ fs::{File, OpenOptions}, - io::{Cursor, Read, Seek, Write}, + io::{BufRead, Cursor, Read, Seek, Write}, path::Path, }; @@ -9,7 +9,7 @@ use tracing::debug; use zip::{read::ZipFile, write::FileOptions, ZipArchive, ZipWriter}; pub use crate::errors::{Error, Result}; -use crate::image::Image; +use crate::image::{Image, ImageBuf}; /// We artificially limit the amount of accepted files to 65535 files per Cbz /// First as it'd be rather impractical for the user to read such enormous Cbz @@ -73,7 +73,7 @@ where /// /// Fails if file size is too large to fit a `usize` on host machine /// or if the content can't be read - pub fn read_by_name(&mut self, name: &str) -> Result { + pub fn read_by_name(&mut self, name: &str) -> Result { let file = self.archive.by_name(name)?; file.try_into() } @@ -88,7 +88,7 @@ where /// Iterate over images present in the Cbz. pub fn for_each(&mut self, mut f: F) where - F: FnMut(Result), + F: FnMut(Result), { for file_name in self.file_names() { f(self.read_by_name(&file_name)); @@ -103,7 +103,7 @@ where /// Returns an error immediately if the provided closure returns an error pub fn try_for_each(&mut self, mut f: F) -> Result<(), E> where - F: FnMut(Result) -> Result<(), E>, + F: FnMut(Result) -> Result<(), E>, { for file_name in self.file_names() { f(self.read_by_name(&file_name))?; @@ -238,18 +238,39 @@ where /// ## Errors /// /// Same behavior as `insert_with_extension_and_file_options` - pub fn insert(&mut self, image: Image) -> Result<()> { + pub fn insert(&mut self, image: Image) -> Result<()> { let extension = image .format() - .and_then(|f| f.extensions_str().first().copied()) - .unwrap_or("png"); + .extensions_str() + .first() + .ok_or(Error::UnknownImageExtension)?; self.insert_with_extension_and_file_options(image, extension, FileOptions::default()) } /// ## Errors /// /// Same behavior as `insert_with_extension_and_file_options` - pub fn insert_with_extension(&mut self, image: Image, extension: &str) -> Result<()> { + pub fn insert_with_file_options( + &mut self, + image: Image, + file_options: FileOptions, + ) -> Result<()> { + let extension = image + .format() + .extensions_str() + .first() + .ok_or(Error::UnknownImageExtension)?; + self.insert_with_extension_and_file_options(image, extension, file_options) + } + + /// ## Errors + /// + /// Same behavior as `insert_with_extension_and_file_options` + pub fn insert_with_extension( + &mut self, + image: Image, + extension: &str, + ) -> Result<()> { self.insert_with_extension_and_file_options(image, extension, FileOptions::default()) } @@ -285,9 +306,9 @@ where /// ## Errors /// /// This fails if the Cbz writer can't be written or if it's full (i.e. its size equals `MAX_FILE_NUMBER`) - pub fn insert_with_extension_and_file_options( + pub fn insert_with_extension_and_file_options( &mut self, - image: Image, + image: Image, extension: &str, file_options: FileOptions, ) -> Result<()> { @@ -352,6 +373,7 @@ impl Writer>> { let mut file = OpenOptions::new() .read(true) .write(true) + .truncate(true) .create(true) .open( path.with_file_name( diff --git a/eco-cbz/src/errors.rs b/eco-cbz/src/errors.rs index ff85b05..7e0c66f 100644 --- a/eco-cbz/src/errors.rs +++ b/eco-cbz/src/errors.rs @@ -35,6 +35,12 @@ pub enum Error { #[error("image error: {0}")] Image(#[from] image::ImageError), + #[error("unknown image format error")] + UnknownImageFormat, + + #[error("unknown image extension error")] + UnknownImageExtension, + #[cfg(feature = "metadata")] #[error("metadata error: {0}")] MetadataFormat(#[from] serde_json::Error), diff --git a/eco-cbz/src/image.rs b/eco-cbz/src/image.rs index 0ed53a4..87549c1 100644 --- a/eco-cbz/src/image.rs +++ b/eco-cbz/src/image.rs @@ -1,5 +1,6 @@ use std::{ - io::{BufRead, Cursor, Read, Seek}, + fs::File, + io::{BufRead, BufReader, Cursor, Read, Seek}, path::Path, }; @@ -14,46 +15,103 @@ pub enum ReadingOrder { Ltr, } -#[derive(Debug, PartialEq)] -pub struct Image { - dynamic_image: DynamicImage, - format: Option, +enum ImageInner { + Reader(Option>), + DynamicImage(DynamicImage), } -impl Image { +impl ImageInner +where + R: BufRead + Seek, +{ + fn decode(&mut self) { + if let Self::Reader(reader) = self { + let reader = reader.take().unwrap(); + *self = Self::DynamicImage(reader.decode().unwrap()); + } + } + + fn dynamic_image(&self) -> Option<&DynamicImage> { + if let Self::DynamicImage(dynamic_image) = self { + Some(dynamic_image) + } else { + None + } + } +} + +impl From for ImageInner { + fn from(dynamic_image: DynamicImage) -> Self { + Self::DynamicImage(dynamic_image) + } +} + +impl From> for ImageInner { + fn from(reader: ImageReader) -> Self { + Self::Reader(Some(reader)) + } +} + +pub struct Image { + format: ImageFormat, + inner: ImageInner, +} + +#[allow(clippy::module_name_repetitions)] +pub type ImageBuf = Image>>; + +#[allow(clippy::module_name_repetitions)] +pub type ImageBytes<'a> = Image>; + +#[allow(clippy::module_name_repetitions)] +pub type ImageFile = Image>; + +impl Image> { /// ## Errors /// /// Fails if the image can't be open or decoded pub fn open(path: impl AsRef) -> Result { - let reader = ImageReader::open(&path)?; - let format = reader.format(); + let reader = ImageReader::open(&path)?.with_guessed_format()?; + let Some(format) = reader.format() else { + return Err(Error::UnknownImageFormat); + }; + Ok(Self { - dynamic_image: reader.decode()?, + inner: reader.into(), format, }) } +} +impl<'a> Image> { /// ## Errors /// - /// Fails if the image format can't be guessed or the image can't be decoded - pub fn try_from_reader(reader: impl BufRead + Seek) -> Result { - let reader = ImageReader::new(reader).with_guessed_format()?; - let format = reader.format(); + /// Fails if the image format can't be guessed + pub fn try_from_bytes(bytes: &'a [u8]) -> Result { + let reader = ImageReader::new(Cursor::new(bytes)).with_guessed_format()?; + let Some(format) = reader.format() else { + return Err(Error::UnknownImageFormat); + }; + Ok(Self { - dynamic_image: reader.decode()?, + inner: reader.into(), format, }) } +} +impl Image>> { /// ## Errors /// - /// Fails if the image format can't be guessed or the image can't be decoded - pub fn try_from_bytes(bytes: &[u8]) -> Result { - let buf = Cursor::new(bytes); - let reader = ImageReader::new(buf).with_guessed_format()?; - let format = reader.format(); + /// Fails if the image format can't be guessed + pub fn try_from_buf(buf: Vec) -> Result { + let reader = ImageReader::new(Cursor::new(buf)).with_guessed_format()?; + let Some(format) = reader.format() else { + return Err(Error::UnknownImageFormat); + }; + Ok(Self { - dynamic_image: reader.decode()?, + inner: reader.into(), format, }) } @@ -63,62 +121,72 @@ impl Image { #[allow(clippy::cast_possible_truncation)] let mut buf = Vec::with_capacity(file.size() as usize); file.read_to_end(&mut buf)?; + Self::try_from_buf(buf) + } +} - Self::try_from_bytes(&buf) +impl Image +where + R: BufRead + Seek, +{ + /// ## Errors + /// + /// Fails if the image format can't be guessed + pub fn try_from_reader(reader: R) -> Result { + let reader = ImageReader::new(reader).with_guessed_format()?; + let Some(format) = reader.format() else { + return Err(Error::UnknownImageFormat); + }; + + Ok(Self { + inner: reader.into(), + format, + }) } - fn from_dynamic_image(dynamic_image: DynamicImage, format: Option) -> Self { + fn from_dynamic_image(dynamic_image: DynamicImage, format: ImageFormat) -> Self { Self { - dynamic_image, + inner: dynamic_image.into(), format, } } #[must_use] - pub fn is_portrait(&self) -> bool { - self.dynamic_image.height() > self.dynamic_image.width() + pub fn is_portrait(&mut self) -> bool { + let image = self.image(); + image.height() > image.width() } #[must_use] - pub fn is_landscape(&self) -> bool { + pub fn is_landscape(&mut self) -> bool { !self.is_portrait() } #[must_use] - pub fn set_contrast(self, contrast: f32) -> Self { - Self::from_dynamic_image(self.dynamic_image.adjust_contrast(contrast), self.format) + pub fn set_contrast(mut self, contrast: f32) -> Self { + Self::from_dynamic_image(self.image().adjust_contrast(contrast), self.format) } #[must_use] - pub fn set_brightness(self, brightness: i32) -> Self { - Self::from_dynamic_image(self.dynamic_image.brighten(brightness), self.format) + pub fn set_brightness(mut self, brightness: i32) -> Self { + Self::from_dynamic_image(self.image().brighten(brightness), self.format) } #[must_use] - pub fn set_blur(self, blur: f32) -> Self { - Self::from_dynamic_image(self.dynamic_image.blur(blur), self.format) + pub fn set_blur(mut self, blur: f32) -> Self { + Self::from_dynamic_image(self.image().blur(blur), self.format) } #[must_use] - pub fn autosplit(self, reading_order: ReadingOrder) -> (Image, Image) { - let img1 = Self::from_dynamic_image( - self.dynamic_image.crop_imm( - 0, - 0, - self.dynamic_image.width() / 2, - self.dynamic_image.height(), - ), - self.format, - ); - let img2 = Self::from_dynamic_image( - self.dynamic_image.crop_imm( - self.dynamic_image.width() / 2, - 0, - self.dynamic_image.width(), - self.dynamic_image.height(), - ), - self.format, - ); + pub fn autosplit(mut self, reading_order: ReadingOrder) -> (Image, Image) { + let format = self.format; + let image = self.image(); + let height = image.height(); + let width = image.width(); + let img_width = width / 2; + + let img1 = Self::from_dynamic_image(image.crop_imm(0, 0, img_width, height), format); + let img2 = Self::from_dynamic_image(image.crop_imm(img_width, 0, width, height), format); match reading_order { ReadingOrder::Ltr => (img1, img2), ReadingOrder::Rtl => (img2, img1), @@ -126,38 +194,54 @@ impl Image { } #[must_use] - pub fn dynamic(&self) -> &DynamicImage { - &self.dynamic_image + #[allow(clippy::missing_panics_doc)] + pub fn image(&mut self) -> &DynamicImage { + self.inner.decode(); + self.inner.dynamic_image().unwrap() } #[must_use] - pub fn format(&self) -> Option { + pub fn format(&self) -> ImageFormat { self.format } pub fn set_format(&mut self, format: ImageFormat) -> &Self { - self.format = Some(format); + self.format = format; self } #[allow(clippy::missing_errors_doc)] pub fn try_into_bytes(self) -> Result> { - let mut buf = Cursor::new(Vec::new()); - self.dynamic_image - .write_to(&mut buf, self.format.unwrap_or(ImageFormat::Png))?; - Ok(buf.into_inner()) + match self.inner { + ImageInner::Reader(Some(reader)) => { + let mut buf = Vec::new(); + reader.into_inner().read_to_end(&mut buf)?; + Ok(buf) + } + ImageInner::DynamicImage(dynamic_image) => { + let mut buf = Cursor::new(Vec::new()); + let format = self.format; + + dynamic_image.write_to(&mut buf, format)?; + Ok(buf.into_inner()) + } + ImageInner::Reader(None) => unreachable!(), + } } } -impl TryFrom for Vec { +impl TryFrom> for Vec +where + R: BufRead + Seek, +{ type Error = Error; - fn try_from(image: Image) -> Result { + fn try_from(image: Image) -> Result { image.try_into_bytes() } } -impl<'a> TryFrom> for Image { +impl<'a> TryFrom> for Image>> { type Error = Error; fn try_from(file: ZipFile<'a>) -> Result { @@ -165,18 +249,18 @@ impl<'a> TryFrom> for Image { } } -impl TryFrom<&[u8]> for Image { +impl TryFrom> for Image>> { type Error = Error; - fn try_from(bytes: &[u8]) -> Result { - Self::try_from_bytes(bytes) + fn try_from(buf: Vec) -> Result { + Self::try_from_buf(buf) } } -impl TryFrom> for Image { +impl<'a> TryFrom<&'a [u8]> for Image> { type Error = Error; - fn try_from(bytes: Vec) -> Result { - Self::try_from_bytes(&bytes) + fn try_from(bytes: &'a [u8]) -> Result { + Self::try_from_bytes(bytes) } } diff --git a/eco-convert/src/lib.rs b/eco-convert/src/lib.rs index 3d345ec..a54d15c 100644 --- a/eco-convert/src/lib.rs +++ b/eco-convert/src/lib.rs @@ -2,6 +2,8 @@ use std::fs; +use ::mobi::Mobi; +use ::pdf::file::FileOptions as PdfFileOptions; use camino::Utf8PathBuf; use eco_cbz::image::ReadingOrder; use eco_pack::pack_imgs_to_cbz; @@ -51,25 +53,44 @@ pub struct ConvertOptions { /// Reading order pub reading_order: ReadingOrder, + + /// If not provided the images are stored as is (fastest), value must be between 0-9 + pub compression_level: Option, } #[allow(clippy::missing_errors_doc)] pub fn convert(opts: ConvertOptions) -> Result<()> { fs::create_dir_all(&opts.outdir)?; - let imgs = match opts.from { - Format::Mobi | Format::Azw3 => mobi_to_imgs(opts.path)?, - Format::Pdf => pdf_to_imgs(opts.path)?, + let cbz_writer = match opts.from { + Format::Mobi | Format::Azw3 => { + let mobi = Mobi::from_path(opts.path)?; + let imgs = mobi_to_imgs(&mobi)?; + info!("found {} imgs", imgs.len()); + pack_imgs_to_cbz( + imgs, + opts.contrast, + opts.brightness, + opts.blur, + opts.autosplit, + opts.reading_order, + opts.compression_level, + )? + } + Format::Pdf => { + let pdf = PdfFileOptions::cached().open(opts.path)?; + let imgs = pdf_to_imgs(&pdf)?; + info!("found {} imgs", imgs.len()); + pack_imgs_to_cbz( + imgs, + opts.contrast, + opts.brightness, + opts.blur, + opts.autosplit, + opts.reading_order, + opts.compression_level, + )? + } }; - info!("found {} imgs", imgs.len()); - - let cbz_writer = pack_imgs_to_cbz( - imgs, - opts.contrast, - opts.brightness, - opts.blur, - opts.autosplit, - opts.reading_order, - )?; cbz_writer.write_to_path(opts.outdir.join(format!("{}.cbz", opts.name)))?; diff --git a/eco-convert/src/mobi/html5ever_parser.rs b/eco-convert/src/mobi/html5ever_parser.rs index 8871a53..113a935 100644 --- a/eco-convert/src/mobi/html5ever_parser.rs +++ b/eco-convert/src/mobi/html5ever_parser.rs @@ -1,6 +1,6 @@ -use std::{fs, io::BufReader, path::Path}; +use std::{fs, io::BufReader}; -use eco_cbz::image::Image; +use eco_cbz::image::ImageBytes; use html5ever::{parse_document, tendril::TendrilSink, ParseOpts}; use markup5ever_rcdom::{Node, NodeData, RcDom}; use mobi::Mobi; @@ -11,11 +11,10 @@ use crate::{utils::base_32, Result}; use super::MobiVersion; #[allow(clippy::missing_errors_doc)] -pub fn convert_to_imgs(path: impl AsRef) -> Result> { - let mobi = Mobi::from_path(path)?; +pub fn convert_to_imgs(mobi: &Mobi) -> Result>> { // Or is it `gen_version`? Both were equal in all the files I tested. let version = MobiVersion::try_from(mobi.metadata.mobi.format_version)?; - let dom = get_dom(&mobi)?; + let dom = get_dom(mobi)?; let imgs = mobi.image_records(); let mut all_imgs = Vec::with_capacity(imgs.len()); visit_node(version, &dom.document, |fid| { @@ -31,8 +30,8 @@ pub fn convert_to_imgs(path: impl AsRef) -> Result> { Ok(all_imgs) } -fn get_dom(m: &Mobi) -> Result { - let html = m.content_as_string_lossy(); +fn get_dom(mobi: &Mobi) -> Result { + let html = mobi.content_as_string_lossy(); fs::write("index.html", html.as_bytes())?; let mut buf = BufReader::new(html.as_bytes()); let dom = parse_document(RcDom::default(), ParseOpts::default()) diff --git a/eco-convert/src/mobi/tl_parser.rs b/eco-convert/src/mobi/tl_parser.rs index 283ed4b..eca9c6e 100644 --- a/eco-convert/src/mobi/tl_parser.rs +++ b/eco-convert/src/mobi/tl_parser.rs @@ -1,6 +1,4 @@ -use std::path::Path; - -use eco_cbz::image::Image; +use eco_cbz::image::ImageBytes; use mobi::Mobi; use tl::{HTMLTag, ParserOptions, VDom}; use tracing::{debug, error, warn}; @@ -10,8 +8,7 @@ use crate::{utils::base_32, Result}; use super::MobiVersion; #[allow(clippy::missing_errors_doc)] -pub fn convert_to_imgs(path: impl AsRef) -> Result> { - let mobi = Mobi::from_path(path)?; +pub fn convert_to_imgs(mobi: &Mobi) -> Result>> { // Or is it `gen_version`? Both were equal in all the files I tested. let version = MobiVersion::try_from(mobi.metadata.mobi.format_version)?; debug!("mobi version {version:#?}"); diff --git a/eco-convert/src/pdf.rs b/eco-convert/src/pdf.rs index c45873d..abd75f0 100644 --- a/eco-convert/src/pdf.rs +++ b/eco-convert/src/pdf.rs @@ -1,18 +1,19 @@ -use std::{io::Cursor, path::Path}; +use std::{io::Cursor, sync::Arc}; use eco_cbz::image::Image; use pdf::{ enc::StreamFilter, - file::FileOptions as PdfFileOptions, + file::{File as PdfFile, ObjectCache, StreamCache}, object::{Resolve, XObject}, }; use tracing::error; use crate::Result; -#[allow(clippy::missing_errors_doc)] -pub fn convert_to_imgs(path: impl AsRef) -> Result> { - let pdf = PdfFileOptions::cached().open(path)?; +#[allow(clippy::missing_errors_doc, clippy::type_complexity)] +pub fn convert_to_imgs( + pdf: &PdfFile, ObjectCache, StreamCache>, +) -> Result>>>> { // We may have actually less images than the count but never more, // at worse we request a slightly bigger capacity than necessary but at best we prevent any further allocations. let mut imgs = Vec::with_capacity(pdf.pages().count()); @@ -27,7 +28,7 @@ pub fn convert_to_imgs(path: impl AsRef) -> Result> { } }; if let XObject::Image(image) = &*resource { - let (image, filter) = match image.raw_image_data(&pdf) { + let (image, filter) = match image.raw_image_data(pdf) { Ok(image_data) => image_data, Err(err) => { error!("failed to get image data: {err}"); @@ -35,7 +36,7 @@ pub fn convert_to_imgs(path: impl AsRef) -> Result> { } }; if let Some(StreamFilter::DCTDecode(_)) = filter { - let img = match Image::try_from_reader(Cursor::new(&image)) { + let img = match Image::try_from_reader(Cursor::new(image)) { Ok(img) => img, Err(err) => { error!("image couldn't be read: {err}"); diff --git a/eco-merge/Cargo.toml b/eco-merge/Cargo.toml index 3c4ce1d..1dee124 100644 --- a/eco-merge/Cargo.toml +++ b/eco-merge/Cargo.toml @@ -10,3 +10,4 @@ eco-cbz.workspace = true glob.workspace = true thiserror.workspace = true tracing.workspace = true +zip.workspace = true diff --git a/eco-merge/src/lib.rs b/eco-merge/src/lib.rs index c81367b..20eedae 100644 --- a/eco-merge/src/lib.rs +++ b/eco-merge/src/lib.rs @@ -4,6 +4,7 @@ use camino::Utf8PathBuf; use eco_cbz::{CbzReader, CbzWriter}; use glob::glob; use tracing::warn; +use zip::{write::FileOptions, CompressionMethod}; pub use crate::errors::{Error, Result}; @@ -19,12 +20,22 @@ pub struct MergeOptions { /// The merged archive name pub name: String, + + /// If not provided the images are stored as is (fastest), value must be between 0-9 + pub compression_level: Option, } #[allow(clippy::missing_errors_doc, clippy::needless_pass_by_value)] pub fn merge(opts: MergeOptions) -> Result<()> { let mut merged_cbz_writer = CbzWriter::default(); + let mut file_options = FileOptions::default(); + if let Some(compression_level) = opts.compression_level { + file_options = file_options.compression_level(Some(compression_level)); + } else { + file_options = file_options.compression_method(CompressionMethod::Stored); + } + for path in glob(&opts.archives_glob)? { let mut current_cbz = CbzReader::try_from_path(path?)?; @@ -36,7 +47,7 @@ pub fn merge(opts: MergeOptions) -> Result<()> { return Ok::<(), Error>(()); } }; - merged_cbz_writer.insert(image)?; + merged_cbz_writer.insert_with_file_options(image, file_options)?; Ok::<(), Error>(()) })?; diff --git a/eco-pack/Cargo.toml b/eco-pack/Cargo.toml index 739dde6..acb957c 100644 --- a/eco-pack/Cargo.toml +++ b/eco-pack/Cargo.toml @@ -11,3 +11,4 @@ glob.workspace = true thiserror.workspace = true tracing.workspace = true tracing-subscriber.workspace = true +zip.workspace = true diff --git a/eco-pack/src/lib.rs b/eco-pack/src/lib.rs index 5a40010..134f6d1 100644 --- a/eco-pack/src/lib.rs +++ b/eco-pack/src/lib.rs @@ -1,14 +1,19 @@ #![deny(clippy::all, clippy::pedantic)] -use std::{env, fs::create_dir_all, io::Cursor}; +use std::{ + env, + fs::create_dir_all, + io::{BufRead, Cursor, Seek}, +}; use camino::{Utf8Path, Utf8PathBuf}; use eco_cbz::{ - image::{Image, ReadingOrder}, + image::{Image, ImageFile, ReadingOrder}, CbzWriter, }; use glob::glob; use tracing::{debug, error}; +use zip::{write::FileOptions, CompressionMethod}; pub use crate::errors::{Error, Result}; @@ -17,7 +22,7 @@ pub mod errors; /// ## Errors /// /// Fails when the glob is invalid, the paths are not utf-8, or the image can't be read and decoded -pub fn get_images_from_glob(glob_expr: impl AsRef) -> Result> { +pub fn get_images_from_glob(glob_expr: impl AsRef) -> Result> { let paths = glob(glob_expr.as_ref())?; let mut imgs = Vec::new(); @@ -34,15 +39,24 @@ pub fn get_images_from_glob(glob_expr: impl AsRef) -> Result> { } #[allow(clippy::missing_errors_doc, clippy::missing_panics_doc)] -pub fn pack_imgs_to_cbz( - imgs: Vec, +pub fn pack_imgs_to_cbz( + imgs: Vec>, contrast: Option, brightness: Option, blur: Option, autosplit: bool, reading_order: ReadingOrder, + compression_level: Option, ) -> Result>>> { let mut cbz_writer = CbzWriter::default(); + + let mut file_options = FileOptions::default(); + if let Some(compression_level) = compression_level { + file_options = file_options.compression_level(Some(compression_level)); + } else { + file_options = file_options.compression_method(CompressionMethod::Stored); + } + for mut img in imgs { if let Some(contrast) = contrast { img = img.set_contrast(contrast); @@ -53,14 +67,13 @@ pub fn pack_imgs_to_cbz( if let Some(blur) = blur { img = img.set_blur(blur); } - - if img.is_landscape() && autosplit { + if autosplit && img.is_landscape() { debug!("splitting landscape file"); let (img_left, img_right) = img.autosplit(reading_order); - cbz_writer.insert(img_left)?; - cbz_writer.insert(img_right)?; + cbz_writer.insert_with_file_options(img_left, file_options)?; + cbz_writer.insert_with_file_options(img_right, file_options)?; } else { - cbz_writer.insert(img)?; + cbz_writer.insert_with_file_options(img, file_options)?; } } @@ -92,6 +105,9 @@ pub struct PackOptions { /// Reading order pub reading_order: ReadingOrder, + + /// If not provided the images are stored as is (fastest), value must be between 0-9 + pub compression_level: Option, } #[allow(clippy::missing_errors_doc)] @@ -114,6 +130,7 @@ pub fn pack(opts: PackOptions) -> Result<()> { opts.blur, opts.autosplit, opts.reading_order, + opts.compression_level, )?; cbz_writer.write_to_path(outdir.join(format!("{}.cbz", opts.name)))?; diff --git a/eco-view/src/lib.rs b/eco-view/src/lib.rs index 7995bf1..ba8414c 100644 --- a/eco-view/src/lib.rs +++ b/eco-view/src/lib.rs @@ -117,7 +117,7 @@ pub struct AppProps { page_loaded_receiver: Cell>>, } -#[allow(clippy::ignored_unit_patterns, clippy::too_many_lines)] +#[allow(clippy::too_many_lines)] fn App(cx: Scope) -> Element { let page_loaded_receiver = cx.props.page_loaded_receiver.replace(None); // Forces reactivity on page loaded diff --git a/eco-view/src/measure.rs b/eco-view/src/measure.rs index d6de072..74c63e8 100644 --- a/eco-view/src/measure.rs +++ b/eco-view/src/measure.rs @@ -10,7 +10,6 @@ pub enum Precision { S, } -#[allow(unused)] pub struct Measure { label: String, start: std::time::Duration, @@ -28,7 +27,6 @@ impl Clone for Measure { } impl Measure { - #[allow(unused)] #[must_use] /// ## Panics pub fn new(label: &str, precision: Precision) -> Self { diff --git a/eco/src/main.rs b/eco/src/main.rs index 077c1b4..7cf7337 100644 --- a/eco/src/main.rs +++ b/eco/src/main.rs @@ -62,6 +62,10 @@ enum Command { /// Reading order #[clap(long, default_value_t = ReadingOrder::Rtl)] reading_order: ReadingOrder, + + /// If not provided the images are stored as is (fastest), value must be between 0-9 + #[clap(long)] + compression_level: Option, }, Merge { /// A glob that matches all the archive to merge @@ -75,6 +79,10 @@ enum Command { /// The merged archive name #[clap(short, long)] name: String, + + /// If not provided the images are stored as is (fastest), value must be between 0-9 + #[clap(long)] + compression_level: Option, }, Pack { /// A glob that matches all the files to pack @@ -107,6 +115,10 @@ enum Command { /// Reading order #[clap(long, default_value_t = ReadingOrder::Rtl)] reading_order: ReadingOrder, + + /// If not provided the images are stored as is (fastest), value must be between 0-9 + #[clap(long)] + compression_level: Option, }, View { /// The path to the e-book file to view @@ -133,6 +145,7 @@ fn main() -> Result<()> { blur, autosplit, reading_order, + compression_level, } => eco_convert::convert(eco_convert::ConvertOptions { path, from: from.into(), @@ -143,15 +156,18 @@ fn main() -> Result<()> { blur, autosplit, reading_order: reading_order.into(), + compression_level, })?, Command::Merge { archives_glob, outdir, name, + compression_level, } => eco_merge::merge(eco_merge::MergeOptions { archives_glob, outdir, name, + compression_level, })?, Command::Pack { files_descriptor, @@ -162,6 +178,7 @@ fn main() -> Result<()> { blur, autosplit, reading_order, + compression_level, } => eco_pack::pack(eco_pack::PackOptions { files_descriptor, outdir, @@ -171,6 +188,7 @@ fn main() -> Result<()> { blur, autosplit, reading_order: reading_order.into(), + compression_level, })?, Command::View { path, type_ } => eco_view::view(eco_view::ViewOptions { path,