From 09e8798cb1ac17afe67a0438f252e4db67bf9945 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 25 Sep 2022 12:03:05 -0600 Subject: [PATCH] Working --- .gitignore | 2 + README.md | 4 ++ cmd/wallartd/main.go | 121 ++++++++++++++++++++++++++++++++++++++++++ cmd/wallartd/pixil.go | 29 ++++++++++ go.mod | 8 +++ go.sum | 4 ++ web/index.html | 26 +++++++++ web/script.mjs | 34 ++++++++++++ web/style.css | 10 ++++ 9 files changed, 238 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cmd/wallartd/main.go create mode 100644 cmd/wallartd/pixil.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 web/index.html create mode 100644 web/script.mjs create mode 100644 web/style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..41c6512 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +wallart.bin +wallart.png diff --git a/README.md b/README.md new file mode 100644 index 0000000..f88fe22 --- /dev/null +++ b/README.md @@ -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/. diff --git a/cmd/wallartd/main.go b/cmd/wallartd/main.go new file mode 100644 index 0000000..ac6ae5d --- /dev/null +++ b/cmd/wallartd/main.go @@ -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) +} diff --git a/cmd/wallartd/pixil.go b/cmd/wallartd/pixil.go new file mode 100644 index 0000000..3e3c318 --- /dev/null +++ b/cmd/wallartd/pixil.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..35f91a8 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a6a2b6b --- /dev/null +++ b/go.sum @@ -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= diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..514a8bd --- /dev/null +++ b/web/index.html @@ -0,0 +1,26 @@ + + + + + + Wall Art Server + + + + + +

Wall Art Server

+

+ Upload a .pixil file to display on the wall. + Animations will loop at 2 frames per second, + for up to 8 frames. +

+
+ + +
+

+ Current image +

+ + \ No newline at end of file diff --git a/web/script.mjs b/web/script.mjs new file mode 100644 index 0000000..28c666f --- /dev/null +++ b/web/script.mjs @@ -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() +} \ No newline at end of file diff --git a/web/style.css b/web/style.css new file mode 100644 index 0000000..298ab63 --- /dev/null +++ b/web/style.css @@ -0,0 +1,10 @@ +html { + font-family: sans-serif; + text-align: center; +} + +img#wallart { + max-width: 90vw; + width: 320px; + image-rendering: pixelated; +} \ No newline at end of file