From 2a1c79ffdc63736f79f83f50c69d499a95a0bb7d Mon Sep 17 00:00:00 2001 From: "Gustavo \"Guz\" L. de Mello" Date: Tue, 11 Jun 2024 16:31:51 -0300 Subject: [PATCH] feat: image component and api --- api/hello.go | 45 ----------- api/image.go | 180 +++++++++++++++++++++++++++++++++++++++++ components/image.templ | 29 +++++++ go.mod | 6 ++ go.sum | 8 ++ pages/homepage.templ | 4 +- 6 files changed, 226 insertions(+), 46 deletions(-) delete mode 100644 api/hello.go create mode 100644 api/image.go create mode 100644 components/image.templ diff --git a/api/hello.go b/api/hello.go deleted file mode 100644 index 7095201..0000000 --- a/api/hello.go +++ /dev/null @@ -1,45 +0,0 @@ -package api - -import ( - "encoding/json" - "fmt" - "io" - "math/rand/v2" - "net/http" -) - -type helloObj struct { - Language string - Hello string -} - -func getHelloList() ([]helloObj, error) { - res, err := http.Get("https://raw.githubusercontent.com/novellac/multilanguage-hello-json/master/hello.json") - if err != nil { - return nil, err - } - bytes, err := io.ReadAll(res.Body) - if err != nil { - return nil, err - } - - var hellos []helloObj - err = json.Unmarshal(bytes, &hellos) - if err != nil { - return nil, err - } - - return hellos, nil -} - -func Hello(w http.ResponseWriter, r *http.Request) { - hellos, err := getHelloList() - var hello string - if err != nil { - hello = "Welcome!" - } else { - hello = hellos[rand.IntN(len(hellos)-1)].Hello - } - - fmt.Fprint(w, hello) -} diff --git a/api/image.go b/api/image.go new file mode 100644 index 0000000..ce219bc --- /dev/null +++ b/api/image.go @@ -0,0 +1,180 @@ +package api + +import ( + "bytes" + "fmt" + "image" + "image/gif" + "image/jpeg" + "image/png" + "io" + "math" + "mime" + "net/http" + "net/url" + "strconv" + + "github.com/chai2010/webp" + "github.com/disintegration/imaging" +) + +func Scale(i image.Image, s float64) image.Image { + r := i.Bounds() + w, h := float64(r.Max.X), float64(r.Max.Y) + + var nw, nh int + if s < 0 { + s = s * -1 + nw, nh = int(math.Round(w/s)), int(math.Round(h/s)) + } else if s > 0 { + s = s * -1 + nw, nh = int(math.Round(w*s)), int(math.Round(h*s)) + } else { + nw, nh = int(w), int(h) + } + + return imaging.Resize(i, nw, nh, imaging.CatmullRom) +} + +func Image(w http.ResponseWriter, r *http.Request) { + params, err := url.ParseQuery(r.URL.RawQuery) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(err.Error())) + return + } + + var u *url.URL + if _, some := params["url"]; !some { + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte("\"url\" parameter missing")) + return + } else { + u, err = url.Parse(params.Get("url")) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(err.Error())) + return + } + + if u.Hostname() == "" { + u.Scheme = r.URL.Scheme + u.Host = r.Host + } + } + + var scale, width, height int + if _, some := params["scale"]; !some { + if _, some := params["width"]; !some { + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte("\"width\" parameter missing")) + return + } else { + width, err = strconv.Atoi(params.Get("width")) + + if err != nil { + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte("\"width\" parameter is not a valid integer")) + return + } + } + if _, some := params["height"]; !some { + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte("\"width\" parameter missing")) + return + } else { + height, err = strconv.Atoi(params.Get("height")) + + if err != nil { + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte("\"height\" parameter is not a valid integer")) + return + } + } + } else { + scale, err = strconv.Atoi(params.Get("scale")) + + if err != nil { + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte("\"scale\" parameter is not a valid integer")) + return + } + } + + imgRes, err := http.Get(u.String()) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(err.Error())) + return + } + if imgRes.StatusCode != 200 { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(fmt.Sprintf("Url returned a status code of %v", imgRes.StatusCode))) + return + } + + data, err := io.ReadAll(imgRes.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(err.Error())) + return + } + + mime := mime.TypeByExtension(u.String()) + if mime == "" { + mime = http.DetectContentType(data) + } + + var img image.Image + reader := bytes.NewReader(data) + switch mime { + case "image/png": + img, err = png.Decode(reader) + case "image/jpeg": + img, err = jpeg.Decode(reader) + case "image/gif": + img, err = gif.Decode(reader) + case "image/webp": + img, err = webp.Decode(reader) + default: + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte("image is not either of \"jpeg\", \"png\", \"gif\", or \"webp\"")) + return + } + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("Error decoding the image:\n" + err.Error())) + return + } + + if scale != 0 { + img = Scale(img, float64(scale)) + } else if width > 0 && height > 0 { + img = imaging.Resize(img, width, height, imaging.CatmullRom) + } + + switch mime { + case "image/png": + err = png.Encode(w, img) + case "image/jpeg": + err = jpeg.Encode(w, img, &jpeg.Options{Quality: 70}) + case "image/gif": + err = gif.Encode(w, img, &gif.Options{NumColors: 256}) + case "image/webp": + err = webp.Encode(w, img, &webp.Options{Lossless: true}) + default: + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte("\"type\" parameter is not either of \"jpeg\", \"png\", \"gif\", or \"webp\"")) + return + } + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("Error encoding the image:\n" + err.Error())) + return + } + + w.Header().Add("Cache-Control", "max-age=604800, stale-while-revalidate=86400, stale-if-error=86400") + w.Header().Add("Content-Type", mime) +} diff --git a/components/image.templ b/components/image.templ new file mode 100644 index 0000000..e6368c1 --- /dev/null +++ b/components/image.templ @@ -0,0 +1,29 @@ +package components + +import "net/url" + +templ Image(src templ.SafeURL, alt string, class string) { + + + + + + + { + +} diff --git a/go.mod b/go.mod index 23152ec..d9ba736 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,9 @@ module www go 1.22.2 require github.com/a-h/templ v0.2.697 + +require ( + github.com/chai2010/webp v1.1.1 // indirect + github.com/disintegration/imaging v1.6.2 // indirect + golang.org/x/image v0.17.0 // indirect +) diff --git a/go.sum b/go.sum index 3fcc2a0..42d66e2 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,12 @@ github.com/a-h/templ v0.2.697 h1:OILxtWvD0NRJaoCOiZCopRDPW8paroKlGsrAiHLykNE= github.com/a-h/templ v0.2.697/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8= +github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk= +github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.17.0 h1:nTRVVdajgB8zCMZVsViyzhnMKPwYeroEERRC64JuLco= +golang.org/x/image v0.17.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/pages/homepage.templ b/pages/homepage.templ index d0b244f..462c488 100644 --- a/pages/homepage.templ +++ b/pages/homepage.templ @@ -140,7 +140,9 @@ templ Homepage(props HomepageProps) {
for _, img := range props.Images { -
+
+ @components.Image(templ.SafeURL(img), "", "block max-w-100%") +
}