Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
549c478a8c |
|||
|
ad0b37d0c3 |
|||
|
c8810f3e86 |
|||
|
ab8dc257e9 |
|||
|
f8ab334cd7 |
|||
|
0523c8e926 |
|||
|
e703be030e |
|||
|
e9fb349352 |
|||
|
a786580da2 |
|||
|
68964f24fd |
5 changed files with 193 additions and 38 deletions
36
Cargo.lock
generated
36
Cargo.lock
generated
|
|
@ -812,6 +812,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
|
"block2 0.6.2",
|
||||||
|
"libc",
|
||||||
"objc2 0.6.4",
|
"objc2 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1647,6 +1649,7 @@ dependencies = [
|
||||||
"dirs",
|
"dirs",
|
||||||
"iced",
|
"iced",
|
||||||
"image",
|
"image",
|
||||||
|
"rfd",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2719,6 +2722,12 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pollster"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portable-atomic"
|
name = "portable-atomic"
|
||||||
version = "1.13.1"
|
version = "1.13.1"
|
||||||
|
|
@ -3019,6 +3028,33 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rfd"
|
||||||
|
version = "0.17.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220"
|
||||||
|
dependencies = [
|
||||||
|
"block2 0.6.2",
|
||||||
|
"dispatch2",
|
||||||
|
"js-sys",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"objc2 0.6.4",
|
||||||
|
"objc2-app-kit 0.3.2",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-foundation 0.3.2",
|
||||||
|
"percent-encoding",
|
||||||
|
"pollster",
|
||||||
|
"raw-window-handle",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"wayland-backend",
|
||||||
|
"wayland-client",
|
||||||
|
"wayland-protocols",
|
||||||
|
"web-sys",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rgb"
|
name = "rgb"
|
||||||
version = "0.8.53"
|
version = "0.8.53"
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,4 @@ edition = "2024"
|
||||||
iced = { version = "0.14.0", features = [ "image" ] }
|
iced = { version = "0.14.0", features = [ "image" ] }
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
image = "0.25.10"
|
image = "0.25.10"
|
||||||
|
rfd = "0.17.2"
|
||||||
|
|
|
||||||
26
flake.nix
26
flake.nix
|
|
@ -40,7 +40,7 @@
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
];
|
];
|
||||||
|
|
||||||
src = craneLib.cleanCargoSource ./.;
|
src = ./.;
|
||||||
};
|
};
|
||||||
|
|
||||||
LD_LIBRARY_PATH = lib.makeLibraryPath dlDeps;
|
LD_LIBRARY_PATH = lib.makeLibraryPath dlDeps;
|
||||||
|
|
@ -58,6 +58,16 @@
|
||||||
desktopName = name;
|
desktopName = name;
|
||||||
icon = name;
|
icon = name;
|
||||||
exec = name;
|
exec = name;
|
||||||
|
mimeTypes = [
|
||||||
|
"image/png"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/gif"
|
||||||
|
"image/webp"
|
||||||
|
"image/avif"
|
||||||
|
"image/tiff"
|
||||||
|
"image/bmp"
|
||||||
|
"image/vnd.microsoft.icon" # .ico
|
||||||
|
];
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
craneLib.buildPackage (
|
craneLib.buildPackage (
|
||||||
|
|
@ -65,12 +75,16 @@
|
||||||
// {
|
// {
|
||||||
inherit cargoArtifacts;
|
inherit cargoArtifacts;
|
||||||
|
|
||||||
nativeBuildInputs = commonArgs.nativeBuildInputs ++ [
|
nativeBuildInputs =
|
||||||
pkgs.autoPatchelfHook
|
commonArgs.nativeBuildInputs
|
||||||
];
|
++ [
|
||||||
|
pkgs.autoPatchelfHook
|
||||||
|
]
|
||||||
|
++ lib.optional pkgs.stdenv.hostPlatform.isLinux pkgs.makeBinaryWrapper;
|
||||||
|
|
||||||
buildInputs = commonArgs.buildInputs ++ [
|
buildInputs = commonArgs.buildInputs ++ [
|
||||||
pkgs.libgcc
|
pkgs.libgcc
|
||||||
|
pkgs.zenity
|
||||||
];
|
];
|
||||||
|
|
||||||
runtimeDependencies = dlDeps;
|
runtimeDependencies = dlDeps;
|
||||||
|
|
@ -80,6 +94,10 @@
|
||||||
postFixup = ''
|
postFixup = ''
|
||||||
mkdir -p "$out/share/applications"
|
mkdir -p "$out/share/applications"
|
||||||
ln -s "${desktopItem}"/share/applications/* "$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 ]}
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
||||||
163
src/main.rs
163
src/main.rs
|
|
@ -1,11 +1,12 @@
|
||||||
use std::env;
|
use std::{env, ffi::OsStr, fs};
|
||||||
|
|
||||||
use iced::{
|
use iced::{
|
||||||
Color, Element, 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, ImageReader, Pixel, Rgba, RgbaImage, imageops};
|
use image::{EncodableLayout, ImageReader, RgbaImage, codecs::webp::WebPEncoder, imageops};
|
||||||
|
use rfd::FileDialog;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum Message {
|
enum Message {
|
||||||
|
|
@ -16,43 +17,53 @@ enum Message {
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct State {
|
struct State {
|
||||||
image: RgbaImage,
|
image: Option<RgbaImage>,
|
||||||
image_display: Option<widget::image::Allocation>,
|
image_display: Option<widget::image::Allocation>,
|
||||||
|
|
||||||
|
error: Option<String>,
|
||||||
}
|
}
|
||||||
impl State {
|
impl State {
|
||||||
fn new() -> (Self, Task<Message>) {
|
fn new() -> (Self, Task<Message>) {
|
||||||
let image = if let Some(path) = env::args().nth(1) {
|
|
||||||
ImageReader::open(path)
|
|
||||||
.unwrap()
|
|
||||||
.decode()
|
|
||||||
.unwrap()
|
|
||||||
.into_rgba8()
|
|
||||||
} else {
|
|
||||||
let mut image = RgbaImage::new(100, 100);
|
|
||||||
|
|
||||||
imageops::horizontal_gradient(
|
|
||||||
&mut image,
|
|
||||||
Rgba::from_slice(&[0, 0, 0, 0]),
|
|
||||||
Rgba::from_slice(&[255, 255, 255, 255]),
|
|
||||||
);
|
|
||||||
|
|
||||||
image
|
|
||||||
};
|
|
||||||
|
|
||||||
let state = State {
|
let state = State {
|
||||||
image_display: None,
|
image_display: None,
|
||||||
image,
|
image: env::args().nth(1).and_then(|path| {
|
||||||
|
Some(
|
||||||
|
ImageReader::open(path)
|
||||||
|
.unwrap()
|
||||||
|
.decode()
|
||||||
|
.unwrap()
|
||||||
|
.into_rgba8(),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
|
||||||
|
error: None,
|
||||||
};
|
};
|
||||||
let allocate_image = state.allocate_image();
|
let allocate_image = state.allocate_image();
|
||||||
|
|
||||||
(state, allocate_image)
|
(state, allocate_image)
|
||||||
}
|
}
|
||||||
fn view(&self) -> Element<'_, Message, Theme, Renderer> {
|
fn view(&self) -> Element<'_, Message, Theme, Renderer> {
|
||||||
if let Some(allocation) = self.image_display.as_ref() {
|
widget::column([
|
||||||
widget::image(allocation.handle()).expand(true).into()
|
if let Some(allocation) = self.image_display.as_ref() {
|
||||||
} else {
|
widget::image::viewer(allocation.handle().clone())
|
||||||
widget::space().into()
|
.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()
|
||||||
|
},
|
||||||
|
if let Some(error) = self.error.as_ref() {
|
||||||
|
widget::text(error).style(widget::text::danger).into()
|
||||||
|
} else {
|
||||||
|
widget::space().into()
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
fn update(&mut self, message: Message) -> Task<Message> {
|
fn update(&mut self, message: Message) -> Task<Message> {
|
||||||
match message {
|
match message {
|
||||||
|
|
@ -74,17 +85,34 @@ impl State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Key::Character("q") => return window::latest().and_then(window::close),
|
Key::Character("o") => {
|
||||||
|
self.error = self.open_image();
|
||||||
|
return self.allocate_image();
|
||||||
|
}
|
||||||
|
|
||||||
Key::Character("r") => {
|
Key::Character("r") => {
|
||||||
self.image = imageops::rotate90(&self.image);
|
let Some(image) = self.image.as_ref() else {
|
||||||
|
eprintln!("no image to rotate");
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
self.image = Some(imageops::rotate90(image));
|
||||||
return self.allocate_image();
|
return self.allocate_image();
|
||||||
}
|
}
|
||||||
Key::Character("i") => {
|
Key::Character("i") => {
|
||||||
imageops::invert(&mut self.image);
|
let Some(image) = self.image.as_mut() else {
|
||||||
|
eprintln!("no image to invert");
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
imageops::invert(image);
|
||||||
return self.allocate_image();
|
return self.allocate_image();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Key::Character("s") => {
|
||||||
|
self.error = self.save_image();
|
||||||
|
}
|
||||||
|
|
||||||
|
Key::Character("q") => return window::latest().and_then(window::close),
|
||||||
|
|
||||||
// ignore unused keys
|
// ignore unused keys
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
|
|
@ -96,11 +124,78 @@ impl State {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_image(&mut self) -> Option<String> {
|
||||||
|
let Some(path) = FileDialog::new()
|
||||||
|
.add_filter(
|
||||||
|
"image",
|
||||||
|
&[
|
||||||
|
"png", "PNG", "jpg", "JPG", "jpeg", "JPEG", "avif", "bmp", "exr", "ff", "gif",
|
||||||
|
"hdr", "ico", "pnm", "qoi", "tga", "tiff", "webp",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.pick_file()
|
||||||
|
else {
|
||||||
|
return Some("no path to open provided".into());
|
||||||
|
};
|
||||||
|
|
||||||
|
let reader = match ImageReader::open(path) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => return Some(format!("failed to create reader: {e}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let decoded_image = match reader.decode() {
|
||||||
|
Ok(i) => i,
|
||||||
|
Err(e) => return Some(format!("failed to decode image: {e}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.image = Some(decoded_image.into_rgba8());
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_image(&self) -> Option<String> {
|
||||||
|
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());
|
||||||
|
};
|
||||||
|
|
||||||
|
match fs::File::create(&path) {
|
||||||
|
Err(e) => return Some(format!("failed to create file: {e}")),
|
||||||
|
Ok(file) => {
|
||||||
|
let maybe_encoder = match path.extension().map(OsStr::to_string_lossy).as_deref() {
|
||||||
|
Some("webp") => Some(WebPEncoder::new_lossless(file)),
|
||||||
|
|
||||||
|
None | Some(_) => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = if let Some(encoder) = maybe_encoder {
|
||||||
|
image.write_with_encoder(encoder)
|
||||||
|
} else {
|
||||||
|
image.save(path)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
return Some(format!("failed to save image: {e}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn allocate_image(&self) -> Task<Message> {
|
fn allocate_image(&self) -> Task<Message> {
|
||||||
|
let Some(image) = self.image.as_ref() else {
|
||||||
|
eprintln!("no image to allocate");
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
|
||||||
widget::image::allocate(widget::image::Handle::from_rgba(
|
widget::image::allocate(widget::image::Handle::from_rgba(
|
||||||
self.image.width(),
|
image.width(),
|
||||||
self.image.height(),
|
image.height(),
|
||||||
unsafe { std::mem::transmute::<_, &'static [u8]>(self.image.as_bytes()) },
|
unsafe { std::mem::transmute::<_, &'static [u8]>(image.as_bytes()) },
|
||||||
))
|
))
|
||||||
.map(Message::ImageDisplayReady)
|
.map(Message::ImageDisplayReady)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
src/usage.txt
Normal file
5
src/usage.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
"o" to open a new image
|
||||||
|
"r" to rotate
|
||||||
|
"i" to invert colors
|
||||||
|
"s" to save the image
|
||||||
|
"q" to quit
|
||||||
Loading…
Reference in a new issue