[MM-62191] Remove disintegration/imaging dependency (#29657)
* Remove disintegration/imaging dependency * Simplify FillCenter logic --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
28dbc3cabb
Коммит
316cde2569
@@ -18,7 +18,7 @@ import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app/imaging"
|
||||
_ "golang.org/x/image/webp"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
@@ -337,7 +337,7 @@ func resizeEmoji(img image.Image, width int, height int) image.Image {
|
||||
if emojiHeight <= MaxEmojiHeight && emojiWidth <= MaxEmojiWidth {
|
||||
return img
|
||||
}
|
||||
return imaging.Fit(img, MaxEmojiWidth, MaxEmojiHeight, imaging.Lanczos)
|
||||
return imaging.Fit(img, MaxEmojiWidth, MaxEmojiHeight)
|
||||
}
|
||||
|
||||
func imageToPaletted(img image.Image) *image.Paletted {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"image"
|
||||
"io"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/anthonynsimon/bild/transform"
|
||||
"github.com/rwcarlsen/goexif/exif"
|
||||
)
|
||||
|
||||
@@ -37,19 +37,19 @@ const (
|
||||
func MakeImageUpright(img image.Image, orientation int) image.Image {
|
||||
switch orientation {
|
||||
case UprightMirrored:
|
||||
return imaging.FlipH(img)
|
||||
return transform.FlipH(img)
|
||||
case UpsideDown:
|
||||
return imaging.Rotate180(img)
|
||||
return transform.Rotate(img, 180, &transform.RotationOptions{ResizeBounds: true})
|
||||
case UpsideDownMirrored:
|
||||
return imaging.FlipV(img)
|
||||
return transform.FlipV(img)
|
||||
case RotatedCWMirrored:
|
||||
return imaging.Transpose(img)
|
||||
return transform.Rotate(transform.FlipH(img), -90, &transform.RotationOptions{ResizeBounds: true})
|
||||
case RotatedCCW:
|
||||
return imaging.Rotate270(img)
|
||||
return transform.Rotate(img, 90, &transform.RotationOptions{ResizeBounds: true})
|
||||
case RotatedCCWMirrored:
|
||||
return imaging.Transverse(img)
|
||||
return transform.Rotate(transform.FlipV(img), -90, &transform.RotationOptions{ResizeBounds: true})
|
||||
case RotatedCW:
|
||||
return imaging.Rotate90(img)
|
||||
return transform.Rotate(img, 270, &transform.RotationOptions{ResizeBounds: true})
|
||||
default:
|
||||
return img
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"image"
|
||||
"image/jpeg"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/anthonynsimon/bild/transform"
|
||||
)
|
||||
|
||||
// GeneratePreview generates the preview for the given image.
|
||||
@@ -18,7 +18,7 @@ func GeneratePreview(img image.Image, width int) image.Image {
|
||||
w := img.Bounds().Dx()
|
||||
|
||||
if w > width {
|
||||
preview = imaging.Resize(img, width, 0, imaging.Lanczos)
|
||||
preview = Resize(img, width, 0, transform.Lanczos)
|
||||
}
|
||||
|
||||
return preview
|
||||
@@ -31,16 +31,16 @@ func GenerateThumbnail(img image.Image, targetWidth, targetHeight int) image.Ima
|
||||
|
||||
// We keep aspect ratio and ensure the output dimensions are never higher than the provided targets.
|
||||
if width > height {
|
||||
return imaging.Resize(img, targetWidth, 0, imaging.Lanczos)
|
||||
return Resize(img, targetWidth, 0, transform.Lanczos)
|
||||
}
|
||||
|
||||
return imaging.Resize(img, 0, targetHeight, imaging.Lanczos)
|
||||
return Resize(img, 0, targetHeight, transform.Lanczos)
|
||||
}
|
||||
|
||||
// GenerateMiniPreviewImage generates the mini preview for the given image.
|
||||
func GenerateMiniPreviewImage(img image.Image, w, h, q int) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
preview := imaging.Resize(img, w, h, imaging.Lanczos)
|
||||
preview := Resize(img, w, h, transform.Lanczos)
|
||||
if err := jpeg.Encode(&buf, preview, &jpeg.Options{Quality: q}); err != nil {
|
||||
return nil, fmt.Errorf("failed to encode image to JPEG format: %w", err)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ package imaging
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"testing"
|
||||
|
||||
"github.com/anthonynsimon/bild/transform"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -75,3 +77,96 @@ func TestGenerateThumbnail(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createTestImage(t *testing.T, width, height int) image.Image {
|
||||
t.Helper()
|
||||
img := image.NewNRGBA(image.Rect(0, 0, width, height))
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
img.Set(x, y, color.NRGBA{uint8(x % 256), uint8(y % 256), 0, 255})
|
||||
}
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
func TestResize(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
img image.Image
|
||||
targetW int
|
||||
targetH int
|
||||
expectedW int
|
||||
expectedH int
|
||||
}{
|
||||
{
|
||||
name: "zero target dimensions",
|
||||
img: createTestImage(t, 100, 50),
|
||||
targetW: 0,
|
||||
targetH: 0,
|
||||
expectedW: 0,
|
||||
expectedH: 0,
|
||||
},
|
||||
{
|
||||
name: "negative target dimensions",
|
||||
img: createTestImage(t, 100, 50),
|
||||
targetW: -1,
|
||||
targetH: 25,
|
||||
expectedW: 0,
|
||||
expectedH: 0,
|
||||
},
|
||||
{
|
||||
name: "zero source dimensions",
|
||||
img: createTestImage(t, 0, 0),
|
||||
targetW: 50,
|
||||
targetH: 25,
|
||||
expectedW: 0,
|
||||
expectedH: 0,
|
||||
},
|
||||
{
|
||||
name: "preserve aspect ratio with width",
|
||||
img: createTestImage(t, 100, 50),
|
||||
targetW: 50,
|
||||
targetH: 0,
|
||||
expectedW: 50,
|
||||
expectedH: 25,
|
||||
},
|
||||
{
|
||||
name: "preserve aspect ratio with width, height > width",
|
||||
img: createTestImage(t, 50, 100),
|
||||
targetW: 50,
|
||||
targetH: 0,
|
||||
expectedW: 50,
|
||||
expectedH: 100,
|
||||
},
|
||||
{
|
||||
name: "preserve aspect ratio with height",
|
||||
img: createTestImage(t, 100, 50),
|
||||
targetW: 0,
|
||||
targetH: 25,
|
||||
expectedW: 50,
|
||||
expectedH: 25,
|
||||
},
|
||||
{
|
||||
name: "preserve aspect ratio with height, height > width",
|
||||
img: createTestImage(t, 50, 100),
|
||||
targetW: 0,
|
||||
targetH: 25,
|
||||
expectedW: 13,
|
||||
expectedH: 25,
|
||||
},
|
||||
{
|
||||
name: "valid target dimensions",
|
||||
img: createTestImage(t, 100, 50),
|
||||
targetW: 50,
|
||||
targetH: 25,
|
||||
expectedW: 50,
|
||||
expectedH: 25,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
resizedImg := Resize(tc.img, tc.targetW, tc.targetH, transform.Lanczos)
|
||||
require.Equal(t, tc.expectedW, resizedImg.Bounds().Dx())
|
||||
require.Equal(t, tc.expectedH, resizedImg.Bounds().Dy())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ package imaging
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/anthonynsimon/bild/clone"
|
||||
"github.com/anthonynsimon/bild/transform"
|
||||
)
|
||||
|
||||
type rawImg interface {
|
||||
@@ -140,8 +142,126 @@ func FillImageTransparency(img image.Image, c color.Color) {
|
||||
}
|
||||
}
|
||||
|
||||
// CropAnchor cuts out a rectangular region with the specified size
|
||||
// from the image using the specified anchor point and returns the cropped image.
|
||||
// Adapted from github.com/disintegration/imaging
|
||||
func CropCenter(img image.Image, w, h int) image.Image {
|
||||
srcBounds := img.Bounds()
|
||||
anchorPoint := image.Pt(srcBounds.Min.X+(srcBounds.Dx()-w)/2, srcBounds.Min.Y+(srcBounds.Dy()-h)/2)
|
||||
r := image.Rect(0, 0, w, h).Add(anchorPoint)
|
||||
b := srcBounds.Intersect(r)
|
||||
return transform.Crop(img, b)
|
||||
}
|
||||
|
||||
// resizeAndCrop resizes the image to the smallest possible size that will cover the specified dimensions,
|
||||
// crops the resized image to the specified dimensions using a centered anchor point and returns
|
||||
// the transformed image.
|
||||
// Adapted from github.com/disintegration/imaging
|
||||
func resizeAndCropCenter(img image.Image, width, height int) image.Image {
|
||||
dstW, dstH := width, height
|
||||
|
||||
srcBounds := img.Bounds()
|
||||
srcW := srcBounds.Dx()
|
||||
srcH := srcBounds.Dy()
|
||||
srcAspectRatio := float64(srcW) / float64(srcH)
|
||||
dstAspectRatio := float64(dstW) / float64(dstH)
|
||||
|
||||
var tmp image.Image
|
||||
if srcAspectRatio < dstAspectRatio {
|
||||
tmp = Resize(img, dstW, 0, transform.Lanczos)
|
||||
} else {
|
||||
tmp = Resize(img, 0, dstH, transform.Lanczos)
|
||||
}
|
||||
|
||||
return CropCenter(tmp, dstW, dstH)
|
||||
}
|
||||
|
||||
// FillCenter creates an image with the specified dimensions and fills it with
|
||||
// the centered and scaled source image.
|
||||
func FillCenter(img image.Image, w, h int) *image.NRGBA {
|
||||
return imaging.Fill(img, w, h, imaging.Center, imaging.Lanczos)
|
||||
// To achieve the correct aspect ratio without stretching, the source image will be cropped.
|
||||
// Adapted from github.com/disintegration/imaging
|
||||
func FillCenter(img image.Image, dstW, dstH int) image.Image {
|
||||
if dstW <= 0 || dstH <= 0 {
|
||||
return &image.RGBA{}
|
||||
}
|
||||
|
||||
srcBounds := img.Bounds()
|
||||
srcW := srcBounds.Dx()
|
||||
srcH := srcBounds.Dy()
|
||||
|
||||
if srcW <= 0 || srcH <= 0 {
|
||||
return &image.RGBA{}
|
||||
}
|
||||
|
||||
if srcW == dstW && srcH == dstH {
|
||||
return clone.AsShallowRGBA(img)
|
||||
}
|
||||
|
||||
return resizeAndCropCenter(img, dstW, dstH)
|
||||
}
|
||||
|
||||
// Fit scales down the image to fit the specified
|
||||
// maximum width and height and returns the transformed image.
|
||||
// Adapted from github.com/disintegration/imaging
|
||||
func Fit(img image.Image, maxW, maxH int) image.Image {
|
||||
if maxW <= 0 || maxH <= 0 {
|
||||
return &image.NRGBA{}
|
||||
}
|
||||
|
||||
srcBounds := img.Bounds()
|
||||
srcW := srcBounds.Dx()
|
||||
srcH := srcBounds.Dy()
|
||||
|
||||
if srcW <= 0 || srcH <= 0 {
|
||||
return &image.RGBA{}
|
||||
}
|
||||
|
||||
if srcW <= maxW && srcH <= maxH {
|
||||
return clone.AsShallowRGBA(img)
|
||||
}
|
||||
|
||||
srcAspectRatio := float64(srcW) / float64(srcH)
|
||||
maxAspectRatio := float64(maxW) / float64(maxH)
|
||||
|
||||
var newW, newH int
|
||||
if srcAspectRatio > maxAspectRatio {
|
||||
newW = maxW
|
||||
newH = int(float64(newW) / srcAspectRatio)
|
||||
} else {
|
||||
newH = maxH
|
||||
newW = int(float64(newH) * srcAspectRatio)
|
||||
}
|
||||
|
||||
return Resize(img, newW, newH, transform.Lanczos)
|
||||
}
|
||||
|
||||
// Resize resizes the image to the specified width and height using the specified resampling filter and returns the transformed image.
|
||||
// If one of width or height is 0, the image aspect ratio is preserved.
|
||||
// Adapted from github.com/disintegration/imaging
|
||||
func Resize(img image.Image, targetWidth, targetHeight int, filter transform.ResampleFilter) image.Image {
|
||||
if targetWidth < 0 || targetHeight < 0 {
|
||||
return &image.NRGBA{}
|
||||
}
|
||||
|
||||
if targetWidth == 0 && targetHeight == 0 {
|
||||
return &image.NRGBA{}
|
||||
}
|
||||
|
||||
srcW := img.Bounds().Dx()
|
||||
srcH := img.Bounds().Dy()
|
||||
if srcW <= 0 || srcH <= 0 {
|
||||
return &image.NRGBA{}
|
||||
}
|
||||
|
||||
// If new width or height is 0 then preserve aspect ratio, minimum 1px.
|
||||
if targetWidth == 0 {
|
||||
tmpW := float64(targetHeight) * float64(srcW) / float64(srcH)
|
||||
targetWidth = int(math.Max(1.0, math.Floor(tmpW+0.5)))
|
||||
}
|
||||
if targetHeight == 0 {
|
||||
tmpH := float64(targetWidth) * float64(srcH) / float64(srcW)
|
||||
targetHeight = int(math.Max(1.0, math.Floor(tmpH+0.5)))
|
||||
}
|
||||
|
||||
return transform.Resize(img, targetWidth, targetHeight, filter)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package imaging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"os"
|
||||
"testing"
|
||||
@@ -115,3 +116,234 @@ func TestFillImageTransparency(t *testing.T) {
|
||||
require.Equal(t, expectedImg, inputImg)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCropCenter(t *testing.T) {
|
||||
imgDir, ok := fileutils.FindDir("tests")
|
||||
require.True(t, ok)
|
||||
|
||||
d, err := NewDecoder(DecoderOptions{})
|
||||
require.NotNil(t, d)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
inputName string
|
||||
outputName string
|
||||
width int
|
||||
height int
|
||||
}{
|
||||
{
|
||||
"Crop to center 100x100",
|
||||
"crop_test_input.png",
|
||||
"crop_test_output_100x100.png",
|
||||
100,
|
||||
100,
|
||||
},
|
||||
{
|
||||
"Crop to center 45x45",
|
||||
"crop_test_input.png",
|
||||
"crop_test_output_45x45.png",
|
||||
45,
|
||||
45,
|
||||
},
|
||||
{
|
||||
"Crop to center 100x45",
|
||||
"crop_test_input.png",
|
||||
"crop_test_output_100x45.png",
|
||||
100,
|
||||
45,
|
||||
},
|
||||
{
|
||||
"Crop to center 45x100",
|
||||
"crop_test_input.png",
|
||||
"crop_test_output_45x100.png",
|
||||
45,
|
||||
100,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
inputFile, err := os.Open(imgDir + "/" + tc.inputName)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, inputFile)
|
||||
defer func() {
|
||||
require.NoError(t, inputFile.Close())
|
||||
}()
|
||||
|
||||
inputImg, format, err := d.Decode(inputFile)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, inputImg)
|
||||
require.Equal(t, "png", format)
|
||||
|
||||
expectedFile, err := os.Open(imgDir + "/" + tc.outputName)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, expectedFile)
|
||||
defer func() {
|
||||
require.NoError(t, expectedFile.Close())
|
||||
}()
|
||||
|
||||
expectedImg, format, err := d.Decode(expectedFile)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, expectedImg)
|
||||
require.Equal(t, "png", format)
|
||||
|
||||
croppedImg := CropCenter(inputImg, tc.width, tc.height)
|
||||
require.Equal(t, expectedImg.Bounds().Dx(), croppedImg.Bounds().Dx())
|
||||
require.Equal(t, expectedImg.Bounds().Dy(), croppedImg.Bounds().Dy())
|
||||
require.Equal(t, expectedImg.(*image.RGBA).Pix, croppedImg.(*image.RGBA).Pix)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFit(t *testing.T) {
|
||||
imgDir, ok := fileutils.FindDir("tests")
|
||||
require.True(t, ok)
|
||||
|
||||
d, err := NewDecoder(DecoderOptions{})
|
||||
require.NotNil(t, d)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
inputName string
|
||||
outputName string
|
||||
width int
|
||||
height int
|
||||
}{
|
||||
{
|
||||
"Fit to 100x100",
|
||||
"fit_test_input.png",
|
||||
"fit_test_output_100x100.png",
|
||||
100,
|
||||
100,
|
||||
},
|
||||
{
|
||||
"Fit to 45x45",
|
||||
"fit_test_input.png",
|
||||
"fit_test_output_45x45.png",
|
||||
45,
|
||||
45,
|
||||
},
|
||||
{
|
||||
"Fit to 100x45",
|
||||
"fit_test_input.png",
|
||||
"fit_test_output_100x45.png",
|
||||
100,
|
||||
45,
|
||||
},
|
||||
{
|
||||
"Fit to 45x100",
|
||||
"fit_test_input.png",
|
||||
"fit_test_output_45x100.png",
|
||||
45,
|
||||
100,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
inputFile, err := os.Open(imgDir + "/" + tc.inputName)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, inputFile)
|
||||
defer func() {
|
||||
require.NoError(t, inputFile.Close())
|
||||
}()
|
||||
|
||||
inputImg, format, err := d.Decode(inputFile)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, inputImg)
|
||||
require.Equal(t, "png", format)
|
||||
|
||||
expectedFile, err := os.Open(imgDir + "/" + tc.outputName)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, expectedFile)
|
||||
defer func() {
|
||||
require.NoError(t, expectedFile.Close())
|
||||
}()
|
||||
|
||||
expectedImg, format, err := d.Decode(expectedFile)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, expectedImg)
|
||||
require.Equal(t, "png", format)
|
||||
|
||||
fittedImg := Fit(inputImg, tc.width, tc.height)
|
||||
require.Equal(t, expectedImg, fittedImg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFillCenter(t *testing.T) {
|
||||
imgDir, ok := fileutils.FindDir("tests")
|
||||
require.True(t, ok)
|
||||
|
||||
d, err := NewDecoder(DecoderOptions{})
|
||||
require.NotNil(t, d)
|
||||
require.NoError(t, err)
|
||||
|
||||
tcs := []struct {
|
||||
name string
|
||||
inputName string
|
||||
outputName string
|
||||
width int
|
||||
height int
|
||||
}{
|
||||
{
|
||||
"Fill center 100x100",
|
||||
"fill_test_input.png",
|
||||
"fill_test_output_100x100.png",
|
||||
100,
|
||||
100,
|
||||
},
|
||||
{
|
||||
"Fill center 45x45",
|
||||
"fill_test_input.png",
|
||||
"fill_test_output_45x45.png",
|
||||
45,
|
||||
45,
|
||||
},
|
||||
{
|
||||
"Fill center 100x45",
|
||||
"fill_test_input.png",
|
||||
"fill_test_output_100x45.png",
|
||||
100,
|
||||
45,
|
||||
},
|
||||
{
|
||||
"Fill center 45x100",
|
||||
"fill_test_input.png",
|
||||
"fill_test_output_45x100.png",
|
||||
45,
|
||||
100,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
inputFile, err := os.Open(imgDir + "/" + tc.inputName)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, inputFile)
|
||||
defer func() {
|
||||
require.NoError(t, inputFile.Close())
|
||||
}()
|
||||
|
||||
inputImg, format, err := d.Decode(inputFile)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, inputImg)
|
||||
require.Equal(t, "png", format)
|
||||
|
||||
expectedFile, err := os.Open(imgDir + "/" + tc.outputName)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, expectedFile)
|
||||
defer func() {
|
||||
require.NoError(t, expectedFile.Close())
|
||||
}()
|
||||
|
||||
expectedImg, format, err := d.Decode(expectedFile)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, expectedImg)
|
||||
require.Equal(t, "png", format)
|
||||
|
||||
filledImg := FillCenter(inputImg, tc.width, tc.height)
|
||||
require.Equal(t, expectedImg.Bounds().Dx(), filledImg.Bounds().Dx())
|
||||
require.Equal(t, expectedImg.Bounds().Dy(), filledImg.Bounds().Dy())
|
||||
require.Equal(t, expectedImg.(*image.RGBA).Pix, filledImg.(*image.RGBA).Pix)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user