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
All checks were successful
Rust / build (push) Successful in 7m36s
Reviewed-on: #2
This commit is contained in:
commit
a903cbed85
206
Cargo.lock
generated
206
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"] }
|
||||
|
|
91
README.md
91
README.md
|
@ -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.
|
||||
|
|
12
flake.lock
12
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": {
|
||||
|
|
|
@ -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
20
src/brightness.rs
Normal 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:?}");
|
||||
}
|
85
src/cli.rs
85
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,
|
||||
}
|
||||
|
|
|
@ -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
172
src/image_processing.rs
Normal 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
507
src/ledwand_dither.rs
Normal 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],
|
||||
];
|
29
src/main.rs
29
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 {
|
||||
|
|
64
src/pixels.rs
Normal file
64
src/pixels.rs
Normal 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");
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
7
src/text.rs
Normal 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), }
|
||||
}
|
Loading…
Reference in a new issue