Compare commits
5 changed files with 38 additions and 193 deletions
36
Cargo.lock
generated
36
Cargo.lock
generated
|
|
@ -812,8 +812,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"block2 0.6.2",
|
||||
"libc",
|
||||
"objc2 0.6.4",
|
||||
]
|
||||
|
||||
|
|
@ -1649,7 +1647,6 @@ dependencies = [
|
|||
"dirs",
|
||||
"iced",
|
||||
"image",
|
||||
"rfd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2722,12 +2719,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "portable-atomic"
|
||||
version = "1.13.1"
|
||||
|
|
@ -3028,33 +3019,6 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "rgb"
|
||||
version = "0.8.53"
|
||||
|
|
|
|||
|
|
@ -7,4 +7,3 @@ edition = "2024"
|
|||
iced = { version = "0.14.0", features = [ "image" ] }
|
||||
dirs = "6.0.0"
|
||||
image = "0.25.10"
|
||||
rfd = "0.17.2"
|
||||
|
|
|
|||
24
flake.nix
24
flake.nix
|
|
@ -40,7 +40,7 @@
|
|||
buildInputs = with pkgs; [
|
||||
];
|
||||
|
||||
src = ./.;
|
||||
src = craneLib.cleanCargoSource ./.;
|
||||
};
|
||||
|
||||
LD_LIBRARY_PATH = lib.makeLibraryPath dlDeps;
|
||||
|
|
@ -58,16 +58,6 @@
|
|||
desktopName = name;
|
||||
icon = name;
|
||||
exec = name;
|
||||
mimeTypes = [
|
||||
"image/png"
|
||||
"image/jpeg"
|
||||
"image/gif"
|
||||
"image/webp"
|
||||
"image/avif"
|
||||
"image/tiff"
|
||||
"image/bmp"
|
||||
"image/vnd.microsoft.icon" # .ico
|
||||
];
|
||||
};
|
||||
in
|
||||
craneLib.buildPackage (
|
||||
|
|
@ -75,16 +65,12 @@
|
|||
// {
|
||||
inherit cargoArtifacts;
|
||||
|
||||
nativeBuildInputs =
|
||||
commonArgs.nativeBuildInputs
|
||||
++ [
|
||||
nativeBuildInputs = commonArgs.nativeBuildInputs ++ [
|
||||
pkgs.autoPatchelfHook
|
||||
]
|
||||
++ lib.optional pkgs.stdenv.hostPlatform.isLinux pkgs.makeBinaryWrapper;
|
||||
];
|
||||
|
||||
buildInputs = commonArgs.buildInputs ++ [
|
||||
pkgs.libgcc
|
||||
pkgs.zenity
|
||||
];
|
||||
|
||||
runtimeDependencies = dlDeps;
|
||||
|
|
@ -94,10 +80,6 @@
|
|||
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 ]}
|
||||
'';
|
||||
}
|
||||
);
|
||||
|
|
|
|||
151
src/main.rs
151
src/main.rs
|
|
@ -1,12 +1,11 @@
|
|||
use std::{env, ffi::OsStr, fs};
|
||||
use std::env;
|
||||
|
||||
use iced::{
|
||||
Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, color, event,
|
||||
Color, Element, Renderer, Subscription, Task, Theme, color, event,
|
||||
keyboard::{self, Key, key},
|
||||
theme, widget, window,
|
||||
};
|
||||
use image::{EncodableLayout, ImageReader, RgbaImage, codecs::webp::WebPEncoder, imageops};
|
||||
use rfd::FileDialog;
|
||||
use image::{EncodableLayout, ImageReader, Pixel, Rgba, RgbaImage, imageops};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Message {
|
||||
|
|
@ -17,53 +16,43 @@ enum Message {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
struct State {
|
||||
image: Option<RgbaImage>,
|
||||
image: RgbaImage,
|
||||
image_display: Option<widget::image::Allocation>,
|
||||
|
||||
error: Option<String>,
|
||||
}
|
||||
impl State {
|
||||
fn new() -> (Self, Task<Message>) {
|
||||
let state = State {
|
||||
image_display: None,
|
||||
image: env::args().nth(1).and_then(|path| {
|
||||
Some(
|
||||
let image = if let Some(path) = env::args().nth(1) {
|
||||
ImageReader::open(path)
|
||||
.unwrap()
|
||||
.decode()
|
||||
.unwrap()
|
||||
.into_rgba8(),
|
||||
)
|
||||
}),
|
||||
.into_rgba8()
|
||||
} else {
|
||||
let mut image = RgbaImage::new(100, 100);
|
||||
|
||||
error: None,
|
||||
imageops::horizontal_gradient(
|
||||
&mut image,
|
||||
Rgba::from_slice(&[0, 0, 0, 0]),
|
||||
Rgba::from_slice(&[255, 255, 255, 255]),
|
||||
);
|
||||
|
||||
image
|
||||
};
|
||||
|
||||
let state = State {
|
||||
image_display: None,
|
||||
image,
|
||||
};
|
||||
let allocate_image = state.allocate_image();
|
||||
|
||||
(state, allocate_image)
|
||||
}
|
||||
fn view(&self) -> Element<'_, Message, Theme, Renderer> {
|
||||
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)
|
||||
.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()
|
||||
widget::image(allocation.handle()).expand(true).into()
|
||||
} else {
|
||||
widget::space().into()
|
||||
},
|
||||
])
|
||||
.into()
|
||||
}
|
||||
}
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
|
|
@ -85,34 +74,17 @@ impl State {
|
|||
}
|
||||
}
|
||||
|
||||
Key::Character("o") => {
|
||||
self.error = self.open_image();
|
||||
return self.allocate_image();
|
||||
}
|
||||
Key::Character("q") => return window::latest().and_then(window::close),
|
||||
|
||||
Key::Character("r") => {
|
||||
let Some(image) = self.image.as_ref() else {
|
||||
eprintln!("no image to rotate");
|
||||
return Task::none();
|
||||
};
|
||||
self.image = Some(imageops::rotate90(image));
|
||||
self.image = imageops::rotate90(&self.image);
|
||||
return self.allocate_image();
|
||||
}
|
||||
Key::Character("i") => {
|
||||
let Some(image) = self.image.as_mut() else {
|
||||
eprintln!("no image to invert");
|
||||
return Task::none();
|
||||
};
|
||||
imageops::invert(image);
|
||||
imageops::invert(&mut self.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
|
||||
_ => {}
|
||||
},
|
||||
|
|
@ -124,78 +96,11 @@ impl State {
|
|||
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> {
|
||||
let Some(image) = self.image.as_ref() else {
|
||||
eprintln!("no image to allocate");
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
widget::image::allocate(widget::image::Handle::from_rgba(
|
||||
image.width(),
|
||||
image.height(),
|
||||
unsafe { std::mem::transmute::<_, &'static [u8]>(image.as_bytes()) },
|
||||
self.image.width(),
|
||||
self.image.height(),
|
||||
unsafe { std::mem::transmute::<_, &'static [u8]>(self.image.as_bytes()) },
|
||||
))
|
||||
.map(Message::ImageDisplayReady)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in a new issue