Axum, htmx and Postscript for fun and profit.

Rustafarian DevRustafarian Dev
3 min read

Table of contents

I was thinking of a fun project to work on, and then I remembered reading about someone writing an HTTP server in Postscript.

When searching, I came across this article: https://www.oreilly.com/openbook/cgi/ch06_02.html.
Not exactly what I was looking for, but it seemed interesting.

For those who don't know PostScript is a page description language (PDL).
A PDL is a computer language that describes the appearance of a printed page.
PostScript was created by Adobe here is the definition of the language
https://www.adobe.com/jp/print/postscript/pdfs/PLRM.pdf .

So this is my attempt to use Axum, htmx, and Postscript. The idea is to create a clock image using Postscript. Use htmx to refresh the clock every second. And use Axum to serve the image and HTML.

Let's start with the endpoints. I am going to create three endpoints.
* /
* /clock
* /clock.jpg

Let's start with the index page which is quick and easy.

async fn index() -> Html<&'static str> {
        Html(r###"
<html>
<head>
  <script src="https://unpkg.com/htmx.org@1.9.5"></script>
</head>
<body style='background:black;color:white'>
<div hx-get="/clock" hx-trigger="every 1s"></div>
</body>
<html>
"###)
}

The /clock endpoint creates the img html snippet.

async fn clock() -> impl IntoResponse {
    let local: DateTime<Local> = Local::now();
    Html(local.time().format(r#"%H:%M:%S<br/><img src="/clock.jpg?h=%H&m=%M&s=%S"/>"#).to_string())
}

The /clock.jpg endpoint creates the image from

#[derive(Deserialize)]
struct MyTime {
  h: u8,
  m: u8,
  s: u8,
}

async fn clock_img(t: Query<MyTime>) -> impl IntoResponse {
    let mut child = Command::new("ghostscript")
    .args(&["-sDEVICE=jpeg", "-sOutputFile=-", "-q", "-g150x150", "-"])
    .stdin(Stdio::piped())
    .stdout(Stdio::piped())
    .stderr(Stdio::null())
    .spawn().unwrap();
     let mut stdin = child.stdin.take().unwrap();
     stdin.write_all(format!(r#"
%!PS-Adobe-3.0 EPSF-3.0
%%BoundingBox: 0 0 150 150
%%EndComments
/max_length     150 def
/line_size      1.5 def
/marker         5 def
/origin         {{0 dup}} def
/center         {{max_length 2 div}} def
/radius         center def
/hour_segment    {{0.50 radius mul}} def
/minute_segment  {{0.80 radius mul}} def
/second_segment  {{0.90 radius mul}} def
/red            {{1 0 0 setrgbcolor}} def
/green          {{0 1 0 setrgbcolor}} def
/blue           {{0 0 1 setrgbcolor}} def
/black          {{0 0 0 setrgbcolor}} def
/hour_angle {{
    {0} {1} 60 div add 3 sub 30 mul
    neg
}} def
/minute_angle {{
        {1} {2} 60 div add 15 sub 6 mul
        neg
}} def
/second_angle {{
        {2} 15 sub 6 mul
        neg
}} def
center dup translate
black clippath fill
line_size setlinewidth
origin radius 0 360 arc blue stroke
gsave
1 1 12 {{
        pop
        radius marker sub 0 moveto
        marker 0 rlineto red stroke
        30 rotate
}} for
grestore
origin moveto
hour_segment hour_angle cos mul
hour_segment hour_angle sin mul
lineto green stroke
origin moveto
minute_segment minute_angle cos mul
minute_segment minute_angle sin mul
lineto green stroke
origin moveto
second_segment second_angle cos mul
second_segment second_angle sin mul
lineto green stroke
origin line_size 2 mul 0 360 arc red fill
showpage
End_of_PostScript_Code
"#, t.h, t.m, t.s).as_bytes()).await.unwrap();
     drop(stdin);
     let out = child.wait_with_output().await.unwrap();
     (
         [(header::CONTENT_TYPE, "image/jpeg")],
        out.stdout
     )
}

Add the routes to Axum


#[tokio::main]
async fn main() {
    // build our application with a single route
    let app = Router::new()
    .route("/", get(index))
    .route("/clock", get(clock))
    .route("/clock.jpg", get(clock_img));

    // run it with hyper on localhost:3000
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

You can see it at http://139.177.201.40:3000/

The source code is here.
https://github.com/rustafariandev/postscript-axum

1
Subscribe to my newsletter

Read articles from Rustafarian Dev directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Rustafarian Dev
Rustafarian Dev