add send image command

This commit is contained in:
Vinzenz Schroeter 2025-03-01 11:51:08 +01:00
parent 4c017895e6
commit e170835d13
6 changed files with 100 additions and 36 deletions

View file

@ -54,6 +54,8 @@ Options:
### Stream ### Stream
``` ```
Continuously send data to the display
Usage: servicepoint-cli stream <COMMAND> Usage: servicepoint-cli stream <COMMAND>
Commands: Commands:
@ -108,9 +110,27 @@ Commands for manipulating pixels
Usage: servicepoint-cli pixels <COMMAND> Usage: servicepoint-cli pixels <COMMAND>
Commands: Commands:
off Reset all pixels to the default (off) state [aliases: r, reset, clear] off Reset all pixels to the default (off) state [aliases: r, reset, clear]
invert Invert the state of all pixels [aliases: i] flip Invert the state of all pixels [aliases: f]
on Set all pixels to the on state 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] <FILE_NAME>
Arguments:
<FILE_NAME>
Options:
--no-hist Disable histogram correction
--no-blur Disable blur
--no-sharp Disable sharpening
--no-dither Disable dithering. Brightness will be adjusted so that around half of the pixels are on.
``` ```
## Contributing ## Contributing

View file

@ -57,10 +57,20 @@ pub enum PixelCommand {
about = "Reset all pixels to the default (off) state" about = "Reset all pixels to the default (off) state"
)] )]
Off, Off,
#[command(visible_alias = "i", about = "Invert the state of all pixels")] #[command(visible_alias = "f", about = "Invert the state of all pixels")]
Invert, Flip,
#[command(about = "Set all pixels to the on state")] #[command(about = "Set all pixels to the on state")]
On, On,
#[command(
visible_alias = "i",
about = "Send an image file (e.g. jpeg or png) to the display."
)]
Image {
#[command(flatten)]
send_image_options: SendImageOptions,
#[command(flatten)]
image_processing_options: ImageProcessingOptions,
},
} }
#[derive(clap::Parser, std::fmt::Debug)] #[derive(clap::Parser, std::fmt::Debug)]
@ -91,7 +101,7 @@ pub enum Protocol {
#[derive(clap::Parser, std::fmt::Debug)] #[derive(clap::Parser, std::fmt::Debug)]
#[clap(about = "Continuously send data to the display")] #[clap(about = "Continuously send data to the display")]
pub enum StreamCommand { pub enum StreamCommand {
#[clap( #[command(
about = "Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin`" about = "Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin`"
)] )]
Stdin { Stdin {
@ -103,12 +113,14 @@ pub enum StreamCommand {
)] )]
slow: bool, 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, \ On Linux Wayland, this pops up a screen or window chooser, \
but it also may directly start streaming your main screen.")] but it also may directly start streaming your main screen.")]
Screen { Screen {
#[command(flatten)] #[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" help = "Show mouse pointer in video feed"
)] )]
pub pointer: bool, pub pointer: bool,
#[transparent]
pub image_processing: ImageProcessingOptions,
} }
#[derive(clap::Parser, std::fmt::Debug, Clone)] #[derive(clap::Parser, std::fmt::Debug, Clone)]
@ -143,3 +152,9 @@ pub struct ImageProcessingOptions {
)] )]
pub no_dither: bool, pub no_dither: bool,
} }
#[derive(clap::Parser, std::fmt::Debug, Clone)]
pub struct SendImageOptions {
#[arg()]
pub file_name: String,
}

View file

@ -1,9 +1,11 @@
use crate::cli::ImageProcessingOptions; use crate::{
use crate::ledwand_dither::{ cli::ImageProcessingOptions,
blur, histogram_correction, median_brightness, ostromoukhov_dither, sharpen, 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}; use servicepoint::{Bitmap, PIXEL_HEIGHT, PIXEL_WIDTH};
pub struct ImageProcessingPipeline { pub struct ImageProcessingPipeline {
@ -22,7 +24,7 @@ impl ImageProcessingPipeline {
} }
fn resize_grayscale(frame: &DynamicImage) -> ImageBuffer<Luma<u8>, Vec<u8>> { fn resize_grayscale(frame: &DynamicImage) -> ImageBuffer<Luma<u8>, Vec<u8>> {
let frame = imageops::grayscale(&frame); let frame = frame.grayscale().to_luma8();
let frame = resize( let frame = resize(
&frame, &frame,
PIXEL_WIDTH as u32, PIXEL_WIDTH as u32,
@ -54,7 +56,7 @@ impl ImageProcessingPipeline {
orig orig
} }
fn grayscale_to_bitmap(&self, mut orig: ImageBuffer<Luma<u8>, Vec<u8>>) -> Bitmap { fn grayscale_to_bitmap(&self, orig: ImageBuffer<Luma<u8>, Vec<u8>>) -> Bitmap {
if self.options.no_dither { if self.options.no_dither {
let cutoff = median_brightness(&orig); let cutoff = median_brightness(&orig);
let bits = orig.iter().map(move |x| x > &cutoff).collect(); let bits = orig.iter().map(move |x| x > &cutoff).collect();

View file

@ -38,7 +38,10 @@ pub fn execute_mode(mode: Mode, connection: Connection) {
Mode::Brightness { brightness_command } => brightness(&connection, brightness_command), Mode::Brightness { brightness_command } => brightness(&connection, brightness_command),
Mode::Stream { stream_command } => match stream_command { Mode::Stream { stream_command } => match stream_command {
StreamCommand::Stdin { slow } => stream_stdin(connection, slow), 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),
}, },
} }
} }

View file

@ -1,12 +1,17 @@
use crate::cli::PixelCommand; use crate::cli::{ImageProcessingOptions, PixelCommand, SendImageOptions};
use crate::image_processing::ImageProcessingPipeline;
use log::info; 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) { pub(crate) fn pixels(connection: &Connection, pixel_command: PixelCommand) {
match pixel_command { match pixel_command {
PixelCommand::Off => pixels_off(connection), PixelCommand::Off => pixels_off(connection),
PixelCommand::Invert => pixels_invert(connection), PixelCommand::Flip => pixels_invert(connection),
PixelCommand::On => pixels_on(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"); .expect("failed to clear pixels");
info!("reset 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");
}

View file

@ -1,12 +1,6 @@
use crate::{ use crate::cli::{ImageProcessingOptions, StreamScreenOptions};
cli::{ImageProcessingOptions, StreamScreenOptions}, use crate::image_processing::ImageProcessingPipeline;
image_processing::ImageProcessingPipeline, use image::{DynamicImage, ImageBuffer, Rgb, Rgba};
ledwand_dither::*,
};
use image::{
imageops::{resize, FilterType},
DynamicImage, ImageBuffer, Luma, Rgb, Rgba,
};
use log::{error, info, warn}; use log::{error, info, warn};
use scap::{ use scap::{
capturer::{Capturer, Options}, capturer::{Capturer, Options},
@ -14,19 +8,26 @@ use scap::{
frame::Frame, frame::Frame,
}; };
use servicepoint::{ use servicepoint::{
Bitmap, Command, CompressionCode, Connection, Origin, FRAME_PACING, PIXEL_HEIGHT, PIXEL_WIDTH, Command, CompressionCode, Connection, Origin, FRAME_PACING, PIXEL_HEIGHT, TILE_HEIGHT,
TILE_HEIGHT, TILE_SIZE, TILE_SIZE,
}; };
use std::time::Duration; 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); info!("Starting capture with options: {:?}", options);
let capturer = match start_capture(&options) { let capturer = match start_capture(&options) {
Some(value) => value, Some(value) => value,
None => return, None => return,
}; };
let pipeline = ImageProcessingPipeline::new(options.image_processing); let pipeline = ImageProcessingPipeline::new(processing_options);
info!("now starting to stream images"); info!("now starting to stream images");
loop { loop {
@ -57,10 +58,11 @@ fn start_capture(options: &StreamScreenOptions) -> Option<Capturer> {
} }
} }
// all options are more like a suggestion
let mut capturer = Capturer::build(Options { let mut capturer = Capturer::build(Options {
fps: FRAME_PACING.div_duration_f32(Duration::from_secs(1)) as u32, fps: FRAME_PACING.div_duration_f32(Duration::from_secs(1)) as u32,
show_cursor: options.pointer, show_cursor: options.pointer,
output_type: scap::frame::FrameType::BGR0, // this is more like a suggestion output_type: scap::frame::FrameType::BGR0,
..Default::default() ..Default::default()
}) })
.expect("failed to create screen capture"); .expect("failed to create screen capture");