diff --git a/Cargo.lock b/Cargo.lock index 0b62a58..cec2b13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,15 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] [[package]] name = "android_system_properties" @@ -11,33 +20,127 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[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 2.0.106", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "av1-grain" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "bitstream-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" + +[[package]] +name = "built" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" + [[package]] name = "bumpalo" version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +[[package]] +name = "bytemuck" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "carbonyl" version = "0.0.3" dependencies = [ "chrono", + "fast_image_resize", "libc", + "sixel-bytes", "unicode-segmentation", "unicode-width", ] [[package]] name = "cc" -version = "1.0.79" +version = "1.2.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] [[package]] name = "cfg-if" @@ -76,6 +179,31 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "cxx" version = "1.0.88" @@ -100,7 +228,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn", + "syn 1.0.107", ] [[package]] @@ -117,9 +245,106 @@ checksum = "357f40d1f06a24b60ae1fe122542c1fb05d28d32acb2aed064e84bc2ad1e252e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fast_image_resize" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd1eda71e8af93f8b00e189404235d82f4de77ea4a0d182b44a7f03994d647c" +dependencies = [ + "bytemuck", + "cfg-if", + "document-features", + "image", + "num-traits", + "rayon", + "thiserror 2.0.17", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.7+wasi-0.2.4", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "iana-time-zone" version = "0.1.53" @@ -144,6 +369,66 @@ dependencies = [ "cxx-build", ] +[[package]] +name = "image" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "ravif", + "rayon", +] + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[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 2.0.106", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + [[package]] name = "js-sys" version = "0.3.60" @@ -155,9 +440,19 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.139" +version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +dependencies = [ + "arbitrary", + "cc", +] [[package]] name = "link-cplusplus" @@ -168,6 +463,12 @@ dependencies = [ "cc", ] +[[package]] +name = "litrs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" + [[package]] name = "log" version = "0.4.17" @@ -177,6 +478,97 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "make-cmd" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8ca8afbe8af1785e09636acb5a41e08a765f5f0340568716c18a8700ba3c0d3" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "moxcms" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08" +dependencies = [ + "num-traits", + "pxfm", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "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 = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[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 2.0.106", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -187,45 +579,307 @@ 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.15" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" -version = "1.17.0" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] [[package]] name = "proc-macro2" -version = "1.0.50" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.106", +] + +[[package]] +name = "pxfm" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde" +dependencies = [ + "num-traits", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" -version = "1.0.23" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[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.16", +] + +[[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", + "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.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "scratch" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "sixel-bytes" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45cad296a72571e80953823496e9a55caf893e264de9a7c5cfd29427fca720fc" +dependencies = [ + "sixel-sys-static", +] + +[[package]] +name = "sixel-sys-static" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2988846c5099382a880a7dd385d38b203a60430710a9c22e538d500e6908f4f9" +dependencies = [ + "make-cmd", + "pkg-config", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + [[package]] name = "syn" version = "1.0.107" @@ -237,6 +891,36 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[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 = "termcolor" version = "1.2.0" @@ -246,6 +930,46 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "time" version = "0.1.45" @@ -253,10 +977,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" dependencies = [ "libc", - "wasi", + "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.6" @@ -275,42 +1033,85 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" -version = "0.2.83" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", + "once_cell", + "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.83" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.83" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -318,22 +1119,25 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.83" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.83" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] [[package]] name = "winapi" @@ -365,3 +1169,38 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] diff --git a/Cargo.toml b/Cargo.toml index 295337c..c1309d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,8 @@ libc = "0.2" unicode-width = "0.1.10" unicode-segmentation = "1.10.0" chrono = "0.4.23" +sixel-bytes = "0.2" +fast_image_resize = { version = "5", features = ["rayon"] } [lib] name = "carbonyl" diff --git a/build.rs b/build.rs index 2ff9a33..09f46fb 100644 --- a/build.rs +++ b/build.rs @@ -12,8 +12,14 @@ fn link_sysroot() { "cargo:rustc-link-arg=--sysroot=./chromium/src/build/linux/debian_bullseye_amd64-sysroot" ); } else { - println!("cargo:warning={}", "x86_64 debian sysroot provided by chromium was not found!"); - println!("cargo:warning={}", "carbonyl may fail to link against a proper libc!"); + println!( + "cargo:warning={}", + "x86_64 debian sysroot provided by chromium was not found!" + ); + println!( + "cargo:warning={}", + "carbonyl may fail to link against a proper libc!" + ); } } @@ -29,8 +35,14 @@ fn link_sysroot() { "cargo:rustc-link-arg=--sysroot=./chromium/src/build/linux/debian_bullseye_i386-sysroot" ); } else { - println!("cargo:warning={}", "x86 debian sysroot provided by chromium was not found!"); - println!("cargo:warning={}", "carbonyl may fail to link against a proper libc!"); + println!( + "cargo:warning={}", + "x86 debian sysroot provided by chromium was not found!" + ); + println!( + "cargo:warning={}", + "carbonyl may fail to link against a proper libc!" + ); } } diff --git a/readme.md b/readme.md index 5d79824..6b86b66 100644 --- a/readme.md +++ b/readme.md @@ -67,6 +67,10 @@ $ carbonyl https://github.com +## Rendering + +Carbonyl now prefers Sixel graphics for page rendering whenever the terminal reports support. Legacy character-based rendering is automatically used as a fallback if Sixel frames cannot be sent. Sixel scrolling is enabled by default so web content can be browsed normally; set `CARBONYL_SIXEL_SCROLL=off` (or `0`) to opt back into the legacy non-scrolling behaviour. + ## Known issues - Fullscreen mode not supported yet diff --git a/src/browser/bridge.rs b/src/browser/bridge.rs index 7b3d629..2e7e030 100644 --- a/src/browser/bridge.rs +++ b/src/browser/bridge.rs @@ -187,12 +187,15 @@ pub extern "C" fn carbonyl_renderer_resize(bridge: RendererPtr) { let mut bridge = bridge.unwrap().lock().unwrap(); let window = bridge.window.update(); let cells = window.cells.clone(); + // Use the full terminal pixel geometry for SIXEL frames. + let geometry = window.graphics_px; log::debug!("resizing renderer, terminal window: {:?}", window); - bridge - .renderer - .render(move |renderer| renderer.set_size(cells)); + bridge.renderer.render(move |renderer| { + renderer.set_size(cells); + renderer.update_sixel_geometry(geometry); + }); } #[no_mangle] @@ -411,6 +414,11 @@ pub extern "C" fn carbonyl_renderer_listen(bridge: RendererPtr, delegate: *mut B Terminal(terminal) => match terminal { TerminalEvent::Name(name) => log::debug!("terminal name: {name}"), TerminalEvent::TrueColorSupported => renderer.enable_true_color(), + TerminalEvent::SixelSupported { .. } => { + let geometry = bridge.lock().unwrap().window.graphics_px; + + renderer.enable_sixel(geometry) + } }, } } diff --git a/src/cli/cli.rs b/src/cli/cli.rs index fc977b7..997d377 100644 --- a/src/cli/cli.rs +++ b/src/cli/cli.rs @@ -9,6 +9,7 @@ pub struct CommandLine { pub zoom: f32, pub debug: bool, pub bitmap: bool, + pub sixel_only: bool, pub program: CommandLineProgram, pub shell_mode: bool, } @@ -16,6 +17,7 @@ pub struct CommandLine { pub enum EnvVar { Debug, Bitmap, + SixelOnly, ShellMode, } @@ -24,6 +26,7 @@ impl EnvVar { match self { EnvVar::Debug => "CARBONYL_ENV_DEBUG", EnvVar::Bitmap => "CARBONYL_ENV_BITMAP", + EnvVar::SixelOnly => "CARBONYL_ENV_SIXEL_ONLY", EnvVar::ShellMode => "CARBONYL_ENV_SHELL_MODE", } } @@ -41,6 +44,7 @@ impl CommandLine { let mut zoom = 1.0; let mut debug = false; let mut bitmap = false; + let mut sixel_only = true; let mut shell_mode = false; let mut program = CommandLineProgram::Main; let args = env::args().skip(1).collect::>(); @@ -77,6 +81,12 @@ impl CommandLine { "-z" | "--zoom" => set_f32!(zoom = zoom / 100.0), "-d" | "--debug" => set!(debug, Debug), "-b" | "--bitmap" => set!(bitmap, Bitmap), + "--sixel-only" => set!(sixel_only, SixelOnly), + "--legacy-text" => { + sixel_only = false; + + env::set_var(EnvVar::SixelOnly, "0"); + } "-h" | "--help" => program = CommandLineProgram::Help, "-v" | "--version" => program = CommandLineProgram::Version, @@ -92,6 +102,14 @@ impl CommandLine { bitmap = true; } + if let Ok(value) = env::var(EnvVar::SixelOnly) { + let normalized = value.trim().to_ascii_lowercase(); + + sixel_only = !matches!(normalized.as_str(), "0" | "false" | "off" | "no"); + } + + env::set_var(EnvVar::SixelOnly, if sixel_only { "1" } else { "0" }); + if env::var(EnvVar::ShellMode).is_ok() { shell_mode = true; } @@ -102,6 +120,7 @@ impl CommandLine { zoom, debug, bitmap, + sixel_only, program, shell_mode, } diff --git a/src/cli/usage.txt b/src/cli/usage.txt index 5a2083d..e6c355c 100644 --- a/src/cli/usage.txt +++ b/src/cli/usage.txt @@ -9,6 +9,7 @@ Usage: carbonyl [options] [url] Options: -f, --fps= set the maximum number of frames per second (default: 60) -z, --zoom= set the zoom level in percent (default: 100) + --legacy-text re-enable the legacy ANSI text renderer -b, --bitmap render text as bitmaps -d, --debug enable debug logs -h, --help display this help message diff --git a/src/input.rs b/src/input.rs index f78e753..f71313e 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,4 +1,5 @@ mod dcs; +mod graphics; mod keyboard; mod listen; mod mouse; diff --git a/src/input/graphics.rs b/src/input/graphics.rs new file mode 100644 index 0000000..12066ac --- /dev/null +++ b/src/input/graphics.rs @@ -0,0 +1,72 @@ +use crate::control_flow; + +use super::{Event, ParseControlFlow, TerminalEvent}; + +#[derive(Default, Clone, Debug)] +pub struct Graphics { + params: Vec, + buffer: Vec, +} + +impl Graphics { + pub fn new() -> Self { + Self::default() + } + + pub fn parse(&mut self, key: u8) -> ParseControlFlow { + match key { + b'0'..=b'9' => { + self.buffer.push(key); + + control_flow!(continue) + } + b';' => { + self.push_param(); + + control_flow!(continue) + } + b'S' => { + self.push_param(); + + control_flow!(break self.event())? + } + _ => control_flow!(break)?, + } + } + + fn push_param(&mut self) { + if self.buffer.is_empty() { + self.params.push(0); + return; + } + + if let Ok(text) = std::str::from_utf8(&self.buffer) { + if let Ok(value) = text.parse::() { + self.params.push(value); + } + } + + self.buffer.clear(); + } + + fn event(&mut self) -> Option { + let params = std::mem::take(&mut self.params); + + if params.len() >= 2 { + let item = params[0]; + let status = params[1]; + + if item == 2 && status == 0 { + let width = params.get(2).copied().unwrap_or_default(); + let height = params.get(3).copied().unwrap_or_default(); + + return Some(Event::Terminal(TerminalEvent::SixelSupported { + width, + height, + })); + } + } + + None + } +} diff --git a/src/input/parser.rs b/src/input/parser.rs index f27b630..f9f6b5c 100644 --- a/src/input/parser.rs +++ b/src/input/parser.rs @@ -1,5 +1,6 @@ use std::ops::ControlFlow; +use super::graphics::Graphics; use crate::input::*; #[derive(Default)] @@ -17,12 +18,14 @@ enum Sequence { Mouse(Mouse), Keyboard(Keyboard), DeviceControl(DeviceControl), + Graphics(Graphics), } #[derive(Clone, Debug)] pub enum TerminalEvent { Name(String), TrueColorSupported, + SixelSupported { width: u32, height: u32 }, } #[derive(Clone, Debug)] @@ -91,12 +94,14 @@ impl Parser { }, Sequence::Control => match key { b'<' => Sequence::Mouse(Mouse::new()), + b'?' => Sequence::Graphics(Graphics::new()), b'1' => Sequence::Keyboard(Keyboard::new()), key => emit!(Keyboard::key(key, 0)), }, Sequence::Mouse(ref mut mouse) => parse!(mouse, key), Sequence::Keyboard(ref mut keyboard) => parse!(keyboard, key), Sequence::DeviceControl(ref mut dcs) => parse!(dcs, key), + Sequence::Graphics(ref mut graphics) => parse!(graphics, key), } } diff --git a/src/input/tty.rs b/src/input/tty.rs index 5cf491b..e4203f9 100644 --- a/src/input/tty.rs +++ b/src/input/tty.rs @@ -87,12 +87,16 @@ impl TTY { write!(out, "\x1b[?{}{}", sequence, if enable { "h" } else { "l" })?; } - // Set the current foreground color to black + // Set the current background color to black write!(out, "\x1b[48;2;0;0;0m")?; // Query current foreground color to for true-color support detection write!(out, "\x1bP$qm\x1b\\")?; // Query current terminal name write!(out, "\x1bP+q544e\x1b\\")?; + // Query graphics capability (XTSMGRAPHICS). Some terminals expect DCS form; + // use it first and fall back to CSI if ignored. + write!(out, "\x1bP?2;1;0S\x1b\\")?; + write!(out, "\x1b[?2;1;0S")?; out.flush() } diff --git a/src/output.rs b/src/output.rs index 9d16b5f..bba4e4a 100644 --- a/src/output.rs +++ b/src/output.rs @@ -6,6 +6,7 @@ mod painter; mod quad; mod render_thread; mod renderer; +mod sixel; mod window; mod xterm; diff --git a/src/output/cell.rs b/src/output/cell.rs index 6c2193d..0e66c4e 100644 --- a/src/output/cell.rs +++ b/src/output/cell.rs @@ -18,6 +18,7 @@ pub struct Cell { /// Text grapheme if any pub grapheme: Option>, pub quadrant: (Color, Color, Color, Color), + pub image: bool, } impl Cell { @@ -31,6 +32,7 @@ impl Cell { Color::black(), Color::black(), ), + image: false, } } } diff --git a/src/output/painter.rs b/src/output/painter.rs index a28862d..5645e84 100644 --- a/src/output/painter.rs +++ b/src/output/painter.rs @@ -1,8 +1,17 @@ -use std::io::{self, Stdout, Write}; +use std::{ + env, + io::{self, Stdout, Write}, +}; -use crate::gfx::{Color, Point}; +use crate::gfx::{Color, Point, Size}; +use crate::utils::log; +use sixel_bytes::DiffusionMethod; -use super::{binarize_quandrant, Cell}; +use super::{ + binarize_quandrant, + sixel::{Error as SixelError, Frame}, + Cell, +}; pub struct Painter { output: Stdout, @@ -13,6 +22,16 @@ pub struct Painter { foreground: Option, background_code: Option, foreground_code: Option, + sixel: Option, + sixel_only: bool, +} + +struct SixelState { + configured: bool, + geometry: Size, + pending: Option, + scrolling: bool, + dither: DiffusionMethod, } impl Painter { @@ -25,10 +44,12 @@ impl Painter { foreground: None, background_code: None, foreground_code: None, + sixel: None, true_color: match std::env::var("COLORTERM").unwrap_or_default().as_str() { "truecolor" | "24bit" => true, _ => false, }, + sixel_only: false, } } @@ -40,8 +61,136 @@ impl Painter { self.true_color = true_color } + pub fn set_sixel_only(&mut self, sixel_only: bool) { + self.sixel_only = sixel_only; + } + + pub fn enable_sixel(&mut self, geometry: Size) { + let state = self.sixel.get_or_insert_with(|| { + let scrolling = env::var("CARBONYL_SIXEL_SCROLL") + .ok() + .and_then(|value| { + let normalized = value.trim().to_ascii_lowercase(); + + match normalized.as_str() { + "1" | "true" | "on" | "yes" => Some(true), + "0" | "false" | "off" | "no" => Some(false), + _ => None, + } + }) + .unwrap_or(false); + + let dither = match env::var("CARBONYL_SIXEL_DITHER") + .unwrap_or_else(|_| "none".into()) + .to_ascii_lowercase() + .as_str() + { + "auto" => DiffusionMethod::Auto, + "fs" | "floyd-steinberg" => DiffusionMethod::FS, + "atkinson" => DiffusionMethod::Atkinson, + "stucki" => DiffusionMethod::Stucki, + "burkes" => DiffusionMethod::Burkes, + "jajuni" | "jarvis" => DiffusionMethod::JaJuNi, + _ => DiffusionMethod::None, + }; + + SixelState { + configured: false, + geometry, + pending: None, + scrolling, + dither, + } + }); + + state.geometry = geometry; + } + + pub fn update_sixel_geometry(&mut self, geometry: Size) { + if let Some(state) = self.sixel.as_mut() { + state.geometry = geometry; + } + } + + pub fn queue_sixel_background(&mut self, pixels: &[u8], size: Size) -> bool { + let Some(state) = self.sixel.as_mut() else { + return false; + }; + + let expected = size.width as usize * size.height as usize * 4; + + if pixels.len() < expected { + log::error!( + "failed to encode sixel frame: unexpected buffer size (expected {expected}, actual {})", + pixels.len() + ); + state.pending = None; + + return false; + } + + let target = if state.geometry.width > 0 && state.geometry.height > 0 { + state.geometry + } else { + size + }; + + log::debug!( + "sixel encode: src={}x{} target={}x{} dither={:?}", + size.width, + size.height, + target.width, + target.height, + state.dither + ); + + match Frame::from_viewport_scaled(pixels, size, target, state.dither) { + Ok(frame) => { + state.pending = Some(frame); + + true + } + Err(SixelError::InvalidSize(invalid)) => { + log::error!("failed to encode sixel frame: viewport {invalid:?} is invalid"); + state.pending = None; + + false + } + Err(SixelError::Encode(error)) => { + log::error!("failed to encode sixel frame: {error}"); + state.pending = None; + + false + } + } + } + + fn sixel_enabled(&self) -> bool { + self.sixel.is_some() + } + pub fn begin(&mut self) -> io::Result<()> { - write!(self.buffer, "\x1b[?25l\x1b[?12l") + write!(self.buffer, "\x1b[?25l\x1b[?12l")?; + + if let Some(state) = self.sixel.as_mut() { + if !state.configured { + if state.scrolling { + write!(self.buffer, "\x1b[?80h")?; + } else { + write!(self.buffer, "\x1b[?80l")?; + } + state.configured = true; + } + + if let Some(frame) = state.pending.take() { + // Reposition cursor without clearing the entire screen. + write!(self.buffer, "\x1b[H")?; + self.buffer.extend_from_slice(&frame.bytes); + write!(self.buffer, "\x1b[H")?; + } + } + + Ok(()) } pub fn end(&mut self, cursor: Option) -> io::Result<()> { @@ -52,6 +201,8 @@ impl Painter { cursor.y + 1, cursor.x + 1 )?; + } else { + write!(self.buffer, "\x1b[?25h\x1b[?12h")?; } self.output.write(self.buffer.as_slice())?; @@ -67,8 +218,17 @@ impl Painter { cursor, quadrant, ref grapheme, + image, } = cell; + if self.sixel_only && self.sixel_enabled() { + return Ok(()); + } + + if self.sixel_enabled() && grapheme.is_none() && image { + return Ok(()); + } + let (char, background, foreground, width) = if let Some(grapheme) = grapheme { if grapheme.index > 0 { return Ok(()); diff --git a/src/output/render_thread.rs b/src/output/render_thread.rs index 43f648c..408d97d 100644 --- a/src/output/render_thread.rs +++ b/src/output/render_thread.rs @@ -59,7 +59,7 @@ impl RenderThread { fn boot(rx: Receiver) { let cmd = CommandLine::parse(); let mut sync = FrameSync::new(cmd.fps); - let mut renderer = Renderer::new(); + let mut renderer = Renderer::new(cmd.sixel_only); let mut needs_render = false; loop { diff --git a/src/output/renderer.rs b/src/output/renderer.rs index f515cad..d5bbb1a 100644 --- a/src/output/renderer.rs +++ b/src/output/renderer.rs @@ -23,11 +23,14 @@ pub struct Renderer { } impl Renderer { - pub fn new() -> Renderer { + pub fn new(sixel_only: bool) -> Renderer { + let mut painter = Painter::new(); + painter.set_sixel_only(sixel_only); + Renderer { nav: Navigation::new(), cells: Vec::with_capacity(0), - painter: Painter::new(), + painter, size: Size::new(0, 0), } } @@ -36,6 +39,14 @@ impl Renderer { self.painter.set_true_color(true) } + pub fn enable_sixel(&mut self, geometry: Size) { + self.painter.enable_sixel(geometry); + } + + pub fn update_sixel_geometry(&mut self, geometry: Size) { + self.painter.update_sixel_geometry(geometry); + } + pub fn keypress(&mut self, key: &Key) -> io::Result { let action = self.nav.keypress(key); @@ -114,6 +125,7 @@ impl Renderer { previous.quadrant = current.quadrant; previous.grapheme = current.grapheme.clone(); + previous.image = current.image; self.painter.paint(current)?; } @@ -125,6 +137,8 @@ impl Renderer { /// Draw the background from a pixel array encoded in RGBA8888 pub fn draw_background(&mut self, pixels: &[u8], pixels_size: Size, rect: Rect) { + let uses_sixel = self.painter.queue_sixel_background(pixels, pixels_size); + let viewport = self.size.cast::(); if pixels.len() < viewport.width * viewport.height * 8 * 4 { @@ -163,12 +177,23 @@ impl Renderer { let (mut x, y) = (left * 2, y * 4); for (_, cell) in &mut self.cells[start..end] { - cell.quadrant = ( - pair(x + 0, y + 0), - pair(x + 1, y + 0), - pair(x + 1, y + 2), - pair(x + 0, y + 2), - ); + if uses_sixel { + cell.quadrant = ( + Color::black(), + Color::black(), + Color::black(), + Color::black(), + ); + cell.grapheme = None; + } else { + cell.quadrant = ( + pair(x + 0, y + 0), + pair(x + 1, y + 0), + pair(x + 1, y + 2), + pair(x + 0, y + 2), + ); + } + cell.image = true; x += 2; } @@ -195,6 +220,7 @@ impl Renderer { self.draw(rect, |cell| { cell.grapheme = None; cell.quadrant = (color, color, color, color); + cell.image = false; }) } @@ -214,6 +240,7 @@ impl Renderer { let right = left + size.width; for (_, current) in self.cells[left..right].iter_mut() { + current.image = false; draw(current) } } @@ -273,6 +300,7 @@ impl Renderer { previous.color != next.color || previous.char != next.char } } { + cell.image = false; cell.grapheme = Some(Rc::new(next)) } } diff --git a/src/output/sixel.rs b/src/output/sixel.rs new file mode 100644 index 0000000..d2eef2c --- /dev/null +++ b/src/output/sixel.rs @@ -0,0 +1,73 @@ +use crate::gfx::Size; +use fast_image_resize::{images::Image, FilterType, PixelType, ResizeAlg, ResizeOptions, Resizer}; +use sixel_bytes::{self, DiffusionMethod, PixelFormat}; + +#[derive(Clone, Debug)] +pub struct Frame { + pub bytes: Vec, +} + +#[derive(Debug)] +pub enum Error { + Encode(sixel_bytes::SixelError), + InvalidSize(Size), +} + +impl From for Error { + fn from(value: sixel_bytes::SixelError) -> Self { + Self::Encode(value) + } +} + +impl Frame { + fn encode_rgba(pixels: &[u8], size: Size, method: DiffusionMethod) -> Result { + if size.width == 0 || size.height == 0 { + return Err(Error::InvalidSize(size)); + } + + let bytes = sixel_bytes::sixel_string( + pixels, + size.width as i32, + size.height as i32, + PixelFormat::BGRA8888, + method, + ) + .map_err(Error::from)? + .into_bytes(); + + Ok(Self { bytes }) + } + + pub fn from_viewport_scaled( + pixels: &[u8], + src_size: Size, + target: Size, + method: DiffusionMethod, + ) -> Result { + if target.width == 0 || target.height == 0 || src_size.width == 0 || src_size.height == 0 { + return Err(Error::InvalidSize(target)); + } + + if src_size == target { + return Self::encode_rgba(pixels, src_size, method); + } + + let src = Image::from_vec_u8( + src_size.width, + src_size.height, + pixels.to_vec(), + PixelType::U8x4, + ) + .map_err(|_| Error::InvalidSize(src_size))?; + let mut dst = Image::new(target.width, target.height, PixelType::U8x4); + let mut resizer = Resizer::new(); + let options = + ResizeOptions::new().resize_alg(ResizeAlg::Convolution(FilterType::CatmullRom)); + resizer + .resize(&src, &mut dst, &options) + .map_err(|_| Error::InvalidSize(target))?; + let buffer = dst.into_vec(); + + Self::encode_rgba(&buffer, target, method) + } +} diff --git a/src/output/window.rs b/src/output/window.rs index a53c507..d7e7697 100644 --- a/src/output/window.rs +++ b/src/output/window.rs @@ -1,5 +1,11 @@ use core::mem::MaybeUninit; -use std::str::FromStr; +use std::{ + fs::OpenOptions, + io::{Read, Write}, + os::fd::AsRawFd, + str::FromStr, + time::{Duration, Instant}, +}; use crate::{cli::CommandLine, gfx::Size, utils::log}; @@ -14,6 +20,10 @@ pub struct Window { pub cells: Size, /// Size of the browser window in pixels pub browser: Size, + /// Full terminal pixel geometry for graphics output + pub graphics_px: Size, + /// Device scale factor used by Chromium (integer preferred) + pub dsf: f32, /// Command line arguments pub cmd: CommandLine, } @@ -26,6 +36,8 @@ impl Window { scale: (0.0, 0.0).into(), cells: (0, 0).into(), browser: (0, 0).into(), + graphics_px: (0, 0).into(), + dsf: 1.0, cmd: CommandLine::parse(), }; @@ -35,7 +47,7 @@ impl Window { } pub fn update(&mut self) -> &Self { - let (mut term, mut cell) = unsafe { + let (mut term, cell) = unsafe { let mut ptr = MaybeUninit::::uninit(); if libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, ptr.as_mut_ptr()) == 0 { @@ -50,11 +62,6 @@ impl Window { } }; - if cell.width == 0 || cell.height == 0 { - cell.width = 8; - cell.height = 16; - } - if term.width == 0 || term.height == 0 { let cols = match parse_var("COLUMNS").unwrap_or(0) { 0 => 80, @@ -77,24 +84,54 @@ impl Window { term.height = rows; } - let zoom = 1.5 * self.cmd.zoom; - let cells = Size::new(term.width.max(1), term.height.max(2) - 1); - let auto_scale = false; - let cell_pixels = if auto_scale { - Size::new(cell.width as f32, cell.height as f32) / cells.cast() - } else { - Size::new(8.0, 16.0) - }; - // Normalize the cells dimensions for an aspect ratio of 1:2 - let cell_width = (cell_pixels.width + cell_pixels.height / 2.0) / 2.0; + let mut cell_pixels = + if term.width > 0 && term.height > 0 && cell.width > 0 && cell.height > 0 { + Size::new( + cell.width as f32 / term.width.max(1) as f32, + cell.height as f32 / term.height.max(1) as f32, + ) + } else { + Size::new(0.0, 0.0) + }; + + if cell_pixels.width <= 0.0 || cell_pixels.height <= 0.0 { + if let Some(win_px) = query_window_pixels() { + cell_pixels = Size::new( + win_px.width / term.width.max(1) as f32, + win_px.height / term.height.max(1) as f32, + ); + } - // Round DPI to 2 decimals for proper viewport computations - self.dpi = (2.0 / cell_width * zoom * 100.0).ceil() / 100.0; - // A virtual cell should contain a 2x4 pixel quadrant - self.scale = Size::new(2.0, 4.0) / self.dpi; + if cell_pixels.width <= 0.0 || cell_pixels.height <= 0.0 { + cell_pixels = query_cell_geometry().unwrap_or(Size::new(8.0, 16.0)); + } + } + // Normalize the cells dimensions for an aspect ratio of 1:2 + self.scale = cell_pixels; // Keep some space for the UI self.cells = Size::new(term.width.max(1), term.height.max(2) - 1).cast(); - self.browser = self.cells.cast::().mul(self.scale).ceil().cast(); + self.graphics_px = Size::new( + (self.cells.width as f32 * cell_pixels.width).round() as u32, + (self.cells.height as f32 * cell_pixels.height).round() as u32, + ); + + // Choose an integer device scale factor (DSF) to avoid fractional raster scaling. + let mut dsf = if cell_pixels.height >= 18.0 { 2.0 } else { 1.0 }; + if let Ok(value) = std::env::var("CARBONYL_DSF") { + match value.trim() { + "1" => dsf = 1.0, + "2" => dsf = 2.0, + "3" => dsf = 3.0, + _ => {} + } + } + self.dsf = dsf; + self.dpi = self.dsf; + + self.browser = Size::new( + (self.graphics_px.width as f32 / self.dsf).round() as u32, + (self.graphics_px.height as f32 / self.dsf).round() as u32, + ); self } @@ -103,3 +140,131 @@ impl Window { fn parse_var(var: &str) -> Option { std::env::var(var).ok()?.parse().ok() } + +fn query_cell_geometry() -> Option> { + let mut tty = OpenOptions::new() + .read(true) + .write(true) + .open("/dev/tty") + .ok()?; + let fd = tty.as_raw_fd(); + let mut term = MaybeUninit::::uninit(); + + unsafe { + if libc::tcgetattr(fd, term.as_mut_ptr()) != 0 { + return None; + } + } + + let original = unsafe { term.assume_init() }; + let mut raw = original; + let c_oflag = raw.c_oflag; + + unsafe { + libc::cfmakeraw(&mut raw); + } + + raw.c_oflag = c_oflag; + + if unsafe { libc::tcsetattr(fd, libc::TCSANOW, &raw) } != 0 { + return None; + } + + struct Restore(libc::c_int, libc::termios); + + impl Drop for Restore { + fn drop(&mut self) { + unsafe { + libc::tcsetattr(self.0, libc::TCSANOW, &self.1); + } + } + } + + let _restore = Restore(fd, original); + + if tty.write_all(b"\x1b[16t").is_err() || tty.flush().is_err() { + return None; + } + + let mut buffer = [0u8; 128]; + let mut length = 0usize; + let deadline = Instant::now() + Duration::from_millis(100); + + while length < buffer.len() && Instant::now() < deadline { + let remaining = deadline.saturating_duration_since(Instant::now()); + let timeout = remaining.as_millis().min(i32::MAX as u128) as libc::c_int; + let mut fds = libc::pollfd { + fd, + events: libc::POLLIN, + revents: 0, + }; + + let result = unsafe { libc::poll(&mut fds, 1, timeout) }; + + if result <= 0 { + break; + } + + match tty.read(&mut buffer[length..]) { + Ok(0) => break, + Ok(read) => { + length += read; + + if buffer[..length].contains(&b't') { + break; + } + } + Err(_) => break, + } + } + + if length == 0 { + return None; + } + + let response = std::str::from_utf8(&buffer[..length]).ok()?; + let start = response.rfind("\u{1b}[6;")?; + let rest = &response[start + 3..]; + let end = rest.find('t')?; + let mut parts = rest[..end].split(';'); + let height = parts.next()?.parse::().ok()?; + let width = parts.next()?.parse::().ok()?; + + if width <= 0.0 || height <= 0.0 { + return None; + } + + Some(Size::new(width, height)) +} + +fn query_window_pixels() -> Option> { + let mut tty = OpenOptions::new() + .read(true) + .write(true) + .open("/dev/tty") + .ok()?; + + tty.write_all(b"\x1b[14t").ok()?; + tty.flush().ok()?; + + let mut buf = [0u8; 128]; + let n = tty.read(&mut buf).ok()?; + + if n == 0 { + return None; + } + + let response = std::str::from_utf8(&buf[..n]).ok()?; + let start = response.rfind("\u{1b}[4;")?; + let rest = &response[start + 3..]; + let end = rest.find('t')?; + let mut parts = rest[..end].split(';'); + let height = parts.next()?.parse::().ok()?; + let width = parts.next()?.parse::().ok()?; + + if width <= 0.0 || height <= 0.0 { + return None; + } + + Some(Size::new(width, height)) +}