chore: merge #10 image-optimzation into dev
feat!: refactor the api endpoint to follow and fix #9
This commit is contained in:
196
api/image.go
196
api/image.go
@@ -2,184 +2,108 @@ package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"errors"
|
||||
"image"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"math"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/chai2010/webp"
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/sunshineplan/imgconv"
|
||||
)
|
||||
|
||||
func Scale(i image.Image, s float64) image.Image {
|
||||
r := i.Bounds()
|
||||
w, h := float64(r.Max.X), float64(r.Max.Y)
|
||||
func ImgOptimize(i image.Image, threshold int) image.Image {
|
||||
w := i.Bounds().Max.X
|
||||
|
||||
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)
|
||||
if threshold >= w {
|
||||
return i
|
||||
}
|
||||
|
||||
return imaging.Resize(i, nw, nh, imaging.CatmullRom)
|
||||
d := w / threshold
|
||||
return imgconv.Resize(i, &imgconv.ResizeOption{Width: w / d})
|
||||
}
|
||||
|
||||
func errorHelper(w http.ResponseWriter) func(msg string, err error, status int) bool {
|
||||
return func(msg string, err error, status int) bool {
|
||||
if err != nil {
|
||||
w.WriteHeader(status)
|
||||
_, err = w.Write([]byte(msg + "\n Error: " + err.Error()))
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte("Error trying to return error code (somehow):\n" + err.Error()))
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func Image(w http.ResponseWriter, r *http.Request) {
|
||||
error := errorHelper(w)
|
||||
|
||||
params, err := url.ParseQuery(r.URL.RawQuery)
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
if error("Error trying to parse query parameters", err, http.StatusInternalServerError) {
|
||||
return
|
||||
}
|
||||
|
||||
var u *url.URL
|
||||
if _, some := params["url"]; !some {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte("\"url\" parameter missing"))
|
||||
error("\"url\" parameter missing", errors.New("Missing argument"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
u, err := url.Parse(params.Get("url"))
|
||||
if error("\"url\" is not a valid URL string", err, http.StatusBadRequest) {
|
||||
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() == "" {
|
||||
if r.URL.Scheme == "" {
|
||||
u.Scheme = "https"
|
||||
} else {
|
||||
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
|
||||
if u.Hostname() == "" {
|
||||
if r.URL.Scheme == "" {
|
||||
u.Scheme = "https"
|
||||
} 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
|
||||
u.Scheme = r.URL.Scheme
|
||||
}
|
||||
u.Host = r.Host
|
||||
}
|
||||
|
||||
imgRes, err := http.Get(u.String())
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
if _, some := params["threshold"]; !some {
|
||||
error("\"threshold\" parameter missing", errors.New("Missing argument"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if imgRes.StatusCode != 200 {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte(fmt.Sprintf("Url returned a status code of %v", imgRes.StatusCode)))
|
||||
threshold, err := strconv.Atoi(params.Get("threshold"))
|
||||
if error("\"threshold\" parameter is not a valid integer", err, http.StatusBadRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(imgRes.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
res, err := http.Get(u.String())
|
||||
if error("Error trying to fetch the image", err, http.StatusInternalServerError) {
|
||||
return
|
||||
}
|
||||
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||||
error(
|
||||
"Error trying to fetch the image, response is a non 2XX code",
|
||||
errors.New("Status code: "+res.Status),
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if error("Error trying to read the image data", err, http.StatusInternalServerError) {
|
||||
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()))
|
||||
img, err := imgconv.Decode(bytes.NewReader(data))
|
||||
if error("Error trying to decode the image", err, http.StatusInternalServerError) {
|
||||
return
|
||||
}
|
||||
|
||||
if scale != 0 {
|
||||
img = Scale(img, float64(scale))
|
||||
} else if width > 0 && height > 0 {
|
||||
img = imaging.Resize(img, width, height, imaging.CatmullRom)
|
||||
}
|
||||
img = ImgOptimize(img, threshold)
|
||||
|
||||
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()))
|
||||
err = webp.Encode(w, img, &webp.Options{Lossless: true})
|
||||
if error("Error trying to encode the image", err, http.StatusInternalServerError) {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Cache-Control", "max-age=604800, stale-while-revalidate=86400, stale-if-error=86400")
|
||||
w.Header().Add("CDN-Cache-Control", "max-age=604800")
|
||||
w.Header().Add("Content-Type", mime)
|
||||
w.Header().Add("Content-Type", "image/webp")
|
||||
}
|
||||
|
||||
@@ -6,23 +6,23 @@ 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)) }
|
||||
srcset={ "/api/image?threshold=1536&url=" + url.PathEscape(string(src)) }
|
||||
/>
|
||||
<source
|
||||
media="(min-width: 1280px)"
|
||||
srcset={ "/api/image?scale=-7&url=" + url.PathEscape(string(src)) }
|
||||
srcset={ "/api/image?threshold=1280&url=" + url.PathEscape(string(src)) }
|
||||
/>
|
||||
<source
|
||||
media="(min-width: 1024px)"
|
||||
srcset={ "/api/image?scale=-8&url=" + url.PathEscape(string(src)) }
|
||||
srcset={ "/api/image?threshold=1024&url=" + url.PathEscape(string(src)) }
|
||||
/>
|
||||
<source
|
||||
media="(min-width: 768px)"
|
||||
srcset={ "/api/image?scale=-9&url=" + url.PathEscape(string(src)) }
|
||||
srcset={ "/api/image?threshold=768&url=" + url.PathEscape(string(src)) }
|
||||
/>
|
||||
<source
|
||||
media="(min-width: 640px)"
|
||||
srcset={ "/api/image?scale=-10&url=" + url.PathEscape(string(src)) }
|
||||
srcset={ "/api/image?threshold=640&url=" + url.PathEscape(string(src)) }
|
||||
/>
|
||||
<img src={ string(src) } alt={ alt } class="w-100% h-100%"/>
|
||||
</picture>
|
||||
|
||||
17
go.mod
17
go.mod
@@ -2,10 +2,21 @@ module www
|
||||
|
||||
go 1.22.2
|
||||
|
||||
require github.com/a-h/templ v0.2.697
|
||||
require (
|
||||
github.com/a-h/templ v0.2.697
|
||||
github.com/chai2010/webp v1.1.1
|
||||
github.com/sunshineplan/imgconv v1.1.10
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/chai2010/webp v1.1.1 // indirect
|
||||
github.com/disintegration/imaging v1.6.2 // indirect
|
||||
github.com/hhrutter/lzw v1.0.0 // indirect
|
||||
github.com/hhrutter/tiff v1.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/pdfcpu/pdfcpu v0.8.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sunshineplan/pdf v1.0.7 // indirect
|
||||
golang.org/x/image v0.17.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
27
go.sum
27
go.sum
@@ -2,11 +2,30 @@ 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=
|
||||
github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0=
|
||||
github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo=
|
||||
github.com/hhrutter/tiff v1.0.1 h1:MIus8caHU5U6823gx7C6jrfoEvfSTGtEFRiM8/LOzC0=
|
||||
github.com/hhrutter/tiff v1.0.1/go.mod h1:zU/dNgDm0cMIa8y8YwcYBeuEEveI4B0owqHyiPpJPHc=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/pdfcpu/pdfcpu v0.8.0 h1:SuEB4uVsPFz1nb802r38YpFpj9TtZh/oB0bGG34IRZw=
|
||||
github.com/pdfcpu/pdfcpu v0.8.0/go.mod h1:jj03y/KKrwigt5xCi8t7px2mATcKuOzkIOoCX62yMho=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/sunshineplan/imgconv v1.1.10 h1:EJZVXLwmvmKgEA1KIJpNSzSssjGaZMdtOLycGFyzxQA=
|
||||
github.com/sunshineplan/imgconv v1.1.10/go.mod h1:de9NsLFCMW2JVom3mjRZu3GceLFwkEIEkf1EGS5rDX4=
|
||||
github.com/sunshineplan/pdf v1.0.7 h1:62xlc079jh4tGLDjiihyyhwVFkn0IsxLyDpHplbG9Ew=
|
||||
github.com/sunshineplan/pdf v1.0.7/go.mod h1:QsEmZCWBE3uFK8PCrM0pua1WDWLNU77YusiDEcY56OQ=
|
||||
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=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
|
||||
Reference in New Issue
Block a user