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
					
				
					 15 changed files with 1134 additions and 223 deletions
				
			
		
							
								
								
									
										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
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								flake.lock
									
										
									
										generated
									
									
									
								
							|  | @ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue