2022-09-25 12:03:05 -06:00
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"flag"
|
2023-09-02 16:10:23 -06:00
|
|
|
|
"fmt"
|
|
|
|
|
"image"
|
|
|
|
|
"image/draw"
|
|
|
|
|
"image/gif"
|
2023-01-06 09:32:16 -07:00
|
|
|
|
"log"
|
2022-09-25 12:03:05 -06:00
|
|
|
|
"net/http"
|
|
|
|
|
"os"
|
2022-09-25 12:34:34 -06:00
|
|
|
|
"path"
|
2022-09-25 12:03:05 -06:00
|
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
2022-09-25 12:34:34 -06:00
|
|
|
|
var cacheDirectory string
|
|
|
|
|
|
2022-09-25 12:03:05 -06:00
|
|
|
|
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()
|
|
|
|
|
|
2023-09-02 16:10:23 -06:00
|
|
|
|
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") {
|
2022-09-25 12:03:05 -06:00
|
|
|
|
http.Error(w, "Invalid file type", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-02 16:10:23 -06:00
|
|
|
|
inputGif, err := gif.DecodeAll(inf)
|
|
|
|
|
if err != nil {
|
2022-09-25 12:03:05 -06:00
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-02 16:10:23 -06:00
|
|
|
|
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,
|
|
|
|
|
)
|
2022-09-25 12:03:05 -06:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-25 12:34:34 -06:00
|
|
|
|
outRaw, err := os.Create(path.Join(cacheDirectory, "wallart.bin"))
|
2022-09-25 12:03:05 -06:00
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer outRaw.Close()
|
|
|
|
|
|
2023-09-02 16:10:23 -06:00
|
|
|
|
outputGifFile, err := os.Create(path.Join(cacheDirectory, "wallart.gif"))
|
2022-09-25 12:03:05 -06:00
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
2023-09-02 16:10:23 -06:00
|
|
|
|
defer outputGifFile.Close()
|
2022-09-25 12:03:05 -06:00
|
|
|
|
|
|
|
|
|
// You only get 8 frames max
|
2023-09-02 16:10:23 -06:00
|
|
|
|
numFrames := len(inputGif.Image)
|
2022-09-25 12:03:05 -06:00
|
|
|
|
if numFrames > 8 {
|
|
|
|
|
numFrames = 8
|
|
|
|
|
}
|
2023-09-02 16:10:23 -06:00
|
|
|
|
if numFrames == 0 {
|
|
|
|
|
http.Error(w, "GIF must have at least one frame", http.StatusBadRequest)
|
|
|
|
|
}
|
2022-09-25 12:03:05 -06:00
|
|
|
|
|
2023-09-02 16:10:23 -06:00
|
|
|
|
outputGif := gif.GIF{
|
|
|
|
|
Image: make([]*image.Paletted, numFrames),
|
|
|
|
|
Delay: make([]int, numFrames),
|
|
|
|
|
LoopCount: 0,
|
|
|
|
|
Disposal: make([]byte, numFrames),
|
|
|
|
|
Config: inputGif.Config,
|
|
|
|
|
BackgroundIndex: 0,
|
2022-09-25 12:03:05 -06:00
|
|
|
|
}
|
|
|
|
|
|
2023-09-02 16:10:23 -06:00
|
|
|
|
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
|
|
|
|
|
|
2022-09-25 12:03:05 -06:00
|
|
|
|
for i := 0; i < numFrames; i++ {
|
2023-09-02 16:10:23 -06:00
|
|
|
|
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,
|
|
|
|
|
)
|
2022-09-25 12:03:05 -06:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-02 17:19:51 -06:00
|
|
|
|
// Implement GIF disposal modes
|
|
|
|
|
// Normally you woud want to use draw.Src instead of draw.Over for the first of each of these.
|
|
|
|
|
// But in this particular application, we want transparency to become black.
|
|
|
|
|
draw.Src.Draw(dst, imgRect, image.Black, image.Point{})
|
2023-09-02 16:10:23 -06:00
|
|
|
|
switch inputGif.Disposal[i] {
|
|
|
|
|
case gif.DisposalNone:
|
2023-09-02 17:19:51 -06:00
|
|
|
|
draw.Over.Draw(dst, previousImg.Rect, previousImg, image.Point{})
|
2023-09-02 16:10:23 -06:00
|
|
|
|
draw.Over.Draw(dst, img.Rect, img, img.Rect.Min)
|
|
|
|
|
undisposedImg = dst
|
|
|
|
|
case gif.DisposalBackground:
|
2023-09-02 17:19:51 -06:00
|
|
|
|
draw.Over.Draw(dst, backgroundImg.Rect, backgroundImg, image.Point{})
|
2023-09-02 16:10:23 -06:00
|
|
|
|
draw.Over.Draw(dst, img.Rect, img, img.Rect.Min)
|
|
|
|
|
case gif.DisposalPrevious:
|
2023-09-02 17:19:51 -06:00
|
|
|
|
draw.Over.Draw(dst, undisposedImg.Rect, undisposedImg, image.Point{})
|
2023-09-02 16:10:23 -06:00
|
|
|
|
draw.Over.Draw(dst, img.Rect, img, img.Rect.Min)
|
|
|
|
|
default:
|
|
|
|
|
draw.Over.Draw(dst, img.Rect, img, img.Rect.Min)
|
2022-09-25 12:39:57 -06:00
|
|
|
|
}
|
|
|
|
|
|
2022-09-25 12:03:05 -06:00
|
|
|
|
// Dump it to the raw file
|
|
|
|
|
for y := 0; y < 8; y++ {
|
|
|
|
|
for x := 0; x < 8; x++ {
|
2023-09-04 14:47:12 -06:00
|
|
|
|
r, g, b, _ := dst.At(x, y).RGBA()
|
2022-09-25 12:03:05 -06:00
|
|
|
|
outRaw.Write([]byte{
|
|
|
|
|
byte(r >> 8),
|
|
|
|
|
byte(g >> 8),
|
|
|
|
|
byte(b >> 8),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-02 16:10:23 -06:00
|
|
|
|
outputGif.Delay[i] = 50
|
|
|
|
|
outputGif.Disposal[i] = 0
|
|
|
|
|
previousImg = dst
|
2022-09-25 12:03:05 -06:00
|
|
|
|
}
|
|
|
|
|
|
2023-09-02 16:10:23 -06:00
|
|
|
|
if err := gif.EncodeAll(outputGifFile, &outputGif); err != nil {
|
2022-09-25 12:03:05 -06:00
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-02 16:10:23 -06:00
|
|
|
|
log.Printf("New image successfully uploaded: %d frames", len(outputGif.Image))
|
2023-01-06 09:32:16 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func logRequestHandler(h http.Handler) http.Handler {
|
|
|
|
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
log.Printf("%s %s %s", r.RemoteAddr, r.Method, r.URL)
|
2023-08-19 11:16:12 -06:00
|
|
|
|
h.ServeHTTP(w, r)
|
2023-01-06 09:32:16 -07:00
|
|
|
|
}
|
|
|
|
|
return http.HandlerFunc(fn)
|
2022-09-25 12:03:05 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
listen := flag.String("listen", ":8080", "listen address")
|
|
|
|
|
web := flag.String("web", "web", "web directory")
|
2022-09-25 12:34:34 -06:00
|
|
|
|
flag.StringVar(&cacheDirectory, "cache", "cache", "cache directory")
|
2022-09-25 12:03:05 -06:00
|
|
|
|
flag.Parse()
|
|
|
|
|
|
2022-09-25 12:34:34 -06:00
|
|
|
|
cacheDir := http.Dir(cacheDirectory)
|
2022-09-25 12:03:05 -06:00
|
|
|
|
http.HandleFunc("/upload", handleUpload)
|
2022-09-25 12:34:34 -06:00
|
|
|
|
http.Handle("/wallart.bin", http.FileServer(cacheDir))
|
2023-09-02 16:10:23 -06:00
|
|
|
|
http.Handle("/wallart.gif", http.FileServer(cacheDir))
|
2022-09-25 12:03:05 -06:00
|
|
|
|
http.Handle("/", http.FileServer(http.Dir(*web)))
|
2023-09-02 16:10:23 -06:00
|
|
|
|
if err := http.ListenAndServe(*listen, logRequestHandler(http.DefaultServeMux)); err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
}
|
2022-09-25 12:03:05 -06:00
|
|
|
|
}
|