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