From 04124a2ace60ebdd5098f90a3afa3d378a6ba5a6 Mon Sep 17 00:00:00 2001 From: Athurg Gooth Date: Fri, 19 May 2023 20:07:39 +0800 Subject: [PATCH] feat: generate thumbnail while get and improve thumbnail quality (#1680) * Use disintegration/imaging to optimize thumbnail quality * Generate thumbnail if not exists while GET it * Changes for `go mod tidy` * Changes for golang comments lint --------- Co-authored-by: Athurg Feng --- common/image.go | 61 ++++++++++++++++++++++++++-------------------- go.mod | 3 +++ go.sum | 4 +++ server/resource.go | 35 ++++++++++---------------- 4 files changed, 55 insertions(+), 48 deletions(-) diff --git a/common/image.go b/common/image.go index 6d9de484..cd71160d 100644 --- a/common/image.go +++ b/common/image.go @@ -4,18 +4,49 @@ import ( "bytes" "fmt" "image" - "image/jpeg" "image/png" + "os" + "path/filepath" + "strings" + + "github.com/disintegration/imaging" ) -const ThumbnailPath = ".thumbnail_cache" +const ( + ThumbnailDir = ".thumbnail_cache" + ThumbnailSize = 302 // Thumbnail size should be defined by frontend +) + +func ResizeImageFile(dst, src string, mime string) error { + srcBytes, err := os.ReadFile(src) + if err != nil { + return fmt.Errorf("Failed to open %s: %s", src, err) + } + + dstBytes, err := ResizeImageBlob(srcBytes, ThumbnailSize, mime) + if err != nil { + return fmt.Errorf("Failed to resise %s: %s", src, err) + } + + err = os.MkdirAll(filepath.Dir(dst), os.ModePerm) + if err != nil { + return fmt.Errorf("Failed to mkdir for %s: %s", dst, err) + } + + err = os.WriteFile(dst, dstBytes, 0666) + if err != nil { + return fmt.Errorf("Failed to write %s: %s", dst, err) + } + + return nil +} func ResizeImageBlob(data []byte, maxSize int, mime string) ([]byte, error) { var err error var oldImage image.Image - switch mime { + switch strings.ToLower(mime) { case "image/jpeg": oldImage, err = jpeg.Decode(bytes.NewReader(data)) case "image/png": @@ -28,29 +59,7 @@ func ResizeImageBlob(data []byte, maxSize int, mime string) ([]byte, error) { return nil, err } - bounds := oldImage.Bounds() - if bounds.Dx() <= maxSize && bounds.Dy() <= maxSize { - return data, nil - } - - oldBounds := oldImage.Bounds() - - dy := maxSize - r := float32(oldBounds.Dy()) / float32(maxSize) - dx := int(float32(oldBounds.Dx()) / r) - if oldBounds.Dx() > oldBounds.Dy() { - dx = maxSize - r = float32(oldBounds.Dx()) / float32(maxSize) - dy = int(float32(oldBounds.Dy()) / r) - } - - newBounds := image.Rect(0, 0, dx, dy) - newImage := image.NewRGBA(newBounds) - for x := 0; x < newBounds.Dx(); x++ { - for y := 0; y < newBounds.Dy(); y++ { - newImage.Set(x, y, oldImage.At(int(float32(x)*r), int(float32(y)*r))) - } - } + newImage := imaging.Resize(oldImage, maxSize, 0, imaging.NearestNeighbor) var newBuffer bytes.Buffer switch mime { diff --git a/go.mod b/go.mod index 357e61e6..0783fb25 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.13.12 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.51 github.com/aws/aws-sdk-go-v2/service/s3 v1.30.3 + github.com/disintegration/imaging v1.6.2 github.com/google/uuid v1.3.0 github.com/gorilla/feeds v1.1.1 github.com/labstack/echo/v4 v4.9.0 @@ -24,6 +25,8 @@ require ( golang.org/x/oauth2 v0.5.0 ) +require golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect + require ( github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.22 // indirect diff --git a/go.sum b/go.sum index eaa894e1..76e9ee82 100644 --- a/go.sum +++ b/go.sum @@ -92,6 +92,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -296,6 +298,8 @@ golang.org/x/exp v0.0.0-20230111222715-75897c7a292a h1:/YWeLOBWYV5WAQORVPkZF3Pq9 golang.org/x/exp v0.0.0-20230111222715-75897c7a292a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/server/resource.go b/server/resource.go index 240750f7..731d15d4 100644 --- a/server/resource.go +++ b/server/resource.go @@ -164,29 +164,11 @@ func (s *Server) registerResourceRoutes(g *echo.Group) { } if filetype == "image/jpeg" || filetype == "image/png" { - _, err := sourceFile.Seek(0, io.SeekStart) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to seek file").SetInternal(err) - } - - fileBytes, err := io.ReadAll(sourceFile) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to load file").SetInternal(err) - } - - thumbnailBytes, err := common.ResizeImageBlob(fileBytes, 302, filetype) + thumbnailPath := path.Join(s.Profile.Data, common.ThumbnailDir, publicID) + err := common.ResizeImageFile(thumbnailPath, filePath, filetype) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate thumbnail").SetInternal(err) } - - dir := filepath.Join(s.Profile.Data, common.ThumbnailPath) - if err = os.MkdirAll(dir, os.ModePerm); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create directory").SetInternal(err) - } - err = os.WriteFile(filepath.Join(dir, publicID), thumbnailBytes, 0666) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create thumbnail").SetInternal(err) - } } resourceCreate = &api.ResourceCreate{ @@ -346,7 +328,7 @@ func (s *Server) registerResourceRoutes(g *echo.Group) { log.Warn(fmt.Sprintf("failed to delete local file with path %s", resource.InternalPath), zap.Error(err)) } - thumbnailPath := filepath.Join(s.Profile.Data, common.ThumbnailPath, resource.PublicID) + thumbnailPath := path.Join(s.Profile.Data, common.ThumbnailDir, resource.PublicID) err = os.Remove(thumbnailPath) if err != nil { log.Warn(fmt.Sprintf("failed to delete local thumbnail with path %s", thumbnailPath), zap.Error(err)) @@ -442,9 +424,18 @@ func (s *Server) registerResourcePublicRoutes(g *echo.Group) { if resource.InternalPath != "" { resourcePath := resource.InternalPath if c.QueryParam("thumbnail") == "1" && (resource.Type == "image/jpeg" || resource.Type == "image/png") { - thumbnailPath := filepath.Join(s.Profile.Data, common.ThumbnailPath, resource.PublicID) + thumbnailPath := path.Join(s.Profile.Data, common.ThumbnailDir, resource.PublicID) if _, err := os.Stat(thumbnailPath); err == nil { resourcePath = thumbnailPath + } else if os.IsNotExist(err) { + err := common.ResizeImageFile(thumbnailPath, resourcePath, resource.Type) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to resize resource: %s", resourcePath)).SetInternal(err) + } + + resourcePath = thumbnailPath + } else { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to check resource thumbnail stat: %s", thumbnailPath)).SetInternal(err) } }