From 497d33e78a8a3f3d2e51b78203699f860d4512d8 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sat, 2 Sep 2023 16:10:23 -0600 Subject: [PATCH] Move from .pixil to .gif --- cmd/wallart-server/main.go | 127 ++++++++++++++++++++++++------------ cmd/wallart-server/pixil.go | 44 ------------- go.mod | 2 - go.sum | 2 - web/index.html | 9 +-- web/script.mjs | 3 +- 6 files changed, 89 insertions(+), 98 deletions(-) delete mode 100644 cmd/wallart-server/pixil.go diff --git a/cmd/wallart-server/main.go b/cmd/wallart-server/main.go index 5095760..35b01e9 100644 --- a/cmd/wallart-server/main.go +++ b/cmd/wallart-server/main.go @@ -1,17 +1,16 @@ package main import ( - "bytes" - "encoding/json" "flag" - "image/png" + "fmt" + "image" + "image/draw" + "image/gif" "log" "net/http" "os" "path" "strings" - - "github.com/kettek/apng" ) var cacheDirectory string @@ -24,24 +23,30 @@ func handleUpload(w http.ResponseWriter, r *http.Request) { } defer inf.Close() - if !strings.HasSuffix(header.Filename, ".pixil") { + if contentType := header.Header.Get("Content-Type"); contentType != "image/gif" { + http.Error( + w, + fmt.Sprintf("Invalid content-type: %s", contentType), + http.StatusBadRequest, + ) + } + if !strings.HasSuffix(header.Filename, ".gif") { http.Error(w, "Invalid file type", http.StatusBadRequest) return } - var pixil Pixil - if err := json.NewDecoder(inf).Decode(&pixil); err != nil { + inputGif, err := gif.DecodeAll(inf) + if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - if err := pixil.Parse(); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - if pixil.width != 8 || pixil.height != 8 { - http.Error(w, "Invalid image size", http.StatusBadRequest) + if inputGif.Config.Width != 8 || inputGif.Config.Height != 8 { + http.Error( + w, + fmt.Sprintf("Invalid image size: %d×%d", inputGif.Config.Width, inputGif.Config.Height), + http.StatusBadRequest, + ) return } @@ -52,45 +57,81 @@ func handleUpload(w http.ResponseWriter, r *http.Request) { } defer outRaw.Close() - outPng, err := os.Create(path.Join(cacheDirectory, "wallart.png")) + outputGifFile, err := os.Create(path.Join(cacheDirectory, "wallart.gif")) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - defer outPng.Close() + defer outputGifFile.Close() // You only get 8 frames max - numFrames := len(pixil.Frames) + numFrames := len(inputGif.Image) if numFrames > 8 { numFrames = 8 } - - outImage := apng.APNG{ - Frames: make([]apng.Frame, len(pixil.Frames)), + if numFrames == 0 { + http.Error(w, "GIF must have at least one frame", http.StatusBadRequest) } + outputGif := gif.GIF{ + Image: make([]*image.Paletted, numFrames), + Delay: make([]int, numFrames), + LoopCount: 0, + Disposal: make([]byte, numFrames), + Config: inputGif.Config, + BackgroundIndex: 0, + } + + imgRect := image.Rectangle{ + image.Point{}, + image.Point{inputGif.Config.Width, inputGif.Config.Height}, + } + + // Draw the background + backgroundImg := image.NewPaletted(imgRect, inputGif.Image[0].Palette) + draw.Src.Draw( + backgroundImg, + imgRect, + image.NewUniform(inputGif.Image[0].Palette[inputGif.BackgroundIndex]), + image.Point{}, + ) + previousImg := backgroundImg + undisposedImg := backgroundImg + 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) + img := inputGif.Image[i] + dst := image.NewPaletted(imgRect, img.Palette) + outputGif.Image[i] = dst + + if img.Bounds().Max.X > 8 || img.Bounds().Max.Y > 8 { + http.Error( + w, + fmt.Sprintf("Invalid frame size: %d×%d", img.Bounds().Max.X, img.Bounds().Max.Y), + http.StatusBadRequest, + ) return } - if img.Bounds().Max.X != 8 || img.Bounds().Max.Y != 8 { - http.Error(w, "Invalid frame size", http.StatusBadRequest) - return + switch inputGif.Disposal[i] { + case gif.DisposalNone: + draw.Src.Draw(dst, previousImg.Rect, previousImg, image.Point{}) + draw.Over.Draw(dst, img.Rect, img, img.Rect.Min) + undisposedImg = dst + case gif.DisposalBackground: + draw.Src.Draw(dst, backgroundImg.Rect, backgroundImg, image.Point{}) + draw.Over.Draw(dst, img.Rect, img, img.Rect.Min) + case gif.DisposalPrevious: + draw.Src.Draw(dst, undisposedImg.Rect, undisposedImg, image.Point{}) + draw.Over.Draw(dst, img.Rect, img, img.Rect.Min) + default: + draw.Src.Draw(dst, img.Rect, image.Black, image.Point{}) + draw.Over.Draw(dst, img.Rect, img, img.Rect.Min) } // 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() + r, g, b, _ := previousImg.At(x, y).RGBA() outRaw.Write([]byte{ byte(r >> 8), byte(g >> 8), @@ -99,19 +140,17 @@ func handleUpload(w http.ResponseWriter, r *http.Request) { } } - outImage.Frames[i] = apng.Frame{ - Image: img, - DelayNumerator: 1, - DelayDenominator: 2, - } + outputGif.Delay[i] = 50 + outputGif.Disposal[i] = 0 + previousImg = dst } - if err := apng.Encode(outPng, outImage); err != nil { + if err := gif.EncodeAll(outputGifFile, &outputGif); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - log.Println("New image successfully uploaded") + log.Printf("New image successfully uploaded: %d frames", len(outputGif.Image)) } func logRequestHandler(h http.Handler) http.Handler { @@ -131,7 +170,9 @@ func main() { cacheDir := http.Dir(cacheDirectory) http.HandleFunc("/upload", handleUpload) http.Handle("/wallart.bin", http.FileServer(cacheDir)) - http.Handle("/wallart.png", http.FileServer(cacheDir)) + http.Handle("/wallart.gif", http.FileServer(cacheDir)) http.Handle("/", http.FileServer(http.Dir(*web))) - http.ListenAndServe(*listen, logRequestHandler(http.DefaultServeMux)) + if err := http.ListenAndServe(*listen, logRequestHandler(http.DefaultServeMux)); err != nil { + log.Fatal(err) + } } diff --git a/cmd/wallart-server/pixil.go b/cmd/wallart-server/pixil.go deleted file mode 100644 index e8115b3..0000000 --- a/cmd/wallart-server/pixil.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "encoding/base64" - "strconv" - "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 int - height int - WidthString string `json:"width"` - HeightString string `json:"height"` - Frames []PixilFrame `json:"frames"` -} - -func (p *Pixil) Parse() error { - var err error - - p.width, err = strconv.Atoi(p.WidthString) - if err != nil { - return err - } - - p.height, err = strconv.Atoi(p.HeightString) - return err -} - -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 index 4db3c06..c0f4a19 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ module git.woozle.org/neale/wallartd go 1.18 - -require github.com/kettek/apng v0.0.0-20220823221153-ff692776a607 diff --git a/go.sum b/go.sum index f642237..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +0,0 @@ -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= diff --git a/web/index.html b/web/index.html index 0fa1334..b263ce2 100644 --- a/web/index.html +++ b/web/index.html @@ -5,25 +5,22 @@ Wall Art Server - +

Wall Art Server

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

-

- Pixilart generates these files. -

- Current image + Current image

\ No newline at end of file diff --git a/web/script.mjs b/web/script.mjs index 28c666f..13efa76 100644 --- a/web/script.mjs +++ b/web/script.mjs @@ -17,7 +17,8 @@ async function submit(event) { if (resp.ok) { updateImages() } else { - alert("Error: " + resp.status) + let body = await resp.text() + alert(`Error: ${resp.status} ${resp.statusText}\n\n${body}`) } }