Files
mattermost/api/emoji.go
Iraquitan Cordeiro Filho 48d64f3f68 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
2016-11-21 23:00:13 -03:00

326 lines
9.7 KiB
Go

// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api
import (
"bytes"
"image"
"image/draw"
"image/gif"
_ "image/jpeg"
"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 = 1000 * 1024 // 1 MB
MaxEmojiWidth = 128
MaxEmojiHeight = 128
)
func InitEmoji() {
l4g.Debug(utils.T("api.emoji.init.debug"))
BaseRoutes.Emoji.Handle("/list", ApiUserRequired(getEmoji)).Methods("GET")
BaseRoutes.Emoji.Handle("/create", ApiUserRequired(createEmoji)).Methods("POST")
BaseRoutes.Emoji.Handle("/delete", ApiUserRequired(deleteEmoji)).Methods("POST")
BaseRoutes.Emoji.Handle("/{id:[A-Za-z0-9_]+}", ApiUserRequiredTrustRequester(getEmojiImage)).Methods("GET")
}
func getEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
if !*utils.Cfg.ServiceSettings.EnableCustomEmoji {
c.Err = model.NewLocAppError("getEmoji", "api.emoji.disabled.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
if result := <-Srv.Store.Emoji().GetAll(); result.Err != nil {
c.Err = result.Err
return
} else {
emoji := result.Data.([]*model.Emoji)
w.Write([]byte(model.EmojiListToJson(emoji)))
}
}
func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
if !*utils.Cfg.ServiceSettings.EnableCustomEmoji {
c.Err = model.NewLocAppError("createEmoji", "api.emoji.disabled.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
if emojiInterface := einterfaces.GetEmojiInterface(); emojiInterface != nil &&
!emojiInterface.CanUserCreateEmoji(c.Session.Roles, c.Session.TeamMembers) {
c.Err = model.NewLocAppError("createEmoji", "api.emoji.create.permissions.app_error", nil, "user_id="+c.Session.UserId)
c.Err.StatusCode = http.StatusUnauthorized
return
}
if len(utils.Cfg.FileSettings.DriverName) == 0 {
c.Err = model.NewLocAppError("createEmoji", "api.emoji.storage.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
if r.ContentLength > MaxEmojiFileSize {
c.Err = model.NewLocAppError("createEmoji", "api.emoji.create.too_large.app_error", nil, "")
c.Err.StatusCode = http.StatusRequestEntityTooLarge
return
}
if err := r.ParseMultipartForm(MaxEmojiFileSize); err != nil {
c.Err = model.NewLocAppError("createEmoji", "api.emoji.create.parse.app_error", nil, err.Error())
c.Err.StatusCode = http.StatusBadRequest
return
}
m := r.MultipartForm
props := m.Value
emoji := model.EmojiFromJson(strings.NewReader(props["emoji"][0]))
if emoji == nil {
c.SetInvalidParam("createEmoji", "emoji")
return
}
// wipe the emoji id so that existing emojis can't get overwritten
emoji.Id = ""
// do our best to validate the emoji before committing anything to the DB so that we don't have to clean up
// orphaned files left over when validation fails later on
emoji.PreSave()
if err := emoji.IsValid(); err != nil {
c.Err = err
c.Err.StatusCode = http.StatusBadRequest
return
}
if emoji.CreatorId != c.Session.UserId {
c.Err = model.NewLocAppError("createEmoji", "api.emoji.create.other_user.app_error", nil, "")
c.Err.StatusCode = http.StatusUnauthorized
return
}
if result := <-Srv.Store.Emoji().GetByName(emoji.Name); result.Err == nil && result.Data != nil {
c.Err = model.NewLocAppError("createEmoji", "api.emoji.create.duplicate.app_error", nil, "")
c.Err.StatusCode = http.StatusBadRequest
return
}
if imageData := m.File["image"]; len(imageData) == 0 {
c.SetInvalidParam("createEmoji", "image")
return
} else if err := uploadEmojiImage(emoji.Id, imageData[0]); err != nil {
c.Err = err
return
}
if result := <-Srv.Store.Emoji().Save(emoji); result.Err != nil {
c.Err = result.Err
return
} else {
w.Write([]byte(result.Data.(*model.Emoji).ToJson()))
}
}
func uploadEmojiImage(id string, imageData *multipart.FileHeader) *model.AppError {
file, err := imageData.Open()
if err != nil {
return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.open.app_error", nil, "")
}
defer file.Close()
buf := bytes.NewBuffer(nil)
io.Copy(buf, file)
// make sure the file is an image and is within the required dimensions
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 {
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
}
func deleteEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
if !*utils.Cfg.ServiceSettings.EnableCustomEmoji {
c.Err = model.NewLocAppError("deleteEmoji", "api.emoji.disabled.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
if len(utils.Cfg.FileSettings.DriverName) == 0 {
c.Err = model.NewLocAppError("deleteImage", "api.emoji.storage.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
props := model.MapFromJson(r.Body)
id := props["id"]
if len(id) == 0 {
c.SetInvalidParam("deleteEmoji", "id")
return
}
if result := <-Srv.Store.Emoji().Get(id); result.Err != nil {
c.Err = result.Err
return
} else {
if c.Session.UserId != result.Data.(*model.Emoji).CreatorId && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
c.Err = model.NewLocAppError("deleteEmoji", "api.emoji.delete.permissions.app_error", nil, "user_id="+c.Session.UserId)
c.Err.StatusCode = http.StatusUnauthorized
return
}
}
if err := (<-Srv.Store.Emoji().Delete(id, model.GetMillis())).Err; err != nil {
c.Err = err
return
}
go deleteEmojiImage(id)
ReturnStatusOK(w)
}
func deleteEmojiImage(id string) {
if err := MoveFile(getEmojiImagePath(id), "emoji/"+id+"/image_deleted"); err != nil {
l4g.Error("Failed to rename image when deleting emoji %v", id)
}
}
func getEmojiImage(c *Context, w http.ResponseWriter, r *http.Request) {
if !*utils.Cfg.ServiceSettings.EnableCustomEmoji {
c.Err = model.NewLocAppError("getEmojiImage", "api.emoji.disabled.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
if len(utils.Cfg.FileSettings.DriverName) == 0 {
c.Err = model.NewLocAppError("getEmojiImage", "api.emoji.storage.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
params := mux.Vars(r)
id := params["id"]
if len(id) == 0 {
c.SetInvalidParam("getEmojiImage", "id")
return
}
if result := <-Srv.Store.Emoji().Get(id); result.Err != nil {
c.Err = result.Err
return
} else {
var img []byte
if data, err := ReadFile(getEmojiImagePath(id)); err != nil {
c.Err = model.NewLocAppError("getEmojiImage", "api.emoji.get_image.read.app_error", nil, err.Error())
return
} else {
img = data
}
if _, imageType, err := image.DecodeConfig(bytes.NewReader(img)); err != nil {
model.NewLocAppError("getEmojiImage", "api.emoji.get_image.decode.app_error", nil, err.Error())
} else {
w.Header().Set("Content-Type", "image/"+imageType)
}
w.Write(img)
}
}
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
}