Compare commits

..
Author SHA1 Message Date
59ef65de70
refactor: try to integrate jpegxl_rs with hooks
The problem is, libjxl_rs does not offer a way to get metadata
before decoding the entire image.

As such, there is no reasonable way to implement ImageDecoder here;
I think it's possible to make it decode the image on init,
but that would lead to unexpected behavior performance-wise.
2026-07-29 15:37:51 -07:00
8 changed files with 343 additions and 953 deletions

720
Cargo.lock generated
View file

@ -24,17 +24,6 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 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]] [[package]]
name = "ahash" name = "ahash"
version = "0.8.12" version = "0.8.12"
@ -66,21 +55,6 @@ dependencies = [
"equator", "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]] [[package]]
name = "android-activity" name = "android-activity"
version = "0.6.1" version = "0.6.1"
@ -136,27 +110,6 @@ version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" 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]] [[package]]
name = "arg_enum_proc_macro" name = "arg_enum_proc_macro"
version = "0.3.4" version = "0.3.4"
@ -460,16 +413,6 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" 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]] [[package]]
name = "block2" name = "block2"
version = "0.5.1" version = "0.5.1"
@ -502,13 +445,28 @@ dependencies = [
] ]
[[package]] [[package]]
name = "brotli-decompressor" name = "bon"
version = "5.0.3" version = "3.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" checksum = "a602c73c7b0148ec6d12af6fd5cc7a46e2eacc8878271a999abac56eed12f561"
dependencies = [ dependencies = [
"alloc-no-stdlib", "bon-macros",
"alloc-stdlib", "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",
] ]
[[package]] [[package]]
@ -567,15 +525,6 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" 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]] [[package]]
name = "calloop" name = "calloop"
version = "0.13.0" version = "0.13.0"
@ -661,16 +610,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 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]] [[package]]
name = "clipboard-win" name = "clipboard-win"
version = "5.4.1" version = "5.4.1"
@ -710,12 +649,6 @@ dependencies = [
"x11rb", "x11rb",
] ]
[[package]]
name = "cmov"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
[[package]] [[package]]
name = "codespan-reporting" name = "codespan-reporting"
version = "0.12.0" version = "0.12.0"
@ -752,18 +685,6 @@ dependencies = [
"crossbeam-utils", "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]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@ -858,21 +779,6 @@ dependencies = [
"unicode-segmentation", "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]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.5.0" version = "1.5.0"
@ -926,15 +832,6 @@ dependencies = [
"wgpu", "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]] [[package]]
name = "ctor" name = "ctor"
version = "0.10.1" version = "0.10.1"
@ -944,21 +841,46 @@ dependencies = [
"dtor", "dtor",
] ]
[[package]]
name = "ctutils"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
dependencies = [
"cmov",
]
[[package]] [[package]]
name = "cursor-icon" name = "cursor-icon"
version = "1.2.0" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" 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]] [[package]]
name = "dav1d" name = "dav1d"
version = "0.11.1" version = "0.11.1"
@ -981,31 +903,6 @@ dependencies = [
"system-deps", "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]] [[package]]
name = "dirs" name = "dirs"
version = "6.0.0" version = "6.0.0"
@ -1247,12 +1144,6 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fixedbitset"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.9" version = "1.1.9"
@ -1261,7 +1152,6 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"miniz_oxide", "miniz_oxide",
"zlib-rs",
] ]
[[package]] [[package]]
@ -1485,10 +1375,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"r-efi 6.0.0", "r-efi 6.0.0",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -1682,24 +1570,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" 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]] [[package]]
name = "iced" name = "iced"
version = "0.14.0" version = "0.14.0"
@ -1887,6 +1757,12 @@ dependencies = [
"winit", "winit",
] ]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "image" name = "image"
version = "0.25.10" version = "0.25.10"
@ -1927,15 +1803,11 @@ dependencies = [
name = "imagey" name = "imagey"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"arboard",
"dirs", "dirs",
"iced", "iced",
"image", "image",
"jxl-oxide", "jpegxl-rs",
"kra-image-integration",
"rfd", "rfd",
"size",
"zip",
] ]
[[package]] [[package]]
@ -1954,15 +1826,6 @@ dependencies = [
"hashbrown 0.17.1", "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]] [[package]]
name = "interpolate_name" name = "interpolate_name"
version = "0.2.4" version = "0.2.4"
@ -2051,6 +1914,29 @@ dependencies = [
"libc", "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]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.103" version = "0.3.103"
@ -2062,186 +1948,6 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "kamadak-exif" name = "kamadak-exif"
version = "0.6.1" version = "0.6.1"
@ -2268,16 +1974,6 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" 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]] [[package]]
name = "kurbo" name = "kurbo"
version = "0.10.4" version = "0.10.4"
@ -2294,12 +1990,6 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
[[package]]
name = "libbz2-rs-sys"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.186" version = "0.2.186"
@ -2407,15 +2097,6 @@ version = "0.16.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" 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]] [[package]]
name = "malloc_buf" name = "malloc_buf"
version = "0.0.6" version = "0.0.6"
@ -2635,12 +2316,6 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "num-conv"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
[[package]] [[package]]
name = "num-derive" name = "num-derive"
version = "0.4.2" version = "0.4.2"
@ -3104,16 +2779,6 @@ dependencies = [
"pin-project-lite", "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]] [[package]]
name = "owned_ttf_parser" name = "owned_ttf_parser"
version = "0.25.1" version = "0.25.1"
@ -3164,33 +2829,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" 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]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 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]] [[package]]
name = "pin-project" name = "pin-project"
version = "1.1.13" version = "1.1.13"
@ -3294,18 +2938,6 @@ dependencies = [
"portable-atomic", "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]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.21" version = "0.2.21"
@ -3321,6 +2953,16 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" 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]] [[package]]
name = "proc-macro-crate" name = "proc-macro-crate"
version = "3.5.0" version = "3.5.0"
@ -3779,28 +3421,6 @@ dependencies = [
"serde_core", "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]] [[package]]
name = "shlex" name = "shlex"
version = "2.0.1" version = "2.0.1"
@ -3848,12 +3468,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "size"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b6709c7b6754dca1311b3c73e79fcce40dd414c782c66d88e8823030093b02b"
[[package]] [[package]]
name = "skrifa" name = "skrifa"
version = "0.37.0" version = "0.37.0"
@ -4025,6 +3639,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "svg_fmt" name = "svg_fmt"
version = "0.4.5" version = "0.4.5"
@ -4157,26 +3777,6 @@ dependencies = [
"zune-jpeg", "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]] [[package]]
name = "tiny-skia" name = "tiny-skia"
version = "0.11.4" version = "0.11.4"
@ -4313,17 +3913,6 @@ dependencies = [
"once_cell", "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]] [[package]]
name = "ttf-parser" name = "ttf-parser"
version = "0.25.1" version = "0.25.1"
@ -4333,18 +3922,6 @@ dependencies = [
"core_maths", "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]] [[package]]
name = "uds_windows" name = "uds_windows"
version = "1.2.1" version = "1.2.1"
@ -5187,24 +4764,6 @@ version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" 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]] [[package]]
name = "x11-dl" name = "x11-dl"
version = "2.21.0" version = "2.21.0"
@ -5367,85 +4926,6 @@ dependencies = [
"syn", "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]] [[package]]
name = "zune-core" name = "zune-core"
version = "0.5.1" version = "0.5.1"

View file

@ -5,25 +5,11 @@ edition = "2024"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
[dependencies] [dependencies]
iced = { version = "0.14.0", features = [ "image" ] }
dirs = "6.0.0" dirs = "6.0.0"
kra-image-integration = "0.1.0"
rfd = "0.17.2" rfd = "0.17.2"
size = "0.5.0" jpegxl-rs = "0.14.0"
zip = "8.6.0"
[dependencies.iced]
version = "0.14.0"
features = [ "image" ]
[dependencies.image] [dependencies.image]
version = "0.25.10" version = "0.25.10"
features = [ "avif-native" ] 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" ]

View file

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

22
src/jxl.rs Normal file
View file

@ -0,0 +1,22 @@
pub struct JxlDecoder<'a> {
metadata: jpegxl_rs::decode::Metadata,
inner: jpegxl_rs::decode::JxlDecoder<'a, 'a>,
}
impl<'a> image::ImageDecoder for JxlDecoder<'a> {
fn color_type(&self) -> image::ColorType {
image::ColorType::Rgba8
}
fn icc_profile(&mut self) -> image::ImageResult<Option<Vec<u8>>> {
Ok(self.metadata.icc_profile.clone())
}
fn dimensions(&self) -> (u32, u32) {}
fn read_image(self, buf: &mut [u8]) -> image::ImageResult<()>
where
Self: Sized,
{
}
fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> image::ImageResult<()> {
self.read_image(buf)
}
}

View file

@ -1,59 +1,50 @@
use std::{ use std::{env, path::Path};
borrow::Cow,
env,
ffi::OsStr,
os::unix::fs::MetadataExt,
path::{Path, PathBuf},
};
use arboard::Clipboard;
use iced::{ use iced::{
Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, event, Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, color, event,
keyboard::{self, Key, key}, keyboard::{self, Key, key},
theme, widget, window, theme, widget, window,
}; };
use image::{EncodableLayout, RgbaImage, imageops}; use image::{EncodableLayout, RgbaImage, imageops};
use size::Size;
mod jxl;
mod utils; mod utils;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
enum Message { enum Message {
ImagePicked(Result<PathBuf, String>),
ImageDisplayReady(Result<widget::image::Allocation, widget::image::Error>), ImageDisplayReady(Result<widget::image::Allocation, widget::image::Error>),
SavePathPicked(Result<PathBuf, String>),
Event(iced::Event), Event(iced::Event),
} }
#[derive(Default)] #[derive(Clone, Debug, Default)]
struct State { struct State {
image: Option<RgbaImage>, image: Option<RgbaImage>,
image_display: Option<widget::image::Allocation>, image_display: Option<widget::image::Allocation>,
image_filter: widget::image::FilterMethod,
error: Option<String>, error: Option<String>,
info: Option<String>,
infobar_shown: bool,
clipboard: Option<Clipboard>,
} }
impl State { impl State {
fn new() -> (Self, Task<Message>) { fn new() -> (Self, Task<Message>) {
let mut state = Self::default(); let mut state = State::default();
if let Some(path) = env::args().nth(1) { state.error = env::args().nth(1).and_then(|path| {
match state.load_image(path) { utils::load_image(path)
Err(e) => state.error = Some(e), .map(|image| state.image = Some(image))
Ok(task) => return (state, task), .err()
} });
} let allocate_image = state.allocate_image();
(state, Task::none()) (state, allocate_image)
} }
fn view(&self) -> Element<'_, Message, Theme, Renderer> { fn view(&self) -> Element<'_, Message, Theme, Renderer> {
let main = self.image_display.as_ref().map_or_else( widget::column([
|| { if let Some(allocation) = self.image_display.as_ref() {
widget::image::viewer(allocation.handle().clone())
.width(Length::Fill)
.height(Length::Fill)
.into()
} else {
widget::container(widget::text(include_str!("usage.txt"))) widget::container(widget::text(include_str!("usage.txt")))
.height(Length::Fill) .height(Length::Fill)
.width(Length::Fill) .width(Length::Fill)
@ -61,59 +52,19 @@ impl State {
.align_y(Alignment::Center) .align_y(Alignment::Center)
.into() .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() { if let Some(error) = self.error.as_ref() {
bar.push( widget::text(error).style(widget::text::danger).into()
widget::container(widget::text(error).style(widget::text::danger)) } else {
.align_x(Alignment::Start) widget::space().into()
.width(Length::Fill) },
.into(), ])
); .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> { fn update(&mut self, message: Message) -> Task<Message> {
match 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) => { Message::ImageDisplayReady(result) => {
self.image_display = Some(result.unwrap()); 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 { Message::Event(iced::Event::Keyboard(keyboard::Event::KeyPressed {
key, key,
@ -124,93 +75,46 @@ impl State {
Key::Named(key::Named::Tab) => { Key::Named(key::Named::Tab) => {
if modifiers.shift() { if modifiers.shift() {
return widget::operation::focus_previous(); return widget::operation::focus_previous();
} } else {
return widget::operation::focus_next(); return widget::operation::focus_next();
} }
Key::Character("o") => {
return Task::perform(utils::pick_image(), Message::ImagePicked);
} }
Key::Character("r") => match self.image.as_ref() { 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() {
None => self.error = Some("no image to rotate".into()), None => self.error = Some("no image to rotate".into()),
Some(image) => { Some(image) => {
self.error = None; self.error = None;
self.image = Some(imageops::rotate90(image)); self.image = Some(imageops::rotate90(image));
return self.allocate_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()), None => self.error = Some("no image to invert".into()),
Some(image) => { Some(image) => {
self.error = None; self.error = None;
imageops::invert(image); imageops::invert(image);
return self.allocate_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") => { Key::Character("s") => {
if self.image.is_some() { self.save_image();
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("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 // ignore unused keys
_ => {} _ => {}
}, },
@ -233,29 +137,18 @@ impl State {
} }
fn load_image(&mut self, path: impl AsRef<Path>) -> Result<Task<Message>, String> { 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.image = Some(image);
self.allocate_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> { fn allocate_image(&self) -> Task<Message> {
let Some(image) = self.image.as_ref() else { let Some(image) = self.image.as_ref() else {
@ -266,18 +159,6 @@ impl State {
widget::image::allocate(widget::image::Handle::from_rgba( widget::image::allocate(widget::image::Handle::from_rgba(
image.width(), image.width(),
image.height(), 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()) }, unsafe { std::mem::transmute::<&[u8], &'static [u8]>(image.as_bytes()) },
)) ))
.map(Message::ImageDisplayReady) .map(Message::ImageDisplayReady)
@ -287,31 +168,26 @@ impl State {
event::listen().map(Message::Event) event::listen().map(Message::Event)
} }
fn title(&self) -> String { fn title(&self) -> String {
self.info.as_ref().map_or_else( match self.image.as_ref() {
|| "imagey".into(), Some(image) => format!("imagey {}x{}", image.width(), image.height()),
|info| { None => "imagey".into(),
format!( }
"imagey {info} {}",
utils::string_from_filter_type(self.image_filter),
)
},
)
} }
} }
fn main() -> Result<(), iced::Error> { fn main() -> Result<(), iced::Error> {
jxl_oxide::integration::register_image_decoding_hook();
kra_image_integration::register();
iced::application(State::new, State::update, State::view) iced::application(State::new, State::update, State::view)
.subscription(State::subscription) .subscription(State::subscription)
.title(State::title) .title(State::title)
.theme(Theme::custom( .theme(Theme::custom(
"custom", "custom",
theme::Palette { theme::Palette {
background: Color::BLACK, background: color!(0x080808),
text: Color::WHITE, text: Color::WHITE,
..theme::Palette::DARK primary: color!(0xff00ff),
success: color!(0x00ff00),
warning: color!(0x880000),
danger: color!(0xff0000),
}, },
)) ))
.run() .run()

View file

@ -1,10 +1,5 @@
'o' to open a new image "o" to open a new image
'r' to rotate "r" to rotate
'i' to invert colors "i" to invert colors
'f' to switch filter "s" to save the image
'b' to toggle the bar "q" to quit
's' to save the image
'q' to quit
'y' or 'c' to yank
'p' or 'v' to put

View file

@ -1,84 +1,150 @@
use std::{ use std::{
borrow::Cow,
ffi::OsStr, ffi::OsStr,
fs,
io::Write,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use iced::widget; use image::{DynamicImage, ImageDecoder, ImageReader, RgbaImage};
use image::{DynamicImage, ImageDecoder, ImageReader, ImageResult, RgbaImage}; use jpegxl_rs::image::ToDynamic;
use rfd::AsyncFileDialog; use rfd::FileDialog;
pub fn load_image(path: impl AsRef<Path>) -> Result<RgbaImage, String> { pub fn load_image(path: impl AsRef<Path>) -> Result<RgbaImage, String> {
load_image_impl(path).map_err(|e| e.to_string()) 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()
));
} }
fn load_image_impl(path: impl AsRef<Path>) -> ImageResult<RgbaImage> { };
let mut decoder = ImageReader::open(&path)?
.with_guessed_format()?
.into_decoder()?;
let oreintation = decoder.orientation()?; match jpegxl_rs::decoder_builder()
.build()
let mut decoded_image = DynamicImage::from_decoder(decoder)?; .unwrap()
.decode_to_image(&data)
// 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(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()) Ok(decoded_image.into_rgba8())
} }
pub async fn pick_image() -> Result<PathBuf, String> { pub fn pick_image() -> Result<PathBuf, String> {
let Some(filehandle) = AsyncFileDialog::new() let Some(path) = FileDialog::new()
.add_filter( .add_filter(
"image", "image",
&[ &[
"png", "PNG", "jpg", "JPG", "jpeg", "JPEG", "avif", "jxl", "bmp", "exr", "ff", "png", "PNG", "jpg", "JPG", "jpeg", "JPEG", "avif", "jxl", "bmp", "exr", "ff",
"gif", "hdr", "ico", "pnm", "qoi", "tga", "tiff", "webp", "kra", "gif", "hdr", "ico", "pnm", "qoi", "tga", "tiff", "webp",
], ],
) )
.pick_file() .pick_file()
.await
else { else {
return Err("no path to open provided".into()); return Err("no path to open provided".into());
}; };
Ok(filehandle.path().to_owned()) Ok(path)
} }
pub async fn pick_save_path() -> Result<PathBuf, String> { pub fn save_image(image: Option<&RgbaImage>) -> Result<(), String> {
let Some(filehandle) = AsyncFileDialog::new().save_file().await else { let Some(image) = image else {
return Err("no image to save".into());
};
let Some(path) = FileDialog::new().save_file() else {
return Err("no path to save provided".into()); return Err("no path to save provided".into());
}; };
Ok(filehandle.path().to_owned()) let mut file = match fs::File::create(&path) {
} Ok(f) => f,
Err(e) => return Err(format!("failed to create file '{}': {e}", path.display())),
};
pub const fn string_from_filter_type(f: widget::image::FilterMethod) -> &'static str { match path.extension().map(OsStr::to_string_lossy).as_deref() {
match f { Some("jxl") => {
widget::image::FilterMethod::Linear => "bilinear", let mut encoder = jpegxl_rs::encoder_builder()
widget::image::FilterMethod::Nearest => "nearest neighbor", .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 fn arboard_from_rgbaimage<'a>(image: &'a RgbaImage) -> arboard::ImageData<'a> { if let Err(e) = file.write(&jxl.data) {
arboard::ImageData { return Err(format!("failed to write jxl to '{}': {e}", path.display()));
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()));
};
} }
pub fn rgbaimage_from_arboard(image: arboard::ImageData) -> RgbaImage { };
RgbaImage::from_raw(
image.width as u32, Ok(())
image.height as u32,
image.bytes.into_owned(),
)
.unwrap()
} }