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 ", 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) }