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",
|
"epd-waveshare",
|
||||||
"image",
|
"image",
|
||||||
"linux-embedded-hal",
|
"linux-embedded-hal",
|
||||||
|
"mime",
|
||||||
"minijinja",
|
"minijinja",
|
||||||
"palette",
|
"palette",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
@ -13,6 +13,7 @@ clap = { version = "4.5.7", features = ["derive"] }
|
||||||
epd-waveshare = { git = "https://github.com/caemor/epd-waveshare.git"}
|
epd-waveshare = { git = "https://github.com/caemor/epd-waveshare.git"}
|
||||||
image = "0.25.1"
|
image = "0.25.1"
|
||||||
linux-embedded-hal = { version = "0.4.0"}
|
linux-embedded-hal = { version = "0.4.0"}
|
||||||
|
mime = "0.3.17"
|
||||||
minijinja = "2.1.0"
|
minijinja = "2.1.0"
|
||||||
palette = "0.7.6"
|
palette = "0.7.6"
|
||||||
serde = { version = "1.0.204", features = ["derive"] }
|
serde = { version = "1.0.204", features = ["derive"] }
|
||||||
|
|
120
src/api.rs
120
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::extract::Multipart;
|
||||||
use axum::http::StatusCode;
|
use axum::http::{header, StatusCode};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::{extract::State, response::Response, routing::post, Router};
|
use axum::{extract::State, response::Response, routing::post, Router};
|
||||||
use image::{DynamicImage, ImageReader};
|
use image::{ImageReader, RgbImage};
|
||||||
use std::io::Cursor;
|
use std::io::{BufWriter, Cursor};
|
||||||
use std::time::Duration;
|
use std::str;
|
||||||
use tracing::{debug, error, info, instrument};
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::display::EInkPanel;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
use tokio::sync::mpsc::{self, Receiver, Sender};
|
use tokio::sync::mpsc::{self, Receiver, Sender};
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
|
use tracing::{debug, error, info, instrument};
|
||||||
|
|
||||||
pub enum ImageFormFields {
|
#[derive(thiserror::Error, Debug)]
|
||||||
DitherType,
|
pub enum ApiError {
|
||||||
ImageFile,
|
#[error("missing image field")]
|
||||||
|
MissingImage,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -86,45 +88,89 @@ pub async fn display_task(
|
||||||
pub fn router() -> Router<Context> {
|
pub fn router() -> Router<Context> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/setimage", post(set_image))
|
.route("/setimage", post(set_image))
|
||||||
.route("/process_image", post(process_image))
|
.route("/preview", post(preview_image))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ImageRequest {
|
struct ImageRequest {
|
||||||
image: Box<DynamicImage>,
|
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))]
|
#[instrument(skip(ctx))]
|
||||||
#[axum::debug_handler]
|
|
||||||
async fn set_image(
|
async fn set_image(
|
||||||
State(ctx): State<Context>,
|
State(ctx): State<Context>,
|
||||||
mut parts: Multipart,
|
parts: Multipart,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
while let Some(field) = parts.next_field().await? {
|
let call = ImageRequest::from_multipart(parts).await?;
|
||||||
let name = field.name().expect("fields always have names").to_string();
|
let mut buf = EInkImage::new(call.palette.value().to_vec());
|
||||||
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 = call.dither_method.get_ditherer();
|
||||||
let mut dither = DitherMethod::Atkinson.get_ditherer();
|
dither.dither(&call.image, &mut buf);
|
||||||
dither.dither(&image.into(), &mut buf);
|
|
||||||
}
|
}
|
||||||
let cmd = DisplaySetCommand { img: Box::new(buf) };
|
let cmd = DisplaySetCommand { img: Box::new(buf) };
|
||||||
ctx.display_channel
|
ctx.display_channel
|
||||||
.send_timeout(cmd, Duration::from_secs(10)).await?;
|
.send_timeout(cmd, Duration::from_secs(10))
|
||||||
}
|
.await?;
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn process_image(mut parts: Multipart) -> Result<impl IntoResponse, AppError> {
|
|
||||||
Ok(StatusCode::OK)
|
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
|
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 {
|
pub enum DitherMethod {
|
||||||
NearestNeighbor,
|
NearestNeighbor,
|
||||||
FloydSteinberg,
|
FloydSteinberg,
|
||||||
|
|
Loading…
Reference in a new issue