Skip to content

Commit 01442a4

Browse files
Serve GIFs as well as WebP (#1065)
Saw #1005 and thought it was a fun little challenge. Tested that this worked locally for: - Previewing with WebP (default) - Previewing with GIF (passing --gif flag) - Updating config using either format - Exporting image using either format Inspected the images shown and confirmed that it does generate a WebP or GIF file as appropriate, and that the src element is set to the appropriate image type.
1 parent 5439bb8 commit 01442a4

File tree

13 files changed

+106
-59
lines changed

13 files changed

+106
-59
lines changed

cmd/serve.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ var (
1212
host string
1313
port int
1414
watch bool
15+
serveGif bool
1516
)
1617

1718
func init() {
@@ -20,6 +21,7 @@ func init() {
2021
ServeCmd.Flags().BoolVarP(&watch, "watch", "w", true, "Reload scripts on change. Does not recurse sub-directories.")
2122
ServeCmd.Flags().IntVarP(&maxDuration, "max_duration", "d", 15000, "Maximum allowed animation duration (ms)")
2223
ServeCmd.Flags().IntVarP(&timeout, "timeout", "", 30000, "Timeout for execution (ms)")
24+
ServeCmd.Flags().BoolVarP(&serveGif, "gif", "", false, "Generate GIF instead of WebP")
2325
}
2426

2527
var ServeCmd = &cobra.Command{
@@ -39,7 +41,7 @@ func serve(cmd *cobra.Command, args []string) error {
3941
fmt.Printf("explicitly setting --watch is unnecessary, since it's the default\n\n")
4042
}
4143

42-
s, err := server.NewServer(host, port, watch, args[0], maxDuration, timeout)
44+
s, err := server.NewServer(host, port, watch, args[0], maxDuration, timeout, serveGif)
4345
if err != nil {
4446
return err
4547
}

server/browser/browser.go

+39-20
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Package browser provides the ability to send WebP images to a browser over
1+
// Package browser provides the ability to send images to a browser over
22
// websockets.
33
package browser
44

@@ -19,17 +19,18 @@ import (
1919
"tidbyt.dev/pixlet/server/loader"
2020
)
2121

22-
// Browser provides a structure for serving WebP images over websockets to
22+
// Browser provides a structure for serving WebP or GIF images over websockets to
2323
// a web browser.
2424
type Browser struct {
2525
addr string // The address to listen on.
2626
title string // The title of the HTML document.
27-
updateChan chan loader.Update // A channel of base64 encoded WebP images.
27+
updateChan chan loader.Update // A channel of base64 encoded images.
2828
watch bool
2929
fo *fanout.Fanout
3030
r *mux.Router
3131
tmpl *template.Template
3232
loader *loader.Loader
33+
serveGif bool // True if serving GIF, false if serving WebP
3334
}
3435

3536
//go:embed preview-mask.png
@@ -43,18 +44,19 @@ var previewHTML string
4344

4445
// previewData is used to populate the HTML template.
4546
type previewData struct {
46-
Title string `json:"title"`
47-
WebP string `json:"webp"`
48-
Watch bool `json:"-"`
49-
Err string `json:"error,omitempty"`
47+
Title string `json:"title"`
48+
Image string `json:"img"`
49+
ImageType string `json:"img_type"`
50+
Watch bool `json:"-"`
51+
Err string `json:"error,omitempty"`
5052
}
5153
type handlerRequest struct {
5254
ID string `json:"id"`
5355
Param string `json:"param"`
5456
}
5557

5658
// NewBrowser sets up a browser structure. Call Run() to kick off the main loops.
57-
func NewBrowser(addr string, title string, watch bool, updateChan chan loader.Update, l *loader.Loader) (*Browser, error) {
59+
func NewBrowser(addr string, title string, watch bool, updateChan chan loader.Update, l *loader.Loader, serveGif bool) (*Browser, error) {
5860
tmpl, err := template.New("preview").Parse(previewHTML)
5961
if err != nil {
6062
return nil, err
@@ -68,6 +70,7 @@ func NewBrowser(addr string, title string, watch bool, updateChan chan loader.Up
6870
title: title,
6971
loader: l,
7072
watch: watch,
73+
serveGif: serveGif,
7174
}
7275

7376
r := mux.NewRouter()
@@ -92,6 +95,7 @@ func NewBrowser(addr string, title string, watch bool, updateChan chan loader.Up
9295
// API endpoints to support the React frontend.
9396
r.HandleFunc("/api/v1/preview", b.previewHandler)
9497
r.HandleFunc("/api/v1/preview.webp", b.imageHandler)
98+
r.HandleFunc("/api/v1/preview.gif", b.imageHandler)
9599
r.HandleFunc("/api/v1/push", b.pushHandler)
96100
r.HandleFunc("/api/v1/schema", b.schemaHandler).Methods("GET")
97101
r.HandleFunc("/api/v1/handlers/{handler}", b.schemaHandlerHandler).Methods("POST")
@@ -103,7 +107,7 @@ func NewBrowser(addr string, title string, watch bool, updateChan chan loader.Up
103107

104108
// Run starts the server process and runs forever in a blocking fashion. The
105109
// main routines include an update watcher to process incomming changes to the
106-
// webp and running the http handlers.
110+
// image and running the http handlers.
107111
func (b *Browser) Run() error {
108112
defer b.fo.Quit()
109113

@@ -170,17 +174,21 @@ func (b *Browser) imageHandler(w http.ResponseWriter, r *http.Request) {
170174
config[k] = val[0]
171175
}
172176

173-
webp, err := b.loader.LoadApplet(config)
177+
img, err := b.loader.LoadApplet(config)
174178
if err != nil {
175179
http.Error(w, "loading applet", http.StatusInternalServerError)
176180
return
177181
}
178182

179-
w.Header().Set("Content-Type", "image/webp")
183+
img_type := "image/webp"
184+
if b.serveGif {
185+
img_type = "image/gif"
186+
}
187+
w.Header().Set("Content-Type", img_type)
180188

181-
data, err := base64.StdEncoding.DecodeString(webp)
189+
data, err := base64.StdEncoding.DecodeString(img)
182190
if err != nil {
183-
http.Error(w, "decoding webp", http.StatusInternalServerError)
191+
http.Error(w, "decoding image", http.StatusInternalServerError)
184192
return
185193
}
186194

@@ -199,10 +207,15 @@ func (b *Browser) previewHandler(w http.ResponseWriter, r *http.Request) {
199207
config[k] = val[0]
200208
}
201209

202-
webp, err := b.loader.LoadApplet(config)
210+
img, err := b.loader.LoadApplet(config)
211+
img_type := "webp"
212+
if b.serveGif {
213+
img_type = "gif"
214+
}
203215
data := &previewData{
204-
WebP: webp,
205-
Title: b.title,
216+
Image: img,
217+
ImageType: img_type,
218+
Title: b.title,
206219
}
207220
if err != nil {
208221
data.Err = err.Error()
@@ -238,13 +251,19 @@ func (b *Browser) websocketHandler(w http.ResponseWriter, r *http.Request) {
238251
}
239252

240253
func (b *Browser) updateWatcher() error {
254+
img_type := "webp"
255+
if b.serveGif {
256+
img_type = "gif"
257+
}
258+
241259
for {
242260
select {
243261
case up := <-b.updateChan:
244262
b.fo.Broadcast(
245263
fanout.WebsocketEvent{
246-
Type: fanout.EventTypeWebP,
247-
Message: up.WebP,
264+
Type: fanout.EventTypeImage,
265+
Message: up.Image,
266+
ImageType: img_type,
248267
},
249268
)
250269

@@ -279,12 +298,12 @@ func (b *Browser) oldRootHandler(w http.ResponseWriter, r *http.Request) {
279298
config[k] = vals[0]
280299
}
281300

282-
webp, err := b.loader.LoadApplet(config)
301+
img, err := b.loader.LoadApplet(config)
283302

284303
data := previewData{
285304
Title: b.title,
286305
Watch: b.watch,
287-
WebP: webp,
306+
Image: img,
288307
}
289308

290309
if err != nil {

server/browser/preview.html

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
<body bgcolor="black">
2222
<div style="border: solid 1px white">
23-
<img id="render" src="data:image/webp;base64,{{ .WebP }}" />
23+
<img id="render" src="data:image/{{ .ImageType }};base64,{{ .Image }}" />
2424
</div>
2525
<div>
2626
<p id="errors" style="color: red;">{{ .Err }}</p>
@@ -53,8 +53,8 @@
5353
const err = document.getElementById("errors");
5454

5555
switch (data.type) {
56-
case "webp":
57-
img.src = "data:image/webp;base64," + data.message;
56+
case "img":
57+
img.src = "data:image/" + data.img_type + ";base64," + data.message;
5858
err.innerHTML = "";
5959
break;
6060
case "error":

server/browser/push.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@ func (b *Browser) pushHandler(w http.ResponseWriter, r *http.Request) {
5353
}
5454
}
5555

56-
webp, err := b.loader.LoadApplet(config)
56+
img, err := b.loader.LoadApplet(config)
5757

5858
payload, err := json.Marshal(
5959
TidbytPushJSON{
6060
DeviceID: deviceID,
61-
Image: webp,
61+
Image: img,
6262
InstallationID: installationID,
6363
Background: background,
6464
},

server/fanout/client.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func (f *Fanout) NewClient(conn *websocket.Conn) *Client {
5252
return c
5353
}
5454

55-
// Send is used to send a webp message to the client.
55+
// Send is used to send an image message to the client.
5656
func (c *Client) Send(event WebsocketEvent) {
5757
c.send <- event
5858
}
@@ -82,7 +82,7 @@ func (c *Client) reader() {
8282
}
8383
}
8484

85-
// writer writes webp events over the socket when it recieves messages via
85+
// writer writes image events over the socket when it recieves messages via
8686
// Send(). It also sends pings to ensure the connection stays alive.
8787
func (c *Client) writer() {
8888
ticker := time.NewTicker(pingPeriod)

server/fanout/event.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
package fanout
22

33
const (
4-
// EventTypeWebP is used to signal what type of message we are sending over
4+
// EventTypeImage is used to signal what type of message we are sending over
55
// the socket.
6-
EventTypeWebP = "webp"
6+
EventTypeImage = "img"
77

88
// EventTypeSchema is used to signal that the schema for a given app has
99
// changed.
1010
EventTypeSchema = "schema"
1111

1212
// EventTypeErr is used to signal there was an error encountered rendering
13-
// the WebP image.
13+
// the image.
1414
EventTypeErr = "error"
1515
)
1616

1717
// WebsocketEvent is a structure used to send messages over the socket.
1818
type WebsocketEvent struct {
19-
// Message is the contents of the message. This is a webp, base64 encoded.
19+
// Message is the contents of the message. This is a webp or gif, base64 encoded.
2020
Message string `json:"message"`
2121

22+
// ImageType indicates whether the Message is webp or gif image.
23+
ImageType string `json:"img_type"`
24+
2225
// Type is the type of message we are sending over the socket.
2326
Type string `json:"type"`
2427
}

server/loader/loader.go

+28-11
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@ type Loader struct {
3030
maxDuration int
3131
initialLoad chan bool
3232
timeout int
33+
renderGif bool
3334
}
3435

3536
type Update struct {
36-
WebP string
37-
Schema string
38-
Err error
37+
Image string
38+
ImageType string
39+
Schema string
40+
Err error
3941
}
4042

4143
// NewLoader instantiates a new loader structure. The loader will read off of
@@ -49,6 +51,7 @@ func NewLoader(
4951
updatesChan chan Update,
5052
maxDuration int,
5153
timeout int,
54+
renderGif bool,
5255
) (*Loader, error) {
5356
l := &Loader{
5457
fs: fs,
@@ -62,6 +65,7 @@ func NewLoader(
6265
maxDuration: maxDuration,
6366
initialLoad: make(chan bool),
6467
timeout: timeout,
68+
renderGif: renderGif,
6569
}
6670

6771
cache := runtime.NewInMemoryCache()
@@ -95,12 +99,16 @@ func (l *Loader) Run() error {
9599
case <-l.requestedChanges:
96100
up := Update{}
97101

98-
webp, err := l.loadApplet(config)
102+
img, err := l.loadApplet(config)
99103
if err != nil {
100104
log.Printf("error loading applet: %v", err)
101105
up.Err = err
102106
} else {
103-
up.WebP = webp
107+
up.Image = img
108+
up.ImageType = "webp"
109+
if l.renderGif {
110+
up.ImageType = "gif"
111+
}
104112
}
105113

106114
l.updatesChan <- up
@@ -109,12 +117,16 @@ func (l *Loader) Run() error {
109117
log.Println("detected updates, reloading")
110118
up := Update{}
111119

112-
webp, err := l.loadApplet(config)
120+
img, err := l.loadApplet(config)
113121
if err != nil {
114122
log.Printf("error loading applet: %v", err)
115123
up.Err = err
116124
} else {
117-
up.WebP = webp
125+
up.Image = img
126+
up.ImageType = "webp"
127+
if l.renderGif {
128+
up.ImageType = "gif"
129+
}
118130
up.Schema = string(l.applet.SchemaJSON)
119131
}
120132

@@ -134,7 +146,7 @@ func (l *Loader) LoadApplet(config map[string]string) (string, error) {
134146
l.configChanges <- config
135147
l.requestedChanges <- true
136148
result := <-l.resultsChan
137-
return result.WebP, result.Err
149+
return result.Image, result.Err
138150
}
139151

140152
func (l *Loader) GetSchema() []byte {
@@ -182,12 +194,17 @@ func (l *Loader) loadApplet(config map[string]string) (string, error) {
182194
if screens.ShowFullAnimation {
183195
maxDuration = 0
184196
}
185-
webp, err := screens.EncodeWebP(maxDuration)
197+
198+
var img []byte
199+
if l.renderGif {
200+
img, err = screens.EncodeGIF(maxDuration)
201+
} else {
202+
img, err = screens.EncodeWebP(maxDuration)
203+
}
186204
if err != nil {
187205
return "", fmt.Errorf("error rendering: %w", err)
188206
}
189-
190-
return base64.StdEncoding.EncodeToString(webp), nil
207+
return base64.StdEncoding.EncodeToString(img), nil
191208
}
192209

193210
func (l *Loader) markInitialLoadComplete() {

server/server.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ type Server struct {
2323
}
2424

2525
// NewServer creates a new server initialized with the applet.
26-
func NewServer(host string, port int, watch bool, path string, maxDuration int, timeout int) (*Server, error) {
26+
func NewServer(host string, port int, watch bool, path string, maxDuration int, timeout int, serveGif bool) (*Server, error) {
2727
fileChanges := make(chan bool, 100)
2828

2929
// check if path exists, and whether it is a directory or a file
@@ -47,13 +47,13 @@ func NewServer(host string, port int, watch bool, path string, maxDuration int,
4747
}
4848

4949
updatesChan := make(chan loader.Update, 100)
50-
l, err := loader.NewLoader(fs, watch, fileChanges, updatesChan, maxDuration, timeout)
50+
l, err := loader.NewLoader(fs, watch, fileChanges, updatesChan, maxDuration, timeout, serveGif)
5151
if err != nil {
5252
return nil, err
5353
}
5454

5555
addr := fmt.Sprintf("%s:%d", host, port)
56-
b, err := browser.NewBrowser(addr, filepath.Base(path), watch, updatesChan, l)
56+
b, err := browser.NewBrowser(addr, filepath.Base(path), watch, updatesChan, l, serveGif)
5757
if err != nil {
5858
return nil, err
5959
}

0 commit comments

Comments
 (0)