Compare commits
83 commits
Author | SHA1 | Date | |
---|---|---|---|
|
63a7cb5c20 | ||
|
533cb094e0 | ||
|
c7f537f4f1 | ||
|
399bff704f | ||
|
1c7d187bd7 | ||
|
4dcb82d3a0 | ||
|
e4dddb4aba | ||
|
2cf1ec385f | ||
|
f72a30b972 | ||
|
6bb627d77b | ||
|
52c15986a8 | ||
|
53bdd265b3 | ||
|
130a75d0ca | ||
|
2a93d99841 | ||
|
6e37c21bf2 | ||
|
9596264322 | ||
|
51ab13b3ef | ||
|
84a3b182ab | ||
|
d2ede29165 | ||
|
066d89d250 | ||
|
5c8fa837ca | ||
|
eae2bd3d48 | ||
|
4ab4d33e0c | ||
|
bff2b660dc | ||
|
ab294950b4 | ||
|
f207ac0b06 | ||
|
4df5c27bdd | ||
|
24d4b5ac2c | ||
|
efb9c7c39d | ||
|
0c3439d788 | ||
|
ae6e2edafa | ||
|
b00ce70504 | ||
|
dc74662263 | ||
|
df2c3747b7 | ||
|
7d5c860d05 | ||
|
0ac3340373 | ||
|
308e3a9d11 | ||
|
ba15f4fda3 | ||
|
5c3ea7a512 | ||
|
c4fcb7bb25 | ||
|
f9b54bb505 | ||
|
d48822fd96 | ||
|
2c01b3761d | ||
|
ba9443fc08 | ||
|
19c6ee71d5 | ||
|
b94421a223 | ||
|
0ac6b3a16d | ||
|
fe1ee27d80 | ||
|
9130f52f66 | ||
|
c58275dc75 | ||
|
af7b4f77bb | ||
|
4d80833bbd | ||
|
2733cd9345 | ||
|
2d73ac2e4c | ||
|
84419e3e5f | ||
|
090d397409 | ||
|
299887ff84 | ||
|
bf58d2cd51 | ||
|
07f2a9fa38 | ||
|
f4e8f7c276 | ||
|
58cf13a564 | ||
|
76ced9bb9d | ||
|
68b90af755 | ||
|
7ca053d918 | ||
bcce055d4c | |||
3527c0a2af | |||
e1edc611bc | |||
5e078c951d | |||
0ac42b0793 | |||
1728b9b2c4 | |||
1508a03717 | |||
0d5532ab08 | |||
9748483e74 | |||
ff6e44072e | |||
f7eb44179d | |||
1ef8d8c4cb | |||
42bd1a49f4 | |||
d69011ddb6 | |||
b6eb5720c9 | |||
2190440b3f | |||
7b39c8e6dc | |||
a1e2111e6d | |||
f688dbe225 |
39 changed files with 2189 additions and 224 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
||||||
/target
|
/target
|
||||||
*.lock
|
*.lock
|
||||||
bmp/out.*
|
bmp/out.*
|
||||||
|
fonts
|
||||||
|
test.*
|
17
Cargo.toml
17
Cargo.toml
|
@ -1,24 +1,9 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["bent", "bent-funny-zone", "bingus", "bong"]
|
members = ["bingus", "bong"]
|
||||||
package.edition = "2021"
|
package.edition = "2021"
|
||||||
package.license = "Unlicense"
|
package.license = "Unlicense"
|
||||||
package.description = "databending made easy"
|
package.description = "databending made easy"
|
||||||
package.authors = ["Breval Ferrari <breee@duck.com>"]
|
package.authors = ["Breval Ferrari <breee@duck.com>"]
|
||||||
package.repository = "https://codeberg.org/p6nj/bent"
|
package.repository = "https://codeberg.org/p6nj/bent"
|
||||||
package.keywords = ["databending", "data-bending", "bending", "bend", "art"]
|
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"
|
|
||||||
|
|
11
README.md
11
README.md
|
@ -1,5 +1,12 @@
|
||||||
# BENT
|
# BENT
|
||||||
|
|
||||||
> Get bent! Databending made easy.
|
> Get bent! Databending made easy.
|
||||||
|
|
||||||
This crate aims to provide a simple GUI to bend any file to another format without dealing with headers.
|
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.
|
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
|
||||||
|
|
|
@ -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 }
|
|
|
@ -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<Self> {
|
|
||||||
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<PathBuf> {
|
|
||||||
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<Self> {
|
|
||||||
Files::try_new(
|
|
||||||
FileDialog::new()
|
|
||||||
.set_title("Input image")
|
|
||||||
.set_directory(working_dir())
|
|
||||||
.add_filter(
|
|
||||||
"images",
|
|
||||||
&ImageFormat::all()
|
|
||||||
.map(ImageFormat::extensions_str)
|
|
||||||
.flatten()
|
|
||||||
.collect::<Vec<&'static &'static str>>(),
|
|
||||||
)
|
|
||||||
.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::<RgbImage>::new(
|
|
||||||
ImageReader::open(dp.files.input)?
|
|
||||||
.decode()
|
|
||||||
.context("can't use this picture")?
|
|
||||||
.into_rgb8(),
|
|
||||||
);
|
|
||||||
let processed = RawData::<RgbImage>::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<u8> = (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::<Vec<u8>>()
|
|
||||||
// );
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn working_dir() -> PathBuf {
|
|
||||||
picture_dir()
|
|
||||||
.or(download_dir())
|
|
||||||
.or(home_dir())
|
|
||||||
.unwrap_or("/".into())
|
|
||||||
}
|
|
|
@ -15,6 +15,10 @@ crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
|
image = { workspace = true }
|
||||||
|
dasp_sample = { workspace = true }
|
||||||
|
bingus = { workspace = true }
|
||||||
|
fundsp = { workspace = true }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
|
|
|
@ -8,12 +8,23 @@
|
||||||
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
|
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const download = (content, filename, contentType) => {
|
||||||
|
if (!contentType) contentType = 'application/octet-stream';
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.setAttribute('visibility', 'hidden');
|
||||||
|
var blob = new Blob([content], { 'type': contentType });
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = filename;
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
const files = () => { return Array.from(document.getElementById("files").files).map(x => x.name); };
|
||||||
|
</script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import init, { greet } from "./pkg/bent.js";
|
import init, { greet } from "./pkg/bent.js";
|
||||||
init().then(() => {
|
init().then(() => {
|
||||||
document.getElementById("greet-button").onclick = () => {
|
document.getElementById("verify-button").onclick = () => {
|
||||||
const name = document.getElementById("name").value;
|
console.log(files());
|
||||||
greet(name);
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -25,10 +36,10 @@
|
||||||
|
|
||||||
<form onsubmit="return false;">
|
<form onsubmit="return false;">
|
||||||
<p>
|
<p>
|
||||||
<label>Enter your name:</label>
|
<label>Select some files:</label>
|
||||||
<input type="text" id="name" />
|
<input type="file" id="files" multiple="true" />
|
||||||
</p>
|
</p>
|
||||||
<button id="greet-button">Greet</button>
|
<button id="verify-button">Verify</button>
|
||||||
<button type="reset">Reset</button>
|
<button type="reset">Reset</button>
|
||||||
</form>
|
</form>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -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)]
|
#[allow(unused_macros)]
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub mod macros;
|
pub mod macros;
|
||||||
|
@ -8,3 +12,49 @@ pub mod js;
|
||||||
pub fn greet(name: &str) {
|
pub fn greet(name: &str) {
|
||||||
println!("Hello, {name}!");
|
println!("Hello, {name}!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(getter_with_clone)]
|
||||||
|
pub struct ImageFile {
|
||||||
|
pub name: String,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn process_files(files: Vec<ImageFile>) {
|
||||||
|
files.into_iter().for_each(|file| {
|
||||||
|
let img = RawData::<RgbImage>::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::<RgbImage>::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::<Vec<u8>>(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
let _ = processed;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "bingus"
|
name = "bingus"
|
||||||
version = "0.1.2"
|
version = "0.6.0"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
description.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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
[dependencies]
|
[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"]
|
||||||
|
|
2
bingus/src/bin.rs
Normal file
2
bingus/src/bin.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
#[path = "bin_/bytes.rs"]
|
||||||
|
mod bytes;
|
40
bingus/src/bin_/bytes.rs
Normal file
40
bingus/src/bin_/bytes.rs
Normal file
|
@ -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<Self, Self::Error> {
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bendable for Bytes {
|
||||||
|
type Unit = u8;
|
||||||
|
fn map<F: Fn(Cow<Self::Unit>) -> 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
|
||||||
|
}
|
||||||
|
}
|
6
bingus/src/doc.rs
Normal file
6
bingus/src/doc.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
pub use printpdf;
|
||||||
|
mod pdf;
|
||||||
|
#[cfg(feature = "shiva")]
|
||||||
|
mod shiva;
|
||||||
|
#[cfg(feature = "shiva")]
|
||||||
|
pub use shiva::*;
|
122
bingus/src/doc/pdf.rs
Normal file
122
bingus/src/doc/pdf.rs
Normal file
|
@ -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<PdfWarnMsg>) {
|
||||||
|
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<Self, Self::Error> {
|
||||||
|
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<F: Fn(Cow<Self::Unit>) -> 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::<Vec<Op>>(),
|
||||||
|
..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()))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
62
bingus/src/doc/shiva.rs
Normal file
62
bingus/src/doc/shiva.rs
Normal file
|
@ -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<Self, Self::Error> {
|
||||||
|
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<F: Fn(Cow<Self::Unit>) -> 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
2
bingus/src/fnt.rs
Normal file
2
bingus/src/fnt.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
mod fontkit;
|
||||||
|
pub use fontkit::Font;
|
49
bingus/src/fnt/fontkit.rs
Normal file
49
bingus/src/fnt/fontkit.rs
Normal file
|
@ -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, Self::Error> {
|
||||||
|
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<F: Fn(Cow<Self::Unit>) -> 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")
|
||||||
|
}
|
||||||
|
}
|
3
bingus/src/img.rs
Normal file
3
bingus/src/img.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mod image;
|
||||||
|
pub use image::ImageBuffer as Image;
|
||||||
|
pub use image::*;
|
383
bingus/src/img/image.rs
Normal file
383
bingus/src/img/image.rs
Normal file
|
@ -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<P: Pixel> IntoDataBytes for ImageBuffer<P, Vec<P::Subpixel>>
|
||||||
|
where
|
||||||
|
Vec<P::Subpixel>: Deref<Target = [P::Subpixel]>,
|
||||||
|
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<Dimensions> 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<u32> for Dimensions {
|
||||||
|
type Output = Dimensions;
|
||||||
|
fn div(self, rhs: u32) -> Self::Output {
|
||||||
|
Dimensions {
|
||||||
|
width: self.width / rhs,
|
||||||
|
height: self.height / rhs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mul<Dimensions> 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<u32> for Dimensions {
|
||||||
|
type Output = Dimensions;
|
||||||
|
fn mul(self, rhs: u32) -> Self::Output {
|
||||||
|
Dimensions {
|
||||||
|
width: self.width * rhs,
|
||||||
|
height: self.height * rhs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Add<Dimensions> 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<u32> for Dimensions {
|
||||||
|
type Output = Dimensions;
|
||||||
|
fn add(self, rhs: u32) -> Self::Output {
|
||||||
|
Dimensions {
|
||||||
|
width: self.width + rhs,
|
||||||
|
height: self.height + rhs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sub<Dimensions> 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<u32> for Dimensions {
|
||||||
|
type Output = Dimensions;
|
||||||
|
fn sub(self, rhs: u32) -> Self::Output {
|
||||||
|
Dimensions {
|
||||||
|
width: self.width - rhs,
|
||||||
|
height: self.height - rhs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: Pixel> TryFromDataBytes for ImageBuffer<P, Vec<P::Subpixel>>
|
||||||
|
where
|
||||||
|
Vec<P::Subpixel>: Deref<Target = [P::Subpixel]>,
|
||||||
|
P::Subpixel: ToBytes + FromBytes + Zero,
|
||||||
|
<P::Subpixel as FromBytes>::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<Self, Self::Error> {
|
||||||
|
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 <P::Subpixel as FromBytes>::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::<Vec<P::Subpixel>>(),
|
||||||
|
crate::Crop::Start => bytes
|
||||||
|
.rchunks_exact(P::Subpixel::zero().to_ne_bytes().as_ref().len())
|
||||||
|
.map(|p| {
|
||||||
|
P::Subpixel::from_ne_bytes(
|
||||||
|
&match <P::Subpixel as FromBytes>::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::<Vec<P::Subpixel>>(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.ok_or_else(|| unreachable!())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: Pixel> Bendable for ImageBuffer<P, Vec<P::Subpixel>>
|
||||||
|
where
|
||||||
|
Vec<P::Subpixel>: Deref<Target = [P::Subpixel]> + DerefMut,
|
||||||
|
P::Subpixel: ToBytes + FromBytes + Send + Sync,
|
||||||
|
<P::Subpixel as FromBytes>::Bytes: for<'a> TryFrom<&'a [u8]>,
|
||||||
|
P: Send + Sync,
|
||||||
|
{
|
||||||
|
type Unit = P;
|
||||||
|
fn map<F: Fn(Cow<Self::Unit>) -> 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<Self, Self::Error> {
|
||||||
|
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::<Rgb<u16>, Vec<u16>>::from_raw(
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
vec![1, 2, 3, 254, 255, 256, 307, 308, 309],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
Ok(image.clone()),
|
||||||
|
ImageBuffer::<Rgb<u16>, Vec<u16>>::try_from_data_bytes(
|
||||||
|
image.into_data_bytes(),
|
||||||
|
Dimensions {
|
||||||
|
width: 3,
|
||||||
|
height: 1
|
||||||
|
},
|
||||||
|
Default::default()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rgb_signed() {
|
||||||
|
let image = ImageBuffer::<Rgb<i16>, Vec<i16>>::from_raw(
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
vec![1, 2, 3, 254, 255, 256, -307, 308, 309],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
Ok(image.clone()),
|
||||||
|
ImageBuffer::<Rgb<i16>, Vec<i16>>::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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,2 +1,280 @@
|
||||||
#![forbid(unused_crate_dependencies)]
|
#[cfg(feature = "binary")]
|
||||||
pub mod rawdata;
|
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<u8>;
|
||||||
|
|
||||||
|
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<dyn AsRef<Path>>;
|
||||||
|
|
||||||
|
fn try_from_data_bytes(
|
||||||
|
bytes: Bytes,
|
||||||
|
format: Self::Format,
|
||||||
|
_crop: crate::Crop,
|
||||||
|
) -> Result<Self, Self::Error>
|
||||||
|
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<F: Fn(Cow<Self::Unit>) -> 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::<Vec<u8>>()).expect("couldn't write file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type DynamicResult = Result<Option<DynamicBendable<'static>>, OpenError>;
|
||||||
|
|
||||||
|
pub fn guess(t: Option<infer::Type>, bytes: Bytes) -> DynamicResult {
|
||||||
|
use MatcherType::*;
|
||||||
|
t.map(|t| (t.matcher_type(), t.extension()))
|
||||||
|
.map(
|
||||||
|
|(matcher, extension)| -> Result<DynamicBendable, OpenError> {
|
||||||
|
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<Path>) -> 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<T: TryFromDataBytes>(
|
||||||
|
self,
|
||||||
|
format: <T as TryFromDataBytes>::Format,
|
||||||
|
crop: Crop,
|
||||||
|
) -> Result<T, <T as TryFromDataBytes>::Error> {
|
||||||
|
T::try_from_data_bytes(self.into_data_bytes(), format, crop)
|
||||||
|
}
|
||||||
|
fn bend_from<T: IntoDataBytes>(
|
||||||
|
b: T,
|
||||||
|
format: <Self as TryFromDataBytes>::Format,
|
||||||
|
crop: Crop,
|
||||||
|
) -> Result<Self, <Self as TryFromDataBytes>::Error> {
|
||||||
|
Self::try_from_data_bytes(b.into_data_bytes(), format, crop)
|
||||||
|
}
|
||||||
|
fn map<F: Fn(Cow<Self::Unit>) -> 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<Self, Self::Error>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait FromDataBytes {
|
||||||
|
type Format;
|
||||||
|
fn from_data_bytes(bytes: Bytes, format: Self::Format, crop: Crop) -> Self
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F, T> FromDataBytes for T
|
||||||
|
where
|
||||||
|
T: TryFromDataBytes<Error = Infallible, Format = F>,
|
||||||
|
{
|
||||||
|
type Format = <T as TryFromDataBytes>::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!())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use derive_new::new;
|
|
||||||
|
|
||||||
pub type Bytes = [u8];
|
|
||||||
|
|
||||||
#[derive(new)]
|
|
||||||
pub struct RawData<D: Deref<Target = Bytes>>(D);
|
|
||||||
|
|
||||||
impl<D> From<RawData<D>> for Vec<u8>
|
|
||||||
where
|
|
||||||
D: Deref<Target = Bytes>,
|
|
||||||
{
|
|
||||||
fn from(value: RawData<D>) -> Self {
|
|
||||||
value.to_owned()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<D> Clone for RawData<D>
|
|
||||||
where
|
|
||||||
D: Clone + Deref<Target = Bytes>,
|
|
||||||
{
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self(self.deref().clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<D> Deref for RawData<D>
|
|
||||||
where
|
|
||||||
D: Deref<Target = Bytes>,
|
|
||||||
{
|
|
||||||
type Target = D;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
5
bingus/src/snd.rs
Normal file
5
bingus/src/snd.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
pub use symphonia::core::*;
|
||||||
|
mod raw;
|
||||||
|
pub use raw::*;
|
||||||
|
mod simphonia;
|
||||||
|
pub use simphonia::*;
|
103
bingus/src/snd/raw.rs
Normal file
103
bingus/src/snd/raw.rs
Normal file
|
@ -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<T>(Vec<T>)
|
||||||
|
where
|
||||||
|
T: Sample;
|
||||||
|
|
||||||
|
impl<T: Sample> RawSamples<T> {
|
||||||
|
pub fn into_inner(self) -> Vec<T> {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> IntoDataBytes for RawSamples<T>
|
||||||
|
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<T> TryFromDataBytes for RawSamples<T>
|
||||||
|
where
|
||||||
|
T: Sample + FromBytes + ToBytes + Zero,
|
||||||
|
<T as FromBytes>::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<Self, Self::Error> {
|
||||||
|
Ok(match crop {
|
||||||
|
crate::Crop::End => bytes
|
||||||
|
.chunks_exact(T::zero().to_ne_bytes().as_ref().len())
|
||||||
|
.map(|p| {
|
||||||
|
T::from_ne_bytes(&match <T as FromBytes>::Bytes::try_from(p) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => unreachable!("you messed up chunk size!"),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<T>>(),
|
||||||
|
crate::Crop::Start => bytes
|
||||||
|
.rchunks_exact(T::zero().to_ne_bytes().as_ref().len())
|
||||||
|
.map(|p| {
|
||||||
|
T::from_ne_bytes(&match <T as FromBytes>::Bytes::try_from(p) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => unreachable!("you messed up chunk size!"),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<T>>(),
|
||||||
|
}
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "rayon")] {
|
||||||
|
impl<T> Bendable for RawSamples<T>
|
||||||
|
where
|
||||||
|
T: Sample + FromBytes + ToBytes + Zero + Send,
|
||||||
|
<T as FromBytes>::Bytes: Sized + for<'a> TryFrom<&'a [u8]>,
|
||||||
|
for<'a> Vec<T>: IntoParallelRefMutIterator<'a, Item = &'a mut T>,
|
||||||
|
{
|
||||||
|
type Unit = T;
|
||||||
|
fn map<F: Fn(Cow<Self::Unit>) -> 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<T> Bendable for RawSamples<T>
|
||||||
|
where
|
||||||
|
T: Sample + FromBytes + ToBytes + Zero + Send,
|
||||||
|
<T as FromBytes>::Bytes: Sized + for<'a> TryFrom<&'a [u8]>,
|
||||||
|
{
|
||||||
|
type Unit = T;
|
||||||
|
fn map<F: Fn(Cow<Self::Unit>) -> Self::Unit + Sync>(mut self, f: F) -> Self {
|
||||||
|
self.0.iter_mut().for_each(|e| *e = f(Cow::Borrowed(e)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
191
bingus/src/snd/simphonia.rs
Normal file
191
bingus/src/snd/simphonia.rs
Normal file
|
@ -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<dyn FormatReader>,
|
||||||
|
decoder: Box<dyn Decoder>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<Audio, AudioOpenError> {
|
||||||
|
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<S> From<Audio> for RawSamples<S>
|
||||||
|
where
|
||||||
|
S: Sample
|
||||||
|
+ FromSample<u8>
|
||||||
|
+ FromSample<u16>
|
||||||
|
+ FromSample<u24>
|
||||||
|
+ FromSample<u32>
|
||||||
|
+ FromSample<i8>
|
||||||
|
+ FromSample<i16>
|
||||||
|
+ FromSample<i24>
|
||||||
|
+ FromSample<i32>
|
||||||
|
+ FromSample<f32>
|
||||||
|
+ FromSample<f64>,
|
||||||
|
{
|
||||||
|
fn from(mut value: Audio) -> Self {
|
||||||
|
RawSamples::from({
|
||||||
|
let mut result = Vec::new();
|
||||||
|
while let Ok(packet) = value.reader.next_packet() {
|
||||||
|
if let Ok(src) = value.decoder.decode(&packet) {
|
||||||
|
dynamic_map!(
|
||||||
|
src,
|
||||||
|
ref audio_buffer,
|
||||||
|
result.append(
|
||||||
|
&mut (0usize..src.spec().channels.count())
|
||||||
|
.flat_map(|ch| audio_buffer
|
||||||
|
.chan(ch)
|
||||||
|
.iter()
|
||||||
|
.map(|s| S::from_sample(*s))
|
||||||
|
.collect::<Vec<S>>())
|
||||||
|
.collect::<Vec<S>>()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::fs::File;
|
||||||
|
|
||||||
|
use project_root::get_project_root;
|
||||||
|
|
||||||
|
use crate::IntoDataBytes;
|
||||||
|
|
||||||
|
use super::Audio;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn open_sample_file() {
|
||||||
|
let original =
|
||||||
|
&include_bytes!("../../../testing material/sound/sample-3s.mp3")[52079 - 51826..];
|
||||||
|
let path = get_project_root()
|
||||||
|
.expect("can't find project root!")
|
||||||
|
.join("testing material")
|
||||||
|
.join("sound")
|
||||||
|
.join("sample-3s.mp3");
|
||||||
|
let result = Audio::open(
|
||||||
|
File::open(&path).expect("can't find test file!"),
|
||||||
|
path.extension().and_then(|s| s.to_str()),
|
||||||
|
)
|
||||||
|
.map(|audio| audio.into_data_bytes());
|
||||||
|
dbg!(original.len());
|
||||||
|
if let Ok(ref r) = result {
|
||||||
|
dbg!(r.len());
|
||||||
|
}
|
||||||
|
assert_eq!(Ok(original), result.as_ref().map(Vec::as_slice));
|
||||||
|
}
|
||||||
|
}
|
2
bingus/src/txt.rs
Normal file
2
bingus/src/txt.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
mod bare;
|
||||||
|
pub use bare::*;
|
34
bingus/src/txt/bare.rs
Normal file
34
bingus/src/txt/bare.rs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
use std::{borrow::Cow, convert::Infallible};
|
||||||
|
|
||||||
|
use crate::{Bendable, Bytes, IntoDataBytes, TryFromDataBytes};
|
||||||
|
|
||||||
|
pub type Text<'a> = Cow<'a, str>;
|
||||||
|
|
||||||
|
impl TryFromDataBytes for Text<'_> {
|
||||||
|
type Error = Infallible;
|
||||||
|
type Format = ();
|
||||||
|
fn try_from_data_bytes(
|
||||||
|
bytes: Bytes,
|
||||||
|
_format: Self::Format,
|
||||||
|
_crop: crate::Crop,
|
||||||
|
) -> Result<Self, Self::Error> {
|
||||||
|
Ok(String::from_utf8_lossy(&bytes).into_owned().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoDataBytes for Text<'_> {
|
||||||
|
fn into_data_bytes(self) -> Bytes {
|
||||||
|
self.as_bytes().to_vec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bendable for Text<'_> {
|
||||||
|
type Unit = char;
|
||||||
|
|
||||||
|
fn map<F: Fn(Cow<Self::Unit>) -> Self::Unit + Sync>(self, f: F) -> Self {
|
||||||
|
self.chars()
|
||||||
|
.map(|c| f(Cow::Owned(c)))
|
||||||
|
.collect::<String>()
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,23 @@
|
||||||
[package]
|
[package]
|
||||||
name = "bong"
|
name = "bong"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
keywords.workspace = true
|
keywords.workspace = true
|
||||||
dependencies.workspace = true
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = "1"
|
||||||
|
bingus = "0.6"
|
||||||
|
strum = { version = "0", features = ["derive"] }
|
||||||
|
derive-new = "0"
|
||||||
|
clap = { version = "4.5.40", features = ["derive"] }
|
||||||
|
derive_wrapper = "0.1.7"
|
||||||
|
unwrap-infallible = "0.1"
|
||||||
|
mp3lame-encoder = { version = "0.2", features = ["std"] }
|
||||||
|
hound = "3.5.1"
|
||||||
|
flacenc = "0.4.0"
|
||||||
|
|
213
bong/src/cli.rs
Normal file
213
bong/src/cli.rs
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
use std::{
|
||||||
|
convert::Infallible,
|
||||||
|
fs::File,
|
||||||
|
io::{self, stdin, stdout, Read},
|
||||||
|
path::PathBuf,
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Context};
|
||||||
|
use bingus::doc::DocumentType;
|
||||||
|
use clap::Parser;
|
||||||
|
use derive_new::new;
|
||||||
|
use strum::{AsRefStr, Display, EnumString};
|
||||||
|
|
||||||
|
#[derive(new, Parser)]
|
||||||
|
#[clap(version, author, about)]
|
||||||
|
/// CLI from the Bent project
|
||||||
|
pub(crate) struct Cli {
|
||||||
|
/// Input file or standard input ("-").
|
||||||
|
pub(crate) input: FileOrStd<Stdin>,
|
||||||
|
// /// List of commands (process or bend to another format)
|
||||||
|
// pub(crate) commands: Vec<Command>,
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub(crate) output_format: OutputFormat,
|
||||||
|
/// Output file or standard output ("-").
|
||||||
|
pub(crate) output: FileOrStd<Stdout>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Parser)]
|
||||||
|
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||||
|
pub struct Dimensions {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Parser)]
|
||||||
|
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||||
|
pub struct ShivaFormat {
|
||||||
|
doc_input_format: DocumentType,
|
||||||
|
doc_output_format: DocumentType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Parser)]
|
||||||
|
pub struct Mp3Format {
|
||||||
|
#[arg(default_value = "44100")]
|
||||||
|
pub sample_rate: u32,
|
||||||
|
#[arg(default_value = "128")]
|
||||||
|
pub bitrate: u16,
|
||||||
|
#[arg(default_value = "5")]
|
||||||
|
pub quality: u8,
|
||||||
|
#[arg(default_value_t)]
|
||||||
|
pub sample_format: Mp3SampleFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, EnumString, Default, Display)]
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
pub enum Mp3SampleFormat {
|
||||||
|
U16,
|
||||||
|
I16,
|
||||||
|
I32,
|
||||||
|
#[default]
|
||||||
|
F32,
|
||||||
|
F64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Parser)]
|
||||||
|
pub struct WavFormat {
|
||||||
|
#[arg(default_value = "48000")]
|
||||||
|
pub sample_rate: u32,
|
||||||
|
#[arg(default_value_t)]
|
||||||
|
pub sample_format: WavSampleFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, EnumString, Display, Default)]
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
pub enum WavSampleFormat {
|
||||||
|
I8,
|
||||||
|
I16,
|
||||||
|
I32,
|
||||||
|
#[default]
|
||||||
|
F32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Parser)]
|
||||||
|
pub struct FlacFormat {
|
||||||
|
#[arg(default_value = "48000")]
|
||||||
|
pub sample_rate: usize,
|
||||||
|
#[arg(value_parser = flac_bps_value_parser, default_value = "16")]
|
||||||
|
pub bps: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flac_bps_value_parser(input: &str) -> anyhow::Result<usize> {
|
||||||
|
input.parse().context("not a number").and_then(|n| match n {
|
||||||
|
8 | 16 | 24 => Ok(n),
|
||||||
|
_ => Err(anyhow!("should be 8, 16 or 24")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<WavSampleFormat> for hound::SampleFormat {
|
||||||
|
fn from(value: WavSampleFormat) -> Self {
|
||||||
|
match value {
|
||||||
|
WavSampleFormat::F32 => hound::SampleFormat::Float,
|
||||||
|
_ => hound::SampleFormat::Int,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Dimensions> for bingus::img::Dimensions {
|
||||||
|
fn from(value: Dimensions) -> Self {
|
||||||
|
Self {
|
||||||
|
width: value.width,
|
||||||
|
height: value.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ShivaFormat> for bingus::doc::ShivaFormat {
|
||||||
|
fn from(value: ShivaFormat) -> Self {
|
||||||
|
Self::new(value.doc_input_format, value.doc_output_format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default)]
|
||||||
|
pub(crate) struct Stdin;
|
||||||
|
#[derive(Clone, Copy, Default)]
|
||||||
|
pub(crate) struct Stdout;
|
||||||
|
|
||||||
|
pub(crate) trait New {
|
||||||
|
type Real;
|
||||||
|
fn new() -> Self::Real;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl New for Stdin {
|
||||||
|
type Real = std::io::Stdin;
|
||||||
|
fn new() -> Self::Real {
|
||||||
|
stdin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl New for Stdout {
|
||||||
|
type Real = std::io::Stdout;
|
||||||
|
fn new() -> Self::Real {
|
||||||
|
stdout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: Clone + Default> FromStr for FileOrStd<S> {
|
||||||
|
type Err = Infallible;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if s == "-" {
|
||||||
|
Ok(FileOrStd::Std(Default::default()))
|
||||||
|
} else {
|
||||||
|
s.parse().map(FileOrStd::File)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||||
|
pub(crate) enum FileOrStd<S: Clone> {
|
||||||
|
File(PathBuf),
|
||||||
|
Std(S),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, S> FileOrStd<S>
|
||||||
|
where
|
||||||
|
S: Clone + New,
|
||||||
|
<S as New>::Real: Read + 'a,
|
||||||
|
{
|
||||||
|
pub(crate) fn reader(self) -> Result<Box<dyn Read + 'a>, io::Error> {
|
||||||
|
Ok(match self {
|
||||||
|
FileOrStd::File(path) => Box::new(File::open(path)?),
|
||||||
|
FileOrStd::Std(_) => Box::new(S::new()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Parser)]
|
||||||
|
pub(crate) enum OutputFormat {
|
||||||
|
Bytes,
|
||||||
|
Pdf,
|
||||||
|
ShivaDocument(ShivaFormat),
|
||||||
|
Font,
|
||||||
|
Mp3(Mp3Format),
|
||||||
|
Wav(WavFormat),
|
||||||
|
Flac(FlacFormat),
|
||||||
|
Text,
|
||||||
|
ImageLuma8(Dimensions),
|
||||||
|
ImageLumaA8(Dimensions),
|
||||||
|
ImageRgb8(Dimensions),
|
||||||
|
ImageRgba8(Dimensions),
|
||||||
|
ImageLuma16(Dimensions),
|
||||||
|
ImageLumaA16(Dimensions),
|
||||||
|
ImageRgb16(Dimensions),
|
||||||
|
ImageRgba16(Dimensions),
|
||||||
|
ImageRgb32F(Dimensions),
|
||||||
|
ImageRgba32F(Dimensions),
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[derive(Clone)]
|
||||||
|
// pub(crate) enum Command {
|
||||||
|
// Into(Bendable),
|
||||||
|
// Process(Process),
|
||||||
|
// }
|
||||||
|
|
||||||
|
#[derive(EnumString, AsRefStr, Clone)]
|
||||||
|
pub(crate) enum Bendable {
|
||||||
|
#[strum(serialize = "bin", serialize = "binary")]
|
||||||
|
Binary,
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[derive(Clone)]
|
||||||
|
// pub(crate) enum Process {}
|
547
bong/src/main.rs
547
bong/src/main.rs
|
@ -1,3 +1,546 @@
|
||||||
fn main() {
|
use std::{
|
||||||
println!("Hello, world!");
|
borrow::Cow,
|
||||||
|
fs::File,
|
||||||
|
io::{Cursor, Write},
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::{anyhow, bail, Context};
|
||||||
|
use bingus::{
|
||||||
|
doc::{printpdf::PdfDocument, ShivaDocument},
|
||||||
|
dynamic::{self, DynamicBendable},
|
||||||
|
fnt::Font,
|
||||||
|
img::{
|
||||||
|
GrayAlphaImage, GrayImage, Image, Luma, LumaA, Rgb, Rgb32FImage, RgbImage, Rgba,
|
||||||
|
Rgba32FImage, RgbaImage,
|
||||||
|
},
|
||||||
|
snd::{conv::IntoSample, sample::i24, RawSamples},
|
||||||
|
txt::Text,
|
||||||
|
Bendable, Bytes, FromDataBytes, IntoDataBytes, TryFromDataBytes,
|
||||||
|
};
|
||||||
|
use clap::Parser;
|
||||||
|
use cli::Cli;
|
||||||
|
use flacenc::{component::BitRepr, error::Verify};
|
||||||
|
use hound::{WavSpec, WavWriter};
|
||||||
|
use mp3lame_encoder::{Builder as Mp3EncoderBuilder, DualPcm, FlushGap};
|
||||||
|
use unwrap_infallible::UnwrapInfallible;
|
||||||
|
|
||||||
|
use crate::cli::{FlacFormat, Mp3Format, New, Stdout, WavFormat};
|
||||||
|
|
||||||
|
mod cli;
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
let Cli {
|
||||||
|
input,
|
||||||
|
output_format,
|
||||||
|
output,
|
||||||
|
} = Cli::parse();
|
||||||
|
let input_bytes = {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
input
|
||||||
|
.reader()
|
||||||
|
.and_then(|mut r| r.read_to_end(&mut buf))
|
||||||
|
.context("reading input")?;
|
||||||
|
buf
|
||||||
|
};
|
||||||
|
let mut bytes = match dynamic::open(&mut Cursor::new(input_bytes.clone()))
|
||||||
|
.context("guessing media type / loading media")?
|
||||||
|
.or(String::from_utf8(input_bytes.clone())
|
||||||
|
.ok()
|
||||||
|
.map(Cow::Owned)
|
||||||
|
.map(DynamicBendable::Text))
|
||||||
|
.unwrap_or(DynamicBendable::Binary(input_bytes))
|
||||||
|
{
|
||||||
|
dynamic::DynamicBendable::Image(dynamic_image) => {
|
||||||
|
Bytes::bend_from(dynamic_image, (), Default::default())
|
||||||
|
}
|
||||||
|
dynamic::DynamicBendable::Binary(b) => Bytes::bend_from(b, (), Default::default()),
|
||||||
|
dynamic::DynamicBendable::Sound(audio) => Bytes::bend_from(audio, (), Default::default()),
|
||||||
|
dynamic::DynamicBendable::Text(cow) => Bytes::bend_from(cow, (), Default::default()),
|
||||||
|
dynamic::DynamicBendable::Doc(shiva_document) => {
|
||||||
|
Bytes::bend_from(shiva_document, (), Default::default())
|
||||||
|
}
|
||||||
|
dynamic::DynamicBendable::Archive(pdf_document) => {
|
||||||
|
Bytes::bend_from(pdf_document, (), Default::default())
|
||||||
|
}
|
||||||
|
dynamic::DynamicBendable::Meta => {
|
||||||
|
unimplemented!("this is not supposed to happen...")
|
||||||
|
}
|
||||||
|
dynamic::DynamicBendable::Font(font) => Bytes::bend_from(font, (), Default::default()),
|
||||||
|
}
|
||||||
|
.context("bending input into bytes")?;
|
||||||
|
match output {
|
||||||
|
cli::FileOrStd::File(file) => {
|
||||||
|
match output_format {
|
||||||
|
cli::OutputFormat::Bytes => {
|
||||||
|
File::bend_from(bytes, Box::new(file), Default::default())
|
||||||
|
.context("writing to file")?;
|
||||||
|
}
|
||||||
|
cli::OutputFormat::Pdf => {
|
||||||
|
File::bend_from(
|
||||||
|
PdfDocument::bend_from(bytes, (), Default::default())
|
||||||
|
.map_err(|s| anyhow!("{}", s))
|
||||||
|
.context("bending into PDF document")?
|
||||||
|
.into_data_bytes(),
|
||||||
|
Box::new(file),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.context("writing PDF to output file")?;
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ShivaDocument(shiva_format) => {
|
||||||
|
File::bend_from(
|
||||||
|
ShivaDocument::bend_from(bytes, shiva_format.into(), Default::default())
|
||||||
|
.context("bending into a Shiva Document")?,
|
||||||
|
Box::new(file),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.context("writing Shiva Document to output file")?;
|
||||||
|
}
|
||||||
|
cli::OutputFormat::Font => {
|
||||||
|
File::bend_from(
|
||||||
|
Font::bend_from(bytes, (), Default::default())
|
||||||
|
.context("bending to font")?
|
||||||
|
.into_data_bytes(),
|
||||||
|
Box::new(file),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.context("writing font to output file")?;
|
||||||
|
}
|
||||||
|
cli::OutputFormat::Mp3(mp3_format) => {
|
||||||
|
eprintln!("warning: Rust MP3 lame encoder is freaky!");
|
||||||
|
let Mp3Format {
|
||||||
|
bitrate,
|
||||||
|
quality,
|
||||||
|
sample_rate,
|
||||||
|
sample_format,
|
||||||
|
} = mp3_format;
|
||||||
|
let buff = {
|
||||||
|
use mp3lame_encoder::{Bitrate::*, Quality::*};
|
||||||
|
let mut encoder = Mp3EncoderBuilder::new()
|
||||||
|
.context("Failed to create MP3 encoder builder")?;
|
||||||
|
encoder
|
||||||
|
.set_num_channels(1)
|
||||||
|
.context("Failed to set MP3 encoder channels")?;
|
||||||
|
encoder
|
||||||
|
.set_sample_rate(sample_rate)
|
||||||
|
.context("Failed to set MP3 encoder sample rate")?;
|
||||||
|
encoder
|
||||||
|
.set_brate(match bitrate {
|
||||||
|
8 => Kbps8,
|
||||||
|
16 => Kbps16,
|
||||||
|
24 => Kbps24,
|
||||||
|
32 => Kbps32,
|
||||||
|
40 => Kbps40,
|
||||||
|
48 => Kbps48,
|
||||||
|
64 => Kbps64,
|
||||||
|
80 => Kbps80,
|
||||||
|
96 => Kbps96,
|
||||||
|
112 => Kbps112,
|
||||||
|
128 => Kbps128,
|
||||||
|
160 => Kbps160,
|
||||||
|
192 => Kbps192,
|
||||||
|
224 => Kbps224,
|
||||||
|
256 => Kbps256,
|
||||||
|
320 => Kbps320,
|
||||||
|
_ => bail!("invalid MP3 bitrate"),
|
||||||
|
})
|
||||||
|
.context("Failed to set MP3 encoder bitrate")?;
|
||||||
|
encoder
|
||||||
|
.set_quality(match quality {
|
||||||
|
0 => Best,
|
||||||
|
1 => SecondBest,
|
||||||
|
2 => NearBest,
|
||||||
|
3 => VeryNice,
|
||||||
|
4 => Nice,
|
||||||
|
5 => Good,
|
||||||
|
6 => Decent,
|
||||||
|
7 => Ok,
|
||||||
|
8 => SecondWorst,
|
||||||
|
9 => Worst,
|
||||||
|
_ => bail!("invalid MP3 quality"),
|
||||||
|
})
|
||||||
|
.context("Failed to set MP3 encoder quality")?;
|
||||||
|
|
||||||
|
let mut encoder = encoder
|
||||||
|
.build()
|
||||||
|
.context("Failed to initialize MP3 encoder")?;
|
||||||
|
|
||||||
|
let (encoded_size, mut output) = {
|
||||||
|
match sample_format {
|
||||||
|
cli::Mp3SampleFormat::U16 => {
|
||||||
|
let (left, right): (Vec<_>, Vec<_>) =
|
||||||
|
RawSamples::<u16>::bend_from(bytes, (), Default::default())
|
||||||
|
.unwrap()
|
||||||
|
.into_inner()
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|c| (c[0], c[1]))
|
||||||
|
.unzip();
|
||||||
|
let input = DualPcm {
|
||||||
|
left: &left,
|
||||||
|
right: &right,
|
||||||
|
};
|
||||||
|
let mut output = Vec::with_capacity(
|
||||||
|
mp3lame_encoder::max_required_buffer_size(
|
||||||
|
left.len() + right.len(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
(
|
||||||
|
encoder
|
||||||
|
.encode(input, output.spare_capacity_mut())
|
||||||
|
.context("Failed MP3 encoding")?,
|
||||||
|
output,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cli::Mp3SampleFormat::I16 => {
|
||||||
|
let (left, right): (Vec<_>, Vec<_>) =
|
||||||
|
RawSamples::<i16>::bend_from(bytes, (), Default::default())
|
||||||
|
.unwrap()
|
||||||
|
.into_inner()
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|c| (c[0], c[1]))
|
||||||
|
.unzip();
|
||||||
|
let input = DualPcm {
|
||||||
|
left: &left,
|
||||||
|
right: &right,
|
||||||
|
};
|
||||||
|
let mut output = Vec::with_capacity(
|
||||||
|
mp3lame_encoder::max_required_buffer_size(
|
||||||
|
left.len() + right.len(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
(
|
||||||
|
encoder
|
||||||
|
.encode(input, output.spare_capacity_mut())
|
||||||
|
.context("Failed MP3 encoding")?,
|
||||||
|
output,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cli::Mp3SampleFormat::I32 => {
|
||||||
|
let (left, right): (Vec<_>, Vec<_>) =
|
||||||
|
RawSamples::<i32>::bend_from(bytes, (), Default::default())
|
||||||
|
.unwrap()
|
||||||
|
.into_inner()
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|c| (c[0], c[1]))
|
||||||
|
.unzip();
|
||||||
|
let input = DualPcm {
|
||||||
|
left: &left,
|
||||||
|
right: &right,
|
||||||
|
};
|
||||||
|
let mut output = Vec::with_capacity(
|
||||||
|
mp3lame_encoder::max_required_buffer_size(
|
||||||
|
left.len() + right.len(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
(
|
||||||
|
encoder
|
||||||
|
.encode(input, output.spare_capacity_mut())
|
||||||
|
.context("Failed MP3 encoding")?,
|
||||||
|
output,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cli::Mp3SampleFormat::F32 => {
|
||||||
|
let (left, right): (Vec<_>, Vec<_>) =
|
||||||
|
RawSamples::<f32>::bend_from(bytes, (), Default::default())
|
||||||
|
.unwrap()
|
||||||
|
.into_inner()
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|c| (c[0], c[1]))
|
||||||
|
.unzip();
|
||||||
|
let input = DualPcm {
|
||||||
|
left: &left,
|
||||||
|
right: &right,
|
||||||
|
};
|
||||||
|
let mut output = Vec::with_capacity(
|
||||||
|
mp3lame_encoder::max_required_buffer_size(
|
||||||
|
left.len() + right.len(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
(
|
||||||
|
encoder
|
||||||
|
.encode(input, output.spare_capacity_mut())
|
||||||
|
.context("Failed MP3 encoding")?,
|
||||||
|
output,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cli::Mp3SampleFormat::F64 => {
|
||||||
|
let (left, right): (Vec<_>, Vec<_>) =
|
||||||
|
RawSamples::<f64>::bend_from(bytes, (), Default::default())
|
||||||
|
.unwrap()
|
||||||
|
.into_inner()
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|c| (c[0], c[1]))
|
||||||
|
.unzip();
|
||||||
|
let input = DualPcm {
|
||||||
|
left: &left,
|
||||||
|
right: &right,
|
||||||
|
};
|
||||||
|
let mut output = Vec::with_capacity(
|
||||||
|
mp3lame_encoder::max_required_buffer_size(
|
||||||
|
left.len() + right.len(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
(
|
||||||
|
encoder
|
||||||
|
.encode(input, output.spare_capacity_mut())
|
||||||
|
.context("Failed MP3 encoding")?,
|
||||||
|
output,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
unsafe {
|
||||||
|
output.set_len(output.len().wrapping_add(encoded_size));
|
||||||
|
}
|
||||||
|
let encoded_size =
|
||||||
|
encoder
|
||||||
|
.flush::<FlushGap>(output.spare_capacity_mut())
|
||||||
|
.context("Failed MP3 flushing (don't know what that means)")?;
|
||||||
|
unsafe {
|
||||||
|
output.set_len(output.len().wrapping_add(encoded_size));
|
||||||
|
}
|
||||||
|
output
|
||||||
|
};
|
||||||
|
File::bend_from(buff, Box::new(file), Default::default())
|
||||||
|
.context("writing MP3 to output file")?;
|
||||||
|
}
|
||||||
|
cli::OutputFormat::Wav(wav_format) => {
|
||||||
|
let WavFormat {
|
||||||
|
sample_rate,
|
||||||
|
sample_format,
|
||||||
|
} = wav_format;
|
||||||
|
match sample_format {
|
||||||
|
cli::WavSampleFormat::I8 => {
|
||||||
|
let samples =
|
||||||
|
RawSamples::<i8>::bend_from(bytes, (), Default::default())
|
||||||
|
.unwrap_infallible();
|
||||||
|
let mut buff =
|
||||||
|
Cursor::new(Vec::with_capacity(samples.as_ref().len() * 8));
|
||||||
|
{
|
||||||
|
let mut writer = WavWriter::new(
|
||||||
|
&mut buff,
|
||||||
|
WavSpec {
|
||||||
|
channels: 1,
|
||||||
|
sample_rate,
|
||||||
|
bits_per_sample: i8::BITS as u16,
|
||||||
|
sample_format: sample_format.into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.context("Failed to create WAV writer")?;
|
||||||
|
for sample in samples.as_ref() {
|
||||||
|
writer.write_sample(*sample)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
File::try_from_data_bytes(
|
||||||
|
buff.into_inner(),
|
||||||
|
Box::new(file),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.context("writing WAV data to output file")?
|
||||||
|
}
|
||||||
|
cli::WavSampleFormat::I16 => {
|
||||||
|
let samples =
|
||||||
|
RawSamples::<i16>::bend_from(bytes, (), Default::default())
|
||||||
|
.unwrap_infallible();
|
||||||
|
let mut buff =
|
||||||
|
Cursor::new(Vec::with_capacity(samples.as_ref().len() * 8));
|
||||||
|
{
|
||||||
|
let mut writer = WavWriter::new(
|
||||||
|
&mut buff,
|
||||||
|
WavSpec {
|
||||||
|
channels: 1,
|
||||||
|
sample_rate,
|
||||||
|
bits_per_sample: i16::BITS as u16,
|
||||||
|
sample_format: sample_format.into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.context("Failed to create WAV writer")?;
|
||||||
|
for sample in samples.as_ref() {
|
||||||
|
writer.write_sample(*sample)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
File::try_from_data_bytes(
|
||||||
|
buff.into_inner(),
|
||||||
|
Box::new(file),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.context("writing WAV data to output file")?
|
||||||
|
}
|
||||||
|
cli::WavSampleFormat::I32 => {
|
||||||
|
let samples =
|
||||||
|
RawSamples::<i32>::bend_from(bytes, (), Default::default())
|
||||||
|
.unwrap_infallible();
|
||||||
|
let mut buff =
|
||||||
|
Cursor::new(Vec::with_capacity(samples.as_ref().len() * 8));
|
||||||
|
{
|
||||||
|
let mut writer = WavWriter::new(
|
||||||
|
&mut buff,
|
||||||
|
WavSpec {
|
||||||
|
channels: 1,
|
||||||
|
sample_rate,
|
||||||
|
bits_per_sample: i32::BITS as u16,
|
||||||
|
sample_format: sample_format.into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.context("Failed to create WAV writer")?;
|
||||||
|
for sample in samples.as_ref() {
|
||||||
|
writer.write_sample(*sample)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
File::try_from_data_bytes(
|
||||||
|
buff.into_inner(),
|
||||||
|
Box::new(file),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.context("writing WAV data to output file")?
|
||||||
|
}
|
||||||
|
cli::WavSampleFormat::F32 => {
|
||||||
|
let samples =
|
||||||
|
RawSamples::<f32>::bend_from(bytes, (), Default::default())
|
||||||
|
.unwrap_infallible();
|
||||||
|
let mut buff =
|
||||||
|
Cursor::new(Vec::with_capacity(samples.as_ref().len() * 8));
|
||||||
|
{
|
||||||
|
let mut writer = WavWriter::new(
|
||||||
|
&mut buff,
|
||||||
|
WavSpec {
|
||||||
|
channels: 1,
|
||||||
|
sample_rate,
|
||||||
|
bits_per_sample: 32,
|
||||||
|
sample_format: sample_format.into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.context("Failed to create WAV writer")?;
|
||||||
|
for sample in samples.as_ref() {
|
||||||
|
writer.write_sample(*sample)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
File::try_from_data_bytes(
|
||||||
|
buff.into_inner(),
|
||||||
|
Box::new(file),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.context("writing WAV data to output file")?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
cli::OutputFormat::Flac(flac_format) => {
|
||||||
|
let FlacFormat { sample_rate, bps } = flac_format;
|
||||||
|
let config = flacenc::config::Encoder::default()
|
||||||
|
.into_verified()
|
||||||
|
.expect("Config data error.");
|
||||||
|
let samples = RawSamples::<i32>::bend_from(bytes, (), Default::default())
|
||||||
|
.unwrap_infallible();
|
||||||
|
let source = flacenc::source::MemSource::from_samples(
|
||||||
|
samples.as_ref().iter().copied()
|
||||||
|
.map(|sample| match bps {
|
||||||
|
8 => IntoSample::<i8>::into_sample(sample) as i32,
|
||||||
|
16 => IntoSample::<i16>::into_sample(sample) as i32,
|
||||||
|
24 => IntoSample::<i24>::into_sample(sample).inner(),
|
||||||
|
_=> unimplemented!("sorry, the current implementation for the flac encoder doesn't support any other bitrate than 8, 16 or 24.")
|
||||||
|
})
|
||||||
|
.collect::<Vec<i32>>()
|
||||||
|
.as_slice(),
|
||||||
|
1,
|
||||||
|
bps,
|
||||||
|
sample_rate,
|
||||||
|
);
|
||||||
|
let flac_stream =
|
||||||
|
flacenc::encode_with_fixed_block_size(&config, source, config.block_size)
|
||||||
|
.expect("Encode failed.");
|
||||||
|
|
||||||
|
// `Stream` imlpements `BitRepr` so you can obtain the encoded stream via
|
||||||
|
// `ByteSink` struct that implements `BitSink`.
|
||||||
|
let mut sink = flacenc::bitsink::ByteSink::new();
|
||||||
|
flac_stream
|
||||||
|
.write(&mut sink)
|
||||||
|
.context("Failed to write samples to FLAC byte sink")?;
|
||||||
|
File::try_from_data_bytes(
|
||||||
|
sink.into_inner(),
|
||||||
|
Box::new(file),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.context("writing FLAC data to output file")?;
|
||||||
|
}
|
||||||
|
cli::OutputFormat::Text => {
|
||||||
|
File::bend_from(
|
||||||
|
Text::try_from_data_bytes(bytes, (), Default::default())
|
||||||
|
.context("invalid UTF-8")?,
|
||||||
|
Box::new(file),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.context("writing text to output file")?;
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ImageLuma8(dimensions) => {
|
||||||
|
GrayImage::from_data_bytes(bytes, dimensions.into(), Default::default())
|
||||||
|
.save(file)
|
||||||
|
.context("writing image data to output file")?
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ImageLumaA8(dimensions) => {
|
||||||
|
GrayAlphaImage::from_data_bytes(bytes, dimensions.into(), Default::default())
|
||||||
|
.save(file)
|
||||||
|
.context("writing image data to output file")?
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ImageRgb8(dimensions) => {
|
||||||
|
RgbImage::from_data_bytes(bytes, dimensions.into(), Default::default())
|
||||||
|
.save(file)
|
||||||
|
.context("writing image data to output file")?
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ImageRgba8(dimensions) => {
|
||||||
|
RgbaImage::from_data_bytes(bytes, dimensions.into(), Default::default())
|
||||||
|
.save(file)
|
||||||
|
.context("writing image data to output file")?
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ImageLuma16(dimensions) => {
|
||||||
|
Image::<Luma<u16>, Vec<u16>>::from_data_bytes(
|
||||||
|
bytes,
|
||||||
|
dimensions.into(),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.save(file)
|
||||||
|
.context("writing image data to output file")?
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ImageLumaA16(dimensions) => {
|
||||||
|
Image::<LumaA<u16>, Vec<u16>>::from_data_bytes(
|
||||||
|
bytes,
|
||||||
|
dimensions.into(),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.save(file)
|
||||||
|
.context("writing image data to output file")?
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ImageRgb16(dimensions) => {
|
||||||
|
Image::<Rgb<u16>, Vec<u16>>::from_data_bytes(
|
||||||
|
bytes,
|
||||||
|
dimensions.into(),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.save(file)
|
||||||
|
.context("writing image data to output file")?
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ImageRgba16(dimensions) => {
|
||||||
|
Image::<Rgba<u16>, Vec<u16>>::from_data_bytes(
|
||||||
|
bytes,
|
||||||
|
dimensions.into(),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.save(file)
|
||||||
|
.context("writing image data to output file")?
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ImageRgb32F(dimensions) => {
|
||||||
|
Rgb32FImage::from_data_bytes(bytes, dimensions.into(), Default::default())
|
||||||
|
.save(file)
|
||||||
|
.context("writing image data to output file")?
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ImageRgba32F(dimensions) => {
|
||||||
|
Rgba32FImage::from_data_bytes(bytes, dimensions.into(), Default::default())
|
||||||
|
.save(file)
|
||||||
|
.context("writing image data to output file")?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
cli::FileOrStd::Std(_) => Stdout::new()
|
||||||
|
.write_all(&mut bytes)
|
||||||
|
.context("writing to stdout")?,
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 24 MiB After Width: | Height: | Size: 24 MiB |
Before Width: | Height: | Size: 799 KiB After Width: | Height: | Size: 799 KiB |
BIN
testing material/doc/pdf/basic-text.pdf
Normal file
BIN
testing material/doc/pdf/basic-text.pdf
Normal file
Binary file not shown.
BIN
testing material/doc/pdf/rights.pdf
Normal file
BIN
testing material/doc/pdf/rights.pdf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
testing material/sound/file_example_OOG_1MG.ogg
Normal file
BIN
testing material/sound/file_example_OOG_1MG.ogg
Normal file
Binary file not shown.
BIN
testing material/sound/file_example_WAV_1MG.wav
Normal file
BIN
testing material/sound/file_example_WAV_1MG.wav
Normal file
Binary file not shown.
BIN
testing material/sound/sample-3s.mp3
Normal file
BIN
testing material/sound/sample-3s.mp3
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue