better dithering, keep aspect ratio, send image #2
					 15 changed files with 1134 additions and 223 deletions
				
			
		
							
								
								
									
										206
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										206
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							|  | @ -85,9 +85,9 @@ dependencies = [ | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "anyhow" | name = "anyhow" | ||||||
| version = "1.0.95" | version = "1.0.96" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" | checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4" | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "arbitrary" | name = "arbitrary" | ||||||
|  | @ -134,9 +134,9 @@ dependencies = [ | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "avif-serialize" | name = "avif-serialize" | ||||||
| version = "0.8.2" | version = "0.8.3" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62" | checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "arrayvec", |  "arrayvec", | ||||||
| ] | ] | ||||||
|  | @ -251,9 +251,9 @@ checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "cc" | name = "cc" | ||||||
| version = "1.2.14" | version = "1.2.16" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" | checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "jobserver", |  "jobserver", | ||||||
|  "libc", |  "libc", | ||||||
|  | @ -304,9 +304,9 @@ dependencies = [ | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "clap" | name = "clap" | ||||||
| version = "4.5.30" | version = "4.5.31" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d" | checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "clap_builder", |  "clap_builder", | ||||||
|  "clap_derive", |  "clap_derive", | ||||||
|  | @ -314,9 +314,9 @@ dependencies = [ | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "clap_builder" | name = "clap_builder" | ||||||
| version = "4.5.30" | version = "4.5.31" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c" | checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "anstream", |  "anstream", | ||||||
|  "anstyle", |  "anstyle", | ||||||
|  | @ -558,10 +558,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" | checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "either" | name = "document-features" | ||||||
| version = "1.13.0" | version = "0.2.11" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | 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]] | [[package]] | ||||||
| name = "env_filter" | name = "env_filter" | ||||||
|  | @ -607,6 +616,20 @@ dependencies = [ | ||||||
|  "zune-inflate", |  "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]] | [[package]] | ||||||
| name = "fdeflate" | name = "fdeflate" | ||||||
| version = "0.3.7" | version = "0.3.7" | ||||||
|  | @ -618,9 +641,9 @@ dependencies = [ | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "flate2" | name = "flate2" | ||||||
| version = "1.0.35" | version = "1.1.0" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" | checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "crc32fast", |  "crc32fast", | ||||||
|  "miniz_oxide", |  "miniz_oxide", | ||||||
|  | @ -772,7 +795,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "cfg-if", |  "cfg-if", | ||||||
|  "libc", |  "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]] | [[package]] | ||||||
|  | @ -952,9 +987,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "libc" | name = "libc" | ||||||
| version = "0.2.169" | version = "0.2.170" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" | checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "libdbus-sys" | name = "libdbus-sys" | ||||||
|  | @ -1013,6 +1048,12 @@ dependencies = [ | ||||||
|  "system-deps", |  "system-deps", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "litrs" | ||||||
|  | version = "0.4.1" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "lock_api" | name = "lock_api" | ||||||
| version = "0.4.12" | version = "0.4.12" | ||||||
|  | @ -1025,9 +1066,9 @@ dependencies = [ | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "log" | name = "log" | ||||||
| version = "0.4.25" | version = "0.4.26" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" | checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "loop9" | name = "loop9" | ||||||
|  | @ -1071,9 +1112,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "miniz_oxide" | name = "miniz_oxide" | ||||||
| version = "0.8.4" | version = "0.8.5" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" | checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "adler2", |  "adler2", | ||||||
|  "simd-adler32", |  "simd-adler32", | ||||||
|  | @ -1322,7 +1363,7 @@ version = "0.2.20" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "zerocopy", |  "zerocopy 0.7.35", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
|  | @ -1390,8 +1431,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "libc", |  "libc", | ||||||
|  "rand_chacha", |  "rand_chacha 0.3.1", | ||||||
|  "rand_core", |  "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]] | [[package]] | ||||||
|  | @ -1401,7 +1453,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "ppv-lite86", |  "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]] | [[package]] | ||||||
|  | @ -1410,7 +1472,17 @@ version = "0.6.4" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" | ||||||
| dependencies = [ | 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]] | [[package]] | ||||||
|  | @ -1439,8 +1511,8 @@ dependencies = [ | ||||||
|  "once_cell", |  "once_cell", | ||||||
|  "paste", |  "paste", | ||||||
|  "profiling", |  "profiling", | ||||||
|  "rand", |  "rand 0.8.5", | ||||||
|  "rand_chacha", |  "rand_chacha 0.3.1", | ||||||
|  "simd_helpers", |  "simd_helpers", | ||||||
|  "system-deps", |  "system-deps", | ||||||
|  "thiserror 1.0.69", |  "thiserror 1.0.69", | ||||||
|  | @ -1485,9 +1557,9 @@ dependencies = [ | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "redox_syscall" | name = "redox_syscall" | ||||||
| version = "0.5.8" | version = "0.5.9" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" | checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "bitflags 2.8.0", |  "bitflags 2.8.0", | ||||||
| ] | ] | ||||||
|  | @ -1560,7 +1632,7 @@ dependencies = [ | ||||||
|  "dbus", |  "dbus", | ||||||
|  "objc", |  "objc", | ||||||
|  "pipewire", |  "pipewire", | ||||||
|  "rand", |  "rand 0.8.5", | ||||||
|  "screencapturekit", |  "screencapturekit", | ||||||
|  "screencapturekit-sys", |  "screencapturekit-sys", | ||||||
|  "sysinfo", |  "sysinfo", | ||||||
|  | @ -1600,18 +1672,18 @@ dependencies = [ | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "serde" | name = "serde" | ||||||
| version = "1.0.217" | version = "1.0.218" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" | checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "serde_derive", |  "serde_derive", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "serde_derive" | name = "serde_derive" | ||||||
| version = "1.0.217" | version = "1.0.218" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" | checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "proc-macro2", |  "proc-macro2", | ||||||
|  "quote", |  "quote", | ||||||
|  | @ -1643,10 +1715,11 @@ dependencies = [ | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "servicepoint-cli" | name = "servicepoint-cli" | ||||||
| version = "0.2.1" | version = "0.3.0" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "clap", |  "clap", | ||||||
|  "env_logger", |  "env_logger", | ||||||
|  |  "fast_image_resize", | ||||||
|  "image", |  "image", | ||||||
|  "log", |  "log", | ||||||
|  "scap", |  "scap", | ||||||
|  | @ -1856,17 +1929,16 @@ dependencies = [ | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "tungstenite" | name = "tungstenite" | ||||||
| version = "0.26.1" | version = "0.26.2" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "413083a99c579593656008130e29255e54dcaae495be556cc26888f211648c24" | checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "byteorder", |  | ||||||
|  "bytes", |  "bytes", | ||||||
|  "data-encoding", |  "data-encoding", | ||||||
|  "http", |  "http", | ||||||
|  "httparse", |  "httparse", | ||||||
|  "log", |  "log", | ||||||
|  "rand", |  "rand 0.9.0", | ||||||
|  "sha1", |  "sha1", | ||||||
|  "thiserror 2.0.11", |  "thiserror 2.0.11", | ||||||
|  "utf-8", |  "utf-8", | ||||||
|  | @ -1874,15 +1946,15 @@ dependencies = [ | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "typenum" | name = "typenum" | ||||||
| version = "1.17.0" | version = "1.18.0" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" | checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "unicode-ident" | name = "unicode-ident" | ||||||
| version = "1.0.16" | version = "1.0.17" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" | checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "unicode-segmentation" | name = "unicode-segmentation" | ||||||
|  | @ -1943,6 +2015,15 @@ version = "0.11.0+wasi-snapshot-preview1" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" | 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]] | [[package]] | ||||||
| name = "wasm-bindgen" | name = "wasm-bindgen" | ||||||
| version = "0.2.100" | version = "0.2.100" | ||||||
|  | @ -2201,13 +2282,22 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "winnow" | name = "winnow" | ||||||
| version = "0.7.2" | version = "0.7.3" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603" | checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "memchr", |  "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]] | [[package]] | ||||||
| name = "wyz" | name = "wyz" | ||||||
| version = "0.5.1" | version = "0.5.1" | ||||||
|  | @ -2233,7 +2323,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "byteorder", |  "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]] | [[package]] | ||||||
|  | @ -2247,6 +2346,17 @@ dependencies = [ | ||||||
|  "syn", |  "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]] | [[package]] | ||||||
| name = "zune-core" | name = "zune-core" | ||||||
| version = "0.4.12" | version = "0.4.12" | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| [package] | [package] | ||||||
| name = "servicepoint-cli" | name = "servicepoint-cli" | ||||||
| description = "A command line interface for the ServicePoint display." | description = "A command line interface for the ServicePoint display." | ||||||
| version = "0.2.1" | version = "0.3.0" | ||||||
| edition = "2021" | edition = "2021" | ||||||
| rust-version = "1.80.0" | rust-version = "1.80.0" | ||||||
| publish = true | publish = true | ||||||
|  | @ -19,3 +19,4 @@ env_logger = "0.11" | ||||||
| log = "0.4" | log = "0.4" | ||||||
| scap = "0.0.8" | scap = "0.0.8" | ||||||
| image = "0.25.5" | 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> | Usage: servicepoint-cli [OPTIONS] <COMMAND> | ||||||
| 
 | 
 | ||||||
| Commands: | Commands: | ||||||
|   reset-everything  [aliases: r] |   reset-everything  Reset both pixels and brightness [aliases: r] | ||||||
|   pixels            [aliases: p] |   pixels            Commands for manipulating pixels [aliases: p] | ||||||
|   brightness        [aliases: b] |   brightness        Commands for manipulating the brightness [aliases: b] | ||||||
|   stream            [aliases: s] |   text              Commands for sending text to the screen [aliases: t] | ||||||
|   help              Print this message or the help of the given subcommand(s) |   help              Print this message or the help of the given subcommand(s) | ||||||
| 
 | 
 | ||||||
| Options: | Options: | ||||||
|  | @ -51,62 +51,93 @@ Options: | ||||||
|   -V, --version                    Print version |   -V, --version                    Print version | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### Stream | ### Pixels | ||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
| Usage: servicepoint-cli stream <COMMAND> | Commands for manipulating pixels | ||||||
|  | 
 | ||||||
|  | Usage: servicepoint-cli pixels <COMMAND> | ||||||
| 
 | 
 | ||||||
| Commands: | Commands: | ||||||
|   stdin   Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin` |   off     Reset all pixels to the default (off) state [aliases: r, reset, clear] | ||||||
|   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. |   flip    Invert the state of all pixels [aliases: f] | ||||||
|   help    Print this message or the help of the given subcommand(s) |   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 | #### 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: | Options: | ||||||
|   -n, --no-dither  Disable dithering |   -p, --pointer     Show mouse pointer in video feed | ||||||
|   -p, --pointer    Show mouse pointer in video feed |       --no-hist     Disable histogram correction | ||||||
|   -h, --help       Print help |       --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. | ||||||
| #### Stdin |       --no-spacers  Do not remove the spacers from the image. | ||||||
| 
 |       --no-aspect   Do not keep aspect ratio when resizing. | ||||||
| ``` |  | ||||||
| Usage: servicepoint-cli stream stdin [OPTIONS] |  | ||||||
| 
 |  | ||||||
| Options: |  | ||||||
|   -s, --slow   |  | ||||||
|   -h, --help  Print help |  | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### Brightness | ### Brightness | ||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
|  | Commands for manipulating the brightness | ||||||
|  | 
 | ||||||
| Usage: servicepoint-cli brightness <COMMAND> | Usage: servicepoint-cli brightness <COMMAND> | ||||||
| 
 | 
 | ||||||
| Commands: | Commands: | ||||||
|   max   Reset brightness to the default (max) level [aliases: r, reset] |   max   Reset brightness to the default (max) level [aliases: r, reset] | ||||||
|   set   Set one brightness for the whole screen [aliases: s] |   set   Set one brightness for the whole screen [aliases: s] | ||||||
|   min   Set brightness to lowest possible level. |   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: | Commands: | ||||||
|   off     Reset all pixels to the default (off) state [aliases: r, reset] |   stdin  Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin` | ||||||
|   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` | ||||||
|  | 
 | ||||||
|  | Usage: servicepoint-cli stream stdin [OPTIONS] | ||||||
|  | 
 | ||||||
|  | Options: | ||||||
|  |   -s, --slow  Wait for a short amount of time before sending the next line | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| ## Contributing | ## Contributing | ||||||
| 
 | 
 | ||||||
| If you have ideas on how to improve the code, add features or improve documentation feel free to open a pull request. | 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
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								flake.lock
									
										
									
										generated
									
									
									
								
							|  | @ -7,11 +7,11 @@ | ||||||
|         ] |         ] | ||||||
|       }, |       }, | ||||||
|       "locked": { |       "locked": { | ||||||
|         "lastModified": 1736429655, |         "lastModified": 1739824009, | ||||||
|         "narHash": "sha256-BwMekRuVlSB9C0QgwKMICiJ5EVbLGjfe4qyueyNQyGI=", |         "narHash": "sha256-fcNrCMUWVLMG3gKC5M9CBqVOAnJtyRvGPxptQFl5mVg=", | ||||||
|         "owner": "nix-community", |         "owner": "nix-community", | ||||||
|         "repo": "naersk", |         "repo": "naersk", | ||||||
|         "rev": "0621e47bd95542b8e1ce2ee2d65d6a1f887a13ce", |         "rev": "e5130d37369bfa600144c2424270c96f0ef0e11d", | ||||||
|         "type": "github" |         "type": "github" | ||||||
|       }, |       }, | ||||||
|       "original": { |       "original": { | ||||||
|  | @ -37,11 +37,11 @@ | ||||||
|     }, |     }, | ||||||
|     "nixpkgs": { |     "nixpkgs": { | ||||||
|       "locked": { |       "locked": { | ||||||
|         "lastModified": 1736549401, |         "lastModified": 1740603184, | ||||||
|         "narHash": "sha256-ibkQrMHxF/7TqAYcQE+tOnIsSEzXmMegzyBWza6uHKM=", |         "narHash": "sha256-t+VaahjQAWyA+Ctn2idyo1yxRIYpaDxMgHkgCNiMJa4=", | ||||||
|         "owner": "nixos", |         "owner": "nixos", | ||||||
|         "repo": "nixpkgs", |         "repo": "nixpkgs", | ||||||
|         "rev": "1dab772dd4a68a7bba5d9460685547ff8e17d899", |         "rev": "f44bd8ca21e026135061a0a57dcf3d0775b67a49", | ||||||
|         "type": "github" |         "type": "github" | ||||||
|       }, |       }, | ||||||
|       "original": { |       "original": { | ||||||
|  |  | ||||||
|  | @ -103,6 +103,7 @@ | ||||||
|                   cargo-expand |                   cargo-expand | ||||||
|                 ]; |                 ]; | ||||||
|               }) |               }) | ||||||
|  |               pkgs.cargo-flamegraph | ||||||
|             ]; |             ]; | ||||||
|             LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath (builtins.concatMap (d: d.buildInputs) inputsFrom)}"; |             LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath (builtins.concatMap (d: d.buildInputs) inputsFrom)}"; | ||||||
|             RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; |             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)] |         #[clap(subcommand)] | ||||||
|         brightness_command: BrightnessCommand, |         brightness_command: BrightnessCommand, | ||||||
|     }, |     }, | ||||||
|     #[command(visible_alias = "s")] |     #[command(visible_alias = "t")] | ||||||
|     Stream { |     Text { | ||||||
|         #[clap(subcommand)] |         #[clap(subcommand)] | ||||||
|         stream_command: StreamCommand, |         text_command: TextCommand, | ||||||
|     }, |     }, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -53,13 +53,36 @@ pub enum PixelCommand { | ||||||
|     #[command(
 |     #[command(
 | ||||||
|         visible_alias = "r", |         visible_alias = "r", | ||||||
|         visible_alias = "reset", |         visible_alias = "reset", | ||||||
|  |         visible_alias = "clear", | ||||||
|         about = "Reset all pixels to the default (off) state" |         about = "Reset all pixels to the default (off) state" | ||||||
|     )] |     )] | ||||||
|     Off, |     Off, | ||||||
|     #[command(visible_alias = "i", about = "Invert the state of all pixels")] |     #[command(visible_alias = "f", about = "Invert the state of all pixels")] | ||||||
|     Invert, |     Flip, | ||||||
|     #[command(about = "Set all pixels to the on state")] |     #[command(about = "Set all pixels to the on state")] | ||||||
|     On, |     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)] | #[derive(clap::Parser, std::fmt::Debug)] | ||||||
|  | @ -88,28 +111,24 @@ pub enum Protocol { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(clap::Parser, std::fmt::Debug)] | #[derive(clap::Parser, std::fmt::Debug)] | ||||||
| #[clap(about = "Continuously send data to the display")] | #[clap(about = "Commands for sending text to the screen")] | ||||||
| pub enum StreamCommand { | pub enum TextCommand { | ||||||
|     #[clap(
 |     #[command(
 | ||||||
|         about = "Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin`" |         about = "Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin`" | ||||||
|     )] |     )] | ||||||
|     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, |         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)] | #[derive(clap::Parser, std::fmt::Debug, Clone)] | ||||||
| pub struct StreamScreenOptions { | pub struct StreamScreenOptions { | ||||||
|     #[arg(long, short, default_value_t = false, help = "Disable dithering")] |  | ||||||
|     pub no_dither: bool, |  | ||||||
| 
 |  | ||||||
|     #[arg(
 |     #[arg(
 | ||||||
|         long, |         long, | ||||||
|         short, |         short, | ||||||
|  | @ -118,3 +137,33 @@ pub struct StreamScreenOptions { | ||||||
|     )] |     )] | ||||||
|     pub pointer: bool, |     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 clap::Parser; | ||||||
| use log::debug; | use log::debug; | ||||||
| use servicepoint::Connection; | use servicepoint::{Brightness, Connection}; | ||||||
| 
 | 
 | ||||||
|  | mod brightness; | ||||||
| mod cli; | mod cli; | ||||||
| mod execute; | mod image_processing; | ||||||
|  | mod ledwand_dither; | ||||||
|  | mod pixels; | ||||||
| mod stream_stdin; | mod stream_stdin; | ||||||
| mod stream_window; | mod stream_window; | ||||||
|  | mod text; | ||||||
| 
 | 
 | ||||||
| fn main() { | fn main() { | ||||||
|     let cli = Cli::parse(); |     let cli = Cli::parse(); | ||||||
|  | @ -16,7 +25,19 @@ fn main() { | ||||||
|     let connection = make_connection(cli.destination, cli.transport); |     let connection = make_connection(cli.destination, cli.transport); | ||||||
|     debug!("connection established: {:#?}", connection); |     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 { | 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 servicepoint::*; | ||||||
| use std::thread::sleep; | 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."); |     warn!("This mode will break when using multi-byte characters and does not support ANSI escape sequences yet."); | ||||||
|     let mut app = App { |     let mut app = App { | ||||||
|         connection, |         connection, | ||||||
|  | @ -13,14 +13,14 @@ pub(crate) fn stream_stdin(connection: Connection, slow: bool) { | ||||||
|     app.run() |     app.run() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| struct App { | struct App<'t> { | ||||||
|     connection: Connection, |     connection: &'t Connection, | ||||||
|     mirror: CharGrid, |     mirror: CharGrid, | ||||||
|     y: usize, |     y: usize, | ||||||
|     slow: bool, |     slow: bool, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl App { | impl App<'_> { | ||||||
|     fn run(&mut self) { |     fn run(&mut self) { | ||||||
|         self.connection |         self.connection | ||||||
|             .send(Command::Clear) |             .send(Command::Clear) | ||||||
|  | @ -63,15 +63,16 @@ impl App { | ||||||
| 
 | 
 | ||||||
|     fn send_mirror(&self) { |     fn send_mirror(&self) { | ||||||
|         self.connection |         self.connection | ||||||
|             .send(Command::Cp437Data( |             .send(Command::Utf8Data( | ||||||
|                 Origin::ZERO, |                 Origin::ZERO, | ||||||
|                 Cp437Grid::from(&self.mirror), |                 self.mirror.clone(), | ||||||
|             )) |             )) | ||||||
|             .expect("couldn't send screen to display"); |             .expect("couldn't send screen to display"); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn single_line(&mut self, line: &str) { |     fn single_line(&mut self, line: &str) { | ||||||
|         let mut line_grid = CharGrid::new(TILE_WIDTH, 1); |         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 line_grid, 0, line); | ||||||
|         Self::line_onto_grid(&mut self.mirror, self.y, line); |         Self::line_onto_grid(&mut self.mirror, self.y, line); | ||||||
|         self.connection |         self.connection | ||||||
|  |  | ||||||
|  | @ -1,61 +1,50 @@ | ||||||
| use crate::cli::StreamScreenOptions; | use crate::{ | ||||||
| use image::{ |     cli::{ImageProcessingOptions, StreamScreenOptions}, | ||||||
|     imageops::{dither, resize, BiLevel, FilterType}, |     image_processing::ImageProcessingPipeline, | ||||||
|     DynamicImage, ImageBuffer, Luma, Rgb, Rgba, |  | ||||||
| }; | }; | ||||||
| use log::{error, info, warn}; | use image::{DynamicImage, ImageBuffer, Rgb, Rgba}; | ||||||
|  | use log::{debug, error, info, trace, warn}; | ||||||
| use scap::{ | use scap::{ | ||||||
|     capturer::{Capturer, Options}, |     capturer::{Capturer, Options}, | ||||||
|     frame::convert_bgra_to_rgb, |     frame::convert_bgra_to_rgb, | ||||||
|     frame::Frame, |     frame::Frame, | ||||||
| }; | }; | ||||||
| use servicepoint::{ | use servicepoint::{Command, CompressionCode, Connection, Origin, FRAME_PACING}; | ||||||
|     Bitmap, Command, CompressionCode, Connection, Origin, FRAME_PACING, PIXEL_HEIGHT, PIXEL_WIDTH, | use std::time::{Duration, Instant}; | ||||||
| }; |  | ||||||
| use std::time::Duration; |  | ||||||
| 
 | 
 | ||||||
| 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); |     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) { |     let capturer = match start_capture(&options) { | ||||||
|         Some(value) => value, |         Some(value) => value, | ||||||
|         None => return, |         None => return, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     let mut bitmap = Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT); |     let mut pipeline = ImageProcessingPipeline::new(processing_options); | ||||||
|  | 
 | ||||||
|     info!("now starting to stream images"); |     info!("now starting to stream images"); | ||||||
|     loop { |     loop { | ||||||
|         let frame = get_next_frame(&capturer, options.no_dither); |         let start = Instant::now(); | ||||||
|         for (mut dest, src) in bitmap.iter_mut().zip(frame.pixels()) { | 
 | ||||||
|             *dest = src.0[0] > u8::MAX / 2; |         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 |         connection | ||||||
|             .send(Command::BitmapLinearWin( |             .send(Command::BitmapLinearWin( | ||||||
|                 Origin::ZERO, |                 Origin::ZERO, | ||||||
|                 bitmap.clone(), |                 bitmap.clone(), | ||||||
|                 CompressionCode::Uncompressed, |                 CompressionCode::default(), | ||||||
|             )) |             )) | ||||||
|             .expect("failed to send frame to display"); |             .expect("failed to send frame to display"); | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| fn get_next_frame(capturer: &Capturer, no_dither: bool) -> ImageBuffer<Luma<u8>, Vec<u8>> { |         debug!("frame time: {:?}", start.elapsed()); | ||||||
|     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); |  | ||||||
|     } |     } | ||||||
|     frame |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn start_capture(options: &StreamScreenOptions) -> Option<Capturer> { | 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 { |     let mut capturer = Capturer::build(Options { | ||||||
|         fps: FRAME_PACING.div_duration_f32(Duration::from_secs(1)) as u32, |         fps: FRAME_PACING.div_duration_f32(Duration::from_secs(1)) as u32, | ||||||
|         show_cursor: options.pointer, |         show_cursor: options.pointer, | ||||||
|         output_type: scap::frame::FrameType::BGR0, // this is more like a suggestion
 |         output_type: scap::frame::FrameType::BGR0, | ||||||
|         ..Default::default() |         ..Default::default() | ||||||
|     }) |     }) | ||||||
|     .expect("failed to create screen capture"); |     .expect("failed to create screen capture"); | ||||||
|  | @ -83,8 +73,16 @@ fn start_capture(options: &StreamScreenOptions) -> Option<Capturer> { | ||||||
|     Some(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 { | 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::BGRx(frame) => bgrx_to_rgb(frame.width, frame.height, frame.data), | ||||||
|         Frame::RGBx(frame) => DynamicImage::from( |         Frame::RGBx(frame) => DynamicImage::from( | ||||||
|             ImageBuffer::<Rgba<_>, _>::from_raw( |             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::BGRA(frame) => bgrx_to_rgb(frame.width, frame.height, frame.data), | ||||||
|         Frame::YUVFrame(_) | Frame::XBGR(_) => panic!("unsupported frame format"), |         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 { | 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…
	
	Add table
		Add a link
		
	
		Reference in a new issue