From b1c3ac85381fcf12f6add59b1a1dd03e935da9ac Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sat, 1 Mar 2025 11:51:08 +0100 Subject: [PATCH] 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");