diff --git a/Cargo.lock b/Cargo.lock index 4757729..459879b 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", @@ -1733,7 +1809,7 @@ checksum = "832a761f35ab3e6664babfbdc6cef35a4860e816ec3916dcfd0882954e98a8a8" dependencies = [ "bit_field", "flume", - "half", + "half 2.2.1", "lebe", "miniz_oxide", "rayon-core", @@ -2400,6 +2476,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 +2680,7 @@ dependencies = [ "bitflags 1.3.2", "bytemuck", "glam", - "half", + "half 2.2.1", "iced_core", "image", "kamadak-exif", @@ -2847,6 +2929,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 +3720,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 +4112,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 +5295,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 +6220,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 +6259,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 +6292,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 +6310,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 +6328,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 +6346,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 +6364,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 +6382,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 +6400,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..69ed245 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,20 @@ where /// ## Errors /// /// Same behavior as `insert_with_extension_and_file_options` - pub fn insert(&mut self, image: Image) -> Result<()> { - let extension = image - .format() - .and_then(|f| f.extensions_str().first().copied()) - .unwrap_or("png"); + #[allow(clippy::missing_panics_doc)] + pub fn insert(&mut self, image: Image) -> Result<()> { + let extension = image.format().extensions_str().first().unwrap(); 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_extension( + &mut self, + image: Image, + extension: &str, + ) -> Result<()> { self.insert_with_extension_and_file_options(image, extension, FileOptions::default()) } @@ -285,9 +287,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<()> { diff --git a/eco-cbz/src/errors.rs b/eco-cbz/src/errors.rs index ff85b05..38bb583 100644 --- a/eco-cbz/src/errors.rs +++ b/eco-cbz/src/errors.rs @@ -35,6 +35,9 @@ pub enum Error { #[error("image error: {0}")] Image(#[from] image::ImageError), + #[error("unknown image format error")] + UnknownImageFormat, + #[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..0a1ddef 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; @@ -56,20 +58,34 @@ pub struct ConvertOptions { #[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, + )? + } + 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, + )? + } }; - 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-pack/src/lib.rs b/eco-pack/src/lib.rs index 5a40010..fc2a0b9 100644 --- a/eco-pack/src/lib.rs +++ b/eco-pack/src/lib.rs @@ -1,10 +1,14 @@ #![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; @@ -17,7 +21,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,8 +38,8 @@ 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, @@ -53,8 +57,7 @@ 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)?;