diff --git a/.gitignore b/.gitignore index 249bc38..9a588e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /target *.lock bmp/out.* +fonts +test.* \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 5f867f1..5c16566 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,24 +1,9 @@ [workspace] resolver = "2" -members = ["bent", "bent-funny-zone", "bingus", "bong"] +members = ["bingus", "bong"] package.edition = "2021" package.license = "Unlicense" package.description = "databending made easy" package.authors = ["Breval Ferrari "] package.repository = "https://codeberg.org/p6nj/bent" package.keywords = ["databending", "data-bending", "bending", "bend", "art"] - -[workspace.dependencies] -fundsp = "0.18.2" -image = "0.25.1" -anyhow = "1.0.86" -bingus = { version = "0.1.0", path = "bingus" } -num-traits = "0.2.19" -dasp_sample = "0.11.0" -rayon = "1.10.0" -dirs = "5.0.1" -rfd = "0.14.1" -derive-new = "0.6.0" -infer = "0.16.0" -indicatif = { version = "0.17.8", features = ["rayon"] } -getset = "0.1.2" diff --git a/README.md b/README.md index 0d513fb..fb9922e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # BENT + > Get bent! Databending made easy. -This crate aims to provide a simple GUI to bend any file to another format without dealing with headers. -Codecs and processors would come as features and be chosen in the settings to recompile the binary. \ No newline at end of file +This project aims to provide a simple GUI to bend any file to another format without dealing with headers. +Codecs and processors would come as features and be chosen in the settings to recompile the binary. + +There are three crates in this project : + +- bingus, the core library +- bong, the CLI binary +- bent, the GUI diff --git a/bent-funny-zone/Cargo.toml b/bent-funny-zone/Cargo.toml deleted file mode 100644 index 1d7e5a2..0000000 --- a/bent-funny-zone/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "bent-funny-zone" -version = "0.1.0" -edition.workspace = true -license.workspace = true -description.workspace = true -authors.workspace = true -repository.workspace = true -keywords.workspace = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -image = { workspace = true } -fundsp = { workspace = true } -anyhow = { workspace = true } -bingus = { workspace = true } -dasp_sample = { workspace = true } -rayon = { workspace = true } -dirs = { workspace = true } -rfd = { workspace = true } -derive-new = { workspace = true } -infer = { workspace = true } -indicatif = { workspace = true } diff --git a/bent-funny-zone/src/main.rs b/bent-funny-zone/src/main.rs deleted file mode 100644 index 22e1e15..0000000 --- a/bent-funny-zone/src/main.rs +++ /dev/null @@ -1,131 +0,0 @@ -use std::path::PathBuf; - -use anyhow::{Context, Result}; -use bingus::rawdata::RawData; -use dasp_sample::{Sample, U24}; -use derive_new::new; -use dirs::{download_dir, home_dir, picture_dir}; -use fundsp::math::uparc; -use image::{io::Reader as ImageReader, ImageFormat, RgbImage}; -use indicatif::{ParallelProgressIterator, ProgressStyle}; -use rayon::{iter::ParallelIterator, slice::ParallelSlice}; -use rfd::FileDialog; - -struct Files { - input: PathBuf, - output: PathBuf, -} - -#[derive(new)] -struct DataPath { - files: Files, -} - -impl Files { - fn try_new(input: PathBuf, output: PathBuf) -> Result { - Ok(Files { - input: Self::verify_image_file(input, "input")?, - output: Self::verify_image_file(output, "output")?, - }) - } - fn verify_image_file(path: PathBuf, label: &'static str) -> Result { - ImageFormat::from_path(&path).with_context(|| { - format!("your {label} image must have a supported extension from this list:") - + &ImageFormat::all() - .map(|format| { - format!( - "\n\t{:?} ({})", - format, - format - .extensions_str() - .iter() - .fold(String::new(), |acc, x| acc + &format!(".{x}, ")) - .strip_suffix(", ") - .unwrap() - ) - }) - .reduce(|acc, x| acc + &x) - .unwrap() - })?; - Ok(path) - } - fn prompt() -> Result { - Files::try_new( - FileDialog::new() - .set_title("Input image") - .set_directory(working_dir()) - .add_filter( - "images", - &ImageFormat::all() - .map(ImageFormat::extensions_str) - .flatten() - .collect::>(), - ) - .pick_file() - .context("no input image selected")?, - FileDialog::new() - .set_title("Output image") - .set_directory(working_dir()) - .save_file() - .context("no output filename provided")?, - ) - } -} - -fn main() -> Result<()> { - let dp = DataPath::new(Files::prompt()?); - let img = RawData::::new( - ImageReader::open(dp.files.input)? - .decode() - .context("can't use this picture")? - .into_rgb8(), - ); - let processed = RawData::::new( - RgbImage::from_raw( - img.width(), - img.height(), - img.par_chunks_exact(3) - .progress_count((img.width() * img.height()).into()) - .with_style(ProgressStyle::with_template( - "[{eta}] {bar:40.green/red} {pos}/{len} pixels", - )?) - .map(|px: &[u8]| (px[2] as i32) | ((px[1] as i32) << 8) | ((px[0] as i32) << 16)) - .map(U24::new_unchecked) - .map(|x| uparc(x.to_sample())) - .flat_map(|sample: f32| { - let rgb: U24 = sample.to_sample(); - let rgo = ((rgb) >> 8.into()) << 8.into(); - let roo = ((rgo) >> 16.into()) << 16.into(); - [ - ((roo) >> 16.into()).inner() as u8, - ((rgo - roo) >> 8.into()).inner() as u8, - (rgb - rgo).inner() as u8, - ] - }) - .collect(), - ) - .unwrap(), - ); - processed.save(dp.files.output)?; - - // let bytes: Vec = (1u8..7).into_iter().collect(); - // assert_eq!( - // bytes, - // bytes - // .chunks_exact(3) - // .map(|px: &[u8]| f64::from_ne_bytes(array::from_fn(|i| px[i % px.len()]))) - // .flat_map(|px: f64| -> [u8; 3] { - // let px = px.to_ne_bytes(); - // array::from_fn(move |i| px[i]) - // }) - // .collect::>() - // ); - Ok(()) -} - -fn working_dir() -> PathBuf { - picture_dir() - .or(download_dir()) - .or(home_dir()) - .unwrap_or("/".into()) -} diff --git a/bent/Cargo.toml b/bent/Cargo.toml index e00aed4..827dc82 100644 --- a/bent/Cargo.toml +++ b/bent/Cargo.toml @@ -15,6 +15,10 @@ crate-type = ["cdylib"] [dependencies] wasm-bindgen = "0.2" +image = { workspace = true } +dasp_sample = { workspace = true } +bingus = { workspace = true } +fundsp = { workspace = true } [profile.release] lto = true diff --git a/bent/index.html b/bent/index.html index 6a253de..5cf5e5c 100644 --- a/bent/index.html +++ b/bent/index.html @@ -8,12 +8,23 @@ + @@ -25,10 +36,10 @@

- - + +

- +
diff --git a/bent/src/lib.rs b/bent/src/lib.rs index d26acc3..49c072c 100644 --- a/bent/src/lib.rs +++ b/bent/src/lib.rs @@ -1,4 +1,8 @@ -use wasm_bindgen::prelude::*; +use bingus::rawdata::RawData; +use dasp_sample::U24; +use fundsp::math::uparc; +use image::{ImageReader, RgbImage}; +use wasm_bindgen::{convert::RefFromWasmAbi, prelude::*}; #[allow(unused_macros)] #[macro_use] pub mod macros; @@ -8,3 +12,49 @@ pub mod js; pub fn greet(name: &str) { println!("Hello, {name}!"); } + +#[wasm_bindgen(getter_with_clone)] +pub struct ImageFile { + pub name: String, + pub data: Vec, +} + +#[wasm_bindgen] +pub fn process_files(files: Vec) { + files.into_iter().for_each(|file| { + let img = RawData::::new( + ImageReader::open(file.name) + .unwrap() + .decode() + .unwrap() + .into_rgb8(), + ); + img.par_pixels_mut().for_each(|px| { + let sample = uparc( + U24::new_unchecked((px[2] as i32) | ((px[1] as i32) << 8) | ((px[0] as i32) << 16)) + .to_sample(), + ); + }); + let processed = RawData::::new( + RgbImage::from_raw( + img.width(), + img.height(), + img.par_chunks_exact(3) + .map(|px: &[u8]| { + (px[2] as i32) | ((px[1] as i32) << 8) | ((px[0] as i32) << 16) + }) + .map(U24::new_unchecked) + .map(|x| uparc(x.to_sample())) + .flat_map(|sample: f32| { + let rgb: U24 = sample.to_sample(); + let rgo = ((rgb) >> 8.into()) << 8.into(); + let roo = ((rgo) >> 16.into()) << 16.into(); + vec![roo, rgo, rgb] + }) + .collect::>(), + ) + .unwrap(), + ); + let _ = processed; + }); +} diff --git a/bingus/Cargo.toml b/bingus/Cargo.toml index 5f28930..824c2e8 100644 --- a/bingus/Cargo.toml +++ b/bingus/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bingus" -version = "0.1.2" +version = "0.6.0" edition.workspace = true license.workspace = true description.workspace = true @@ -10,4 +10,54 @@ keywords.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -derive-new = { workspace = true } +image = { version = "0.25", optional = true } +num = { version = "0", optional = true } +rayon = { version = "1", optional = true } +infer = "0" +thiserror = "2" +derive-new = "0" +derive_wrapper = "0.1" +symphonia = { version = "0.5", features = ["all"], optional = true } +printpdf = { version = "0.8.2", features = [ + "bmp", + "dds", + "gif", + "hdr", + "ico", + "jpeg", + "png", + "pnm", + "tga", + "tiff", + "js-sys", + "webp", +], optional = true } +shiva = { version = "1.4.9", optional = true } +anyhow = { version = "1.0", optional = true } +font-kit = { version = "0.14.2", features = [ + "loader-freetype-default", +], optional = true } +cfg-if = "1.0.0" + +[dev-dependencies] +project-root = "0" +pretty_assertions = "1" + +[features] +default = ["all-formats"] +all-formats = ["binary", "documents", "fonts", "pictures", "music", "text"] + +binary = [] +documents = ["shiva", "printpdf"] +fonts = ["font-kit"] +pictures = ["image"] +music = ["symphonia", "dep:num"] +text = [] + +shiva = ["dep:shiva", "dep:anyhow"] +printpdf = ["dep:printpdf"] +font-kit = ["dep:font-kit"] +image = ["dep:image", "dep:num"] +symphonia = ["dep:symphonia"] + +rayon = ["image?/rayon", "printpdf?/rayon", "dep:rayon"] diff --git a/bingus/src/bin.rs b/bingus/src/bin.rs new file mode 100644 index 0000000..1e42f77 --- /dev/null +++ b/bingus/src/bin.rs @@ -0,0 +1,2 @@ +#[path = "bin_/bytes.rs"] +mod bytes; diff --git a/bingus/src/bin_/bytes.rs b/bingus/src/bin_/bytes.rs new file mode 100644 index 0000000..6b059f3 --- /dev/null +++ b/bingus/src/bin_/bytes.rs @@ -0,0 +1,40 @@ +use std::{borrow::Cow, convert::Infallible}; + +use cfg_if::cfg_if; +#[cfg(feature = "rayon")] +use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; + +use crate::{Bendable, Bytes, IntoDataBytes, TryFromDataBytes}; + +impl IntoDataBytes for Bytes { + fn into_data_bytes(self) -> Bytes { + self + } +} + +impl TryFromDataBytes for Bytes { + type Error = Infallible; + type Format = (); + fn try_from_data_bytes( + bytes: Bytes, + _: Self::Format, + _: crate::Crop, + ) -> Result { + Ok(bytes) + } +} + +impl Bendable for Bytes { + type Unit = u8; + fn map) -> Self::Unit + Sync>(mut self, f: F) -> Self { + cfg_if! { + if #[cfg(feature = "rayon")] { + let iter = self.par_iter_mut(); + } else { + let iter = self.iter_mut(); + } + } + iter.for_each(|e| *e = f(Cow::Borrowed(e))); + self + } +} diff --git a/bingus/src/doc.rs b/bingus/src/doc.rs new file mode 100644 index 0000000..57af55a --- /dev/null +++ b/bingus/src/doc.rs @@ -0,0 +1,6 @@ +pub use printpdf; +mod pdf; +#[cfg(feature = "shiva")] +mod shiva; +#[cfg(feature = "shiva")] +pub use shiva::*; diff --git a/bingus/src/doc/pdf.rs b/bingus/src/doc/pdf.rs new file mode 100644 index 0000000..43a0277 --- /dev/null +++ b/bingus/src/doc/pdf.rs @@ -0,0 +1,122 @@ +use std::borrow::Cow; + +use cfg_if::cfg_if; +use printpdf::{ + Op, PdfDocument, PdfPage, PdfParseErrorSeverity, PdfParseOptions, PdfSaveOptions, PdfWarnMsg, +}; +#[cfg(feature = "rayon")] +use rayon::iter::{IntoParallelIterator, ParallelIterator}; + +use crate::{Bendable, IntoDataBytes, TryFromDataBytes}; + +fn clean_up_warnings(warnings: &mut Vec) { + warnings.retain(|warning| { + warning.severity == PdfParseErrorSeverity::Warning + || warning.severity == PdfParseErrorSeverity::Error + }); +} + +impl TryFromDataBytes for PdfDocument { + type Error = String; + type Format = (); + + fn try_from_data_bytes( + bytes: crate::Bytes, + _format: Self::Format, + _crop: crate::Crop, + ) -> Result { + let mut warnings = Vec::new(); + let result = PdfDocument::parse( + &bytes, + &PdfParseOptions { + fail_on_error: false, + }, + &mut warnings, + ); + clean_up_warnings(&mut warnings); + for warning in warnings { + println!("Warning: {:#?}", warning); + } + result + } +} + +impl IntoDataBytes for PdfDocument { + fn into_data_bytes(self) -> crate::Bytes { + let mut warnings = Vec::new(); + let result = self.save( + &PdfSaveOptions { + image_optimization: None, + ..Default::default() + }, + &mut warnings, + ); + clean_up_warnings(&mut warnings); + for warning in warnings { + println!("Warning: {:#?}", warning); + } + result + } +} + +impl Bendable for PdfDocument { + type Unit = Op; + + fn map) -> Self::Unit + Sync>(self, f: F) -> Self { + let PdfDocument { + metadata, + resources, + bookmarks, + pages, + } = self; + PdfDocument { + pages: pages + .into_iter() + .map(|page| PdfPage { + ops: { + cfg_if! { + if #[cfg(feature = "rayon")] { + page.ops.into_par_iter() + } else { + page.ops.into_iter() + } + } + } + .map(|op| f(Cow::Owned(op))) + .collect::>(), + ..page + }) + .collect(), + metadata, + resources, + bookmarks, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn map_integrity() { + let mut warnings = Vec::new(); + let original = PdfDocument::parse( + include_bytes!("../../../testing material/doc/pdf/basic-text.pdf"), + &PdfParseOptions { + fail_on_error: true, + }, + &mut warnings, + ) + .unwrap(); + clean_up_warnings(&mut warnings); + for warning in warnings { + println!("Warning: {:#?}", warning); + } + assert_eq!( + format!("{:?}", original), + format!("{:?}", original.clone().map(|u| u.into_owned())) + ) + } +} diff --git a/bingus/src/doc/shiva.rs b/bingus/src/doc/shiva.rs new file mode 100644 index 0000000..d8b08c7 --- /dev/null +++ b/bingus/src/doc/shiva.rs @@ -0,0 +1,62 @@ +use std::borrow::Cow; + +use derive_new::new; +pub use shiva::core::DocumentType; +use shiva::core::{bytes::Bytes, Document, Element}; + +use crate::{Bendable, IntoDataBytes, TryFromDataBytes}; + +#[derive(new)] +pub struct ShivaDocument { + document: Document, + output_format: DocumentType, +} + +#[derive(new)] +pub struct ShivaFormat { + input_format: DocumentType, + output_format: DocumentType, +} + +impl TryFromDataBytes for ShivaDocument { + type Error = anyhow::Error; + type Format = ShivaFormat; + + fn try_from_data_bytes( + bytes: crate::Bytes, + format: Self::Format, + _crop: crate::Crop, + ) -> Result { + Ok(ShivaDocument::new( + Document::parse(&Bytes::from(bytes), format.input_format)?, + format.output_format, + )) + } +} + +impl IntoDataBytes for ShivaDocument { + fn into_data_bytes(self) -> crate::Bytes { + self.document + .generate(self.output_format) + .expect("can't crash here! so close!") + .to_vec() + } +} + +impl Bendable for ShivaDocument { + type Unit = Element; + + fn map) -> Self::Unit + Sync>(self, f: F) -> Self { + ShivaDocument::new( + Document::new( + self.document + .get_all_elements() + .into_iter() + .map(Cow::Borrowed) + .map(f) + .collect(), + ), + self.output_format, + ) + } +} diff --git a/bingus/src/fnt.rs b/bingus/src/fnt.rs new file mode 100644 index 0000000..800fe2e --- /dev/null +++ b/bingus/src/fnt.rs @@ -0,0 +1,2 @@ +mod fontkit; +pub use fontkit::Font; diff --git a/bingus/src/fnt/fontkit.rs b/bingus/src/fnt/fontkit.rs new file mode 100644 index 0000000..4b8721c --- /dev/null +++ b/bingus/src/fnt/fontkit.rs @@ -0,0 +1,49 @@ +use cfg_if::cfg_if; +use font_kit::error::FontLoadingError; +pub use font_kit::font::Font; +use std::{borrow::Cow, sync::Arc}; + +use crate::{Bendable, IntoDataBytes, TryFromDataBytes}; + +impl TryFromDataBytes for Font { + type Error = FontLoadingError; + type Format = (); + + fn try_from_data_bytes( + bytes: crate::Bytes, + _format: Self::Format, + _crop: crate::Crop, + ) -> Result { + Self::from_bytes(Arc::new(bytes), 0) + } +} + +impl IntoDataBytes for Font { + fn into_data_bytes(self) -> crate::Bytes { + self.copy_font_data() + .map(|v| v.as_ref().clone()) + .unwrap_or_default() + } +} + +impl Bendable for Font { + type Unit = u8; + + fn map) -> Self::Unit + Sync>(self, f: F) -> Self { + Self::try_from_data_bytes( + { + let bytes = self.into_data_bytes(); + cfg_if! { + if #[cfg(feature = "binary")] { + bytes.map(f) + } else { + bytes.into_iter().map(|e| f(Cow::Owned(e))).collect() + } + } + }, + (), + Default::default(), + ) + .expect("coudn't get font back from bytes after map") + } +} diff --git a/bingus/src/img.rs b/bingus/src/img.rs new file mode 100644 index 0000000..bb13e63 --- /dev/null +++ b/bingus/src/img.rs @@ -0,0 +1,3 @@ +mod image; +pub use image::ImageBuffer as Image; +pub use image::*; diff --git a/bingus/src/img/image.rs b/bingus/src/img/image.rs new file mode 100644 index 0000000..f8edff5 --- /dev/null +++ b/bingus/src/img/image.rs @@ -0,0 +1,383 @@ +use std::{ + borrow::Cow, + convert::Infallible, + iter::once, + ops::{Add, Deref, DerefMut, Div, Mul, Sub}, +}; + +use cfg_if::cfg_if; +pub use image::*; +use num::{ + traits::{FromBytes, ToBytes}, + Zero, +}; +#[cfg(feature = "rayon")] +use rayon::iter::ParallelIterator; +use thiserror::Error; + +use crate::{Bendable, FromDataBytes, IntoDataBytes, TryFromDataBytes}; + +impl IntoDataBytes for ImageBuffer> +where + Vec: Deref, + P::Subpixel: ToBytes, +{ + fn into_data_bytes(self) -> crate::Bytes { + self.iter() + .flat_map(|subpixel| subpixel.to_ne_bytes().as_ref().to_vec()) + .collect() + } +} + +#[cfg_attr(debug_assertions, derive(Debug))] +pub struct Dimensions { + pub width: u32, + pub height: u32, +} + +impl Div for Dimensions { + type Output = Dimensions; + fn div(self, rhs: Dimensions) -> Self::Output { + Dimensions { + width: self.width / rhs.width, + height: self.height / rhs.height, + } + } +} + +impl Div for Dimensions { + type Output = Dimensions; + fn div(self, rhs: u32) -> Self::Output { + Dimensions { + width: self.width / rhs, + height: self.height / rhs, + } + } +} + +impl Mul for Dimensions { + type Output = Dimensions; + fn mul(self, rhs: Dimensions) -> Self::Output { + Dimensions { + width: self.width * rhs.width, + height: self.height * rhs.height, + } + } +} + +impl Mul for Dimensions { + type Output = Dimensions; + fn mul(self, rhs: u32) -> Self::Output { + Dimensions { + width: self.width * rhs, + height: self.height * rhs, + } + } +} + +impl Add for Dimensions { + type Output = Dimensions; + fn add(self, rhs: Dimensions) -> Self::Output { + Dimensions { + width: self.width + rhs.width, + height: self.height + rhs.height, + } + } +} + +impl Add for Dimensions { + type Output = Dimensions; + fn add(self, rhs: u32) -> Self::Output { + Dimensions { + width: self.width + rhs, + height: self.height + rhs, + } + } +} + +impl Sub for Dimensions { + type Output = Dimensions; + fn sub(self, rhs: Dimensions) -> Self::Output { + Dimensions { + width: self.width - rhs.width, + height: self.height - rhs.height, + } + } +} + +impl Sub for Dimensions { + type Output = Dimensions; + fn sub(self, rhs: u32) -> Self::Output { + Dimensions { + width: self.width - rhs, + height: self.height - rhs, + } + } +} + +impl TryFromDataBytes for ImageBuffer> +where + Vec: Deref, + P::Subpixel: ToBytes + FromBytes + Zero, + ::Bytes: for<'a> TryFrom<&'a [u8]>, +{ + type Error = Infallible; + type Format = Dimensions; + fn try_from_data_bytes( + bytes: crate::Bytes, + format: Self::Format, + crop: crate::Crop, + ) -> Result { + ImageBuffer::from_raw( + format.width, + format.height, + match crop { + // TODO: separate outer crop from inner crop + crate::Crop::End => bytes + .chunks_exact(P::Subpixel::zero().to_ne_bytes().as_ref().len()) + .map(|p| { + P::Subpixel::from_ne_bytes( + &match ::Bytes::try_from(p) { + Ok(v) => v, + Err(_) => unreachable!("you messed up chunk size!"), + }, + ) + }) + .chain(once(P::Subpixel::zero()).cycle()) + .take( + format.width as usize * format.height as usize * P::CHANNEL_COUNT as usize, + ) + .collect::>(), + crate::Crop::Start => bytes + .rchunks_exact(P::Subpixel::zero().to_ne_bytes().as_ref().len()) + .map(|p| { + P::Subpixel::from_ne_bytes( + &match ::Bytes::try_from(p) { + Ok(v) => v, + Err(_) => unreachable!("you messed up chunk size!"), + }, + ) + }) + .chain(once(P::Subpixel::zero()).cycle()) + .take( + format.width as usize * format.height as usize * P::CHANNEL_COUNT as usize, + ) + .collect::>(), + }, + ) + .ok_or_else(|| unreachable!()) + } +} + +impl Bendable for ImageBuffer> +where + Vec: Deref + DerefMut, + P::Subpixel: ToBytes + FromBytes + Send + Sync, + ::Bytes: for<'a> TryFrom<&'a [u8]>, + P: Send + Sync, +{ + type Unit = P; + fn map) -> Self::Unit + Sync>(mut self, f: F) -> Self { + cfg_if! { + if #[cfg(feature = "rayon")] { + let iter = self.par_pixels_mut(); + } else { + let iter = self.pixels_mut(); + } + } + iter.for_each(|p| *p = f(Cow::Borrowed(p))); + self + } +} + +impl IntoDataBytes for DynamicImage { + fn into_data_bytes(self) -> crate::Bytes { + self.into_bytes() + } +} + +#[derive(Debug, Error)] +#[error("this color type is not supported yet... sorry")] +pub struct UnsupportedColorType; + +impl TryFromDataBytes for DynamicImage { + type Format = (Dimensions, ColorType); + type Error = UnsupportedColorType; + fn try_from_data_bytes( + bytes: crate::Bytes, + format: Self::Format, + crop: crate::Crop, + ) -> Result { + match format.1 { + ColorType::L8 => Ok(DynamicImage::ImageLuma8(ImageBuffer::from_data_bytes( + bytes, format.0, crop, + ))), + ColorType::La8 => Ok(DynamicImage::ImageLumaA8(ImageBuffer::from_data_bytes( + bytes, format.0, crop, + ))), + ColorType::Rgb8 => Ok(DynamicImage::ImageRgb8(ImageBuffer::from_data_bytes( + bytes, format.0, crop, + ))), + ColorType::Rgba8 => Ok(DynamicImage::ImageRgba8(ImageBuffer::from_data_bytes( + bytes, format.0, crop, + ))), + ColorType::L16 => Ok(DynamicImage::ImageLuma16(ImageBuffer::from_data_bytes( + bytes, format.0, crop, + ))), + ColorType::La16 => Ok(DynamicImage::ImageLumaA16(ImageBuffer::from_data_bytes( + bytes, format.0, crop, + ))), + ColorType::Rgb16 => Ok(DynamicImage::ImageRgb16(ImageBuffer::from_data_bytes( + bytes, format.0, crop, + ))), + ColorType::Rgba16 => Ok(DynamicImage::ImageRgba16(ImageBuffer::from_data_bytes( + bytes, format.0, crop, + ))), + ColorType::Rgb32F => Ok(DynamicImage::ImageRgb32F(ImageBuffer::from_data_bytes( + bytes, format.0, crop, + ))), + ColorType::Rgba32F => Ok(DynamicImage::ImageRgba32F(ImageBuffer::from_data_bytes( + bytes, format.0, crop, + ))), + _ => Err(UnsupportedColorType), + } + } +} + +#[cfg(test)] +mod tests { + #[cfg(test)] + mod ser_de { + use super::super::{ + Dimensions, ImageBuffer, IntoDataBytes, Rgb, Rgb32FImage, RgbImage, RgbaImage, + TryFromDataBytes, + }; + + #[test] + fn empty() { + let image = RgbImage::new(0, 0); + assert_eq!( + Ok(image.clone()), + RgbImage::try_from_data_bytes( + image.into_data_bytes(), + Dimensions { + width: 0, + height: 0 + }, + Default::default() + ) + ) + } + + #[test] + fn simple() { + let image = RgbImage::from_raw(3, 1, vec![1, 2, 3, 4, 5, 6, 7, 8, 9]).unwrap(); + assert_eq!( + Ok(image.clone()), + RgbImage::try_from_data_bytes( + image.into_data_bytes(), + Dimensions { + width: 3, + height: 1 + }, + Default::default() + ) + ) + } + + #[test] + fn rgba() { + let image = + RgbaImage::from_raw(3, 1, vec![1, 2, 3, 0, 4, 5, 6, 1, 7, 8, 9, 2]).unwrap(); + assert_eq!( + Ok(image.clone()), + RgbaImage::try_from_data_bytes( + image.into_data_bytes(), + Dimensions { + width: 3, + height: 1 + }, + Default::default() + ) + ) + } + + #[test] + fn rgb_u16() { + let image = ImageBuffer::, Vec>::from_raw( + 3, + 1, + vec![1, 2, 3, 254, 255, 256, 307, 308, 309], + ) + .unwrap(); + assert_eq!( + Ok(image.clone()), + ImageBuffer::, Vec>::try_from_data_bytes( + image.into_data_bytes(), + Dimensions { + width: 3, + height: 1 + }, + Default::default() + ) + ) + } + + #[test] + fn rgb_signed() { + let image = ImageBuffer::, Vec>::from_raw( + 3, + 1, + vec![1, 2, 3, 254, 255, 256, -307, 308, 309], + ) + .unwrap(); + assert_eq!( + Ok(image.clone()), + ImageBuffer::, Vec>::try_from_data_bytes( + image.into_data_bytes(), + Dimensions { + width: 3, + height: 1 + }, + Default::default() + ) + ) + } + + #[test] + fn rgb_f32() { + let image = Rgb32FImage::from_raw( + 3, + 1, + vec![1.0, 2.0, 3.0, 254.0, 255.0, 256.1, 307.0, 308.0, 309.0], + ) + .unwrap(); + assert_eq!( + Ok(image.clone()), + Rgb32FImage::try_from_data_bytes( + image.into_data_bytes(), + Dimensions { + width: 3, + height: 1 + }, + Default::default() + ) + ) + } + } + + #[cfg(test)] + mod effects { + use crate::Bendable; + + use super::super::{Pixel, RgbImage}; + + #[test] + fn fill_with_funny_number() { + let image = RgbImage::new(8, 16); + let new_image = image.clone().map(|p| p.map(|_channel| 42u8)); + assert_ne!(image.clone(), new_image); + assert_eq!(42, new_image.get_pixel(1, 2).channels()[1]) + } + } +} diff --git a/bingus/src/lib.rs b/bingus/src/lib.rs index e295326..30c7bfc 100644 --- a/bingus/src/lib.rs +++ b/bingus/src/lib.rs @@ -1,2 +1,280 @@ -#![forbid(unused_crate_dependencies)] -pub mod rawdata; +#[cfg(feature = "binary")] +pub mod bin; +#[cfg(feature = "documents")] +pub mod doc; +#[cfg(feature = "fonts")] +pub mod fnt; +#[cfg(feature = "pictures")] +pub mod img; +#[cfg(feature = "music")] +pub mod snd; +#[cfg(feature = "text")] +pub mod txt; + +pub type Bytes = Vec; + +pub mod dynamic { + #[cfg(feature = "text")] + use std::string::FromUtf8Error; + use std::{ + borrow::Cow, + fs::File, + io::{self, Cursor, Read, Write}, + path::Path, + }; + + #[cfg(feature = "documents")] + use super::doc::ShivaDocument; + #[cfg(feature = "fonts")] + use super::fnt::Font as FontKitFont; + #[cfg(feature = "pictures")] + use super::img::{self, DynamicImage}; + #[cfg(feature = "music")] + use super::snd::{self, Audio}; + #[cfg(feature = "text")] + use super::txt::Text; + use super::{Bendable, Bytes, IntoDataBytes, TryFromDataBytes}; + #[cfg(not(feature = "text"))] + use std::marker::PhantomData; + + use cfg_if::cfg_if; + #[cfg(feature = "fonts")] + use font_kit::error::FontLoadingError; + pub use infer::*; + #[cfg(feature = "documents")] + use printpdf::PdfDocument; + #[cfg(feature = "documents")] + use shiva::core::{bytes, Document, DocumentType}; + use thiserror::Error; + + pub enum DynamicBendable<'a> { + #[cfg(feature = "pictures")] + Image(DynamicImage), + #[cfg(feature = "binary")] + Binary(Bytes), + #[cfg(feature = "music")] + Sound(Audio), + #[cfg(feature = "text")] + Text(Text<'a>), + #[cfg(not(feature = "text"))] + Phantom(PhantomData<&'a ()>), + #[cfg(feature = "documents")] + Doc(ShivaDocument), + #[cfg(feature = "documents")] + Archive(PdfDocument), + Meta, + #[cfg(feature = "fonts")] + Font(FontKitFont), + } + + #[cfg(feature = "shiva")] + #[derive(Debug, Error)] + #[error("extension is unknown by Shiva")] + pub struct ShivaUnknownExtensionError; + + #[cfg(feature = "shiva")] + #[derive(Debug, Error)] + pub enum ShivaError { + #[error("{0}")] + UnknownExtension(#[from] ShivaUnknownExtensionError), + #[error("{0}")] + Anyhow(#[from] anyhow::Error), + } + + #[derive(Debug, Error)] + pub enum OpenError { + #[error("io: {0}")] + Io(#[from] io::Error), + #[cfg(feature = "pictures")] + #[error("image: {0}")] + Image(#[from] img::ImageError), + #[cfg(feature = "music")] + #[error("audio: {0}")] + Audio(#[from] snd::AudioOpenError), + #[cfg(feature = "documents")] + #[error("pdf: {0}")] + Pdf(String), + #[cfg(feature = "text")] + #[error("text: {0}")] + Text(#[from] FromUtf8Error), + #[cfg(feature = "documents")] + #[error("document: {0}")] + Document(#[from] ShivaError), + #[cfg(feature = "fonts")] + #[error("font: {0:?}")] + Font(#[from] FontLoadingError), + } + + impl TryFromDataBytes for File { + type Error = io::Error; + type Format = Box>; + + fn try_from_data_bytes( + bytes: Bytes, + format: Self::Format, + _crop: crate::Crop, + ) -> Result + where + Self: Sized, + { + let mut file = File::create(format.as_ref())?; + file.write_all(&bytes)?; + Ok(file) + } + } + + impl IntoDataBytes for File { + fn into_data_bytes(self) -> Bytes { + self.bytes().flatten().collect() // !! will return an empty vec if it can't read the file! + } + } + + impl Bendable for File { + type Unit = u8; + + /// /!\ may panic with io errors /!\ + fn map) -> Self::Unit + Sync>(mut self, f: F) -> Self { + let mut bytes = Vec::new(); + self.read_to_end(&mut bytes).expect("couldn't read file"); + cfg_if! { + if #[cfg(feature = "binary")] { + self.write_all(&bytes.map(f)).expect("couldn't write file"); + } else { + self.write_all(&bytes.into_iter().map(|e| f(Cow::Owned(e))).collect::>()).expect("couldn't write file"); + } + } + self + } + } + + pub type DynamicResult = Result>, OpenError>; + + pub fn guess(t: Option, bytes: Bytes) -> DynamicResult { + use MatcherType::*; + t.map(|t| (t.matcher_type(), t.extension())) + .map( + |(matcher, extension)| -> Result { + Ok(match matcher { + #[cfg(feature = "pictures")] + Image => DynamicBendable::Image(img::load_from_memory(&bytes)?), + #[cfg(feature = "music")] + Audio => DynamicBendable::Sound(crate::snd::Audio::open(Cursor::new(bytes), None)?), + #[cfg(feature = "documents")] + Archive if extension == "pdf" => DynamicBendable::Archive( + PdfDocument::try_from_data_bytes( + bytes, + (), + Default::default(), + ) + .map_err(OpenError::Pdf)?, + ), + #[cfg(feature = "documents")] + Archive | Doc => { + let document_type = DocumentType::from_extension(extension) + .ok_or(ShivaUnknownExtensionError) + .map_err(ShivaError::UnknownExtension)?; + DynamicBendable::Doc(ShivaDocument::new( + Document::parse( + &bytes::Bytes::from(bytes), + document_type, + ) + .map_err(ShivaError::Anyhow)?, + document_type, + )) + } + #[cfg(feature = "fonts")] + Font => DynamicBendable::Font(FontKitFont::try_from_data_bytes( + bytes, + (), + Default::default(), + )?), + #[cfg(feature = "text")] + Text => DynamicBendable::Text(crate::txt::Text::try_from_data_bytes( + bytes, + (), + Default::default(), + ).unwrap()), + #[cfg(feature = "binary")] + _ => DynamicBendable::Binary(bytes), + #[cfg(not(feature = "binary"))] + _ => unimplemented!("no format reader available to open this thing (turn on the 'binary' feature to default to binary data)"), + }) + }, + ) + .transpose() + } + + pub fn open_file(path: impl AsRef) -> DynamicResult { + open(&mut File::open(path)?) + } + + pub fn open(source: &mut impl Read) -> DynamicResult { + let contents = { + let mut c = Vec::new(); + source.read_to_end(&mut c)?; + c + }; + guess(infer::get(&contents), contents) + } +} + +use std::{borrow::Cow, convert::Infallible}; + +pub trait Bendable: TryFromDataBytes + IntoDataBytes { + type Unit; + fn bend_into( + self, + format: ::Format, + crop: Crop, + ) -> Result::Error> { + T::try_from_data_bytes(self.into_data_bytes(), format, crop) + } + fn bend_from( + b: T, + format: ::Format, + crop: Crop, + ) -> Result::Error> { + Self::try_from_data_bytes(b.into_data_bytes(), format, crop) + } + fn map) -> Self::Unit + Sync>(self, f: F) -> Self; +} + +pub trait IntoDataBytes: Sized { + fn into_data_bytes(self) -> Bytes; +} + +#[derive(Default)] +pub enum Crop { + Start, + #[default] + End, +} + +pub trait TryFromDataBytes { + type Error; + type Format; + fn try_from_data_bytes( + bytes: Bytes, + format: Self::Format, + crop: Crop, + ) -> Result + where + Self: Sized; +} + +pub trait FromDataBytes { + type Format; + fn from_data_bytes(bytes: Bytes, format: Self::Format, crop: Crop) -> Self + where + Self: Sized; +} + +impl FromDataBytes for T +where + T: TryFromDataBytes, +{ + type Format = ::Format; + fn from_data_bytes(bytes: Bytes, format: Self::Format, crop: Crop) -> Self { + T::try_from_data_bytes(bytes, format, crop).unwrap_or_else(|_| unreachable!()) + } +} diff --git a/bingus/src/rawdata.rs b/bingus/src/rawdata.rs deleted file mode 100644 index d0ac87b..0000000 --- a/bingus/src/rawdata.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::ops::Deref; - -use derive_new::new; - -pub type Bytes = [u8]; - -#[derive(new)] -pub struct RawData>(D); - -impl From> for Vec -where - D: Deref, -{ - fn from(value: RawData) -> Self { - value.to_owned() - } -} - -impl Clone for RawData -where - D: Clone + Deref, -{ - fn clone(&self) -> Self { - Self(self.deref().clone()) - } -} - -impl Deref for RawData -where - D: Deref, -{ - type Target = D; - fn deref(&self) -> &Self::Target { - &self.0 - } -} diff --git a/bingus/src/snd.rs b/bingus/src/snd.rs new file mode 100644 index 0000000..dda4d48 --- /dev/null +++ b/bingus/src/snd.rs @@ -0,0 +1,5 @@ +pub use symphonia::core::*; +mod raw; +pub use raw::*; +mod simphonia; +pub use simphonia::*; diff --git a/bingus/src/snd/raw.rs b/bingus/src/snd/raw.rs new file mode 100644 index 0000000..e82250e --- /dev/null +++ b/bingus/src/snd/raw.rs @@ -0,0 +1,103 @@ +use std::{borrow::Cow, convert::Infallible}; + +use super::sample::Sample; +use cfg_if::cfg_if; +use derive_wrapper::{AsRef, From}; +use num::{ + traits::{FromBytes, ToBytes}, + Zero, +}; +#[cfg(feature = "rayon")] +use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; + +use crate::{Bendable, IntoDataBytes, TryFromDataBytes}; + +#[derive(From, AsRef)] +pub struct RawSamples(Vec) +where + T: Sample; + +impl RawSamples { + pub fn into_inner(self) -> Vec { + self.0 + } +} + +impl IntoDataBytes for RawSamples +where + T: Sample + ToBytes, +{ + fn into_data_bytes(self) -> crate::Bytes { + self.as_ref() + .iter() + .flat_map(|subpixel| subpixel.to_ne_bytes().as_ref().to_vec()) + .collect() + } +} + +impl TryFromDataBytes for RawSamples +where + T: Sample + FromBytes + ToBytes + Zero, + ::Bytes: Sized + for<'a> TryFrom<&'a [u8]>, +{ + type Error = Infallible; + type Format = (); + fn try_from_data_bytes( + bytes: crate::Bytes, + _format: Self::Format, + crop: crate::Crop, + ) -> Result { + Ok(match crop { + crate::Crop::End => bytes + .chunks_exact(T::zero().to_ne_bytes().as_ref().len()) + .map(|p| { + T::from_ne_bytes(&match ::Bytes::try_from(p) { + Ok(v) => v, + Err(_) => unreachable!("you messed up chunk size!"), + }) + }) + .collect::>(), + crate::Crop::Start => bytes + .rchunks_exact(T::zero().to_ne_bytes().as_ref().len()) + .map(|p| { + T::from_ne_bytes(&match ::Bytes::try_from(p) { + Ok(v) => v, + Err(_) => unreachable!("you messed up chunk size!"), + }) + }) + .collect::>(), + } + .into()) + } +} + +cfg_if! { +if #[cfg(feature = "rayon")] { + impl Bendable for RawSamples + where + T: Sample + FromBytes + ToBytes + Zero + Send, + ::Bytes: Sized + for<'a> TryFrom<&'a [u8]>, + for<'a> Vec: IntoParallelRefMutIterator<'a, Item = &'a mut T>, + { + type Unit = T; + fn map) -> Self::Unit + Sync>(mut self, f: F) -> Self { + self.0.par_iter_mut().for_each(|e| *e = f(Cow::Borrowed(e))); + self + } + fn format() -> crate::dynamic::Format { + crate::Format::Sound + } + } +} else { + impl Bendable for RawSamples + where + T: Sample + FromBytes + ToBytes + Zero + Send, + ::Bytes: Sized + for<'a> TryFrom<&'a [u8]>, + { + type Unit = T; + fn map) -> Self::Unit + Sync>(mut self, f: F) -> Self { + self.0.iter_mut().for_each(|e| *e = f(Cow::Borrowed(e))); + self + } + } +}} diff --git a/bingus/src/snd/simphonia.rs b/bingus/src/snd/simphonia.rs new file mode 100644 index 0000000..d79c598 --- /dev/null +++ b/bingus/src/snd/simphonia.rs @@ -0,0 +1,191 @@ +use std::io::{self, Read}; + +use derive_new::new; +use symphonia::{ + core::{ + audio::Signal, + codecs::{Decoder, CODEC_TYPE_NULL}, + conv::FromSample, + formats::FormatReader, + io::{MediaSource, MediaSourceStream}, + probe::Hint, + sample::{i24, u24}, + }, + default, +}; +use thiserror::Error; + +use crate::IntoDataBytes; + +use super::{sample::Sample, RawSamples}; + +/// Audio, unlike DynamicImage, isn't directly bendable +/// because its underlying data format isn't decided automatically +/// so you have to attribute it yourself by turning it into a [RawSamples] struct +/// with your chosen sample format. +#[derive(new)] +pub struct Audio { + reader: Box, + decoder: Box, +} + +#[derive(Debug, Error)] +pub enum AudioOpenError { + #[error("IO error: {0}")] + Io(#[from] io::Error), + #[error("symphonia can't open this file: {0}")] + Symphonia(#[from] symphonia::core::errors::Error), +} + +impl PartialEq for AudioOpenError { + fn eq(&self, other: &Self) -> bool { + match self { + AudioOpenError::Io(_) => matches!(other, AudioOpenError::Io(_)), + AudioOpenError::Symphonia(_) => matches!(other, AudioOpenError::Symphonia(_)), + } + } +} + +impl Audio { + pub fn open( + source: impl MediaSource + 'static, + extension: Option<&str>, + ) -> Result { + let registry = default::get_codecs(); + let probe = default::get_probe(); + let mss = MediaSourceStream::new(Box::new(source), Default::default()); + let reader = probe + .format( + &{ + let mut hint = Hint::new(); + if let Some(e) = extension { + hint.with_extension(e); + } + hint + }, + mss, + &Default::default(), + &Default::default(), + )? + .format; + let decoder = registry.make( + reader + .tracks() + .iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + .map(|t| &t.codec_params) + .unwrap_or(&Default::default()), + &Default::default(), + )?; + Ok(Audio::new(reader, decoder)) + } +} + +impl IntoDataBytes for Audio { + fn into_data_bytes(self) -> crate::Bytes { + self.reader.into_inner().bytes().flatten().collect() + } +} + +macro_rules! dynamic_map( + ($dynimage: expr, $image: pat => $action: expr) => ({ + match $dynimage { + symphonia::core::audio::AudioBufferRef::U8($image) => symphonia::core::audio::AudioBufferRef::U8($action), + symphonia::core::audio::AudioBufferRef::U16($image) => symphonia::core::audio::AudioBufferRef::U16($action), + symphonia::core::audio::AudioBufferRef::U24($image) => symphonia::core::audio::AudioBufferRef::U24($action), + symphonia::core::audio::AudioBufferRef::U32($image) => symphonia::core::audio::AudioBufferRef::U32($action), + symphonia::core::audio::AudioBufferRef::S8($image) => symphonia::core::audio::AudioBufferRef::S8($action), + symphonia::core::audio::AudioBufferRef::S16($image) => symphonia::core::audio::AudioBufferRef::S16($action), + symphonia::core::audio::AudioBufferRef::S24($image) => symphonia::core::audio::AudioBufferRef::S24($action), + symphonia::core::audio::AudioBufferRef::S32($image) => symphonia::core::audio::AudioBufferRef::S32($action), + symphonia::core::audio::AudioBufferRef::F32($image) => symphonia::core::audio::AudioBufferRef::F32($action), + symphonia::core::audio::AudioBufferRef::F64($image) => symphonia::core::audio::AudioBufferRef::F64($action), + } + }); + + ($dynimage: expr, $image:pat_param, $action: expr) => ( + match $dynimage { + symphonia::core::audio::AudioBufferRef::U8($image) => $action, + symphonia::core::audio::AudioBufferRef::U16($image) => $action, + symphonia::core::audio::AudioBufferRef::U24($image) => $action, + symphonia::core::audio::AudioBufferRef::U32($image) => $action, + symphonia::core::audio::AudioBufferRef::S8($image) => $action, + symphonia::core::audio::AudioBufferRef::S16($image) => $action, + symphonia::core::audio::AudioBufferRef::S24($image) => $action, + symphonia::core::audio::AudioBufferRef::S32($image) => $action, + symphonia::core::audio::AudioBufferRef::F32($image) => $action, + symphonia::core::audio::AudioBufferRef::F64($image) => $action, + } + ); +); + +impl From