partially functional ui?
This commit is contained in:
parent
91be01196f
commit
b1fd13fc59
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
|
|
55
src/app.rs
55
src/app.rs
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
11
src/eink.rs
11
src/eink.rs
|
@ -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
63
templates/app.html
Normal 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 %}
|
|
@ -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>
|
|
21
templates/partials/base.html
Normal file
21
templates/partials/base.html
Normal 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
4
templates/preview.html
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<div id="preview-area">
|
||||||
|
|
||||||
|
<img src="data:image/png;base64,{{image_contents}}">
|
||||||
|
</div>
|
Loading…
Reference in a new issue