Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
e8761f8fcf |
8 changed files with 162 additions and 380 deletions
96
Cargo.lock
generated
96
Cargo.lock
generated
|
|
@ -136,27 +136,6 @@ version = "1.4.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
version = "3.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
|
||||
dependencies = [
|
||||
"clipboard-win",
|
||||
"image",
|
||||
"log",
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-foundation 0.3.2",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"windows-sys 0.59.0",
|
||||
"wl-clipboard-rs",
|
||||
"x11rb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arg_enum_proc_macro"
|
||||
version = "0.3.4"
|
||||
|
|
@ -1247,12 +1226,6 @@ version = "0.1.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
|
|
@ -1927,14 +1900,11 @@ dependencies = [
|
|||
name = "imagey"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"dirs",
|
||||
"iced",
|
||||
"image",
|
||||
"jxl-oxide",
|
||||
"kra-image-integration",
|
||||
"rfd",
|
||||
"size",
|
||||
"zip",
|
||||
]
|
||||
|
||||
|
|
@ -2268,16 +2238,6 @@ version = "3.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
||||
|
||||
[[package]]
|
||||
name = "kra-image-integration"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ab1db0e6caced9f4b26057c10ea190c447a8be4fcac03afd7213e35168ad700"
|
||||
dependencies = [
|
||||
"image",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kurbo"
|
||||
version = "0.10.4"
|
||||
|
|
@ -3104,16 +3064,6 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "owned_ttf_parser"
|
||||
version = "0.25.1"
|
||||
|
|
@ -3180,17 +3130,6 @@ version = "2.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.13"
|
||||
|
|
@ -3848,12 +3787,6 @@ version = "0.1.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "size"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b6709c7b6754dca1311b3c73e79fcce40dd414c782c66d88e8823030093b02b"
|
||||
|
||||
[[package]]
|
||||
name = "skrifa"
|
||||
version = "0.37.0"
|
||||
|
|
@ -4313,17 +4246,6 @@ dependencies = [
|
|||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree_magic_mini"
|
||||
version = "3.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"nom",
|
||||
"petgraph",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ttf-parser"
|
||||
version = "0.25.1"
|
||||
|
|
@ -5187,24 +5109,6 @@ version = "0.57.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "wl-clipboard-rs"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"os_pipe",
|
||||
"rustix 1.1.4",
|
||||
"thiserror 2.0.18",
|
||||
"tree_magic_mini",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols-wlr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11-dl"
|
||||
version = "2.21.0"
|
||||
|
|
|
|||
|
|
@ -6,9 +6,7 @@ license = "AGPL-3.0-or-later"
|
|||
|
||||
[dependencies]
|
||||
dirs = "6.0.0"
|
||||
kra-image-integration = "0.1.0"
|
||||
rfd = "0.17.2"
|
||||
size = "0.5.0"
|
||||
zip = "8.6.0"
|
||||
|
||||
[dependencies.iced]
|
||||
|
|
@ -23,7 +21,3 @@ features = [ "avif-native" ]
|
|||
version = "0.12.6"
|
||||
# I really wish the image crate was named better
|
||||
features = [ "image" ]
|
||||
|
||||
[dependencies.arboard]
|
||||
version = "3.6.1"
|
||||
features = [ "wayland-data-control" ]
|
||||
|
|
|
|||
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)
|
||||
31
flake.nix
31
flake.nix
|
|
@ -19,25 +19,22 @@
|
|||
cargoToml = fromTOML (builtins.readFile ./Cargo.toml);
|
||||
name = cargoToml.package.name;
|
||||
|
||||
dlDeps =
|
||||
with pkgs;
|
||||
[
|
||||
# needed for both x11 and wayland
|
||||
libxkbcommon
|
||||
libGL
|
||||
dlDeps = with pkgs; [
|
||||
# libdbus, for rfd
|
||||
dbus
|
||||
|
||||
libx11
|
||||
libxcursor
|
||||
libxi
|
||||
libxcb
|
||||
]
|
||||
++ lib.optionals stdenv.hostPlatform.isLinux [
|
||||
# libdbus, for rfd
|
||||
dbus.lib
|
||||
# needed for both x11 and wayland
|
||||
libxkbcommon
|
||||
libGL
|
||||
vulkan-loader
|
||||
|
||||
vulkan-loader
|
||||
wayland
|
||||
];
|
||||
wayland
|
||||
|
||||
libx11
|
||||
libxcursor
|
||||
libxi
|
||||
libxcb
|
||||
];
|
||||
|
||||
commonArgs = {
|
||||
# all that's needed for artifacts and checks
|
||||
|
|
|
|||
40
src/kra.rs
Normal file
40
src/kra.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
use std::{
|
||||
ffi::OsString,
|
||||
io::{self, Read},
|
||||
};
|
||||
|
||||
use image::{
|
||||
ImageDecoder, ImageError, ImageReader, ImageResult,
|
||||
error::DecodingError,
|
||||
hooks::{self, GenericReader},
|
||||
};
|
||||
use zip::ZipArchive;
|
||||
|
||||
pub fn register() -> bool {
|
||||
hooks::register_decoding_hook(OsString::from("kra"), Box::new(hook))
|
||||
}
|
||||
|
||||
fn hook<'a>(reader: GenericReader<'a>) -> ImageResult<Box<dyn ImageDecoder + 'a>> {
|
||||
let mut zip = ZipArchive::new(reader).map_err(to_image_error)?;
|
||||
|
||||
let mut reader = zip.by_name("mergedimage.png").map_err(to_image_error)?;
|
||||
|
||||
// reading it all and wrapping it with a Cursor
|
||||
// is the only way I know to give it Seek
|
||||
// (which is required by ImageReader)
|
||||
let mut buf = Vec::new();
|
||||
reader.read_to_end(&mut buf)?;
|
||||
|
||||
let image_reader = ImageReader::with_format(io::Cursor::new(buf), image::ImageFormat::Png);
|
||||
|
||||
Ok(Box::new(image_reader.into_decoder()?))
|
||||
}
|
||||
|
||||
fn to_image_error(
|
||||
e: impl Into<Box<dyn std::error::Error + std::marker::Send + Sync + 'static>>,
|
||||
) -> ImageError {
|
||||
ImageError::Decoding(DecodingError::new(
|
||||
image::error::ImageFormatHint::PathExtension("kra".into()),
|
||||
e,
|
||||
))
|
||||
}
|
||||
248
src/main.rs
248
src/main.rs
|
|
@ -1,46 +1,34 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
env,
|
||||
ffi::OsStr,
|
||||
os::unix::fs::MetadataExt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use std::{borrow::Cow, env, ffi::OsStr, path::Path};
|
||||
|
||||
use arboard::Clipboard;
|
||||
use iced::{
|
||||
Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, event,
|
||||
keyboard::{self, Key, key},
|
||||
theme, widget, window,
|
||||
};
|
||||
use image::{EncodableLayout, RgbaImage, imageops};
|
||||
use size::Size;
|
||||
|
||||
mod kra;
|
||||
mod utils;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Message {
|
||||
ImagePicked(Result<PathBuf, String>),
|
||||
ImageDisplayReady(Result<widget::image::Allocation, widget::image::Error>),
|
||||
SavePathPicked(Result<PathBuf, String>),
|
||||
|
||||
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<String>,
|
||||
info: Option<String>,
|
||||
infobar_shown: bool,
|
||||
|
||||
clipboard: Option<Clipboard>,
|
||||
}
|
||||
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) {
|
||||
|
|
@ -52,68 +40,39 @@ impl State {
|
|||
(state, Task::none())
|
||||
}
|
||||
fn view(&self) -> Element<'_, Message, Theme, Renderer> {
|
||||
let main = self.image_display.as_ref().map_or_else(
|
||||
|| {
|
||||
widget::container(widget::text(include_str!("usage.txt")))
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fill)
|
||||
.align_x(Alignment::Center)
|
||||
.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 main = Vec::new();
|
||||
|
||||
main.push(if let Some(allocation) = self.image_display.as_ref() {
|
||||
widget::image::viewer(allocation.handle().clone())
|
||||
.filter_method(self.image_filter)
|
||||
.max_scale(50.)
|
||||
.min_scale(1.)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
} else {
|
||||
widget::container(widget::text(include_str!("usage.txt")))
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fill)
|
||||
.align_x(Alignment::Center)
|
||||
.align_y(Alignment::Center)
|
||||
.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(),
|
||||
);
|
||||
main.push(widget::text(error).style(widget::text::danger).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(),
|
||||
);
|
||||
main.push(widget::text(self.title()).into());
|
||||
}
|
||||
|
||||
widget::column([main, widget::row(bar).into()]).into()
|
||||
widget::column(main).into()
|
||||
}
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::ImagePicked(result) => match result.and_then(|path| self.load_image(path)) {
|
||||
Err(e) => self.error = Some(e),
|
||||
Ok(task) => {
|
||||
self.error = None;
|
||||
return task;
|
||||
}
|
||||
},
|
||||
Message::ImageDisplayReady(result) => {
|
||||
self.image_display = Some(result.unwrap());
|
||||
}
|
||||
Message::SavePathPicked(result) => {
|
||||
self.error = result
|
||||
.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()),
|
||||
)
|
||||
})
|
||||
.err();
|
||||
}
|
||||
|
||||
Message::Event(iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key,
|
||||
|
|
@ -124,30 +83,39 @@ impl State {
|
|||
Key::Named(key::Named::Tab) => {
|
||||
if modifiers.shift() {
|
||||
return widget::operation::focus_previous();
|
||||
} else {
|
||||
return widget::operation::focus_next();
|
||||
}
|
||||
return widget::operation::focus_next();
|
||||
}
|
||||
|
||||
Key::Character("o") => {
|
||||
return Task::perform(utils::pick_image(), Message::ImagePicked);
|
||||
}
|
||||
Key::Character("o") => match self.pick_and_load_image() {
|
||||
Ok(task) => {
|
||||
self.error = None;
|
||||
return task;
|
||||
}
|
||||
Err(e) => self.error = Some(e),
|
||||
},
|
||||
|
||||
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();
|
||||
}
|
||||
},
|
||||
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("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();
|
||||
}
|
||||
};
|
||||
}
|
||||
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("f") => {
|
||||
self.image_filter = match self.image_filter {
|
||||
widget::image::FilterMethod::Linear => widget::image::FilterMethod::Nearest,
|
||||
|
|
@ -159,58 +127,11 @@ impl State {
|
|||
}
|
||||
|
||||
Key::Character("s") => {
|
||||
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());
|
||||
self.save_image();
|
||||
}
|
||||
|
||||
Key::Character("q") => return window::latest().and_then(window::close),
|
||||
|
||||
Key::Character("y") | Key::Character("c") => {
|
||||
if let Some(image) = self.image.as_ref() {
|
||||
match match self.clipboard.take() {
|
||||
Some(c) => Ok(c),
|
||||
None => Clipboard::new(),
|
||||
} {
|
||||
Ok(mut clipboard) => {
|
||||
self.error = clipboard
|
||||
.set_image(utils::arboard_from_rgbaimage(image))
|
||||
.map_err(|e| e.to_string())
|
||||
.err();
|
||||
self.clipboard = Some(clipboard);
|
||||
}
|
||||
Err(e) => self.error = Some(e.to_string()),
|
||||
}
|
||||
} else {
|
||||
self.error = Some("no image to yank".into());
|
||||
}
|
||||
}
|
||||
Key::Character("p") | Key::Character("v") => {
|
||||
match match self.clipboard.take() {
|
||||
Some(c) => Ok(c),
|
||||
None => Clipboard::new(),
|
||||
} {
|
||||
Ok(mut clipboard) => {
|
||||
let result = clipboard.get_image().map(utils::rgbaimage_from_arboard);
|
||||
|
||||
self.error = result.as_ref().map_err(|e| e.to_string()).err();
|
||||
self.clipboard = Some(clipboard);
|
||||
|
||||
if let Ok(image) = result {
|
||||
self.info = Some(format!("{}x{}", image.width(), image.height(),));
|
||||
if image.dimensions() < (100, 100) {
|
||||
self.image_filter = widget::image::FilterMethod::Nearest;
|
||||
}
|
||||
self.image = Some(image);
|
||||
return self.allocate_image();
|
||||
}
|
||||
}
|
||||
Err(e) => self.error = Some(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
// ignore unused keys
|
||||
_ => {}
|
||||
},
|
||||
|
|
@ -235,50 +156,33 @@ impl State {
|
|||
fn load_image(&mut self, path: impl AsRef<Path>) -> Result<Task<Message>, String> {
|
||||
utils::load_image(&path).map(|image| {
|
||||
self.info = Some(format!(
|
||||
"{} {} {}x{}",
|
||||
"{} {}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())
|
||||
),
|
||||
.map(OsStr::to_string_lossy)
|
||||
.unwrap_or(Cow::Borrowed("[no file]")),
|
||||
image.width(),
|
||||
image.height(),
|
||||
));
|
||||
if image.dimensions() < (100, 100) {
|
||||
self.image_filter = widget::image::FilterMethod::Nearest;
|
||||
}
|
||||
self.image = Some(image);
|
||||
self.allocate_image()
|
||||
self.allocate_image(image)
|
||||
})
|
||||
}
|
||||
fn pick_and_load_image(&mut self) -> Result<Task<Message>, String> {
|
||||
utils::pick_image().and_then(|path| self.load_image(path))
|
||||
}
|
||||
|
||||
fn allocate_image(&self) -> Task<Message> {
|
||||
let Some(image) = self.image.as_ref() else {
|
||||
eprintln!("no image to allocate");
|
||||
return Task::none();
|
||||
};
|
||||
fn save_image(&mut self) {
|
||||
self.error = utils::save_image(self.image.as_ref()).err();
|
||||
}
|
||||
|
||||
fn allocate_image(&self, image: RgbaImage) -> Task<Message> {
|
||||
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()) },
|
||||
image.into_vec(),
|
||||
))
|
||||
.map(Message::ImageDisplayReady)
|
||||
}
|
||||
|
|
@ -287,21 +191,19 @@ impl State {
|
|||
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.info.as_ref() {
|
||||
Some(info) => format!(
|
||||
"imagey {info} {}",
|
||||
utils::string_from_filter_type(self.image_filter),
|
||||
),
|
||||
None => "imagey".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), iced::Error> {
|
||||
jxl_oxide::integration::register_image_decoding_hook();
|
||||
kra_image_integration::register();
|
||||
kra::register();
|
||||
|
||||
iced::application(State::new, State::update, State::view)
|
||||
.subscription(State::subscription)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,3 @@
|
|||
'b' to toggle the bar
|
||||
's' to save the image
|
||||
'q' to quit
|
||||
|
||||
'y' or 'c' to yank
|
||||
'p' or 'v' to put
|
||||
|
|
|
|||
77
src/utils.rs
77
src/utils.rs
|
|
@ -1,18 +1,26 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use iced::widget;
|
||||
use image::{DynamicImage, ImageDecoder, ImageReader, ImageResult, RgbaImage};
|
||||
use rfd::AsyncFileDialog;
|
||||
use rfd::FileDialog;
|
||||
|
||||
pub fn image_from_handle(handle: widget::image::Handle) -> RgbaImage {
|
||||
match handle {
|
||||
widget::image::Handle::Rgba {
|
||||
id,
|
||||
width,
|
||||
height,
|
||||
pixels,
|
||||
} => RgbaImage::from_raw(width, height, pixels.as_ref()).unwrap(),
|
||||
_ => panic!("handle should always hold rgba data"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_image(path: impl AsRef<Path>) -> Result<RgbaImage, String> {
|
||||
load_image_impl(path).map_err(|e| e.to_string())
|
||||
_load_image(path).map_err(|e| e.to_string())
|
||||
}
|
||||
fn load_image_impl(path: impl AsRef<Path>) -> ImageResult<RgbaImage> {
|
||||
let mut decoder = ImageReader::open(&path)?
|
||||
fn _load_image(path: impl AsRef<Path>) -> ImageResult<RgbaImage> {
|
||||
let mut decoder = ImageReader::open(path)?
|
||||
.with_guessed_format()?
|
||||
.into_decoder()?;
|
||||
|
||||
|
|
@ -20,22 +28,12 @@ fn load_image_impl(path: impl AsRef<Path>) -> ImageResult<RgbaImage> {
|
|||
|
||||
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")
|
||||
{
|
||||
decoded_image.apply_orientation(oreintation);
|
||||
}
|
||||
decoded_image.apply_orientation(oreintation);
|
||||
|
||||
Ok(decoded_image.into_rgba8())
|
||||
}
|
||||
pub async fn pick_image() -> Result<PathBuf, String> {
|
||||
let Some(filehandle) = AsyncFileDialog::new()
|
||||
pub fn pick_image() -> Result<PathBuf, String> {
|
||||
let Some(path) = FileDialog::new()
|
||||
.add_filter(
|
||||
"image",
|
||||
&[
|
||||
|
|
@ -44,41 +42,32 @@ pub async fn pick_image() -> Result<PathBuf, String> {
|
|||
],
|
||||
)
|
||||
.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, String> {
|
||||
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())
|
||||
if let Err(e) = image.save(&path) {
|
||||
return Err(e.to_string());
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub const fn string_from_filter_type(f: widget::image::FilterMethod) -> &'static str {
|
||||
pub fn string_from_filter_type(f: widget::image::FilterMethod) -> &'static str {
|
||||
match f {
|
||||
widget::image::FilterMethod::Linear => "bilinear",
|
||||
widget::image::FilterMethod::Nearest => "nearest neighbor",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn arboard_from_rgbaimage<'a>(image: &'a RgbaImage) -> arboard::ImageData<'a> {
|
||||
arboard::ImageData {
|
||||
width: image.width() as usize,
|
||||
height: image.height() as usize,
|
||||
bytes: Cow::Borrowed(image.as_ref()),
|
||||
}
|
||||
}
|
||||
pub fn rgbaimage_from_arboard(image: arboard::ImageData) -> RgbaImage {
|
||||
RgbaImage::from_raw(
|
||||
image.width as u32,
|
||||
image.height as u32,
|
||||
image.bytes.into_owned(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue