mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
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 <athurg@gooth.org>
This commit is contained in:
@ -4,18 +4,49 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
|
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"image/png"
|
"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) {
|
func ResizeImageBlob(data []byte, maxSize int, mime string) ([]byte, error) {
|
||||||
var err error
|
var err error
|
||||||
var oldImage image.Image
|
var oldImage image.Image
|
||||||
|
|
||||||
switch mime {
|
switch strings.ToLower(mime) {
|
||||||
case "image/jpeg":
|
case "image/jpeg":
|
||||||
oldImage, err = jpeg.Decode(bytes.NewReader(data))
|
oldImage, err = jpeg.Decode(bytes.NewReader(data))
|
||||||
case "image/png":
|
case "image/png":
|
||||||
@ -28,29 +59,7 @@ func ResizeImageBlob(data []byte, maxSize int, mime string) ([]byte, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
bounds := oldImage.Bounds()
|
newImage := imaging.Resize(oldImage, maxSize, 0, imaging.NearestNeighbor)
|
||||||
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)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var newBuffer bytes.Buffer
|
var newBuffer bytes.Buffer
|
||||||
switch mime {
|
switch mime {
|
||||||
|
3
go.mod
3
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/credentials v1.13.12
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.51
|
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/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/google/uuid v1.3.0
|
||||||
github.com/gorilla/feeds v1.1.1
|
github.com/gorilla/feeds v1.1.1
|
||||||
github.com/labstack/echo/v4 v4.9.0
|
github.com/labstack/echo/v4 v4.9.0
|
||||||
@ -24,6 +25,8 @@ require (
|
|||||||
golang.org/x/oauth2 v0.5.0
|
golang.org/x/oauth2 v0.5.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
|
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
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.22 // indirect
|
||||||
|
4
go.sum
4
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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.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.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
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/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-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-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-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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
@ -164,29 +164,11 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if filetype == "image/jpeg" || filetype == "image/png" {
|
if filetype == "image/jpeg" || filetype == "image/png" {
|
||||||
_, err := sourceFile.Seek(0, io.SeekStart)
|
thumbnailPath := path.Join(s.Profile.Data, common.ThumbnailDir, publicID)
|
||||||
if err != nil {
|
err := common.ResizeImageFile(thumbnailPath, filePath, filetype)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate thumbnail").SetInternal(err)
|
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{
|
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))
|
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)
|
err = os.Remove(thumbnailPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn(fmt.Sprintf("failed to delete local thumbnail with path %s", thumbnailPath), zap.Error(err))
|
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 != "" {
|
if resource.InternalPath != "" {
|
||||||
resourcePath := resource.InternalPath
|
resourcePath := resource.InternalPath
|
||||||
if c.QueryParam("thumbnail") == "1" && (resource.Type == "image/jpeg" || resource.Type == "image/png") {
|
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 {
|
if _, err := os.Stat(thumbnailPath); err == nil {
|
||||||
resourcePath = thumbnailPath
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user