[MM-29241] Image logic refactor (#17702)

* Image logic refactor

* Add missing translations

* Improve prepareImage

* Use iota

* Limit image encoder concurrency

* Unexport validation methods

* Avoid shortening on exported names

* Remove unnecessary complexity
Этот коммит содержится в:
Claudio Costa
2021-06-05 11:08:29 +02:00
коммит произвёл GitHub
родитель 6ffd31b6bd
Коммит 39c3b8ebf9
35 изменённых файлов: 1338 добавлений и 309 удалений

Просмотреть файл

@@ -11,6 +11,7 @@ import (
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"github.com/mattermost/mattermost-server/v5/app/imaging"
"github.com/mattermost/mattermost-server/v5/app/request" "github.com/mattermost/mattermost-server/v5/app/request"
"github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/shared/i18n" "github.com/mattermost/mattermost-server/v5/shared/i18n"
@@ -526,7 +527,7 @@ func (a *App) SetBotIconImage(botUserId string, file io.ReadSeeker) *model.AppEr
return err return err
} }
if _, err := parseSVG(file); err != nil { if _, err := imaging.ParseSVG(file); err != nil {
return model.NewAppError("SetBotIconImage", "api.bot.set_bot_icon_image.parse.app_error", nil, err.Error(), http.StatusBadRequest) return model.NewAppError("SetBotIconImage", "api.bot.set_bot_icon_image.parse.app_error", nil, err.Error(), http.StatusBadRequest)
} }

Просмотреть файл

@@ -5,10 +5,6 @@ package app
import ( import (
"bytes" "bytes"
"image"
_ "image/gif"
_ "image/jpeg"
"image/png"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"time" "time"
@@ -32,28 +28,17 @@ func (a *App) SaveBrandImage(imageData *multipart.FileHeader) *model.AppError {
} }
defer file.Close() defer file.Close()
// Decode image config first to check dimensions before loading the whole thing into memory later on if err = checkImageLimits(file); err != nil {
config, _, err := image.DecodeConfig(file) return model.NewAppError("SaveBrandImage", "brand.save_brand_image.check_image_limits.app_error", nil, err.Error(), http.StatusBadRequest)
if err != nil {
return model.NewAppError("SaveBrandImage", "brand.save_brand_image.decode_config.app_error", nil, err.Error(), http.StatusBadRequest)
} }
// This casting is done to prevent overflow on 32 bit systems (not needed img, _, err := a.srv.imgDecoder.Decode(file)
// in 64 bits systems because images can't have more than 32 bits height or
// width)
if int64(config.Width)*int64(config.Height) > model.MaxImageSize {
return model.NewAppError("SaveBrandImage", "brand.save_brand_image.too_large.app_error", nil, "", http.StatusBadRequest)
}
file.Seek(0, 0)
img, _, err := image.Decode(file)
if err != nil { if err != nil {
return model.NewAppError("SaveBrandImage", "brand.save_brand_image.decode.app_error", nil, err.Error(), http.StatusBadRequest) return model.NewAppError("SaveBrandImage", "brand.save_brand_image.decode.app_error", nil, err.Error(), http.StatusBadRequest)
} }
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
err = png.Encode(buf, img) err = a.srv.imgEncoder.EncodePNG(buf, img)
if err != nil { if err != nil {
return model.NewAppError("SaveBrandImage", "brand.save_brand_image.encode.app_error", nil, err.Error(), http.StatusInternalServerError) return model.NewAppError("SaveBrandImage", "brand.save_brand_image.encode.app_error", nil, err.Error(), http.StatusInternalServerError)
} }

Просмотреть файл

@@ -11,10 +11,7 @@ import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"image" "image"
"image/color"
"image/draw"
"image/gif" "image/gif"
"image/jpeg"
"io" "io"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
@@ -27,13 +24,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/disintegration/imaging" "github.com/mattermost/mattermost-server/v5/app/imaging"
_ "github.com/oov/psd"
"github.com/pkg/errors"
"github.com/rwcarlsen/goexif/exif"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
"github.com/mattermost/mattermost-server/v5/app/request" "github.com/mattermost/mattermost-server/v5/app/request"
"github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/plugin" "github.com/mattermost/mattermost-server/v5/plugin"
@@ -42,41 +33,20 @@ import (
"github.com/mattermost/mattermost-server/v5/shared/mlog" "github.com/mattermost/mattermost-server/v5/shared/mlog"
"github.com/mattermost/mattermost-server/v5/store" "github.com/mattermost/mattermost-server/v5/store"
"github.com/mattermost/mattermost-server/v5/utils" "github.com/mattermost/mattermost-server/v5/utils"
"github.com/pkg/errors"
) )
const ( const (
/* maxImageRes = int64(6048 * 4032) // 24 megapixels, up to ~196MB as a raw image
EXIF Image Orientations imageThumbnailWidth = 120
1 2 3 4 5 6 7 8 imageThumbnailHeight = 100
imagePreviewWidth = 1920
888888 888888 88 88 8888888888 88 88 8888888888 miniPreviewImageWidth = 16
88 88 88 88 88 88 88 88 88 88 88 88 miniPreviewImageHeight = 16
8888 8888 8888 8888 88 8888888888 8888888888 88 jpegEncQuality = 90
88 88 88 88 maxUploadInitialBufferSize = 1024 * 1024 // 1MB
88 88 888888 888888 maxContentExtractionSize = 1024 * 1024 // 1MB
*/
Upright = 1
UprightMirrored = 2
UpsideDown = 3
UpsideDownMirrored = 4
RotatedCWMirrored = 5
RotatedCCW = 6
RotatedCCWMirrored = 7
RotatedCW = 8
MaxImageSize = int64(6048 * 4032) // 24 megapixels, roughly 36MB as a raw image
ImageThumbnailWidth = 120
ImageThumbnailHeight = 100
ImageThumbnailRatio = float64(ImageThumbnailHeight) / float64(ImageThumbnailWidth)
ImagePreviewWidth = 1920
maxUploadInitialBufferSize = 1024 * 1024 // 1Mb
// Deprecated
ImageThumbnailPixelWidth = 120
ImageThumbnailPixelHeight = 100
ImagePreviewPixelWidth = 1920
MaxContentExtractionSize = 1024 * 1024 // 1Mb
) )
func (a *App) FileBackend() (filestore.FileBackend, *model.AppError) { func (a *App) FileBackend() (filestore.FileBackend, *model.AppError) {
@@ -683,6 +653,9 @@ type UploadFileTask struct {
pluginsEnvironment *plugin.Environment pluginsEnvironment *plugin.Environment
writeFile func(io.Reader, string) (int64, *model.AppError) writeFile func(io.Reader, string) (int64, *model.AppError)
saveToDatabase func(*model.FileInfo) (*model.FileInfo, error) saveToDatabase func(*model.FileInfo) (*model.FileInfo, error)
imgDecoder *imaging.Decoder
imgEncoder *imaging.Encoder
} }
func (t *UploadFileTask) init(a *App) { func (t *UploadFileTask) init(a *App) {
@@ -729,6 +702,8 @@ func (a *App) UploadFileX(c *request.Context, channelID, name string, input io.R
Name: filepath.Base(name), Name: filepath.Base(name),
Input: input, Input: input,
maxFileSize: *a.Config().FileSettings.MaxFileSize, maxFileSize: *a.Config().FileSettings.MaxFileSize,
imgDecoder: a.srv.imgDecoder,
imgEncoder: a.srv.imgEncoder,
} }
for _, o := range opts { for _, o := range opts {
o(t) o(t)
@@ -811,7 +786,7 @@ func (a *App) UploadFileX(c *request.Context, channelID, name string, input io.R
func (t *UploadFileTask) preprocessImage() *model.AppError { func (t *UploadFileTask) preprocessImage() *model.AppError {
// If SVG, attempt to extract dimensions and then return // If SVG, attempt to extract dimensions and then return
if t.fileinfo.MimeType == "image/svg+xml" { if t.fileinfo.MimeType == "image/svg+xml" {
svgInfo, err := parseSVG(t.teeInput) svgInfo, err := imaging.ParseSVG(t.teeInput)
if err != nil { if err != nil {
mlog.Warn("Failed to parse SVG", mlog.Err(err)) mlog.Warn("Failed to parse SVG", mlog.Err(err))
} }
@@ -824,21 +799,17 @@ func (t *UploadFileTask) preprocessImage() *model.AppError {
} }
// If we fail to decode, return "as is". // If we fail to decode, return "as is".
config, _, err := image.DecodeConfig(t.teeInput) w, h, err := imaging.GetDimensions(t.teeInput)
if err != nil { if err != nil {
return nil return nil
} }
t.fileinfo.Width = w
t.fileinfo.Height = h
t.fileinfo.Width = config.Width if err = checkImageResolutionLimit(w, h); err != nil {
t.fileinfo.Height = config.Height
// Check dimensions before loading the whole thing into memory later on.
// This casting is done to prevent overflow on 32 bit systems (not needed
// in 64 bits systems because images can't have more than 32 bits height or
// width)
if int64(t.fileinfo.Width)*int64(t.fileinfo.Height) > MaxImageSize {
return t.newAppError("api.file.upload_file.large_image_detailed.app_error", http.StatusBadRequest) return t.newAppError("api.file.upload_file.large_image_detailed.app_error", http.StatusBadRequest)
} }
t.fileinfo.HasPreviewImage = true t.fileinfo.HasPreviewImage = true
nameWithoutExtension := t.Name[:strings.LastIndex(t.Name, ".")] nameWithoutExtension := t.Name[:strings.LastIndex(t.Name, ".")]
t.fileinfo.PreviewPath = t.pathPrefix() + nameWithoutExtension + "_preview.jpg" t.fileinfo.PreviewPath = t.pathPrefix() + nameWithoutExtension + "_preview.jpg"
@@ -847,11 +818,11 @@ func (t *UploadFileTask) preprocessImage() *model.AppError {
// check the image orientation with goexif; consume the bytes we // check the image orientation with goexif; consume the bytes we
// already have first, then keep Tee-ing from input. // already have first, then keep Tee-ing from input.
// TODO: try to reuse exif's .Raw buffer rather than Tee-ing // TODO: try to reuse exif's .Raw buffer rather than Tee-ing
if t.imageOrientation, err = getImageOrientation(io.MultiReader(bytes.NewReader(t.buf.Bytes()), t.teeInput)); err == nil && if t.imageOrientation, err = imaging.GetImageOrientation(io.MultiReader(bytes.NewReader(t.buf.Bytes()), t.teeInput)); err == nil &&
(t.imageOrientation == RotatedCWMirrored || (t.imageOrientation == imaging.RotatedCWMirrored ||
t.imageOrientation == RotatedCCW || t.imageOrientation == imaging.RotatedCCW ||
t.imageOrientation == RotatedCCWMirrored || t.imageOrientation == imaging.RotatedCCWMirrored ||
t.imageOrientation == RotatedCW) { t.imageOrientation == imaging.RotatedCW) {
t.fileinfo.Width, t.fileinfo.Height = t.fileinfo.Height, t.fileinfo.Width t.fileinfo.Width, t.fileinfo.Height = t.fileinfo.Height, t.fileinfo.Width
} }
@@ -877,35 +848,32 @@ func (t *UploadFileTask) postprocessImage(file io.Reader) {
return return
} }
decoded, typ := t.decoded, t.imageType decoded, imgType := t.decoded, t.imageType
if decoded == nil { if decoded == nil {
var err error var err error
decoded, typ, err = image.Decode(file) var release func()
decoded, imgType, release, err = t.imgDecoder.DecodeMemBounded(file)
if err != nil { if err != nil {
mlog.Error("Unable to decode image", mlog.Err(err)) mlog.Error("Unable to decode image", mlog.Err(err))
return return
} }
defer release()
} }
// Fill in the background of a potentially-transparent png file as // Fill in the background of a potentially-transparent png file as white
// white. if imgType == "png" {
if typ == "png" { imaging.FillImageTransparency(decoded, image.White)
dst := image.NewRGBA(decoded.Bounds())
draw.Draw(dst, dst.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
draw.Draw(dst, dst.Bounds(), decoded, decoded.Bounds().Min, draw.Over)
decoded = dst
} }
decoded = makeImageUpright(decoded, t.imageOrientation) decoded = imaging.MakeImageUpright(decoded, t.imageOrientation)
if decoded == nil { if decoded == nil {
return return
} }
const jpegQuality = 90
writeJPEG := func(img image.Image, path string) { writeJPEG := func(img image.Image, path string) {
r, w := io.Pipe() r, w := io.Pipe()
go func() { go func() {
err := jpeg.Encode(w, img, &jpeg.Options{Quality: jpegQuality}) err := t.imgEncoder.EncodeJPEG(w, img, jpegEncQuality)
if err != nil { if err != nil {
mlog.Error("Unable to encode image as jpeg", mlog.String("path", path), mlog.Err(err)) mlog.Error("Unable to encode image as jpeg", mlog.String("path", path), mlog.Err(err))
w.CloseWithError(err) w.CloseWithError(err)
@@ -926,18 +894,23 @@ func (t *UploadFileTask) postprocessImage(file io.Reader) {
// This is needed on mobile in case of animated GIFs. // This is needed on mobile in case of animated GIFs.
go func() { go func() {
defer wg.Done() defer wg.Done()
writeJPEG(genThumbnail(decoded), t.fileinfo.ThumbnailPath) writeJPEG(imaging.GenerateThumbnail(decoded, imageThumbnailWidth, imageThumbnailHeight), t.fileinfo.ThumbnailPath)
}() }()
go func() { go func() {
defer wg.Done() defer wg.Done()
writeJPEG(genPreview(decoded), t.fileinfo.PreviewPath) writeJPEG(imaging.GeneratePreview(decoded, imagePreviewWidth), t.fileinfo.PreviewPath)
}() }()
go func() { go func() {
defer wg.Done() defer wg.Done()
if t.fileinfo.MiniPreview == nil { if t.fileinfo.MiniPreview == nil {
t.fileinfo.MiniPreview = model.GenerateMiniPreviewImage(decoded) if miniPreview, err := imaging.GenerateMiniPreviewImage(decoded,
miniPreviewImageWidth, miniPreviewImageHeight, jpegEncQuality); err != nil {
mlog.Info("Unable to generate mini preview image", mlog.Err(err))
} else {
t.fileinfo.MiniPreview = &miniPreview
}
} }
}() }()
wg.Wait() wg.Wait()
@@ -984,11 +957,11 @@ func (a *App) DoUploadFileExpectModification(c *request.Context, now time.Time,
return nil, data, err return nil, data, err
} }
if orientation, err := getImageOrientation(bytes.NewReader(data)); err == nil && if orientation, err := imaging.GetImageOrientation(bytes.NewReader(data)); err == nil &&
(orientation == RotatedCWMirrored || (orientation == imaging.RotatedCWMirrored ||
orientation == RotatedCCW || orientation == imaging.RotatedCCW ||
orientation == RotatedCCWMirrored || orientation == imaging.RotatedCCWMirrored ||
orientation == RotatedCW) { orientation == imaging.RotatedCW) {
info.Width, info.Height = info.Height, info.Width info.Width, info.Height = info.Height, info.Width
} }
@@ -1000,12 +973,8 @@ func (a *App) DoUploadFileExpectModification(c *request.Context, now time.Time,
info.Path = pathPrefix + filename info.Path = pathPrefix + filename
if info.IsImage() { if info.IsImage() {
// Check dimensions before loading the whole thing into memory later on if limitErr := checkImageResolutionLimit(info.Width, info.Height); limitErr != nil {
// This casting is done to prevent overflow on 32 bit systems (not needed err := model.NewAppError("uploadFile", "api.file.upload_file.large_image.app_error", map[string]interface{}{"Filename": filename}, limitErr.Error(), http.StatusBadRequest)
// in 64 bits systems because images can't have more than 32 bits height or
// width)
if int64(info.Width)*int64(info.Height) > MaxImageSize {
err := model.NewAppError("uploadFile", "api.file.upload_file.large_image.app_error", map[string]interface{}{"Filename": filename}, "", http.StatusBadRequest)
return nil, data, err return nil, data, err
} }
@@ -1070,8 +1039,11 @@ func (a *App) HandleImages(previewPathList []string, thumbnailPathList []string,
wg := new(sync.WaitGroup) wg := new(sync.WaitGroup)
for i := range fileData { for i := range fileData {
img, _, _ := prepareImage(fileData[i]) img, release, err := prepareImage(a.srv.imgDecoder, bytes.NewReader(fileData[i]))
if img != nil { if err != nil {
mlog.Debug("Failed to prepare image", mlog.Err(err))
continue
}
wg.Add(2) wg.Add(2)
go func(img image.Image, path string) { go func(img image.Image, path string) {
defer wg.Done() defer wg.Done()
@@ -1082,101 +1054,60 @@ func (a *App) HandleImages(previewPathList []string, thumbnailPathList []string,
defer wg.Done() defer wg.Done()
a.generatePreviewImage(img, path) a.generatePreviewImage(img, path)
}(img, previewPathList[i]) }(img, previewPathList[i])
}
}
wg.Wait() wg.Wait()
release()
}
} }
func prepareImage(fileData []byte) (image.Image, int, int) { func prepareImage(imgDecoder *imaging.Decoder, imgData io.ReadSeeker) (img image.Image, release func(), err error) {
// Decode image bytes into Image object // Decode image bytes into Image object
img, imgType, err := image.Decode(bytes.NewReader(fileData)) var imgType string
img, imgType, release, err = imgDecoder.DecodeMemBounded(imgData)
if err != nil { if err != nil {
mlog.Error("Unable to decode image", mlog.Err(err)) return nil, nil, fmt.Errorf("prepareImage: failed to decode image: %w", err)
return nil, 0, 0
} }
width := img.Bounds().Dx()
height := img.Bounds().Dy()
// Fill in the background of a potentially-transparent png file as white // Fill in the background of a potentially-transparent png file as white
if imgType == "png" { if imgType == "png" {
dst := image.NewRGBA(img.Bounds()) imaging.FillImageTransparency(img, image.White)
draw.Draw(dst, dst.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
draw.Draw(dst, dst.Bounds(), img, img.Bounds().Min, draw.Over)
img = dst
} }
imgData.Seek(0, io.SeekStart)
// Flip the image to be upright // Flip the image to be upright
orientation, _ := getImageOrientation(bytes.NewReader(fileData)) orientation, err := imaging.GetImageOrientation(imgData)
img = makeImageUpright(img, orientation)
return img, width, height
}
func makeImageUpright(img image.Image, orientation int) image.Image {
switch orientation {
case UprightMirrored:
return imaging.FlipH(img)
case UpsideDown:
return imaging.Rotate180(img)
case UpsideDownMirrored:
return imaging.FlipV(img)
case RotatedCWMirrored:
return imaging.Transpose(img)
case RotatedCCW:
return imaging.Rotate270(img)
case RotatedCCWMirrored:
return imaging.Transverse(img)
case RotatedCW:
return imaging.Rotate90(img)
default:
return img
}
}
func getImageOrientation(input io.Reader) (int, error) {
exifData, err := exif.Decode(input)
if err != nil { if err != nil {
return Upright, err mlog.Debug("GetImageOrientation failed", mlog.Err(err))
} }
img = imaging.MakeImageUpright(img, orientation)
tag, err := exifData.Get("Orientation") return img, release, nil
if err != nil {
return Upright, err
}
orientation, err := tag.Int(0)
if err != nil {
return Upright, err
}
return orientation, nil
} }
func (a *App) generateThumbnailImage(img image.Image, thumbnailPath string) { func (a *App) generateThumbnailImage(img image.Image, thumbnailPath string) {
buf := new(bytes.Buffer) var buf bytes.Buffer
if err := jpeg.Encode(buf, genThumbnail(img), &jpeg.Options{Quality: 90}); err != nil { if err := a.srv.imgEncoder.EncodeJPEG(&buf, imaging.GenerateThumbnail(img, imageThumbnailWidth, imageThumbnailHeight), jpegEncQuality); err != nil {
mlog.Error("Unable to encode image as jpeg", mlog.String("path", thumbnailPath), mlog.Err(err)) mlog.Error("Unable to encode image as jpeg", mlog.String("path", thumbnailPath), mlog.Err(err))
return return
} }
if _, err := a.WriteFile(buf, thumbnailPath); err != nil { if _, err := a.WriteFile(&buf, thumbnailPath); err != nil {
mlog.Error("Unable to upload thumbnail", mlog.String("path", thumbnailPath), mlog.Err(err)) mlog.Error("Unable to upload thumbnail", mlog.String("path", thumbnailPath), mlog.Err(err))
return return
} }
} }
func (a *App) generatePreviewImage(img image.Image, previewPath string) { func (a *App) generatePreviewImage(img image.Image, previewPath string) {
preview := genPreview(img) var buf bytes.Buffer
preview := imaging.GeneratePreview(img, imagePreviewWidth)
buf := new(bytes.Buffer) if err := a.srv.imgEncoder.EncodeJPEG(&buf, preview, jpegEncQuality); err != nil {
if err := jpeg.Encode(buf, preview, &jpeg.Options{Quality: 90}); err != nil {
mlog.Error("Unable to encode image as preview jpg", mlog.Err(err), mlog.String("path", previewPath)) mlog.Error("Unable to encode image as preview jpg", mlog.Err(err), mlog.String("path", previewPath))
return return
} }
if _, err := a.WriteFile(buf, previewPath); err != nil { if _, err := a.WriteFile(&buf, previewPath); err != nil {
mlog.Error("Unable to upload preview", mlog.Err(err), mlog.String("path", previewPath)) mlog.Error("Unable to upload preview", mlog.Err(err), mlog.String("path", previewPath))
return return
} }
@@ -1186,18 +1117,26 @@ func (a *App) generatePreviewImage(img image.Image, previewPath string) {
// will save fileinfo with the preview added // will save fileinfo with the preview added
func (a *App) generateMiniPreview(fi *model.FileInfo) { func (a *App) generateMiniPreview(fi *model.FileInfo) {
if fi.IsImage() && fi.MiniPreview == nil { if fi.IsImage() && fi.MiniPreview == nil {
data, err := a.ReadFile(fi.Path) file, err := a.FileReader(fi.Path)
if err != nil { if err != nil {
mlog.Error("error reading image file", mlog.Err(err)) mlog.Debug("error reading image file", mlog.Err(err))
return return
} }
img, _, _ := prepareImage(data) defer file.Close()
if img == nil { img, release, imgErr := prepareImage(a.srv.imgDecoder, file)
if imgErr != nil {
mlog.Debug("generateMiniPreview: prepareImage failed", mlog.Err(imgErr))
return return
} }
fi.MiniPreview = model.GenerateMiniPreviewImage(img) defer release()
if miniPreview, err := imaging.GenerateMiniPreviewImage(img,
miniPreviewImageWidth, miniPreviewImageHeight, jpegEncQuality); err != nil {
mlog.Info("Unable to generate mini preview image", mlog.Err(err))
} else {
fi.MiniPreview = &miniPreview
}
if _, appErr := a.Srv().Store.FileInfo().Upsert(fi); appErr != nil { if _, appErr := a.Srv().Store.FileInfo().Upsert(fi); appErr != nil {
mlog.Error("creating mini preview failed", mlog.Err(appErr)) mlog.Debug("creating mini preview failed", mlog.Err(appErr))
} else { } else {
a.Srv().Store.FileInfo().InvalidateFileInfosForPostCache(fi.PostId, false) a.Srv().Store.FileInfo().InvalidateFileInfosForPostCache(fi.PostId, false)
} }
@@ -1411,8 +1350,8 @@ func (a *App) ExtractContentFromFileInfo(fileInfo *model.FileInfo) error {
return errors.Wrap(err, "failed to extract file content") return errors.Wrap(err, "failed to extract file content")
} }
if text != "" { if text != "" {
if len(text) > MaxContentExtractionSize { if len(text) > maxContentExtractionSize {
text = text[0:MaxContentExtractionSize] text = text[0:maxContentExtractionSize]
} }
if storeErr := a.Srv().Store.FileInfo().SetContent(fileInfo.Id, text); storeErr != nil { if storeErr := a.Srv().Store.FileInfo().SetContent(fileInfo.Id, text); storeErr != nil {
return errors.Wrap(storeErr, "failed to save the extracted file content") return errors.Wrap(storeErr, "failed to save the extracted file content")

Просмотреть файл

@@ -4,47 +4,29 @@
package app package app
import ( import (
"image" "fmt"
"io"
"github.com/disintegration/imaging" "github.com/mattermost/mattermost-server/v5/app/imaging"
) )
func genThumbnail(img image.Image) image.Image { func checkImageResolutionLimit(w, h int) error {
thumb := img // This casting is done to prevent overflow on 32 bit systems (not needed
w := img.Bounds().Dx() // in 64 bits systems because images can't have more than 32 bits height or
h := img.Bounds().Dy() // width)
imageRes := int64(w) * int64(h)
if h > ImageThumbnailHeight || w > ImageThumbnailWidth { if imageRes > maxImageRes {
ratio := float64(h) / float64(w) return fmt.Errorf("image resolution is too high: %d, max allowed is %d", imageRes, maxImageRes)
if ratio < ImageThumbnailRatio {
// we pre-calculate the thumbnail's width to make sure we are not upscaling.
targetWidth := int(float64(ImageThumbnailHeight) * float64(w) / float64(h))
if targetWidth <= w {
thumb = imaging.Resize(img, 0, ImageThumbnailHeight, imaging.Lanczos)
} else {
thumb = imaging.Resize(img, ImageThumbnailWidth, 0, imaging.Lanczos)
}
} else {
// we pre-calculate the thumbnail's height to make sure we are not upscaling.
targetHeight := int(float64(ImageThumbnailWidth) * float64(h) / float64(w))
if targetHeight <= h {
thumb = imaging.Resize(img, ImageThumbnailWidth, 0, imaging.Lanczos)
} else {
thumb = imaging.Resize(img, 0, ImageThumbnailHeight, imaging.Lanczos)
}
}
} }
return thumb return nil
} }
func genPreview(img image.Image) image.Image { func checkImageLimits(imageData io.Reader) error {
preview := img w, h, err := imaging.GetDimensions(imageData)
w := img.Bounds().Dx() if err != nil {
return fmt.Errorf("failed to get image dimensions: %w", err)
if w > ImagePreviewWidth {
preview = imaging.Resize(img, ImagePreviewWidth, 0, imaging.Lanczos)
} }
return preview return checkImageResolutionLimit(w, h)
} }

146
app/imaging/decode.go Обычный файл
Просмотреть файл

@@ -0,0 +1,146 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"errors"
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"io"
"sync"
_ "github.com/oov/psd"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
)
// DecoderOptions holds configuration options for an image decoder.
type DecoderOptions struct {
// The level of concurrency for the decoder. This defines a limit on the
// number of concurrently running encoding goroutines.
ConcurrencyLevel int
}
func (o *DecoderOptions) validate() error {
if o.ConcurrencyLevel < 0 {
return errors.New("ConcurrencyLevel must be non-negative")
}
return nil
}
// Decoder holds the necessary state to decode images.
// This is safe to be used from multiple goroutines.
type Decoder struct {
sem chan struct{}
opts DecoderOptions
}
// NewDecoder creates and returns a new image decoder with the given options.
func NewDecoder(opts DecoderOptions) (*Decoder, error) {
var d Decoder
if err := opts.validate(); err != nil {
return nil, fmt.Errorf("imaging: error validating decoder options: %w", err)
}
if opts.ConcurrencyLevel > 0 {
d.sem = make(chan struct{}, opts.ConcurrencyLevel)
}
d.opts = opts
return &d, nil
}
// Decode decodes the given encoded data and returns the decoded image.
func (d *Decoder) Decode(rd io.Reader) (img image.Image, format string, err error) {
if d.opts.ConcurrencyLevel != 0 {
d.sem <- struct{}{}
defer func() { <-d.sem }()
}
img, format, err = image.Decode(rd)
if err != nil {
return nil, "", fmt.Errorf("imaging: failed to decode image: %w", err)
}
return img, format, nil
}
// DecodeMemBounded works similarly to Decode but also returns a release function that
// must be called when access to the raw image is not needed anymore.
// This sets the raw image data pointer to nil in an attempt to help the GC to re-use the underlying data as soon as possible.
func (d *Decoder) DecodeMemBounded(rd io.Reader) (img image.Image, format string, releaseFunc func(), err error) {
if d.opts.ConcurrencyLevel != 0 {
d.sem <- struct{}{}
defer func() {
if err != nil {
<-d.sem
}
}()
}
img, format, err = image.Decode(rd)
if err != nil {
return nil, "", nil, fmt.Errorf("imaging: failed to decode image: %w", err)
}
var once sync.Once
releaseFunc = func() {
if d.opts.ConcurrencyLevel == 0 {
return
}
once.Do(func() {
if img != nil {
releaseImageData(img)
}
<-d.sem
})
}
return img, format, releaseFunc, nil
}
// DecodeConfig returns the image config for the given data.
func (d *Decoder) DecodeConfig(rd io.Reader) (image.Config, string, error) {
img, format, err := image.DecodeConfig(rd)
if err != nil {
return image.Config{}, "", fmt.Errorf("imaging: failed to decode image config: %w", err)
}
return img, format, nil
}
// GetDimensions returns the dimensions for the given encoded image data.
func GetDimensions(imageData io.Reader) (int, int, error) {
cfg, _, err := image.DecodeConfig(imageData)
if seeker, ok := imageData.(io.ReadSeeker); ok {
defer seeker.Seek(0, 0)
}
return cfg.Width, cfg.Height, err
}
// This is only needed to try and simplify GC work.
func releaseImageData(img image.Image) {
switch raw := img.(type) {
case *image.Alpha:
raw.Pix = nil
case *image.Alpha16:
raw.Pix = nil
case *image.Gray:
raw.Pix = nil
case *image.Gray16:
raw.Pix = nil
case *image.NRGBA:
raw.Pix = nil
case *image.NRGBA64:
raw.Pix = nil
case *image.Paletted:
raw.Pix = nil
case *image.RGBA:
raw.Pix = nil
case *image.RGBA64:
raw.Pix = nil
default:
return
}
}

89
app/imaging/decode_bench_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,89 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"fmt"
"os"
"runtime"
"sync"
"testing"
"github.com/mattermost/mattermost-server/v5/utils/fileutils"
"github.com/stretchr/testify/require"
)
func BenchmarkDecoderDecode(b *testing.B) {
n := runtime.NumCPU()
for k := 1; k <= n; k++ {
b.Run(fmt.Sprintf("%d concurrency", k), func(b *testing.B) {
d, err := NewDecoder(DecoderOptions{
ConcurrencyLevel: k,
})
require.NotNil(b, d)
require.NoError(b, err)
imgDir, ok := fileutils.FindDir("tests")
require.True(b, ok)
b.ResetTimer()
var wg sync.WaitGroup
for i := 0; i < b.N; i++ {
b.StopTimer()
wg.Add(1)
imgFile, err := os.Open(imgDir + "/fill_test_opaque.png")
require.NoError(b, err)
defer imgFile.Close()
b.StartTimer()
go func() {
defer wg.Done()
img, _, err := d.Decode(imgFile)
require.NoError(b, err)
require.NotNil(b, img)
}()
}
wg.Wait()
})
}
}
func BenchmarkDecoderDecodeMemBounded(b *testing.B) {
n := runtime.NumCPU()
for k := 1; k <= n; k++ {
b.Run(fmt.Sprintf("%d concurrency", k), func(b *testing.B) {
d, err := NewDecoder(DecoderOptions{
ConcurrencyLevel: k,
})
require.NotNil(b, d)
require.NoError(b, err)
imgDir, ok := fileutils.FindDir("tests")
require.True(b, ok)
b.ResetTimer()
var wg sync.WaitGroup
for i := 0; i < b.N; i++ {
b.StopTimer()
wg.Add(1)
imgFile, err := os.Open(imgDir + "/fill_test_opaque.png")
require.NoError(b, err)
defer imgFile.Close()
b.StartTimer()
go func() {
defer wg.Done()
img, _, release, err := d.DecodeMemBounded(imgFile)
require.NoError(b, err)
require.NotNil(b, img)
release()
}()
}
wg.Wait()
})
}
}

225
app/imaging/decode_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,225 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"bytes"
"os"
"sync"
"testing"
"github.com/mattermost/mattermost-server/v5/utils/fileutils"
"github.com/stretchr/testify/require"
)
func TestNewDecoder(t *testing.T) {
t.Run("invalid options", func(t *testing.T) {
d, err := NewDecoder(DecoderOptions{
ConcurrencyLevel: -1,
})
require.Nil(t, d)
require.Error(t, err)
})
t.Run("empty options", func(t *testing.T) {
d, err := NewDecoder(DecoderOptions{})
require.NotNil(t, d)
require.NoError(t, err)
require.Nil(t, d.sem)
})
t.Run("valid options", func(t *testing.T) {
d, err := NewDecoder(DecoderOptions{
ConcurrencyLevel: 4,
})
require.NotNil(t, d)
require.NoError(t, err)
require.NotNil(t, d.sem)
require.Equal(t, 4, cap(d.sem))
})
}
func TestDecoderDecode(t *testing.T) {
t.Run("default options", func(t *testing.T) {
d, err := NewDecoder(DecoderOptions{})
require.NotNil(t, d)
require.NoError(t, err)
imgDir, ok := fileutils.FindDir("tests")
require.True(t, ok)
imgFile, err := os.Open(imgDir + "/test.png")
require.NoError(t, err)
require.NotNil(t, imgFile)
defer imgFile.Close()
img, format, err := d.Decode(imgFile)
require.NoError(t, err)
require.NotNil(t, img)
require.Equal(t, "png", format)
})
t.Run("concurrency bounded", func(t *testing.T) {
d, err := NewDecoder(DecoderOptions{
ConcurrencyLevel: 1,
})
require.NotNil(t, d)
require.NoError(t, err)
imgDir, ok := fileutils.FindDir("tests")
require.True(t, ok)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
imgFile, err := os.Open(imgDir + "/test.png")
require.NoError(t, err)
require.NotNil(t, imgFile)
defer imgFile.Close()
img, format, err := d.Decode(imgFile)
require.NoError(t, err)
require.NotNil(t, img)
require.Equal(t, "png", format)
}()
go func() {
defer wg.Done()
imgFile, err := os.Open(imgDir + "/test.png")
require.NoError(t, err)
require.NotNil(t, imgFile)
defer imgFile.Close()
img, format, err := d.Decode(imgFile)
require.NoError(t, err)
require.NotNil(t, img)
require.Equal(t, "png", format)
}()
wg.Wait()
require.Empty(t, d.sem)
})
}
func TestDecoderDecodeMemBounded(t *testing.T) {
t.Run("concurrency bounded", func(t *testing.T) {
d, err := NewDecoder(DecoderOptions{
ConcurrencyLevel: 1,
})
require.NotNil(t, d)
require.NoError(t, err)
imgDir, ok := fileutils.FindDir("tests")
require.True(t, ok)
imgFile, err := os.Open(imgDir + "/test.png")
require.NoError(t, err)
require.NotNil(t, imgFile)
defer imgFile.Close()
var wg sync.WaitGroup
wg.Add(2)
var lock sync.Mutex
go func() {
defer wg.Done()
img, format, release, err := d.DecodeMemBounded(imgFile)
lock.Lock()
imgFile.Seek(0, 0)
lock.Unlock()
require.NoError(t, err)
defer release()
require.NotNil(t, img)
require.Equal(t, "png", format)
require.NotNil(t, release)
require.NotEmpty(t, d.sem)
}()
go func() {
defer wg.Done()
img, format, release, err := d.DecodeMemBounded(imgFile)
lock.Lock()
imgFile.Seek(0, 0)
lock.Unlock()
require.NoError(t, err)
defer release()
require.NotNil(t, img)
require.Equal(t, "png", format)
require.NotNil(t, release)
require.NotEmpty(t, d.sem)
}()
wg.Wait()
require.Empty(t, d.sem)
})
t.Run("decode error", func(t *testing.T) {
d, err := NewDecoder(DecoderOptions{
ConcurrencyLevel: 1,
})
require.NotNil(t, d)
require.NoError(t, err)
var data bytes.Buffer
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
img, format, release, err := d.DecodeMemBounded(&data)
require.Error(t, err)
require.Nil(t, img)
require.Empty(t, format)
require.Nil(t, release)
}()
go func() {
defer wg.Done()
img, format, release, err := d.DecodeMemBounded(&data)
require.Error(t, err)
require.Nil(t, img)
require.Empty(t, format)
require.Nil(t, release)
}()
wg.Wait()
require.Empty(t, d.sem)
})
t.Run("multiple releases", func(t *testing.T) {
d, err := NewDecoder(DecoderOptions{
ConcurrencyLevel: 1,
})
require.NotNil(t, d)
require.NoError(t, err)
imgDir, ok := fileutils.FindDir("tests")
require.True(t, ok)
imgFile, err := os.Open(imgDir + "/test.png")
require.NoError(t, err)
require.NotNil(t, imgFile)
defer imgFile.Close()
img, format, release, err := d.DecodeMemBounded(imgFile)
require.NoError(t, err)
require.NotNil(t, img)
require.Equal(t, "png", format)
require.NotNil(t, release)
require.Len(t, d.sem, 1)
release()
require.Empty(t, d.sem)
release()
require.Empty(t, d.sem)
release()
require.Empty(t, d.sem)
})
}

86
app/imaging/encode.go Обычный файл
Просмотреть файл

@@ -0,0 +1,86 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"errors"
"fmt"
"image"
"io"
"image/jpeg"
"image/png"
)
// EncoderOptions holds configuration options for an image encoder.
type EncoderOptions struct {
// The level of concurrency for the encoder. This defines a limit on the
// number of concurrently running encoding goroutines.
ConcurrencyLevel int
}
func (o *EncoderOptions) validate() error {
if o.ConcurrencyLevel < 0 {
return errors.New("ConcurrencyLevel must be non-negative")
}
return nil
}
// Decoder holds the necessary state to encode images.
// This is safe to be used from multiple goroutines.
type Encoder struct {
sem chan struct{}
opts EncoderOptions
pngEncoder *png.Encoder
}
// NewEncoder creates and returns a new image encoder with the given options.
func NewEncoder(opts EncoderOptions) (*Encoder, error) {
var e Encoder
if err := opts.validate(); err != nil {
return nil, fmt.Errorf("imaging: error validating encoder options: %w", err)
}
if opts.ConcurrencyLevel > 0 {
e.sem = make(chan struct{}, opts.ConcurrencyLevel)
}
e.opts = opts
e.pngEncoder = &png.Encoder{}
return &e, nil
}
// EncodeJPEG encodes the given image in JPEG format and writes the data to
// the passed writer.
func (e *Encoder) EncodeJPEG(wr io.Writer, img image.Image, quality int) error {
if e.opts.ConcurrencyLevel > 0 {
e.sem <- struct{}{}
defer func() {
<-e.sem
}()
}
var encOpts jpeg.Options
encOpts.Quality = quality
if err := jpeg.Encode(wr, img, &encOpts); err != nil {
return fmt.Errorf("imaging: failed to encode jpeg: %w", err)
}
return nil
}
// EncodePNG encodes the given image in PNG format and writes the data to
// the passed writer.
func (e *Encoder) EncodePNG(wr io.Writer, img image.Image) error {
if e.opts.ConcurrencyLevel > 0 {
e.sem <- struct{}{}
defer func() {
<-e.sem
}()
}
if err := e.pngEncoder.Encode(wr, img); err != nil {
return fmt.Errorf("imaging: failed to encode png: %w", err)
}
return nil
}

90
app/imaging/encode_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,90 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"bytes"
"image"
"sync"
"testing"
"github.com/stretchr/testify/require"
)
func TestNewEncoder(t *testing.T) {
t.Run("invalid options", func(t *testing.T) {
e, err := NewEncoder(EncoderOptions{
ConcurrencyLevel: -1,
})
require.Nil(t, e)
require.Error(t, err)
})
t.Run("empty options", func(t *testing.T) {
e, err := NewEncoder(EncoderOptions{})
require.NotNil(t, e)
require.NoError(t, err)
require.Nil(t, e.sem)
})
t.Run("valid options", func(t *testing.T) {
e, err := NewEncoder(EncoderOptions{
ConcurrencyLevel: 4,
})
require.NotNil(t, e)
require.NoError(t, err)
require.NotNil(t, e.sem)
require.Equal(t, 4, cap(e.sem))
})
}
func TestEncoderEncode(t *testing.T) {
t.Run("default options", func(t *testing.T) {
e, err := NewEncoder(EncoderOptions{})
require.NotNil(t, e)
require.NoError(t, err)
var buf bytes.Buffer
rawImg := image.NewRGBA(image.Rect(0, 0, 1280, 1024))
err = e.EncodePNG(&buf, rawImg)
require.NoError(t, err)
require.NotEmpty(t, buf)
err = e.EncodeJPEG(&buf, rawImg, 50)
require.NoError(t, err)
require.NotEmpty(t, buf)
})
t.Run("concurrency bounded", func(t *testing.T) {
e, err := NewEncoder(EncoderOptions{
ConcurrencyLevel: 1,
})
require.NotNil(t, e)
require.NoError(t, err)
rawImg := image.NewRGBA(image.Rect(0, 0, 1280, 1024))
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
var buf bytes.Buffer
err := e.EncodePNG(&buf, rawImg)
require.NoError(t, err)
require.NotEmpty(t, buf)
}()
go func() {
defer wg.Done()
var buf bytes.Buffer
err := e.EncodeJPEG(&buf, rawImg, 50)
require.NoError(t, err)
require.NotEmpty(t, buf)
}()
wg.Wait()
require.Empty(t, e.sem)
})
}

76
app/imaging/orientation.go Обычный файл
Просмотреть файл

@@ -0,0 +1,76 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"image"
"io"
"github.com/disintegration/imaging"
"github.com/rwcarlsen/goexif/exif"
)
const (
/*
EXIF Image Orientations
1 2 3 4 5 6 7 8
888888 888888 88 88 8888888888 88 88 8888888888
88 88 88 88 88 88 88 88 88 88 88 88
8888 8888 8888 8888 88 8888888888 8888888888 88
88 88 88 88
88 88 888888 888888
*/
Upright = iota + 1
UprightMirrored
UpsideDown
UpsideDownMirrored
RotatedCWMirrored
RotatedCCW
RotatedCCWMirrored
RotatedCW
)
// MakeImageUpright changes the orientation of the given image.
func MakeImageUpright(img image.Image, orientation int) image.Image {
switch orientation {
case UprightMirrored:
return imaging.FlipH(img)
case UpsideDown:
return imaging.Rotate180(img)
case UpsideDownMirrored:
return imaging.FlipV(img)
case RotatedCWMirrored:
return imaging.Transpose(img)
case RotatedCCW:
return imaging.Rotate270(img)
case RotatedCCWMirrored:
return imaging.Transverse(img)
case RotatedCW:
return imaging.Rotate90(img)
default:
return img
}
}
// GetImageOrientation reads the input data and returns the EXIF encoded
// image orientation.
func GetImageOrientation(input io.Reader) (int, error) {
exifData, err := exif.Decode(input)
if err != nil {
return Upright, err
}
tag, err := exifData.Get("Orientation")
if err != nil {
return Upright, err
}
orientation, err := tag.Int(0)
if err != nil {
return Upright, err
}
return orientation, nil
}

66
app/imaging/preview.go Обычный файл
Просмотреть файл

@@ -0,0 +1,66 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"bytes"
"fmt"
"image"
"image/jpeg"
"github.com/disintegration/imaging"
)
// GeneratePreview generates the preview for the given image.
func GeneratePreview(img image.Image, width int) image.Image {
preview := img
w := img.Bounds().Dx()
if w > width {
preview = imaging.Resize(img, width, 0, imaging.Lanczos)
}
return preview
}
// GenerateThumbnail generates the thumbnail for the given image.
func GenerateThumbnail(img image.Image, width, height int) image.Image {
thumb := img
w := img.Bounds().Dx()
h := img.Bounds().Dy()
expectedRatio := float64(height) / float64(width)
if h > height || w > width {
ratio := float64(h) / float64(w)
if ratio < expectedRatio {
// we pre-calculate the thumbnail's width to make sure we are not upscaling.
targetWidth := int(float64(height) * float64(w) / float64(h))
if targetWidth <= w {
thumb = imaging.Resize(img, 0, height, imaging.Lanczos)
} else {
thumb = imaging.Resize(img, width, 0, imaging.Lanczos)
}
} else {
// we pre-calculate the thumbnail's height to make sure we are not upscaling.
targetHeight := int(float64(width) * float64(h) / float64(w))
if targetHeight <= h {
thumb = imaging.Resize(img, width, 0, imaging.Lanczos)
} else {
thumb = imaging.Resize(img, 0, height, imaging.Lanczos)
}
}
}
return thumb
}
// 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)
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)
}
return buf.Bytes(), nil
}

Просмотреть файл

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
package app package imaging
import ( import (
"encoding/xml" "encoding/xml"
@@ -12,12 +12,14 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
// SVGInfo holds information for a SVG image.
type SVGInfo struct { type SVGInfo struct {
Width int Width int
Height int Height int
} }
func parseSVG(svgReader io.Reader) (SVGInfo, error) { // ParseSVG returns information for the given SVG input data.
func ParseSVG(svgReader io.Reader) (SVGInfo, error) {
var parsedSVG struct { var parsedSVG struct {
Width string `xml:"width,attr,omitempty"` Width string `xml:"width,attr,omitempty"`
Height string `xml:"height,attr,omitempty"` Height string `xml:"height,attr,omitempty"`

Просмотреть файл

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
package app package imaging
import ( import (
"fmt" "fmt"
@@ -45,7 +45,7 @@ func TestParseValidSVGData(t *testing.T) {
generateSVGData(width, height, false, true, true), // missing viewBox, properly formed width & height; alternate format generateSVGData(width, height, false, true, true), // missing viewBox, properly formed width & height; alternate format
} }
for index, svg := range validSVGs { for index, svg := range validSVGs {
svgInfo, err := parseSVG(svg) svgInfo, err := ParseSVG(svg)
if err != nil { if err != nil {
t.Errorf("Should be able to parse SVG attributes at index %d, but was not able to: err = %v", index, err) t.Errorf("Should be able to parse SVG attributes at index %d, but was not able to: err = %v", index, err)
} else { } else {
@@ -68,7 +68,7 @@ func TestParseInvalidSVGData(t *testing.T) {
generateSVGData(width, 0, false, true, false), // missing viewBox, malformed height, properly formed width generateSVGData(width, 0, false, true, false), // missing viewBox, malformed height, properly formed width
} }
for index, svg := range invalidSVGs { for index, svg := range invalidSVGs {
_, err := parseSVG(svg) _, err := ParseSVG(svg)
if err == nil { if err == nil {
t.Errorf("Should not be able to parse SVG attributes at index %d, but was definitely able to!", index) t.Errorf("Should not be able to parse SVG attributes at index %d, but was definitely able to!", index)
} }

147
app/imaging/utils.go Обычный файл
Просмотреть файл

@@ -0,0 +1,147 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"image"
"image/color"
"github.com/disintegration/imaging"
)
type rawImg interface {
Set(x, y int, c color.Color)
Opaque() bool
}
func isFullyTransparent(c color.Color) bool {
// TODO: This can be optimized by checking the color type and
// only extract the needed alpha value.
_, _, _, a := c.RGBA()
return a == 0
}
// FillImageTransparency fills in-place all the fully transparent pixels of the
// input image with the given color.
func FillImageTransparency(img image.Image, c color.Color) {
var i rawImg
bounds := img.Bounds()
fillFunc := func() {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
if isFullyTransparent(img.At(x, y)) {
i.Set(x, y, c)
}
}
}
}
switch raw := img.(type) {
case *image.Alpha:
i = raw
case *image.Alpha16:
i = raw
case *image.Gray:
i = raw
case *image.Gray16:
i = raw
case *image.NRGBA:
i = raw
col := color.NRGBAModel.Convert(c).(color.NRGBA)
fillFunc = func() {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
i := raw.PixOffset(x, y)
if raw.Pix[i+3] == 0x00 {
raw.Pix[i] = col.R
raw.Pix[i+1] = col.G
raw.Pix[i+2] = col.B
raw.Pix[i+3] = col.A
}
}
}
}
case *image.NRGBA64:
i = raw
col := color.NRGBA64Model.Convert(c).(color.NRGBA64)
fillFunc = func() {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
i := raw.PixOffset(x, y)
a := uint16(raw.Pix[i+6])<<8 | uint16(raw.Pix[i+7])
if a == 0 {
raw.Pix[i] = uint8(col.R >> 8)
raw.Pix[i+1] = uint8(col.R)
raw.Pix[i+2] = uint8(col.G >> 8)
raw.Pix[i+3] = uint8(col.G)
raw.Pix[i+4] = uint8(col.B >> 8)
raw.Pix[i+5] = uint8(col.B)
raw.Pix[i+6] = uint8(col.A >> 8)
raw.Pix[i+7] = uint8(col.A)
}
}
}
}
case *image.Paletted:
i = raw
fillFunc = func() {
for i := range raw.Palette {
if isFullyTransparent(raw.Palette[i]) {
raw.Palette[i] = c
}
}
}
case *image.RGBA:
i = raw
col := color.RGBAModel.Convert(c).(color.RGBA)
fillFunc = func() {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
i := raw.PixOffset(x, y)
if raw.Pix[i+3] == 0x00 {
raw.Pix[i] = col.R
raw.Pix[i+1] = col.G
raw.Pix[i+2] = col.B
raw.Pix[i+3] = col.A
}
}
}
}
case *image.RGBA64:
i = raw
col := color.RGBA64Model.Convert(c).(color.RGBA64)
fillFunc = func() {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
i := raw.PixOffset(x, y)
a := uint16(raw.Pix[i+6])<<8 | uint16(raw.Pix[i+7])
if a == 0 {
raw.Pix[i] = uint8(col.R >> 8)
raw.Pix[i+1] = uint8(col.R)
raw.Pix[i+2] = uint8(col.G >> 8)
raw.Pix[i+3] = uint8(col.G)
raw.Pix[i+4] = uint8(col.B >> 8)
raw.Pix[i+5] = uint8(col.B)
raw.Pix[i+6] = uint8(col.A >> 8)
raw.Pix[i+7] = uint8(col.A)
}
}
}
}
default:
return
}
if !i.Opaque() {
fillFunc()
}
}
// 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)
}

103
app/imaging/utils_bench_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,103 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"image"
"image/color"
"image/draw"
"testing"
)
func fillImageTransparencyOld(img image.Image, c color.Color) {
dst := image.NewRGBA(img.Bounds())
draw.Draw(dst, dst.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
draw.Draw(dst, dst.Bounds(), img, img.Bounds().Min, draw.Over)
}
func fullyOpaqueGen(w, h int) func() image.Image {
return func() image.Image {
dst := image.NewRGBA(image.Rect(0, 0, w, h))
draw.Draw(dst, dst.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
return dst
}
}
func partiallyOpaqueGen(w, h int) func() image.Image {
return func() image.Image {
dst := image.NewRGBA(image.Rect(0, 0, w, h))
draw.Draw(dst, image.Rect(0, 0, w/2, h/2), image.NewUniform(color.White), image.Point{}, draw.Src)
return dst
}
}
func fullyTransparentGen(w, h int) func() image.Image {
return func() image.Image {
return image.NewRGBA(image.Rect(0, 0, w, h))
}
}
func fullyOpaquePaletteGen(w, h int) func() image.Image {
return func() image.Image {
return image.NewPaletted(image.Rect(0, 0, w, h), []color.Color{image.White})
}
}
func fullyTransparentPaletteGen(w, h int) func() image.Image {
return func() image.Image {
return image.NewPaletted(image.Rect(0, 0, w, h), []color.Color{image.Transparent})
}
}
var tcs = []struct {
name string
imgGen func() image.Image
}{
{
"10MPx fully transparent RGBA",
fullyTransparentGen(1000, 1000),
},
{
"10MPx partially opaque RGBA",
partiallyOpaqueGen(1000, 1000),
},
{
"10MPx fully opaque RGBA",
fullyOpaqueGen(1000, 1000),
},
{
"10MPx fully opaque palette",
fullyOpaquePaletteGen(1000, 1000),
},
{
"10MPx fully transparent palette",
fullyTransparentPaletteGen(1000, 1000),
},
}
func BenchmarkFillImageTransparency(b *testing.B) {
for _, tc := range tcs {
b.Run(tc.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
b.StopTimer()
img := tc.imgGen()
b.StartTimer()
FillImageTransparency(img, image.White)
}
})
}
}
func BenchmarkFillImageTransparencyOld(b *testing.B) {
for _, tc := range tcs {
b.Run(tc.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
b.StopTimer()
img := tc.imgGen()
b.StartTimer()
fillImageTransparencyOld(img, image.White)
}
})
}
}

115
app/imaging/utils_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,115 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"bytes"
"image/color"
"io/ioutil"
"os"
"testing"
"github.com/mattermost/mattermost-server/v5/utils/fileutils"
"github.com/stretchr/testify/require"
)
func TestFillImageTransparency(t *testing.T) {
tcs := []struct {
name string
inputName string
outputName string
fillColor color.Color
}{
{
"8-bit Palette",
"fill_test_8bit_palette.png",
"fill_test_8bit_palette_out.png",
color.RGBA{0, 255, 0, 255},
},
{
"8-bit RGB",
"fill_test_8bit_rgb.png",
"fill_test_8bit_rgb_out.png",
color.RGBA{0, 255, 0, 255},
},
{
"8-bit RGBA",
"fill_test_8bit_rgba.png",
"fill_test_8bit_rgba_out.png",
color.RGBA{0, 255, 0, 255},
},
{
"16-bit RGB",
"fill_test_16bit_rgb.png",
"fill_test_16bit_rgb_out.png",
color.RGBA{0, 255, 0, 255},
},
{
"16-bit RGBA",
"fill_test_16bit_rgba.png",
"fill_test_16bit_rgba_out.png",
color.RGBA{0, 255, 0, 255},
},
}
imgDir, ok := fileutils.FindDir("tests")
require.True(t, ok)
e, err := NewEncoder(EncoderOptions{})
require.NotNil(t, e)
require.NoError(t, err)
d, err := NewDecoder(DecoderOptions{})
require.NotNil(t, d)
require.NoError(t, err)
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 inputFile.Close()
inputImg, format, err := d.Decode(inputFile)
require.NoError(t, err)
require.NotNil(t, inputImg)
require.Equal(t, "png", format)
expectedBytes, err := ioutil.ReadFile(imgDir + "/" + tc.outputName)
require.NoError(t, err)
FillImageTransparency(inputImg, tc.fillColor)
var b bytes.Buffer
err = e.EncodePNG(&b, inputImg)
require.NoError(t, err)
require.Equal(t, expectedBytes, b.Bytes())
})
}
t.Run("Opaque image", func(t *testing.T) {
inputFile, err := os.Open(imgDir + "/fill_test_opaque.png")
require.NoError(t, err)
require.NotNil(t, inputFile)
defer inputFile.Close()
inputImg, format, err := d.Decode(inputFile)
require.NoError(t, err)
require.NotNil(t, inputImg)
require.Equal(t, "png", format)
inputFile.Seek(0, 0)
expectedImg, format, err := d.Decode(inputFile)
require.NoError(t, err)
require.NotNil(t, expectedImg)
require.Equal(t, "png", format)
FillImageTransparency(inputImg, color.RGBA{0, 255, 0, 255})
require.Equal(t, expectedImg, inputImg)
})
}

Просмотреть файл

@@ -38,6 +38,7 @@ import (
"golang.org/x/crypto/acme/autocert" "golang.org/x/crypto/acme/autocert"
"github.com/mattermost/mattermost-server/v5/app/featureflag" "github.com/mattermost/mattermost-server/v5/app/featureflag"
"github.com/mattermost/mattermost-server/v5/app/imaging"
"github.com/mattermost/mattermost-server/v5/app/request" "github.com/mattermost/mattermost-server/v5/app/request"
"github.com/mattermost/mattermost-server/v5/audit" "github.com/mattermost/mattermost-server/v5/audit"
"github.com/mattermost/mattermost-server/v5/config" "github.com/mattermost/mattermost-server/v5/config"
@@ -207,6 +208,9 @@ type Server struct {
featureFlagStop chan struct{} featureFlagStop chan struct{}
featureFlagStopped chan struct{} featureFlagStopped chan struct{}
featureFlagSynchronizerMutex sync.Mutex featureFlagSynchronizerMutex sync.Mutex
imgDecoder *imaging.Decoder
imgEncoder *imaging.Encoder
} }
func NewServer(options ...Option) (*Server, error) { func NewServer(options ...Option) (*Server, error) {
@@ -245,6 +249,20 @@ func NewServer(options ...Option) (*Server, error) {
mlog.Error("Could not initiate logging", mlog.Err(err)) mlog.Error("Could not initiate logging", mlog.Err(err))
} }
var imgErr error
s.imgDecoder, imgErr = imaging.NewDecoder(imaging.DecoderOptions{
ConcurrencyLevel: runtime.NumCPU(),
})
if imgErr != nil {
return nil, errors.Wrap(imgErr, "failed to create image decoder")
}
s.imgEncoder, imgErr = imaging.NewEncoder(imaging.EncoderOptions{
ConcurrencyLevel: runtime.NumCPU(),
})
if imgErr != nil {
return nil, errors.Wrap(imgErr, "failed to create image encoder")
}
// This is called after initLogging() to avoid a race condition. // This is called after initLogging() to avoid a race condition.
mlog.Info("Server is initializing...", mlog.String("go_version", runtime.Version())) mlog.Info("Server is initializing...", mlog.String("go_version", runtime.Version()))

Просмотреть файл

@@ -7,6 +7,7 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"image"
"mime/multipart" "mime/multipart"
"regexp" "regexp"
"strings" "strings"
@@ -39,7 +40,13 @@ func (a *App) SlackImport(c *request.Context, fileData multipart.File, fileSize
GeneratePreviewImage: a.generatePreviewImage, GeneratePreviewImage: a.generatePreviewImage,
InvalidateAllCaches: func() { a.srv.InvalidateAllCaches() }, InvalidateAllCaches: func() { a.srv.InvalidateAllCaches() },
MaxPostSize: func() int { return a.srv.MaxPostSize() }, MaxPostSize: func() int { return a.srv.MaxPostSize() },
PrepareImage: prepareImage, PrepareImage: func(fileData []byte) (image.Image, func(), error) {
img, release, err := prepareImage(a.srv.imgDecoder, bytes.NewReader(fileData))
if err != nil {
return nil, nil, err
}
return img, release, err
},
} }
importer := slackimport.New(a.srv.Store, actions, a.Config()) importer := slackimport.New(a.srv.Store, actions, a.Config())

Просмотреть файл

@@ -9,15 +9,13 @@ import (
"errors" "errors"
"fmt" "fmt"
"image" "image"
"image/png"
"io" "io"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"github.com/disintegration/imaging" "github.com/mattermost/mattermost-server/v5/app/imaging"
"github.com/mattermost/mattermost-server/v5/app/request" "github.com/mattermost/mattermost-server/v5/app/request"
"github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/plugin" "github.com/mattermost/mattermost-server/v5/plugin"
@@ -1898,21 +1896,11 @@ func (a *App) SetTeamIconFromMultiPartFile(teamID string, file multipart.File) *
return model.NewAppError("setTeamIcon", "api.team.set_team_icon.storage.app_error", nil, "", http.StatusNotImplemented) return model.NewAppError("setTeamIcon", "api.team.set_team_icon.storage.app_error", nil, "", http.StatusNotImplemented)
} }
// Decode image config first to check dimensions before loading the whole thing into memory later on if limitErr := checkImageLimits(file); limitErr != nil {
config, _, err := image.DecodeConfig(file) return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.check_image_limits.app_error",
if err != nil { nil, limitErr.Error(), http.StatusBadRequest)
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.decode_config.app_error", nil, err.Error(), http.StatusBadRequest)
} }
// This casting is done to prevent overflow on 32 bit systems (not needed
// in 64 bits systems because images can't have more than 32 bits height or
// width)
if int64(config.Width)*int64(config.Height) > model.MaxImageSize {
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.too_large.app_error", nil, "", http.StatusBadRequest)
}
file.Seek(0, 0)
return a.SetTeamIconFromFile(team, file) return a.SetTeamIconFromFile(team, file)
} }
@@ -1923,15 +1911,15 @@ func (a *App) SetTeamIconFromFile(team *model.Team, file io.Reader) *model.AppEr
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.decode.app_error", nil, err.Error(), http.StatusBadRequest) return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.decode.app_error", nil, err.Error(), http.StatusBadRequest)
} }
orientation, _ := getImageOrientation(file) orientation, _ := imaging.GetImageOrientation(file)
img = makeImageUpright(img, orientation) img = imaging.MakeImageUpright(img, orientation)
// Scale team icon // Scale team icon
teamIconWidthAndHeight := 128 teamIconWidthAndHeight := 128
img = imaging.Fill(img, teamIconWidthAndHeight, teamIconWidthAndHeight, imaging.Center, imaging.Lanczos) img = imaging.FillCenter(img, teamIconWidthAndHeight, teamIconWidthAndHeight)
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
err = png.Encode(buf, img) err = a.srv.imgEncoder.EncodePNG(buf, img)
if err != nil { if err != nil {
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.encode.app_error", nil, err.Error(), http.StatusInternalServerError) return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.encode.app_error", nil, err.Error(), http.StatusInternalServerError)
} }

Просмотреть файл

@@ -265,14 +265,11 @@ func (a *App) UploadData(c *request.Context, us *model.UploadSession, rd io.Read
// image post-processing // image post-processing
if info.IsImage() { if info.IsImage() {
// Check dimensions before loading the whole thing into memory later on if limitErr := checkImageResolutionLimit(info.Width, info.Height); limitErr != nil {
// This casting is done to prevent overflow on 32 bit systems (not needed
// in 64 bits systems because images can't have more than 32 bits height or
// width)
if int64(info.Width)*int64(info.Height) > MaxImageSize {
return nil, model.NewAppError("uploadData", "app.upload.upload_data.large_image.app_error", return nil, model.NewAppError("uploadData", "app.upload.upload_data.large_image.app_error",
map[string]interface{}{"Filename": us.Filename, "Width": info.Width, "Height": info.Height}, "", http.StatusBadRequest) map[string]interface{}{"Filename": us.Filename, "Width": info.Width, "Height": info.Height}, "", http.StatusBadRequest)
} }
nameWithoutExtension := info.Name[:strings.LastIndex(info.Name, ".")] nameWithoutExtension := info.Name[:strings.LastIndex(info.Name, ".")]
info.PreviewPath = filepath.Dir(info.Path) + "/" + nameWithoutExtension + "_preview.jpg" info.PreviewPath = filepath.Dir(info.Path) + "/" + nameWithoutExtension + "_preview.jpg"
info.ThumbnailPath = filepath.Dir(info.Path) + "/" + nameWithoutExtension + "_thumb.jpg" info.ThumbnailPath = filepath.Dir(info.Path) + "/" + nameWithoutExtension + "_thumb.jpg"

Просмотреть файл

@@ -14,8 +14,6 @@ import (
"image" "image"
"image/color" "image/color"
"image/draw" "image/draw"
_ "image/gif"
_ "image/jpeg"
"image/png" "image/png"
"io" "io"
"io/ioutil" "io/ioutil"
@@ -25,10 +23,10 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/disintegration/imaging"
"github.com/golang/freetype" "github.com/golang/freetype"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/mattermost/mattermost-server/v5/app/imaging"
"github.com/mattermost/mattermost-server/v5/app/request" "github.com/mattermost/mattermost-server/v5/app/request"
"github.com/mattermost/mattermost-server/v5/einterfaces" "github.com/mattermost/mattermost-server/v5/einterfaces"
"github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/model"
@@ -894,39 +892,29 @@ func (a *App) SetProfileImage(userID string, imageData *multipart.FileHeader) *m
} }
func (a *App) SetProfileImageFromMultiPartFile(userID string, file multipart.File) *model.AppError { func (a *App) SetProfileImageFromMultiPartFile(userID string, file multipart.File) *model.AppError {
// Decode image config first to check dimensions before loading the whole thing into memory later on if limitErr := checkImageLimits(file); limitErr != nil {
config, _, err := image.DecodeConfig(file) return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.check_image_limits.app_error", nil, "", http.StatusBadRequest)
if err != nil {
return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.decode_config.app_error", nil, err.Error(), http.StatusBadRequest)
} }
// This casting is done to prevent overflow on 32 bit systems (not needed
// in 64 bits systems because images can't have more than 32 bits height or
// width)
if int64(config.Width)*int64(config.Height) > model.MaxImageSize {
return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.too_large.app_error", nil, "", http.StatusBadRequest)
}
file.Seek(0, 0)
return a.SetProfileImageFromFile(userID, file) return a.SetProfileImageFromFile(userID, file)
} }
func (a *App) AdjustImage(file io.Reader) (*bytes.Buffer, *model.AppError) { func (a *App) AdjustImage(file io.Reader) (*bytes.Buffer, *model.AppError) {
// Decode image into Image object // Decode image into Image object
img, _, err := image.Decode(file) img, _, err := a.srv.imgDecoder.Decode(file)
if err != nil { if err != nil {
return nil, model.NewAppError("SetProfileImage", "api.user.upload_profile_user.decode.app_error", nil, err.Error(), http.StatusBadRequest) return nil, model.NewAppError("SetProfileImage", "api.user.upload_profile_user.decode.app_error", nil, err.Error(), http.StatusBadRequest)
} }
orientation, _ := getImageOrientation(file) orientation, _ := imaging.GetImageOrientation(file)
img = makeImageUpright(img, orientation) img = imaging.MakeImageUpright(img, orientation)
// Scale profile image // Scale profile image
profileWidthAndHeight := 128 profileWidthAndHeight := 128
img = imaging.Fill(img, profileWidthAndHeight, profileWidthAndHeight, imaging.Center, imaging.Lanczos) img = imaging.FillCenter(img, profileWidthAndHeight, profileWidthAndHeight)
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
err = png.Encode(buf, img) err = a.srv.imgEncoder.EncodePNG(buf, img)
if err != nil { if err != nil {
return nil, model.NewAppError("SetProfileImage", "api.user.upload_profile_user.encode.app_error", nil, err.Error(), http.StatusInternalServerError) return nil, model.NewAppError("SetProfileImage", "api.user.upload_profile_user.encode.app_error", nil, err.Error(), http.StatusInternalServerError)
} }

Просмотреть файл

@@ -3011,12 +3011,12 @@
"translation": "Empty array under 'image' in request." "translation": "Empty array under 'image' in request."
}, },
{ {
"id": "api.team.set_team_icon.decode.app_error", "id": "api.team.set_team_icon.check_image_limits.app_error",
"translation": "Could not decode team icon." "translation": "Image limits check failed. Resolution is too high."
}, },
{ {
"id": "api.team.set_team_icon.decode_config.app_error", "id": "api.team.set_team_icon.decode.app_error",
"translation": "Could not decode team icon metadata." "translation": "Could not decode team icon."
}, },
{ {
"id": "api.team.set_team_icon.encode.app_error", "id": "api.team.set_team_icon.encode.app_error",
@@ -4199,12 +4199,12 @@
"translation": "Empty array under 'image' in request." "translation": "Empty array under 'image' in request."
}, },
{ {
"id": "api.user.upload_profile_user.decode.app_error", "id": "api.user.upload_profile_user.check_image_limits.app_error",
"translation": "Could not decode profile image." "translation": "Image limits check failed. Resolution is too high."
}, },
{ {
"id": "api.user.upload_profile_user.decode_config.app_error", "id": "api.user.upload_profile_user.decode.app_error",
"translation": "Could not save profile image. File does not appear to be a valid image." "translation": "Could not decode profile image."
}, },
{ {
"id": "api.user.upload_profile_user.encode.app_error", "id": "api.user.upload_profile_user.encode.app_error",
@@ -6743,12 +6743,12 @@
"translation": "Failed to close user index." "translation": "Failed to close user index."
}, },
{ {
"id": "brand.save_brand_image.decode.app_error", "id": "brand.save_brand_image.check_image_limits.app_error",
"translation": "Unable to decode the image data." "translation": "Image limits check failed. Resolution is too high."
}, },
{ {
"id": "brand.save_brand_image.decode_config.app_error", "id": "brand.save_brand_image.decode.app_error",
"translation": "Unable to get image metadata." "translation": "Unable to decode the image data."
}, },
{ {
"id": "brand.save_brand_image.encode.app_error", "id": "brand.save_brand_image.encode.app_error",
@@ -6762,10 +6762,6 @@
"id": "brand.save_brand_image.save_image.app_error", "id": "brand.save_brand_image.save_image.app_error",
"translation": "Unable to write the image file to your file storage. Please check your connection and try again." "translation": "Unable to write the image file to your file storage. Please check your connection and try again."
}, },
{
"id": "brand.save_brand_image.too_large.app_error",
"translation": "Unable to read the image file. Make sure the image size is less than 2 MB and try again."
},
{ {
"id": "cli.license.critical", "id": "cli.license.critical",
"translation": "Feature requires an upgrade to Enterprise Edition and the inclusion of a license key. Please contact your System Administrator." "translation": "Feature requires an upgrade to Enterprise Edition and the inclusion of a license key. Please contact your System Administrator."

Просмотреть файл

@@ -4,20 +4,14 @@
package model package model
import ( import (
"bytes"
"encoding/json" "encoding/json"
"image" "image"
"image/gif" "image/gif"
"image/jpeg"
"io" "io"
"mime" "mime"
"net/http" "net/http"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/disintegration/imaging"
"github.com/mattermost/mattermost-server/v5/shared/mlog"
) )
const ( const (
@@ -162,19 +156,6 @@ func NewInfo(name string) *FileInfo {
return info return info
} }
func GenerateMiniPreviewImage(img image.Image) *[]byte {
preview := imaging.Resize(img, 16, 16, imaging.Lanczos)
buf := new(bytes.Buffer)
if err := jpeg.Encode(buf, preview, &jpeg.Options{Quality: 90}); err != nil {
mlog.Info("Unable to encode image as mini preview jpg", mlog.Err(err))
return nil
}
data := buf.Bytes()
return &data
}
func GetInfoForBytes(name string, data io.ReadSeeker, size int) (*FileInfo, *AppError) { func GetInfoForBytes(name string, data io.ReadSeeker, size int) (*FileInfo, *AppError) {
info := &FileInfo{ info := &FileInfo{
Name: name, Name: name,

Просмотреть файл

@@ -93,7 +93,7 @@ type Actions struct {
GeneratePreviewImage func(image.Image, string) GeneratePreviewImage func(image.Image, string)
InvalidateAllCaches func() InvalidateAllCaches func()
MaxPostSize func() int MaxPostSize func() int
PrepareImage func(fileData []byte) (image.Image, int, int) PrepareImage func(fileData []byte) (image.Image, func(), error)
} }
// SlackImporter is a service that allows to import slack dumps into mattermost // SlackImporter is a service that allows to import slack dumps into mattermost
@@ -772,12 +772,14 @@ func (si *SlackImporter) oldImportFile(timestamp time.Time, file io.Reader, team
} }
if fileInfo.IsImage() && fileInfo.MimeType != "image/svg+xml" { if fileInfo.IsImage() && fileInfo.MimeType != "image/svg+xml" {
img, _, _ := si.actions.PrepareImage(data) img, release, err := si.actions.PrepareImage(data)
if img != nil { if err != nil {
return nil, err
}
defer release()
si.actions.GenerateThumbnailImage(img, fileInfo.ThumbnailPath) si.actions.GenerateThumbnailImage(img, fileInfo.ThumbnailPath)
si.actions.GeneratePreviewImage(img, fileInfo.PreviewPath) si.actions.GeneratePreviewImage(img, fileInfo.PreviewPath)
} }
}
return fileInfo, nil return fileInfo, nil
} }

Двоичные данные
tests/fill_test_16bit_rgb.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.2 KiB

Двоичные данные
tests/fill_test_16bit_rgb_out.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.2 KiB

Двоичные данные
tests/fill_test_16bit_rgba.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.5 KiB

Двоичные данные
tests/fill_test_16bit_rgba_out.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.5 KiB

Двоичные данные
tests/fill_test_8bit_palette.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 724 B

Двоичные данные
tests/fill_test_8bit_palette_out.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 140 B

Двоичные данные
tests/fill_test_8bit_rgb.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 889 B

Двоичные данные
tests/fill_test_8bit_rgb_out.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 896 B

Двоичные данные
tests/fill_test_8bit_rgba.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.1 KiB

Двоичные данные
tests/fill_test_8bit_rgba_out.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.1 KiB

Двоичные данные
tests/fill_test_opaque.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 299 B