add send image command

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

View file

@ -54,6 +54,8 @@ Options:
### Stream
```
Continuously send data to the display
Usage: servicepoint-cli stream <COMMAND>
Commands:
@ -108,9 +110,27 @@ Commands for manipulating pixels
Usage: servicepoint-cli pixels <COMMAND>
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] <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

View file

@ -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,
}

View file

@ -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<Luma<u8>, Vec<u8>> {
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<Luma<u8>, Vec<u8>>) -> Bitmap {
fn grayscale_to_bitmap(&self, orig: ImageBuffer<Luma<u8>, Vec<u8>>) -> Bitmap {
if self.options.no_dither {
let cutoff = median_brightness(&orig);
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::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),
},
}
}

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 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");
}

View file

@ -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<Capturer> {
}
}
// all options are more like a suggestion
let mut capturer = Capturer::build(Options {
fps: FRAME_PACING.div_duration_f32(Duration::from_secs(1)) as u32,
show_cursor: options.pointer,
output_type: scap::frame::FrameType::BGR0, // this is more like a suggestion
output_type: scap::frame::FrameType::BGR0,
..Default::default()
})
.expect("failed to create screen capture");