Compare commits

..
Author SHA1 Message Date
603678ba34
refactor: move kra to crate
in the interest of reusing that code in a gallery app
2026-08-03 23:30:21 -07:00
db457fc4e7
docs: add readme 2026-08-03 11:27:29 -07:00
e57c1c9322
fix: jxls being rotated twice 2026-08-01 10:22:41 -07:00
d3bc84769e
refactor: improve clipboard put code style 2026-07-31 20:17:25 -07:00
cb5e1d837a
feat: image copy and paste 2026-07-31 20:08:58 -07:00
1f909904c2
fix: print error on failed to read metadata
this should be very rare, so no need to display it or abort
2026-07-31 17:47:12 -07:00
808a8bf31d
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
2026-07-31 17:32:52 -07:00
6fa0e29eae
feat: display file size 2026-07-31 17:23:29 -07:00
18de039e77
feat: make pickers async 2026-07-31 15:53:11 -07:00
9d4e24e809
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)
2026-07-31 14:39:18 -07:00
94c905db38
build: seperate linux-specific deps 2026-07-31 14:19:53 -07:00
754dd6126d
fix: wrong dbus output (?) 2026-07-31 11:21:37 -07:00
26c17d5cf9
docs: add safety note 2026-07-31 11:14:05 -07:00
5a84b56025
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.
2026-07-31 09:55:08 -07:00
e98f268e59
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.
2026-07-31 09:51:57 -07:00
973703726f
feat: guess format from magic numbers as well
this allows opening files with extensions that don't match the filetype
2026-07-31 09:16:38 -07:00
fca4577c0f
refactor(kra): factor out error mapping 2026-07-31 00:24:39 -07:00
57ad7dfb1e
build: reduce indirection slightly 2026-07-31 00:05:48 -07:00
b8b4d8698c
build: remove no-op wrapper argument 2026-07-31 00:03:35 -07:00
13976b91ea
fix: add .kra (krita) support to mimeTypes 2026-07-30 23:54:37 -07:00
101adc7a89
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
2026-07-30 23:40:14 -07:00
c96a1cbdd1
feat: default to FilterMethod::Nearest on small image
helps make pixelart sharp without the need to change it yourself
2026-07-30 22:54:56 -07:00
fc92ff60e0
feat: kra (krita) support 2026-07-30 22:41:17 -07:00
2e8a6c8546
feat: use single quotes in usage to reduce noise 2026-07-30 21:43:06 -07:00
50b5ed58b0
feat: add to usage reference 2026-07-30 21:40:14 -07:00
b1157474c9
feat: simplify theme 2026-07-30 20:35:45 -07:00
9bdd556c9d
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
2026-07-30 20:27:34 -07:00
7ce87e2870
feat: optional infobar
literally the title; except on the bottom
2026-07-30 20:17:21 -07:00
97ce6754dd
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
2026-07-30 19:21:12 -07:00
f279c3aa15
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)
2026-07-30 19:19:23 -07:00
e7e1400c3a
style: avoid toml curly braces
else it causes messy diffs and is generally less writable and readable
2026-07-29 16:11:48 -07:00
5a83fe39e9
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)
2026-07-29 16:11:35 -07:00
7 changed files with 949 additions and 319 deletions

720
Cargo.lock generated
View file

@ -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"
@ -55,6 +66,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"
@ -110,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"
@ -413,6 +460,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"
@ -445,28 +502,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]]
@ -525,6 +567,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 +661,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 +710,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 +752,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 +858,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 +926,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,46 +944,21 @@ 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"
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"
@ -903,6 +981,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"
@ -1144,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"
@ -1152,6 +1261,7 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
"zlib-rs",
]
[[package]]
@ -1375,8 +1485,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]]
@ -1570,6 +1682,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"
@ -1757,12 +1887,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"
@ -1803,11 +1927,15 @@ dependencies = [
name = "imagey"
version = "0.1.0"
dependencies = [
"arboard",
"dirs",
"iced",
"image",
"jpegxl-rs",
"jxl-oxide",
"kra-image-integration",
"rfd",
"size",
"zip",
]
[[package]]
@ -1826,6 +1954,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"
@ -1914,29 +2051,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 +2062,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"
@ -1974,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"
@ -1990,6 +2294,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"
@ -2097,6 +2407,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"
@ -2316,6 +2635,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"
@ -2779,6 +3104,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.61.2",
]
[[package]]
name = "owned_ttf_parser"
version = "0.25.1"
@ -2829,12 +3164,33 @@ 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"
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"
@ -2938,6 +3294,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"
@ -2953,16 +3321,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"
@ -3421,6 +3779,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"
@ -3468,6 +3848,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"
@ -3639,12 +4025,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"
@ -3777,6 +4157,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"
@ -3913,6 +4313,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"
@ -3922,6 +4333,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"
@ -4764,6 +5187,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"
@ -4926,6 +5367,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"

View file

@ -6,8 +6,10 @@ license = "AGPL-3.0-or-later"
[dependencies]
dirs = "6.0.0"
kra-image-integration = "0.1.0"
rfd = "0.17.2"
jpegxl-rs = "0.14.0"
size = "0.5.0"
zip = "8.6.0"
[dependencies.iced]
version = "0.14.0"
@ -16,3 +18,12 @@ features = [ "image" ]
[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" ]
[dependencies.arboard]
version = "3.6.1"
features = [ "wayland-data-control" ]

41
README.md Normal file
View file

@ -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)

View file

@ -19,18 +19,24 @@
cargoToml = fromTOML (builtins.readFile ./Cargo.toml);
name = cargoToml.package.name;
dlDeps = with pkgs; [
dlDeps =
with pkgs;
[
# needed for both x11 and wayland
libxkbcommon
libGL
vulkan-loader
wayland
libx11
libxcursor
libxi
libxcb
]
++ lib.optionals stdenv.hostPlatform.isLinux [
# libdbus, for rfd
dbus.lib
vulkan-loader
wayland
];
commonArgs = {
@ -39,15 +45,12 @@
pkg-config
];
buildInputs = with pkgs; [
libjxl
dav1d
];
src = ./.;
};
LD_LIBRARY_PATH = lib.makeLibraryPath dlDeps;
# Build *just* the cargo dependencies,
# to reuse them for build and test derivations.
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
@ -71,6 +74,7 @@
"image/tiff"
"image/bmp"
"image/vnd.microsoft.icon" # .ico
"application/x-krita"
];
};
in
@ -79,19 +83,13 @@
// {
inherit cargoArtifacts;
nativeBuildInputs =
commonArgs.nativeBuildInputs
++ [
nativeBuildInputs = commonArgs.nativeBuildInputs ++ [
pkgs.autoPatchelfHook
]
++ lib.optional pkgs.stdenv.hostPlatform.isLinux pkgs.makeBinaryWrapper;
];
buildInputs =
commonArgs.buildInputs
++ [
buildInputs = commonArgs.buildInputs ++ [
pkgs.libgcc
]
++ lib.optional pkgs.stdenv.hostPlatform.isLinux pkgs.zenity;
];
runtimeDependencies = dlDeps;
@ -100,10 +98,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 ]}
'';
}
);
@ -123,7 +117,7 @@
};
devShells.default = craneLib.devShell {
inherit LD_LIBRARY_PATH;
LD_LIBRARY_PATH = lib.makeLibraryPath dlDeps;
inputsFrom = [ crate ];
packages = [ pkgs.rust-analyzer ];

View file

@ -1,49 +1,59 @@
use std::{env, path::Path};
use std::{
borrow::Cow,
env,
ffi::OsStr,
os::unix::fs::MetadataExt,
path::{Path, PathBuf},
};
use arboard::Clipboard;
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,
};
use image::{EncodableLayout, RgbaImage, imageops};
use size::Size;
mod utils;
#[derive(Clone, Debug)]
enum Message {
ImagePicked(Result<PathBuf, String>),
ImageDisplayReady(Result<widget::image::Allocation, widget::image::Error>),
SavePathPicked(Result<PathBuf, String>),
Event(iced::Event),
}
#[derive(Clone, Debug, Default)]
#[derive(Default)]
struct State {
image: Option<RgbaImage>,
image_display: Option<widget::image::Allocation>,
image_filter: widget::image::FilterMethod,
error: Option<String>,
info: Option<String>,
infobar_shown: bool,
clipboard: Option<Clipboard>,
}
impl State {
fn new() -> (Self, Task<Message>) {
let mut state = State::default();
let mut state = Self::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> {
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 {
let main = self.image_display.as_ref().map_or_else(
|| {
widget::container(widget::text(include_str!("usage.txt")))
.height(Length::Fill)
.width(Length::Fill)
@ -51,19 +61,59 @@ impl State {
.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()
},
])
|allocation| {
widget::image::viewer(allocation.handle().clone())
.filter_method(self.image_filter)
.max_scale(50.)
.min_scale(1.)
.width(Length::Fill)
.height(Length::Fill)
.into()
},
);
let mut bar = Vec::new();
if let Some(error) = self.error.as_ref() {
bar.push(
widget::container(widget::text(error).style(widget::text::danger))
.align_x(Alignment::Start)
.width(Length::Fill)
.into(),
);
}
if self.infobar_shown && self.info.is_some() {
bar.push(
widget::container(widget::text(self.title()))
.align_x(Alignment::End)
.width(Length::Fill)
.into(),
);
}
widget::column([main, widget::row(bar).into()]).into()
}
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::ImagePicked(result) => match result.and_then(|path| self.load_image(path)) {
Err(e) => self.error = Some(e),
Ok(task) => {
self.error = None;
return task;
}
},
Message::ImageDisplayReady(result) => {
self.image_display = Some(result.unwrap());
}
Message::SavePathPicked(result) => {
self.error = result
.and_then(|path| {
self.image.as_ref().map_or_else(
|| Err("no image to save".into()),
|image| image.save(path).map_err(|e| e.to_string()),
)
})
.err();
}
Message::Event(iced::Event::Keyboard(keyboard::Event::KeyPressed {
key,
@ -74,46 +124,93 @@ impl State {
Key::Named(key::Named::Tab) => {
if modifiers.shift() {
return widget::operation::focus_previous();
} else {
}
return widget::operation::focus_next();
}
Key::Character("o") => {
return Task::perform(utils::pick_image(), Message::ImagePicked);
}
Key::Character("o") => match self.pick_and_load_image() {
Ok(task) => {
self.error = None;
return task;
}
Err(e) => self.error = Some(e),
},
Key::Character("r") => {
match self.image.as_ref() {
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() {
},
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,
widget::image::FilterMethod::Nearest => widget::image::FilterMethod::Linear,
}
}
Key::Character("b") => {
self.infobar_shown = !self.infobar_shown;
}
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),
Key::Character("y") | Key::Character("c") => {
if let Some(image) = self.image.as_ref() {
match match self.clipboard.take() {
Some(c) => Ok(c),
None => Clipboard::new(),
} {
Ok(mut clipboard) => {
self.error = clipboard
.set_image(utils::arboard_from_rgbaimage(image))
.map_err(|e| e.to_string())
.err();
self.clipboard = Some(clipboard);
}
Err(e) => self.error = Some(e.to_string()),
}
} else {
self.error = Some("no image to yank".into());
}
}
Key::Character("p") | Key::Character("v") => {
match match self.clipboard.take() {
Some(c) => Ok(c),
None => Clipboard::new(),
} {
Ok(mut clipboard) => {
let result = clipboard.get_image().map(utils::rgbaimage_from_arboard);
self.error = result.as_ref().map_err(|e| e.to_string()).err();
self.clipboard = Some(clipboard);
if let Ok(image) = result {
self.info = Some(format!("{}x{}", image.width(), image.height(),));
if image.dimensions() < (100, 100) {
self.image_filter = widget::image::FilterMethod::Nearest;
}
self.image = Some(image);
return self.allocate_image();
}
}
Err(e) => self.error = Some(e.to_string()),
}
}
// ignore unused keys
_ => {}
},
@ -136,18 +233,29 @@ impl State {
}
fn load_image(&mut self, path: impl AsRef<Path>) -> Result<Task<Message>, String> {
utils::load_image(path).map(|image| {
utils::load_image(&path).map(|image| {
self.info = Some(format!(
"{} {} {}x{}",
path.as_ref()
.file_name()
.map_or(Cow::Borrowed("[no file]"), OsStr::to_string_lossy),
path.as_ref().metadata().map_or_else(
|e| {
eprintln!("failed to read metadata: {e}");
Cow::Borrowed("[no size]")
},
|m| Cow::Owned(Size::from_bytes(m.size()).to_string())
),
image.width(),
image.height(),
));
if image.dimensions() < (100, 100) {
self.image_filter = widget::image::FilterMethod::Nearest;
}
self.image = Some(image);
self.allocate_image()
})
}
fn pick_and_load_image(&mut self) -> Result<Task<Message>, 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<Message> {
let Some(image) = self.image.as_ref() else {
@ -158,6 +266,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)
@ -167,26 +287,31 @@ 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()),
None => "imagey".into(),
}
self.info.as_ref().map_or_else(
|| "imagey".into(),
|info| {
format!(
"imagey {info} {}",
utils::string_from_filter_type(self.image_filter),
)
},
)
}
}
fn main() -> Result<(), iced::Error> {
jxl_oxide::integration::register_image_decoding_hook();
kra_image_integration::register();
iced::application(State::new, State::update, State::view)
.subscription(State::subscription)
.title(State::title)
.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()

View file

@ -1,5 +1,10 @@
"o" to open a new image
"r" to rotate
"i" to invert colors
"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
'y' or 'c' to yank
'p' or 'v' to put

View file

@ -1,150 +1,84 @@
use std::{
borrow::Cow,
ffi::OsStr,
fs,
io::Write,
path::{Path, PathBuf},
};
use image::{DynamicImage, ImageDecoder, ImageReader, RgbaImage};
use jpegxl_rs::image::ToDynamic;
use rfd::FileDialog;
use iced::widget;
use image::{DynamicImage, ImageDecoder, ImageReader, ImageResult, RgbaImage};
use rfd::AsyncFileDialog;
pub fn load_image(path: impl AsRef<Path>) -> Result<RgbaImage, String> {
let path = path.as_ref();
load_image_impl(path).map_err(|e| e.to_string())
}
fn load_image_impl(path: impl AsRef<Path>) -> ImageResult<RgbaImage> {
let mut decoder = ImageReader::open(&path)?
.with_guessed_format()?
.into_decoder()?;
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()
));
}
};
let oreintation = decoder.orientation()?;
match jpegxl_rs::decoder_builder()
.build()
.unwrap()
.decode_to_image(&data)
let mut decoded_image = DynamicImage::from_decoder(decoder)?;
// the condition is a workaround to not rotate JXL images twice;
// since they are already rotated by the decoder
// (while jpegs for instance aren't)
if !path
.as_ref()
.extension()
.map(OsStr::to_string_lossy)
.is_some_and(|s| s == "jxl")
{
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
}
};
Ok(decoded_image.into_rgba8())
}
pub fn pick_image() -> Result<PathBuf, String> {
let Some(path) = FileDialog::new()
pub async fn pick_image() -> Result<PathBuf, String> {
let Some(filehandle) = AsyncFileDialog::new()
.add_filter(
"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()
.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<PathBuf, String> {
let Some(filehandle) = AsyncFileDialog::new().save_file().await else {
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())),
};
Ok(filehandle.path().to_owned())
}
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::<u8, u8>(&rgb_image, rgb_image.width(), rgb_image.height()) {
Ok(j) => j,
Err(e) => {
return Err(format!("failed to encode jxl '{}': {e}", path.display()));
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",
}
};
}
if let Err(e) = file.write(&jxl.data) {
return Err(format!("failed to write jxl to '{}': {e}", path.display()));
};
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()),
}
_ => {
if let Err(e) = image.save(&path) {
return Err(format!("failed to save '{}': {e}", path.display()));
};
}
};
Ok(())
}
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()
}