From 304317a86ed3a43ee03cae48e1d41edd653a3e0e Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Thu, 13 Feb 2025 20:05:28 +0100 Subject: [PATCH 01/16] fix pixels on inverts --- src/cli.rs | 12 +++++++++--- src/execute.rs | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index fa420d6..3ef2286 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -94,11 +94,17 @@ pub enum StreamCommand { 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.")] + 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, @@ -107,7 +113,7 @@ pub enum StreamCommand { #[derive(clap::Parser, std::fmt::Debug, Clone)] pub struct StreamScreenOptions { - #[arg(long, short, default_value_t = false, help = "Disable dithering")] + #[arg(long, short, default_value_t = false, help = "Disable dithering - improves performance")] pub no_dither: bool, #[arg( diff --git a/src/execute.rs b/src/execute.rs index ad91eba..17ac5df 100644 --- a/src/execute.rs +++ b/src/execute.rs @@ -23,14 +23,14 @@ 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) + 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)) + .send(Command::BitmapLinear(0, mask, CompressionCode::Lzma)) .expect("could not send command") } From 2dcf092100003c60b99a7839f1fa7704a0cfa0eb Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Thu, 13 Feb 2025 20:14:29 +0100 Subject: [PATCH 02/16] split execute.rs --- src/brightness.rs | 20 +++++++++++++ src/cli.rs | 1 + src/execute.rs | 73 ----------------------------------------------- src/main.rs | 30 ++++++++++++++++--- src/pixels.rs | 34 ++++++++++++++++++++++ 5 files changed, 81 insertions(+), 77 deletions(-) create mode 100644 src/brightness.rs delete mode 100644 src/execute.rs create mode 100644 src/pixels.rs diff --git a/src/brightness.rs b/src/brightness.rs new file mode 100644 index 0000000..09b7e55 --- /dev/null +++ b/src/brightness.rs @@ -0,0 +1,20 @@ +use servicepoint::{Brightness, Command, Connection}; +use log::info; +use crate::cli::BrightnessCommand; + +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:?}"); +} \ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs index 3ef2286..8f205f1 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -53,6 +53,7 @@ pub enum PixelCommand { #[command( visible_alias = "r", visible_alias = "reset", + visible_alias = "clear", about = "Reset all pixels to the default (off) state" )] Off, diff --git a/src/execute.rs b/src/execute.rs deleted file mode 100644 index 17ac5df..0000000 --- a/src/execute.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::cli::{BrightnessCommand, Mode, PixelCommand, StreamCommand}; -use crate::stream_stdin::stream_stdin; -use crate::stream_window::stream_window; -use log::info; -use servicepoint::{BitVec, Brightness, Command, CompressionCode, Connection, PIXEL_COUNT}; - -pub fn execute_mode(mode: Mode, connection: Connection) { - match mode { - Mode::ResetEverything => { - brightness_reset(&connection); - pixels_reset(&connection); - } - Mode::Pixels { pixel_command } => pixels(&connection, pixel_command), - Mode::Brightness { brightness_command } => brightness(&connection, brightness_command), - Mode::Stream { stream_command } => match stream_command { - StreamCommand::Stdin { slow } => stream_stdin(connection, slow), - StreamCommand::Screen { options } => stream_window(&connection, options), - }, - } -} - -fn pixels(connection: &Connection, pixel_command: PixelCommand) { - match pixel_command { - PixelCommand::Off => pixels_reset(connection), - PixelCommand::Invert => pixels_invert(connection), - PixelCommand::On => pixels_on(connection), - } -} - -fn pixels_on(connection: &Connection) { - let mask = BitVec::repeat(true, PIXEL_COUNT); - connection - .send(Command::BitmapLinear(0, mask, CompressionCode::Lzma)) - .expect("could not send command") -} - -fn pixels_invert(connection: &Connection) { - let mask = BitVec::repeat(true, PIXEL_COUNT); - connection - .send(Command::BitmapLinearXor(0, mask, CompressionCode::Lzma)) - .expect("could not send command") -} - -fn brightness(connection: &Connection, brightness_command: BrightnessCommand) { - match brightness_command { - BrightnessCommand::Max => brightness_reset(connection), - BrightnessCommand::Min => brightness_set(connection, Brightness::MIN), - BrightnessCommand::Set { brightness } => { - brightness_set(connection, Brightness::saturating_from(brightness)) - } - } -} - -fn pixels_reset(connection: &Connection) { - connection - .send(Command::Clear) - .expect("failed to clear pixels"); - info!("Reset pixels"); -} - -fn brightness_reset(connection: &Connection) { - connection - .send(Command::Brightness(Brightness::MAX)) - .expect("Failed to reset brightness to maximum"); - info!("Reset brightness"); -} - -fn brightness_set(connection: &Connection, brightness: Brightness) { - connection - .send(Command::Brightness(brightness)) - .expect("Failed to set brightness"); - info!("set brightness to {brightness:?}"); -} diff --git a/src/main.rs b/src/main.rs index a9878ca..bbdd20a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,17 @@ -use crate::cli::{Cli, Protocol}; +use crate::{ + brightness::{brightness, brightness_set}, + cli::{Cli, Mode, Protocol, StreamCommand}, + pixels::{pixels, pixels_off}, + stream_stdin::stream_stdin, + stream_window::stream_window, +}; use clap::Parser; use log::debug; -use servicepoint::Connection; +use servicepoint::{Brightness, Connection}; +mod brightness; mod cli; -mod execute; +mod pixels; mod stream_stdin; mod stream_window; @@ -16,7 +23,22 @@ 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::Stream { stream_command } => match stream_command { + StreamCommand::Stdin { slow } => stream_stdin(connection, slow), + StreamCommand::Screen { options } => stream_window(&connection, options), + }, + } } fn make_connection(destination: String, transport: Protocol) -> Connection { diff --git a/src/pixels.rs b/src/pixels.rs new file mode 100644 index 0000000..0e4597e --- /dev/null +++ b/src/pixels.rs @@ -0,0 +1,34 @@ +use servicepoint::{BitVec, Command, CompressionCode, Connection, PIXEL_COUNT}; +use log::info; +use crate::cli::PixelCommand; + +pub(crate) fn pixels(connection: &Connection, pixel_command: PixelCommand) { + match pixel_command { + PixelCommand::Off => pixels_off(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::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"); +} \ No newline at end of file From 1de6caa8a7b09fcec35adc59a7c72a68aa15aec9 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Fri, 28 Feb 2025 10:48:03 +0100 Subject: [PATCH 03/16] implement histogram correction from CCCB_Ledwand --- src/brightness.rs | 6 +- src/cli.rs | 7 ++- src/ledwand_dither.rs | 127 ++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + src/pixels.rs | 6 +- src/stream_stdin.rs | 1 + src/stream_window.rs | 27 +++++---- 7 files changed, 158 insertions(+), 17 deletions(-) create mode 100644 src/ledwand_dither.rs diff --git a/src/brightness.rs b/src/brightness.rs index 09b7e55..adead44 100644 --- a/src/brightness.rs +++ b/src/brightness.rs @@ -1,6 +1,6 @@ -use servicepoint::{Brightness, Command, Connection}; -use log::info; use crate::cli::BrightnessCommand; +use log::info; +use servicepoint::{Brightness, Command, Connection}; pub(crate) fn brightness(connection: &Connection, brightness_command: BrightnessCommand) { match brightness_command { @@ -17,4 +17,4 @@ pub(crate) fn brightness_set(connection: &Connection, brightness: Brightness) { .send(Command::Brightness(brightness)) .expect("Failed to set brightness"); info!("set brightness to {brightness:?}"); -} \ No newline at end of file +} diff --git a/src/cli.rs b/src/cli.rs index 8f205f1..12a478e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -114,7 +114,12 @@ pub enum StreamCommand { #[derive(clap::Parser, std::fmt::Debug, Clone)] pub struct StreamScreenOptions { - #[arg(long, short, default_value_t = false, help = "Disable dithering - improves performance")] + #[arg( + long, + short, + default_value_t = false, + help = "Disable dithering - improves performance" + )] pub no_dither: bool, #[arg( diff --git a/src/ledwand_dither.rs b/src/ledwand_dither.rs new file mode 100644 index 0000000..c235a95 --- /dev/null +++ b/src/ledwand_dither.rs @@ -0,0 +1,127 @@ +//! Based on https://github.com/WarkerAnhaltRanger/CCCB_Ledwand + +use image::GrayImage; +use servicepoint::{PIXEL_HEIGHT, PIXEL_WIDTH}; + +pub struct LedwandDither { + options: LedwandDitherOptions, + tmpbuf: GrayImage, +} + +#[derive(Debug, Default)] +pub struct LedwandDitherOptions { + pub size: Option<(u32, u32)>, +} + +type GrayHistogram = [usize; 256]; + +struct HistogramCorrection { + pre_offset: f32, + post_offset: f32, + factor: f32, +} + +impl LedwandDither { + pub fn new(options: LedwandDitherOptions) -> Self { + let (width, height) = options + .size + .unwrap_or((PIXEL_WIDTH as u32, PIXEL_HEIGHT as u32)); + Self { + tmpbuf: GrayImage::new(width, height), + options, + } + } + + pub fn histogram_correction(image: &mut GrayImage) { + let histogram = Self::make_histogram(image); + let correction = Self::determine_histogram_correction(image, histogram); + Self::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() / 100; + + let mut num_pixels = 0; + let mut brightness = 0; + + let mincut = loop { + num_pixels += histogram[brightness as usize] as usize; + brightness += 1; + if num_pixels >= adjustment_pixels { + break u8::min(brightness, 20); + } + }; + + let minshift = loop { + num_pixels += histogram[brightness as usize] 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] 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) { + let midpoint = image.width() / 2; + for (x, _, pixel) in image.enumerate_pixels_mut() { + if x > midpoint { + continue; + } + + 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 = Self::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] as usize; + if num_pixels >= midpoint { + return brightness; + } + } + + unreachable!("Somehow less pixels where counted in the histogram than exist in the image") + } +} diff --git a/src/main.rs b/src/main.rs index bbdd20a..cd7937c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ use servicepoint::{Brightness, Connection}; mod brightness; mod cli; +mod ledwand_dither; mod pixels; mod stream_stdin; mod stream_window; diff --git a/src/pixels.rs b/src/pixels.rs index 0e4597e..4e6e3cf 100644 --- a/src/pixels.rs +++ b/src/pixels.rs @@ -1,6 +1,6 @@ -use servicepoint::{BitVec, Command, CompressionCode, Connection, PIXEL_COUNT}; -use log::info; use crate::cli::PixelCommand; +use log::info; +use servicepoint::{BitVec, Command, CompressionCode, Connection, PIXEL_COUNT}; pub(crate) fn pixels(connection: &Connection, pixel_command: PixelCommand) { match pixel_command { @@ -31,4 +31,4 @@ pub(crate) fn pixels_off(connection: &Connection) { .send(Command::Clear) .expect("failed to clear pixels"); info!("reset pixels"); -} \ No newline at end of file +} diff --git a/src/stream_stdin.rs b/src/stream_stdin.rs index a349aa2..82109ba 100644 --- a/src/stream_stdin.rs +++ b/src/stream_stdin.rs @@ -72,6 +72,7 @@ impl App { fn single_line(&mut self, line: &str) { let mut line_grid = CharGrid::new(TILE_WIDTH, 1); + line_grid.fill(' '); Self::line_onto_grid(&mut line_grid, 0, line); Self::line_onto_grid(&mut self.mirror, self.y, line); self.connection diff --git a/src/stream_window.rs b/src/stream_window.rs index e3a626a..db2b0bd 100644 --- a/src/stream_window.rs +++ b/src/stream_window.rs @@ -1,4 +1,5 @@ use crate::cli::StreamScreenOptions; +use crate::ledwand_dither::{LedwandDither, LedwandDitherOptions}; use image::{ imageops::{dither, resize, BiLevel, FilterType}, DynamicImage, ImageBuffer, Luma, Rgb, Rgba, @@ -26,9 +27,18 @@ pub fn stream_window(connection: &Connection, options: StreamScreenOptions) { let mut bitmap = Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT); info!("now starting to stream images"); loop { - let frame = get_next_frame(&capturer, options.no_dither); + let mut frame = get_next_frame(&capturer); + + LedwandDither::histogram_correction(&mut frame); + let cutoff = if options.no_dither { + LedwandDither::median_brightness(&frame) + } else { + dither(&mut frame, &BiLevel); + u8::MAX / 2 + }; + for (mut dest, src) in bitmap.iter_mut().zip(frame.pixels()) { - *dest = src.0[0] > u8::MAX / 2; + *dest = src.0[0] > cutoff; } connection @@ -41,21 +51,18 @@ pub fn stream_window(connection: &Connection, options: StreamScreenOptions) { } } -fn get_next_frame(capturer: &Capturer, no_dither: bool) -> ImageBuffer, Vec> { +/// returns next frame from the capturer, resized and grayscale +fn get_next_frame(capturer: &Capturer) -> ImageBuffer, Vec> { let frame = capturer.get_next_frame().expect("failed to capture frame"); let frame = frame_to_image(frame); let frame = frame.grayscale().to_luma8(); - let mut frame = resize( + + 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 { From ea7262f8f534b9654ebfb4968ea876a90b6ace05 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Fri, 28 Feb 2025 11:58:04 +0100 Subject: [PATCH 04/16] implement blur --- src/ledwand_dither.rs | 57 ++++++++++++++++++++++++++++++++++--------- src/stream_window.rs | 3 ++- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/ledwand_dither.rs b/src/ledwand_dither.rs index c235a95..ce92b03 100644 --- a/src/ledwand_dither.rs +++ b/src/ledwand_dither.rs @@ -1,6 +1,6 @@ //! Based on https://github.com/WarkerAnhaltRanger/CCCB_Ledwand -use image::GrayImage; +use image::{GenericImage, GrayImage}; use servicepoint::{PIXEL_HEIGHT, PIXEL_WIDTH}; pub struct LedwandDither { @@ -50,13 +50,13 @@ impl LedwandDither { image: &GrayImage, histogram: GrayHistogram, ) -> HistogramCorrection { - let adjustment_pixels = image.len() / 100; + 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] as usize; + num_pixels += histogram[brightness as usize]; brightness += 1; if num_pixels >= adjustment_pixels { break u8::min(brightness, 20); @@ -64,7 +64,7 @@ impl LedwandDither { }; let minshift = loop { - num_pixels += histogram[brightness as usize] as usize; + num_pixels += histogram[brightness as usize]; brightness += 1; if num_pixels >= 2 * adjustment_pixels { break u8::min(brightness, 64); @@ -74,7 +74,7 @@ impl LedwandDither { brightness = u8::MAX; num_pixels = 0; let maxshift = loop { - num_pixels += histogram[brightness as usize] as usize; + num_pixels += histogram[brightness as usize]; brightness -= 1; if num_pixels >= 2 * adjustment_pixels { break u8::max(brightness, 192); @@ -92,12 +92,7 @@ impl LedwandDither { } fn apply_histogram_correction(image: &mut GrayImage, correction: HistogramCorrection) { - let midpoint = image.width() / 2; - for (x, _, pixel) in image.enumerate_pixels_mut() { - if x > midpoint { - continue; - } - + 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; @@ -116,7 +111,7 @@ impl LedwandDither { let mut num_pixels = 0; for brightness in u8::MIN..=u8::MAX { - num_pixels += histogram[brightness as usize] as usize; + num_pixels += histogram[brightness as usize]; if num_pixels >= midpoint { return brightness; } @@ -124,4 +119,42 @@ impl LedwandDither { 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()); + + Self::copy_border(source, destination); + Self::blur_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; + } + } + } } diff --git a/src/stream_window.rs b/src/stream_window.rs index db2b0bd..5ab7c3d 100644 --- a/src/stream_window.rs +++ b/src/stream_window.rs @@ -29,10 +29,11 @@ pub fn stream_window(connection: &Connection, options: StreamScreenOptions) { loop { let mut frame = get_next_frame(&capturer); - LedwandDither::histogram_correction(&mut frame); let cutoff = if options.no_dither { LedwandDither::median_brightness(&frame) } else { + LedwandDither::histogram_correction(&mut frame); + LedwandDither::blur(&frame.clone(), &mut frame); dither(&mut frame, &BiLevel); u8::MAX / 2 }; From f64365f5bd4991c49ccd934dda34348b5eda207e Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Fri, 28 Feb 2025 12:12:39 +0100 Subject: [PATCH 05/16] implement sharpen --- src/ledwand_dither.rs | 24 ++++++++++++++++++++++++ src/stream_window.rs | 10 ++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/ledwand_dither.rs b/src/ledwand_dither.rs index ce92b03..135515c 100644 --- a/src/ledwand_dither.rs +++ b/src/ledwand_dither.rs @@ -127,6 +127,13 @@ impl LedwandDither { Self::blur_inner_pixels(source, destination); } + pub fn sharpen(source: &GrayImage, destination: &mut GrayImage) { + assert_eq!(source.len(), destination.len()); + + Self::copy_border(source, destination); + Self::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() { @@ -157,4 +164,21 @@ impl LedwandDither { } } } + + 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; + } + } + } } diff --git a/src/stream_window.rs b/src/stream_window.rs index 5ab7c3d..0358c61 100644 --- a/src/stream_window.rs +++ b/src/stream_window.rs @@ -29,11 +29,17 @@ pub fn stream_window(connection: &Connection, options: StreamScreenOptions) { loop { let mut frame = get_next_frame(&capturer); + LedwandDither::histogram_correction(&mut frame); + + let mut orig = frame.clone(); + LedwandDither::blur(&orig, &mut frame); + + std::mem::swap(&mut frame, &mut orig); + LedwandDither::sharpen(&orig, &mut frame); + let cutoff = if options.no_dither { LedwandDither::median_brightness(&frame) } else { - LedwandDither::histogram_correction(&mut frame); - LedwandDither::blur(&frame.clone(), &mut frame); dither(&mut frame, &BiLevel); u8::MAX / 2 }; From 9d5b21673a581605fff14b899e697a70a0c578c7 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Fri, 28 Feb 2025 17:38:38 +0100 Subject: [PATCH 06/16] ostromoukhov dither --- src/cli.rs | 8 - src/ledwand_dither.rs | 649 +++++++++++++++++++++++++++++++----------- src/stream_window.rs | 25 +- 3 files changed, 492 insertions(+), 190 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 12a478e..1ee5783 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -114,14 +114,6 @@ pub enum StreamCommand { #[derive(clap::Parser, std::fmt::Debug, Clone)] pub struct StreamScreenOptions { - #[arg( - long, - short, - default_value_t = false, - help = "Disable dithering - improves performance" - )] - pub no_dither: bool, - #[arg( long, short, diff --git a/src/ledwand_dither.rs b/src/ledwand_dither.rs index 135515c..be1091e 100644 --- a/src/ledwand_dither.rs +++ b/src/ledwand_dither.rs @@ -1,17 +1,7 @@ //! Based on https://github.com/WarkerAnhaltRanger/CCCB_Ledwand -use image::{GenericImage, GrayImage}; -use servicepoint::{PIXEL_HEIGHT, PIXEL_WIDTH}; - -pub struct LedwandDither { - options: LedwandDitherOptions, - tmpbuf: GrayImage, -} - -#[derive(Debug, Default)] -pub struct LedwandDitherOptions { - pub size: Option<(u32, u32)>, -} +use image::GrayImage; +use servicepoint::{BitVec, Bitmap, PIXEL_HEIGHT}; type GrayHistogram = [usize; 256]; @@ -21,164 +11,495 @@ struct HistogramCorrection { factor: f32, } -impl LedwandDither { - pub fn new(options: LedwandDitherOptions) -> Self { - let (width, height) = options - .size - .unwrap_or((PIXEL_WIDTH as u32, PIXEL_HEIGHT as u32)); - Self { - tmpbuf: GrayImage::new(width, height), - options, +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; } } - pub fn histogram_correction(image: &mut GrayImage) { - let histogram = Self::make_histogram(image); - let correction = Self::determine_histogram_correction(image, histogram); - Self::apply_histogram_correction(image, correction) + 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)]; } - - fn make_histogram(image: &GrayImage) -> GrayHistogram { - let mut histogram = [0; 256]; - for pixel in image.pixels() { - histogram[pixel.0[0] as usize] += 1; - } - histogram + 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 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 = Self::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()); - - Self::copy_border(source, destination); - Self::blur_inner_pixels(source, destination); - } - - pub fn sharpen(source: &GrayImage, destination: &mut GrayImage) { - assert_eq!(source.len(), destination.len()); - - Self::copy_border(source, destination); - Self::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; - } +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 0..width as usize { + ostromoukhov_dither_pixel( + &mut source, + &mut destination, + start + x, + width as usize, + y == (height - 1) as usize, + 1, + bias, + ); + } + } else { + for x in (0..width as usize).rev() { + ostromoukhov_dither_pixel( + &mut source, + &mut destination, + start + 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 old_pixel = source[position]; + + let destination_value = old_pixel > bias; + destination.set(position, destination_value); + + let error = if destination_value { + 255 - old_pixel + } else { + old_pixel + }; + + 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]); + } +} + +const ERROR_DIFFUSION_MATRIX: [[i16; 3]; 256] = [ + [0, 1, 0], + [1, 0, 0], + [1, 0, 1], + [2, 0, 1], + [2, 0, 2], + [3, 0, 2], + [4, 0, 2], + [4, 1, 2], + [5, 1, 2], + [5, 2, 2], + [5, 3, 2], + [6, 3, 2], + [6, 3, 3], + [7, 3, 3], + [7, 4, 3], + [8, 4, 3], + [8, 5, 3], + [9, 5, 3], + [9, 5, 4], + [10, 6, 3], + [10, 6, 4], + [11, 7, 3], + [11, 7, 4], + [11, 8, 4], + [12, 7, 5], + [12, 7, 6], + [12, 7, 7], + [12, 7, 8], + [12, 7, 9], + [13, 7, 9], + [13, 7, 10], + [13, 7, 11], + [13, 7, 12], + [14, 7, 12], + [14, 8, 12], + [15, 8, 12], + [15, 9, 12], + [16, 9, 12], + [16, 10, 12], + [17, 10, 12], + [17, 11, 12], + [18, 12, 11], + [19, 12, 11], + [19, 13, 11], + [20, 13, 11], + [20, 14, 11], + [21, 15, 10], + [22, 15, 10], + [22, 17, 9], + [23, 17, 9], + [24, 18, 8], + [24, 19, 8], + [25, 19, 8], + [26, 20, 7], + [26, 21, 7], + [27, 22, 6], + [28, 23, 5], + [28, 24, 5], + [29, 25, 4], + [30, 26, 3], + [31, 26, 3], + [31, 28, 2], + [32, 28, 2], + [33, 29, 1], + [34, 30, 0], + [33, 31, 1], + [32, 33, 1], + [32, 33, 2], + [31, 34, 3], + [30, 36, 3], + [29, 37, 4], + [29, 37, 5], + [28, 39, 5], + [32, 34, 7], + [37, 29, 8], + [42, 23, 10], + [46, 19, 11], + [51, 13, 12], + [52, 14, 13], + [53, 13, 12], + [53, 14, 13], + [54, 14, 13], + [55, 14, 13], + [55, 14, 13], + [56, 15, 14], + [57, 14, 13], + [56, 15, 15], + [55, 17, 15], + [54, 18, 16], + [53, 20, 16], + [52, 21, 17], + [52, 22, 17], + [51, 24, 17], + [50, 25, 18], + [49, 27, 18], + [47, 29, 19], + [48, 29, 19], + [48, 29, 20], + [49, 29, 20], + [49, 30, 20], + [50, 31, 20], + [50, 31, 20], + [51, 31, 20], + [51, 31, 21], + [52, 31, 21], + [52, 32, 21], + [53, 32, 21], + [53, 32, 22], + [55, 32, 21], + [56, 31, 22], + [58, 31, 21], + [59, 30, 22], + [61, 30, 21], + [62, 29, 22], + [64, 29, 21], + [65, 28, 22], + [67, 28, 21], + [68, 27, 22], + [70, 27, 21], + [71, 26, 22], + [73, 26, 21], + [75, 25, 21], + [76, 25, 21], + [78, 24, 21], + [80, 23, 21], + [81, 23, 21], + [83, 22, 21], + [85, 21, 20], + [85, 22, 21], + [85, 22, 22], + [84, 24, 22], + [84, 24, 23], + [84, 25, 23], + [83, 27, 23], + [83, 28, 23], + [82, 29, 24], + [82, 30, 24], + [81, 31, 25], + [80, 32, 26], + [80, 33, 26], + [79, 35, 26], + [79, 36, 26], + [78, 37, 27], + [77, 38, 28], + [77, 39, 28], + [76, 41, 28], + [75, 42, 29], + [75, 43, 29], + [74, 44, 30], + [74, 45, 30], + [75, 46, 30], + [75, 46, 30], + [76, 46, 30], + [76, 46, 31], + [77, 46, 31], + [77, 47, 31], + [78, 47, 31], + [78, 47, 32], + [79, 47, 32], + [79, 48, 32], + [80, 49, 32], + [83, 46, 32], + [86, 44, 32], + [90, 42, 31], + [93, 40, 31], + [96, 39, 30], + [100, 36, 30], + [103, 35, 29], + [106, 33, 29], + [110, 30, 29], + [113, 29, 28], + [114, 29, 28], + [115, 29, 28], + [115, 29, 28], + [116, 30, 29], + [117, 29, 28], + [117, 30, 29], + [118, 30, 29], + [119, 30, 29], + [109, 43, 27], + [100, 57, 23], + [90, 71, 20], + [80, 85, 17], + [70, 99, 14], + [74, 98, 12], + [78, 97, 10], + [81, 96, 9], + [85, 95, 7], + [89, 94, 5], + [92, 93, 4], + [96, 92, 2], + [100, 91, 0], + [100, 90, 2], + [100, 88, 5], + [100, 87, 7], + [99, 86, 10], + [99, 85, 12], + [99, 84, 14], + [99, 82, 17], + [98, 81, 20], + [98, 80, 22], + [98, 79, 24], + [98, 77, 27], + [98, 76, 29], + [97, 75, 32], + [97, 73, 35], + [97, 72, 37], + [96, 71, 40], + [96, 69, 43], + [96, 67, 46], + [96, 66, 48], + [95, 65, 51], + [95, 63, 54], + [95, 61, 57], + [94, 60, 60], + [94, 58, 63], + [94, 57, 65], + [93, 55, 69], + [93, 54, 71], + [93, 52, 74], + [92, 51, 77], + [92, 49, 80], + [91, 47, 84], + [91, 46, 86], + [93, 49, 82], + [96, 52, 77], + [98, 55, 73], + [101, 58, 68], + [104, 61, 63], + [106, 65, 58], + [109, 68, 53], + [111, 71, 49], + [114, 74, 44], + [116, 78, 39], + [118, 76, 40], + [119, 74, 42], + [120, 73, 43], + [122, 71, 44], + [123, 69, 46], + [124, 67, 48], + [125, 66, 49], + [127, 64, 50], + [128, 62, 52], + [129, 60, 54], + [131, 58, 55], + [132, 57, 56], + [136, 47, 63], + [139, 38, 70], + [143, 29, 76], + [147, 19, 83], + [151, 9, 90], + [154, 0, 97], + [160, 0, 92], + [171, 0, 82], + [183, 0, 71], + [184, 0, 71], +]; diff --git a/src/stream_window.rs b/src/stream_window.rs index 0358c61..6975ebf 100644 --- a/src/stream_window.rs +++ b/src/stream_window.rs @@ -1,7 +1,7 @@ use crate::cli::StreamScreenOptions; -use crate::ledwand_dither::{LedwandDither, LedwandDitherOptions}; +use crate::ledwand_dither::*; use image::{ - imageops::{dither, resize, BiLevel, FilterType}, + imageops::{resize, FilterType}, DynamicImage, ImageBuffer, Luma, Rgb, Rgba, }; use log::{error, info, warn}; @@ -11,42 +11,31 @@ use scap::{ frame::Frame, }; use servicepoint::{ - Bitmap, Command, CompressionCode, Connection, Origin, FRAME_PACING, PIXEL_HEIGHT, PIXEL_WIDTH, + Command, CompressionCode, Connection, Origin, FRAME_PACING, PIXEL_HEIGHT, PIXEL_WIDTH, }; use std::time::Duration; pub fn stream_window(connection: &Connection, options: StreamScreenOptions) { 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); info!("now starting to stream images"); loop { let mut frame = get_next_frame(&capturer); - LedwandDither::histogram_correction(&mut frame); + histogram_correction(&mut frame); let mut orig = frame.clone(); - LedwandDither::blur(&orig, &mut frame); + blur(&orig, &mut frame); std::mem::swap(&mut frame, &mut orig); - LedwandDither::sharpen(&orig, &mut frame); + sharpen(&orig, &mut frame); - let cutoff = if options.no_dither { - LedwandDither::median_brightness(&frame) - } else { - dither(&mut frame, &BiLevel); - u8::MAX / 2 - }; - - for (mut dest, src) in bitmap.iter_mut().zip(frame.pixels()) { - *dest = src.0[0] > cutoff; - } + let bitmap = ostromoukhov_dither(frame, u8::MAX / 2); connection .send(Command::BitmapLinearWin( From 70cc466253dde2a4a9dc8fd7646101ee056b916f Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Fri, 28 Feb 2025 18:49:29 +0100 Subject: [PATCH 07/16] add options to disable steps --- src/cli.rs | 16 ++++++++++++++++ src/stream_window.rs | 28 ++++++++++++++++++++-------- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 1ee5783..8de65ef 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -121,4 +121,20 @@ pub struct StreamScreenOptions { help = "Show mouse pointer in video feed" )] pub pointer: bool, + + #[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, } diff --git a/src/stream_window.rs b/src/stream_window.rs index 6975ebf..bfa9806 100644 --- a/src/stream_window.rs +++ b/src/stream_window.rs @@ -10,9 +10,7 @@ use scap::{ frame::convert_bgra_to_rgb, frame::Frame, }; -use servicepoint::{ - Command, CompressionCode, Connection, Origin, FRAME_PACING, PIXEL_HEIGHT, PIXEL_WIDTH, -}; +use servicepoint::{Bitmap, Command, CompressionCode, Connection, Origin, FRAME_PACING, PIXEL_HEIGHT, PIXEL_WIDTH}; use std::time::Duration; pub fn stream_window(connection: &Connection, options: StreamScreenOptions) { @@ -27,15 +25,29 @@ pub fn stream_window(connection: &Connection, options: StreamScreenOptions) { loop { let mut frame = get_next_frame(&capturer); - histogram_correction(&mut frame); + if !options.no_hist { + histogram_correction(&mut frame); + } let mut orig = frame.clone(); - blur(&orig, &mut frame); - std::mem::swap(&mut frame, &mut orig); - sharpen(&orig, &mut frame); + if !options.no_blur { + blur(&orig, &mut frame); + std::mem::swap(&mut frame, &mut orig); + } - let bitmap = ostromoukhov_dither(frame, u8::MAX / 2); + if !options.no_sharp { + sharpen(&orig, &mut frame); + std::mem::swap(&mut frame, &mut orig); + } + + let bitmap = if 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) + }; connection .send(Command::BitmapLinearWin( From ae1571bcd188cb2519f99ef671fce01a6697d55a Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Fri, 28 Feb 2025 22:46:33 +0100 Subject: [PATCH 08/16] update README, version, cargo update --- Cargo.lock | 174 ++++++++++++++++++++++++++++++++++++++--------------- Cargo.toml | 2 +- README.md | 30 +++++---- src/cli.rs | 3 +- 4 files changed, 147 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 31e39ae..19ef7a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,9 +85,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.95" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4" [[package]] name = "arbitrary" @@ -134,9 +134,9 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62" +checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e" dependencies = [ "arrayvec", ] @@ -251,9 +251,9 @@ checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" [[package]] name = "cc" -version = "1.2.14" +version = "1.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" dependencies = [ "jobserver", "libc", @@ -304,9 +304,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.30" +version = "4.5.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d" +checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" dependencies = [ "clap_builder", "clap_derive", @@ -314,9 +314,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.30" +version = "4.5.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c" +checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" dependencies = [ "anstream", "anstyle", @@ -559,9 +559,9 @@ checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" [[package]] name = "either" -version = "1.13.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d" [[package]] name = "env_filter" @@ -618,9 +618,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 +772,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 +964,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" @@ -1025,9 +1037,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 +1083,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 +1334,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 +1402,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 +1424,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 +1443,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 +1482,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 +1528,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 +1603,7 @@ dependencies = [ "dbus", "objc", "pipewire", - "rand", + "rand 0.8.5", "screencapturekit", "screencapturekit-sys", "sysinfo", @@ -1600,18 +1643,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,7 +1686,7 @@ dependencies = [ [[package]] name = "servicepoint-cli" -version = "0.2.1" +version = "0.3.0" dependencies = [ "clap", "env_logger", @@ -1856,17 +1899,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 +1916,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 +1985,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 +2252,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 +2293,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 +2316,17 @@ dependencies = [ "syn", ] +[[package]] +name = "zerocopy-derive" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712c8386f4f4299382c9abee219bee7084f78fb939d88b6840fcc1320d5f6da2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zune-core" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 8076574..54edfe6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "servicepoint-cli" description = "A command line interface for the ServicePoint display." -version = "0.2.1" +version = "0.3.0" edition = "2021" rust-version = "1.80.0" publish = true diff --git a/README.md b/README.md index 439cf6d..90ab17a 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,10 @@ cargo run -- Usage: servicepoint-cli [OPTIONS] Commands: - reset-everything [aliases: r] - pixels [aliases: p] - brightness [aliases: b] - stream [aliases: s] + reset-everything Reset both pixels and brightness [aliases: r] + pixels Commands for manipulating pixels [aliases: p] + brightness Commands for manipulating the brightness [aliases: b] + stream Continuously send data to the display [aliases: s] help Print this message or the help of the given subcommand(s) Options: @@ -59,52 +59,58 @@ Usage: servicepoint-cli stream 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) ``` #### 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. + Usage: servicepoint-cli stream screen [OPTIONS] Options: - -n, --no-dither Disable dithering -p, --pointer Show mouse pointer in video feed - -h, --help Print help + --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. ``` #### Stdin ``` +Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin` + Usage: servicepoint-cli stream stdin [OPTIONS] Options: - -s, --slow - -h, --help Print help + -s, --slow Wait for a short amount of time before sending the next line ``` ### Brightness ``` +Commands for manipulating the brightness + Usage: servicepoint-cli brightness Commands: max Reset brightness to the default (max) level [aliases: r, reset] set Set one brightness for the whole screen [aliases: s] min Set brightness to lowest possible level. - help Print this message or the help of the given subcommand(s) ``` ### Pixels ``` +Commands for manipulating pixels + Usage: servicepoint-cli pixels Commands: - off Reset all pixels to the default (off) state [aliases: r, reset] + off Reset all pixels to the default (off) state [aliases: r, reset, clear] 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) ``` ## Contributing diff --git a/src/cli.rs b/src/cli.rs index 8de65ef..6830f98 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -133,8 +133,7 @@ pub struct StreamScreenOptions { #[arg( long, - help = "Disable dithering. - Brightness will be adjusted so that around half of the pixels are on." + help = "Disable dithering. Brightness will be adjusted so that around half of the pixels are on." )] pub no_dither: bool, } From 117e6a8bf7f3986dd089423a7860d42590a87ea3 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Fri, 28 Feb 2025 22:47:24 +0100 Subject: [PATCH 09/16] update flake --- flake.lock | 12 ++++++------ flake.nix | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index abacd0c..e99b4ca 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1736429655, - "narHash": "sha256-BwMekRuVlSB9C0QgwKMICiJ5EVbLGjfe4qyueyNQyGI=", + "lastModified": 1739824009, + "narHash": "sha256-fcNrCMUWVLMG3gKC5M9CBqVOAnJtyRvGPxptQFl5mVg=", "owner": "nix-community", "repo": "naersk", - "rev": "0621e47bd95542b8e1ce2ee2d65d6a1f887a13ce", + "rev": "e5130d37369bfa600144c2424270c96f0ef0e11d", "type": "github" }, "original": { @@ -37,11 +37,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1736549401, - "narHash": "sha256-ibkQrMHxF/7TqAYcQE+tOnIsSEzXmMegzyBWza6uHKM=", + "lastModified": 1740603184, + "narHash": "sha256-t+VaahjQAWyA+Ctn2idyo1yxRIYpaDxMgHkgCNiMJa4=", "owner": "nixos", "repo": "nixpkgs", - "rev": "1dab772dd4a68a7bba5d9460685547ff8e17d899", + "rev": "f44bd8ca21e026135061a0a57dcf3d0775b67a49", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 8bf4958..405734a 100644 --- a/flake.nix +++ b/flake.nix @@ -103,6 +103,7 @@ cargo-expand ]; }) + pkgs.cargo-flamegraph ]; LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath (builtins.concatMap (d: d.buildInputs) inputsFrom)}"; RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; From 19f24f933107d25398f44450cab256596d192485 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sat, 1 Mar 2025 11:18:42 +0100 Subject: [PATCH 10/16] extract image processing --- src/cli.rs | 6 ++++ src/image_processing.rs | 66 +++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + src/stream_window.rs | 58 +++++++++--------------------------- 4 files changed, 87 insertions(+), 44 deletions(-) create mode 100644 src/image_processing.rs diff --git a/src/cli.rs b/src/cli.rs index 6830f98..db64ca7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -122,6 +122,12 @@ pub struct StreamScreenOptions { )] pub pointer: bool, + #[transparent] + pub image_processing: ImageProcessingOptions, +} + +#[derive(clap::Parser, std::fmt::Debug, Clone)] +pub struct ImageProcessingOptions { #[arg(long, help = "Disable histogram correction")] pub no_hist: bool, diff --git a/src/image_processing.rs b/src/image_processing.rs new file mode 100644 index 0000000..f36bdc9 --- /dev/null +++ b/src/image_processing.rs @@ -0,0 +1,66 @@ +use crate::cli::ImageProcessingOptions; +use crate::ledwand_dither::{ + blur, histogram_correction, median_brightness, ostromoukhov_dither, sharpen, +}; +use image::imageops::{resize, FilterType}; +use image::{imageops, DynamicImage, ImageBuffer, Luma}; +use servicepoint::{Bitmap, PIXEL_HEIGHT, PIXEL_WIDTH}; + +pub struct ImageProcessingPipeline { + options: ImageProcessingOptions, +} + +impl ImageProcessingPipeline { + pub fn new(options: ImageProcessingOptions) -> Self { + Self { options } + } + + pub fn process(&self, frame: DynamicImage) -> Bitmap { + let frame = Self::resize_grayscale(&frame); + let frame = self.grayscale_processing(frame); + self.grayscale_to_bitmap(frame) + } + + fn resize_grayscale(frame: &DynamicImage) -> ImageBuffer, Vec> { + let frame = imageops::grayscale(&frame); + let frame = resize( + &frame, + PIXEL_WIDTH as u32, + PIXEL_HEIGHT as u32, + FilterType::Nearest, + ); + frame + } + + fn grayscale_processing( + &self, + mut frame: ImageBuffer, Vec>, + ) -> ImageBuffer, Vec> { + 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); + } + orig + } + + fn grayscale_to_bitmap(&self, mut orig: ImageBuffer, Vec>) -> Bitmap { + 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) + } + } +} diff --git a/src/main.rs b/src/main.rs index cd7937c..2a86e33 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ use servicepoint::{Brightness, Connection}; mod brightness; mod cli; +mod image_processing; mod ledwand_dither; mod pixels; mod stream_stdin; diff --git a/src/stream_window.rs b/src/stream_window.rs index bfa9806..da8d866 100644 --- a/src/stream_window.rs +++ b/src/stream_window.rs @@ -1,5 +1,8 @@ -use crate::cli::StreamScreenOptions; -use crate::ledwand_dither::*; +use crate::{ + cli::{ImageProcessingOptions, StreamScreenOptions}, + image_processing::ImageProcessingPipeline, + ledwand_dither::*, +}; use image::{ imageops::{resize, FilterType}, DynamicImage, ImageBuffer, Luma, Rgb, Rgba, @@ -10,45 +13,26 @@ use scap::{ frame::convert_bgra_to_rgb, frame::Frame, }; -use servicepoint::{Bitmap, Command, CompressionCode, Connection, Origin, FRAME_PACING, PIXEL_HEIGHT, PIXEL_WIDTH}; +use servicepoint::{ + Bitmap, Command, CompressionCode, Connection, Origin, FRAME_PACING, PIXEL_HEIGHT, PIXEL_WIDTH, + TILE_HEIGHT, TILE_SIZE, +}; use std::time::Duration; pub fn stream_window(connection: &Connection, options: StreamScreenOptions) { info!("Starting capture with options: {:?}", options); - let capturer = match start_capture(&options) { Some(value) => value, None => return, }; + let pipeline = ImageProcessingPipeline::new(options.image_processing); + info!("now starting to stream images"); loop { - let mut frame = get_next_frame(&capturer); - - if !options.no_hist { - histogram_correction(&mut frame); - } - - let mut orig = frame.clone(); - - if !options.no_blur { - blur(&orig, &mut frame); - std::mem::swap(&mut frame, &mut orig); - } - - if !options.no_sharp { - sharpen(&orig, &mut frame); - std::mem::swap(&mut frame, &mut orig); - } - - let bitmap = if 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) - }; - + let frame = capturer.get_next_frame().expect("failed to capture frame"); + let frame = frame_to_image(frame); + let bitmap = pipeline.process(frame); connection .send(Command::BitmapLinearWin( Origin::ZERO, @@ -59,20 +43,6 @@ pub fn stream_window(connection: &Connection, options: StreamScreenOptions) { } } -/// returns next frame from the capturer, resized and grayscale -fn get_next_frame(capturer: &Capturer) -> ImageBuffer, Vec> { - let frame = capturer.get_next_frame().expect("failed to capture frame"); - let frame = frame_to_image(frame); - let frame = frame.grayscale().to_luma8(); - - resize( - &frame, - PIXEL_WIDTH as u32, - PIXEL_HEIGHT as u32, - FilterType::Nearest, - ) -} - fn start_capture(options: &StreamScreenOptions) -> Option { if !scap::is_supported() { error!("platform not supported by scap"); From b1c3ac85381fcf12f6add59b1a1dd03e935da9ac Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sat, 1 Mar 2025 11:51:08 +0100 Subject: [PATCH 11/16] add send image command --- README.md | 26 +++++++++++++++++++++++--- src/cli.rs | 31 +++++++++++++++++++++++-------- src/image_processing.rs | 16 +++++++++------- src/main.rs | 5 ++++- src/pixels.rs | 28 +++++++++++++++++++++++++--- src/stream_window.rs | 30 ++++++++++++++++-------------- 6 files changed, 100 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 90ab17a..6cf3ca7 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,8 @@ Options: ### Stream ``` +Continuously send data to the display + Usage: servicepoint-cli stream Commands: @@ -108,9 +110,27 @@ Commands for manipulating pixels Usage: servicepoint-cli pixels Commands: - off Reset all pixels to the default (off) state [aliases: r, reset, clear] - invert Invert the state of all pixels [aliases: i] - on Set all pixels to the on state + 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] +``` + +#### Image + +``` +Send an image file (e.g. jpeg or png) to the display. + +Usage: servicepoint-cli pixels image [OPTIONS] + +Arguments: + + +Options: + --no-hist Disable histogram correction + --no-blur Disable blur + --no-sharp Disable sharpening + --no-dither Disable dithering. Brightness will be adjusted so that around half of the pixels are on. ``` ## Contributing diff --git a/src/cli.rs b/src/cli.rs index db64ca7..664916d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -57,10 +57,20 @@ pub enum PixelCommand { 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, + }, } #[derive(clap::Parser, std::fmt::Debug)] @@ -91,7 +101,7 @@ pub enum Protocol { #[derive(clap::Parser, std::fmt::Debug)] #[clap(about = "Continuously send data to the display")] pub enum StreamCommand { - #[clap( + #[command( about = "Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin`" )] Stdin { @@ -103,12 +113,14 @@ pub enum StreamCommand { )] slow: bool, }, - #[clap(about = "Stream the default source to the display. \ + #[command(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, + stream_options: StreamScreenOptions, + #[command(flatten)] + image_processing: ImageProcessingOptions, }, } @@ -121,9 +133,6 @@ pub struct StreamScreenOptions { help = "Show mouse pointer in video feed" )] pub pointer: bool, - - #[transparent] - pub image_processing: ImageProcessingOptions, } #[derive(clap::Parser, std::fmt::Debug, Clone)] @@ -143,3 +152,9 @@ pub struct ImageProcessingOptions { )] pub no_dither: bool, } + +#[derive(clap::Parser, std::fmt::Debug, Clone)] +pub struct SendImageOptions { + #[arg()] + pub file_name: String, +} diff --git a/src/image_processing.rs b/src/image_processing.rs index f36bdc9..2b3b1b7 100644 --- a/src/image_processing.rs +++ b/src/image_processing.rs @@ -1,9 +1,11 @@ -use crate::cli::ImageProcessingOptions; -use crate::ledwand_dither::{ - blur, histogram_correction, median_brightness, ostromoukhov_dither, sharpen, +use crate::{ + cli::ImageProcessingOptions, + ledwand_dither::{blur, histogram_correction, median_brightness, ostromoukhov_dither, sharpen}, +}; +use image::{ + imageops::{resize, FilterType}, + DynamicImage, ImageBuffer, Luma, }; -use image::imageops::{resize, FilterType}; -use image::{imageops, DynamicImage, ImageBuffer, Luma}; use servicepoint::{Bitmap, PIXEL_HEIGHT, PIXEL_WIDTH}; pub struct ImageProcessingPipeline { @@ -22,7 +24,7 @@ impl ImageProcessingPipeline { } fn resize_grayscale(frame: &DynamicImage) -> ImageBuffer, Vec> { - let frame = imageops::grayscale(&frame); + let frame = frame.grayscale().to_luma8(); let frame = resize( &frame, PIXEL_WIDTH as u32, @@ -54,7 +56,7 @@ impl ImageProcessingPipeline { orig } - fn grayscale_to_bitmap(&self, mut orig: ImageBuffer, Vec>) -> Bitmap { + fn grayscale_to_bitmap(&self, orig: ImageBuffer, Vec>) -> Bitmap { if self.options.no_dither { let cutoff = median_brightness(&orig); let bits = orig.iter().map(move |x| x > &cutoff).collect(); diff --git a/src/main.rs b/src/main.rs index 2a86e33..a23a130 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,7 +38,10 @@ pub fn execute_mode(mode: Mode, connection: Connection) { 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), + StreamCommand::Screen { + stream_options, + image_processing, + } => stream_window(&connection, stream_options, image_processing), }, } } diff --git a/src/pixels.rs b/src/pixels.rs index 4e6e3cf..ea0242e 100644 --- a/src/pixels.rs +++ b/src/pixels.rs @@ -1,12 +1,17 @@ -use crate::cli::PixelCommand; +use crate::cli::{ImageProcessingOptions, PixelCommand, SendImageOptions}; +use crate::image_processing::ImageProcessingPipeline; use log::info; -use servicepoint::{BitVec, Command, CompressionCode, Connection, PIXEL_COUNT}; +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::Invert => pixels_invert(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), } } @@ -32,3 +37,20 @@ pub(crate) fn pixels_off(connection: &Connection) { .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 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"); +} diff --git a/src/stream_window.rs b/src/stream_window.rs index da8d866..c89368c 100644 --- a/src/stream_window.rs +++ b/src/stream_window.rs @@ -1,12 +1,6 @@ -use crate::{ - cli::{ImageProcessingOptions, StreamScreenOptions}, - image_processing::ImageProcessingPipeline, - ledwand_dither::*, -}; -use image::{ - imageops::{resize, FilterType}, - DynamicImage, ImageBuffer, Luma, Rgb, Rgba, -}; +use crate::cli::{ImageProcessingOptions, StreamScreenOptions}; +use crate::image_processing::ImageProcessingPipeline; +use image::{DynamicImage, ImageBuffer, Rgb, Rgba}; use log::{error, info, warn}; use scap::{ capturer::{Capturer, Options}, @@ -14,19 +8,26 @@ use scap::{ frame::Frame, }; use servicepoint::{ - Bitmap, Command, CompressionCode, Connection, Origin, FRAME_PACING, PIXEL_HEIGHT, PIXEL_WIDTH, - TILE_HEIGHT, TILE_SIZE, + Command, CompressionCode, Connection, Origin, FRAME_PACING, PIXEL_HEIGHT, TILE_HEIGHT, + TILE_SIZE, }; use std::time::Duration; -pub fn stream_window(connection: &Connection, options: StreamScreenOptions) { +const SPACER_HEIGHT: usize = TILE_SIZE / 2; +const PIXEL_HEIGHT_INCLUDING_SPACERS: usize = SPACER_HEIGHT * (TILE_HEIGHT - 1) + PIXEL_HEIGHT; + +pub fn stream_window( + connection: &Connection, + options: StreamScreenOptions, + processing_options: ImageProcessingOptions, +) { info!("Starting capture with options: {:?}", options); let capturer = match start_capture(&options) { Some(value) => value, None => return, }; - let pipeline = ImageProcessingPipeline::new(options.image_processing); + let pipeline = ImageProcessingPipeline::new(processing_options); info!("now starting to stream images"); loop { @@ -57,10 +58,11 @@ fn start_capture(options: &StreamScreenOptions) -> Option { } } + // all options are more like a suggestion let mut capturer = Capturer::build(Options { fps: FRAME_PACING.div_duration_f32(Duration::from_secs(1)) as u32, show_cursor: options.pointer, - output_type: scap::frame::FrameType::BGR0, // this is more like a suggestion + output_type: scap::frame::FrameType::BGR0, ..Default::default() }) .expect("failed to create screen capture"); From 0521e103ec68848f851663a880cacc54d4880be8 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sat, 1 Mar 2025 12:53:16 +0100 Subject: [PATCH 12/16] remove spacers in image processing --- src/cli.rs | 3 +++ src/image_processing.rs | 60 +++++++++++++++++++++++++++++++++++------ src/pixels.rs | 1 + src/stream_window.rs | 14 ++++------ 4 files changed, 61 insertions(+), 17 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 664916d..0f8dc8a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -151,6 +151,9 @@ pub struct ImageProcessingOptions { 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, } #[derive(clap::Parser, std::fmt::Debug, Clone)] diff --git a/src/image_processing.rs b/src/image_processing.rs index 2b3b1b7..4c06e68 100644 --- a/src/image_processing.rs +++ b/src/image_processing.rs @@ -6,32 +6,57 @@ use image::{ imageops::{resize, FilterType}, DynamicImage, ImageBuffer, Luma, }; -use servicepoint::{Bitmap, PIXEL_HEIGHT, PIXEL_WIDTH}; +use log::{debug, trace}; +use servicepoint::{Bitmap, Grid, PIXEL_HEIGHT, PIXEL_WIDTH, TILE_HEIGHT, TILE_SIZE}; +use std::time::Instant; +#[derive(Debug)] pub struct ImageProcessingPipeline { options: ImageProcessingOptions, } +const SPACER_HEIGHT: usize = TILE_SIZE / 2; +const PIXEL_HEIGHT_INCLUDING_SPACERS: usize = SPACER_HEIGHT * (TILE_HEIGHT - 1) + PIXEL_HEIGHT; + impl ImageProcessingPipeline { pub fn new(options: ImageProcessingOptions) -> Self { + debug!("Creating image pipeline: {:?}", options); Self { options } } pub fn process(&self, frame: DynamicImage) -> Bitmap { - let frame = Self::resize_grayscale(&frame); + let start_time = Instant::now(); + + let frame = self.resize_grayscale(frame); let frame = self.grayscale_processing(frame); - self.grayscale_to_bitmap(frame) + let mut result = self.grayscale_to_bitmap(frame); + + if !self.options.no_spacers { + result = Self::remove_spacers(result); + } + + trace!("image processing took {:?}", start_time.elapsed()); + result } - fn resize_grayscale(frame: &DynamicImage) -> ImageBuffer, Vec> { + fn resize_grayscale(&self, frame: DynamicImage) -> ImageBuffer, Vec> { + // TODO: keep aspect ratio + // TODO: make it work for non-maximum sizes + let frame = frame.grayscale().to_luma8(); - let frame = resize( + + let target_height = if self.options.no_spacers { + PIXEL_HEIGHT + } else { + PIXEL_HEIGHT_INCLUDING_SPACERS + }; + + resize( &frame, PIXEL_WIDTH as u32, - PIXEL_HEIGHT as u32, + target_height as u32, FilterType::Nearest, - ); - frame + ) } fn grayscale_processing( @@ -65,4 +90,23 @@ impl ImageProcessingPipeline { ostromoukhov_dither(orig, u8::MAX / 2) } } + + fn remove_spacers(bitmap: Bitmap) -> Bitmap { + let mut result = Bitmap::max_sized(); + + let mut source_y = 0; + for result_y in 0..result.height() { + if result_y != 0 && result_y % TILE_SIZE == 0 { + source_y += 4; + } + + for x in 0..result.width() { + result.set(x, result_y, bitmap.get(x, source_y)); + } + + source_y += 1; + } + + result + } } diff --git a/src/pixels.rs b/src/pixels.rs index ea0242e..a4fece0 100644 --- a/src/pixels.rs +++ b/src/pixels.rs @@ -53,4 +53,5 @@ fn pixels_image( CompressionCode::default(), )) .expect("failed to send image command"); + info!("sent image to display"); } diff --git a/src/stream_window.rs b/src/stream_window.rs index c89368c..e66cf26 100644 --- a/src/stream_window.rs +++ b/src/stream_window.rs @@ -1,5 +1,7 @@ -use crate::cli::{ImageProcessingOptions, StreamScreenOptions}; -use crate::image_processing::ImageProcessingPipeline; +use crate::{ + cli::{ImageProcessingOptions, StreamScreenOptions}, + image_processing::ImageProcessingPipeline, +}; use image::{DynamicImage, ImageBuffer, Rgb, Rgba}; use log::{error, info, warn}; use scap::{ @@ -7,15 +9,9 @@ use scap::{ frame::convert_bgra_to_rgb, frame::Frame, }; -use servicepoint::{ - Command, CompressionCode, Connection, Origin, FRAME_PACING, PIXEL_HEIGHT, TILE_HEIGHT, - TILE_SIZE, -}; +use servicepoint::{Command, CompressionCode, Connection, Origin, FRAME_PACING}; use std::time::Duration; -const SPACER_HEIGHT: usize = TILE_SIZE / 2; -const PIXEL_HEIGHT_INCLUDING_SPACERS: usize = SPACER_HEIGHT * (TILE_HEIGHT - 1) + PIXEL_HEIGHT; - pub fn stream_window( connection: &Connection, options: StreamScreenOptions, From a1fa13b6e557590c9c66e2f78f4a6f214f174ce9 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sun, 2 Mar 2025 01:26:09 +0100 Subject: [PATCH 13/16] fast resize, now higher quality; keep aspect ratio --- Cargo.lock | 30 ++++++++++ Cargo.toml | 1 + src/image_processing.rs | 126 ++++++++++++++++++++++++++++------------ src/ledwand_dither.rs | 30 +++++----- src/pixels.rs | 2 +- src/stream_window.rs | 31 +++++++--- 6 files changed, 160 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19ef7a5..b9ad7ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -557,6 +557,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + [[package]] name = "either" version = "1.14.0" @@ -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" @@ -1025,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" @@ -1690,6 +1719,7 @@ version = "0.3.0" dependencies = [ "clap", "env_logger", + "fast_image_resize", "image", "log", "scap", diff --git a/Cargo.toml b/Cargo.toml index 54edfe6..a8cd60f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,4 @@ env_logger = "0.11" log = "0.4" scap = "0.0.8" image = "0.25.5" +fast_image_resize = { version = "5.1.2", features = ["image"] } diff --git a/src/image_processing.rs b/src/image_processing.rs index 4c06e68..bcc3104 100644 --- a/src/image_processing.rs +++ b/src/image_processing.rs @@ -2,29 +2,39 @@ use crate::{ cli::ImageProcessingOptions, ledwand_dither::{blur, histogram_correction, median_brightness, ostromoukhov_dither, sharpen}, }; -use image::{ - imageops::{resize, FilterType}, - DynamicImage, ImageBuffer, Luma, -}; +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::time::Instant; +use std::{default::Default, time::Instant}; #[derive(Debug)] pub struct ImageProcessingPipeline { options: ImageProcessingOptions, + resizer: Resizer, + render_size: (usize, usize), } const SPACER_HEIGHT: usize = TILE_SIZE / 2; -const PIXEL_HEIGHT_INCLUDING_SPACERS: usize = SPACER_HEIGHT * (TILE_HEIGHT - 1) + PIXEL_HEIGHT; impl ImageProcessingPipeline { pub fn new(options: ImageProcessingOptions) -> Self { debug!("Creating image pipeline: {:?}", options); - Self { options } + + let spacers_height = if options.no_spacers { + 0 + } else { + SPACER_HEIGHT * (TILE_HEIGHT - 1) + }; + + Self { + options, + resizer: Resizer::new(), + render_size: (PIXEL_WIDTH, PIXEL_HEIGHT + spacers_height), + } } - pub fn process(&self, frame: DynamicImage) -> Bitmap { + pub fn process(&mut self, frame: DynamicImage) -> Bitmap { let start_time = Instant::now(); let frame = self.resize_grayscale(frame); @@ -35,34 +45,31 @@ impl ImageProcessingPipeline { result = Self::remove_spacers(result); } - trace!("image processing took {:?}", start_time.elapsed()); + trace!("pipeline took {:?}", start_time.elapsed()); result } - fn resize_grayscale(&self, frame: DynamicImage) -> ImageBuffer, Vec> { - // TODO: keep aspect ratio - // TODO: make it work for non-maximum sizes + fn resize_grayscale(&mut self, frame: DynamicImage) -> GrayImage { + let start_time = Instant::now(); - let frame = frame.grayscale().to_luma8(); + let (scaled_width, scaled_height) = self.fit_size((frame.width(), frame.height())); + let mut dst_image = DynamicImage::new(scaled_width, scaled_height, frame.color()); - let target_height = if self.options.no_spacers { - PIXEL_HEIGHT - } else { - PIXEL_HEIGHT_INCLUDING_SPACERS - }; + self.resizer + .resize(&frame, &mut dst_image, &ResizeOptions::default()) + .expect("image resize failed"); - resize( - &frame, - PIXEL_WIDTH as u32, - target_height as u32, - FilterType::Nearest, - ) + 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: ImageBuffer, Vec>, - ) -> ImageBuffer, Vec> { + fn grayscale_processing(&self, mut frame: GrayImage) -> GrayImage { + let start_time = Instant::now(); if !self.options.no_hist { histogram_correction(&mut frame); } @@ -78,35 +85,78 @@ impl ImageProcessingPipeline { 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: ImageBuffer, Vec>) -> Bitmap { - if self.options.no_dither { + 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(bitmap: Bitmap) -> Bitmap { - let mut result = Bitmap::max_sized(); + fn remove_spacers(source: Bitmap) -> Bitmap { + let start_time = Instant::now(); + + let full_tile_rows_with_spacers = source.height() / (TILE_SIZE + SPACER_HEIGHT); + let remaining_pixel_rows = source.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 = source.height() - total_spacer_height; + trace!( + "spacers take up {total_spacer_height}, resulting in height {height_without_spacers}" + ); + + let mut result = Bitmap::new(source.width(), height_without_spacers); let mut source_y = 0; for result_y in 0..result.height() { - if result_y != 0 && result_y % TILE_SIZE == 0 { - source_y += 4; - } - for x in 0..result.width() { - result.set(x, result_y, bitmap.get(x, source_y)); + 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 fit_size(&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, 0); + + let width_scale = target_width as f32 / source_width as f32; + let height_scale = target_height as f32 / source_height as f32; + let scale = f32::min(width_scale, height_scale); + + let height = (source_height as f32 * scale) as u32; + let mut width = (source_width as f32 * scale) as u32; + + if width % TILE_SIZE as u32 != 0 { + // because we do not have many pixels, round up even if it is a worse fit + width += 8 - width % 8; + } + + let result = (width, height); + trace!( + "scaling {:?} to {:?} to fit {:?}", + source, + result, + self.render_size + ); result } } diff --git a/src/ledwand_dither.rs b/src/ledwand_dither.rs index be1091e..6dbf370 100644 --- a/src/ledwand_dither.rs +++ b/src/ledwand_dither.rs @@ -174,11 +174,11 @@ pub(crate) fn ostromoukhov_dither(source: GrayImage, bias: u8) -> Bitmap { for y in 0..height as usize { let start = y * width as usize; if y % 2 == 0 { - for x in 0..width as usize { + for x in start..start + width as usize { ostromoukhov_dither_pixel( &mut source, &mut destination, - start + x, + x, width as usize, y == (height - 1) as usize, 1, @@ -186,11 +186,11 @@ pub(crate) fn ostromoukhov_dither(source: GrayImage, bias: u8) -> Bitmap { ); } } else { - for x in (0..width as usize).rev() { + for x in (start..start + width as usize).rev() { ostromoukhov_dither_pixel( &mut source, &mut destination, - start + x, + x, width as usize, y == (height - 1) as usize, -1, @@ -213,17 +213,9 @@ fn ostromoukhov_dither_pixel( direction: isize, bias: u8, ) { - let old_pixel = source[position]; - - let destination_value = old_pixel > bias; + let (destination_value, error) = gray_to_bit(source[position], bias); destination.set(position, destination_value); - - let error = if destination_value { - 255 - old_pixel - } else { - old_pixel - }; - + 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; @@ -245,6 +237,16 @@ fn ostromoukhov_dither_pixel( } } +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], diff --git a/src/pixels.rs b/src/pixels.rs index a4fece0..c92be07 100644 --- a/src/pixels.rs +++ b/src/pixels.rs @@ -44,7 +44,7 @@ fn pixels_image( processing_options: ImageProcessingOptions, ) { let image = image::open(&options.file_name).expect("failed to open image file"); - let pipeline = ImageProcessingPipeline::new(processing_options); + let mut pipeline = ImageProcessingPipeline::new(processing_options); let bitmap = pipeline.process(image); connection .send(Command::BitmapLinearWin( diff --git a/src/stream_window.rs b/src/stream_window.rs index e66cf26..f8e41ec 100644 --- a/src/stream_window.rs +++ b/src/stream_window.rs @@ -3,14 +3,14 @@ use crate::{ image_processing::ImageProcessingPipeline, }; use image::{DynamicImage, ImageBuffer, Rgb, Rgba}; -use log::{error, info, warn}; +use log::{debug, error, info, trace, warn}; use scap::{ capturer::{Capturer, Options}, frame::convert_bgra_to_rgb, frame::Frame, }; use servicepoint::{Command, CompressionCode, Connection, Origin, FRAME_PACING}; -use std::time::Duration; +use std::time::{Duration, Instant}; pub fn stream_window( connection: &Connection, @@ -23,20 +23,27 @@ pub fn stream_window( None => return, }; - let pipeline = ImageProcessingPipeline::new(processing_options); + let mut pipeline = ImageProcessingPipeline::new(processing_options); info!("now starting to stream images"); loop { - let frame = capturer.get_next_frame().expect("failed to capture frame"); + 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"); + + debug!("frame time: {:?}", start.elapsed()); } } @@ -66,8 +73,16 @@ fn start_capture(options: &StreamScreenOptions) -> Option { Some(capturer) } +fn capture_frame(capturer: &Capturer) -> Frame { + let start_time = Instant::now(); + let result = capturer.get_next_frame().expect("failed to capture frame"); + trace!("capture took: {:?}", start_time.elapsed()); + result +} + fn frame_to_image(frame: Frame) -> DynamicImage { - match frame { + let start_time = Instant::now(); + let result = match frame { Frame::BGRx(frame) => bgrx_to_rgb(frame.width, frame.height, frame.data), Frame::RGBx(frame) => DynamicImage::from( ImageBuffer::, _>::from_raw( @@ -84,7 +99,9 @@ fn frame_to_image(frame: Frame) -> DynamicImage { ), Frame::BGRA(frame) => bgrx_to_rgb(frame.width, frame.height, frame.data), Frame::YUVFrame(_) | Frame::XBGR(_) => panic!("unsupported frame format"), - } + }; + trace!("conversion to image took: {:?}", start_time.elapsed()); + result } fn bgrx_to_rgb(width: i32, height: i32, data: Vec) -> DynamicImage { From 0ac6b77ed088b10d0035e25e3f900865b268dcd7 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sun, 2 Mar 2025 13:46:06 +0100 Subject: [PATCH 14/16] keep aspect is optional --- README.md | 22 ++++++++++------- src/cli.rs | 3 +++ src/image_processing.rs | 54 ++++++++++++++++++++++++----------------- src/ledwand_dither.rs | 2 +- 4 files changed, 49 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 6cf3ca7..873fd0f 100644 --- a/README.md +++ b/README.md @@ -71,11 +71,13 @@ Stream the default source to the display. On Linux Wayland, this pops up a scree Usage: servicepoint-cli stream screen [OPTIONS] Options: - -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. + -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. ``` #### Stdin @@ -127,10 +129,12 @@ Arguments: Options: - --no-hist Disable histogram correction - --no-blur Disable blur - --no-sharp Disable sharpening - --no-dither Disable dithering. Brightness will be adjusted so that around half of the pixels are on. + --no-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. ``` ## Contributing diff --git a/src/cli.rs b/src/cli.rs index 0f8dc8a..a919664 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -154,6 +154,9 @@ pub struct ImageProcessingOptions { #[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)] diff --git a/src/image_processing.rs b/src/image_processing.rs index bcc3104..4e6baa8 100644 --- a/src/image_processing.rs +++ b/src/image_processing.rs @@ -12,7 +12,7 @@ use std::{default::Default, time::Instant}; pub struct ImageProcessingPipeline { options: ImageProcessingOptions, resizer: Resizer, - render_size: (usize, usize), + render_size: (u32, u32), } const SPACER_HEIGHT: usize = TILE_SIZE / 2; @@ -21,16 +21,17 @@ impl ImageProcessingPipeline { pub fn new(options: ImageProcessingOptions) -> Self { debug!("Creating image pipeline: {:?}", options); - let spacers_height = if options.no_spacers { - 0 - } else { - SPACER_HEIGHT * (TILE_HEIGHT - 1) - }; + let height = PIXEL_HEIGHT + + if options.no_spacers { + 0 + } else { + SPACER_HEIGHT * (TILE_HEIGHT - 1) + }; Self { options, resizer: Resizer::new(), - render_size: (PIXEL_WIDTH, PIXEL_HEIGHT + spacers_height), + render_size: (PIXEL_WIDTH as u32, height as u32), } } @@ -52,7 +53,11 @@ impl ImageProcessingPipeline { fn resize_grayscale(&mut self, frame: DynamicImage) -> GrayImage { let start_time = Instant::now(); - let (scaled_width, scaled_height) = self.fit_size((frame.width(), frame.height())); + 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 @@ -106,20 +111,13 @@ impl ImageProcessingPipeline { fn remove_spacers(source: Bitmap) -> Bitmap { let start_time = Instant::now(); - let full_tile_rows_with_spacers = source.height() / (TILE_SIZE + SPACER_HEIGHT); - let remaining_pixel_rows = source.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 = source.height() - total_spacer_height; - trace!( - "spacers take up {total_spacer_height}, resulting in height {height_without_spacers}" - ); - - let mut result = Bitmap::new(source.width(), height_without_spacers); + 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..result.width() { + for result_y in 0..result_height { + for x in 0..width { result.set(x, result_y, source.get(x, source_y)); } @@ -133,10 +131,22 @@ impl ImageProcessingPipeline { result } - fn fit_size(&self, source: (u32, u32)) -> (u32, u32) { + 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, 0); + 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; diff --git a/src/ledwand_dither.rs b/src/ledwand_dither.rs index 6dbf370..d0e4b43 100644 --- a/src/ledwand_dither.rs +++ b/src/ledwand_dither.rs @@ -215,7 +215,7 @@ fn ostromoukhov_dither_pixel( ) { 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; From 11d9ac0bcb773a613c642437cc2f51a06462b6a6 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sun, 2 Mar 2025 14:09:04 +0100 Subject: [PATCH 15/16] restructure cli --- README.md | 117 ++++++++++++++++++++++---------------------- src/cli.rs | 31 ++++++------ src/main.rs | 14 ++---- src/pixels.rs | 11 ++++- src/stream_stdin.rs | 8 +-- src/text.rs | 7 +++ 6 files changed, 100 insertions(+), 88 deletions(-) create mode 100644 src/text.rs diff --git a/README.md b/README.md index 873fd0f..be1dab7 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Commands: reset-everything Reset both pixels and brightness [aliases: r] pixels Commands for manipulating pixels [aliases: p] brightness Commands for manipulating the brightness [aliases: b] - stream Continuously send data to the display [aliases: s] + text Commands for sending text to the screen [aliases: t] help Print this message or the help of the given subcommand(s) Options: @@ -51,59 +51,6 @@ Options: -V, --version Print version ``` -### Stream - -``` -Continuously send data to the display - -Usage: servicepoint-cli stream - -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. -``` - -#### 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. - -Usage: servicepoint-cli stream screen [OPTIONS] - -Options: - -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. -``` - -#### 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 -``` - -### Brightness - -``` -Commands for manipulating the brightness - -Usage: servicepoint-cli brightness - -Commands: - max Reset brightness to the default (max) level [aliases: r, reset] - set Set one brightness for the whole screen [aliases: s] - min Set brightness to lowest possible level. -``` - ### Pixels ``` @@ -112,10 +59,11 @@ Commands for manipulating pixels Usage: servicepoint-cli pixels Commands: - 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] + 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 @@ -137,6 +85,59 @@ Options: --no-aspect Do not keep aspect ratio when resizing. ``` +#### 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. + +Usage: servicepoint-cli pixels screen [OPTIONS] + +Options: + -p, --pointer Show mouse pointer in video feed + --no-hist Disable histogram correction + --no-blur Disable blur + --no-sharp Disable sharpening + --no-dither Disable dithering. Brightness will be adjusted so that around half of the pixels are on. + --no-spacers Do not remove the spacers from the image. + --no-aspect Do not keep aspect ratio when resizing. +``` + +### Brightness + +``` +Commands for manipulating the brightness + +Usage: servicepoint-cli brightness + +Commands: + max Reset brightness to the default (max) level [aliases: r, reset] + set Set one brightness for the whole screen [aliases: s] + min Set brightness to lowest possible level. +``` + +### Text + +``` +Commands for sending text to the screen + +Usage: servicepoint-cli text + +Commands: + stdin Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin` +``` + +#### Stdin + +``` +Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin` + +Usage: servicepoint-cli stream stdin [OPTIONS] + +Options: + -s, --slow Wait for a short amount of time before sending the next line +``` + + ## Contributing If you have ideas on how to improve the code, add features or improve documentation feel free to open a pull request. diff --git a/src/cli.rs b/src/cli.rs index a919664..365ff5a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -40,10 +40,10 @@ pub enum Mode { #[clap(subcommand)] brightness_command: BrightnessCommand, }, - #[command(visible_alias = "s")] - Stream { + #[command(visible_alias = "t")] + Text { #[clap(subcommand)] - stream_command: StreamCommand, + text_command: TextCommand, }, } @@ -71,6 +71,18 @@ pub enum PixelCommand { #[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)] @@ -99,8 +111,8 @@ pub enum Protocol { } #[derive(clap::Parser, std::fmt::Debug)] -#[clap(about = "Continuously send data to the display")] -pub enum StreamCommand { +#[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`" )] @@ -113,15 +125,6 @@ pub enum StreamCommand { )] slow: bool, }, - #[command(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)] - stream_options: StreamScreenOptions, - #[command(flatten)] - image_processing: ImageProcessingOptions, - }, } #[derive(clap::Parser, std::fmt::Debug, Clone)] diff --git a/src/main.rs b/src/main.rs index a23a130..4ca1df4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,8 @@ use crate::{ brightness::{brightness, brightness_set}, - cli::{Cli, Mode, Protocol, StreamCommand}, + cli::{Cli, Mode, Protocol}, pixels::{pixels, pixels_off}, - stream_stdin::stream_stdin, - stream_window::stream_window, + text::text }; use clap::Parser; use log::debug; @@ -16,6 +15,7 @@ mod ledwand_dither; mod pixels; mod stream_stdin; mod stream_window; +mod text; fn main() { let cli = Cli::parse(); @@ -36,13 +36,7 @@ pub fn execute_mode(mode: Mode, connection: 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 { - stream_options, - image_processing, - } => stream_window(&connection, stream_options, image_processing), - }, + Mode::Text { text_command} => text(&connection, text_command), } } diff --git a/src/pixels.rs b/src/pixels.rs index c92be07..da1aa6d 100644 --- a/src/pixels.rs +++ b/src/pixels.rs @@ -1,5 +1,8 @@ -use crate::cli::{ImageProcessingOptions, PixelCommand, SendImageOptions}; -use crate::image_processing::ImageProcessingPipeline; +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}; @@ -12,6 +15,10 @@ pub(crate) fn pixels(connection: &Connection, pixel_command: PixelCommand) { 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), } } diff --git a/src/stream_stdin.rs b/src/stream_stdin.rs index 82109ba..0bd7c04 100644 --- a/src/stream_stdin.rs +++ b/src/stream_stdin.rs @@ -2,7 +2,7 @@ use log::warn; use servicepoint::*; use std::thread::sleep; -pub(crate) fn stream_stdin(connection: Connection, slow: bool) { +pub(crate) fn stream_stdin(connection: &Connection, slow: bool) { warn!("This mode will break when using multi-byte characters and does not support ANSI escape sequences yet."); let mut app = App { connection, @@ -13,14 +13,14 @@ pub(crate) fn stream_stdin(connection: Connection, slow: bool) { app.run() } -struct App { - connection: Connection, +struct App<'t> { + connection: &'t Connection, mirror: CharGrid, y: usize, slow: bool, } -impl App { +impl App<'_> { fn run(&mut self) { self.connection .send(Command::Clear) diff --git a/src/text.rs b/src/text.rs new file mode 100644 index 0000000..247b9ad --- /dev/null +++ b/src/text.rs @@ -0,0 +1,7 @@ +use servicepoint::Connection; +use crate::cli::TextCommand; +use crate::stream_stdin::stream_stdin; + +pub fn text(connection: &Connection, command: TextCommand) { + match command { TextCommand::Stdin { slow } => stream_stdin(connection, slow), } +} From 6eee677ed4ae63fde651eaf29c9fc6a120b9c83c Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sun, 2 Mar 2025 15:06:39 +0100 Subject: [PATCH 16/16] send stdin as UTF instead of CP437 --- src/stream_stdin.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stream_stdin.rs b/src/stream_stdin.rs index 0bd7c04..b8b6cfb 100644 --- a/src/stream_stdin.rs +++ b/src/stream_stdin.rs @@ -63,9 +63,9 @@ 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"); }