diff --git a/Cargo.lock b/Cargo.lock index 31e39ae..b9ad7ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,9 +85,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.95" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4" [[package]] name = "arbitrary" @@ -134,9 +134,9 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62" +checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e" dependencies = [ "arrayvec", ] @@ -251,9 +251,9 @@ checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" [[package]] name = "cc" -version = "1.2.14" +version = "1.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" dependencies = [ "jobserver", "libc", @@ -304,9 +304,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.30" +version = "4.5.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d" +checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" dependencies = [ "clap_builder", "clap_derive", @@ -314,9 +314,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.30" +version = "4.5.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c" +checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" dependencies = [ "anstream", "anstyle", @@ -558,10 +558,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" [[package]] -name = "either" -version = "1.13.0" +name = "document-features" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + +[[package]] +name = "either" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d" [[package]] name = "env_filter" @@ -607,6 +616,20 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fast_image_resize" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55264ccc579fc127eebf6c6c1841d0c160d79a44c8f6f97047b7bc4a9c0d1a5" +dependencies = [ + "bytemuck", + "cfg-if", + "document-features", + "image", + "num-traits", + "thiserror 1.0.69", +] + [[package]] name = "fdeflate" version = "0.3.7" @@ -618,9 +641,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.35" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" dependencies = [ "crc32fast", "miniz_oxide", @@ -772,7 +795,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets", ] [[package]] @@ -952,9 +987,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.170" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" [[package]] name = "libdbus-sys" @@ -1013,6 +1048,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + [[package]] name = "lock_api" version = "0.4.12" @@ -1025,9 +1066,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.25" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" [[package]] name = "loop9" @@ -1071,9 +1112,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" dependencies = [ "adler2", "simd-adler32", @@ -1322,7 +1363,7 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -1390,8 +1431,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.2", + "zerocopy 0.8.21", ] [[package]] @@ -1401,7 +1453,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.2", ] [[package]] @@ -1410,7 +1472,17 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a509b1a2ffbe92afab0e55c8fd99dea1c280e8171bd2d88682bb20bc41cbc2c" +dependencies = [ + "getrandom 0.3.1", + "zerocopy 0.8.21", ] [[package]] @@ -1439,8 +1511,8 @@ dependencies = [ "once_cell", "paste", "profiling", - "rand", - "rand_chacha", + "rand 0.8.5", + "rand_chacha 0.3.1", "simd_helpers", "system-deps", "thiserror 1.0.69", @@ -1485,9 +1557,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f" dependencies = [ "bitflags 2.8.0", ] @@ -1560,7 +1632,7 @@ dependencies = [ "dbus", "objc", "pipewire", - "rand", + "rand 0.8.5", "screencapturekit", "screencapturekit-sys", "sysinfo", @@ -1600,18 +1672,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.217" +version = "1.0.218" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.218" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" dependencies = [ "proc-macro2", "quote", @@ -1643,10 +1715,11 @@ dependencies = [ [[package]] name = "servicepoint-cli" -version = "0.2.1" +version = "0.3.0" dependencies = [ "clap", "env_logger", + "fast_image_resize", "image", "log", "scap", @@ -1856,17 +1929,16 @@ dependencies = [ [[package]] name = "tungstenite" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413083a99c579593656008130e29255e54dcaae495be556cc26888f211648c24" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" dependencies = [ - "byteorder", "bytes", "data-encoding", "http", "httparse", "log", - "rand", + "rand 0.9.0", "sha1", "thiserror 2.0.11", "utf-8", @@ -1874,15 +1946,15 @@ dependencies = [ [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unicode-ident" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" +checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" [[package]] name = "unicode-segmentation" @@ -1943,6 +2015,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -2201,13 +2282,22 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603" +checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "wyz" version = "0.5.1" @@ -2233,7 +2323,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf01143b2dd5d134f11f545cf9f1431b13b749695cb33bcce051e7568f99478" +dependencies = [ + "zerocopy-derive 0.8.21", ] [[package]] @@ -2247,6 +2346,17 @@ dependencies = [ "syn", ] +[[package]] +name = "zerocopy-derive" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712c8386f4f4299382c9abee219bee7084f78fb939d88b6840fcc1320d5f6da2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zune-core" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 8076574..a8cd60f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "servicepoint-cli" description = "A command line interface for the ServicePoint display." -version = "0.2.1" +version = "0.3.0" edition = "2021" rust-version = "1.80.0" publish = true @@ -19,3 +19,4 @@ env_logger = "0.11" log = "0.4" scap = "0.0.8" image = "0.25.5" +fast_image_resize = { version = "5.1.2", features = ["image"] } diff --git a/README.md b/README.md index 439cf6d..be1dab7 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,10 @@ cargo run -- Usage: servicepoint-cli [OPTIONS] Commands: - reset-everything [aliases: r] - pixels [aliases: p] - brightness [aliases: b] - stream [aliases: s] + reset-everything Reset both pixels and brightness [aliases: r] + pixels Commands for manipulating pixels [aliases: p] + brightness Commands for manipulating the brightness [aliases: b] + text Commands for sending text to the screen [aliases: t] help Print this message or the help of the given subcommand(s) Options: @@ -51,62 +51,93 @@ Options: -V, --version Print version ``` -### Stream +### Pixels ``` -Usage: servicepoint-cli stream +Commands for manipulating pixels + +Usage: servicepoint-cli pixels Commands: - stdin Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin` - screen Stream the default source to the display. On Linux Wayland, this pops up a screen or window chooser, but it also may directly start streaming your main screen. - help Print this message or the help of the given subcommand(s) + off Reset all pixels to the default (off) state [aliases: r, reset, clear] + flip Invert the state of all pixels [aliases: f] + on Set all pixels to the on state + image Send an image file (e.g. jpeg or png) to the display. [aliases: i] + screen Stream the default screen capture source to the display. On Linux Wayland, this pops up a screen or window chooser, but it also may directly start streaming your main screen. [aliases: s] +``` + +#### Image + +``` +Send an image file (e.g. jpeg or png) to the display. + +Usage: servicepoint-cli pixels image [OPTIONS] + +Arguments: + + +Options: + --no-hist Disable histogram correction + --no-blur Disable blur + --no-sharp Disable sharpening + --no-dither Disable dithering. Brightness will be adjusted so that around half of the pixels are on. + --no-spacers Do not remove the spacers from the image. + --no-aspect Do not keep aspect ratio when resizing. ``` #### Screen ``` -Usage: servicepoint-cli stream screen [OPTIONS] +Stream the default screen capture source to the display. On Linux Wayland, this pops up a screen or window chooser, but it also may directly start streaming your main screen. + +Usage: servicepoint-cli pixels screen [OPTIONS] Options: - -n, --no-dither Disable dithering - -p, --pointer Show mouse pointer in video feed - -h, --help Print help -``` - -#### Stdin - -``` -Usage: servicepoint-cli stream stdin [OPTIONS] - -Options: - -s, --slow - -h, --help Print help + -p, --pointer Show mouse pointer in video feed + --no-hist Disable histogram correction + --no-blur Disable blur + --no-sharp Disable sharpening + --no-dither Disable dithering. Brightness will be adjusted so that around half of the pixels are on. + --no-spacers Do not remove the spacers from the image. + --no-aspect Do not keep aspect ratio when resizing. ``` ### Brightness ``` +Commands for manipulating the brightness + Usage: servicepoint-cli brightness Commands: max Reset brightness to the default (max) level [aliases: r, reset] set Set one brightness for the whole screen [aliases: s] min Set brightness to lowest possible level. - help Print this message or the help of the given subcommand(s) ``` -### Pixels +### Text ``` -Usage: servicepoint-cli pixels +Commands for sending text to the screen + +Usage: servicepoint-cli text Commands: - off Reset all pixels to the default (off) state [aliases: r, reset] - invert Invert the state of all pixels [aliases: i] - on Set all pixels to the on state - help Print this message or the help of the given subcommand(s) + stdin Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin` ``` +#### Stdin + +``` +Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin` + +Usage: servicepoint-cli stream stdin [OPTIONS] + +Options: + -s, --slow Wait for a short amount of time before sending the next line +``` + + ## Contributing If you have ideas on how to improve the code, add features or improve documentation feel free to open a pull request. diff --git a/flake.lock b/flake.lock index abacd0c..e99b4ca 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1736429655, - "narHash": "sha256-BwMekRuVlSB9C0QgwKMICiJ5EVbLGjfe4qyueyNQyGI=", + "lastModified": 1739824009, + "narHash": "sha256-fcNrCMUWVLMG3gKC5M9CBqVOAnJtyRvGPxptQFl5mVg=", "owner": "nix-community", "repo": "naersk", - "rev": "0621e47bd95542b8e1ce2ee2d65d6a1f887a13ce", + "rev": "e5130d37369bfa600144c2424270c96f0ef0e11d", "type": "github" }, "original": { @@ -37,11 +37,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1736549401, - "narHash": "sha256-ibkQrMHxF/7TqAYcQE+tOnIsSEzXmMegzyBWza6uHKM=", + "lastModified": 1740603184, + "narHash": "sha256-t+VaahjQAWyA+Ctn2idyo1yxRIYpaDxMgHkgCNiMJa4=", "owner": "nixos", "repo": "nixpkgs", - "rev": "1dab772dd4a68a7bba5d9460685547ff8e17d899", + "rev": "f44bd8ca21e026135061a0a57dcf3d0775b67a49", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 8bf4958..405734a 100644 --- a/flake.nix +++ b/flake.nix @@ -103,6 +103,7 @@ cargo-expand ]; }) + pkgs.cargo-flamegraph ]; LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath (builtins.concatMap (d: d.buildInputs) inputsFrom)}"; RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; diff --git a/src/brightness.rs b/src/brightness.rs new file mode 100644 index 0000000..adead44 --- /dev/null +++ b/src/brightness.rs @@ -0,0 +1,20 @@ +use crate::cli::BrightnessCommand; +use log::info; +use servicepoint::{Brightness, Command, Connection}; + +pub(crate) fn brightness(connection: &Connection, brightness_command: BrightnessCommand) { + match brightness_command { + BrightnessCommand::Max => brightness_set(connection, Brightness::MAX), + BrightnessCommand::Min => brightness_set(connection, Brightness::MIN), + BrightnessCommand::Set { brightness } => { + brightness_set(connection, Brightness::saturating_from(brightness)) + } + } +} + +pub(crate) fn brightness_set(connection: &Connection, brightness: Brightness) { + connection + .send(Command::Brightness(brightness)) + .expect("Failed to set brightness"); + info!("set brightness to {brightness:?}"); +} diff --git a/src/cli.rs b/src/cli.rs index fa420d6..365ff5a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -40,10 +40,10 @@ pub enum Mode { #[clap(subcommand)] brightness_command: BrightnessCommand, }, - #[command(visible_alias = "s")] - Stream { + #[command(visible_alias = "t")] + Text { #[clap(subcommand)] - stream_command: StreamCommand, + text_command: TextCommand, }, } @@ -53,13 +53,36 @@ pub enum PixelCommand { #[command( visible_alias = "r", visible_alias = "reset", + visible_alias = "clear", about = "Reset all pixels to the default (off) state" )] Off, - #[command(visible_alias = "i", about = "Invert the state of all pixels")] - Invert, + #[command(visible_alias = "f", about = "Invert the state of all pixels")] + Flip, #[command(about = "Set all pixels to the on state")] On, + #[command( + visible_alias = "i", + about = "Send an image file (e.g. jpeg or png) to the display." + )] + Image { + #[command(flatten)] + send_image_options: SendImageOptions, + #[command(flatten)] + image_processing_options: ImageProcessingOptions, + }, + #[command( + visible_alias = "s", + about = "Stream the default screen capture source to the display. \ + On Linux Wayland, this pops up a screen or window chooser, \ + but it also may directly start streaming your main screen." + )] + Screen { + #[command(flatten)] + stream_options: StreamScreenOptions, + #[command(flatten)] + image_processing: ImageProcessingOptions, + }, } #[derive(clap::Parser, std::fmt::Debug)] @@ -88,28 +111,24 @@ pub enum Protocol { } #[derive(clap::Parser, std::fmt::Debug)] -#[clap(about = "Continuously send data to the display")] -pub enum StreamCommand { - #[clap( +#[clap(about = "Commands for sending text to the screen")] +pub enum TextCommand { + #[command( about = "Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin`" )] Stdin { - #[arg(long, short, default_value_t = false)] + #[arg( + long, + short, + default_value_t = false, + help = "Wait for a short amount of time before sending the next line" + )] slow: bool, }, - #[clap(about = "Stream the default source to the display. \ - On Linux Wayland, this pops up a screen or window chooser, but it also may directly start streaming your main screen.")] - Screen { - #[command(flatten)] - options: StreamScreenOptions, - }, } #[derive(clap::Parser, std::fmt::Debug, Clone)] pub struct StreamScreenOptions { - #[arg(long, short, default_value_t = false, help = "Disable dithering")] - pub no_dither: bool, - #[arg( long, short, @@ -118,3 +137,33 @@ pub struct StreamScreenOptions { )] pub pointer: bool, } + +#[derive(clap::Parser, std::fmt::Debug, Clone)] +pub struct ImageProcessingOptions { + #[arg(long, help = "Disable histogram correction")] + pub no_hist: bool, + + #[arg(long, help = "Disable blur")] + pub no_blur: bool, + + #[arg(long, help = "Disable sharpening")] + pub no_sharp: bool, + + #[arg( + long, + help = "Disable dithering. Brightness will be adjusted so that around half of the pixels are on." + )] + pub no_dither: bool, + + #[arg(long, help = "Do not remove the spacers from the image.")] + pub no_spacers: bool, + + #[arg(long, help = "Do not keep aspect ratio when resizing.")] + pub no_aspect: bool, +} + +#[derive(clap::Parser, std::fmt::Debug, Clone)] +pub struct SendImageOptions { + #[arg()] + pub file_name: String, +} diff --git a/src/execute.rs b/src/execute.rs deleted file mode 100644 index ad91eba..0000000 --- a/src/execute.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::cli::{BrightnessCommand, Mode, PixelCommand, StreamCommand}; -use crate::stream_stdin::stream_stdin; -use crate::stream_window::stream_window; -use log::info; -use servicepoint::{BitVec, Brightness, Command, CompressionCode, Connection, PIXEL_COUNT}; - -pub fn execute_mode(mode: Mode, connection: Connection) { - match mode { - Mode::ResetEverything => { - brightness_reset(&connection); - pixels_reset(&connection); - } - Mode::Pixels { pixel_command } => pixels(&connection, pixel_command), - Mode::Brightness { brightness_command } => brightness(&connection, brightness_command), - Mode::Stream { stream_command } => match stream_command { - StreamCommand::Stdin { slow } => stream_stdin(connection, slow), - StreamCommand::Screen { options } => stream_window(&connection, options), - }, - } -} - -fn pixels(connection: &Connection, pixel_command: PixelCommand) { - match pixel_command { - PixelCommand::Off => pixels_reset(connection), - PixelCommand::Invert => pixels_invert(connection), - PixelCommand::On => pixels_on(connection) - } -} - -fn pixels_on(connection: &Connection) { - let mask = BitVec::repeat(true, PIXEL_COUNT); - connection - .send(Command::BitmapLinearXor(0, mask, CompressionCode::Lzma)) - .expect("could not send command") -} - -fn pixels_invert(connection: &Connection) { - let mask = BitVec::repeat(true, PIXEL_COUNT); - connection - .send(Command::BitmapLinearXor(0, mask, CompressionCode::Lzma)) - .expect("could not send command") -} - -fn brightness(connection: &Connection, brightness_command: BrightnessCommand) { - match brightness_command { - BrightnessCommand::Max => brightness_reset(connection), - BrightnessCommand::Min => brightness_set(connection, Brightness::MIN), - BrightnessCommand::Set { brightness } => { - brightness_set(connection, Brightness::saturating_from(brightness)) - } - } -} - -fn pixels_reset(connection: &Connection) { - connection - .send(Command::Clear) - .expect("failed to clear pixels"); - info!("Reset pixels"); -} - -fn brightness_reset(connection: &Connection) { - connection - .send(Command::Brightness(Brightness::MAX)) - .expect("Failed to reset brightness to maximum"); - info!("Reset brightness"); -} - -fn brightness_set(connection: &Connection, brightness: Brightness) { - connection - .send(Command::Brightness(brightness)) - .expect("Failed to set brightness"); - info!("set brightness to {brightness:?}"); -} diff --git a/src/image_processing.rs b/src/image_processing.rs new file mode 100644 index 0000000..4e6baa8 --- /dev/null +++ b/src/image_processing.rs @@ -0,0 +1,172 @@ +use crate::{ + cli::ImageProcessingOptions, + ledwand_dither::{blur, histogram_correction, median_brightness, ostromoukhov_dither, sharpen}, +}; +use fast_image_resize::{ResizeOptions, Resizer}; +use image::{DynamicImage, GrayImage}; +use log::{debug, trace}; +use servicepoint::{Bitmap, Grid, PIXEL_HEIGHT, PIXEL_WIDTH, TILE_HEIGHT, TILE_SIZE}; +use std::{default::Default, time::Instant}; + +#[derive(Debug)] +pub struct ImageProcessingPipeline { + options: ImageProcessingOptions, + resizer: Resizer, + render_size: (u32, u32), +} + +const SPACER_HEIGHT: usize = TILE_SIZE / 2; + +impl ImageProcessingPipeline { + pub fn new(options: ImageProcessingOptions) -> Self { + debug!("Creating image pipeline: {:?}", options); + + let height = PIXEL_HEIGHT + + if options.no_spacers { + 0 + } else { + SPACER_HEIGHT * (TILE_HEIGHT - 1) + }; + + Self { + options, + resizer: Resizer::new(), + render_size: (PIXEL_WIDTH as u32, height as u32), + } + } + + pub fn process(&mut self, frame: DynamicImage) -> Bitmap { + let start_time = Instant::now(); + + let frame = self.resize_grayscale(frame); + let frame = self.grayscale_processing(frame); + let mut result = self.grayscale_to_bitmap(frame); + + if !self.options.no_spacers { + result = Self::remove_spacers(result); + } + + trace!("pipeline took {:?}", start_time.elapsed()); + result + } + + fn resize_grayscale(&mut self, frame: DynamicImage) -> GrayImage { + let start_time = Instant::now(); + + let (scaled_width, scaled_height) = if self.options.no_aspect { + self.render_size + } else { + self.calc_scaled_size_keep_aspect((frame.width(), frame.height())) + }; + let mut dst_image = DynamicImage::new(scaled_width, scaled_height, frame.color()); + + self.resizer + .resize(&frame, &mut dst_image, &ResizeOptions::default()) + .expect("image resize failed"); + + trace!("resizing took {:?}", start_time.elapsed()); + + let start_time = Instant::now(); + let result = dst_image.into_luma8(); + trace!("grayscale took {:?}", start_time.elapsed()); + + result + } + + fn grayscale_processing(&self, mut frame: GrayImage) -> GrayImage { + let start_time = Instant::now(); + if !self.options.no_hist { + histogram_correction(&mut frame); + } + + let mut orig = frame.clone(); + + if !self.options.no_blur { + blur(&orig, &mut frame); + std::mem::swap(&mut frame, &mut orig); + } + + if !self.options.no_sharp { + sharpen(&orig, &mut frame); + std::mem::swap(&mut frame, &mut orig); + } + + trace!("image processing took {:?}", start_time.elapsed()); + orig + } + + fn grayscale_to_bitmap(&self, orig: GrayImage) -> Bitmap { + let start_time = Instant::now(); + let result = if self.options.no_dither { + let cutoff = median_brightness(&orig); + let bits = orig.iter().map(move |x| x > &cutoff).collect(); + Bitmap::from_bitvec(orig.width() as usize, bits) + } else { + ostromoukhov_dither(orig, u8::MAX / 2) + }; + trace!("bitmap conversion took {:?}", start_time.elapsed()); + result + } + + fn remove_spacers(source: Bitmap) -> Bitmap { + let start_time = Instant::now(); + + let width = source.width(); + let result_height = Self::calc_height_without_spacers(source.height()); + let mut result = Bitmap::new(width, result_height); + + let mut source_y = 0; + for result_y in 0..result_height { + for x in 0..width { + result.set(x, result_y, source.get(x, source_y)); + } + + if result_y != 0 && result_y % TILE_SIZE == 0 { + source_y += SPACER_HEIGHT; + } + source_y += 1; + } + + trace!("removing spacers took {:?}", start_time.elapsed()); + result + } + + fn calc_height_without_spacers(height: usize) -> usize { + let full_tile_rows_with_spacers = height / (TILE_SIZE + SPACER_HEIGHT); + let remaining_pixel_rows = height % (TILE_SIZE + SPACER_HEIGHT); + let total_spacer_height = full_tile_rows_with_spacers * SPACER_HEIGHT + + remaining_pixel_rows.saturating_sub(TILE_SIZE); + let height_without_spacers = height - total_spacer_height; + trace!( + "spacers take up {total_spacer_height}, resulting in final height {height_without_spacers}" + ); + height_without_spacers + } + + fn calc_scaled_size_keep_aspect(&self, source: (u32, u32)) -> (u32, u32) { + let (source_width, source_height) = source; + let (target_width, target_height) = self.render_size; + debug_assert_eq!(target_width % TILE_SIZE as u32, 0); + + let width_scale = target_width as f32 / source_width as f32; + let height_scale = target_height as f32 / source_height as f32; + let scale = f32::min(width_scale, height_scale); + + let height = (source_height as f32 * scale) as u32; + let mut width = (source_width as f32 * scale) as u32; + + if width % TILE_SIZE as u32 != 0 { + // because we do not have many pixels, round up even if it is a worse fit + width += 8 - width % 8; + } + + let result = (width, height); + trace!( + "scaling {:?} to {:?} to fit {:?}", + source, + result, + self.render_size + ); + result + } +} diff --git a/src/ledwand_dither.rs b/src/ledwand_dither.rs new file mode 100644 index 0000000..d0e4b43 --- /dev/null +++ b/src/ledwand_dither.rs @@ -0,0 +1,507 @@ +//! Based on https://github.com/WarkerAnhaltRanger/CCCB_Ledwand + +use image::GrayImage; +use servicepoint::{BitVec, Bitmap, PIXEL_HEIGHT}; + +type GrayHistogram = [usize; 256]; + +struct HistogramCorrection { + pre_offset: f32, + post_offset: f32, + factor: f32, +} + +pub fn histogram_correction(image: &mut GrayImage) { + let histogram = make_histogram(image); + let correction = determine_histogram_correction(image, histogram); + apply_histogram_correction(image, correction) +} + +fn make_histogram(image: &GrayImage) -> GrayHistogram { + let mut histogram = [0; 256]; + for pixel in image.pixels() { + histogram[pixel.0[0] as usize] += 1; + } + histogram +} + +fn determine_histogram_correction( + image: &GrayImage, + histogram: GrayHistogram, +) -> HistogramCorrection { + let adjustment_pixels = image.len() / PIXEL_HEIGHT; + + let mut num_pixels = 0; + let mut brightness = 0; + + let mincut = loop { + num_pixels += histogram[brightness as usize]; + brightness += 1; + if num_pixels >= adjustment_pixels { + break u8::min(brightness, 20); + } + }; + + let minshift = loop { + num_pixels += histogram[brightness as usize]; + brightness += 1; + if num_pixels >= 2 * adjustment_pixels { + break u8::min(brightness, 64); + } + }; + + brightness = u8::MAX; + num_pixels = 0; + let maxshift = loop { + num_pixels += histogram[brightness as usize]; + brightness -= 1; + if num_pixels >= 2 * adjustment_pixels { + break u8::max(brightness, 192); + } + }; + + let pre_offset = -(mincut as f32 / 2.); + let post_offset = -(minshift as f32); + let factor = (255.0 - post_offset) / maxshift as f32; + HistogramCorrection { + pre_offset, + post_offset, + factor, + } +} + +fn apply_histogram_correction(image: &mut GrayImage, correction: HistogramCorrection) { + for pixel in image.pixels_mut() { + let pixel = &mut pixel.0[0]; + let value = + (*pixel as f32 + correction.pre_offset) * correction.factor + correction.post_offset; + *pixel = value.clamp(0f32, u8::MAX as f32) as u8; + } +} + +pub fn median_brightness(image: &GrayImage) -> u8 { + let histogram = make_histogram(image); + let midpoint = image.len() / 2; + + debug_assert_eq!( + image.len(), + histogram.iter().copied().map(usize::from).sum() + ); + + let mut num_pixels = 0; + for brightness in u8::MIN..=u8::MAX { + num_pixels += histogram[brightness as usize]; + if num_pixels >= midpoint { + return brightness; + } + } + + unreachable!("Somehow less pixels where counted in the histogram than exist in the image") +} + +pub fn blur(source: &GrayImage, destination: &mut GrayImage) { + assert_eq!(source.len(), destination.len()); + + copy_border(source, destination); + blur_inner_pixels(source, destination); +} + +pub fn sharpen(source: &GrayImage, destination: &mut GrayImage) { + assert_eq!(source.len(), destination.len()); + + copy_border(source, destination); + sharpen_inner_pixels(source, destination); +} + +fn copy_border(source: &GrayImage, destination: &mut GrayImage) { + let last_row = source.height() - 1; + for x in 0..source.width() { + destination[(x, 0)] = source[(x, 0)]; + destination[(x, last_row)] = source[(x, last_row)]; + } + let last_col = source.width() - 1; + for y in 0..source.height() { + destination[(0, y)] = source[(0, y)]; + destination[(last_col, y)] = source[(last_col, y)]; + } +} + +fn blur_inner_pixels(source: &GrayImage, destination: &mut GrayImage) { + for y in 1..source.height() - 2 { + for x in 1..source.width() - 2 { + let weighted_sum = source.get_pixel(x - 1, y - 1).0[0] as u32 + + source.get_pixel(x, y - 1).0[0] as u32 + + source.get_pixel(x + 1, y - 1).0[0] as u32 + + source.get_pixel(x - 1, y).0[0] as u32 + + 8 * source.get_pixel(x, y).0[0] as u32 + + source.get_pixel(x + 1, y).0[0] as u32 + + source.get_pixel(x - 1, y + 1).0[0] as u32 + + source.get_pixel(x, y + 1).0[0] as u32 + + source.get_pixel(x + 1, y + 1).0[0] as u32; + let blurred = weighted_sum / 16; + destination.get_pixel_mut(x, y).0[0] = + blurred.clamp(u8::MIN as u32, u8::MAX as u32) as u8; + } + } +} + +fn sharpen_inner_pixels(source: &GrayImage, destination: &mut GrayImage) { + for y in 1..source.height() - 2 { + for x in 1..source.width() - 2 { + let weighted_sum = -(source.get_pixel(x - 1, y - 1).0[0] as i32) + - source.get_pixel(x, y - 1).0[0] as i32 + - source.get_pixel(x + 1, y - 1).0[0] as i32 + - source.get_pixel(x - 1, y).0[0] as i32 + + 9 * source.get_pixel(x, y).0[0] as i32 + - source.get_pixel(x + 1, y).0[0] as i32 + - source.get_pixel(x - 1, y + 1).0[0] as i32 + - source.get_pixel(x, y + 1).0[0] as i32 + - source.get_pixel(x + 1, y + 1).0[0] as i32; + destination.get_pixel_mut(x, y).0[0] = + weighted_sum.clamp(u8::MIN as i32, u8::MAX as i32) as u8; + } + } +} + +pub(crate) fn ostromoukhov_dither(source: GrayImage, bias: u8) -> Bitmap { + let width = source.width(); + let height = source.height(); + assert_eq!(width % 8, 0); + + let mut source = source.into_raw(); + let mut destination = BitVec::repeat(false, source.len()); + + for y in 0..height as usize { + let start = y * width as usize; + if y % 2 == 0 { + for x in start..start + width as usize { + ostromoukhov_dither_pixel( + &mut source, + &mut destination, + x, + width as usize, + y == (height - 1) as usize, + 1, + bias, + ); + } + } else { + for x in (start..start + width as usize).rev() { + ostromoukhov_dither_pixel( + &mut source, + &mut destination, + x, + width as usize, + y == (height - 1) as usize, + -1, + bias, + ); + } + } + } + + Bitmap::from_bitvec(width as usize, destination) +} + +#[inline] +fn ostromoukhov_dither_pixel( + source: &mut [u8], + destination: &mut BitVec, + position: usize, + width: usize, + last_row: bool, + direction: isize, + bias: u8, +) { + let (destination_value, error) = gray_to_bit(source[position], bias); + destination.set(position, destination_value); + + let mut diffuse = |to: usize, mat: i16| { + let diffuse_value = source[to] as i16 + mat; + source[to] = diffuse_value.clamp(u8::MIN.into(), u8::MAX.into()) as u8; + }; + + let lookup = if destination_value { + ERROR_DIFFUSION_MATRIX[error as usize].map(move |i| -i) + } else { + ERROR_DIFFUSION_MATRIX[error as usize] + }; + diffuse((position as isize + direction) as usize, lookup[0]); + + if !last_row { + diffuse( + ((position + width) as isize - direction) as usize, + lookup[1], + ); + diffuse(((position + width) as isize) as usize, lookup[2]); + } +} + +fn gray_to_bit(old_pixel: u8, bias: u8) -> (bool, u8) { + let destination_value = old_pixel > bias; + let error = if destination_value { + 255 - old_pixel + } else { + old_pixel + }; + (destination_value, error) +} + +const ERROR_DIFFUSION_MATRIX: [[i16; 3]; 256] = [ + [0, 1, 0], + [1, 0, 0], + [1, 0, 1], + [2, 0, 1], + [2, 0, 2], + [3, 0, 2], + [4, 0, 2], + [4, 1, 2], + [5, 1, 2], + [5, 2, 2], + [5, 3, 2], + [6, 3, 2], + [6, 3, 3], + [7, 3, 3], + [7, 4, 3], + [8, 4, 3], + [8, 5, 3], + [9, 5, 3], + [9, 5, 4], + [10, 6, 3], + [10, 6, 4], + [11, 7, 3], + [11, 7, 4], + [11, 8, 4], + [12, 7, 5], + [12, 7, 6], + [12, 7, 7], + [12, 7, 8], + [12, 7, 9], + [13, 7, 9], + [13, 7, 10], + [13, 7, 11], + [13, 7, 12], + [14, 7, 12], + [14, 8, 12], + [15, 8, 12], + [15, 9, 12], + [16, 9, 12], + [16, 10, 12], + [17, 10, 12], + [17, 11, 12], + [18, 12, 11], + [19, 12, 11], + [19, 13, 11], + [20, 13, 11], + [20, 14, 11], + [21, 15, 10], + [22, 15, 10], + [22, 17, 9], + [23, 17, 9], + [24, 18, 8], + [24, 19, 8], + [25, 19, 8], + [26, 20, 7], + [26, 21, 7], + [27, 22, 6], + [28, 23, 5], + [28, 24, 5], + [29, 25, 4], + [30, 26, 3], + [31, 26, 3], + [31, 28, 2], + [32, 28, 2], + [33, 29, 1], + [34, 30, 0], + [33, 31, 1], + [32, 33, 1], + [32, 33, 2], + [31, 34, 3], + [30, 36, 3], + [29, 37, 4], + [29, 37, 5], + [28, 39, 5], + [32, 34, 7], + [37, 29, 8], + [42, 23, 10], + [46, 19, 11], + [51, 13, 12], + [52, 14, 13], + [53, 13, 12], + [53, 14, 13], + [54, 14, 13], + [55, 14, 13], + [55, 14, 13], + [56, 15, 14], + [57, 14, 13], + [56, 15, 15], + [55, 17, 15], + [54, 18, 16], + [53, 20, 16], + [52, 21, 17], + [52, 22, 17], + [51, 24, 17], + [50, 25, 18], + [49, 27, 18], + [47, 29, 19], + [48, 29, 19], + [48, 29, 20], + [49, 29, 20], + [49, 30, 20], + [50, 31, 20], + [50, 31, 20], + [51, 31, 20], + [51, 31, 21], + [52, 31, 21], + [52, 32, 21], + [53, 32, 21], + [53, 32, 22], + [55, 32, 21], + [56, 31, 22], + [58, 31, 21], + [59, 30, 22], + [61, 30, 21], + [62, 29, 22], + [64, 29, 21], + [65, 28, 22], + [67, 28, 21], + [68, 27, 22], + [70, 27, 21], + [71, 26, 22], + [73, 26, 21], + [75, 25, 21], + [76, 25, 21], + [78, 24, 21], + [80, 23, 21], + [81, 23, 21], + [83, 22, 21], + [85, 21, 20], + [85, 22, 21], + [85, 22, 22], + [84, 24, 22], + [84, 24, 23], + [84, 25, 23], + [83, 27, 23], + [83, 28, 23], + [82, 29, 24], + [82, 30, 24], + [81, 31, 25], + [80, 32, 26], + [80, 33, 26], + [79, 35, 26], + [79, 36, 26], + [78, 37, 27], + [77, 38, 28], + [77, 39, 28], + [76, 41, 28], + [75, 42, 29], + [75, 43, 29], + [74, 44, 30], + [74, 45, 30], + [75, 46, 30], + [75, 46, 30], + [76, 46, 30], + [76, 46, 31], + [77, 46, 31], + [77, 47, 31], + [78, 47, 31], + [78, 47, 32], + [79, 47, 32], + [79, 48, 32], + [80, 49, 32], + [83, 46, 32], + [86, 44, 32], + [90, 42, 31], + [93, 40, 31], + [96, 39, 30], + [100, 36, 30], + [103, 35, 29], + [106, 33, 29], + [110, 30, 29], + [113, 29, 28], + [114, 29, 28], + [115, 29, 28], + [115, 29, 28], + [116, 30, 29], + [117, 29, 28], + [117, 30, 29], + [118, 30, 29], + [119, 30, 29], + [109, 43, 27], + [100, 57, 23], + [90, 71, 20], + [80, 85, 17], + [70, 99, 14], + [74, 98, 12], + [78, 97, 10], + [81, 96, 9], + [85, 95, 7], + [89, 94, 5], + [92, 93, 4], + [96, 92, 2], + [100, 91, 0], + [100, 90, 2], + [100, 88, 5], + [100, 87, 7], + [99, 86, 10], + [99, 85, 12], + [99, 84, 14], + [99, 82, 17], + [98, 81, 20], + [98, 80, 22], + [98, 79, 24], + [98, 77, 27], + [98, 76, 29], + [97, 75, 32], + [97, 73, 35], + [97, 72, 37], + [96, 71, 40], + [96, 69, 43], + [96, 67, 46], + [96, 66, 48], + [95, 65, 51], + [95, 63, 54], + [95, 61, 57], + [94, 60, 60], + [94, 58, 63], + [94, 57, 65], + [93, 55, 69], + [93, 54, 71], + [93, 52, 74], + [92, 51, 77], + [92, 49, 80], + [91, 47, 84], + [91, 46, 86], + [93, 49, 82], + [96, 52, 77], + [98, 55, 73], + [101, 58, 68], + [104, 61, 63], + [106, 65, 58], + [109, 68, 53], + [111, 71, 49], + [114, 74, 44], + [116, 78, 39], + [118, 76, 40], + [119, 74, 42], + [120, 73, 43], + [122, 71, 44], + [123, 69, 46], + [124, 67, 48], + [125, 66, 49], + [127, 64, 50], + [128, 62, 52], + [129, 60, 54], + [131, 58, 55], + [132, 57, 56], + [136, 47, 63], + [139, 38, 70], + [143, 29, 76], + [147, 19, 83], + [151, 9, 90], + [154, 0, 97], + [160, 0, 92], + [171, 0, 82], + [183, 0, 71], + [184, 0, 71], +]; diff --git a/src/main.rs b/src/main.rs index a9878ca..4ca1df4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,21 @@ -use crate::cli::{Cli, Protocol}; +use crate::{ + brightness::{brightness, brightness_set}, + cli::{Cli, Mode, Protocol}, + pixels::{pixels, pixels_off}, + text::text +}; use clap::Parser; use log::debug; -use servicepoint::Connection; +use servicepoint::{Brightness, Connection}; +mod brightness; mod cli; -mod execute; +mod image_processing; +mod ledwand_dither; +mod pixels; mod stream_stdin; mod stream_window; +mod text; fn main() { let cli = Cli::parse(); @@ -16,7 +25,19 @@ fn main() { let connection = make_connection(cli.destination, cli.transport); debug!("connection established: {:#?}", connection); - execute::execute_mode(cli.command, connection); + execute_mode(cli.command, connection); +} + +pub fn execute_mode(mode: Mode, connection: Connection) { + match mode { + Mode::ResetEverything => { + brightness_set(&connection, Brightness::MAX); + pixels_off(&connection); + } + Mode::Pixels { pixel_command } => pixels(&connection, pixel_command), + Mode::Brightness { brightness_command } => brightness(&connection, brightness_command), + Mode::Text { text_command} => text(&connection, text_command), + } } fn make_connection(destination: String, transport: Protocol) -> Connection { diff --git a/src/pixels.rs b/src/pixels.rs new file mode 100644 index 0000000..da1aa6d --- /dev/null +++ b/src/pixels.rs @@ -0,0 +1,64 @@ +use crate::{ + image_processing::ImageProcessingPipeline, + cli::{ImageProcessingOptions, PixelCommand, SendImageOptions}, + stream_window::stream_window +}; +use log::info; +use servicepoint::{BitVec, Command, CompressionCode, Connection, Origin, PIXEL_COUNT}; + +pub(crate) fn pixels(connection: &Connection, pixel_command: PixelCommand) { + match pixel_command { + PixelCommand::Off => pixels_off(connection), + PixelCommand::Flip => pixels_invert(connection), + PixelCommand::On => pixels_on(connection), + PixelCommand::Image { + image_processing_options: processing_options, + send_image_options: image_options, + } => pixels_image(connection, image_options, processing_options), + PixelCommand::Screen { + stream_options, + image_processing, + } => stream_window(connection, stream_options, image_processing), + } +} + +fn pixels_on(connection: &Connection) { + let mask = BitVec::repeat(true, PIXEL_COUNT); + connection + .send(Command::BitmapLinear(0, mask, CompressionCode::Lzma)) + .expect("could not send command"); + info!("turned on all pixels") +} + +fn pixels_invert(connection: &Connection) { + let mask = BitVec::repeat(true, PIXEL_COUNT); + connection + .send(Command::BitmapLinearXor(0, mask, CompressionCode::Lzma)) + .expect("could not send command"); + info!("inverted all pixels"); +} + +pub(crate) fn pixels_off(connection: &Connection) { + connection + .send(Command::Clear) + .expect("failed to clear pixels"); + info!("reset pixels"); +} + +fn pixels_image( + connection: &Connection, + options: SendImageOptions, + processing_options: ImageProcessingOptions, +) { + let image = image::open(&options.file_name).expect("failed to open image file"); + let mut pipeline = ImageProcessingPipeline::new(processing_options); + let bitmap = pipeline.process(image); + connection + .send(Command::BitmapLinearWin( + Origin::ZERO, + bitmap, + CompressionCode::default(), + )) + .expect("failed to send image command"); + info!("sent image to display"); +} diff --git a/src/stream_stdin.rs b/src/stream_stdin.rs index a349aa2..b8b6cfb 100644 --- a/src/stream_stdin.rs +++ b/src/stream_stdin.rs @@ -2,7 +2,7 @@ use log::warn; use servicepoint::*; use std::thread::sleep; -pub(crate) fn stream_stdin(connection: Connection, slow: bool) { +pub(crate) fn stream_stdin(connection: &Connection, slow: bool) { warn!("This mode will break when using multi-byte characters and does not support ANSI escape sequences yet."); let mut app = App { connection, @@ -13,14 +13,14 @@ pub(crate) fn stream_stdin(connection: Connection, slow: bool) { app.run() } -struct App { - connection: Connection, +struct App<'t> { + connection: &'t Connection, mirror: CharGrid, y: usize, slow: bool, } -impl App { +impl App<'_> { fn run(&mut self) { self.connection .send(Command::Clear) @@ -63,15 +63,16 @@ impl App { fn send_mirror(&self) { self.connection - .send(Command::Cp437Data( + .send(Command::Utf8Data( Origin::ZERO, - Cp437Grid::from(&self.mirror), + self.mirror.clone(), )) .expect("couldn't send screen to display"); } fn single_line(&mut self, line: &str) { let mut line_grid = CharGrid::new(TILE_WIDTH, 1); + line_grid.fill(' '); Self::line_onto_grid(&mut line_grid, 0, line); Self::line_onto_grid(&mut self.mirror, self.y, line); self.connection diff --git a/src/stream_window.rs b/src/stream_window.rs index e3a626a..f8e41ec 100644 --- a/src/stream_window.rs +++ b/src/stream_window.rs @@ -1,61 +1,50 @@ -use crate::cli::StreamScreenOptions; -use image::{ - imageops::{dither, resize, BiLevel, FilterType}, - DynamicImage, ImageBuffer, Luma, Rgb, Rgba, +use crate::{ + cli::{ImageProcessingOptions, StreamScreenOptions}, + image_processing::ImageProcessingPipeline, }; -use log::{error, info, warn}; +use image::{DynamicImage, ImageBuffer, Rgb, Rgba}; +use log::{debug, error, info, trace, warn}; use scap::{ capturer::{Capturer, Options}, frame::convert_bgra_to_rgb, frame::Frame, }; -use servicepoint::{ - Bitmap, Command, CompressionCode, Connection, Origin, FRAME_PACING, PIXEL_HEIGHT, PIXEL_WIDTH, -}; -use std::time::Duration; +use servicepoint::{Command, CompressionCode, Connection, Origin, FRAME_PACING}; +use std::time::{Duration, Instant}; -pub fn stream_window(connection: &Connection, options: StreamScreenOptions) { +pub fn stream_window( + connection: &Connection, + options: StreamScreenOptions, + processing_options: ImageProcessingOptions, +) { info!("Starting capture with options: {:?}", options); - warn!("this implementation does not drop any frames - set a lower fps or disable dithering if your computer cannot keep up."); - let capturer = match start_capture(&options) { Some(value) => value, None => return, }; - let mut bitmap = Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT); + let mut pipeline = ImageProcessingPipeline::new(processing_options); + info!("now starting to stream images"); loop { - let frame = get_next_frame(&capturer, options.no_dither); - for (mut dest, src) in bitmap.iter_mut().zip(frame.pixels()) { - *dest = src.0[0] > u8::MAX / 2; - } + let start = Instant::now(); + + let frame = capture_frame(&capturer); + let frame = frame_to_image(frame); + let bitmap = pipeline.process(frame); + + trace!("bitmap ready to send in: {:?}", start.elapsed()); connection .send(Command::BitmapLinearWin( Origin::ZERO, bitmap.clone(), - CompressionCode::Uncompressed, + CompressionCode::default(), )) .expect("failed to send frame to display"); - } -} -fn get_next_frame(capturer: &Capturer, no_dither: bool) -> ImageBuffer, Vec> { - let frame = capturer.get_next_frame().expect("failed to capture frame"); - let frame = frame_to_image(frame); - let frame = frame.grayscale().to_luma8(); - let mut frame = resize( - &frame, - PIXEL_WIDTH as u32, - PIXEL_HEIGHT as u32, - FilterType::Nearest, - ); - - if !no_dither { - dither(&mut frame, &BiLevel); + debug!("frame time: {:?}", start.elapsed()); } - frame } fn start_capture(options: &StreamScreenOptions) -> Option { @@ -72,10 +61,11 @@ fn start_capture(options: &StreamScreenOptions) -> Option { } } + // all options are more like a suggestion let mut capturer = Capturer::build(Options { fps: FRAME_PACING.div_duration_f32(Duration::from_secs(1)) as u32, show_cursor: options.pointer, - output_type: scap::frame::FrameType::BGR0, // this is more like a suggestion + output_type: scap::frame::FrameType::BGR0, ..Default::default() }) .expect("failed to create screen capture"); @@ -83,8 +73,16 @@ fn start_capture(options: &StreamScreenOptions) -> Option { Some(capturer) } +fn capture_frame(capturer: &Capturer) -> Frame { + let start_time = Instant::now(); + let result = capturer.get_next_frame().expect("failed to capture frame"); + trace!("capture took: {:?}", start_time.elapsed()); + result +} + fn frame_to_image(frame: Frame) -> DynamicImage { - match frame { + let start_time = Instant::now(); + let result = match frame { Frame::BGRx(frame) => bgrx_to_rgb(frame.width, frame.height, frame.data), Frame::RGBx(frame) => DynamicImage::from( ImageBuffer::, _>::from_raw( @@ -101,7 +99,9 @@ fn frame_to_image(frame: Frame) -> DynamicImage { ), Frame::BGRA(frame) => bgrx_to_rgb(frame.width, frame.height, frame.data), Frame::YUVFrame(_) | Frame::XBGR(_) => panic!("unsupported frame format"), - } + }; + trace!("conversion to image took: {:?}", start_time.elapsed()); + result } fn bgrx_to_rgb(width: i32, height: i32, data: Vec) -> DynamicImage { diff --git a/src/text.rs b/src/text.rs new file mode 100644 index 0000000..247b9ad --- /dev/null +++ b/src/text.rs @@ -0,0 +1,7 @@ +use servicepoint::Connection; +use crate::cli::TextCommand; +use crate::stream_stdin::stream_stdin; + +pub fn text(connection: &Connection, command: TextCommand) { + match command { TextCommand::Stdin { slow } => stream_stdin(connection, slow), } +}