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