diff --git a/Cargo.lock b/Cargo.lock index 89d0adb..e9b7fd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" + [[package]] name = "allocator-api2" version = "0.2.21" @@ -103,12 +109,58 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "av1-grain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e" +dependencies = [ + "arrayvec", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -186,6 +238,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -201,6 +259,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitstream-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" + [[package]] name = "bstr" version = "1.11.3" @@ -211,6 +275,12 @@ dependencies = [ "serde", ] +[[package]] +name = "built" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" + [[package]] name = "bumpalo" version = "3.17.0" @@ -223,6 +293,18 @@ version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.9.0" @@ -262,9 +344,21 @@ version = "1.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" dependencies = [ + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -357,6 +451,12 @@ dependencies = [ "windows", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.3" @@ -622,12 +722,36 @@ version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" +[[package]] +name = "exr" +version = "1.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "filedescriptor" version = "0.8.2" @@ -755,6 +879,16 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.31.1" @@ -854,6 +988,45 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "image" +version = "0.25.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" + [[package]] name = "indexmap" version = "2.7.1" @@ -884,6 +1057,17 @@ dependencies = [ "syn", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "is-terminal" version = "0.4.15" @@ -910,6 +1094,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -925,6 +1118,21 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + [[package]] name = "js-sys" version = "0.3.77" @@ -941,12 +1149,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +[[package]] +name = "libfuzzer-sys" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libredox" version = "0.1.3" @@ -979,6 +1203,15 @@ version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "lru" version = "0.12.5" @@ -997,6 +1230,16 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1016,6 +1259,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -1030,6 +1274,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nom" version = "7.1.3" @@ -1040,6 +1290,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1080,12 +1336,53 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1258,12 +1555,34 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.93" @@ -1273,6 +1592,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.32.0" @@ -1291,6 +1644,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + [[package]] name = "ratatui" version = "0.29.0" @@ -1313,6 +1696,56 @@ dependencies = [ "unicode-width 0.2.0", ] +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.12.1", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "system-deps", + "thiserror 1.0.69", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + [[package]] name = "rayon" version = "1.10.0" @@ -1586,6 +2019,21 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -1679,6 +2127,25 @@ dependencies = [ "walkdir", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "television" version = "0.10.6" @@ -1696,6 +2163,7 @@ dependencies = [ "gag", "human-panic", "ignore", + "image", "nom", "nucleo", "parking_lot", @@ -1818,6 +2286,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.37" @@ -2040,12 +2519,29 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "v_frame" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "walkdir" version = "2.5.0" @@ -2139,6 +2635,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "winapi" version = "0.3.9" @@ -2328,3 +2830,48 @@ name = "xterm-color" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4de5f056fb9dc8b7908754867544e26145767187aaac5a98495e88ad7cb8a80f" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index fe3112a..0095f0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,8 @@ bat = { version = "0.25", default-features = false, features = ["regex-onig"] } gag = "1.0" nucleo = "0.5" toml = "0.8" +image = "0.25" + [target.'cfg(windows)'.dependencies] winapi-util = "0.1.9" diff --git a/television/preview/mod.rs b/television/preview/mod.rs index 8bca931..1188c7b 100644 --- a/television/preview/mod.rs +++ b/television/preview/mod.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use crate::channels::entry::{Entry, PreviewType}; use devicons::FileIcon; +use ratatui::layout::Rect; pub mod ansi; pub mod cache; @@ -9,6 +10,7 @@ pub mod previewers; // previewer types use crate::utils::cache::RingSet; +use crate::utils::image::ImagePreviewWidget; use crate::utils::syntax::HighlightedLines; pub use previewers::basic::BasicPreviewer; pub use previewers::basic::BasicPreviewerConfig; @@ -30,6 +32,7 @@ pub enum PreviewContent { PlainText(Vec), PlainTextWrapped(String), AnsiText(String), + Image(ImagePreviewWidget), } impl PreviewContent { @@ -44,6 +47,9 @@ impl PreviewContent { PreviewContent::AnsiText(text) => { text.lines().count().try_into().unwrap_or(u16::MAX) } + PreviewContent::Image(image) => { + image.height().try_into().unwrap_or(u16::MAX) + } _ => 0, } } @@ -203,11 +209,15 @@ impl Previewer { } } - fn dispatch_request(&mut self, entry: &Entry) -> Option> { + fn dispatch_request( + &mut self, + entry: &Entry, + preview_window: Option, + ) -> Option> { match &entry.preview_type { PreviewType::Basic => Some(self.basic.preview(entry)), PreviewType::EnvVar => Some(self.env_var.preview(entry)), - PreviewType::Files => self.file.preview(entry), + PreviewType::Files => self.file.preview(entry, preview_window), PreviewType::Command(cmd) => self.command.preview(entry, cmd), PreviewType::None => Some(Arc::new(Preview::default())), } @@ -217,17 +227,23 @@ impl Previewer { // faster, but since it's already running in the background and quite // fast for most standard file sizes, plus we're caching the previews, // I'm not sure the extra complexity is worth it. - pub fn preview(&mut self, entry: &Entry) -> Option> { + pub fn preview( + &mut self, + entry: &Entry, + preview_window: Option, + ) -> Option> { // if we haven't acknowledged the request yet, acknowledge it self.requests.push(entry.clone()); - if let Some(preview) = self.dispatch_request(entry) { + if let Some(preview) = self.dispatch_request(entry, preview_window) { return Some(preview); } // lookup request stack and return the most recent preview available for request in self.requests.back_to_front() { - if let Some(preview) = self.dispatch_request(&request) { + if let Some(preview) = + self.dispatch_request(&request, preview_window) + { return Some(preview); } } diff --git a/television/preview/previewers/files.rs b/television/preview/previewers/files.rs index 32e7c92..6aa1c9c 100644 --- a/television/preview/previewers/files.rs +++ b/television/preview/previewers/files.rs @@ -1,6 +1,8 @@ use crate::utils::files::{read_into_lines_capped, ReadResult}; use crate::utils::syntax::HighlightedLines; +use image::ImageReader; use parking_lot::Mutex; +use ratatui::layout::Rect; use rustc_hash::{FxBuildHasher, FxHashSet}; use std::collections::HashSet; use std::fs::File; @@ -10,13 +12,13 @@ use std::sync::{ atomic::{AtomicU8, Ordering}, Arc, }; - use syntect::{highlighting::Theme, parsing::SyntaxSet}; use tracing::{debug, trace, warn}; use crate::channels::entry; use crate::preview::cache::PreviewCache; use crate::preview::{previewers::meta, Preview, PreviewContent}; +use crate::utils::image::ImagePreviewWidget; use crate::utils::{ files::FileType, strings::preprocess_line, @@ -78,20 +80,28 @@ impl FilePreviewer { self.cache.lock().get(&entry.name) } - pub fn preview(&mut self, entry: &entry::Entry) -> Option> { + pub fn preview( + &mut self, + entry: &entry::Entry, + preview_window: Option, + ) -> Option> { if let Some(preview) = self.cached(entry) { trace!("Preview cache hit for {:?}", entry.name); if preview.partial_offset.is_some() { // preview is partial, spawn a task to compute the next chunk // and return the partial preview debug!("Spawning partial preview task for {:?}", entry.name); - self.handle_preview_request(entry, Some(preview.clone())); + self.handle_preview_request( + entry, + Some(preview.clone()), + preview_window, + ); } Some(preview) } else { // preview is not in cache, spawn a task to compute the preview trace!("Preview cache miss for {:?}", entry.name); - self.handle_preview_request(entry, None); + self.handle_preview_request(entry, None, preview_window); None } } @@ -100,6 +110,7 @@ impl FilePreviewer { &mut self, entry: &entry::Entry, partial_preview: Option>, + preview_window: Option, ) { if self.in_flight_previews.lock().contains(&entry.name) { trace!("Preview already in flight for {:?}", entry.name); @@ -126,6 +137,7 @@ impl FilePreviewer { &syntax_theme, &concurrent_tasks, &in_flight_previews, + preview_window, ); }); } @@ -141,6 +153,7 @@ impl FilePreviewer { /// This ends up being the max size of partial previews. const PARTIAL_BUFREAD_SIZE: usize = 5 * 1024 * 1024; +#[allow(clippy::too_many_arguments)] pub fn try_preview( entry: &entry::Entry, partial_preview: Option>, @@ -149,6 +162,7 @@ pub fn try_preview( syntax_theme: &Arc, concurrent_tasks: &Arc, in_flight_previews: &Arc>>, + preview_window: Option, ) { debug!("Computing preview for {:?}", entry.name); let path = PathBuf::from(&entry.name); @@ -236,8 +250,66 @@ pub fn try_preview( cache.lock().insert(entry.name.clone(), &p); } } + } else if matches!(FileType::from(&path), FileType::Image) { + cache.lock().insert( + entry.name.clone(), + &meta::loading(&format!("Loading {}", entry.name)), + ); + + debug!("File {:?} is an image", entry.name); + let option_image = match ImageReader::open(path) { + Ok(reader) => match reader.with_guessed_format() { + Ok(reader) => match reader.decode() { + Ok(image) => Some(image), + Err(e) => { + warn!( + "Error impossible to decode {}: {:?}", + entry.name, e + ); + None + } + }, + Err(e) => { + warn!( + "Error impossible to guess the format of {}: {:?}", + entry.name, e + ); + None + } + }, + Err(e) => { + warn!("Error opening image {}: {:?}", entry.name, e); + None + } + }; + if let Some(image) = option_image { + let preview_window_dimension = preview_window.map(|rect| { + ( + u32::from(rect.width.saturating_sub(2)), + u32::from(rect.height.saturating_sub(2)), + ) // - 2 for the margin + }); + let image_preview_widget = ImagePreviewWidget::from_dynamic_image( + image, + preview_window_dimension, + ); + let total_lines = + image_preview_widget.height().try_into().unwrap_or(u16::MAX); + let content = PreviewContent::Image(image_preview_widget); + let preview = Arc::new(Preview::new( + entry.name.clone(), + content, + entry.icon, + None, + total_lines, + )); + cache.lock().insert(entry.name.clone(), &preview); + } else { + let p = meta::not_supported(&entry.name); + cache.lock().insert(entry.name.clone(), &p); + } } else { - debug!("File isn't text-based: {:?}", entry.name); + debug!("File format isn't supported for preview: {:?}", entry.name); let preview = meta::not_supported(&entry.name); cache.lock().insert(entry.name.clone(), &preview); } diff --git a/television/screen/preview.rs b/television/screen/preview.rs index 0525d86..529e5b5 100644 --- a/television/screen/preview.rs +++ b/television/screen/preview.rs @@ -4,13 +4,17 @@ use crate::preview::{ PREVIEW_NOT_SUPPORTED_MSG, TIMEOUT_MSG, }; use crate::screen::colors::{Colorscheme, PreviewColorscheme}; +use crate::utils::image::ImagePreviewWidget; use crate::utils::strings::{ replace_non_printable, shrink_with_ellipsis, ReplaceNonPrintableConfig, EMPTY_STRING, }; use anyhow::Result; use devicons::FileIcon; -use ratatui::widgets::{Block, BorderType, Borders, Padding, Paragraph, Wrap}; +use ratatui::buffer::Buffer; +use ratatui::widgets::{ + Block, BorderType, Borders, Padding, Paragraph, Widget, Wrap, +}; use ratatui::Frame; use ratatui::{ layout::{Alignment, Rect}, @@ -22,6 +26,22 @@ use std::str::FromStr; const FILL_CHAR_SLANTED: char = '╱'; const FILL_CHAR_EMPTY: char = ' '; +pub enum PreviewWidget<'a> { + Paragraph(Paragraph<'a>), + Image(ImagePreviewWidget), +} +impl Widget for PreviewWidget<'_> { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + match self { + PreviewWidget::Paragraph(p) => p.render(area, buf), + PreviewWidget::Image(image) => image.render(area, buf), + } + } +} + #[allow(clippy::too_many_arguments)] pub fn draw_preview_content_block( f: &mut Frame, @@ -38,9 +58,8 @@ pub fn draw_preview_content_block( &preview_state.preview.title, use_nerd_font_icons, )?; - // render the preview content - let rp = build_preview_paragraph( + let rp = build_preview_widget( inner, &preview_state.preview.content, preview_state.target_line, @@ -48,16 +67,17 @@ pub fn draw_preview_content_block( colorscheme, ); f.render_widget(rp, inner); + Ok(()) } -pub fn build_preview_paragraph<'a>( +pub fn build_preview_widget<'a>( inner: Rect, preview_content: &'a PreviewContent, target_line: Option, preview_scroll: u16, colorscheme: &'a Colorscheme, -) -> Paragraph<'a> { +) -> PreviewWidget<'a> { let preview_block = Block::default().style(Style::default()).padding(Padding { top: 0, @@ -65,65 +85,77 @@ pub fn build_preview_paragraph<'a>( bottom: 0, left: 1, }); + match preview_content { - PreviewContent::AnsiText(text) => { - build_ansi_text_paragraph(text, preview_block, preview_scroll) - } - PreviewContent::PlainText(content) => build_plain_text_paragraph( - content, - preview_block, - target_line, - preview_scroll, - colorscheme.preview, + PreviewContent::AnsiText(text) => PreviewWidget::Paragraph( + build_ansi_text_paragraph(text, preview_block, preview_scroll), ), - PreviewContent::PlainTextWrapped(content) => { + PreviewContent::PlainText(content) => { + PreviewWidget::Paragraph(build_plain_text_paragraph( + content, + preview_block, + target_line, + preview_scroll, + colorscheme.preview, + )) + } + PreviewContent::PlainTextWrapped(content) => PreviewWidget::Paragraph( build_plain_text_wrapped_paragraph( content, preview_block, colorscheme.preview, ) - .scroll((preview_scroll, 0)) - } + .scroll((preview_scroll, 0)), + ), PreviewContent::SyntectHighlightedText(highlighted_lines) => { - build_syntect_highlighted_paragraph( + PreviewWidget::Paragraph(build_syntect_highlighted_paragraph( &highlighted_lines.lines, preview_block, target_line, preview_scroll, colorscheme.preview, inner.height, - ) + )) } + PreviewContent::Image(image) => PreviewWidget::Image(image.clone()), + // meta - PreviewContent::Loading => { + PreviewContent::Loading => PreviewWidget::Paragraph( build_meta_preview_paragraph(inner, LOADING_MSG, FILL_CHAR_EMPTY) .block(preview_block) .alignment(Alignment::Left) - .style(Style::default().add_modifier(Modifier::ITALIC)) - } - PreviewContent::NotSupported => build_meta_preview_paragraph( - inner, - PREVIEW_NOT_SUPPORTED_MSG, - FILL_CHAR_EMPTY, - ) - .block(preview_block) - .alignment(Alignment::Left) - .style(Style::default().add_modifier(Modifier::ITALIC)), - PreviewContent::FileTooLarge => build_meta_preview_paragraph( - inner, - FILE_TOO_LARGE_MSG, - FILL_CHAR_EMPTY, - ) - .block(preview_block) - .alignment(Alignment::Left) - .style(Style::default().add_modifier(Modifier::ITALIC)), - PreviewContent::Timeout => { + .style(Style::default().add_modifier(Modifier::ITALIC)), + ), + PreviewContent::NotSupported => PreviewWidget::Paragraph( + build_meta_preview_paragraph( + inner, + PREVIEW_NOT_SUPPORTED_MSG, + FILL_CHAR_EMPTY, + ) + .block(preview_block) + .alignment(Alignment::Left) + .style(Style::default().add_modifier(Modifier::ITALIC)), + ), + PreviewContent::FileTooLarge => PreviewWidget::Paragraph( + build_meta_preview_paragraph( + inner, + FILE_TOO_LARGE_MSG, + FILL_CHAR_EMPTY, + ) + .block(preview_block) + .alignment(Alignment::Left) + .style(Style::default().add_modifier(Modifier::ITALIC)), + ), + + PreviewContent::Timeout => PreviewWidget::Paragraph( build_meta_preview_paragraph(inner, TIMEOUT_MSG, FILL_CHAR_EMPTY) + .block(preview_block) + .alignment(Alignment::Left) + .style(Style::default().add_modifier(Modifier::ITALIC)), + ), + PreviewContent::Empty => { + PreviewWidget::Paragraph(Paragraph::new(Text::raw(EMPTY_STRING))) } - .block(preview_block) - .alignment(Alignment::Left) - .style(Style::default().add_modifier(Modifier::ITALIC)), - PreviewContent::Empty => Paragraph::new(Text::raw(EMPTY_STRING)), } } diff --git a/television/television.rs b/television/television.rs index 07161e6..d3b9c3e 100644 --- a/television/television.rs +++ b/television/television.rs @@ -304,7 +304,10 @@ impl Television { && !matches!(selected_entry.preview_type, PreviewType::None) { // preview content - if let Some(preview) = self.previewer.preview(selected_entry) { + if let Some(preview) = self + .previewer + .preview(selected_entry, self.ui_state.layout.preview_window) + { // only update if the preview content has changed if self.preview_state.preview.title != preview.title { self.preview_state.update( diff --git a/television/utils/files.rs b/television/utils/files.rs index f21d613..70400b4 100644 --- a/television/utils/files.rs +++ b/television/utils/files.rs @@ -106,6 +106,7 @@ pub fn get_file_size(path: &Path) -> Option { #[derive(Debug)] pub enum FileType { Text, + Image, Other, Unknown, } @@ -117,6 +118,9 @@ where fn from(path: P) -> Self { debug!("Getting file type for {:?}", path); let p = path.as_ref(); + if is_accepted_image_extension(p) { + return FileType::Image; + } if is_known_text_extension(p) { return FileType::Text; } @@ -487,3 +491,28 @@ pub fn get_known_text_file_extensions() -> &'static FxHashSet<&'static str> { .collect() }) } + +pub fn is_accepted_image_extension

(path: P) -> bool +where + P: AsRef, +{ + path.as_ref() + .extension() + .and_then(|ext| ext.to_str()) + .is_some_and(|ext| get_known_image_file_extensions().contains(ext)) +} +pub static KNOWN_IMAGE_FILE_EXTENSIONS: OnceLock> = + OnceLock::new(); +pub fn get_known_image_file_extensions() -> &'static FxHashSet<&'static str> { + KNOWN_IMAGE_FILE_EXTENSIONS.get_or_init(|| { + [ + // "avif", requires the avif-native feature, uses the libdav1d C library. + // dds, dosen't work for some reason + "bmp", "ff", "gif", "hdr", "ico", "jpeg", "jpg", "exr", "png", + "pnm", "qoi", "tga", "tif", "webp", + ] + .iter() + .copied() + .collect() + }) +} diff --git a/television/utils/image.rs b/television/utils/image.rs new file mode 100644 index 0000000..f657db1 --- /dev/null +++ b/television/utils/image.rs @@ -0,0 +1,185 @@ +use image::imageops::FilterType; +use image::{DynamicImage, Pixel, Rgba}; +use ratatui::buffer::{Buffer, Cell}; +use ratatui::layout::{Position, Rect}; +use ratatui::prelude::Color; +use ratatui::widgets::Widget; +use std::fmt::Debug; +use std::hash::Hash; + +static PIXEL_STRING: &str = "▀"; +const FILTER_TYPE: FilterType = FilterType::Lanczos3; + +// use to reduce the size of the image before storing it +const DEFAULT_CACHED_WIDTH: u32 = 50; +const DEFAULT_CACHED_HEIGHT: u32 = 100; + +const GRAY: Rgba = Rgba([242, 242, 242, 255]); +const WHITE: Rgba = Rgba([255, 255, 255, 255]); + +#[derive(Clone, Debug, Hash, PartialEq)] +pub struct ImagePreviewWidget { + cells: Vec>, +} + +impl Widget for &ImagePreviewWidget { + fn render(self, area: Rect, buf: &mut Buffer) { + let height = self.height(); + let width = self.width(); + // offset of the left top corner where the image is centered + let total_width = usize::from(area.width) + 2 * usize::from(area.x); + let x_offset = total_width.saturating_sub(width) / 2 + 1; + let total_height = usize::from(area.height) + 2 * usize::from(area.y); + let y_offset = total_height.saturating_sub(height) / 2; + + let (area_border_up, area_border_down) = + (area.y, area.y + area.height); + let (area_border_left, area_border_right) = + (area.x, area.x + area.width); + for (y, row) in self.cells.iter().enumerate() { + let pos_y = u16::try_from(y_offset + y).unwrap_or(u16::MAX); + if pos_y >= area_border_up && pos_y < area_border_down { + for (x, cell) in row.iter().enumerate() { + let pos_x = + u16::try_from(x_offset + x).unwrap_or(u16::MAX); + if pos_x >= area_border_left && pos_x <= area_border_right + { + if let Some(buf_cell) = + buf.cell_mut(Position::new(pos_x, pos_y)) + { + *buf_cell = cell.clone(); + } + } + } + } + } + } +} +impl ImagePreviewWidget { + pub fn new(cells: Vec>) -> ImagePreviewWidget { + ImagePreviewWidget { cells } + } + + pub fn height(&self) -> usize { + self.cells.len() + } + pub fn width(&self) -> usize { + if self.height() > 0 { + self.cells[0].len() + } else { + 0 + } + } + + pub fn from_dynamic_image( + dynamic_image: DynamicImage, + dimension: Option<(u32, u32)>, + ) -> Self { + let (window_width, window_height) = + dimension.unwrap_or((DEFAULT_CACHED_WIDTH, DEFAULT_CACHED_HEIGHT)); + let (max_width, max_height) = (window_width, window_height * 2 - 2); // -2 to have some space with the title + + // first quick resize + let big_resized_image = if dynamic_image.width() > max_width * 4 + || dynamic_image.height() > max_height * 4 + { + dynamic_image.resize( + max_width * 4, + max_height * 4, + FilterType::Nearest, + ) + } else { + dynamic_image + }; + + // this time resize with the filter + let resized_image = if big_resized_image.width() > max_width + || big_resized_image.height() > max_height + { + big_resized_image.resize(max_width, max_height, FILTER_TYPE) + } else { + big_resized_image + }; + + let cells = Self::cells_from_dynamic_image(resized_image); + ImagePreviewWidget::new(cells) + } + + fn cells_from_dynamic_image(image: DynamicImage) -> Vec> { + let image_rgba = image.into_rgba8(); + + //creation of the grid of cell + image_rgba + // iter over pair of rows + .rows() + .step_by(2) + .zip(image_rgba.rows().skip(1).step_by(2)) + .enumerate() + .map(|(double_row_y, (row_1, row_2))| { + // create rows of cells + row_1 + .into_iter() + .zip(row_2) + .enumerate() + .map(|(x, (color_up, color_down))| { + let position = (x, double_row_y); + DoublePixel::new(*color_up, *color_down) + .add_grid_background(position) + .into_cell() + }) + .collect::>() + }) + .collect::>>() + } +} + +// util to convert Rgba into ratatui's Cell +struct DoublePixel { + color_up: Rgba, + color_down: Rgba, +} +impl DoublePixel { + pub fn new(color_up: Rgba, color_down: Rgba) -> Self { + Self { + color_up, + color_down, + } + } + + pub fn add_grid_background(mut self, position: (usize, usize)) -> Self { + let color_up = self.color_up.0; + let color_down = self.color_down.0; + self.color_up = Self::blend_with_background(color_up, position, 0); + self.color_down = Self::blend_with_background(color_down, position, 1); + self + } + + fn blend_with_background( + color: impl Into>, + position: (usize, usize), + offset: usize, + ) -> Rgba { + let color = color.into(); + if color[3] == 255 { + color + } else { + let is_white = (position.0 + position.1 * 2 + offset) % 2 == 0; + let mut base = if is_white { WHITE } else { GRAY }; + base.blend(&color); + base + } + } + + pub fn into_cell(self) -> Cell { + let mut cell = Cell::new(PIXEL_STRING); + cell.set_bg(Self::convert_image_color_to_ratatui_color( + self.color_down, + )) + .set_fg(Self::convert_image_color_to_ratatui_color(self.color_up)); + cell + } + + fn convert_image_color_to_ratatui_color(color: Rgba) -> Color { + Color::Rgb(color[0], color[1], color[2]) + } +} diff --git a/television/utils/mod.rs b/television/utils/mod.rs index 0b88158..9192f4a 100644 --- a/television/utils/mod.rs +++ b/television/utils/mod.rs @@ -3,6 +3,7 @@ pub mod clipboard; pub mod command; pub mod files; pub mod hashmaps; +pub mod image; pub mod indices; pub mod input; pub mod metadata;