diff --git a/Cargo.lock b/Cargo.lock index 28a622f..89bb953 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1308,6 +1308,7 @@ dependencies = [ "epd-waveshare", "image", "linux-embedded-hal", + "mime", "minijinja", "palette", "serde", diff --git a/Cargo.toml b/Cargo.toml index 81c09f4..ef391f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ clap = { version = "4.5.7", features = ["derive"] } epd-waveshare = { git = "https://github.com/caemor/epd-waveshare.git"} image = "0.25.1" linux-embedded-hal = { version = "0.4.0"} +mime = "0.3.17" minijinja = "2.1.0" palette = "0.7.6" serde = { version = "1.0.204", features = ["derive"] } diff --git a/src/api.rs b/src/api.rs index 2e794ff..c590007 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,21 +1,23 @@ -use crate::imageproc::{DitherMethod, EInkImage, }; +use crate::display::EInkPanel; +use crate::imageproc::{DitherMethod, DitherPalette, EInkImage}; use axum::extract::Multipart; -use axum::http::StatusCode; +use axum::http::{header, StatusCode}; use axum::response::IntoResponse; use axum::{extract::State, response::Response, routing::post, Router}; -use image::{DynamicImage, ImageReader}; -use std::io::Cursor; -use std::time::Duration; -use tracing::{debug, error, info, instrument}; - -use crate::display::EInkPanel; +use image::{ImageReader, RgbImage}; +use std::io::{BufWriter, Cursor}; +use std::str; +use std::str::FromStr; use std::sync::Arc; +use std::time::Duration; use tokio::sync::mpsc::{self, Receiver, Sender}; use tokio::task::JoinHandle; +use tracing::{debug, error, info, instrument}; -pub enum ImageFormFields { - DitherType, - ImageFile, +#[derive(thiserror::Error, Debug)] +pub enum ApiError { + #[error("missing image field")] + MissingImage, } #[derive(Clone)] @@ -86,45 +88,89 @@ pub async fn display_task( pub fn router() -> Router { Router::new() .route("/setimage", post(set_image)) - .route("/process_image", post(process_image)) + .route("/preview", post(preview_image)) } #[derive(Debug)] -pub struct ImageRequest { - image: Box, - +struct ImageRequest { + image: Box, + dither_method: DitherMethod, + palette: DitherPalette, +} + +impl ImageRequest { + async fn from_multipart(mut parts: Multipart) -> Result { + let mut img = None; + let mut palette = None; + let mut dither_method = None; + while let Some(field) = parts.next_field().await? { + match field.name() { + Some("image") => { + let data = field.bytes().await?; + let reader = ImageReader::new(Cursor::new(data)) + .with_guessed_format() + .expect("cursor never fails"); + let image = reader.decode()?; + img = Some(Box::new(image.into())); + } + Some("palette") => { + let data = field.bytes().await?; + let val = str::from_utf8(&data)?; + palette = Some(DitherPalette::from_str(val)?); + } + Some("dither_method") => { + let data = field.bytes().await?; + let val = str::from_utf8(&data)?; + dither_method = Some(DitherMethod::from_str(val)?); + } + _ => {} + } + } + if let Some(i) = img { + Ok(Self { + image: i, + dither_method: dither_method.unwrap_or(DitherMethod::NearestNeighbor), + palette: palette.unwrap_or(DitherPalette::Default), + }) + } else { + Err(ApiError::MissingImage.into()) + } + } } #[instrument(skip(ctx))] -#[axum::debug_handler] async fn set_image( State(ctx): State, - mut parts: Multipart, + parts: Multipart, ) -> Result { - while let Some(field) = parts.next_field().await? { - let name = field.name().expect("fields always have names").to_string(); - let data = field.bytes().await?; - debug!("Length of `{}` is {} bytes", name, data.len()); - if &name == "image" { - let reader = ImageReader::new(Cursor::new(data)) - .with_guessed_format() - .expect("Cursor io never fails"); - debug!("Guessed format: {:?}", reader.format()); - - let mut buf = EInkImage::default(); - { - let image = reader.decode()?; - let mut dither = DitherMethod::Atkinson.get_ditherer(); - dither.dither(&image.into(), &mut buf); - } - let cmd = DisplaySetCommand { img: Box::new(buf) }; - ctx.display_channel - .send_timeout(cmd, Duration::from_secs(10)).await?; - } + let call = ImageRequest::from_multipart(parts).await?; + let mut buf = EInkImage::new(call.palette.value().to_vec()); + { + let mut dither = call.dither_method.get_ditherer(); + dither.dither(&call.image, &mut buf); } - Ok(()) -} - -async fn process_image(mut parts: Multipart) -> Result { + let cmd = DisplaySetCommand { img: Box::new(buf) }; + ctx.display_channel + .send_timeout(cmd, Duration::from_secs(10)) + .await?; Ok(StatusCode::OK) } + +/// generates a dithered image based on the given image and the dithering parameters. +/// Can be used to see how the dithering and palette choices affect the result. +async fn preview_image(parts: Multipart) -> Result { + let call = ImageRequest::from_multipart(parts).await?; + let mut buf = EInkImage::new(call.palette.value().to_vec()); + { + let mut dither = call.dither_method.get_ditherer(); + dither.dither(&call.image, &mut buf); + } + // Convert buf into a png image. + let img = buf.into_rgbimage(); + + let mut buffer = Cursor::new(Vec::new()); + img.write_to(&mut buffer, image::ImageFormat::Png)?; + + let headers = [(header::CONTENT_TYPE, mime::IMAGE_PNG.to_string())]; + Ok((StatusCode::OK, headers, buffer.into_inner())) +} diff --git a/src/imageproc.rs b/src/imageproc.rs index 0ccb289..b60e11b 100644 --- a/src/imageproc.rs +++ b/src/imageproc.rs @@ -20,9 +20,32 @@ const DISPLAY_PALETTE: [Srgb; 7] = [ Srgb::new(0.757, 0.443, 0.165), // Orange ]; -pub enum DitherPalette {} +const SIMPLE_PALETTE: [Srgb; 7] = [ + Srgb::new(0.0,0.0,0.0), // Black + Srgb::new(1.0,1.0,1.0), // White + Srgb::new(0.0, 1.0, 0.0), // Green + Srgb::new(0.0, 0.0, 1.0), // Blue + Srgb::new(1.0, 0.0, 0.0), // Red + Srgb::new(1.0, 1.0, 0.0), // Yellow + Srgb::new(0.757, 0.443, 0.165), // Orange +]; -#[derive(strum::EnumString, Serialize, Deserialize, PartialEq, Eq)] +#[derive(strum::EnumString, Serialize, Deserialize, PartialEq, Eq, Debug)] +pub enum DitherPalette { + Default, + Simple, +} + +impl DitherPalette { + pub fn value(&self) -> &[Srgb] { + match self { + Self::Default => &DISPLAY_PALETTE, + Self::Simple => &DISPLAY_PALETTE, // FIXME: use simple pallete based on binary. + } + } +} + +#[derive(strum::EnumString, Serialize, Deserialize, PartialEq, Eq, Debug)] pub enum DitherMethod { NearestNeighbor, FloydSteinberg,