Compare commits

..
3 changed files with 147 additions and 196 deletions

View file

@ -85,12 +85,10 @@
] ]
++ lib.optional pkgs.stdenv.hostPlatform.isLinux pkgs.makeBinaryWrapper; ++ lib.optional pkgs.stdenv.hostPlatform.isLinux pkgs.makeBinaryWrapper;
buildInputs = buildInputs = commonArgs.buildInputs ++ [
commonArgs.buildInputs
++ [
pkgs.libgcc pkgs.libgcc
] pkgs.zenity
++ lib.optional pkgs.stdenv.hostPlatform.isLinux pkgs.zenity; ];
runtimeDependencies = dlDeps; runtimeDependencies = dlDeps;

View file

@ -1,13 +1,13 @@
use std::{env, path::Path}; use std::{env, ffi::OsStr, fs, io::Write, path::Path};
use iced::{ use iced::{
Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, color, event, Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, color, event,
keyboard::{self, Key, key}, keyboard::{self, Key, key},
theme, widget, window, theme, widget, window,
}; };
use image::{EncodableLayout, RgbaImage, imageops}; use image::{DynamicImage, EncodableLayout, ImageDecoder, ImageReader, RgbaImage, imageops};
use jpegxl_rs::image::ToDynamic;
mod utils; use rfd::FileDialog;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
enum Message { enum Message {
@ -27,11 +27,7 @@ impl State {
fn new() -> (Self, Task<Message>) { fn new() -> (Self, Task<Message>) {
let mut state = State::default(); let mut state = State::default();
state.error = env::args().nth(1).and_then(|path| { state.error = env::args().nth(1).and_then(|path| state.load_image(path));
utils::load_image(path)
.map(|image| state.image = Some(image))
.err()
});
let allocate_image = state.allocate_image(); let allocate_image = state.allocate_image();
(state, allocate_image) (state, allocate_image)
@ -80,7 +76,8 @@ impl State {
} }
Key::Character("o") => { Key::Character("o") => {
return self.pick_and_load_image(); self.error = self.open_image();
return self.allocate_image();
} }
Key::Character("r") => { Key::Character("r") => {
@ -105,7 +102,7 @@ impl State {
} }
Key::Character("s") => { Key::Character("s") => {
self.save_image(); self.error = self.save_image();
} }
Key::Character("q") => return window::latest().and_then(window::close), Key::Character("q") => return window::latest().and_then(window::close),
@ -114,15 +111,6 @@ impl State {
_ => {} _ => {}
}, },
Message::Event(iced::Event::Window(window_event)) => match window_event {
window::Event::FileDropped(file) => {
return self.load_image(file);
}
// ignore unused window events
_ => {}
},
// ignore unused events // ignore unused events
Message::Event(_) => {} Message::Event(_) => {}
} }
@ -130,35 +118,151 @@ impl State {
Task::none() Task::none()
} }
#[must_use] fn load_image(&mut self, path: impl AsRef<Path>) -> Option<String> {
fn load_image(&mut self, path: impl AsRef<Path>) -> Task<Message> { let path = path.as_ref();
let task;
(self.error, task) = match utils::load_image(path) { let decoded_image = match path.extension().map(OsStr::to_string_lossy).as_deref() {
Ok(image) => { Some("jxl") => {
self.image = Some(image); let data = match std::fs::read(path) {
(None, self.allocate_image()) Ok(d) => d,
Err(e) => {
return Some(format!(
"failed to read data from '{}': {e}",
path.display()
));
} }
Err(e) => (Some(e), Task::none()),
}; };
task match jpegxl_rs::decoder_builder()
.build()
.unwrap()
.decode_to_image(&data)
{
Ok(Some(decoded_image)) => decoded_image,
Ok(None) => {
return Some(format!(
"failed to convert jxl '{}' to DynamicImage",
path.display()
));
}
Err(e) => {
return Some(format!("failed to decode jxl '{}': {e}", path.display()));
}
}
}
_ => {
let reader = match ImageReader::open(path) {
Ok(r) => r,
Err(e) => {
return Some(format!(
"failed to create reader for '{}': {e}",
path.display()
));
} }
#[must_use]
fn pick_and_load_image(&mut self) -> Task<Message> {
let task;
(self.error, task) = match utils::pick_image() {
Ok(path) => (None, self.load_image(path)),
Err(e) => (Some(e), Task::none()),
}; };
task let mut decoder = match reader.into_decoder() {
Ok(d) => d,
Err(e) => {
return Some(format!(
"failed to create decoder for '{}': {e}",
path.display()
));
}
};
let oreintation = match decoder.orientation() {
Ok(o) => o,
Err(e) => {
return Some(format!(
"failed to get oreintation of '{}': {e}",
path.display()
));
}
};
let mut decoded_image = match DynamicImage::from_decoder(decoder) {
Ok(i) => i,
Err(e) => {
return Some(format!("failed to decode image '{}': {e}", path.display()));
}
};
decoded_image.apply_orientation(oreintation);
decoded_image
}
};
self.image = Some(decoded_image.into_rgba8());
None
}
fn open_image(&mut self) -> Option<String> {
let Some(path) = FileDialog::new()
.add_filter(
"image",
&[
"png", "PNG", "jpg", "JPG", "jpeg", "JPEG", "avif", "jxl", "bmp", "exr", "ff",
"gif", "hdr", "ico", "pnm", "qoi", "tga", "tiff", "webp",
],
)
.pick_file()
else {
return Some("no path to open provided".into());
};
self.load_image(path)
} }
fn save_image(&mut self) { fn save_image(&self) -> Option<String> {
self.error = utils::save_image(self.image.as_ref()); let Some(image) = self.image.as_ref() else {
return Some("no image to save".into());
};
let Some(path) = FileDialog::new().save_file() else {
return Some("no path to save provided".into());
};
let mut file = match fs::File::create(&path) {
Ok(f) => f,
Err(e) => return Some(format!("failed to create file '{}': {e}", path.display())),
};
match path.extension().map(OsStr::to_string_lossy).as_deref() {
Some("jxl") => {
let mut encoder = jpegxl_rs::encoder_builder()
.speed(jpegxl_rs::encode::EncoderSpeed::Glacier)
.lossless(true)
.build()
.unwrap();
let rgb_image = DynamicImage::from(image.to_owned()).into_rgb8();
let jxl = match encoder.encode::<u8, u8>(
&rgb_image,
rgb_image.width(),
rgb_image.height(),
) {
Ok(j) => j,
Err(e) => {
return Some(format!("failed to encode jxl '{}': {e}", path.display()));
}
};
if let Err(e) = file.write(&jxl.data) {
return Some(format!("failed to write jxl to '{}': {e}", path.display()));
};
}
_ => {
if let Err(e) = image.save(&path) {
return Some(format!("failed to save '{}': {e}", path.display()));
};
}
};
None
} }
#[must_use]
fn allocate_image(&self) -> Task<Message> { fn allocate_image(&self) -> Task<Message> {
let Some(image) = self.image.as_ref() else { let Some(image) = self.image.as_ref() else {
eprintln!("no image to allocate"); eprintln!("no image to allocate");

View file

@ -1,151 +0,0 @@
use std::{
ffi::OsStr,
fs,
io::Write,
path::{Path, PathBuf},
};
use image::{DynamicImage, ImageDecoder, ImageReader, RgbaImage};
use jpegxl_rs::image::ToDynamic;
use rfd::FileDialog;
pub fn load_image(path: impl AsRef<Path>) -> Result<RgbaImage, String> {
let path = path.as_ref();
let decoded_image = match path.extension().map(OsStr::to_string_lossy).as_deref() {
Some("jxl") => {
let data = match std::fs::read(path) {
Ok(d) => d,
Err(e) => {
return Err(format!(
"failed to read data from '{}': {e}",
path.display()
));
}
};
match jpegxl_rs::decoder_builder()
.build()
.unwrap()
.decode_to_image(&data)
{
Ok(Some(decoded_image)) => decoded_image,
Ok(None) => {
return Err(format!(
"failed to convert jxl '{}' to DynamicImage",
path.display()
));
}
Err(e) => {
return Err(format!("failed to decode jxl '{}': {e}", path.display()));
}
}
}
_ => {
let reader = match ImageReader::open(path) {
Ok(r) => r,
Err(e) => {
return Err(format!(
"failed to create reader for '{}': {e}",
path.display()
));
}
};
let mut decoder = match reader.into_decoder() {
Ok(d) => d,
Err(e) => {
return Err(format!(
"failed to create decoder for '{}': {e}",
path.display()
));
}
};
let oreintation = match decoder.orientation() {
Ok(o) => o,
Err(e) => {
return Err(format!(
"failed to get oreintation of '{}': {e}",
path.display()
));
}
};
let mut decoded_image = match DynamicImage::from_decoder(decoder) {
Ok(i) => i,
Err(e) => {
return Err(format!("failed to decode image '{}': {e}", path.display()));
}
};
decoded_image.apply_orientation(oreintation);
decoded_image
}
};
Ok(decoded_image.into_rgba8())
}
pub fn pick_image() -> Result<PathBuf, String> {
let Some(path) = FileDialog::new()
.add_filter(
"image",
&[
"png", "PNG", "jpg", "JPG", "jpeg", "JPEG", "avif", "jxl", "bmp", "exr", "ff",
"gif", "hdr", "ico", "pnm", "qoi", "tga", "tiff", "webp",
],
)
.pick_file()
else {
return Err("no path to open provided".into());
};
Ok(path)
}
#[must_use]
pub fn save_image(image: Option<&RgbaImage>) -> Option<String> {
let Some(image) = image else {
return Some("no image to save".into());
};
let Some(path) = FileDialog::new().save_file() else {
return Some("no path to save provided".into());
};
let mut file = match fs::File::create(&path) {
Ok(f) => f,
Err(e) => return Some(format!("failed to create file '{}': {e}", path.display())),
};
match path.extension().map(OsStr::to_string_lossy).as_deref() {
Some("jxl") => {
let mut encoder = jpegxl_rs::encoder_builder()
.speed(jpegxl_rs::encode::EncoderSpeed::Glacier)
.lossless(true)
.build()
.unwrap();
let rgb_image = DynamicImage::from(image.to_owned()).into_rgb8();
let jxl =
match encoder.encode::<u8, u8>(&rgb_image, rgb_image.width(), rgb_image.height()) {
Ok(j) => j,
Err(e) => {
return Some(format!("failed to encode jxl '{}': {e}", path.display()));
}
};
if let Err(e) = file.write(&jxl.data) {
return Some(format!("failed to write jxl to '{}': {e}", path.display()));
};
}
_ => {
if let Err(e) = image.save(&path) {
return Some(format!("failed to save '{}': {e}", path.display()));
};
}
};
None
}