Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
4a5ffdbd96 |
7 changed files with 764 additions and 1395 deletions
1527
Cargo.lock
generated
1527
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
15
Cargo.toml
15
Cargo.toml
|
|
@ -6,24 +6,13 @@ license = "AGPL-3.0-or-later"
|
|||
|
||||
[dependencies]
|
||||
dirs = "6.0.0"
|
||||
misc-image-integration = "0.1.0"
|
||||
resvg = "0.48.1"
|
||||
rfd = "0.17.2"
|
||||
size = "0.5.0"
|
||||
zip = "8.6.0"
|
||||
jpegxl-rs = "0.14.0"
|
||||
|
||||
[dependencies.iced]
|
||||
version = "0.15.0-dev"
|
||||
version = "0.14.0"
|
||||
features = [ "image" ]
|
||||
|
||||
[dependencies.image]
|
||||
version = "0.25.10"
|
||||
features = [ "avif-native" ]
|
||||
|
||||
[dependencies.jxl-oxide]
|
||||
version = "0.12.6"
|
||||
# I really wish the image crate was named better
|
||||
features = [ "image" ]
|
||||
|
||||
[patch.crates-io]
|
||||
iced.git = "https://github.com/iced-rs/iced"
|
||||
|
|
|
|||
41
README.md
41
README.md
|
|
@ -1,41 +0,0 @@
|
|||
# imagey
|
||||
|
||||
image viewer and maybe editor; inspired by mpv's simplicity
|
||||
|
||||
name is subject to change, suggestions welcome :)
|
||||
|
||||
## usage
|
||||
|
||||
```sh
|
||||
# try it out!
|
||||
nix run git+https://git.federated.nexus/electria/imagey
|
||||
|
||||
# install it imperatively
|
||||
nix profile install git+https://git.federated.nexus/electria/imagey
|
||||
```
|
||||
|
||||
keybinds are shown on startup if you didn't start it with an image
|
||||
|
||||
## known issues
|
||||
|
||||
### JXL
|
||||
|
||||
1. encoding is not implemented (jxl-oxide is decoding-only)
|
||||
2. oreintation is incorrect in some cases
|
||||
|
||||
if the image has metadata oreintation AND
|
||||
|
||||
it's a jxl without the right extension OR
|
||||
it's a jpeg with the jxl extension
|
||||
|
||||
this is due to the JXL decoder automatically rotating the image,
|
||||
while the JPEG decoder (for instance) requires the extra step.
|
||||
|
||||
my workaround is to check the path of the input file,
|
||||
not changing the oreintation if it has the jxl extension;
|
||||
causing these caveats for cases where the extension is wrong.
|
||||
|
||||
### clipboard
|
||||
|
||||
1. large images (eg photos) don't seem to copy on linux/wayland,
|
||||
(despite set_image not returning any error)
|
||||
39
flake.nix
39
flake.nix
|
|
@ -19,24 +19,18 @@
|
|||
cargoToml = fromTOML (builtins.readFile ./Cargo.toml);
|
||||
name = cargoToml.package.name;
|
||||
|
||||
dlDeps =
|
||||
with pkgs;
|
||||
[
|
||||
dlDeps = with pkgs; [
|
||||
# needed for both x11 and wayland
|
||||
libxkbcommon
|
||||
libGL
|
||||
vulkan-loader
|
||||
|
||||
wayland
|
||||
|
||||
libx11
|
||||
libxcursor
|
||||
libxi
|
||||
libxcb
|
||||
]
|
||||
++ lib.optionals stdenv.hostPlatform.isLinux [
|
||||
# libdbus, for rfd
|
||||
dbus.lib
|
||||
|
||||
vulkan-loader
|
||||
wayland
|
||||
];
|
||||
|
||||
commonArgs = {
|
||||
|
|
@ -45,12 +39,15 @@
|
|||
pkg-config
|
||||
];
|
||||
buildInputs = with pkgs; [
|
||||
libjxl
|
||||
dav1d
|
||||
];
|
||||
|
||||
src = ./.;
|
||||
};
|
||||
|
||||
LD_LIBRARY_PATH = lib.makeLibraryPath dlDeps;
|
||||
|
||||
# Build *just* the cargo dependencies,
|
||||
# to reuse them for build and test derivations.
|
||||
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||
|
|
@ -74,8 +71,6 @@
|
|||
"image/tiff"
|
||||
"image/bmp"
|
||||
"image/vnd.microsoft.icon" # .ico
|
||||
"application/x-krita"
|
||||
"image/svg+xml"
|
||||
];
|
||||
};
|
||||
in
|
||||
|
|
@ -84,13 +79,19 @@
|
|||
// {
|
||||
inherit cargoArtifacts;
|
||||
|
||||
nativeBuildInputs = commonArgs.nativeBuildInputs ++ [
|
||||
nativeBuildInputs =
|
||||
commonArgs.nativeBuildInputs
|
||||
++ [
|
||||
pkgs.autoPatchelfHook
|
||||
];
|
||||
]
|
||||
++ lib.optional pkgs.stdenv.hostPlatform.isLinux pkgs.makeBinaryWrapper;
|
||||
|
||||
buildInputs = commonArgs.buildInputs ++ [
|
||||
buildInputs =
|
||||
commonArgs.buildInputs
|
||||
++ [
|
||||
pkgs.libgcc
|
||||
];
|
||||
]
|
||||
++ lib.optional pkgs.stdenv.hostPlatform.isLinux pkgs.zenity;
|
||||
|
||||
runtimeDependencies = dlDeps;
|
||||
|
||||
|
|
@ -99,6 +100,10 @@
|
|||
postFixup = ''
|
||||
mkdir -p "$out/share/applications"
|
||||
ln -s "${desktopItem}"/share/applications/* "$out/share/applications/"
|
||||
''
|
||||
+ lib.optionalString pkgs.stdenv.hostPlatform.isLinux /* sh */ ''
|
||||
wrapProgram $out/bin/${name} --inherit-argv0 \
|
||||
--prefix PATH : ${lib.makeBinPath [ pkgs.zenity ]}
|
||||
'';
|
||||
}
|
||||
);
|
||||
|
|
@ -118,7 +123,7 @@
|
|||
};
|
||||
|
||||
devShells.default = craneLib.devShell {
|
||||
LD_LIBRARY_PATH = lib.makeLibraryPath dlDeps;
|
||||
inherit LD_LIBRARY_PATH;
|
||||
|
||||
inputsFrom = [ crate ];
|
||||
packages = [ pkgs.rust-analyzer ];
|
||||
|
|
|
|||
314
src/main.rs
314
src/main.rs
|
|
@ -1,72 +1,49 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
env,
|
||||
ffi::OsStr,
|
||||
os::unix::fs::MetadataExt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use std::{env, path::Path};
|
||||
|
||||
use iced::{
|
||||
Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, clipboard, event,
|
||||
Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, color, event,
|
||||
keyboard::{self, Key, key},
|
||||
theme, widget, window,
|
||||
};
|
||||
use image::{EncodableLayout, RgbaImage, imageops};
|
||||
use size::Size;
|
||||
|
||||
mod utils;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Message {
|
||||
Open,
|
||||
Rotate,
|
||||
Invert,
|
||||
Filter,
|
||||
Bar,
|
||||
Save,
|
||||
Quit,
|
||||
|
||||
Yank,
|
||||
Put,
|
||||
ImageYanked(Result<(), clipboard::Error>),
|
||||
ImagePut(Result<clipboard::Image, clipboard::Error>),
|
||||
|
||||
FocusNext,
|
||||
FocusPrevious,
|
||||
|
||||
ImagePicked(Result<PathBuf, Cow<'static, str>>),
|
||||
ImageDisplayReady(Result<widget::image::Allocation, widget::image::Error>),
|
||||
SavePathPicked(Result<PathBuf, Cow<'static, str>>),
|
||||
|
||||
FileDropped(PathBuf),
|
||||
Event(iced::Event),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct State {
|
||||
image: Option<RgbaImage>,
|
||||
image_display: Option<widget::image::Allocation>,
|
||||
image_filter: widget::image::FilterMethod,
|
||||
|
||||
error: Option<Cow<'static, str>>,
|
||||
info: Option<String>,
|
||||
infobar_shown: bool,
|
||||
error: Option<String>,
|
||||
}
|
||||
impl State {
|
||||
fn new() -> (Self, Task<Message>) {
|
||||
let mut state = Self::default();
|
||||
let mut state = State::default();
|
||||
|
||||
if let Some(path) = env::args().nth(1) {
|
||||
match state.load_image(path) {
|
||||
Err(e) => state.error = Some(e.into()),
|
||||
Ok(task) => return (state, task),
|
||||
}
|
||||
}
|
||||
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();
|
||||
|
||||
(state, Task::none())
|
||||
(state, allocate_image)
|
||||
}
|
||||
fn view(&self) -> Element<'_, Message, Theme, Renderer> {
|
||||
let main = self.image_display.as_ref().map_or_else(
|
||||
|| {
|
||||
widget::column([
|
||||
if let Some(allocation) = self.image_display.as_ref() {
|
||||
widget::image::viewer(allocation.handle().clone())
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
} else {
|
||||
widget::container(widget::text(include_str!("usage.txt")))
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fill)
|
||||
|
|
@ -74,168 +51,103 @@ impl State {
|
|||
.align_y(Alignment::Center)
|
||||
.into()
|
||||
},
|
||||
|allocation| {
|
||||
widget::image::viewer(allocation.handle().clone())
|
||||
.filter_method(self.image_filter)
|
||||
.max_scale(50.)
|
||||
.min_scale(1.)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
},
|
||||
);
|
||||
|
||||
let mut bar = Vec::new();
|
||||
if let Some(error) = self.error.as_ref() {
|
||||
bar.push(
|
||||
widget::container(widget::text(error).style(widget::text::danger))
|
||||
.align_x(Alignment::Start)
|
||||
.width(Length::Fill)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if self.infobar_shown && self.info.is_some() {
|
||||
bar.push(
|
||||
widget::container(widget::text(self.title()))
|
||||
.align_x(Alignment::End)
|
||||
.width(Length::Fill)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
widget::column([main, widget::row(bar).into()]).into()
|
||||
widget::text(error).style(widget::text::danger).into()
|
||||
} else {
|
||||
widget::space().into()
|
||||
},
|
||||
])
|
||||
.into()
|
||||
}
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::ImagePicked(result) => {
|
||||
match result.and_then(|path| self.load_image(path).map_err(Cow::from)) {
|
||||
Err(e) => self.error = Some(e.into()),
|
||||
Message::ImageDisplayReady(result) => {
|
||||
self.image_display = Some(result.unwrap());
|
||||
}
|
||||
|
||||
Message::Event(iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key,
|
||||
modifiers,
|
||||
..
|
||||
})) => match key.as_ref() {
|
||||
// input field cycling
|
||||
Key::Named(key::Named::Tab) => {
|
||||
if modifiers.shift() {
|
||||
return widget::operation::focus_previous();
|
||||
} else {
|
||||
return widget::operation::focus_next();
|
||||
}
|
||||
}
|
||||
|
||||
Key::Character("o") => match self.pick_and_load_image() {
|
||||
Ok(task) => {
|
||||
self.error = None;
|
||||
return task;
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::ImageDisplayReady(result) => {
|
||||
self.image_display = Some(result.unwrap());
|
||||
}
|
||||
Message::SavePathPicked(result) => {
|
||||
self.error = result
|
||||
.map_err(Cow::from)
|
||||
.and_then(|path| {
|
||||
self.image.as_ref().map_or_else(
|
||||
|| Err("no image to save".into()),
|
||||
|image| image.save(path).map_err(|e| e.to_string().into()),
|
||||
)
|
||||
})
|
||||
.err();
|
||||
}
|
||||
Err(e) => self.error = Some(e),
|
||||
},
|
||||
|
||||
Message::FocusNext => return widget::operation::focus_next(),
|
||||
Message::FocusPrevious => return widget::operation::focus_previous(),
|
||||
|
||||
Message::Open => {
|
||||
return Task::perform(utils::pick_image(), Message::ImagePicked);
|
||||
}
|
||||
|
||||
Message::Rotate => match self.image.as_ref() {
|
||||
Key::Character("r") => {
|
||||
match self.image.as_ref() {
|
||||
None => self.error = Some("no image to rotate".into()),
|
||||
Some(image) => {
|
||||
self.error = None;
|
||||
self.image = Some(imageops::rotate90(image));
|
||||
return self.allocate_image();
|
||||
}
|
||||
},
|
||||
Message::Invert => match self.image.as_mut() {
|
||||
};
|
||||
}
|
||||
Key::Character("i") => {
|
||||
match self.image.as_mut() {
|
||||
None => self.error = Some("no image to invert".into()),
|
||||
Some(image) => {
|
||||
self.error = None;
|
||||
imageops::invert(image);
|
||||
return self.allocate_image();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Key::Character("s") => {
|
||||
self.save_image();
|
||||
}
|
||||
|
||||
Key::Character("q") => return window::latest().and_then(window::close),
|
||||
|
||||
// ignore unused keys
|
||||
_ => {}
|
||||
},
|
||||
Message::Filter => {
|
||||
self.image_filter = match self.image_filter {
|
||||
widget::image::FilterMethod::Linear => widget::image::FilterMethod::Nearest,
|
||||
widget::image::FilterMethod::Nearest => widget::image::FilterMethod::Linear,
|
||||
}
|
||||
}
|
||||
Message::Bar => {
|
||||
self.infobar_shown = !self.infobar_shown;
|
||||
}
|
||||
|
||||
Message::Save => {
|
||||
if self.image.is_some() {
|
||||
self.error = None;
|
||||
return Task::perform(utils::pick_save_path(), Message::SavePathPicked);
|
||||
}
|
||||
self.error = Some("no image to save".into());
|
||||
}
|
||||
|
||||
Message::Quit => return window::latest().and_then(window::close),
|
||||
|
||||
Message::Yank => {
|
||||
if let Some(image) = self.image.as_ref() {
|
||||
return clipboard::write(utils::clipboard_image_from_rgbaimage(&image))
|
||||
.map(Message::ImageYanked);
|
||||
} else {
|
||||
self.error = Some("no image to yank".into());
|
||||
}
|
||||
}
|
||||
Message::Put => return clipboard::read_image().map(Message::ImagePut),
|
||||
|
||||
Message::ImageYanked(result) => {
|
||||
self.error = result.err().map(|e| format!("{e:?}").into());
|
||||
}
|
||||
Message::ImagePut(result) => {
|
||||
self.error = result.as_ref().err().map(|e| format!("{e:?}").into());
|
||||
|
||||
if let Ok(image) = result {
|
||||
self.info = Some(format!("{}x{}", image.size.width, image.size.height,));
|
||||
if (image.size.width, image.size.height) < (100, 100) {
|
||||
self.image_filter = widget::image::FilterMethod::Nearest;
|
||||
}
|
||||
self.image = Some(utils::rgbaimage_from_clipboard_image(image));
|
||||
return self.allocate_image();
|
||||
}
|
||||
}
|
||||
|
||||
Message::FileDropped(path) => match self.load_image(path) {
|
||||
Message::Event(iced::Event::Window(window::Event::FileDropped(file))) => {
|
||||
match self.load_image(file) {
|
||||
Ok(task) => {
|
||||
self.error = None;
|
||||
return task;
|
||||
}
|
||||
Err(e) => self.error = Some(e.into()),
|
||||
},
|
||||
Err(e) => self.error = Some(e),
|
||||
}
|
||||
}
|
||||
|
||||
// ignore unused events
|
||||
Message::Event(_) => {}
|
||||
}
|
||||
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn load_image(&mut self, path: impl AsRef<Path>) -> Result<Task<Message>, String> {
|
||||
utils::load_image(&path).map(|image| {
|
||||
self.info = Some(format!(
|
||||
"{} {} {}x{}",
|
||||
path.as_ref()
|
||||
.file_name()
|
||||
.map_or(Cow::Borrowed("[no file]"), OsStr::to_string_lossy),
|
||||
path.as_ref().metadata().map_or_else(
|
||||
|e| {
|
||||
eprintln!("failed to read metadata: {e}");
|
||||
Cow::Borrowed("[no size]")
|
||||
},
|
||||
|m| Cow::Owned(Size::from_bytes(m.size()).to_string())
|
||||
),
|
||||
image.width(),
|
||||
image.height(),
|
||||
));
|
||||
if image.dimensions() < (100, 100) {
|
||||
self.image_filter = widget::image::FilterMethod::Nearest;
|
||||
}
|
||||
utils::load_image(path).map(|image| {
|
||||
self.image = Some(image);
|
||||
self.allocate_image()
|
||||
})
|
||||
}
|
||||
fn pick_and_load_image(&mut self) -> Result<Task<Message>, String> {
|
||||
utils::pick_image().and_then(|path| self.load_image(path))
|
||||
}
|
||||
|
||||
fn save_image(&mut self) {
|
||||
self.error = utils::save_image(self.image.as_ref()).err();
|
||||
}
|
||||
|
||||
fn allocate_image(&self) -> Task<Message> {
|
||||
let Some(image) = self.image.as_ref() else {
|
||||
|
|
@ -246,79 +158,35 @@ impl State {
|
|||
widget::image::allocate(widget::image::Handle::from_rgba(
|
||||
image.width(),
|
||||
image.height(),
|
||||
// SAFETY: this is a sort of race condition;
|
||||
// will cause panics with large enough images,
|
||||
// when they are edited consecutively.
|
||||
//
|
||||
// this can be reproduced by holding down 'i' with a 10k by 10k px image,
|
||||
// which will invert the colors rapidly and eventually crash the app.
|
||||
//
|
||||
// however, that is not such a problem in this case,
|
||||
// and I don't know a different way of doing this that doesn't copy
|
||||
// (performance loss, which is can be pretty big)
|
||||
// or likely cause flickering instead in such cases
|
||||
// (like using Handle over Allocation to immediately drop the last one)
|
||||
unsafe { std::mem::transmute::<&[u8], &'static [u8]>(image.as_bytes()) },
|
||||
))
|
||||
.map(Message::ImageDisplayReady)
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
event::listen().filter_map(|event| match event {
|
||||
iced::Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) => {
|
||||
match key.as_ref() {
|
||||
Key::Named(key::Named::Tab) => Some(if modifiers.shift() {
|
||||
Message::FocusPrevious
|
||||
} else {
|
||||
Message::FocusNext
|
||||
}),
|
||||
|
||||
Key::Character("o") => Some(Message::Open),
|
||||
Key::Character("r") => Some(Message::Rotate),
|
||||
Key::Character("i") => Some(Message::Invert),
|
||||
Key::Character("f") => Some(Message::Filter),
|
||||
Key::Character("b") => Some(Message::Bar),
|
||||
Key::Character("s") => Some(Message::Save),
|
||||
Key::Character("q") => Some(Message::Quit),
|
||||
|
||||
Key::Character("y") | Key::Character("c") => Some(Message::Yank),
|
||||
Key::Character("p") | Key::Character("v") => Some(Message::Put),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
iced::Event::Window(window::Event::FileDropped(path)) => {
|
||||
Some(Message::FileDropped(path))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
event::listen().map(Message::Event)
|
||||
}
|
||||
fn title(&self) -> String {
|
||||
self.info.as_ref().map_or_else(
|
||||
|| "imagey".into(),
|
||||
|info| {
|
||||
format!(
|
||||
"imagey {info} {}",
|
||||
utils::string_from_filter_type(self.image_filter),
|
||||
)
|
||||
},
|
||||
)
|
||||
match self.image.as_ref() {
|
||||
Some(image) => format!("imagey {}x{}", image.width(), image.height()),
|
||||
None => "imagey".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), iced::Error> {
|
||||
jxl_oxide::integration::register_image_decoding_hook();
|
||||
misc_image_integration::register();
|
||||
|
||||
iced::application(State::new, State::update, State::view)
|
||||
.subscription(State::subscription)
|
||||
.title(State::title)
|
||||
.theme(Theme::custom(
|
||||
"custom",
|
||||
theme::palette::Seed {
|
||||
background: Color::BLACK,
|
||||
theme::Palette {
|
||||
background: color!(0x080808),
|
||||
text: Color::WHITE,
|
||||
..theme::palette::Seed::DARK
|
||||
primary: color!(0xff00ff),
|
||||
success: color!(0x00ff00),
|
||||
warning: color!(0x880000),
|
||||
danger: color!(0xff0000),
|
||||
},
|
||||
))
|
||||
.run()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
'o' to open a new image
|
||||
'r' to rotate
|
||||
'i' to invert colors
|
||||
'f' to switch filter
|
||||
'b' to toggle the bar
|
||||
's' to save the image
|
||||
'q' to quit
|
||||
|
||||
'y' or 'c' to yank
|
||||
'p' or 'v' to put
|
||||
"o" to open a new image
|
||||
"r" to rotate
|
||||
"i" to invert colors
|
||||
"s" to save the image
|
||||
"q" to quit
|
||||
|
|
|
|||
162
src/utils.rs
162
src/utils.rs
|
|
@ -1,84 +1,150 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use iced::{clipboard, widget};
|
||||
use image::{DynamicImage, EncodableLayout, ImageDecoder, ImageReader, ImageResult, RgbaImage};
|
||||
use rfd::AsyncFileDialog;
|
||||
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> {
|
||||
load_image_impl(path).map_err(|e| e.to_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()
|
||||
));
|
||||
}
|
||||
fn load_image_impl(path: impl AsRef<Path>) -> ImageResult<RgbaImage> {
|
||||
let mut decoder = ImageReader::open(&path)?
|
||||
.with_guessed_format()?
|
||||
.into_decoder()?;
|
||||
};
|
||||
|
||||
let oreintation = decoder.orientation()?;
|
||||
|
||||
let mut decoded_image = DynamicImage::from_decoder(decoder)?;
|
||||
|
||||
// the condition is a workaround to not rotate JXL images twice;
|
||||
// since they are already rotated by the decoder
|
||||
// (while jpegs for instance aren't)
|
||||
if !path
|
||||
.as_ref()
|
||||
.extension()
|
||||
.map(OsStr::to_string_lossy)
|
||||
.is_some_and(|s| s == "jxl")
|
||||
match jpegxl_rs::decoder_builder()
|
||||
.build()
|
||||
.unwrap()
|
||||
.decode_to_image(&data)
|
||||
{
|
||||
decoded_image.apply_orientation(oreintation);
|
||||
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 async fn pick_image() -> Result<PathBuf, Cow<'static, str>> {
|
||||
let Some(filehandle) = AsyncFileDialog::new()
|
||||
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", "kra", "svg", "svgz",
|
||||
"gif", "hdr", "ico", "pnm", "qoi", "tga", "tiff", "webp",
|
||||
],
|
||||
)
|
||||
.pick_file()
|
||||
.await
|
||||
else {
|
||||
return Err("no path to open provided".into());
|
||||
};
|
||||
|
||||
Ok(filehandle.path().to_owned())
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub async fn pick_save_path() -> Result<PathBuf, Cow<'static, str>> {
|
||||
let Some(filehandle) = AsyncFileDialog::new().save_file().await else {
|
||||
pub fn save_image(image: Option<&RgbaImage>) -> Result<(), String> {
|
||||
let Some(image) = image else {
|
||||
return Err("no image to save".into());
|
||||
};
|
||||
|
||||
let Some(path) = FileDialog::new().save_file() else {
|
||||
return Err("no path to save provided".into());
|
||||
};
|
||||
|
||||
Ok(filehandle.path().to_owned())
|
||||
}
|
||||
let mut file = match fs::File::create(&path) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return Err(format!("failed to create file '{}': {e}", path.display())),
|
||||
};
|
||||
|
||||
pub const fn string_from_filter_type(f: widget::image::FilterMethod) -> &'static str {
|
||||
match f {
|
||||
widget::image::FilterMethod::Linear => "bilinear",
|
||||
widget::image::FilterMethod::Nearest => "nearest neighbor",
|
||||
}
|
||||
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 Err(format!("failed to encode jxl '{}': {e}", path.display()));
|
||||
}
|
||||
};
|
||||
|
||||
pub fn clipboard_image_from_rgbaimage(image: &RgbaImage) -> clipboard::Image {
|
||||
clipboard::Image {
|
||||
size: (image.width(), image.height()).into(),
|
||||
// SAFETY: this is also probably a race condition,
|
||||
// where if the user switches the image before it's done,
|
||||
// then something will die.
|
||||
//
|
||||
// however, especially with larger images,
|
||||
// copying them is just too expensive.
|
||||
rgba: unsafe { std::mem::transmute::<&[u8], &'static [u8]>(image.as_bytes()) }.into(),
|
||||
if let Err(e) = file.write(&jxl.data) {
|
||||
return Err(format!("failed to write jxl to '{}': {e}", path.display()));
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
if let Err(e) = image.save(&path) {
|
||||
return Err(format!("failed to save '{}': {e}", path.display()));
|
||||
};
|
||||
}
|
||||
pub fn rgbaimage_from_clipboard_image(image: clipboard::Image) -> RgbaImage {
|
||||
RgbaImage::from_raw(image.size.width, image.size.height, image.rgba.into()).unwrap()
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue