wip: flesh out api, add preview call

This commit is contained in:
saji 2024-07-30 00:35:43 -05:00
parent 624ad1f101
commit c7e57f3d3b
4 changed files with 114 additions and 43 deletions

1
Cargo.lock generated
View file

@ -1308,6 +1308,7 @@ dependencies = [
"epd-waveshare",
"image",
"linux-embedded-hal",
"mime",
"minijinja",
"palette",
"serde",

View file

@ -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"] }

View file

@ -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<Context> {
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<DynamicImage>,
struct ImageRequest {
image: Box<RgbImage>,
dither_method: DitherMethod,
palette: DitherPalette,
}
impl ImageRequest {
async fn from_multipart(mut parts: Multipart) -> Result<Self, AppError> {
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<Context>,
mut parts: Multipart,
parts: Multipart,
) -> Result<impl IntoResponse, AppError> {
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 call = ImageRequest::from_multipart(parts).await?;
let mut buf = EInkImage::new(call.palette.value().to_vec());
{
let image = reader.decode()?;
let mut dither = DitherMethod::Atkinson.get_ditherer();
dither.dither(&image.into(), &mut buf);
let mut dither = call.dither_method.get_ditherer();
dither.dither(&call.image, &mut buf);
}
let cmd = DisplaySetCommand { img: Box::new(buf) };
ctx.display_channel
.send_timeout(cmd, Duration::from_secs(10)).await?;
}
}
Ok(())
}
async fn process_image(mut parts: Multipart) -> Result<impl IntoResponse, AppError> {
.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<impl IntoResponse, AppError> {
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()))
}

View file

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