Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
dab5f8e09f |
|||
|
d577fe2e76 |
|||
|
c3a5919e87 |
|||
|
b11264dc4b |
3 changed files with 196 additions and 147 deletions
10
flake.nix
10
flake.nix
|
|
@ -85,10 +85,12 @@
|
||||||
]
|
]
|
||||||
++ lib.optional pkgs.stdenv.hostPlatform.isLinux pkgs.makeBinaryWrapper;
|
++ lib.optional pkgs.stdenv.hostPlatform.isLinux pkgs.makeBinaryWrapper;
|
||||||
|
|
||||||
buildInputs = commonArgs.buildInputs ++ [
|
buildInputs =
|
||||||
pkgs.libgcc
|
commonArgs.buildInputs
|
||||||
pkgs.zenity
|
++ [
|
||||||
];
|
pkgs.libgcc
|
||||||
|
]
|
||||||
|
++ lib.optional pkgs.stdenv.hostPlatform.isLinux pkgs.zenity;
|
||||||
|
|
||||||
runtimeDependencies = dlDeps;
|
runtimeDependencies = dlDeps;
|
||||||
|
|
||||||
|
|
|
||||||
182
src/main.rs
182
src/main.rs
|
|
@ -1,13 +1,13 @@
|
||||||
use std::{env, ffi::OsStr, fs, io::Write, path::Path};
|
use std::{env, 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::{DynamicImage, EncodableLayout, ImageDecoder, ImageReader, RgbaImage, imageops};
|
use image::{EncodableLayout, RgbaImage, imageops};
|
||||||
use jpegxl_rs::image::ToDynamic;
|
|
||||||
use rfd::FileDialog;
|
mod utils;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum Message {
|
enum Message {
|
||||||
|
|
@ -27,7 +27,11 @@ 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.load_image(path));
|
state.error = env::args().nth(1).and_then(|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)
|
||||||
|
|
@ -76,8 +80,7 @@ impl State {
|
||||||
}
|
}
|
||||||
|
|
||||||
Key::Character("o") => {
|
Key::Character("o") => {
|
||||||
self.error = self.open_image();
|
return self.pick_and_load_image();
|
||||||
return self.allocate_image();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Key::Character("r") => {
|
Key::Character("r") => {
|
||||||
|
|
@ -102,7 +105,7 @@ impl State {
|
||||||
}
|
}
|
||||||
|
|
||||||
Key::Character("s") => {
|
Key::Character("s") => {
|
||||||
self.error = self.save_image();
|
self.save_image();
|
||||||
}
|
}
|
||||||
|
|
||||||
Key::Character("q") => return window::latest().and_then(window::close),
|
Key::Character("q") => return window::latest().and_then(window::close),
|
||||||
|
|
@ -111,6 +114,15 @@ 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(_) => {}
|
||||||
}
|
}
|
||||||
|
|
@ -118,151 +130,35 @@ impl State {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_image(&mut self, path: impl AsRef<Path>) -> Option<String> {
|
#[must_use]
|
||||||
let path = path.as_ref();
|
fn load_image(&mut self, path: impl AsRef<Path>) -> Task<Message> {
|
||||||
|
let task;
|
||||||
let decoded_image = match path.extension().map(OsStr::to_string_lossy).as_deref() {
|
(self.error, task) = match utils::load_image(path) {
|
||||||
Some("jxl") => {
|
Ok(image) => {
|
||||||
let data = match std::fs::read(path) {
|
self.image = Some(image);
|
||||||
Ok(d) => d,
|
(None, self.allocate_image())
|
||||||
Err(e) => {
|
|
||||||
return Some(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 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()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
Err(e) => (Some(e), Task::none()),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.image = Some(decoded_image.into_rgba8());
|
task
|
||||||
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
fn open_image(&mut self) -> Option<String> {
|
#[must_use]
|
||||||
let Some(path) = FileDialog::new()
|
fn pick_and_load_image(&mut self) -> Task<Message> {
|
||||||
.add_filter(
|
let task;
|
||||||
"image",
|
(self.error, task) = match utils::pick_image() {
|
||||||
&[
|
Ok(path) => (None, self.load_image(path)),
|
||||||
"png", "PNG", "jpg", "JPG", "jpeg", "JPEG", "avif", "jxl", "bmp", "exr", "ff",
|
Err(e) => (Some(e), Task::none()),
|
||||||
"gif", "hdr", "ico", "pnm", "qoi", "tga", "tiff", "webp",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.pick_file()
|
|
||||||
else {
|
|
||||||
return Some("no path to open provided".into());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.load_image(path)
|
task
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_image(&self) -> Option<String> {
|
fn save_image(&mut self) {
|
||||||
let Some(image) = self.image.as_ref() else {
|
self.error = utils::save_image(self.image.as_ref());
|
||||||
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");
|
||||||
|
|
|
||||||
151
src/utils.rs
Normal file
151
src/utils.rs
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue