Move from .pixil to .gif
This commit is contained in:
parent
fcfc2e0ad5
commit
497d33e78a
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
2
go.mod
|
@ -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
2
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=
|
|
|
@ -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>
|
|
@ -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}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue