Log requests
[litoprism.git] / src / main.rs
index 53849d9188a138ac9c58cc8c9d22bec8a25c39d2..0405e228365a2954edf816918cf0e681675848c0 100644 (file)
@@ -1,21 +1,36 @@
+use std::convert::From;
+use std::env;
 use std::io::Cursor;
 use std::fmt::Display;
 use std::fmt;
 use std::borrow::BorrowMut;
+use std::sync::{Arc, Mutex};
+use log::LevelFilter;
 use num_traits::Zero;
 use num_traits::cast::AsPrimitive;
 use percent_encoding::percent_decode_str;
-use actix_web::{get, web, App, HttpServer, HttpRequest, HttpResponse, Responder, ResponseError, Result};
+use actix_web::{middleware::Logger, get, web, App, HttpServer, HttpRequest, HttpResponse, Responder, ResponseError, Result};
 use actix_web::body::BoxBody;
 use actix_web::http::StatusCode;
 use image::{ImageBuffer, ColorType, Rgb, RgbImage, write_buffer_with_format};
 use image::ImageOutputFormat::Png;
-use image::imageops::{FilterType, resize};
+use image::imageops::{FilterType, overlay, resize};
+use image::error::{LimitError, LimitErrorKind};
+use actix_web::cookie::time::OffsetDateTime;
 
 
+const TIMEAVATAR_SIZE_U32: u32 = 6;
+const TIMEAVATAR_SIZE_I64: i64 = TIMEAVATAR_SIZE_U32 as i64;
+
 #[derive(Debug)]
 struct ImageError(image::ImageError);
 
+impl ImageError {
+    fn dimension_error() -> ImageError {
+        ImageError(image::ImageError::Limits(LimitError::from_kind(LimitErrorKind::DimensionError)))
+    }
+}
+
 impl Display for ImageError {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         write!(f, "{}", &self.0)
@@ -28,17 +43,55 @@ impl ResponseError for ImageError {
     }
 
     fn error_response(&self) -> HttpResponse<BoxBody> {
-        HttpResponse::InternalServerError().finish()
+        HttpResponse::InternalServerError().body(format!("{}\n", &self))
+    }
+}
+
+#[derive(Debug)]
+enum CanvasError {
+    NotExists,
+    DimensionMismatch((u32, u32)),
+    ImageError(ImageError)
+}
+
+impl Display for CanvasError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "Canvas Error: see error_response() for more details")
+    }
+}
+
+impl ResponseError for CanvasError {
+    fn status_code(&self) -> StatusCode {
+        match &self {
+            CanvasError::NotExists => StatusCode::NOT_FOUND,
+            CanvasError::DimensionMismatch(_) => StatusCode::BAD_REQUEST,
+            CanvasError::ImageError(e) => e.status_code()
+        }
+    }
+
+    fn error_response(&self) -> HttpResponse<BoxBody> {
+        match &self {
+            CanvasError::NotExists => HttpResponse::NotFound().body("Canvas does not exist."),
+            CanvasError::DimensionMismatch(dim) =>
+                HttpResponse::BadRequest().body(format!("Supplied dimensions don't match the dimensions of existing canvas {:?}.", dim)),
+            CanvasError::ImageError(e) => e.error_response()
+        }
+    }
+}
+
+impl From<ImageError> for CanvasError {
+    fn from(e: ImageError) -> CanvasError {
+        CanvasError::ImageError(e)
     }
 }
 
 struct ToRgbIter<'a, T> where T: AsPrimitive<u8> {
-    iter: Box<&'a mut dyn Iterator<Item = T>>
+    iter: &'a mut dyn Iterator<Item = T>
 }
 
 impl<'a, T: AsPrimitive<u8>> ToRgbIter<'a, T> {
-    fn new(data: &'a mut (dyn Iterator<Item = T>)) -> ToRgbIter<T> {
-        ToRgbIter { iter: Box::new(data) }
+    fn new(iter: &'a mut (dyn Iterator<Item = T>)) -> ToRgbIter<T> {
+        ToRgbIter { iter }
     }
 }
 
@@ -46,9 +99,8 @@ impl<T: AsPrimitive<u8> + Zero> Iterator for ToRgbIter<'_, T> {
     type Item = Rgb<u8>;
     fn next(&mut self) -> Option<Rgb<u8>> {
         if let Some(r) = self.iter.next() {
-            let g = self.iter.next().unwrap_or(T::zero());
-            let b = self.iter.next().unwrap_or(T::zero());
-            println!("{} {} {}", r.as_(), g.as_(), b.as_());
+            let g = self.iter.next().unwrap_or_else(T::zero);
+            let b = self.iter.next().unwrap_or_else(T::zero);
             Some(Rgb([r.as_(), g.as_(), b.as_()]))
         }
         else {
@@ -64,40 +116,297 @@ fn to_imageresult<T> (result: Result<T, image::ImageError>) -> Result<T, ImageEr
     }
 }
 
+fn to_canvasresult<T: std::fmt::Debug> (result: Result<T, ImageError>) -> Result<T, CanvasError> {
+    match result {
+        Ok(v) => Ok(v),
+        Err(e) => Err(CanvasError::ImageError(e))
+    }
+}
+
+#[derive(Debug)]
+struct Canvas {
+    pen: usize,
+    img: RgbImage
+}
+
+impl Canvas {
+    fn new(width: u32, height: u32) -> Canvas {
+        Canvas { pen: 0, img: ImageBuffer::new(width, height) }
+    }
+
+    fn from_image(img: RgbImage) -> Canvas {
+        Canvas { pen: 0, img }
+    }
+
+    /*fn replace(&self, img: RgbImage, pen: usize) -> Canvas {
+        self.img = img;
+        self.pen = pen;
+        self
+    }*/
+
+    fn advance_pen(mut self, offset: usize) -> Canvas {
+        //Canvas { pen: self.pen + offset, img: self.img }
+        self.pen += offset;
+        self.pen %= self.img.width() as usize * self.img.height() as usize;
+        self
+    }
+}
+
+struct Canvas0(Option<Canvas>);
+struct Canvas1(Option<Canvas>);
+struct Canvas2(Option<Canvas>);
+
 #[get("/hello/{name}")]
 async fn greet(name: web::Path<String>, req: HttpRequest) -> impl Responder {
     println!("{:?}", req);
-    format!("Hello {name}!")
+    format!("Hello {name}!\n{:?}", req.headers().get("user-agent"))
 }
 
-#[get("/gen/0/{data}")]
-async fn img_gen(req: HttpRequest) -> Result<impl Responder> {
-    let mut cursor = Cursor::new(Vec::new());
-    let mut img: RgbImage = ImageBuffer::new(32, 32);
-    let mut pixels = img.pixels_mut();
-    let data = req.uri().path().split("/").skip(3).next().unwrap();
-
-    println!("{:?}", data);
-    for sp in ToRgbIter::new(percent_decode_str(&data).into_iter().borrow_mut()) {
-        let mut dp = pixels.next().unwrap();
-        println!("{:?}", sp);
+fn rgb_encode_to_canvas(mut canvas: Canvas, data: &mut dyn Iterator<Item = u8>) -> Result<Canvas, ImageError> {
+    let mut pixels = canvas.img.pixels_mut().skip(canvas.pen);
+    let mut counter = 0;
+
+    for sp in ToRgbIter::new(data) {
+        let mut dp = match pixels.next() {
+            Some(pixel) => pixel,
+            None => {
+                pixels = canvas.img.pixels_mut().skip(0);
+                pixels.next().ok_or_else(ImageError::dimension_error)?
+            }
+        };
         dp.0 = sp.0;
+        counter += 1;
+    }
+
+    Ok(canvas.advance_pen(counter))
+}
+
+fn rgb_encode(img: RgbImage, data: &mut dyn Iterator<Item = u8>) -> Result<RgbImage, ImageError> {
+    Ok(rgb_encode_to_canvas(Canvas::from_image(img), data)?.img)
+}
+
+fn apply_to_canvas<'a, F>(f: F, canvas_option: &'a mut Option<Canvas>, dimensions: Option<(u32, u32)>, data: &str) -> Result<&'a mut Canvas, CanvasError>
+    where F: Fn(Canvas, &mut dyn Iterator<Item = u8>) -> Result<Canvas, CanvasError>
+{
+    let canvas = match canvas_option.take() {
+        Some(canvas) => match dimensions {
+            Some(dimensions) => {
+                if dimensions == canvas.img.dimensions() {
+                    Ok(canvas)
+                }
+                else {
+                    Err(CanvasError::DimensionMismatch(canvas_option.insert(canvas).img.dimensions()))
+                }
+            },
+            None =>
+                Ok(canvas)
+        },
+        None => match dimensions {
+            Some((dim_x, dim_y)) =>
+                Ok(Canvas::new(dim_x, dim_y)),
+            None =>
+                Err(CanvasError::NotExists)
+        }
+    }?;
+
+    Ok(canvas_option.insert(f(canvas, percent_decode_str(data).into_iter().borrow_mut())?))
+}
+
+fn make_png(dim_x: u32, dim_y: u32, scale: u32, data: &mut dyn Iterator<Item = u8>) -> Result<Cursor<Vec<u8>>, ImageError> {
+    // Image must not be larger than 1 megapixel
+    if dim_x * dim_y * scale > 1048576 {
+        return Err(ImageError::dimension_error())
     }
 
-    let img = resize(&img, 512, 512, FilterType::Nearest);
-    println!("Here!");
-    to_imageresult(write_buffer_with_format(&mut cursor, &img, 512, 512, ColorType::Rgb8, Png))?;
+    let img: RgbImage = rgb_encode(ImageBuffer::new(dim_x, dim_y), data)?;
+    let cursor = image_to_cursor(img, scale)?;
+    Ok(cursor)
+}
+
+fn image_to_cursor(img: RgbImage, scale: u32) -> Result<Cursor<Vec<u8>>, ImageError> {
+    let (dim_x, dim_y) = img.dimensions();
+    let (dim_x, dim_y) = (dim_x * scale, dim_y * scale);
+
+    // Image must not be larger than 1 megapixel
+    if dim_x * dim_y > 1048576 {
+        return Err(ImageError::dimension_error())
+    }
+
+    let mut cursor = Cursor::new(Vec::new());
+    let img = resize(&img, dim_x, dim_y, FilterType::Nearest);
+    to_imageresult(write_buffer_with_format(&mut cursor, &img, dim_x, dim_y, ColorType::Rgb8, Png))?;
+    Ok(cursor)
+}
+
+
+#[get("/gen/0/{data}")]
+async fn img_gen0(req: HttpRequest) -> Result<impl Responder> {
+    let data = req.uri().path().split('/').nth(3).unwrap();
+    let cursor = make_png(32, 32, 16, percent_decode_str(data).into_iter().borrow_mut())?;
+    Ok(HttpResponse::build(StatusCode::OK)
+       .content_type("image/png")
+       .body(cursor.into_inner()))
+}
+
+#[get("/gen/1/{dim_x}/{dim_y}/{scale}/{data}")]
+async fn img_gen1(req: HttpRequest, path: web::Path<(u32, u32, u32)>) -> Result<impl Responder> {
+    let (dim_x, dim_y, scale) = path.into_inner();
+    let data = req.uri().path().split('/').nth(6).unwrap();
+    let cursor = make_png(dim_x, dim_y, scale, percent_decode_str(data).into_iter().borrow_mut())?;
+    Ok(HttpResponse::build(StatusCode::OK)
+       .content_type("image/png")
+       .body(cursor.into_inner()))
+}
+
+#[get("/gen/2/{dim_x}/{dim_y}/{scale}")]
+async fn img_gen2(req: HttpRequest, path: web::Path<(u32, u32, u32)>) -> Result<impl Responder> {
+    let (dim_x, dim_y, scale) = path.into_inner();
+    let data = match req.headers().get("user-agent") {
+        Some(header) => header.to_str().unwrap_or(""),
+        _ => ""
+    };
+    let cursor = make_png(dim_x, dim_y, scale, &mut data.bytes())?;
+    Ok(HttpResponse::build(StatusCode::OK)
+       .content_type("image/png")
+       .body(cursor.into_inner()))
+}
+
+#[get("/gen/3/{scale}")]
+async fn img_gen3(req: HttpRequest, path: web::Path<u32>) -> Result<impl Responder> {
+    let scale = path.into_inner();
+    let data = match req.headers().get("user-agent") {
+        Some(header) => header.to_str().unwrap_or(""),
+        _ => ""
+    };
+    let rgbimg: RgbImage = rgb_encode(ImageBuffer::new(TIMEAVATAR_SIZE_U32, TIMEAVATAR_SIZE_U32), &mut data.bytes())?;
+    let mut resimg: RgbImage = ImageBuffer::new(60 * TIMEAVATAR_SIZE_U32, 24 * TIMEAVATAR_SIZE_U32);
+    let time = OffsetDateTime::now_utc().time();
+    let (hour, minute): (i64, i64) = (time.hour().into(), time.minute().into());
+    overlay(&mut resimg, &rgbimg, minute * TIMEAVATAR_SIZE_I64, hour * TIMEAVATAR_SIZE_I64);
+    let cursor = image_to_cursor(resimg, scale)?;
+    Ok(HttpResponse::build(StatusCode::OK)
+       .content_type("image/png")
+       .body(cursor.into_inner()))
+}
+
+#[get("/pgen/0/{data}")]
+async fn img_pgen0(req: HttpRequest, canvas0: web::Data<Arc<Mutex<Canvas0>>>) -> Result<impl Responder> {
+    let data = req.uri().path().split('/').nth(3).unwrap();
+    let scale = 16;
+    let img: RgbImage = ({
+        let canvas_option = &mut *canvas0.lock().unwrap();
+        Ok(apply_to_canvas( |c, d| to_canvasresult(rgb_encode_to_canvas(c, d)), &mut canvas_option.0, None, data)?.img.clone())
+    } as Result<RgbImage, CanvasError>)?;
+    let cursor = image_to_cursor(img, scale)?;
+    Ok(HttpResponse::build(StatusCode::OK)
+       .content_type("image/png")
+       .body(cursor.into_inner()))
+}
+
+#[get("/pgen/1/{dim_x}/{dim_y}/{scale}/{data}")]
+async fn img_pgen1(req: HttpRequest, path: web::Path<(u32, u32, u32)>, canvas1: web::Data<Arc<Mutex<Canvas1>>>) -> Result<impl Responder> {
+    let (dim_x, dim_y, scale) = path.into_inner();
+    let data = req.uri().path().split('/').nth(6).unwrap();
+    let img: RgbImage = ({
+        let canvas_option = &mut *canvas1.lock().unwrap();
+        Ok(apply_to_canvas( |c, d| to_canvasresult(rgb_encode_to_canvas(c, d)), &mut canvas_option.0, Some((dim_x, dim_y)), data)?.img.clone())
+    } as Result<RgbImage, CanvasError>)?;
+    let cursor = image_to_cursor(img, scale)?;
+    Ok(HttpResponse::build(StatusCode::OK)
+       .content_type("image/png")
+       .body(cursor.into_inner()))
+}
+
+#[get("/pgen/2/{dim_x}/{dim_y}/{scale}")]
+async fn img_pgen2(req: HttpRequest, path: web::Path<(u32, u32, u32)>, canvas1: web::Data<Arc<Mutex<Canvas1>>>) -> Result<impl Responder> {
+    let (dim_x, dim_y, scale) = path.into_inner();
+    let data = match req.headers().get("user-agent") {
+        Some(header) => header.to_str().unwrap_or(""),
+        _ => ""
+    };
+    let img: RgbImage = ({
+        let canvas_option = &mut *canvas1.lock().unwrap();
+        Ok(apply_to_canvas( |c, d| to_canvasresult(rgb_encode_to_canvas(c, d)), &mut canvas_option.0, Some((dim_x, dim_y)), data)?.img.clone())
+    } as Result<RgbImage, CanvasError>)?;
+    let cursor = image_to_cursor(img, scale)?;
     Ok(HttpResponse::build(StatusCode::OK)
        .content_type("image/png")
        .body(cursor.into_inner()))
 }
 
+#[get("/pgen/3/{scale}")]
+async fn img_pgen3(req: HttpRequest, path: web::Path<u32>, canvas2: web::Data<Arc<Mutex<Canvas2>>>) -> Result<impl Responder> {
+    let scale = path.into_inner();
+    let data = match req.headers().get("user-agent") {
+        Some(header) => header.to_str().unwrap_or(""),
+        _ => ""
+    };
+    let time = OffsetDateTime::now_utc().time();
+    let (hour, minute): (i64, i64) = (time.hour().into(), time.minute().into());
+    let rgbimg: RgbImage = rgb_encode(ImageBuffer::new(TIMEAVATAR_SIZE_U32, TIMEAVATAR_SIZE_U32), &mut data.bytes())?;
+    let resimg: RgbImage = ({
+        let canvas_option = &mut *canvas2.lock().unwrap();
+        let mut canvas = match canvas_option.0.take() {
+            Some(canvas) => canvas,
+            None => Canvas::new(60 * TIMEAVATAR_SIZE_U32, 24 * TIMEAVATAR_SIZE_U32)
+        };
+        overlay(&mut canvas.img, &rgbimg, minute * TIMEAVATAR_SIZE_I64, hour * TIMEAVATAR_SIZE_I64);
+        Ok(canvas_option.0.insert(canvas).img.clone())
+    } as Result<RgbImage, CanvasError>)?;
+    let cursor = image_to_cursor(resimg, scale)?;
+    Ok(HttpResponse::build(StatusCode::OK)
+       .content_type("image/png")
+       .body(cursor.into_inner()))
+}
+
+#[get("/pdrop/{canvas_id}")]
+async fn pdrop(canvas_id: web::Path<u8>,
+    canvas0: web::Data<Arc<Mutex<Canvas0>>>,
+    canvas1: web::Data<Arc<Mutex<Canvas1>>>,
+    canvas2: web::Data<Arc<Mutex<Canvas2>>>) -> Result<impl Responder> {
+
+    let canvas_id = canvas_id.into_inner();
+    match canvas_id {
+        0 => canvas0.lock().unwrap().0.take(),
+        1 => canvas1.lock().unwrap().0.take(),
+        2 => canvas2.lock().unwrap().0.take(),
+        _ => None
+    }.ok_or(CanvasError::NotExists)?;
+    Ok(HttpResponse::build(StatusCode::OK)
+       .body(format!("Canvas {} dropped.", canvas_id)))
+}
+
+
 #[actix_web::main] // or #[tokio::main]
 async fn main() -> std::io::Result<()> {
-    HttpServer::new(|| {
-        App::new().service(greet).service(img_gen)
+    env_logger::builder()
+        .filter_module("actix_web", LevelFilter::Info)
+        .parse_default_env()
+        .init();
+
+    let listenaddress = env::args().nth(1).unwrap_or_else(|| "127.0.0.1:8080".to_string());
+    let canvas0 = Arc::new(Mutex::new(Canvas0(Some(Canvas::new(32, 32)))));
+    let canvas1 = Arc::new(Mutex::new(Canvas1(None)));
+    let canvas2 = Arc::new(Mutex::new(Canvas2(None)));
+
+    HttpServer::new(move || {
+        App::new()
+            .wrap(Logger::default())
+            .service(greet)
+            .service(img_gen0)
+            .service(img_gen1)
+            .service(img_gen2)
+            .service(img_gen3)
+            .service(img_pgen0)
+            .service(img_pgen1)
+            .service(img_pgen2)
+            .service(img_pgen3)
+            .service(pdrop)
+            .app_data(web::Data::new(canvas0.clone()))
+            .app_data(web::Data::new(canvas1.clone()))
+            .app_data(web::Data::new(canvas2.clone()))
     })
-    .bind(("127.0.0.1", 8080))?
+    .bind(listenaddress)?
     .run()
     .await
 }