partially functional ui?
Some checks failed
cargo_test_bench / Run Tests (push) Failing after 1m29s
cargo_test_bench / Run Benchmarks (push) Failing after 1m27s

This commit is contained in:
saji 2024-08-07 00:03:04 -05:00
parent 91be01196f
commit b1fd13fc59
10 changed files with 164 additions and 38 deletions

7
Cargo.lock generated
View file

@ -267,6 +267,12 @@ dependencies = [
"rustc-demangle", "rustc-demangle",
] ]
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]] [[package]]
name = "bit_field" name = "bit_field"
version = "0.10.2" version = "0.10.2"
@ -1488,6 +1494,7 @@ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
"axum-macros", "axum-macros",
"base64",
"clap", "clap",
"criterion", "criterion",
"epd-waveshare", "epd-waveshare",

View file

@ -9,6 +9,7 @@ edition = "2021"
anyhow = "1.0.86" anyhow = "1.0.86"
axum = { version = "0.7.5", features = ["macros", "multipart"] } axum = { version = "0.7.5", features = ["macros", "multipart"] }
axum-macros = "0.4.1" axum-macros = "0.4.1"
base64 = "0.22.1"
clap = { version = "4.5.7", features = ["derive"] } 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"

View file

@ -23,10 +23,10 @@ pub fn router() -> Router<AppState> {
} }
#[derive(Debug)] #[derive(Debug)]
struct ImageRequest { pub struct ImageRequest {
image: Box<RgbImage>, pub image: Box<RgbImage>,
dither_method: DitherMethod, pub dither_method: DitherMethod,
palette: Palette, pub palette: Palette,
} }
#[async_trait] #[async_trait]
@ -36,6 +36,7 @@ where
{ {
type Rejection = AppError; type Rejection = AppError;
#[instrument(skip_all)]
async fn from_request(req: axum::extract::Request, state: &S) -> Result<Self, Self::Rejection> { async fn from_request(req: axum::extract::Request, state: &S) -> Result<Self, Self::Rejection> {
let mut parts = Multipart::from_request(req, state).await?; let mut parts = Multipart::from_request(req, state).await?;
let mut img = None; let mut img = None;

View file

@ -1,16 +1,21 @@
use crate::api; use crate::api;
use crate::display::create_display_thread; use crate::display::create_display_thread;
use crate::dither::DitheredImage; use crate::dither::{DitherMethod, DitheredImage};
use axum::extract::{Path, State}; use crate::eink::Palette;
use axum::extract::State;
use axum::http::{header, StatusCode}; use axum::http::{header, StatusCode};
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use axum::routing::get; use axum::routing::{get, post};
use axum::Router; use axum::Router;
use base64::{engine::general_purpose::STANDARD, Engine as _};
use image::imageops::{resize, FilterType};
use include_dir::{include_dir, Dir, DirEntry}; use include_dir::{include_dir, Dir, DirEntry};
use minijinja::Environment; use minijinja::{context, Environment};
use std::io::Cursor;
use std::sync::mpsc; use std::sync::mpsc;
use std::sync::Arc; use std::sync::Arc;
use std::thread::JoinHandle; use std::thread::JoinHandle;
use strum::VariantNames;
use tracing::{debug, warn}; use tracing::{debug, warn};
use tracing::{error, info, instrument}; use tracing::{error, info, instrument};
@ -100,26 +105,54 @@ fn make_environment() -> Result<Environment<'static>, anyhow::Error> {
pub fn make_app_router() -> Router { pub fn make_app_router() -> Router {
Router::new() Router::new()
.route("/app/*path", get(app_handler)) .route("/app", get(app_handler))
.route("/app/preview", post(app_preview))
.nest("/api", api::router()) .nest("/api", api::router())
.route("/assets", get(asset_handler)) .route("/assets", get(asset_handler))
.with_state(AppState::default()) .with_state(AppState::default())
} }
#[instrument(skip(ctx))] #[instrument(skip(ctx))]
async fn app_handler( async fn app_handler(State(ctx): State<AppState>) -> Result<impl IntoResponse, AppError> {
State(ctx): State<AppState>, let template = ctx.templates.get_template("app.html")?;
Path(path): Path<String>,
) -> Result<impl IntoResponse, AppError> {
let template = ctx.templates.get_template(&path.to_string())?;
let content = template.render("")?; let content = template.render(context! {
palettes => Palette::VARIANTS,
dither_methods => DitherMethod::VARIANTS,
version => env!("CARGO_PKG_VERSION"),
})?;
let headers = [(header::CONTENT_TYPE, mime::TEXT_HTML_UTF_8.to_string())]; let headers = [(header::CONTENT_TYPE, mime::TEXT_HTML_UTF_8.to_string())];
Ok((StatusCode::OK, headers, content)) Ok((StatusCode::OK, headers, content))
} }
async fn app_preview(
State(ctx): State<AppState>,
img_req: api::ImageRequest,
) -> Result<impl IntoResponse, AppError> {
let mut buf = DitheredImage::new(800, 480, img_req.palette.value().to_vec());
let resized = resize(&*img_req.image, 800, 480, FilterType::Lanczos3);
{
let mut dither = img_req.dither_method.get_ditherer();
dither.dither(&resized, &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 template = ctx.templates.get_template("preview.html")?;
let datavec = STANDARD.encode(buffer.into_inner());
let content = template.render(context! {
image_contents => datavec
})?;
let headers = [(header::CONTENT_TYPE, mime::TEXT_HTML_UTF_8.to_string())];
Ok((StatusCode::OK, headers, content))
}
async fn asset_handler() -> Result<impl IntoResponse, AppError> { async fn asset_handler() -> Result<impl IntoResponse, AppError> {
Ok(StatusCode::OK) Ok(StatusCode::OK)
} }

View file

@ -9,7 +9,15 @@ use palette::{cast::FromComponents, IntoColor, Lab, Srgb};
use rayon::prelude::*; use rayon::prelude::*;
#[derive( #[derive(
strum::EnumString, strum::Display, Serialize, Deserialize, PartialEq, Eq, Debug, Clone, strum::EnumString,
strum::Display,
Serialize,
Deserialize,
PartialEq,
Eq,
Debug,
Clone,
strum::VariantNames,
)] )]
pub enum DitherMethod { pub enum DitherMethod {
NearestNeighbor, NearestNeighbor,

View file

@ -29,7 +29,16 @@ const SIMPLE_PALETTE: [Srgb; 7] = [
]; ];
#[derive( #[derive(
strum::EnumString, strum::Display, Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy, strum::EnumString,
strum::Display,
Serialize,
Deserialize,
PartialEq,
Eq,
Debug,
Clone,
Copy,
strum::VariantNames,
)] )]
pub enum Palette { pub enum Palette {
Default, Default,

63
templates/app.html Normal file
View file

@ -0,0 +1,63 @@
{% extends "partials/base.html" %}
{% block title %} Pi Frame Server {% endblock %}
{% block content %}
<header class="container">
<hgroup>
<h1>Pi EINK Frame Server</h1>
<p>Display things accurately (ish)</p>
</header>
<main class="container">
<form hx-encoding="multipart/form-data" enctype='multipart/form-data'>
<fieldset class="grid">
<div>
<fieldset>
<legend>Target Palette</legend>
{% for m in palettes %}
<input type="radio" id="palette-{{m}}" name="palette" value="{{m}}"
{%- if loop.first %} checked {% endif %}>
<label htmlFor="palette-{{m}}">{{m}}</label>
{% endfor %}
</fieldset>
<small> "Default" uses a real-life based color scheme. "Simple" uses an idealized palette.</small>
</div>
<div>
<label for="dither-select">Dither Method</label>
<select id="dither-select" name="dither_method">
{% for m in dither_methods %}
<option {%if loop.first %} selected {% endif %} value="{{m}}">{{m}}</option>
{% endfor %}
</select>
<small>
Different dither methods produce better results for certain images.
</small>
</div>
</fieldset>
<label for="image-input">Image</label>
<input type="file" name="image" id="image-input" accept="image/*">
<small>Images will be resized to <code>800x480</code>, which may stretch the image.</small>
<div class="grid">
<button hx-post="/api/setimage" hx-disabled-elt="this">Set Display</button>
<button class="secondary"
hx-post="/app/preview"
hx-disabled-elt="this"
hx-target="#preview-area"
hx-swap="outerHTML"
>
Preview
</button>
</div>
</form>
<div id="preview-area" hidden></div>
</main>
<footer class="container-fluid">
pi-frame-server Version {{ version }}
</footer>
{% endblock %}

View file

@ -1,21 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hello Bulma!</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css">
</head>
<body>
<section class="section">
<div class="container">
<h1 class="title">
Hello World
</h1>
<p class="subtitle">
My first website with <strong>Bulma</strong>!
</p>
</div>
</section>
</body>
</html>

View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %} My Site {% endblock %}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
<script src="https://unpkg.com/htmx.org@2.0.1" integrity="sha384-QWGpdj554B4ETpJJC9z+ZHJcA/i59TyjxEPXiiUgN2WmTyV5OEZWCD6gQhgkdpB/" crossorigin="anonymous"></script>
{% block head %}{% endblock %}
</head>
<body>
{% block content %}
<h1 class="title">
Hello World
</h1>
<p class="subtitle">
My first website with <strong>Bulma</strong>!
</p>
{% endblock %}
</body>
</html>

4
templates/preview.html Normal file
View file

@ -0,0 +1,4 @@
<div id="preview-area">
<img src="data:image/png;base64,{{image_contents}}">
</div>