Merge pull request 'better dithering, keep aspect ratio, send image' (#2) from next into main
All checks were successful
Rust / build (push) Successful in 7m36s

Reviewed-on: #2
This commit is contained in:
vinzenz 2025-03-02 15:29:55 +01:00
commit a903cbed85
15 changed files with 1134 additions and 223 deletions

206
Cargo.lock generated
View file

@ -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"

View file

@ -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"] }

View file

@ -37,10 +37,10 @@ cargo run -- <args>
Usage: servicepoint-cli [OPTIONS] <COMMAND>
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 <COMMAND>
Commands for manipulating pixels
Usage: servicepoint-cli pixels <COMMAND>
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] <FILE_NAME>
Arguments:
<FILE_NAME>
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 <COMMAND>
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 <COMMAND>
Commands for sending text to the screen
Usage: servicepoint-cli text <COMMAND>
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.

View file

@ -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": {

View file

@ -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}";

20
src/brightness.rs Normal file
View file

@ -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:?}");
}

View file

@ -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,
}

View file

@ -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:?}");
}

172
src/image_processing.rs Normal file
View file

@ -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
}
}

507
src/ledwand_dither.rs Normal file
View file

@ -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],
];

View file

@ -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 {

64
src/pixels.rs Normal file
View file

@ -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");
}

View file

@ -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

View file

@ -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<Luma<u8>, Vec<u8>> {
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<Capturer> {
@ -72,10 +61,11 @@ fn start_capture(options: &StreamScreenOptions) -> Option<Capturer> {
}
}
// 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<Capturer> {
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::<Rgba<_>, _>::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<u8>) -> DynamicImage {

7
src/text.rs Normal file
View file

@ -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), }
}