diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/api/client/instance/instancepatch_test.go | 2 | ||||
| -rw-r--r-- | internal/api/client/media/mediacreate_test.go | 4 | ||||
| -rw-r--r-- | internal/media/imaging.go | 623 | ||||
| -rw-r--r-- | internal/media/manager_test.go | 16 | ||||
| -rw-r--r-- | internal/media/thumbnail.go | 44 | 
5 files changed, 653 insertions, 36 deletions
diff --git a/internal/api/client/instance/instancepatch_test.go b/internal/api/client/instance/instancepatch_test.go index 6148ed93e..8b099984d 100644 --- a/internal/api/client/instance/instancepatch_test.go +++ b/internal/api/client/instance/instancepatch_test.go @@ -858,7 +858,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {    "static_url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/small/`+instanceAccount.AvatarMediaAttachment.ID+`.webp",`+`    "thumbnail_static_type": "image/webp",    "thumbnail_description": "A bouncing little green peglin.", -  "blurhash": "LF9kG$RR4YtP%dR+V^t5D,oxx?WC" +  "blurhash": "LE9801Rl4Yt5%dWCV]t5Dmoex?WC"  }`, string(instanceV2ThumbnailJson))  	// double extra special bonus: now update the image description without changing the image diff --git a/internal/api/client/media/mediacreate_test.go b/internal/api/client/media/mediacreate_test.go index e7f98d6d7..2eec8341f 100644 --- a/internal/api/client/media/mediacreate_test.go +++ b/internal/api/client/media/mediacreate_test.go @@ -206,7 +206,7 @@ func (suite *MediaCreateTestSuite) TestMediaCreateSuccessful() {  			Y: 0.5,  		},  	}, *attachmentReply.Meta) -	suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", *attachmentReply.Blurhash) +	suite.Equal("LiB|W-#6RQR.~qvzRjWF_3rqV@a$", *attachmentReply.Blurhash)  	suite.NotEmpty(attachmentReply.ID)  	suite.NotEmpty(attachmentReply.URL)  	suite.NotEmpty(attachmentReply.PreviewURL) @@ -291,7 +291,7 @@ func (suite *MediaCreateTestSuite) TestMediaCreateSuccessfulV2() {  			Y: 0.5,  		},  	}, *attachmentReply.Meta) -	suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", *attachmentReply.Blurhash) +	suite.Equal("LiB|W-#6RQR.~qvzRjWF_3rqV@a$", *attachmentReply.Blurhash)  	suite.NotEmpty(attachmentReply.ID)  	suite.Nil(attachmentReply.URL)  	suite.NotEmpty(attachmentReply.PreviewURL) diff --git a/internal/media/imaging.go b/internal/media/imaging.go new file mode 100644 index 000000000..a9f73a066 --- /dev/null +++ b/internal/media/imaging.go @@ -0,0 +1,623 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program.  If not, see <http://www.gnu.org/licenses/>. + +package media + +import ( +	"image" +	"image/color" +	"math" +) + +// NOTE: +// the following code is borrowed from +// github.com/disintegration/imaging +// and collapses in some places for our +// particular usecases and with parallel() +// function (spans work across goroutines) +// removed, instead working synchronously. +// +// at gotosocial we take particular +// care about where we spawn goroutines +// to ensure we're in control of the +// amount of concurrency in relation +// to the amount configured by user. + +// resizeDownLinear resizes image to given width x height using linear resampling. +// This is specifically optimized for resizing down (i.e. smaller), else is noop. +func resizeDownLinear(img image.Image, width, height int) image.Image { +	srcW, srcH := img.Bounds().Dx(), img.Bounds().Dy() +	if srcW <= 0 || srcH <= 0 || +		width < 0 || height < 0 { +		return &image.NRGBA{} +	} + +	if width == 0 { +		// If no width is given, use aspect preserving width. +		tmp := float64(height) * float64(srcW) / float64(srcH) +		width = int(math.Max(1.0, math.Floor(tmp+0.5))) +	} + +	if height == 0 { +		// If no height is given, use aspect preserving height. +		tmp := float64(width) * float64(srcH) / float64(srcW) +		height = int(math.Max(1.0, math.Floor(tmp+0.5))) +	} + +	if width < srcW { +		// Width is smaller, resize horizontally. +		img = resizeHorizontalLinear(img, width) +	} + +	if height < srcH { +		// Height is smaller, resize vertically. +		img = resizeVerticalLinear(img, height) +	} + +	return img +} + +// flipH flips the image horizontally (left to right). +func flipH(img image.Image) image.Image { +	src := newScanner(img) +	dstW := src.w +	dstH := src.h +	rowSize := dstW * 4 +	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH)) +	for y := 0; y < dstH; y++ { +		i := y * dst.Stride +		srcY := y +		src.scan(0, srcY, src.w, srcY+1, dst.Pix[i:i+rowSize]) +		reverse(dst.Pix[i : i+rowSize]) +	} +	return dst +} + +// flipV flips the image vertically (from top to bottom). +func flipV(img image.Image) image.Image { +	src := newScanner(img) +	dstW := src.w +	dstH := src.h +	rowSize := dstW * 4 +	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH)) +	for y := 0; y < dstH; y++ { +		i := y * dst.Stride +		srcY := dstH - y - 1 +		src.scan(0, srcY, src.w, srcY+1, dst.Pix[i:i+rowSize]) +	} +	return dst +} + +// rotate90 rotates the image 90 counter-clockwise. +func rotate90(img image.Image) image.Image { +	src := newScanner(img) +	dstW := src.h +	dstH := src.w +	rowSize := dstW * 4 +	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH)) +	for y := 0; y < dstH; y++ { +		i := y * dst.Stride +		srcX := dstH - y - 1 +		src.scan(srcX, 0, srcX+1, src.h, dst.Pix[i:i+rowSize]) +	} +	return dst +} + +// rotate180 rotates the image 180 counter-clockwise. +func rotate180(img image.Image) image.Image { +	src := newScanner(img) +	dstW := src.w +	dstH := src.h +	rowSize := dstW * 4 +	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH)) +	for y := 0; y < dstH; y++ { +		i := y * dst.Stride +		srcY := dstH - y - 1 +		src.scan(0, srcY, src.w, srcY+1, dst.Pix[i:i+rowSize]) +		reverse(dst.Pix[i : i+rowSize]) +	} +	return dst +} + +// rotate270 rotates the image 270 counter-clockwise. +func rotate270(img image.Image) image.Image { +	src := newScanner(img) +	dstW := src.h +	dstH := src.w +	rowSize := dstW * 4 +	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH)) +	for y := 0; y < dstH; y++ { +		i := y * dst.Stride +		srcX := y +		src.scan(srcX, 0, srcX+1, src.h, dst.Pix[i:i+rowSize]) +		reverse(dst.Pix[i : i+rowSize]) +	} +	return dst +} + +// transpose flips the image horizontally and rotates 90 counter-clockwise. +func transpose(img image.Image) image.Image { +	src := newScanner(img) +	dstW := src.h +	dstH := src.w +	rowSize := dstW * 4 +	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH)) +	for y := 0; y < dstH; y++ { +		i := y * dst.Stride +		srcX := y +		src.scan(srcX, 0, srcX+1, src.h, dst.Pix[i:i+rowSize]) +	} +	return dst +} + +// transverse flips the image vertically and rotates 90 counter-clockwise. +func transverse(img image.Image) image.Image { +	src := newScanner(img) +	dstW := src.h +	dstH := src.w +	rowSize := dstW * 4 +	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH)) +	for y := 0; y < dstH; y++ { +		i := y * dst.Stride +		srcX := dstH - y - 1 +		src.scan(srcX, 0, srcX+1, src.h, dst.Pix[i:i+rowSize]) +		reverse(dst.Pix[i : i+rowSize]) +	} +	return dst +} + +// resizeHorizontalLinear resizes image to given width using linear resampling. +func resizeHorizontalLinear(img image.Image, dstWidth int) image.Image { +	src := newScanner(img) +	dst := image.NewRGBA(image.Rect(0, 0, dstWidth, src.h)) +	weights := precomputeWeightsLinear(dstWidth, src.w) +	scanLine := make([]uint8, src.w*4) +	for y := 0; y < src.h; y++ { +		src.scan(0, y, src.w, y+1, scanLine) +		j0 := y * dst.Stride +		for x := range weights { +			var r, g, b, a float64 +			for _, w := range weights[x] { +				i := w.index * 4 +				s := scanLine[i : i+4 : i+4] +				aw := float64(s[3]) * w.weight +				r += float64(s[0]) * aw +				g += float64(s[1]) * aw +				b += float64(s[2]) * aw +				a += aw +			} +			if a != 0 { +				aInv := 1 / a +				j := j0 + x*4 +				d := dst.Pix[j : j+4 : j+4] +				d[0] = clampFloat(r * aInv) +				d[1] = clampFloat(g * aInv) +				d[2] = clampFloat(b * aInv) +				d[3] = clampFloat(a) +			} +		} +	} +	return dst +} + +// resizeVerticalLinear resizes image to given height using linear resampling. +func resizeVerticalLinear(img image.Image, height int) image.Image { +	src := newScanner(img) +	dst := image.NewNRGBA(image.Rect(0, 0, src.w, height)) +	weights := precomputeWeightsLinear(height, src.h) +	scanLine := make([]uint8, src.h*4) +	for x := 0; x < src.w; x++ { +		src.scan(x, 0, x+1, src.h, scanLine) +		for y := range weights { +			var r, g, b, a float64 +			for _, w := range weights[y] { +				i := w.index * 4 +				s := scanLine[i : i+4 : i+4] +				aw := float64(s[3]) * w.weight +				r += float64(s[0]) * aw +				g += float64(s[1]) * aw +				b += float64(s[2]) * aw +				a += aw +			} +			if a != 0 { +				aInv := 1 / a +				j := y*dst.Stride + x*4 +				d := dst.Pix[j : j+4 : j+4] +				d[0] = clampFloat(r * aInv) +				d[1] = clampFloat(g * aInv) +				d[2] = clampFloat(b * aInv) +				d[3] = clampFloat(a) +			} +		} +	} +	return dst +} + +type indexWeight struct { +	index  int +	weight float64 +} + +func precomputeWeightsLinear(dstSize, srcSize int) [][]indexWeight { +	du := float64(srcSize) / float64(dstSize) +	scale := du +	if scale < 1.0 { +		scale = 1.0 +	} + +	ru := math.Ceil(scale) +	out := make([][]indexWeight, dstSize) +	tmp := make([]indexWeight, 0, dstSize*int(ru+2)*2) + +	for v := 0; v < dstSize; v++ { +		fu := (float64(v)+0.5)*du - 0.5 + +		begin := int(math.Ceil(fu - ru)) +		if begin < 0 { +			begin = 0 +		} +		end := int(math.Floor(fu + ru)) +		if end > srcSize-1 { +			end = srcSize - 1 +		} + +		var sum float64 +		for u := begin; u <= end; u++ { +			w := resampleLinear((float64(u) - fu) / scale) +			if w != 0 { +				sum += w +				tmp = append(tmp, indexWeight{index: u, weight: w}) +			} +		} +		if sum != 0 { +			for i := range tmp { +				tmp[i].weight /= sum +			} +		} + +		out[v] = tmp +		tmp = tmp[len(tmp):] +	} + +	return out +} + +// resampleLinear is the resample kernel func for linear filtering. +func resampleLinear(x float64) float64 { +	x = math.Abs(x) +	if x < 1.0 { +		return 1.0 - x +	} +	return 0 +} + +// scanner wraps an image.Image for +// easier size access and image type +// agnostic access to data at coords. +type scanner struct { +	image   image.Image +	w, h    int +	palette []color.NRGBA +} + +// newScanner wraps an image.Image in scanner{} type. +func newScanner(img image.Image) *scanner { +	b := img.Bounds() +	s := &scanner{ +		image: img, + +		w: b.Dx(), +		h: b.Dy(), +	} +	if img, ok := img.(*image.Paletted); ok { +		s.palette = make([]color.NRGBA, len(img.Palette)) +		for i := 0; i < len(img.Palette); i++ { +			s.palette[i] = color.NRGBAModel.Convert(img.Palette[i]).(color.NRGBA) +		} +	} +	return s +} + +// scan scans the given rectangular region of the image into dst. +func (s *scanner) scan(x1, y1, x2, y2 int, dst []uint8) { +	switch img := s.image.(type) { +	case *image.NRGBA: +		size := (x2 - x1) * 4 +		j := 0 +		i := y1*img.Stride + x1*4 +		if size == 4 { +			for y := y1; y < y2; y++ { +				d := dst[j : j+4 : j+4] +				s := img.Pix[i : i+4 : i+4] +				d[0] = s[0] +				d[1] = s[1] +				d[2] = s[2] +				d[3] = s[3] +				j += size +				i += img.Stride +			} +		} else { +			for y := y1; y < y2; y++ { +				copy(dst[j:j+size], img.Pix[i:i+size]) +				j += size +				i += img.Stride +			} +		} + +	case *image.NRGBA64: +		j := 0 +		for y := y1; y < y2; y++ { +			i := y*img.Stride + x1*8 +			for x := x1; x < x2; x++ { +				s := img.Pix[i : i+8 : i+8] +				d := dst[j : j+4 : j+4] +				d[0] = s[0] +				d[1] = s[2] +				d[2] = s[4] +				d[3] = s[6] +				j += 4 +				i += 8 +			} +		} + +	case *image.RGBA: +		j := 0 +		for y := y1; y < y2; y++ { +			i := y*img.Stride + x1*4 +			for x := x1; x < x2; x++ { +				d := dst[j : j+4 : j+4] +				a := img.Pix[i+3] +				switch a { +				case 0: +					d[0] = 0 +					d[1] = 0 +					d[2] = 0 +					d[3] = a +				case 0xff: +					s := img.Pix[i : i+4 : i+4] +					d[0] = s[0] +					d[1] = s[1] +					d[2] = s[2] +					d[3] = a +				default: +					s := img.Pix[i : i+4 : i+4] +					r16 := uint16(s[0]) +					g16 := uint16(s[1]) +					b16 := uint16(s[2]) +					a16 := uint16(a) +					d[0] = uint8(r16 * 0xff / a16) +					d[1] = uint8(g16 * 0xff / a16) +					d[2] = uint8(b16 * 0xff / a16) +					d[3] = a +				} +				j += 4 +				i += 4 +			} +		} + +	case *image.RGBA64: +		j := 0 +		for y := y1; y < y2; y++ { +			i := y*img.Stride + x1*8 +			for x := x1; x < x2; x++ { +				s := img.Pix[i : i+8 : i+8] +				d := dst[j : j+4 : j+4] +				a := s[6] +				switch a { +				case 0: +					d[0] = 0 +					d[1] = 0 +					d[2] = 0 +				case 0xff: +					d[0] = s[0] +					d[1] = s[2] +					d[2] = s[4] +				default: +					r32 := uint32(s[0])<<8 | uint32(s[1]) +					g32 := uint32(s[2])<<8 | uint32(s[3]) +					b32 := uint32(s[4])<<8 | uint32(s[5]) +					a32 := uint32(s[6])<<8 | uint32(s[7]) +					d[0] = uint8((r32 * 0xffff / a32) >> 8) +					d[1] = uint8((g32 * 0xffff / a32) >> 8) +					d[2] = uint8((b32 * 0xffff / a32) >> 8) +				} +				d[3] = a +				j += 4 +				i += 8 +			} +		} + +	case *image.Gray: +		j := 0 +		for y := y1; y < y2; y++ { +			i := y*img.Stride + x1 +			for x := x1; x < x2; x++ { +				c := img.Pix[i] +				d := dst[j : j+4 : j+4] +				d[0] = c +				d[1] = c +				d[2] = c +				d[3] = 0xff +				j += 4 +				i++ +			} +		} + +	case *image.Gray16: +		j := 0 +		for y := y1; y < y2; y++ { +			i := y*img.Stride + x1*2 +			for x := x1; x < x2; x++ { +				c := img.Pix[i] +				d := dst[j : j+4 : j+4] +				d[0] = c +				d[1] = c +				d[2] = c +				d[3] = 0xff +				j += 4 +				i += 2 +			} +		} + +	case *image.YCbCr: +		j := 0 +		x1 += img.Rect.Min.X +		x2 += img.Rect.Min.X +		y1 += img.Rect.Min.Y +		y2 += img.Rect.Min.Y + +		hy := img.Rect.Min.Y / 2 +		hx := img.Rect.Min.X / 2 +		for y := y1; y < y2; y++ { +			iy := (y-img.Rect.Min.Y)*img.YStride + (x1 - img.Rect.Min.X) + +			var yBase int +			switch img.SubsampleRatio { +			case image.YCbCrSubsampleRatio444, image.YCbCrSubsampleRatio422: +				yBase = (y - img.Rect.Min.Y) * img.CStride +			case image.YCbCrSubsampleRatio420, image.YCbCrSubsampleRatio440: +				yBase = (y/2 - hy) * img.CStride +			} + +			for x := x1; x < x2; x++ { +				var ic int +				switch img.SubsampleRatio { +				case image.YCbCrSubsampleRatio444, image.YCbCrSubsampleRatio440: +					ic = yBase + (x - img.Rect.Min.X) +				case image.YCbCrSubsampleRatio422, image.YCbCrSubsampleRatio420: +					ic = yBase + (x/2 - hx) +				default: +					ic = img.COffset(x, y) +				} + +				yy1 := int32(img.Y[iy]) * 0x10101 +				cb1 := int32(img.Cb[ic]) - 128 +				cr1 := int32(img.Cr[ic]) - 128 + +				r := yy1 + 91881*cr1 +				if uint32(r)&0xff000000 == 0 { +					r >>= 16 +				} else { +					r = ^(r >> 31) +				} + +				g := yy1 - 22554*cb1 - 46802*cr1 +				if uint32(g)&0xff000000 == 0 { +					g >>= 16 +				} else { +					g = ^(g >> 31) +				} + +				b := yy1 + 116130*cb1 +				if uint32(b)&0xff000000 == 0 { +					b >>= 16 +				} else { +					b = ^(b >> 31) +				} + +				d := dst[j : j+4 : j+4] +				d[0] = uint8(r) +				d[1] = uint8(g) +				d[2] = uint8(b) +				d[3] = 0xff + +				iy++ +				j += 4 +			} +		} + +	case *image.Paletted: +		j := 0 +		for y := y1; y < y2; y++ { +			i := y*img.Stride + x1 +			for x := x1; x < x2; x++ { +				c := s.palette[img.Pix[i]] +				d := dst[j : j+4 : j+4] +				d[0] = c.R +				d[1] = c.G +				d[2] = c.B +				d[3] = c.A +				j += 4 +				i++ +			} +		} + +	default: +		j := 0 +		b := s.image.Bounds() +		x1 += b.Min.X +		x2 += b.Min.X +		y1 += b.Min.Y +		y2 += b.Min.Y +		for y := y1; y < y2; y++ { +			for x := x1; x < x2; x++ { +				r16, g16, b16, a16 := s.image.At(x, y).RGBA() +				d := dst[j : j+4 : j+4] +				switch a16 { +				case 0xffff: +					d[0] = uint8(r16 >> 8) +					d[1] = uint8(g16 >> 8) +					d[2] = uint8(b16 >> 8) +					d[3] = 0xff +				case 0: +					d[0] = 0 +					d[1] = 0 +					d[2] = 0 +					d[3] = 0 +				default: +					d[0] = uint8(((r16 * 0xffff) / a16) >> 8) +					d[1] = uint8(((g16 * 0xffff) / a16) >> 8) +					d[2] = uint8(((b16 * 0xffff) / a16) >> 8) +					d[3] = uint8(a16 >> 8) +				} +				j += 4 +			} +		} +	} +} + +// reverse reverses the data +// in contained pixel slice. +func reverse(pix []uint8) { +	if len(pix) <= 4 { +		return +	} +	i := 0 +	j := len(pix) - 4 +	for i < j { +		pi := pix[i : i+4 : i+4] +		pj := pix[j : j+4 : j+4] +		pi[0], pj[0] = pj[0], pi[0] +		pi[1], pj[1] = pj[1], pi[1] +		pi[2], pj[2] = pj[2], pi[2] +		pi[3], pj[3] = pj[3], pi[3] +		i += 4 +		j -= 4 +	} +} + +// clampFloat rounds and clamps float64 value to fit into uint8. +func clampFloat(x float64) uint8 { +	v := int64(x + 0.5) +	if v > 255 { +		return 255 +	} +	if v > 0 { +		return uint8(v) +	} +	return 0 +} diff --git a/internal/media/manager_test.go b/internal/media/manager_test.go index ff38176f1..29ed95ffa 100644 --- a/internal/media/manager_test.go +++ b/internal/media/manager_test.go @@ -276,7 +276,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcess() {  	suite.Equal("image/jpeg", attachment.Thumbnail.ContentType)  	suite.Equal(269739, attachment.File.FileSize)  	suite.Equal(22858, attachment.Thumbnail.FileSize) -	suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash) +	suite.Equal("LiB|W-#6RQR.~qvzRjWF_3rqV@a$", attachment.Blurhash)  	// now make sure the attachment is in the database  	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(312453, attachment.File.FileSize)  	suite.Equal(5648, attachment.Thumbnail.FileSize) -	suite.Equal("LfIYH~xtNskCxtfPW.kB_4aespof", attachment.Blurhash) +	suite.Equal("LgIYH}xtNsofxtfPW.j[_4axn+of", attachment.Blurhash)  	// now make sure the attachment is in the database  	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(109569, attachment.File.FileSize)  	suite.Equal(2976, attachment.Thumbnail.FileSize) -	suite.Equal("LJQJfm?bM{?b~qRjt7WBayWBofWB", attachment.Blurhash) +	suite.Equal("LIQJfl_3IU?b~qM{ofayWBWVofRj", attachment.Blurhash)  	// now make sure the attachment is in the database  	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(1409625, attachment.File.FileSize)  	suite.Equal(14478, attachment.Thumbnail.FileSize) -	suite.Equal("LJF?FZV@RO.99DM_RPWAx]V?ayMw", attachment.Blurhash) +	suite.Equal("LLF$qyaeRO.9DgM_RPaetkV@WCMw", attachment.Blurhash)  	// now make sure the attachment is in the database  	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(17471, attachment.File.FileSize)  	suite.Equal(6446, attachment.Thumbnail.FileSize) -	suite.Equal("LFQT7e.A%O%4?co$M}M{_1W9~TxV", attachment.Blurhash) +	suite.Equal("LGP%YL.A-?tA.9o#RURQ~ojp^~xW", attachment.Blurhash)  	// now make sure the attachment is in the database  	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(18832, attachment.File.FileSize)  	suite.Equal(3592, attachment.Thumbnail.FileSize) -	suite.Equal("LCONII.A%Oxw?co#M}M{_1ac~TxV", attachment.Blurhash) +	suite.Equal("LCN^lE.A-?xd?co#N1RQ~ojp~SxW", attachment.Blurhash)  	// now make sure the attachment is in the database  	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(269739, attachment.File.FileSize)  	suite.Equal(22858, attachment.Thumbnail.FileSize) -	suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash) +	suite.Equal("LiB|W-#6RQR.~qvzRjWF_3rqV@a$", attachment.Blurhash)  	// now make sure the attachment is in the database  	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(269739, attachment.File.FileSize)  	suite.Equal(22858, attachment.Thumbnail.FileSize) -	suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash) +	suite.Equal("LiB|W-#6RQR.~qvzRjWF_3rqV@a$", attachment.Blurhash)  	// now make sure the attachment is in the database  	dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID) diff --git a/internal/media/thumbnail.go b/internal/media/thumbnail.go index a562dc2ad..322af8d7e 100644 --- a/internal/media/thumbnail.go +++ b/internal/media/thumbnail.go @@ -28,7 +28,6 @@ import (  	"strings"  	"github.com/buckket/go-blurhash" -	"github.com/disintegration/imaging"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/log"  	"golang.org/x/image/webp" @@ -248,32 +247,25 @@ func generateNativeThumb(  	// taking orientation into account.  	switch orientation {  	case orientationFlipH: -		img = imaging.FlipH(img) +		img = flipH(img)  	case orientationFlipV: -		img = imaging.FlipV(img) +		img = flipV(img)  	case orientationRotate90: -		img = imaging.Rotate90(img) +		img = rotate90(img)  	case orientationRotate180: -		img = imaging.Rotate180(img) +		img = rotate180(img)  	case orientationRotate270: -		img = imaging.Rotate270(img) +		img = rotate270(img)  	case orientationTranspose: -		img = imaging.Transpose(img) +		img = transpose(img)  	case orientationTransverse: -		img = imaging.Transverse(img) +		img = transverse(img)  	} -	// Resize image to dimens only if necessary. -	if img.Bounds().Dx() > maxThumbWidth || -		img.Bounds().Dy() > maxThumbHeight { -		// 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, -		) -	} +	// Resize image to dimens. +	img = resizeDownLinear(img, +		width, height, +	)  	// Open output file at given path.  	outfile, err := os.Create(outpath) @@ -293,9 +285,10 @@ func generateNativeThumb(  	}  	if needBlurhash { -		// for generating blurhashes, it's more cost effective to -		// lose detail since it's blurry, so make a tiny version. -		tiny := imaging.Resize(img, 32, 0, imaging.NearestNeighbor) +		// for generating blurhashes, it's more +		// cost effective to lose detail since +		// it's blurry, so make a tiny version. +		tiny := resizeDownLinear(img, 32, 0)  		// Drop the larger image  		// ref as soon as possible @@ -332,9 +325,10 @@ func generateWebpBlurhash(filepath string) (string, error) {  		return "", gtserror.Newf("error decoding file %s: %w", filepath, err)  	} -	// for generating blurhashes, it's more cost effective to -	// lose detail since it's blurry, so make a tiny version. -	tiny := imaging.Resize(img, 32, 0, imaging.NearestNeighbor) +	// for generating blurhashes, it's more +	// cost effective to lose detail since +	// it's blurry, so make a tiny version. +	tiny := resizeDownLinear(img, 32, 0)  	// Drop the larger image  	// ref as soon as possible  | 
