aboutsummaryrefslogtreecommitdiff
path: root/bin/webp/main.go
blob: 93103cb67cea4078d480a966e51f4bfbf082cd2a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
package main

// webp is a small HTTP server that listens on a Unix socket and
// receives an image as a POST value; it returns a webp encoded image.
// The `quality` header is an integer ranging from 0-100 indicating the
// webp quality, it is required. The `lossless` and `exact` headers
// specify that the image is lossless and to preserve RGB values in
// transparent areas, respectively. These headers may only be set to
// "true" and their absence implies that they are false.
//
// This program exists because there is no pure-Go implementation of
// a webp encoder and using CGO is not desirable for most binaries.
// Furthermore, forking a cwebp binary per image conversion may not
// scale well for a given application, especially if this forking occurs
// on a per-user-request basis. Additionally cwebp requires that the
// image be loaded from disk which may not be desirable while this
// program works purely with byte streams.
//
// This program should be shipped with, started, managed, and killed
// by the parent process that needs webp support. It supports clean
// shutdown in response to SIGTERM. Errors and requests are logged to
// stderr.
//
// Uncompressed TIFF was originally used exclusively as the transit
// format because it supports all image color spaces as well as
// transparency. Compression is unneeded because it wastes CPU cycles on
// the encode and decode side for an IPC call, which has no appreciable
// bandwidth limit. This was changed because it spends a lot of memory
// on the client side.

import (
	"context"
	"fmt"
	"image"
	"log"
	"net"
	"net/http"
	"os"
	"os/signal"
	"strconv"
	"syscall"

	_ "image/jpeg"
	_ "image/png"

	_ "golang.org/x/image/tiff"

	"github.com/chai2010/webp"
)

func writeError(w http.ResponseWriter, code int, message string, args ...any) {
	w.WriteHeader(code)
	w.Write([]byte(fmt.Sprintf(message+"\n", args...)))
	log.Printf("error: %d: %s", code, fmt.Sprintf(message, args...))
}

type webpConverterHandler struct{}

func (h *webpConverterHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Support liveness check
	if r.URL.Path == "/ping" {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("pong"))
		return
	}

	img, _, err := image.Decode(r.Body)
	if err != nil {
		writeError(w, http.StatusBadRequest, "decode failed: %s", err)
		return
	}

	quality, err := strconv.Atoi(r.Header.Get("quality"))
	if err != nil {
		writeError(w, http.StatusBadRequest, "unable to parse quality header: %s", err)
		return
	}

	w.WriteHeader(http.StatusOK)
	err = webp.Encode(w, img, &webp.Options{
		Lossless: r.Header.Get("lossless") == "true",
		Exact:    r.Header.Get("exact") == "true",
		Quality:  float32(quality),
	})
	if err != nil {
		log.Printf("error encoding web: %s", err)
	}

	log.Printf(`"%s %s" %s`, r.Method, r.URL.String(), r.Header.Get("Content-Length"))
}

func main() {
	if len(os.Args) != 2 {
		log.Fatalf("usage: %s <socket path>", os.Args[0])
	}

	sig := make(chan os.Signal)
	signal.Notify(sig, syscall.SIGTERM)

	spath := os.Args[1]
	os.Remove(spath)

	server := http.Server{
		Handler:  &webpConverterHandler{},
		ErrorLog: log.Default(),
	}

	listener, err := net.Listen("unix", spath)
	if err != nil {
		log.Fatalf("error: %s", err)
	}

	log.Printf("listening on %s", spath)
	go server.Serve(listener)

	<-sig
	log.Printf("process received SIG_TERM, quitting")
	server.Shutdown(context.Background())
	os.Remove(spath)
}