wip: flesh out api, add preview call
This commit is contained in:
parent
624ad1f101
commit
c7e57f3d3b
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1308,6 +1308,7 @@ dependencies = [
|
|||
"epd-waveshare",
|
||||
"image",
|
||||
"linux-embedded-hal",
|
||||
"mime",
|
||||
"minijinja",
|
||||
"palette",
|
||||
"serde",
|
||||
|
|
|
@ -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"] }
|
||||
|
|
126
src/api.rs
126
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<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 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<impl IntoResponse, AppError> {
|
||||
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<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()))
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue