[chore] Only call imaging.Resize when necessary, use even tinier blurhashes (#3247)

* [chore] Use `imaging.Fit`, use even tinier blurhashes

* avoid calling resize if not necessary

* update blurhashes + thumb
This commit is contained in:
tobi 2024-08-29 17:43:14 +02:00 committed by GitHub
parent 277b043633
commit e10aa76612
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 60 additions and 53 deletions

View file

@ -858,7 +858,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
"static_url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/small/`+instanceAccount.AvatarMediaAttachment.ID+`.webp",`+` "static_url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/small/`+instanceAccount.AvatarMediaAttachment.ID+`.webp",`+`
"thumbnail_static_type": "image/webp", "thumbnail_static_type": "image/webp",
"thumbnail_description": "A bouncing little green peglin.", "thumbnail_description": "A bouncing little green peglin.",
"blurhash": "LE9as6M}4YtO%dRlWEt6Dmoxx?WC" "blurhash": "LF9kG$RR4YtP%dR+V^t5D,oxx?WC"
}`, string(instanceV2ThumbnailJson)) }`, string(instanceV2ThumbnailJson))
// double extra special bonus: now update the image description without changing the image // double extra special bonus: now update the image description without changing the image

View file

@ -206,7 +206,7 @@ func (suite *MediaCreateTestSuite) TestMediaCreateSuccessful() {
Y: 0.5, Y: 0.5,
}, },
}, *attachmentReply.Meta) }, *attachmentReply.Meta)
suite.Equal("LjCGfG#6RkRn_NvzRjWF?urqV@a$", *attachmentReply.Blurhash) suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", *attachmentReply.Blurhash)
suite.NotEmpty(attachmentReply.ID) suite.NotEmpty(attachmentReply.ID)
suite.NotEmpty(attachmentReply.URL) suite.NotEmpty(attachmentReply.URL)
suite.NotEmpty(attachmentReply.PreviewURL) suite.NotEmpty(attachmentReply.PreviewURL)
@ -291,7 +291,7 @@ func (suite *MediaCreateTestSuite) TestMediaCreateSuccessfulV2() {
Y: 0.5, Y: 0.5,
}, },
}, *attachmentReply.Meta) }, *attachmentReply.Meta)
suite.Equal("LjCGfG#6RkRn_NvzRjWF?urqV@a$", *attachmentReply.Blurhash) suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", *attachmentReply.Blurhash)
suite.NotEmpty(attachmentReply.ID) suite.NotEmpty(attachmentReply.ID)
suite.Nil(attachmentReply.URL) suite.Nil(attachmentReply.URL)
suite.NotEmpty(attachmentReply.PreviewURL) suite.NotEmpty(attachmentReply.PreviewURL)

View file

@ -276,7 +276,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcess() {
suite.Equal("image/jpeg", attachment.Thumbnail.ContentType) suite.Equal("image/jpeg", attachment.Thumbnail.ContentType)
suite.Equal(269739, attachment.File.FileSize) suite.Equal(269739, attachment.File.FileSize)
suite.Equal(22858, attachment.Thumbnail.FileSize) suite.Equal(22858, attachment.Thumbnail.FileSize)
suite.Equal("LjCGfG#6RkRn_NvzRjWF?urqV@a$", attachment.Blurhash) suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
// now make sure the attachment is in the database // now make sure the attachment is in the database
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID) dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
@ -429,7 +429,7 @@ func (suite *ManagerTestSuite) TestSlothVineProcess() {
suite.Equal("image/webp", attachment.Thumbnail.ContentType) suite.Equal("image/webp", attachment.Thumbnail.ContentType)
suite.Equal(312453, attachment.File.FileSize) suite.Equal(312453, attachment.File.FileSize)
suite.Equal(5648, attachment.Thumbnail.FileSize) suite.Equal(5648, attachment.Thumbnail.FileSize)
suite.Equal("LhIrNMt6Nsj[t7ayW.j[_4WBsWkB", attachment.Blurhash) suite.Equal("LfIYH~xtNskCxtfPW.kB_4aespof", attachment.Blurhash)
// now make sure the attachment is in the database // now make sure the attachment is in the database
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID) dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
@ -489,7 +489,7 @@ func (suite *ManagerTestSuite) TestLongerMp4Process() {
suite.Equal("image/webp", attachment.Thumbnail.ContentType) suite.Equal("image/webp", attachment.Thumbnail.ContentType)
suite.Equal(109569, attachment.File.FileSize) suite.Equal(109569, attachment.File.FileSize)
suite.Equal(2976, attachment.Thumbnail.FileSize) suite.Equal(2976, attachment.Thumbnail.FileSize)
suite.Equal("L8QJfm~qD%_3_3D%t7RjM{j[ofRj", attachment.Blurhash) suite.Equal("LJQJfm?bM{?b~qRjt7WBayWBofWB", attachment.Blurhash)
// now make sure the attachment is in the database // now make sure the attachment is in the database
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID) dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
@ -549,7 +549,7 @@ func (suite *ManagerTestSuite) TestBirdnestMp4Process() {
suite.Equal("image/webp", attachment.Thumbnail.ContentType) suite.Equal("image/webp", attachment.Thumbnail.ContentType)
suite.Equal(1409625, attachment.File.FileSize) suite.Equal(1409625, attachment.File.FileSize)
suite.Equal(14478, attachment.Thumbnail.FileSize) suite.Equal(14478, attachment.Thumbnail.FileSize)
suite.Equal("LKF~w1RjRO.99DM_RPaetkV?WCMw", attachment.Blurhash) suite.Equal("LJF?FZV@RO.99DM_RPWAx]V?ayMw", attachment.Blurhash)
// now make sure the attachment is in the database // now make sure the attachment is in the database
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID) dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
@ -657,7 +657,7 @@ func (suite *ManagerTestSuite) TestPngNoAlphaChannelProcess() {
suite.Equal("image/jpeg", attachment.Thumbnail.ContentType) suite.Equal("image/jpeg", attachment.Thumbnail.ContentType)
suite.Equal(17471, attachment.File.FileSize) suite.Equal(17471, attachment.File.FileSize)
suite.Equal(6446, attachment.Thumbnail.FileSize) suite.Equal(6446, attachment.Thumbnail.FileSize)
suite.Equal("LDQcrD%i-?aj%ho#M~RP~nf3~nt2", attachment.Blurhash) suite.Equal("LFQT7e.A%O%4?co$M}M{_1W9~TxV", attachment.Blurhash)
// now make sure the attachment is in the database // now make sure the attachment is in the database
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID) dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
@ -713,7 +713,7 @@ func (suite *ManagerTestSuite) TestPngAlphaChannelProcess() {
suite.Equal("image/webp", attachment.Thumbnail.ContentType) suite.Equal("image/webp", attachment.Thumbnail.ContentType)
suite.Equal(18832, attachment.File.FileSize) suite.Equal(18832, attachment.File.FileSize)
suite.Equal(3592, attachment.Thumbnail.FileSize) suite.Equal(3592, attachment.Thumbnail.FileSize)
suite.Equal("LBOW$@%i-rak%go#RSRP_1av~Ts+", attachment.Blurhash) suite.Equal("LCONII.A%Oxw?co#M}M{_1ac~TxV", attachment.Blurhash)
// now make sure the attachment is in the database // now make sure the attachment is in the database
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID) dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
@ -769,7 +769,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessWithCallback() {
suite.Equal("image/jpeg", attachment.Thumbnail.ContentType) suite.Equal("image/jpeg", attachment.Thumbnail.ContentType)
suite.Equal(269739, attachment.File.FileSize) suite.Equal(269739, attachment.File.FileSize)
suite.Equal(22858, attachment.Thumbnail.FileSize) suite.Equal(22858, attachment.Thumbnail.FileSize)
suite.Equal("LjCGfG#6RkRn_NvzRjWF?urqV@a$", attachment.Blurhash) suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
// now make sure the attachment is in the database // now make sure the attachment is in the database
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID) dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
@ -847,7 +847,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessWithDiskStorage() {
suite.Equal("image/jpeg", attachment.Thumbnail.ContentType) suite.Equal("image/jpeg", attachment.Thumbnail.ContentType)
suite.Equal(269739, attachment.File.FileSize) suite.Equal(269739, attachment.File.FileSize)
suite.Equal(22858, attachment.Thumbnail.FileSize) suite.Equal(22858, attachment.Thumbnail.FileSize)
suite.Equal("LjCGfG#6RkRn_NvzRjWF?urqV@a$", attachment.Blurhash) suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
// now make sure the attachment is in the database // now make sure the attachment is in the database
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID) dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)

View file

@ -34,6 +34,40 @@
"golang.org/x/image/webp" "golang.org/x/image/webp"
) )
const (
maxThumbWidth = 512
maxThumbHeight = 512
)
// thumbSize returns the dimensions to use for an input
// image of given width / height, for its outgoing thumbnail.
// This attempts to maintains the original image aspect ratio.
func thumbSize(width, height int, aspect float32) (int, int) {
switch {
// Simplest case, within bounds!
case width < maxThumbWidth &&
height < maxThumbHeight:
return width, height
// Width is larger side.
case width > height:
// i.e. height = newWidth * (height / width)
height = int(float32(maxThumbWidth) / aspect)
return maxThumbWidth, height
// Height is larger side.
case height > width:
// i.e. width = newHeight * (width / height)
width = int(float32(maxThumbHeight) * aspect)
return width, maxThumbHeight
// Square.
default:
return maxThumbWidth, maxThumbHeight
}
}
// generateThumb generates a thumbnail for the // generateThumb generates a thumbnail for the
// input file at path, resizing it to the given // input file at path, resizing it to the given
// dimensions and generating a blurhash if needed. // dimensions and generating a blurhash if needed.
@ -229,11 +263,17 @@ func generateNativeThumb(
img = imaging.Transverse(img) img = imaging.Transverse(img)
} }
// Resize image to dimens. // Resize image to dimens only if necessary.
img = imaging.Resize(img, if img.Bounds().Dx() > maxThumbWidth ||
width, height, img.Bounds().Dy() > maxThumbHeight {
imaging.Linear, // Note: We could call "imaging.Fit" here
) // but there's no point, as we've already
// calculated target dimensions beforehand.
img = imaging.Resize(img,
width, height,
imaging.Linear,
)
}
// Open output file at given path. // Open output file at given path.
outfile, err := os.Create(outpath) outfile, err := os.Create(outpath)
@ -255,7 +295,7 @@ func generateNativeThumb(
if needBlurhash { if needBlurhash {
// for generating blurhashes, it's more cost effective to // for generating blurhashes, it's more cost effective to
// lose detail since it's blurry, so make a tiny version. // lose detail since it's blurry, so make a tiny version.
tiny := imaging.Resize(img, 64, 64, imaging.NearestNeighbor) tiny := imaging.Resize(img, 32, 0, imaging.NearestNeighbor)
// Drop the larger image // Drop the larger image
// ref as soon as possible // ref as soon as possible
@ -294,7 +334,7 @@ func generateWebpBlurhash(filepath string) (string, error) {
// for generating blurhashes, it's more cost effective to // for generating blurhashes, it's more cost effective to
// lose detail since it's blurry, so make a tiny version. // lose detail since it's blurry, so make a tiny version.
tiny := imaging.Resize(img, 64, 64, imaging.NearestNeighbor) tiny := imaging.Resize(img, 32, 0, imaging.NearestNeighbor)
// Drop the larger image // Drop the larger image
// ref as soon as possible // ref as soon as possible

View file

@ -39,39 +39,6 @@ func getExtension(path string) string {
return "" return ""
} }
// thumbSize returns the dimensions to use for an input
// image of given width / height, for its outgoing thumbnail.
// This attempts to maintains the original image aspect ratio.
func thumbSize(width, height int, aspect float32) (int, int) {
const (
maxThumbWidth = 512
maxThumbHeight = 512
)
switch {
// Simplest case, within bounds!
case width < maxThumbWidth &&
height < maxThumbHeight:
return width, height
// Width is larger side.
case width > height:
// i.e. height = newWidth * (height / width)
height = int(float32(maxThumbWidth) / aspect)
return maxThumbWidth, height
// Height is larger side.
case height > width:
// i.e. width = newHeight * (width / height)
width = int(float32(maxThumbHeight) * aspect)
return width, maxThumbHeight
// Square.
default:
return maxThumbWidth, maxThumbHeight
}
}
// getMimeType returns a suitable mimetype for file extension. // getMimeType returns a suitable mimetype for file extension.
func getMimeType(ext string) string { func getMimeType(ext string) string {
const defaultType = "application/octet-stream" const defaultType = "application/octet-stream"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -1078,7 +1078,7 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
Thumbnail: gtsmodel.Thumbnail{ Thumbnail: gtsmodel.Thumbnail{
Path: "01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg", Path: "01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg",
ContentType: "image/jpeg", ContentType: "image/jpeg",
FileSize: 20394, FileSize: 20395,
URL: "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.webp", URL: "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.webp",
}, },
Avatar: util.Ptr(false), Avatar: util.Ptr(false),
@ -1124,7 +1124,7 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
Thumbnail: gtsmodel.Thumbnail{ Thumbnail: gtsmodel.Thumbnail{
Path: "062G5WYKY35KKD12EMSM3F8PJ8/attachment/small/01PFPMWK2FF0D9WMHEJHR07C3R.jpeg", Path: "062G5WYKY35KKD12EMSM3F8PJ8/attachment/small/01PFPMWK2FF0D9WMHEJHR07C3R.jpeg",
ContentType: "image/webp", ContentType: "image/webp",
FileSize: 20394, FileSize: 20395,
URL: "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/small/01PFPMWK2FF0D9WMHEJHR07C3R.webp", URL: "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/small/01PFPMWK2FF0D9WMHEJHR07C3R.webp",
}, },
Avatar: util.Ptr(false), Avatar: util.Ptr(false),