package caddywebp import ( "bytes" "fmt" "image" "image/gif" "image/jpeg" "image/png" "io" "log" "net/http" "os" "path" "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/chai2010/webp" "golang.org/x/image/bmp" ) const Quality = 80 func init() { log.Println("webp plugin") caddy.RegisterModule(Webp{}) httpcaddyfile.RegisterHandlerDirective("webp", parseCaddyfile) } func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { return Webp{}, nil } type Webp struct { } func (Webp) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.handlers.webp", New: func() caddy.Module { return new(Webp) }, } } func decodeGif(r io.Reader) (image.Image, error) { i, err := gif.DecodeAll(r) // Give up if we can't decode or decoded multiple images if err != nil || len(i.Image) != 1 { return nil, err } return i.Image[0], nil } func (s Webp) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { if !strings.Contains(r.Header.Get("Accept"), "image/webp") { // Browser does not advertise webp support for this request so let's bail return next.ServeHTTP(w, r) } repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) root := repl.ReplaceAll("{http.vars.root}", ".") cacheFile:=fmt.Sprintf("%s%s",caddyhttp.SanitizedPathJoin(root + "/.webpcache/", r.URL.Path +"?"), r.URL.RawQuery) origFile := caddyhttp.SanitizedPathJoin(root, r.URL.Path) cstat, err:=os.Stat(cacheFile) if err==nil { // Cache file exists origStat, err:=os.Stat(origFile) if err == nil && origStat.ModTime().Before(cstat.ModTime()) { // Serve cached file and fly away fh,err:=os.Open(cacheFile) if err == nil { // We couldn't open for whatever reason so let's not serve it w.Header().Set("content-type", "image/webp") w.Header().Add("webpstatus", "fromcache") w.WriteHeader(http.StatusOK) io.Copy(w,fh) return nil } } } // Let's encode and create a cached file cachePath,_:=path.Split(cacheFile) _,err=os.Stat(cachePath) if err != nil { err=os.MkdirAll(cachePath, 0600) if err != nil { log.Printf("Creating cache path: " + err.Error()) } } resp := &response{} err = next.ServeHTTP(resp, r) if err != nil { return err } ct := http.DetectContentType(resp.Body.Bytes()) var decoder func(io.Reader) (image.Image, error) switch ct { case "image/jpeg": decoder = jpeg.Decode case "image/png": decoder = png.Decode case "image/bmp": decoder = bmp.Decode case "image/gif": decoder = decodeGif default: return next.ServeHTTP(w, r) } img, err := decoder(bytes.NewReader(resp.Body.Bytes())) if err != nil || img == nil { log.Println(err) return next.ServeHTTP(w, r) } var buf bytes.Buffer err = webp.Encode(&buf, img, &webp.Options{Lossless: false, Quality: Quality}) if err != nil { log.Printf(err.Error()) return next.ServeHTTP(w, r) } b:=buf.Bytes() fh,err:=os.Create(cacheFile) fh.Write(b) // buf.WriteTo(fh) fh.Close() w.Header().Set("content-Type", "image/webp") w.Header().Add("webpstatus", "tocache") w.WriteHeader(http.StatusOK) w.Write(b) // buf.WriteTo(w) return nil } type response struct { header http.Header Body bytes.Buffer } func (s *response) Header() http.Header { return http.Header{} } func (s *response) Write(data []byte) (int, error) { s.Body.Write(data) return len(data), nil } func (s *response) WriteHeader(i int) { return }