This commit is contained in:
Neale Pickett 2022-09-25 12:03:05 -06:00
commit 09e8798cb1
9 changed files with 238 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
wallart.bin
wallart.png

4
README.md Normal file
View File

@ -0,0 +1,4 @@
This is a server for my network-connected wall art thingy.
It is built to accept `.pixil` files,
which are generated by https://pixilart.com/.

121
cmd/wallartd/main.go Normal file
View File

@ -0,0 +1,121 @@
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"image/png"
"net/http"
"os"
"strings"
"github.com/kettek/apng"
)
func handleUpload(w http.ResponseWriter, r *http.Request) {
inf, header, err := r.FormFile("image")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer inf.Close()
if !strings.HasSuffix(header.Filename, ".pixil") {
http.Error(w, "Invalid file type", http.StatusBadRequest)
return
}
var pixil Pixil
if err := json.NewDecoder(inf).Decode(&pixil); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if pixil.Width != 8 || pixil.Height != 8 {
http.Error(w, "Invalid image size", http.StatusBadRequest)
return
}
outRaw, err := os.OpenFile("wallart.bin", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer outRaw.Close()
outPng, err := os.Create("wallart.png")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer outPng.Close()
// You only get 8 frames max
numFrames := len(pixil.Frames)
if numFrames > 8 {
numFrames = 8
}
outImage := apng.APNG{
Frames: make([]apng.Frame, len(pixil.Frames)),
}
for i := 0; i < numFrames; i++ {
frame := pixil.Frames[i]
preview := frame.GetPreview()
if preview == nil {
http.Error(w, "Invalid frame", http.StatusBadRequest)
return
}
img, err := png.Decode(bytes.NewReader(preview))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Dump it to the raw file
for y := 0; y < 8; y++ {
for x := 0; x < 8; x++ {
r, g, b, _ := img.At(x, y).RGBA()
outRaw.Write([]byte{
byte(r >> 8),
byte(g >> 8),
byte(b >> 8),
})
}
}
outImage.Frames[i] = apng.Frame{
Image: img,
DelayNumerator: 1,
DelayDenominator: 2,
}
}
if err := apng.Encode(outPng, outImage); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintln(w, "It worked")
}
func handleWallartBin(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "wallart.bin")
}
func handleWallartPng(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "wallart.png")
}
func main() {
listen := flag.String("listen", ":8080", "listen address")
web := flag.String("web", "web", "web directory")
flag.Parse()
http.HandleFunc("/wallart.bin", handleWallartBin)
http.HandleFunc("/wallart.png", handleWallartPng)
http.HandleFunc("/upload", handleUpload)
http.Handle("/", http.FileServer(http.Dir(*web)))
http.ListenAndServe(*listen, nil)
}

29
cmd/wallartd/pixil.go Normal file
View File

@ -0,0 +1,29 @@
package main
import (
"encoding/base64"
"strings"
)
// A naiive subset of what's in a .pixil file
type PixilFrame struct {
Active bool `json:"active"`
Preview string `json:"preview"`
}
type Pixil struct {
Width uint `json:"width"`
Height uint `json:"height"`
Frames []PixilFrame `json:"frames"`
}
func (frame *PixilFrame) GetPreview() []byte {
if !frame.Active {
return nil
}
if !strings.HasPrefix(frame.Preview, "data:image/png") {
return nil
}
png := strings.Split(frame.Preview, ",")[1]
preview, _ := base64.StdEncoding.DecodeString(png)
return preview
}

8
go.mod Normal file
View File

@ -0,0 +1,8 @@
module git.woozle.org/neale/wallartd
go 1.18
require (
github.com/kettek/apng v0.0.0-20220823221153-ff692776a607
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
)

4
go.sum Normal file
View File

@ -0,0 +1,4 @@
github.com/kettek/apng v0.0.0-20220823221153-ff692776a607 h1:8tP9cdXzcGX2AvweVVG/lxbI7BSjWbNNUustwJ9dQVA=
github.com/kettek/apng v0.0.0-20220823221153-ff692776a607/go.mod h1:x78/VRQYKuCftMWS0uK5e+F5RJ7S4gSlESRWI0Prl6Q=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=

26
web/index.html Normal file
View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wall Art Server</title>
<link rel="stylesheet" href="style.css">
<link rel="icon" href="wallart.png">
<script src="script.mjs" type="module"></script>
</head>
<body>
<h1>Wall Art Server</h1>
<p>
Upload a <code>.pixil</code> file to display on the wall.
Animations will loop at 2 frames per second,
for up to 8 frames.
</p>
<form action="upload" method="post" enctype="multipart/form-data">
<input type="file" name="image">
<input type="submit" value="Upload">
</form>
<p>
<img id="wallart" src="wallart.png" alt="Current image">
</p>
</body>
</html>

34
web/script.mjs Normal file
View File

@ -0,0 +1,34 @@
function updateImages() {
for (let image of document.querySelectorAll('img')) {
let url = new URL(image.src)
url.searchParams.set("t", Date.now())
image.src = url.toString()
}
}
async function submit(event) {
event.preventDefault()
let form = event.target
let data = new FormData(form)
let url = form.action
let method = form.method
let headers = new Headers()
let resp = await fetch(url, {method, headers, body: data})
if (resp.ok) {
updateImages()
} else {
alert("Error: " + resp.status)
}
}
function init() {
for (let f of document.querySelectorAll("form")) {
f.addEventListener("submit", event => submit(event))
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init)
} else {
init()
}

10
web/style.css Normal file
View File

@ -0,0 +1,10 @@
html {
font-family: sans-serif;
text-align: center;
}
img#wallart {
max-width: 90vw;
width: 320px;
image-rendering: pixelated;
}