diff --git a/Cargo.lock b/Cargo.lock index 8cf4097..b164cb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aes" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8eb277bec05f56a0e0591f155a484cbd0f4f07ff2905051a48c72f004f7ed58" +dependencies = [ + "cipher", + "cpubits", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.12" @@ -125,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" @@ -428,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" @@ -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,6 +944,15 @@ dependencies = [ "dtor", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "cursor-icon" version = "1.2.0" @@ -869,6 +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" @@ -1110,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" @@ -1118,6 +1261,7 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -1341,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]] @@ -1536,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" @@ -1763,11 +1927,15 @@ dependencies = [ name = "imagey" version = "0.1.0" dependencies = [ + "arboard", "dirs", "iced", "image", "jxl-oxide", + "kra-image-integration", "rfd", + "size", + "zip", ] [[package]] @@ -1786,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" @@ -2091,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" @@ -2107,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" @@ -2214,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" @@ -2433,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" @@ -2896,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" @@ -2946,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" @@ -3055,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" @@ -3528,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" @@ -3575,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" @@ -3878,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" @@ -4014,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" @@ -4023,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" @@ -4865,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" @@ -5027,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" diff --git a/Cargo.toml b/Cargo.toml index 487f499..96d9fb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,10 @@ license = "AGPL-3.0-or-later" [dependencies] dirs = "6.0.0" +kra-image-integration = "0.1.0" rfd = "0.17.2" +size = "0.5.0" +zip = "8.6.0" [dependencies.iced] version = "0.14.0" @@ -20,3 +23,7 @@ features = [ "avif-native" ] version = "0.12.6" # I really wish the image crate was named better features = [ "image" ] + +[dependencies.arboard] +version = "3.6.1" +features = [ "wayland-data-control" ] diff --git a/README.md b/README.md new file mode 100644 index 0000000..545d7ef --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# imagey + +image viewer and maybe editor; inspired by mpv's simplicity + +name is subject to change, suggestions welcome :) + +## usage + +```sh +# try it out! +nix run git+https://git.federated.nexus/electria/imagey + +# install it imperatively +nix profile install git+https://git.federated.nexus/electria/imagey +``` + +keybinds are shown on startup if you didn't start it with an image + +## known issues + +### JXL + +1. encoding is not implemented (jxl-oxide is decoding-only) +2. oreintation is incorrect in some cases + + if the image has metadata oreintation AND + + it's a jxl without the right extension OR + it's a jpeg with the jxl extension + + this is due to the JXL decoder automatically rotating the image, + while the JPEG decoder (for instance) requires the extra step. + + my workaround is to check the path of the input file, + not changing the oreintation if it has the jxl extension; + causing these caveats for cases where the extension is wrong. + +### clipboard + +1. large images (eg photos) don't seem to copy on linux/wayland, + (despite set_image not returning any error) diff --git a/flake.nix b/flake.nix index 72d435c..5a57299 100644 --- a/flake.nix +++ b/flake.nix @@ -19,19 +19,25 @@ cargoToml = fromTOML (builtins.readFile ./Cargo.toml); name = cargoToml.package.name; - dlDeps = with pkgs; [ - # needed for both x11 and wayland - libxkbcommon - libGL - vulkan-loader + dlDeps = + with pkgs; + [ + # needed for both x11 and wayland + libxkbcommon + libGL - wayland + libx11 + libxcursor + libxi + libxcb + ] + ++ lib.optionals stdenv.hostPlatform.isLinux [ + # libdbus, for rfd + dbus.lib - libx11 - libxcursor - libxi - libxcb - ]; + vulkan-loader + wayland + ]; commonArgs = { # all that's needed for artifacts and checks @@ -45,8 +51,6 @@ src = ./.; }; - LD_LIBRARY_PATH = lib.makeLibraryPath dlDeps; - # Build *just* the cargo dependencies, # to reuse them for build and test derivations. cargoArtifacts = craneLib.buildDepsOnly commonArgs; @@ -70,6 +74,7 @@ "image/tiff" "image/bmp" "image/vnd.microsoft.icon" # .ico + "application/x-krita" ]; }; in @@ -78,19 +83,13 @@ // { inherit cargoArtifacts; - nativeBuildInputs = - commonArgs.nativeBuildInputs - ++ [ - pkgs.autoPatchelfHook - ] - ++ lib.optional pkgs.stdenv.hostPlatform.isLinux pkgs.makeBinaryWrapper; + nativeBuildInputs = commonArgs.nativeBuildInputs ++ [ + pkgs.autoPatchelfHook + ]; - buildInputs = - commonArgs.buildInputs - ++ [ - pkgs.libgcc - ] - ++ lib.optional pkgs.stdenv.hostPlatform.isLinux pkgs.zenity; + buildInputs = commonArgs.buildInputs ++ [ + pkgs.libgcc + ]; runtimeDependencies = dlDeps; @@ -99,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 ]} ''; } ); @@ -122,7 +117,7 @@ }; devShells.default = craneLib.devShell { - inherit LD_LIBRARY_PATH; + LD_LIBRARY_PATH = lib.makeLibraryPath dlDeps; inputsFrom = [ crate ]; packages = [ pkgs.rust-analyzer ]; diff --git a/src/main.rs b/src/main.rs index 18d35a0..c676e44 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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), ImageDisplayReady(Result), + SavePathPicked(Result), Event(iced::Event), } -#[derive(Clone, Debug, Default)] +#[derive(Default)] struct State { image: Option, image_display: Option, + image_filter: widget::image::FilterMethod, error: Option, + info: Option, + infobar_shown: bool, + + clipboard: Option, } impl State { fn new() -> (Self, Task) { - 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() }, - ]) - .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 { 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(); } + return widget::operation::focus_next(); } - Key::Character("o") => match self.pick_and_load_image() { - Ok(task) => { + Key::Character("o") => { + return Task::perform(utils::pick_image(), Message::ImagePicked); + } + + Key::Character("r") => match self.image.as_ref() { + None => self.error = Some("no image to rotate".into()), + Some(image) => { self.error = None; - return task; + self.image = Some(imageops::rotate90(image)); + return self.allocate_image(); } - Err(e) => self.error = Some(e), }, - - Key::Character("r") => { - match self.image.as_ref() { - None => self.error = Some("no image to rotate".into()), - Some(image) => { - self.error = None; - self.image = Some(imageops::rotate90(image)); - return self.allocate_image(); - } - }; + Key::Character("i") => match self.image.as_mut() { + None => self.error = Some("no image to invert".into()), + Some(image) => { + self.error = None; + imageops::invert(image); + return self.allocate_image(); + } + }, + Key::Character("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("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("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) -> Result, String> { - utils::load_image(path).map(|image| { + utils::load_image(&path).map(|image| { + self.info = Some(format!( + "{} {} {}x{}", + path.as_ref() + .file_name() + .map_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, String> { - utils::pick_image().and_then(|path| self.load_image(path)) - } - - fn save_image(&mut self) { - self.error = utils::save_image(self.image.as_ref()).err(); - } fn allocate_image(&self) -> Task { let Some(image) = self.image.as_ref() else { @@ -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,15 +287,21 @@ 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) @@ -183,12 +309,9 @@ fn main() -> Result<(), iced::Error> { .theme(Theme::custom( "custom", theme::Palette { - background: color!(0x080808), + background: Color::BLACK, text: Color::WHITE, - primary: color!(0xff00ff), - success: color!(0x00ff00), - warning: color!(0x880000), - danger: color!(0xff0000), + ..theme::Palette::DARK }, )) .run() diff --git a/src/usage.txt b/src/usage.txt index 94cf3cf..9df1bc2 100644 --- a/src/usage.txt +++ b/src/usage.txt @@ -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 diff --git a/src/utils.rs b/src/utils.rs index 1477a4e..ae1dbb1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,81 +1,84 @@ -use std::path::{Path, PathBuf}; +use std::{ + borrow::Cow, + ffi::OsStr, + path::{Path, PathBuf}, +}; -use image::{DynamicImage, ImageDecoder, ImageReader, RgbaImage}; -use rfd::FileDialog; +use iced::widget; +use image::{DynamicImage, ImageDecoder, ImageReader, ImageResult, RgbaImage}; +use rfd::AsyncFileDialog; pub fn load_image(path: impl AsRef) -> Result { - let path = path.as_ref(); + load_image_impl(path).map_err(|e| e.to_string()) +} +fn load_image_impl(path: impl AsRef) -> ImageResult { + let mut decoder = ImageReader::open(&path)? + .with_guessed_format()? + .into_decoder()?; - let reader = match ImageReader::open(path) { - Ok(r) => r, - Err(e) => { - return Err(format!( - "failed to create reader for '{}': {e}", - path.display() - )); - } - }; + let oreintation = decoder.orientation()?; - let mut decoder = match reader.into_decoder() { - Ok(d) => d, - Err(e) => { - return Err(format!( - "failed to create decoder for '{}': {e}", - path.display() - )); - } - }; + let mut decoded_image = DynamicImage::from_decoder(decoder)?; - 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); + // the condition is a workaround to not rotate JXL images twice; + // since they are already rotated by the decoder + // (while jpegs for instance aren't) + if !path + .as_ref() + .extension() + .map(OsStr::to_string_lossy) + .is_some_and(|s| s == "jxl") + { + decoded_image.apply_orientation(oreintation); + } Ok(decoded_image.into_rgba8()) } -pub fn pick_image() -> Result { - let Some(path) = FileDialog::new() +pub async fn pick_image() -> Result { + 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 { + let Some(filehandle) = AsyncFileDialog::new().save_file().await else { return Err("no path to save provided".into()); }; - if let Err(e) = image.save(&path) { - return Err(format!("failed to save '{}': {e}", path.display())); - }; + Ok(filehandle.path().to_owned()) +} - Ok(()) +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", + } +} + +pub fn arboard_from_rgbaimage<'a>(image: &'a RgbaImage) -> arboard::ImageData<'a> { + arboard::ImageData { + width: image.width() as usize, + height: image.height() as usize, + bytes: Cow::Borrowed(image.as_ref()), + } +} +pub fn rgbaimage_from_arboard(image: arboard::ImageData) -> RgbaImage { + RgbaImage::from_raw( + image.width as u32, + image.height as u32, + image.bytes.into_owned(), + ) + .unwrap() }