feat: image component and api

This commit is contained in:
Gustavo "Guz" L. de Mello
2024-06-11 16:31:51 -03:00
parent 7244e5503d
commit 2a1c79ffdc
6 changed files with 226 additions and 46 deletions

View File

@@ -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)
}

180
api/image.go Normal file
View File

@@ -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)
}

29
components/image.templ Normal file
View File

@@ -0,0 +1,29 @@
package components
import "net/url"
templ Image(src templ.SafeURL, alt string, class string) {
<picture class={ class }>
<source
media="(min-width: 1536px)"
srcset={ "/api/image?scale=-6&url=" + url.PathEscape(string(src)) }
/>
<source
media="(min-width: 1280px)"
srcset={ "/api/image?scale=-7&url=" + url.PathEscape(string(src)) }
/>
<source
media="(min-width: 1024px)"
srcset={ "/api/image?scale=-8&url=" + url.PathEscape(string(src)) }
/>
<source
media="(min-width: 768px)"
srcset={ "/api/image?scale=-9&url=" + url.PathEscape(string(src)) }
/>
<source
media="(min-width: 640px)"
srcset={ "/api/image?scale=-10&url=" + url.PathEscape(string(src)) }
/>
<img src={ string(src) } alt={ alt } class="w-100% h-100%"/>
</picture>
}

6
go.mod
View File

@@ -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
)

8
go.sum
View File

@@ -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=

View File

@@ -140,7 +140,9 @@ templ Homepage(props HomepageProps) {
</hgroup>
<div class="flex flex-wrap flex-col w-100% max-h-65vh md:max-h-70vh overflow-hidden">
for _, img := range props.Images {
<div class="w-50% md:w-33.3%"><img src={ img } class="max-w-100%"/></div>
<div class="w-50% md:w-33.3%">
@components.Image(templ.SafeURL(img), "", "block max-w-100%")
</div>
}
</div>
</div>