From e4dddb4aba0106f0a895eb3a02c023a114ab420c Mon Sep 17 00:00:00 2001 From: brevalferrari Date: Fri, 20 Jun 2025 10:55:48 +0200 Subject: [PATCH] complete CLI for input & output to a different format (MP3 lame is freaky) --- .gitignore | 1 + bingus/Cargo.toml | 3 +- bingus/src/snd/raw.rs | 10 +- bong/Cargo.toml | 5 +- bong/src/cli.rs | 213 ++++++++++++----- bong/src/main.rs | 539 +++++++++++++++++++++++++++++++++++++++++- 6 files changed, 700 insertions(+), 71 deletions(-) diff --git a/.gitignore b/.gitignore index 3ddfb97..9a588e0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.lock bmp/out.* fonts +test.* \ No newline at end of file diff --git a/bingus/Cargo.toml b/bingus/Cargo.toml index fbba639..824c2e8 100644 --- a/bingus/Cargo.toml +++ b/bingus/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bingus" -version = "0.5.1" +version = "0.6.0" edition.workspace = true license.workspace = true description.workspace = true @@ -16,7 +16,6 @@ rayon = { version = "1", optional = true } infer = "0" thiserror = "2" derive-new = "0" -strum = { version = "0.27", features = ["derive"] } derive_wrapper = "0.1" symphonia = { version = "0.5", features = ["all"], optional = true } printpdf = { version = "0.8.2", features = [ diff --git a/bingus/src/snd/raw.rs b/bingus/src/snd/raw.rs index 4ef1bd4..e82250e 100644 --- a/bingus/src/snd/raw.rs +++ b/bingus/src/snd/raw.rs @@ -1,4 +1,4 @@ -use std::borrow::Cow; +use std::{borrow::Cow, convert::Infallible}; use super::sample::Sample; use cfg_if::cfg_if; @@ -17,6 +17,12 @@ 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, @@ -34,7 +40,7 @@ where T: Sample + FromBytes + ToBytes + Zero, ::Bytes: Sized + for<'a> TryFrom<&'a [u8]>, { - type Error = (); + type Error = Infallible; type Format = (); fn try_from_data_bytes( bytes: crate::Bytes, diff --git a/bong/Cargo.toml b/bong/Cargo.toml index f4350e0..0513948 100644 --- a/bong/Cargo.toml +++ b/bong/Cargo.toml @@ -17,4 +17,7 @@ strum = { version = "*", features = ["derive"] } derive-new = "*" clap = { version = "4.5.40", features = ["derive"] } derive_wrapper = "0.1.7" -# unwrap-infallible = "0.1" +unwrap-infallible = "0.1" +mp3lame-encoder = { version = "0.2", features = ["std"] } +hound = "3.5.1" +flacenc = "0.4.0" diff --git a/bong/src/cli.rs b/bong/src/cli.rs index c8cbed0..5d311da 100644 --- a/bong/src/cli.rs +++ b/bong/src/cli.rs @@ -1,56 +1,131 @@ use std::{ - io::{self, stdin, stdout}, - ops::Not, + 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 derive_wrapper::AsRef; -use strum::{AsRefStr, EnumIter, EnumString}; +use strum::{AsRefStr, Display, EnumString}; #[derive(new, Parser)] #[clap(version, author, about)] /// CLI from the Bent project -pub(super) struct Cli { - /// Input format (leave empty for auto mode) - #[arg(short, long)] - pub(super) input_format: Option, +pub(crate) struct Cli { /// Input file or standard input ("-"). - pub(super) input: FileOrStd, + pub(crate) input: FileOrStd, // /// List of commands (process or bend to another format) - // pub(super) commands: Vec, + // pub(crate) commands: Vec, + #[command(subcommand)] + pub(crate) output_format: OutputFormat, /// Onput file or standard output ("-"). - pub(super) output: FileOrStd, + pub(crate) output: FileOrStd, } -#[derive(AsRef)] +#[derive(Clone, Copy, Parser)] #[cfg_attr(debug_assertions, derive(Debug))] -struct File(std::fs::File); +pub struct Dimensions { + pub width: u32, + pub height: u32, +} -impl Clone for File { - fn clone(&self) -> Self { - File( - self.0 - .try_clone() - .expect("can't clone this file's handle :("), - ) +#[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 { + 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 for hound::SampleFormat { + fn from(value: WavSampleFormat) -> Self { + match value { + WavSampleFormat::F32 => hound::SampleFormat::Float, + _ => hound::SampleFormat::Int, + } } } -impl FromStr for File { - type Err = io::Error; - fn from_str(s: &str) -> Result { - std::fs::File::open(s).map(File) +impl From for bingus::img::Dimensions { + fn from(value: Dimensions) -> Self { + Self { + width: value.width, + height: value.height, + } + } +} + +impl From for bingus::doc::ShivaFormat { + fn from(value: ShivaFormat) -> Self { + Self::new(value.doc_input_format, value.doc_output_format) } } #[derive(Clone, Copy, Default)] -struct Stdin; +pub(crate) struct Stdin; #[derive(Clone, Copy, Default)] -struct Stdout; +pub(crate) struct Stdout; -trait New { +pub(crate) trait New { type Real; fn new() -> Self::Real; } @@ -70,61 +145,69 @@ impl New for Stdout { } impl FromStr for FileOrStd { - type Err = io::Error; + type Err = Infallible; fn from_str(s: &str) -> Result { - s.is_empty() - .not() - .then_some(s.parse().map(FileOrStd::File)) - .unwrap_or(Ok(FileOrStd::Std(Default::default()))) + if s == "-" { + Ok(FileOrStd::Std(Default::default())) + } else { + s.parse().map(FileOrStd::File) + } } } #[derive(Clone)] #[cfg_attr(debug_assertions, derive(Debug))] -pub(super) enum FileOrStd { - File(File), +pub(crate) enum FileOrStd { + File(PathBuf), Std(S), } -pub(super) enum OutputFormat {} +impl<'a, S> FileOrStd +where + S: Clone + New, + ::Real: Read + 'a, +{ + pub(crate) fn reader(self) -> Result, 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(super) enum Command { +// pub(crate) enum Command { // Into(Bendable), // Process(Process), // } #[derive(EnumString, AsRefStr, Clone)] -pub(super) enum Bendable { +pub(crate) enum Bendable { #[strum(serialize = "bin", serialize = "binary")] Binary, } -#[derive(Clone)] -pub(super) enum Process {} - -#[derive(EnumString, EnumIter, Clone, Copy)] -enum MatcherType { - Audio, - Doc, - Font, - Image, - Pdf, - Text, - Raw, -} - -impl From for bingus::MatcherType { - fn from(value: MatcherType) -> Self { - use bingus::MatcherType::*; - match value { - MatcherType::Audio => Audio, - MatcherType::Doc => Doc, - MatcherType::Font => Font, - MatcherType::Image => Image, - MatcherType::Pdf => Archive, - MatcherType::Text => Text, - MatcherType::Raw => Custom, - } - } -} +// #[derive(Clone)] +// pub(crate) enum Process {} diff --git a/bong/src/main.rs b/bong/src/main.rs index fe46634..5292fe7 100644 --- a/bong/src/main.rs +++ b/bong/src/main.rs @@ -1,9 +1,546 @@ +use std::{ + 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<()> { - Cli::parse(); + 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::::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::::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::::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::::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::::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::(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::::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::::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::::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::::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::::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::::into_sample(sample) as i32, + 16 => IntoSample::::into_sample(sample) as i32, + 24 => IntoSample::::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::>() + .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::, Vec>::from_data_bytes( + bytes, + dimensions.into(), + Default::default(), + ) + .save(file) + .context("writing image data to output file")? + } + cli::OutputFormat::ImageLumaA16(dimensions) => { + Image::, Vec>::from_data_bytes( + bytes, + dimensions.into(), + Default::default(), + ) + .save(file) + .context("writing image data to output file")? + } + cli::OutputFormat::ImageRgb16(dimensions) => { + Image::, Vec>::from_data_bytes( + bytes, + dimensions.into(), + Default::default(), + ) + .save(file) + .context("writing image data to output file")? + } + cli::OutputFormat::ImageRgba16(dimensions) => { + Image::, Vec>::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(()) }