Compare commits

..
5 changed files with 38 additions and 193 deletions

36
Cargo.lock generated
View file

@ -812,8 +812,6 @@ 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",
] ]
@ -1649,7 +1647,6 @@ dependencies = [
"dirs", "dirs",
"iced", "iced",
"image", "image",
"rfd",
] ]
[[package]] [[package]]
@ -2722,12 +2719,6 @@ 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"
@ -3028,33 +3019,6 @@ 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"

View file

@ -7,4 +7,3 @@ 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"

View file

@ -40,7 +40,7 @@
buildInputs = with pkgs; [ buildInputs = with pkgs; [
]; ];
src = ./.; src = craneLib.cleanCargoSource ./.;
}; };
LD_LIBRARY_PATH = lib.makeLibraryPath dlDeps; LD_LIBRARY_PATH = lib.makeLibraryPath dlDeps;
@ -58,16 +58,6 @@
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 (
@ -75,16 +65,12 @@
// { // {
inherit cargoArtifacts; inherit cargoArtifacts;
nativeBuildInputs = nativeBuildInputs = commonArgs.nativeBuildInputs ++ [
commonArgs.nativeBuildInputs pkgs.autoPatchelfHook
++ [ ];
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;
@ -94,10 +80,6 @@
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 ]}
''; '';
} }
); );

View file

@ -1,12 +1,11 @@
use std::{env, ffi::OsStr, fs}; use std::env;
use iced::{ use iced::{
Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, color, event, Color, Element, Renderer, Subscription, Task, Theme, color, event,
keyboard::{self, Key, key}, keyboard::{self, Key, key},
theme, widget, window, theme, widget, window,
}; };
use image::{EncodableLayout, ImageReader, RgbaImage, codecs::webp::WebPEncoder, imageops}; use image::{EncodableLayout, ImageReader, Pixel, Rgba, RgbaImage, imageops};
use rfd::FileDialog;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
enum Message { enum Message {
@ -17,53 +16,43 @@ enum Message {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct State { struct State {
image: Option<RgbaImage>, image: 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: env::args().nth(1).and_then(|path| { image,
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> {
widget::column([ if let Some(allocation) = self.image_display.as_ref() {
if let Some(allocation) = self.image_display.as_ref() { widget::image(allocation.handle()).expand(true).into()
widget::image::viewer(allocation.handle().clone()) } else {
.width(Length::Fill) widget::space().into()
.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 {
@ -85,34 +74,17 @@ impl State {
} }
} }
Key::Character("o") => { Key::Character("q") => return window::latest().and_then(window::close),
self.error = self.open_image();
return self.allocate_image();
}
Key::Character("r") => { Key::Character("r") => {
let Some(image) = self.image.as_ref() else { self.image = imageops::rotate90(&self.image);
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") => {
let Some(image) = self.image.as_mut() else { imageops::invert(&mut self.image);
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
_ => {} _ => {}
}, },
@ -124,78 +96,11 @@ 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(
image.width(), self.image.width(),
image.height(), self.image.height(),
unsafe { std::mem::transmute::<_, &'static [u8]>(image.as_bytes()) }, unsafe { std::mem::transmute::<_, &'static [u8]>(self.image.as_bytes()) },
)) ))
.map(Message::ImageDisplayReady) .map(Message::ImageDisplayReady)
} }

View file

@ -1,5 +0,0 @@
"o" to open a new image
"r" to rotate
"i" to invert colors
"s" to save the image
"q" to quit