Move from .pixil to .gif

This commit is contained in:
Neale Pickett 2023-09-02 16:10:23 -06:00
parent fcfc2e0ad5
commit 497d33e78a
6 changed files with 89 additions and 98 deletions

View File

@ -1,17 +1,16 @@
package main package main
import ( import (
"bytes"
"encoding/json"
"flag" "flag"
"image/png" "fmt"
"image"
"image/draw"
"image/gif"
"log" "log"
"net/http" "net/http"
"os" "os"
"path" "path"
"strings" "strings"
"github.com/kettek/apng"
) )
var cacheDirectory string var cacheDirectory string
@ -24,24 +23,30 @@ func handleUpload(w http.ResponseWriter, r *http.Request) {
} }
defer inf.Close() 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) http.Error(w, "Invalid file type", http.StatusBadRequest)
return return
} }
var pixil Pixil inputGif, err := gif.DecodeAll(inf)
if err := json.NewDecoder(inf).Decode(&pixil); err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
if err := pixil.Parse(); err != nil { if inputGif.Config.Width != 8 || inputGif.Config.Height != 8 {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(
return w,
} fmt.Sprintf("Invalid image size: %d×%d", inputGif.Config.Width, inputGif.Config.Height),
http.StatusBadRequest,
if pixil.width != 8 || pixil.height != 8 { )
http.Error(w, "Invalid image size", http.StatusBadRequest)
return return
} }
@ -52,45 +57,81 @@ func handleUpload(w http.ResponseWriter, r *http.Request) {
} }
defer outRaw.Close() defer outRaw.Close()
outPng, err := os.Create(path.Join(cacheDirectory, "wallart.png")) outputGifFile, err := os.Create(path.Join(cacheDirectory, "wallart.gif"))
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
defer outPng.Close() defer outputGifFile.Close()
// You only get 8 frames max // You only get 8 frames max
numFrames := len(pixil.Frames) numFrames := len(inputGif.Image)
if numFrames > 8 { if numFrames > 8 {
numFrames = 8 numFrames = 8
} }
if numFrames == 0 {
outImage := apng.APNG{ http.Error(w, "GIF must have at least one frame", http.StatusBadRequest)
Frames: make([]apng.Frame, len(pixil.Frames)),
} }
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++ { for i := 0; i < numFrames; i++ {
frame := pixil.Frames[i] img := inputGif.Image[i]
preview := frame.GetPreview() dst := image.NewPaletted(imgRect, img.Palette)
if preview == nil { outputGif.Image[i] = dst
http.Error(w, "Invalid frame", http.StatusBadRequest)
return if img.Bounds().Max.X > 8 || img.Bounds().Max.Y > 8 {
} http.Error(
img, err := png.Decode(bytes.NewReader(preview)) w,
if err != nil { fmt.Sprintf("Invalid frame size: %d×%d", img.Bounds().Max.X, img.Bounds().Max.Y),
http.Error(w, err.Error(), http.StatusInternalServerError) http.StatusBadRequest,
)
return return
} }
if img.Bounds().Max.X != 8 || img.Bounds().Max.Y != 8 { switch inputGif.Disposal[i] {
http.Error(w, "Invalid frame size", http.StatusBadRequest) case gif.DisposalNone:
return 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 // Dump it to the raw file
for y := 0; y < 8; y++ { for y := 0; y < 8; y++ {
for x := 0; x < 8; x++ { 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{ outRaw.Write([]byte{
byte(r >> 8), byte(r >> 8),
byte(g >> 8), byte(g >> 8),
@ -99,19 +140,17 @@ func handleUpload(w http.ResponseWriter, r *http.Request) {
} }
} }
outImage.Frames[i] = apng.Frame{ outputGif.Delay[i] = 50
Image: img, outputGif.Disposal[i] = 0
DelayNumerator: 1, previousImg = dst
DelayDenominator: 2,
}
} }
if err := apng.Encode(outPng, outImage); err != nil { if err := gif.EncodeAll(outputGifFile, &outputGif); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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 { func logRequestHandler(h http.Handler) http.Handler {
@ -131,7 +170,9 @@ func main() {
cacheDir := http.Dir(cacheDirectory) cacheDir := http.Dir(cacheDirectory)
http.HandleFunc("/upload", handleUpload) http.HandleFunc("/upload", handleUpload)
http.Handle("/wallart.bin", http.FileServer(cacheDir)) 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.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)
}
} }

View File

@ -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
}

2
go.mod
View File

@ -1,5 +1,3 @@
module git.woozle.org/neale/wallartd module git.woozle.org/neale/wallartd
go 1.18 go 1.18
require github.com/kettek/apng v0.0.0-20220823221153-ff692776a607

2
go.sum
View File

@ -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=

View File

@ -5,25 +5,22 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wall Art Server</title> <title>Wall Art Server</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<link rel="icon" href="wallart.png"> <link rel="icon" href="wallart.gif">
<script src="script.mjs" type="module"></script> <script src="script.mjs" type="module"></script>
</head> </head>
<body> <body>
<h1>Wall Art Server</h1> <h1>Wall Art Server</h1>
<p> <p>
Upload a <code>.pixil</code> file to display on the wall. Upload a <code>.gif</code> file to display on the wall.
Animations will loop at 2 frames per second, Animations will loop at 2 frames per second,
for up to 8 frames. for up to 8 frames.
</p> </p>
<p>
<a href="https://pixilart.com/">Pixilart</a> generates these files.
</p>
<form action="upload" method="post" enctype="multipart/form-data"> <form action="upload" method="post" enctype="multipart/form-data">
<input type="file" name="image"> <input type="file" name="image">
<input type="submit" value="Upload"> <input type="submit" value="Upload">
</form> </form>
<p> <p>
<img id="wallart" src="wallart.png" alt="Current image"> <img id="wallart" src="wallart.gif" alt="Current image">
</p> </p>
</body> </body>
</html> </html>

View File

@ -17,7 +17,8 @@ async function submit(event) {
if (resp.ok) { if (resp.ok) {
updateImages() updateImages()
} else { } else {
alert("Error: " + resp.status) let body = await resp.text()
alert(`Error: ${resp.status} ${resp.statusText}\n\n${body}`)
} }
} }