Working
This commit is contained in:
commit
09e8798cb1
|
@ -0,0 +1,2 @@
|
|||
wallart.bin
|
||||
wallart.png
|
|
@ -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/.
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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>
|
|
@ -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()
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
html {
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img#wallart {
|
||||
max-width: 90vw;
|
||||
width: 320px;
|
||||
image-rendering: pixelated;
|
||||
}
|
Loading…
Reference in New Issue