PLT-4277: Allow larger custom emojis by resizing (#4447)
Add function to resize image using resize.Thumbnail. Add function to resize gif using previous function. Add function to convert image.Image to image.Palleted. Add logic to identify image type and resize them if they are larger than MaxEmojiHeight or MaxEmojiWidth. Also increase MaxEmojiFileSize. * fix: Add github.com/nfnt to vendor * fix: Fix max file size and if logic in resizeEmoji * test: Fix and add new tests for new resize feature * fix: Fix and update translations to fit new feature * fix: Add requested changes
Этот коммит содержится в:
коммит произвёл
enahum
родитель
9b7e2e50e3
Коммит
48d64f3f68
87
api/emoji.go
87
api/emoji.go
@@ -6,23 +6,26 @@ package api
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
"image/draw"
|
||||
"image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"image/png"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/platform/einterfaces"
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/mattermost/platform/utils"
|
||||
"image/color/palette"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxEmojiFileSize = 64 * 1024 // 64 KB
|
||||
MaxEmojiFileSize = 1000 * 1024 // 1 MB
|
||||
MaxEmojiWidth = 128
|
||||
MaxEmojiHeight = 128
|
||||
)
|
||||
@@ -147,11 +150,39 @@ func uploadEmojiImage(id string, imageData *multipart.FileHeader) *model.AppErro
|
||||
if config, _, err := image.DecodeConfig(bytes.NewReader(buf.Bytes())); err != nil {
|
||||
return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.image.app_error", nil, err.Error())
|
||||
} else if config.Width > MaxEmojiWidth || config.Height > MaxEmojiHeight {
|
||||
return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.large_image.app_error", nil, "")
|
||||
}
|
||||
|
||||
if err := WriteFile(buf.Bytes(), getEmojiImagePath(id)); err != nil {
|
||||
return err
|
||||
data := buf.Bytes()
|
||||
newbuf := bytes.NewBuffer(nil)
|
||||
if info, err := model.GetInfoForBytes(imageData.Filename, data); err != nil {
|
||||
return err
|
||||
} else if info.MimeType == "image/gif" {
|
||||
if gif_data, err := gif.DecodeAll(bytes.NewReader(data)); err != nil {
|
||||
return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_decode_error", nil, "")
|
||||
} else {
|
||||
resized_gif := resizeEmojiGif(gif_data)
|
||||
if err := gif.EncodeAll(newbuf, resized_gif); err != nil {
|
||||
return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_encode_error", nil, "")
|
||||
}
|
||||
if err := WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if img, _, err := image.Decode(bytes.NewReader(data)); err != nil {
|
||||
return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.large_image.decode_error", nil, "")
|
||||
} else {
|
||||
resized_image := resizeEmoji(img, config.Width, config.Height)
|
||||
if err := png.Encode(newbuf, resized_image); err != nil {
|
||||
return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.large_image.encode_error", nil, "")
|
||||
}
|
||||
if err := WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := WriteFile(buf.Bytes(), getEmojiImagePath(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -252,3 +283,43 @@ func getEmojiImage(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
func getEmojiImagePath(id string) string {
|
||||
return "emoji/" + id + "/image"
|
||||
}
|
||||
|
||||
func resizeEmoji(img image.Image, width int, height int) image.Image {
|
||||
emojiWidth := float64(width)
|
||||
emojiHeight := float64(height)
|
||||
|
||||
var emoji image.Image
|
||||
if emojiHeight <= MaxEmojiHeight && emojiWidth <= MaxEmojiWidth {
|
||||
emoji = img
|
||||
} else {
|
||||
emoji = imaging.Fit(img, MaxEmojiWidth, MaxEmojiHeight, imaging.Lanczos)
|
||||
}
|
||||
return emoji
|
||||
}
|
||||
|
||||
func resizeEmojiGif(gifImg *gif.GIF) *gif.GIF {
|
||||
// Create a new RGBA image to hold the incremental frames.
|
||||
firstFrame := gifImg.Image[0].Bounds()
|
||||
b := image.Rect(0, 0, firstFrame.Dx(), firstFrame.Dy())
|
||||
img := image.NewRGBA(b)
|
||||
|
||||
resizedImage := image.Image(nil)
|
||||
// Resize each frame.
|
||||
for index, frame := range gifImg.Image {
|
||||
bounds := frame.Bounds()
|
||||
draw.Draw(img, bounds, frame, bounds.Min, draw.Over)
|
||||
resizedImage = resizeEmoji(img, firstFrame.Dx(), firstFrame.Dy())
|
||||
gifImg.Image[index] = imageToPaletted(resizedImage)
|
||||
}
|
||||
// Set new gif width and height
|
||||
gifImg.Config.Width = resizedImage.Bounds().Dx()
|
||||
gifImg.Config.Height = resizedImage.Bounds().Dy()
|
||||
return gifImg
|
||||
}
|
||||
|
||||
func imageToPaletted(img image.Image) *image.Paletted {
|
||||
b := img.Bounds()
|
||||
pm := image.NewPaletted(b, palette.Plan9)
|
||||
draw.FloydSteinberg.Draw(pm, b, img, image.ZP)
|
||||
return pm
|
||||
}
|
||||
|
||||
@@ -177,8 +177,8 @@ func TestCreateEmoji(t *testing.T) {
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
if _, err := Client.CreateEmoji(emoji, createTestGif(t, 1000, 10), "image.gif"); err == nil {
|
||||
t.Fatal("shouldn't be able to create an emoji that's too wide")
|
||||
if _, err := Client.CreateEmoji(emoji, createTestGif(t, 1000, 10), "image.gif"); err != nil {
|
||||
t.Fatal("should be able to create an emoji that's too wide by resizing it")
|
||||
}
|
||||
|
||||
// try to create an emoji that's too tall
|
||||
@@ -186,8 +186,8 @@ func TestCreateEmoji(t *testing.T) {
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
if _, err := Client.CreateEmoji(emoji, createTestGif(t, 10, 1000), "image.gif"); err == nil {
|
||||
t.Fatal("shouldn't be able to create an emoji that's too tall")
|
||||
if _, err := Client.CreateEmoji(emoji, createTestGif(t, 10, 1000), "image.gif"); err != nil {
|
||||
t.Fatal("should be able to create an emoji that's too tall by resizing it")
|
||||
}
|
||||
|
||||
// try to create an emoji that's too large
|
||||
@@ -195,7 +195,7 @@ func TestCreateEmoji(t *testing.T) {
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
if _, err := Client.CreateEmoji(emoji, createTestAnimatedGif(t, 100, 100, 4000), "image.gif"); err == nil {
|
||||
if _, err := Client.CreateEmoji(emoji, createTestAnimatedGif(t, 100, 100, 10000), "image.gif"); err == nil {
|
||||
t.Fatal("shouldn't be able to create an emoji that's too large")
|
||||
}
|
||||
|
||||
@@ -424,3 +424,52 @@ func TestGetEmojiImage(t *testing.T) {
|
||||
t.Fatal("should've failed to get image for deleted emoji")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResizeEmoji(t *testing.T) {
|
||||
// try to resize a jpeg image within MaxEmojiWidth and MaxEmojiHeight
|
||||
small_img_data := createTestJpeg(t, MaxEmojiWidth, MaxEmojiHeight)
|
||||
if small_img, _, err := image.Decode(bytes.NewReader(small_img_data)); err != nil {
|
||||
t.Fatal("failed to decode jpeg bytes to image.Image")
|
||||
} else {
|
||||
resized_img := resizeEmoji(small_img, small_img.Bounds().Dx(), small_img.Bounds().Dy())
|
||||
if resized_img.Bounds().Dx() > MaxEmojiWidth || resized_img.Bounds().Dy() > MaxEmojiHeight {
|
||||
t.Fatal("resized jpeg width and height should not be greater than MaxEmojiWidth or MaxEmojiHeight")
|
||||
}
|
||||
if resized_img != small_img {
|
||||
t.Fatal("should've returned small_img itself")
|
||||
}
|
||||
}
|
||||
// try to resize a jpeg image
|
||||
jpeg_data := createTestJpeg(t, 256, 256)
|
||||
if jpeg_img, _, err := image.Decode(bytes.NewReader(jpeg_data)); err != nil {
|
||||
t.Fatal("failed to decode jpeg bytes to image.Image")
|
||||
} else {
|
||||
resized_jpeg := resizeEmoji(jpeg_img, jpeg_img.Bounds().Dx(), jpeg_img.Bounds().Dy())
|
||||
if resized_jpeg.Bounds().Dx() > MaxEmojiWidth || resized_jpeg.Bounds().Dy() > MaxEmojiHeight {
|
||||
t.Fatal("resized jpeg width and height should not be greater than MaxEmojiWidth or MaxEmojiHeight")
|
||||
}
|
||||
}
|
||||
// try to resize a png image
|
||||
png_data := createTestJpeg(t, 256, 256)
|
||||
if png_img, _, err := image.Decode(bytes.NewReader(png_data)); err != nil {
|
||||
t.Fatal("failed to decode png bytes to image.Image")
|
||||
} else {
|
||||
resized_png := resizeEmoji(png_img, png_img.Bounds().Dx(), png_img.Bounds().Dy())
|
||||
if resized_png.Bounds().Dx() > MaxEmojiWidth || resized_png.Bounds().Dy() > MaxEmojiHeight {
|
||||
t.Fatal("resized png width and height should not be greater than MaxEmojiWidth or MaxEmojiHeight")
|
||||
}
|
||||
}
|
||||
// try to resize an animated gif
|
||||
gif_data := createTestAnimatedGif(t, 256, 256, 10)
|
||||
if gif_img, err := gif.DecodeAll(bytes.NewReader(gif_data)); err != nil {
|
||||
t.Fatal("failed to decode gif bytes to gif.GIF")
|
||||
} else {
|
||||
resized_gif := resizeEmojiGif(gif_img)
|
||||
if resized_gif.Config.Width > MaxEmojiWidth || resized_gif.Config.Height > MaxEmojiHeight {
|
||||
t.Fatal("resized gif width and height should not be greater than MaxEmojiWidth or MaxEmojiHeight")
|
||||
}
|
||||
if len(resized_gif.Image) != len(gif_img.Image) {
|
||||
t.Fatal("resized gif should have the same number of frames as original gif")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
i18n/en.json
18
i18n/en.json
@@ -791,7 +791,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.emoji.create.too_large.app_error",
|
||||
"translation": "Unable to create emoji. Image must be less than 64 KB in size."
|
||||
"translation": "Unable to create emoji. Image must be less than 1 MB in size."
|
||||
},
|
||||
{
|
||||
"id": "api.emoji.delete.permissions.app_error",
|
||||
@@ -822,8 +822,20 @@
|
||||
"translation": "Unable to create emoji. File must be a PNG, JPEG, or GIF."
|
||||
},
|
||||
{
|
||||
"id": "api.emoji.upload.large_image.app_error",
|
||||
"translation": "Unable to create emoji. Image must be at most 128 by 128 pixels."
|
||||
"id": "api.emoji.upload.large_image.decode_error",
|
||||
"translation": "Unable to create emoji. An error occurred when trying to decode the image."
|
||||
},
|
||||
{
|
||||
"id": "api.emoji.upload.large_image.encode_error",
|
||||
"translation": "Unable to create emoji. An error occurred when trying to encode the image."
|
||||
},
|
||||
{
|
||||
"id": "api.emoji.upload.large_image.gif_decode_error",
|
||||
"translation": "Unable to create emoji. An error occurred when trying to decode the GIF image."
|
||||
},
|
||||
{
|
||||
"id": "api.emoji.upload.large_image.gif_encode_error",
|
||||
"translation": "Unable to create emoji. An error occurred when trying to encode the GIF image."
|
||||
},
|
||||
{
|
||||
"id": "api.file.get_file.public_disabled.app_error",
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
"add_emoji.header": "Add",
|
||||
"add_emoji.image": "Image",
|
||||
"add_emoji.image.button": "Select",
|
||||
"add_emoji.image.help": "Choose the image for your emoji. The image can be a gif, png, or jpeg file with a max size of 64 KB and dimensions up to 128 by 128 pixels.",
|
||||
"add_emoji.image.help": "Choose the image for your emoji. The image can be a gif, png, or jpeg file with a max size of 1 MB. Dimensions will automatically resize to fit 128 by 128 pixels but keeping aspect ratio.",
|
||||
"add_emoji.imageRequired": "An image is required for the emoji",
|
||||
"add_emoji.name": "Name",
|
||||
"add_emoji.name.help": "Choose a name for your emoji made of up to 64 characters consisting of lowercase letters, numbers, and the symbols '-' and '_'.",
|
||||
|
||||
Ссылка в новой задаче
Block a user