From 5a83fe39e9f08786134bdb2dfa510acd9297e11a Mon Sep 17 00:00:00 2001 From: electria Date: Wed, 29 Jul 2026 15:58:08 -0700 Subject: [PATCH 01/32] feat: switch to pure-rust jxl decoder this makes the logic drastically simpler; as it comes with image crate integration. however, images can no longer be saved as JXL with this version (as jxl-oxide is decoder-only) --- Cargo.lock | 301 ++++++++++++++++++++++++++++++++++----------------- Cargo.toml | 6 +- flake.nix | 1 - src/main.rs | 2 + src/utils.rs | 147 +++++++------------------ 5 files changed, 247 insertions(+), 210 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 20e11eb..8cf4097 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,6 +55,21 @@ dependencies = [ "equator", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "android-activity" version = "0.6.1" @@ -445,28 +460,13 @@ dependencies = [ ] [[package]] -name = "bon" -version = "3.9.3" +name = "brotli-decompressor" +version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a602c73c7b0148ec6d12af6fd5cc7a46e2eacc8878271a999abac56eed12f561" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" dependencies = [ - "bon-macros", - "rustversion", -] - -[[package]] -name = "bon-macros" -version = "3.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f" -dependencies = [ - "darling", - "ident_case", - "prettyplease", - "proc-macro2", - "quote", - "rustversion", - "syn", + "alloc-no-stdlib", + "alloc-stdlib", ] [[package]] @@ -847,40 +847,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core", - "quote", - "syn", -] - [[package]] name = "dav1d" version = "0.11.1" @@ -1757,12 +1723,6 @@ dependencies = [ "winit", ] -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "image" version = "0.25.10" @@ -1806,7 +1766,7 @@ dependencies = [ "dirs", "iced", "image", - "jpegxl-rs", + "jxl-oxide", "rfd", ] @@ -1914,29 +1874,6 @@ dependencies = [ "libc", ] -[[package]] -name = "jpegxl-rs" -version = "0.14.0+libjxl-0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7d16fe73fb6d2e16b986392b716409c6ae6fabd35769d66bea391b016cdbc9" -dependencies = [ - "bon", - "byteorder", - "half", - "image", - "jpegxl-sys", - "thiserror 2.0.18", -] - -[[package]] -name = "jpegxl-sys" -version = "0.12.1+libjxl-0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00cb4f7ffb45ee4327e9ec6ca1a732f3abef470ee008f7d4be3afb414075cd7d" -dependencies = [ - "pkg-config", -] - [[package]] name = "js-sys" version = "0.3.103" @@ -1948,6 +1885,186 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jxl-bitstream" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b480e752277e29eb4054f69546887a9b84656fe78c08f54ba5850ced98a378fe" +dependencies = [ + "tracing", +] + +[[package]] +name = "jxl-coding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd972bcd125e776f1eb241ac50e39f956095a1c2770c64736c968f8946bd9a3c" +dependencies = [ + "jxl-bitstream", + "tracing", +] + +[[package]] +name = "jxl-color" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f316b1358c1711755b3ee8e8cb5c4a1dad12e796233088a7a513440782de80b2" +dependencies = [ + "jxl-bitstream", + "jxl-coding", + "jxl-grid", + "jxl-image", + "jxl-oxide-common", + "jxl-threadpool", + "tracing", +] + +[[package]] +name = "jxl-frame" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d967c6fd669c7c01060b5022d8835fa82fd46b06ffc98b549f17600a097c2b3" +dependencies = [ + "jxl-bitstream", + "jxl-coding", + "jxl-grid", + "jxl-image", + "jxl-modular", + "jxl-oxide-common", + "jxl-threadpool", + "jxl-vardct", + "tracing", +] + +[[package]] +name = "jxl-grid" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01671307879a033bfa52e6e8784b941aca770b3f3a7d33830b455b6844f793fb" +dependencies = [ + "tracing", +] + +[[package]] +name = "jxl-image" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f752d62577c702a94dbbce4045caf08cb58639e8a4d56464b40ecf33ffe565" +dependencies = [ + "jxl-bitstream", + "jxl-grid", + "jxl-oxide-common", + "tracing", +] + +[[package]] +name = "jxl-jbr" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35d032bcec660647828527ff42c6f5776d2fd44b8357f9f6d9ac6dc07218e46" +dependencies = [ + "brotli-decompressor", + "jxl-bitstream", + "jxl-frame", + "jxl-grid", + "jxl-image", + "jxl-modular", + "jxl-oxide-common", + "jxl-threadpool", + "jxl-vardct", + "tracing", +] + +[[package]] +name = "jxl-modular" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2f045b24c738dd91d482be385512b512721ae08a671bd4b27bf1c47f215235" +dependencies = [ + "jxl-bitstream", + "jxl-coding", + "jxl-grid", + "jxl-oxide-common", + "jxl-threadpool", + "tracing", +] + +[[package]] +name = "jxl-oxide" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d36c662923f47586880211f3bc7c0d83fb3a9b410d278c7bde93450748abeef3" +dependencies = [ + "brotli-decompressor", + "bytemuck", + "image", + "jxl-bitstream", + "jxl-color", + "jxl-frame", + "jxl-grid", + "jxl-image", + "jxl-jbr", + "jxl-oxide-common", + "jxl-render", + "jxl-threadpool", + "tracing", +] + +[[package]] +name = "jxl-oxide-common" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62394c5021b3a9e7e0dbb2d639d555d019090c9946c39f6d3b09d390db4157b" +dependencies = [ + "jxl-bitstream", +] + +[[package]] +name = "jxl-render" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34386bfdb6a19b5a30cc9beb4d475d537422c31ae8c39bb69640fcce3fcaf19" +dependencies = [ + "bytemuck", + "jxl-bitstream", + "jxl-coding", + "jxl-color", + "jxl-frame", + "jxl-grid", + "jxl-image", + "jxl-modular", + "jxl-oxide-common", + "jxl-threadpool", + "jxl-vardct", + "tracing", +] + +[[package]] +name = "jxl-threadpool" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f15eb830aa77a7f21148d72e153562a26bfe570139bd4922eab1908dd499d3" +dependencies = [ + "rayon", + "rayon-core", + "tracing", +] + +[[package]] +name = "jxl-vardct" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce72a18c6d3a47172ab6c479be2bdb56f22066b5d7092663f03b4490820b4511" +dependencies = [ + "jxl-bitstream", + "jxl-coding", + "jxl-grid", + "jxl-modular", + "jxl-oxide-common", + "jxl-threadpool", + "tracing", +] + [[package]] name = "kamadak-exif" version = "0.6.1" @@ -2953,16 +3070,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -3639,12 +3746,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "svg_fmt" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index 519eeb1..bbe6fcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,12 @@ license = "AGPL-3.0-or-later" iced = { version = "0.14.0", features = [ "image" ] } dirs = "6.0.0" rfd = "0.17.2" -jpegxl-rs = "0.14.0" [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" ] diff --git a/flake.nix b/flake.nix index ddb2efc..72d435c 100644 --- a/flake.nix +++ b/flake.nix @@ -39,7 +39,6 @@ pkg-config ]; buildInputs = with pkgs; [ - libjxl dav1d ]; diff --git a/src/main.rs b/src/main.rs index 3d8618d..18d35a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -175,6 +175,8 @@ impl State { } fn main() -> Result<(), iced::Error> { + jxl_oxide::integration::register_image_decoding_hook(); + iced::application(State::new, State::update, State::view) .subscription(State::subscription) .title(State::title) diff --git a/src/utils.rs b/src/utils.rs index 961a27d..1477a4e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,91 +1,50 @@ -use std::{ - ffi::OsStr, - fs, - io::Write, - path::{Path, PathBuf}, -}; +use std::path::{Path, PathBuf}; use image::{DynamicImage, ImageDecoder, ImageReader, RgbaImage}; -use jpegxl_rs::image::ToDynamic; use rfd::FileDialog; pub fn load_image(path: impl AsRef) -> Result { 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() - )); - } - }; - - match jpegxl_rs::decoder_builder() - .build() - .unwrap() - .decode_to_image(&data) - { - 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 + 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); + Ok(decoded_image.into_rgba8()) } pub fn pick_image() -> Result { @@ -114,36 +73,8 @@ pub fn save_image(image: Option<&RgbaImage>) -> Result<(), String> { return Err("no path to save provided".into()); }; - let mut file = match fs::File::create(&path) { - Ok(f) => f, - Err(e) => return Err(format!("failed to create file '{}': {e}", path.display())), - }; - - 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::(&rgb_image, rgb_image.width(), rgb_image.height()) { - Ok(j) => j, - Err(e) => { - return Err(format!("failed to encode jxl '{}': {e}", path.display())); - } - }; - - 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())); - }; - } + if let Err(e) = image.save(&path) { + return Err(format!("failed to save '{}': {e}", path.display())); }; Ok(()) From e7e1400c3adf38fd1c10f209696505600c17f85e Mon Sep 17 00:00:00 2001 From: electria Date: Wed, 29 Jul 2026 16:01:19 -0700 Subject: [PATCH 02/32] style: avoid toml curly braces else it causes messy diffs and is generally less writable and readable --- Cargo.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index bbe6fcb..487f499 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,10 +5,13 @@ edition = "2024" license = "AGPL-3.0-or-later" [dependencies] -iced = { version = "0.14.0", features = [ "image" ] } dirs = "6.0.0" rfd = "0.17.2" +[dependencies.iced] +version = "0.14.0" +features = [ "image" ] + [dependencies.image] version = "0.25.10" features = [ "avif-native" ] From f279c3aa15fc700ecae5ff28afe50c9562fe35f9 Mon Sep 17 00:00:00 2001 From: electria Date: Thu, 30 Jul 2026 19:19:23 -0700 Subject: [PATCH 03/32] feat: f to switch FilterMethod useful to scale pixelart images nicely. This makes me consider expanding the status bar to include info; partially because I don't use window decorations usually (so I don't see the info in the title) --- src/main.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main.rs b/src/main.rs index 18d35a0..9ff87f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ enum Message { struct State { image: Option, image_display: Option, + image_filter: widget::image::FilterMethod, error: Option, } @@ -40,6 +41,7 @@ impl State { widget::column([ if let Some(allocation) = self.image_display.as_ref() { widget::image::viewer(allocation.handle().clone()) + .filter_method(self.image_filter) .width(Length::Fill) .height(Length::Fill) .into() @@ -107,6 +109,12 @@ impl State { } }; } + Key::Character("f") => { + self.image_filter = match self.image_filter { + widget::image::FilterMethod::Linear => widget::image::FilterMethod::Nearest, + widget::image::FilterMethod::Nearest => widget::image::FilterMethod::Linear, + } + } Key::Character("s") => { self.save_image(); From 97ce6754ddf870ff6701d22dcb468421549f22e9 Mon Sep 17 00:00:00 2001 From: electria Date: Thu, 30 Jul 2026 19:21:12 -0700 Subject: [PATCH 04/32] feat: change scale limits zooming in a lot is sometimes useful, but zooming out is basically useless and annoying (when you want to put it back to covering the full window) if you really need to zoom out, you can always resize the window --- src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main.rs b/src/main.rs index 9ff87f7..7a4cba4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,6 +42,8 @@ impl State { 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() From 7ce87e2870fb907331551ceeea0265bcbb5fd1ae Mon Sep 17 00:00:00 2001 From: electria Date: Thu, 30 Jul 2026 20:17:21 -0700 Subject: [PATCH 05/32] feat: optional infobar literally the title; except on the bottom --- src/main.rs | 76 +++++++++++++++++++++++++++++++++------------------- src/utils.rs | 8 ++++++ 2 files changed, 56 insertions(+), 28 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7a4cba4..522e8c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::{env, path::Path}; +use std::{borrow::Cow, env, ffi::OsStr, path::Path}; use iced::{ Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, color, event, @@ -23,6 +23,8 @@ struct State { image_filter: widget::image::FilterMethod, error: Option, + info: Option, + infobar_shown: bool, } impl State { fn new() -> (Self, Task) { @@ -38,30 +40,33 @@ impl State { (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()) - .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() - }, - if let Some(error) = self.error.as_ref() { - widget::text(error).style(widget::text::danger).into() - } else { - widget::space().into() - }, - ]) - .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() + }); + + if let Some(error) = self.error.as_ref() { + main.push(widget::text(error).style(widget::text::danger).into()); + } + if self.infobar_shown && self.info.is_some() { + main.push(widget::text(self.title()).into()); + } + + widget::column(main).into() } fn update(&mut self, message: Message) -> Task { match message { @@ -117,6 +122,9 @@ impl State { widget::image::FilterMethod::Nearest => widget::image::FilterMethod::Linear, } } + Key::Character("b") => { + self.infobar_shown = !self.infobar_shown; + } Key::Character("s") => { self.save_image(); @@ -146,7 +154,16 @@ impl State { } fn load_image(&mut self, path: impl AsRef) -> Result, String> { - utils::load_image(path).map(|image| { + utils::load_image(&path).map(|image| { + self.info = Some(format!( + "{} {}x{}", + path.as_ref() + .file_name() + .map(OsStr::to_string_lossy) + .unwrap_or(Cow::Borrowed("[no file]")), + image.width(), + image.height(), + )); self.image = Some(image); self.allocate_image() }) @@ -177,8 +194,11 @@ impl State { event::listen().map(Message::Event) } fn title(&self) -> String { - match self.image.as_ref() { - Some(image) => format!("imagey {}x{}", image.width(), image.height()), + match self.info.as_ref() { + Some(info) => format!( + "imagey {info} {}", + utils::string_from_filter_type(self.image_filter), + ), None => "imagey".into(), } } diff --git a/src/utils.rs b/src/utils.rs index 1477a4e..cadc05a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,6 @@ use std::path::{Path, PathBuf}; +use iced::widget; use image::{DynamicImage, ImageDecoder, ImageReader, RgbaImage}; use rfd::FileDialog; @@ -79,3 +80,10 @@ pub fn save_image(image: Option<&RgbaImage>) -> Result<(), String> { Ok(()) } + +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", + } +} From 9bdd556c9df9e4fd3b5f8d9f858603dafcd42795 Mon Sep 17 00:00:00 2001 From: electria Date: Thu, 30 Jul 2026 20:26:21 -0700 Subject: [PATCH 06/32] refactor: use newer helper method in State::new this should keep it from printing "no image to allocate" each time when started without an argument --- src/main.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index 522e8c7..4de72f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,14 +30,14 @@ impl State { fn new() -> (Self, Task) { let mut state = State::default(); - 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(); + if let Some(path) = env::args().nth(1) { + match state.load_image(path) { + Err(e) => state.error = Some(e), + Ok(task) => return (state, task), + } + } - (state, allocate_image) + (state, Task::none()) } fn view(&self) -> Element<'_, Message, Theme, Renderer> { let mut main = Vec::new(); From b1157474c9bd9d5a8ac4b7b0c4832d50f3be924a Mon Sep 17 00:00:00 2001 From: electria Date: Thu, 30 Jul 2026 20:35:45 -0700 Subject: [PATCH 07/32] feat: simplify theme --- src/main.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 4de72f4..4168550 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, env, ffi::OsStr, path::Path}; use iced::{ - Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, color, event, + Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, event, keyboard::{self, Key, key}, theme, widget, window, }; @@ -213,12 +213,9 @@ fn main() -> Result<(), iced::Error> { .theme(Theme::custom( "custom", theme::Palette { - background: color!(0x080808), + background: Color::BLACK, text: Color::WHITE, - primary: color!(0xff00ff), - success: color!(0x00ff00), - warning: color!(0x880000), - danger: color!(0xff0000), + ..theme::Palette::DARK }, )) .run() From 50b5ed58b06be6f7fe3cbef36a6beddc71409ad7 Mon Sep 17 00:00:00 2001 From: electria Date: Thu, 30 Jul 2026 21:40:14 -0700 Subject: [PATCH 08/32] feat: add to usage reference --- src/usage.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/usage.txt b/src/usage.txt index 94cf3cf..b23e1ad 100644 --- a/src/usage.txt +++ b/src/usage.txt @@ -1,5 +1,7 @@ "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 From 2e8a6c8546f5a86d0154fc6abfb6b6930ba49762 Mon Sep 17 00:00:00 2001 From: electria Date: Thu, 30 Jul 2026 21:43:06 -0700 Subject: [PATCH 09/32] feat: use single quotes in usage to reduce noise --- src/usage.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/usage.txt b/src/usage.txt index b23e1ad..1d4c6c1 100644 --- a/src/usage.txt +++ b/src/usage.txt @@ -1,7 +1,7 @@ -"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 +'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 From fc92ff60e0925377ed25d41e777bbfa6435d8f9d Mon Sep 17 00:00:00 2001 From: electria Date: Thu, 30 Jul 2026 22:41:17 -0700 Subject: [PATCH 10/32] feat: kra (krita) support --- Cargo.lock | 323 +++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/utils.rs | 139 ++++++++++++++++------ 3 files changed, 430 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8cf4097..2664b09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aes" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8eb277bec05f56a0e0591f155a484cbd0f4f07ff2905051a48c72f004f7ed58" +dependencies = [ + "cipher", + "cpubits", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.12" @@ -428,6 +439,16 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", + "zeroize", +] + [[package]] name = "block2" version = "0.5.1" @@ -525,6 +546,15 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +[[package]] +name = "bzip2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" +dependencies = [ + "libbz2-rs-sys", +] + [[package]] name = "calloop" version = "0.13.0" @@ -610,6 +640,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clipboard-win" version = "5.4.1" @@ -649,6 +689,12 @@ dependencies = [ "x11rb", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "codespan-reporting" version = "0.12.0" @@ -685,6 +731,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "core-foundation" version = "0.9.4" @@ -779,6 +837,21 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -832,6 +905,15 @@ dependencies = [ "wgpu", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctor" version = "0.10.1" @@ -841,6 +923,15 @@ dependencies = [ "dtor", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "cursor-icon" version = "1.2.0" @@ -869,6 +960,31 @@ dependencies = [ "system-deps", ] +[[package]] +name = "deflate64" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "ctutils", + "zeroize", +] + [[package]] name = "dirs" version = "6.0.0" @@ -1118,6 +1234,7 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -1341,8 +1458,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", + "wasm-bindgen", ] [[package]] @@ -1536,6 +1655,24 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest", +] + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + [[package]] name = "iced" version = "0.14.0" @@ -1768,6 +1905,7 @@ dependencies = [ "image", "jxl-oxide", "rfd", + "zip", ] [[package]] @@ -1786,6 +1924,15 @@ dependencies = [ "hashbrown 0.17.1", ] +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + [[package]] name = "interpolate_name" version = "0.2.4" @@ -2107,6 +2254,12 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" +[[package]] +name = "libbz2-rs-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" + [[package]] name = "libc" version = "0.2.186" @@ -2214,6 +2367,15 @@ version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +[[package]] +name = "lzma-rust2" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca93e534d1142d1d0dcca6d25fe302508a5dfb40b302802904577725ea0b695b" +dependencies = [ + "sha2", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -2433,6 +2595,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-derive" version = "0.4.2" @@ -2946,6 +3114,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" +[[package]] +name = "pbkdf2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -3055,6 +3233,18 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppmd-rust" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -3528,6 +3718,28 @@ dependencies = [ "serde_core", ] +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "2.0.1" @@ -3878,6 +4090,26 @@ dependencies = [ "zune-jpeg", ] +[[package]] +name = "time" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +dependencies = [ + "deranged", + "js-sys", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + [[package]] name = "tiny-skia" version = "0.11.4" @@ -4023,6 +4255,18 @@ dependencies = [ "core_maths", ] +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + [[package]] name = "uds_windows" version = "1.2.1" @@ -5027,6 +5271,85 @@ dependencies = [ "syn", ] +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zip" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" +dependencies = [ + "aes", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.4.3", + "hmac", + "indexmap", + "lzma-rust2", + "memchr", + "pbkdf2", + "ppmd-rust", + "sha1", + "time", + "typed-path", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zune-core" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 487f499..9015f09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ license = "AGPL-3.0-or-later" [dependencies] dirs = "6.0.0" rfd = "0.17.2" +zip = "8.6.0" [dependencies.iced] version = "0.14.0" diff --git a/src/utils.rs b/src/utils.rs index cadc05a..626ceb8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,50 +1,123 @@ -use std::path::{Path, PathBuf}; +use std::{ + fs, + io::{self, Read}, + path::{Path, PathBuf}, +}; use iced::widget; use image::{DynamicImage, ImageDecoder, ImageReader, RgbaImage}; use rfd::FileDialog; +use zip::ZipArchive; pub fn load_image(path: impl AsRef) -> Result { let path = path.as_ref(); - let reader = match ImageReader::open(path) { - Ok(r) => r, - Err(e) => { + let decoded_image = if path.extension().is_some_and(|ext| ext == "kra") { + let reader = match fs::File::open(path).map(io::BufReader::new) { + Ok(r) => r, + Err(e) => { + return Err(format!( + "failed to create reader for '{}': {e}", + path.display() + )); + } + }; + + let mut zip = match ZipArchive::new(reader) { + Ok(z) => z, + Err(e) => { + return Err(format!( + "failed to open zip archive '{}': {e}", + path.display() + )); + } + }; + + let mut reader = match zip.by_name("mergedimage.png") { + Ok(r) => r, + Err(e) => { + return Err(format!( + "failed to open file in zip archive '{}': {e}", + path.display() + )); + } + }; + + // 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(); + if let Err(e) = reader.read_to_end(&mut buf) { return Err(format!( - "failed to create reader for '{}': {e}", + "error when reading file in zip archive '{}': {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 image_reader = ImageReader::with_format(io::Cursor::new(buf), image::ImageFormat::Png); - let oreintation = match decoder.orientation() { - Ok(o) => o, - Err(e) => { - return Err(format!( - "failed to get oreintation of '{}': {e}", - path.display() - )); - } - }; + let decoder = match image_reader.into_decoder() { + Ok(d) => d, + Err(e) => { + return Err(format!( + "failed to create decoder for '{}': {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())); - } - }; + let 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 + } else { + let image_reader = match ImageReader::open(path) { + Ok(r) => r, + Err(e) => { + return Err(format!( + "failed to create image reader for '{}': {e}", + path.display() + )); + } + }; + + let mut decoder = match image_reader.into_decoder() { + Ok(d) => d, + Err(e) => { + return Err(format!( + "failed to create decoder for '{}': {e}", + path.display() + )); + } + }; + + // we don't need to check this for the kra branch + // since PNGs don't have metadata oreintation + 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()) } @@ -54,7 +127,7 @@ pub fn pick_image() -> Result { "image", &[ "png", "PNG", "jpg", "JPG", "jpeg", "JPEG", "avif", "jxl", "bmp", "exr", "ff", - "gif", "hdr", "ico", "pnm", "qoi", "tga", "tiff", "webp", + "gif", "hdr", "ico", "pnm", "qoi", "tga", "tiff", "webp", "kra", ], ) .pick_file() From c96a1cbdd1ed6fcc1de379299c86b2cac5444594 Mon Sep 17 00:00:00 2001 From: electria Date: Thu, 30 Jul 2026 22:54:56 -0700 Subject: [PATCH 11/32] feat: default to FilterMethod::Nearest on small image helps make pixelart sharp without the need to change it yourself --- src/main.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main.rs b/src/main.rs index 4168550..9d88b1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -164,6 +164,9 @@ impl State { image.width(), image.height(), )); + if image.dimensions() < (100, 100) { + self.image_filter = widget::image::FilterMethod::Nearest; + } self.image = Some(image); self.allocate_image() }) From 101adc7a89ba0750c1955b73e5074c7e819fb95f Mon Sep 17 00:00:00 2001 From: electria Date: Thu, 30 Jul 2026 23:40:14 -0700 Subject: [PATCH 12/32] refactor: split kra (krita) logic into image hook may further split this into its own crate; seems quite useful! (albeit somewhat short and easy) since there aren't any runtime dependancies, it could even go into image-extras maybe --- src/kra.rs | 41 +++++++++++++++ src/main.rs | 2 + src/utils.rs | 143 +++++++++++++-------------------------------------- 3 files changed, 78 insertions(+), 108 deletions(-) create mode 100644 src/kra.rs diff --git a/src/kra.rs b/src/kra.rs new file mode 100644 index 0000000..5e3d626 --- /dev/null +++ b/src/kra.rs @@ -0,0 +1,41 @@ +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> { + let mut zip = ZipArchive::new(reader).map_err(|e| { + ImageError::Decoding(DecodingError::new( + image::error::ImageFormatHint::PathExtension("kra".into()), + e, + )) + })?; + + let mut reader = zip.by_name("mergedimage.png").map_err(|e| { + ImageError::Decoding(DecodingError::new( + image::error::ImageFormatHint::PathExtension("kra".into()), + e, + )) + })?; + + // 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()?)) +} diff --git a/src/main.rs b/src/main.rs index 9d88b1d..3e65e8d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ use iced::{ }; use image::{EncodableLayout, RgbaImage, imageops}; +mod kra; mod utils; #[derive(Clone, Debug)] @@ -209,6 +210,7 @@ impl State { fn main() -> Result<(), iced::Error> { jxl_oxide::integration::register_image_decoding_hook(); + kra::register(); iced::application(State::new, State::update, State::view) .subscription(State::subscription) diff --git a/src/utils.rs b/src/utils.rs index 626ceb8..3d3ea0b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,124 +1,51 @@ -use std::{ - fs, - io::{self, Read}, - path::{Path, PathBuf}, -}; +use std::path::{Path, PathBuf}; use iced::widget; use image::{DynamicImage, ImageDecoder, ImageReader, RgbaImage}; use rfd::FileDialog; -use zip::ZipArchive; pub fn load_image(path: impl AsRef) -> Result { let path = path.as_ref(); - let decoded_image = if path.extension().is_some_and(|ext| ext == "kra") { - let reader = match fs::File::open(path).map(io::BufReader::new) { - Ok(r) => r, - Err(e) => { - return Err(format!( - "failed to create reader for '{}': {e}", - path.display() - )); - } - }; - - let mut zip = match ZipArchive::new(reader) { - Ok(z) => z, - Err(e) => { - return Err(format!( - "failed to open zip archive '{}': {e}", - path.display() - )); - } - }; - - let mut reader = match zip.by_name("mergedimage.png") { - Ok(r) => r, - Err(e) => { - return Err(format!( - "failed to open file in zip archive '{}': {e}", - path.display() - )); - } - }; - - // 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(); - if let Err(e) = reader.read_to_end(&mut buf) { + let image_reader = match ImageReader::open(path) { + Ok(r) => r, + Err(e) => { return Err(format!( - "error when reading file in zip archive '{}': {e}", + "failed to create image reader for '{}': {e}", path.display() )); - }; - - let image_reader = ImageReader::with_format(io::Cursor::new(buf), image::ImageFormat::Png); - - let decoder = match image_reader.into_decoder() { - Ok(d) => d, - Err(e) => { - return Err(format!( - "failed to create decoder for '{}': {e}", - path.display() - )); - } - }; - - let decoded_image = match DynamicImage::from_decoder(decoder) { - Ok(i) => i, - Err(e) => { - return Err(format!("failed to decode image '{}': {e}", path.display())); - } - }; - - decoded_image - } else { - let image_reader = match ImageReader::open(path) { - Ok(r) => r, - Err(e) => { - return Err(format!( - "failed to create image reader for '{}': {e}", - path.display() - )); - } - }; - - let mut decoder = match image_reader.into_decoder() { - Ok(d) => d, - Err(e) => { - return Err(format!( - "failed to create decoder for '{}': {e}", - path.display() - )); - } - }; - - // we don't need to check this for the kra branch - // since PNGs don't have metadata oreintation - 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 + } }; + let mut decoder = match image_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); + Ok(decoded_image.into_rgba8()) } pub fn pick_image() -> Result { From 13976b91ea00ad2377bc04c25d8face6f9f363be Mon Sep 17 00:00:00 2001 From: electria Date: Thu, 30 Jul 2026 23:54:37 -0700 Subject: [PATCH 13/32] fix: add .kra (krita) support to mimeTypes --- flake.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/flake.nix b/flake.nix index 72d435c..a3d6d7d 100644 --- a/flake.nix +++ b/flake.nix @@ -70,6 +70,7 @@ "image/tiff" "image/bmp" "image/vnd.microsoft.icon" # .ico + "application/x-krita" ]; }; in From b8b4d8698c2c55db73018a7104bf430f78afbdeb Mon Sep 17 00:00:00 2001 From: electria Date: Fri, 31 Jul 2026 00:03:35 -0700 Subject: [PATCH 14/32] build: remove no-op wrapper argument --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index a3d6d7d..7d22772 100644 --- a/flake.nix +++ b/flake.nix @@ -102,7 +102,7 @@ ln -s "${desktopItem}"/share/applications/* "$out/share/applications/" '' + lib.optionalString pkgs.stdenv.hostPlatform.isLinux /* sh */ '' - wrapProgram $out/bin/${name} --inherit-argv0 \ + wrapProgram $out/bin/${name} \ --prefix PATH : ${lib.makeBinPath [ pkgs.zenity ]} ''; } From 57ad7dfb1ef6ce82b74610ba9bad2a8b1af497b2 Mon Sep 17 00:00:00 2001 From: electria Date: Fri, 31 Jul 2026 00:05:48 -0700 Subject: [PATCH 15/32] build: reduce indirection slightly --- flake.nix | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index 7d22772..2fc6470 100644 --- a/flake.nix +++ b/flake.nix @@ -45,8 +45,6 @@ src = ./.; }; - LD_LIBRARY_PATH = lib.makeLibraryPath dlDeps; - # Build *just* the cargo dependencies, # to reuse them for build and test derivations. cargoArtifacts = craneLib.buildDepsOnly commonArgs; @@ -123,7 +121,7 @@ }; devShells.default = craneLib.devShell { - inherit LD_LIBRARY_PATH; + LD_LIBRARY_PATH = lib.makeLibraryPath dlDeps; inputsFrom = [ crate ]; packages = [ pkgs.rust-analyzer ]; From fca4577c0f35c3bff69296f18419f6089e064ec5 Mon Sep 17 00:00:00 2001 From: electria Date: Fri, 31 Jul 2026 00:24:39 -0700 Subject: [PATCH 16/32] refactor(kra): factor out error mapping --- src/kra.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/kra.rs b/src/kra.rs index 5e3d626..3642370 100644 --- a/src/kra.rs +++ b/src/kra.rs @@ -15,19 +15,9 @@ pub fn register() -> bool { } fn hook<'a>(reader: GenericReader<'a>) -> ImageResult> { - let mut zip = ZipArchive::new(reader).map_err(|e| { - ImageError::Decoding(DecodingError::new( - image::error::ImageFormatHint::PathExtension("kra".into()), - e, - )) - })?; + let mut zip = ZipArchive::new(reader).map_err(to_image_error)?; - let mut reader = zip.by_name("mergedimage.png").map_err(|e| { - ImageError::Decoding(DecodingError::new( - image::error::ImageFormatHint::PathExtension("kra".into()), - e, - )) - })?; + 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 @@ -39,3 +29,12 @@ fn hook<'a>(reader: GenericReader<'a>) -> ImageResult Ok(Box::new(image_reader.into_decoder()?)) } + +fn to_image_error( + e: impl Into>, +) -> ImageError { + ImageError::Decoding(DecodingError::new( + image::error::ImageFormatHint::PathExtension("kra".into()), + e, + )) +} From 973703726f3ad009ff0a6af6b5bd1984896a5486 Mon Sep 17 00:00:00 2001 From: electria Date: Fri, 31 Jul 2026 09:16:38 -0700 Subject: [PATCH 17/32] feat: guess format from magic numbers as well this allows opening files with extensions that don't match the filetype --- src/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.rs b/src/utils.rs index 3d3ea0b..12512cf 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -7,7 +7,7 @@ use rfd::FileDialog; pub fn load_image(path: impl AsRef) -> Result { let path = path.as_ref(); - let image_reader = match ImageReader::open(path) { + let image_reader = match ImageReader::open(path).and_then(|r| r.with_guessed_format()) { Ok(r) => r, Err(e) => { return Err(format!( From e98f268e59f19853564f631771f093feb94fb968 Mon Sep 17 00:00:00 2001 From: electria Date: Fri, 31 Jul 2026 09:37:16 -0700 Subject: [PATCH 18/32] refactor: do not format each ImageError this makes the code much nicer, but also it reduces the amount of extra fluff in the error messages. --- src/utils.rs | 48 ++++++++++-------------------------------------- 1 file changed, 10 insertions(+), 38 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 12512cf..dafd1f2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,48 +1,20 @@ use std::path::{Path, PathBuf}; use iced::widget; -use image::{DynamicImage, ImageDecoder, ImageReader, RgbaImage}; +use image::{DynamicImage, ImageDecoder, ImageReader, ImageResult, RgbaImage}; use rfd::FileDialog; pub fn load_image(path: impl AsRef) -> Result { - let path = path.as_ref(); + _load_image(path).map_err(|e| e.to_string()) +} +fn _load_image(path: impl AsRef) -> ImageResult { + let mut decoder = ImageReader::open(path)? + .with_guessed_format()? + .into_decoder()?; - let image_reader = match ImageReader::open(path).and_then(|r| r.with_guessed_format()) { - Ok(r) => r, - Err(e) => { - return Err(format!( - "failed to create image reader for '{}': {e}", - path.display() - )); - } - }; + let oreintation = decoder.orientation()?; - let mut decoder = match image_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())); - } - }; + let mut decoded_image = DynamicImage::from_decoder(decoder)?; decoded_image.apply_orientation(oreintation); @@ -75,7 +47,7 @@ pub fn save_image(image: Option<&RgbaImage>) -> Result<(), String> { }; if let Err(e) = image.save(&path) { - return Err(format!("failed to save '{}': {e}", path.display())); + return Err(e.to_string()); }; Ok(()) From 5a84b56025c69c3e5ac78bb35921c95ebfa639c1 Mon Sep 17 00:00:00 2001 From: electria Date: Fri, 31 Jul 2026 09:55:08 -0700 Subject: [PATCH 19/32] build: use libdbus over zenity I completely misunderstood the RFD doc comments, thought that save file dialogs were not avaliable on linux outside of gtk this helps avoid the wrapper, which is nice; but I imagine it would fail if portals are unavaliable. --- flake.nix | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/flake.nix b/flake.nix index 2fc6470..40636f4 100644 --- a/flake.nix +++ b/flake.nix @@ -20,6 +20,9 @@ name = cargoToml.package.name; dlDeps = with pkgs; [ + # libdbus, for rfd + dbus + # needed for both x11 and wayland libxkbcommon libGL @@ -77,19 +80,13 @@ // { inherit cargoArtifacts; - nativeBuildInputs = - commonArgs.nativeBuildInputs - ++ [ - pkgs.autoPatchelfHook - ] - ++ lib.optional pkgs.stdenv.hostPlatform.isLinux pkgs.makeBinaryWrapper; + nativeBuildInputs = commonArgs.nativeBuildInputs ++ [ + pkgs.autoPatchelfHook + ]; - buildInputs = - commonArgs.buildInputs - ++ [ - pkgs.libgcc - ] - ++ lib.optional pkgs.stdenv.hostPlatform.isLinux pkgs.zenity; + buildInputs = commonArgs.buildInputs ++ [ + pkgs.libgcc + ]; runtimeDependencies = dlDeps; @@ -98,10 +95,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} \ - --prefix PATH : ${lib.makeBinPath [ pkgs.zenity ]} ''; } ); From 26c17d5cf9bf6ba47e03b212e3cf1db2233d34c0 Mon Sep 17 00:00:00 2001 From: electria Date: Fri, 31 Jul 2026 11:14:05 -0700 Subject: [PATCH 20/32] docs: add safety note --- src/main.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main.rs b/src/main.rs index 3e65e8d..5ad2bf3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -189,6 +189,18 @@ 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) From 754dd6126d0937819482a89e5ec2185d36ff239d Mon Sep 17 00:00:00 2001 From: electria Date: Fri, 31 Jul 2026 11:21:37 -0700 Subject: [PATCH 21/32] fix: wrong dbus output (?) --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 40636f4..d7fa343 100644 --- a/flake.nix +++ b/flake.nix @@ -21,7 +21,7 @@ dlDeps = with pkgs; [ # libdbus, for rfd - dbus + dbus.lib # needed for both x11 and wayland libxkbcommon From 94c905db386991fd1de615d43cc4da1ae78df86e Mon Sep 17 00:00:00 2001 From: electria Date: Fri, 31 Jul 2026 14:19:53 -0700 Subject: [PATCH 22/32] build: seperate linux-specific deps --- flake.nix | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/flake.nix b/flake.nix index d7fa343..5a57299 100644 --- a/flake.nix +++ b/flake.nix @@ -19,22 +19,25 @@ cargoToml = fromTOML (builtins.readFile ./Cargo.toml); name = cargoToml.package.name; - dlDeps = with pkgs; [ - # libdbus, for rfd - dbus.lib + dlDeps = + with pkgs; + [ + # needed for both x11 and wayland + libxkbcommon + libGL - # needed for both x11 and wayland - libxkbcommon - libGL - vulkan-loader + libx11 + libxcursor + libxi + libxcb + ] + ++ lib.optionals stdenv.hostPlatform.isLinux [ + # libdbus, for rfd + dbus.lib - wayland - - libx11 - libxcursor - libxi - libxcb - ]; + vulkan-loader + wayland + ]; commonArgs = { # all that's needed for artifacts and checks From 9d4e24e8099a2cffa6df0c1c87c724c629f65838 Mon Sep 17 00:00:00 2001 From: electria Date: Fri, 31 Jul 2026 14:36:36 -0700 Subject: [PATCH 23/32] refactor: apply clippy lints cargo clippy --no-deps -- --deny clippy::nursery --deny clippy::pedantic (does not quite pass due to unused `&self` in State::subscription) I am considering ignoring events when the file picker is opened, but that isn't a very nice solution (should just make it async atp) --- src/main.rs | 95 ++++++++++++++++++++++++++-------------------------- src/utils.rs | 8 ++--- 2 files changed, 51 insertions(+), 52 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5ad2bf3..c951b31 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,7 +29,7 @@ struct State { } impl State { fn new() -> (Self, Task) { - let mut state = State::default(); + let mut state = Self::default(); if let Some(path) = env::args().nth(1) { match state.load_image(path) { @@ -43,22 +43,25 @@ impl State { fn view(&self) -> Element<'_, Message, Theme, Renderer> { 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() - }); + main.push(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() + }, + )); if let Some(error) = self.error.as_ref() { main.push(widget::text(error).style(widget::text::danger).into()); @@ -84,9 +87,8 @@ 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") => match self.pick_and_load_image() { @@ -97,26 +99,22 @@ impl State { 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, @@ -160,8 +158,7 @@ impl State { "{} {}x{}", path.as_ref() .file_name() - .map(OsStr::to_string_lossy) - .unwrap_or(Cow::Borrowed("[no file]")), + .map_or(Cow::Borrowed("[no file]"), OsStr::to_string_lossy), image.width(), image.height(), )); @@ -210,13 +207,15 @@ impl State { event::listen().map(Message::Event) } fn title(&self) -> String { - match self.info.as_ref() { - Some(info) => format!( - "imagey {info} {}", - utils::string_from_filter_type(self.image_filter), - ), - None => "imagey".into(), - } + self.info.as_ref().map_or_else( + || "imagey".into(), + |info| { + format!( + "imagey {info} {}", + utils::string_from_filter_type(self.image_filter), + ) + }, + ) } } diff --git a/src/utils.rs b/src/utils.rs index dafd1f2..cc523c4 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -5,9 +5,9 @@ use image::{DynamicImage, ImageDecoder, ImageReader, ImageResult, RgbaImage}; use rfd::FileDialog; pub fn load_image(path: impl AsRef) -> Result { - _load_image(path).map_err(|e| e.to_string()) + load_image_impl(path).map_err(|e| e.to_string()) } -fn _load_image(path: impl AsRef) -> ImageResult { +fn load_image_impl(path: impl AsRef) -> ImageResult { let mut decoder = ImageReader::open(path)? .with_guessed_format()? .into_decoder()?; @@ -48,12 +48,12 @@ pub fn save_image(image: Option<&RgbaImage>) -> Result<(), String> { if let Err(e) = image.save(&path) { return Err(e.to_string()); - }; + } Ok(()) } -pub fn string_from_filter_type(f: widget::image::FilterMethod) -> &'static str { +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", From 18de039e77575f2b0233bb9957f940a9a8dbd5a5 Mon Sep 17 00:00:00 2001 From: electria Date: Fri, 31 Jul 2026 15:11:41 -0700 Subject: [PATCH 24/32] feat: make pickers async --- src/main.rs | 49 +++++++++++++++++++++++++++++++++---------------- src/utils.rs | 23 ++++++++--------------- 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/src/main.rs b/src/main.rs index c951b31..ba75666 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,9 @@ -use std::{borrow::Cow, env, ffi::OsStr, path::Path}; +use std::{ + borrow::Cow, + env, + ffi::OsStr, + path::{Path, PathBuf}, +}; use iced::{ Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, event, @@ -12,7 +17,9 @@ mod utils; #[derive(Clone, Debug)] enum Message { + ImagePicked(Result), ImageDisplayReady(Result), + SavePathPicked(Result), Event(iced::Event), } @@ -74,9 +81,26 @@ impl State { } fn update(&mut self, message: Message) -> Task { 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, @@ -91,13 +115,9 @@ impl State { return widget::operation::focus_next(); } - Key::Character("o") => match self.pick_and_load_image() { - Ok(task) => { - self.error = None; - return task; - } - Err(e) => self.error = Some(e), - }, + Key::Character("o") => { + return Task::perform(utils::pick_image(), Message::ImagePicked); + } Key::Character("r") => match self.image.as_ref() { None => self.error = Some("no image to rotate".into()), @@ -126,7 +146,11 @@ impl State { } Key::Character("s") => { - self.save_image(); + 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()); } Key::Character("q") => return window::latest().and_then(window::close), @@ -169,13 +193,6 @@ impl State { self.allocate_image() }) } - fn pick_and_load_image(&mut self) -> Result, 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 { let Some(image) = self.image.as_ref() else { diff --git a/src/utils.rs b/src/utils.rs index cc523c4..fc1841d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,7 +2,7 @@ use std::path::{Path, PathBuf}; use iced::widget; use image::{DynamicImage, ImageDecoder, ImageReader, ImageResult, RgbaImage}; -use rfd::FileDialog; +use rfd::AsyncFileDialog; pub fn load_image(path: impl AsRef) -> Result { load_image_impl(path).map_err(|e| e.to_string()) @@ -20,8 +20,8 @@ fn load_image_impl(path: impl AsRef) -> ImageResult { Ok(decoded_image.into_rgba8()) } -pub fn pick_image() -> Result { - let Some(path) = FileDialog::new() +pub async fn pick_image() -> Result { + let Some(filehandle) = AsyncFileDialog::new() .add_filter( "image", &[ @@ -30,27 +30,20 @@ pub fn pick_image() -> Result { ], ) .pick_file() + .await else { return Err("no path to open provided".into()); }; - Ok(path) + Ok(filehandle.path().to_owned()) } -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 { +pub async fn pick_save_path() -> Result { + let Some(filehandle) = AsyncFileDialog::new().save_file().await else { return Err("no path to save provided".into()); }; - if let Err(e) = image.save(&path) { - return Err(e.to_string()); - } - - Ok(()) + Ok(filehandle.path().to_owned()) } pub const fn string_from_filter_type(f: widget::image::FilterMethod) -> &'static str { From 6fa0e29eae2fe13e7c140447b1def1ee87828f8c Mon Sep 17 00:00:00 2001 From: electria Date: Fri, 31 Jul 2026 17:23:29 -0700 Subject: [PATCH 25/32] feat: display file size --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/main.rs | 9 ++++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 2664b09..4c05ae7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1905,6 +1905,7 @@ dependencies = [ "image", "jxl-oxide", "rfd", + "size", "zip", ] @@ -3787,6 +3788,12 @@ 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" diff --git a/Cargo.toml b/Cargo.toml index 9015f09..b830559 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ license = "AGPL-3.0-or-later" [dependencies] dirs = "6.0.0" rfd = "0.17.2" +size = "0.5.0" zip = "8.6.0" [dependencies.iced] diff --git a/src/main.rs b/src/main.rs index ba75666..2acbbfa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use std::{ borrow::Cow, env, ffi::OsStr, + os::unix::fs::MetadataExt, path::{Path, PathBuf}, }; @@ -11,6 +12,7 @@ use iced::{ theme, widget, window, }; use image::{EncodableLayout, RgbaImage, imageops}; +use size::Size; mod kra; mod utils; @@ -179,10 +181,15 @@ impl State { fn load_image(&mut self, path: impl AsRef) -> Result, 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(Cow::Borrowed("[no size]"), |m| Cow::Owned( + Size::from_bytes(m.size()).to_string() + )), image.width(), image.height(), )); From 808a8bf31d789f5af8ca505c717f5bc648bc466d Mon Sep 17 00:00:00 2001 From: electria Date: Fri, 31 Jul 2026 17:31:34 -0700 Subject: [PATCH 26/32] feat: put the bars together this made sense now that I made error messages shorter, and figured out how to properly align things in a row. looks good! might even consider keeping the infobar on by default --- src/main.rs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2acbbfa..b89efc0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,9 +50,7 @@ impl State { (state, Task::none()) } fn view(&self) -> Element<'_, Message, Theme, Renderer> { - let mut main = Vec::new(); - - main.push(self.image_display.as_ref().map_or_else( + let main = self.image_display.as_ref().map_or_else( || { widget::container(widget::text(include_str!("usage.txt"))) .height(Length::Fill) @@ -70,16 +68,27 @@ impl State { .height(Length::Fill) .into() }, - )); + ); + let mut bar = Vec::new(); if let Some(error) = self.error.as_ref() { - main.push(widget::text(error).style(widget::text::danger).into()); + 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() { - main.push(widget::text(self.title()).into()); + bar.push( + widget::container(widget::text(self.title())) + .align_x(Alignment::End) + .width(Length::Fill) + .into(), + ); } - widget::column(main).into() + widget::column([main, widget::row(bar).into()]).into() } fn update(&mut self, message: Message) -> Task { match message { From 1f909904c2479543dbb72ae70a714c2f36aac6c7 Mon Sep 17 00:00:00 2001 From: electria Date: Fri, 31 Jul 2026 17:47:12 -0700 Subject: [PATCH 27/32] fix: print error on failed to read metadata this should be very rare, so no need to display it or abort --- src/main.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index b89efc0..46cc96a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -194,11 +194,13 @@ impl State { path.as_ref() .file_name() .map_or(Cow::Borrowed("[no file]"), OsStr::to_string_lossy), - path.as_ref() - .metadata() - .map_or(Cow::Borrowed("[no size]"), |m| Cow::Owned( - Size::from_bytes(m.size()).to_string() - )), + 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(), )); From cb5e1d837a65684b437087a0319a41d67f7d024b Mon Sep 17 00:00:00 2001 From: electria Date: Fri, 31 Jul 2026 19:36:58 -0700 Subject: [PATCH 28/32] feat: image copy and paste --- Cargo.lock | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 4 +++ src/main.rs | 51 ++++++++++++++++++++++++++++++++- src/usage.txt | 3 ++ src/utils.rs | 21 +++++++++++++- 5 files changed, 155 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c05ae7..d8f99d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,6 +136,27 @@ 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" @@ -1226,6 +1247,12 @@ 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" @@ -1900,6 +1927,7 @@ dependencies = [ name = "imagey" version = "0.1.0" dependencies = [ + "arboard", "dirs", "iced", "image", @@ -3065,6 +3093,16 @@ 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.52.0", +] + [[package]] name = "owned_ttf_parser" version = "0.25.1" @@ -3131,6 +3169,17 @@ 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" @@ -4253,6 +4302,17 @@ 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" @@ -5116,6 +5176,24 @@ 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" diff --git a/Cargo.toml b/Cargo.toml index b830559..382a20f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,7 @@ 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" ] diff --git a/src/main.rs b/src/main.rs index 46cc96a..2c16d88 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use std::{ path::{Path, PathBuf}, }; +use arboard::Clipboard; use iced::{ Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, event, keyboard::{self, Key, key}, @@ -26,7 +27,7 @@ enum Message { Event(iced::Event), } -#[derive(Clone, Debug, Default)] +#[derive(Default)] struct State { image: Option, image_display: Option, @@ -35,6 +36,8 @@ struct State { error: Option, info: Option, infobar_shown: bool, + + clipboard: Option, } impl State { fn new() -> (Self, Task) { @@ -166,6 +169,52 @@ impl State { 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) => { + match clipboard.get_image().map(utils::rgbaimage_from_arboard) { + Ok(image) => { + self.error = None; + self.clipboard = Some(clipboard); + + 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()), + }; + self.clipboard = Some(clipboard); + } + Err(e) => self.error = Some(e.to_string()), + } + } + // ignore unused keys _ => {} }, diff --git a/src/usage.txt b/src/usage.txt index 1d4c6c1..9df1bc2 100644 --- a/src/usage.txt +++ b/src/usage.txt @@ -5,3 +5,6 @@ 'b' to toggle the bar 's' to save the image 'q' to quit + +'y' or 'c' to yank +'p' or 'v' to put diff --git a/src/utils.rs b/src/utils.rs index fc1841d..474c3b3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,7 @@ -use std::path::{Path, PathBuf}; +use std::{ + borrow::Cow, + path::{Path, PathBuf}, +}; use iced::widget; use image::{DynamicImage, ImageDecoder, ImageReader, ImageResult, RgbaImage}; @@ -52,3 +55,19 @@ pub const fn string_from_filter_type(f: widget::image::FilterMethod) -> &'static 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() +} From d3bc84769eca56e1caa4629ea7ae4582fcfb6d26 Mon Sep 17 00:00:00 2001 From: electria Date: Fri, 31 Jul 2026 20:17:25 -0700 Subject: [PATCH 29/32] refactor: improve clipboard put code style --- src/main.rs | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2c16d88..013c128 100644 --- a/src/main.rs +++ b/src/main.rs @@ -194,22 +194,19 @@ impl State { None => Clipboard::new(), } { Ok(mut clipboard) => { - match clipboard.get_image().map(utils::rgbaimage_from_arboard) { - Ok(image) => { - self.error = None; - self.clipboard = Some(clipboard); + let result = clipboard.get_image().map(utils::rgbaimage_from_arboard); - 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()), - }; + 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()), } From e57c1c9322bf0620514838e7203a87eef657a13b Mon Sep 17 00:00:00 2001 From: electria Date: Sat, 1 Aug 2026 10:22:41 -0700 Subject: [PATCH 30/32] fix: jxls being rotated twice --- src/utils.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 474c3b3..ae1dbb1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,6 @@ use std::{ borrow::Cow, + ffi::OsStr, path::{Path, PathBuf}, }; @@ -11,7 +12,7 @@ pub fn load_image(path: impl AsRef) -> Result { load_image_impl(path).map_err(|e| e.to_string()) } fn load_image_impl(path: impl AsRef) -> ImageResult { - let mut decoder = ImageReader::open(path)? + let mut decoder = ImageReader::open(&path)? .with_guessed_format()? .into_decoder()?; @@ -19,7 +20,17 @@ fn load_image_impl(path: impl AsRef) -> ImageResult { let mut decoded_image = DynamicImage::from_decoder(decoder)?; - decoded_image.apply_orientation(oreintation); + // 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); + } Ok(decoded_image.into_rgba8()) } From db457fc4e7af44b8333c75a1e25516fdcd06529e Mon Sep 17 00:00:00 2001 From: electria Date: Mon, 3 Aug 2026 11:00:15 -0700 Subject: [PATCH 31/32] docs: add readme --- README.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..545d7ef --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# 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) From 603678ba343278ee7ecf126bc28de7232b55911c Mon Sep 17 00:00:00 2001 From: electria Date: Mon, 3 Aug 2026 23:30:21 -0700 Subject: [PATCH 32/32] refactor: move kra to crate in the interest of reusing that code in a gallery app --- Cargo.lock | 13 ++++++++++++- Cargo.toml | 1 + src/kra.rs | 40 ---------------------------------------- src/main.rs | 3 +-- 4 files changed, 14 insertions(+), 43 deletions(-) delete mode 100644 src/kra.rs diff --git a/Cargo.lock b/Cargo.lock index d8f99d0..b164cb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1932,6 +1932,7 @@ dependencies = [ "iced", "image", "jxl-oxide", + "kra-image-integration", "rfd", "size", "zip", @@ -2267,6 +2268,16 @@ 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" @@ -3100,7 +3111,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 382a20f..96d9fb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +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" diff --git a/src/kra.rs b/src/kra.rs deleted file mode 100644 index 3642370..0000000 --- a/src/kra.rs +++ /dev/null @@ -1,40 +0,0 @@ -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> { - 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>, -) -> ImageError { - ImageError::Decoding(DecodingError::new( - image::error::ImageFormatHint::PathExtension("kra".into()), - e, - )) -} diff --git a/src/main.rs b/src/main.rs index 013c128..c676e44 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,6 @@ use iced::{ use image::{EncodableLayout, RgbaImage, imageops}; use size::Size; -mod kra; mod utils; #[derive(Clone, Debug)] @@ -302,7 +301,7 @@ impl State { fn main() -> Result<(), iced::Error> { jxl_oxide::integration::register_image_decoding_hook(); - kra::register(); + kra_image_integration::register(); iced::application(State::new, State::update, State::view) .subscription(State::subscription)