// (c) 2018 Karthik Karanth (https://github.com/medakk/gotiny) // (c) 2018-2021 Andreas Neue package main import ( "flag" "fmt" "io" "log" "net/http" "strconv" "strings" "github.com/davidbyttow/govips/v2/vips" ) var ( imgServer = flag.String("s", "https://images.example.com/", "URL prefix") listenAddr = flag.String("l", "0.0.0.0:8080", "Listen address") sizesFilter = flag.String("f", "", "Allowed sizes") ) func init() { flag.Parse() } func transform(w http.ResponseWriter, r *http.Request) { // Get the query parameters from the request URL path := r.URL.EscapedPath() parm := strings.Split(path, "/") if len(parm) < 4 { writeError(w, "", "", "error in arguments", http.StatusBadRequest) return } method := parm[1] args := parm[2] imgPath := strings.Join(parm[3:], "/") // Start fetching the image from the given url resp, err := http.Get(*imgServer + imgPath) if err != nil { writeError(w, imgPath, method, "failed to fetch image", http.StatusNotFound) return } defer resp.Body.Close() // Ensure that a valid response was given if resp.StatusCode/100 != 2 { writeError(w, imgPath, method, fmt.Sprintf("failed to fetch image (%v)", resp.StatusCode), http.StatusBadRequest) return } buf := []byte{} errMsg := "" errCode := 0 switch method { case "pass": break case "resize": buf, errMsg, errCode = doResize(resp.Body, args) if buf == nil { writeError(w, imgPath, method, errMsg, errCode) } case "blur": buf, errMsg, errCode = doBlur(resp.Body, args) if buf == nil { writeError(w, imgPath, method, errMsg, errCode) } default: writeError(w, imgPath, method, "unknown method", http.StatusBadRequest) return } _, err = w.Write(buf) if err != nil { writeError(w, imgPath, method, fmt.Sprintf("%v", err), http.StatusInternalServerError) return } } func doResize(body io.ReadCloser, args string) ([]byte, string, int) { allowed := strings.Split(*sizesFilter, ",") forbidden := true if allowed[0] == "*" { forbidden = false } else { for _, v := range allowed { if args == v { forbidden = false break } } } if forbidden { return nil, "size is not allowed", http.StatusForbidden } size := strings.Split(args, "x") if len(size) < 2 { return nil, "width and height are required", http.StatusBadRequest } width := size[0] height := size[1] // Validate that all three required fields are present if width == "" || height == "" { return nil, "width and height are required", http.StatusBadRequest } // Convert width and height to integers iw, errW := strconv.Atoi(width) ih, errH := strconv.Atoi(height) if errW != nil || errH != nil { return nil, "width and height must be integers", http.StatusBadRequest } // Read from response body, convert and write to output buffer img, err := vips.NewImageFromReader(body) if err != nil { return nil, fmt.Sprintf("%v", err), http.StatusInternalServerError } err = img.Thumbnail(iw, ih, 0) if err != nil { return nil, fmt.Sprintf("%v", err), http.StatusInternalServerError } ep := vips.NewDefaultExportParams() // default export params are sufficient buf, _, _ := img.Export(ep) if err != nil { return nil, fmt.Sprintf("%v", err), http.StatusInternalServerError } return buf, "", 0 } func doBlur(body io.ReadCloser, args string) ([]byte, string, int) { // Convert width and heigh to integers sigma, err := strconv.ParseFloat(args, 64) if err != nil { return nil, "invalid sigma", http.StatusInternalServerError } img, err := vips.NewImageFromReader(body) if err != nil { return nil, fmt.Sprintf("%v", err), http.StatusInternalServerError } err = img.GaussianBlur(sigma) if err != nil { return nil, fmt.Sprintf("%v", err), http.StatusInternalServerError } ep := vips.NewDefaultExportParams() // default export params are sufficient buf, _, _ := img.Export(ep) if err != nil { return nil, fmt.Sprintf("%v", err), http.StatusInternalServerError } return buf, "", 0 } func writeError(w http.ResponseWriter, imgPath, method, err string, status int) { w.WriteHeader(status) w.Write([]byte(fmt.Sprintf("Error [%s %s]: %s", method, imgPath, err))) } func main() { // Start vips with the default configuration vips.Startup(nil) defer vips.Shutdown() http.HandleFunc("/", transform) log.Fatal(http.ListenAndServe(*listenAddr, nil)) }