[MM-63436] Replace Exif parser dependency (#30479)
* Replace Exif parser dependency * Improve forward seeking logic * Fix linting * Stop decoding upon finding tag * Use latest version of imagemeta dependency * Don't skip TIFF reader tests * Log improvements --------- Co-authored-by: Mattermost Build <build@mattermost.com>
@@ -60,7 +60,8 @@ func fileBytes(t *testing.T, path string) []byte {
|
||||
}
|
||||
|
||||
func testDoUploadFileRequest(t testing.TB, c *model.Client4, url string, blob []byte, contentType string,
|
||||
contentLength int64) (*model.FileUploadResponse, *model.Response, error) {
|
||||
contentLength int64,
|
||||
) (*model.FileUploadResponse, *model.Response, error) {
|
||||
req, err := http.NewRequest("POST", c.APIURL+"/files"+url, bytes.NewReader(blob))
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -390,6 +391,19 @@ func TestUploadFiles(t *testing.T) {
|
||||
expectedImageMiniPreview: []bool{true},
|
||||
expectedCreatorId: th.BasicUser.Id,
|
||||
},
|
||||
// 5MB+ JPEG
|
||||
{
|
||||
title: "Happy image thumbnail/preview 5MB+",
|
||||
names: []string{"orientation_test_9.jpeg"},
|
||||
expectedImageThumbnailNames: []string{"orientation_test_9_expected_thumb.jpeg"},
|
||||
expectedImagePreviewNames: []string{"orientation_test_9_expected_preview.jpeg"},
|
||||
expectImage: true,
|
||||
expectedImageWidths: []int{4000},
|
||||
expectedImageHeights: []int{2667},
|
||||
expectedImageHasPreview: []bool{true},
|
||||
expectedImageMiniPreview: []bool{true},
|
||||
expectedCreatorId: th.BasicUser.Id,
|
||||
},
|
||||
// TIFF preview test
|
||||
{
|
||||
title: "Happy image thumbnail/preview 9",
|
||||
|
||||
@@ -1664,7 +1664,6 @@ func getTeamIcon(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
team, err := c.App.GetTeam(c.Params.TeamId)
|
||||
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
@@ -1742,7 +1741,7 @@ func setTeamIcon(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
imageData := imageArray[0]
|
||||
|
||||
if err := c.App.SetTeamIcon(c.Params.TeamId, imageData); err != nil {
|
||||
if err := c.App.SetTeamIcon(c.AppContext, c.Params.TeamId, imageData); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
@@ -770,11 +770,8 @@ func (t *UploadFileTask) init(a *App) {
|
||||
// upload, returning a rejection error. In this case FileInfo would have
|
||||
// contained the last "good" FileInfo before the execution of that plugin.
|
||||
func (a *App) UploadFileX(c request.CTX, channelID, name string, input io.Reader,
|
||||
opts ...func(*UploadFileTask)) (*model.FileInfo, *model.AppError) {
|
||||
c = c.WithLogger(c.Logger().With(
|
||||
mlog.String("file_name", name),
|
||||
))
|
||||
|
||||
opts ...func(*UploadFileTask),
|
||||
) (*model.FileInfo, *model.AppError) {
|
||||
t := &UploadFileTask{
|
||||
Logger: c.Logger(),
|
||||
ChannelId: filepath.Base(channelID),
|
||||
@@ -790,6 +787,12 @@ func (a *App) UploadFileX(c request.CTX, channelID, name string, input io.Reader
|
||||
o(t)
|
||||
}
|
||||
|
||||
c = c.WithLogger(c.Logger().With(
|
||||
mlog.String("file_name", name),
|
||||
mlog.String("channel_id", channelID),
|
||||
mlog.String("user_id", t.UserId),
|
||||
))
|
||||
|
||||
if *a.Config().FileSettings.DriverName == "" {
|
||||
return nil, t.newAppError("api.file.upload_file.storage.app_error", http.StatusNotImplemented)
|
||||
}
|
||||
@@ -880,14 +883,14 @@ func (t *UploadFileTask) preprocessImage() *model.AppError {
|
||||
}
|
||||
|
||||
// If we fail to decode, return "as is".
|
||||
w, h, err := imaging.GetDimensions(t.teeInput)
|
||||
cfg, format, err := t.imgDecoder.DecodeConfig(t.teeInput)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
t.fileinfo.Width = w
|
||||
t.fileinfo.Height = h
|
||||
t.fileinfo.Width = cfg.Width
|
||||
t.fileinfo.Height = cfg.Height
|
||||
|
||||
if err = checkImageResolutionLimit(w, h, t.maxImageRes); err != nil {
|
||||
if err = checkImageResolutionLimit(cfg.Width, cfg.Height, t.maxImageRes); err != nil {
|
||||
return t.newAppError("api.file.upload_file.large_image_detailed.app_error", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
@@ -899,12 +902,14 @@ func (t *UploadFileTask) preprocessImage() *model.AppError {
|
||||
// check the image orientation with goexif; consume the bytes we
|
||||
// already have first, then keep Tee-ing from input.
|
||||
// TODO: try to reuse exif's .Raw buffer rather than Tee-ing
|
||||
if t.imageOrientation, err = imaging.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), format); err == nil &&
|
||||
(t.imageOrientation == imaging.RotatedCWMirrored ||
|
||||
t.imageOrientation == imaging.RotatedCCW ||
|
||||
t.imageOrientation == imaging.RotatedCCWMirrored ||
|
||||
t.imageOrientation == imaging.RotatedCW) {
|
||||
t.fileinfo.Width, t.fileinfo.Height = t.fileinfo.Height, t.fileinfo.Width
|
||||
} else if err != nil {
|
||||
t.Logger.Warn("Failed to get image orientation", mlog.Err(err))
|
||||
}
|
||||
|
||||
// For animated GIFs disable the preview; since we have to Decode gifs
|
||||
@@ -1045,12 +1050,14 @@ func (a *App) DoUploadFileExpectModification(c request.CTX, now time.Time, rawTe
|
||||
return nil, data, err
|
||||
}
|
||||
|
||||
if orientation, err := imaging.GetImageOrientation(bytes.NewReader(data)); err == nil &&
|
||||
if orientation, err := imaging.GetImageOrientation(bytes.NewReader(data), info.MimeType); err == nil &&
|
||||
(orientation == imaging.RotatedCWMirrored ||
|
||||
orientation == imaging.RotatedCCW ||
|
||||
orientation == imaging.RotatedCCWMirrored ||
|
||||
orientation == imaging.RotatedCW) {
|
||||
info.Width, info.Height = info.Height, info.Width
|
||||
} else if err != nil {
|
||||
c.Logger().Warn("Failed to get image orientation", mlog.Err(err))
|
||||
}
|
||||
|
||||
info.Id = model.NewId()
|
||||
@@ -1161,7 +1168,7 @@ func prepareImage(rctx request.CTX, imgDecoder *imaging.Decoder, imgData io.Read
|
||||
imgData.Seek(0, io.SeekStart)
|
||||
|
||||
// Flip the image to be upright
|
||||
orientation, err := imaging.GetImageOrientation(imgData)
|
||||
orientation, err := imaging.GetImageOrientation(imgData, imgType)
|
||||
if err != nil {
|
||||
rctx.Logger().Debug("GetImageOrientation failed", mlog.Err(err))
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
package imaging
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/anthonynsimon/bild/transform"
|
||||
"github.com/rwcarlsen/goexif/exif"
|
||||
"github.com/bep/imagemeta"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -33,6 +35,8 @@ const (
|
||||
RotatedCW
|
||||
)
|
||||
|
||||
var errStopDecoding = fmt.Errorf("stop decoding")
|
||||
|
||||
// MakeImageUpright changes the orientation of the given image.
|
||||
func MakeImageUpright(img image.Image, orientation int) image.Image {
|
||||
switch orientation {
|
||||
@@ -55,23 +59,101 @@ func MakeImageUpright(img image.Image, orientation int) image.Image {
|
||||
}
|
||||
}
|
||||
|
||||
type fwSeeker struct {
|
||||
r io.Reader
|
||||
pos int64
|
||||
}
|
||||
|
||||
func (f *fwSeeker) Read(p []byte) (int, error) {
|
||||
n, err := f.r.Read(p)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
f.pos += int64(n)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (f *fwSeeker) Seek(offset int64, whence int) (int64, error) {
|
||||
isForwardSeek := (whence == io.SeekStart && offset >= f.pos) ||
|
||||
(whence == io.SeekCurrent && offset >= 0)
|
||||
|
||||
// We only support seeking forward.
|
||||
if !isForwardSeek {
|
||||
return 0, fmt.Errorf("seeking backwards is not supported")
|
||||
}
|
||||
|
||||
toRead := offset
|
||||
if whence == io.SeekStart {
|
||||
toRead -= f.pos
|
||||
}
|
||||
|
||||
// Seeking forward means we can simply discard the data.
|
||||
n, err := io.CopyN(io.Discard, f.r, toRead)
|
||||
if err != nil {
|
||||
return n, fmt.Errorf("failed to seek: %w", err)
|
||||
}
|
||||
|
||||
f.pos += n
|
||||
|
||||
return f.pos, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// image orientation. Supported formats are JPEG, PNG, TIFF, and WebP.
|
||||
// Passing an io.ReadSeeker is preferable as we can't guarantee a plain
|
||||
// io.Reader will work for all formats (e.g. TIFF requires backwards seeking).
|
||||
func GetImageOrientation(input io.Reader, format string) (int, error) {
|
||||
orientation := Upright
|
||||
|
||||
// Strip the "image/" prefix from the format in case it's a MIME type.
|
||||
format, _ = strings.CutPrefix(format, "image/")
|
||||
|
||||
var imgFormat imagemeta.ImageFormat
|
||||
switch format {
|
||||
case "jpeg":
|
||||
imgFormat = imagemeta.JPEG
|
||||
case "png":
|
||||
imgFormat = imagemeta.PNG
|
||||
case "tiff":
|
||||
imgFormat = imagemeta.TIFF
|
||||
case "webp":
|
||||
imgFormat = imagemeta.WebP
|
||||
default:
|
||||
// We don't support EXIF on any other format.
|
||||
return orientation, fmt.Errorf("unsupported image format: %s", format)
|
||||
}
|
||||
|
||||
var rs io.ReadSeeker
|
||||
if r, ok := input.(io.ReadSeeker); ok {
|
||||
rs = r
|
||||
} else {
|
||||
rs = &fwSeeker{r: input}
|
||||
}
|
||||
|
||||
opts := imagemeta.Options{
|
||||
R: rs,
|
||||
HandleTag: func(tag imagemeta.TagInfo) error {
|
||||
if tag.Tag == "Orientation" {
|
||||
if o, ok := tag.Value.(uint16); ok {
|
||||
orientation = int(o)
|
||||
// Stop decoding after we've found the orientation tag]
|
||||
// since it's the only one we care about.
|
||||
return errStopDecoding
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
ShouldHandleTag: func(tag imagemeta.TagInfo) bool {
|
||||
// We only care about the orientation tag.
|
||||
return tag.Tag == "Orientation"
|
||||
},
|
||||
Sources: imagemeta.EXIF, // We only care about EXIF data.
|
||||
ImageFormat: imgFormat,
|
||||
}
|
||||
|
||||
if err := imagemeta.Decode(opts); err != nil && !errors.Is(err, errStopDecoding) {
|
||||
return Upright, fmt.Errorf("failed to decode exif data: %w", err)
|
||||
}
|
||||
|
||||
tag, err := exifData.Get("Orientation")
|
||||
if err != nil {
|
||||
return Upright, fmt.Errorf("failed to get orientation field from exif data: %w", err)
|
||||
}
|
||||
|
||||
orientation, err := tag.Int(0)
|
||||
if err != nil {
|
||||
return Upright, fmt.Errorf("failed to get value from exif tag: %w", err)
|
||||
}
|
||||
|
||||
return orientation, nil
|
||||
}
|
||||
|
||||
199
server/channels/app/imaging/orientation_test.go
Обычный файл
@@ -0,0 +1,199 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package imaging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFwSeeker(t *testing.T) {
|
||||
t.Run("Read", func(t *testing.T) {
|
||||
data := []byte("hello world")
|
||||
reader := bytes.NewReader(data)
|
||||
seeker := &fwSeeker{r: reader}
|
||||
|
||||
// Read the first 5 bytes
|
||||
buf := make([]byte, 5)
|
||||
n, err := seeker.Read(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 5, n)
|
||||
require.Equal(t, []byte("hello"), buf)
|
||||
|
||||
// Read the next 6 bytes
|
||||
buf = make([]byte, 6)
|
||||
n, err = seeker.Read(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 6, n)
|
||||
require.Equal(t, []byte(" world"), buf)
|
||||
|
||||
// Try to read more, should get EOF
|
||||
buf = make([]byte, 1)
|
||||
n, err = seeker.Read(buf)
|
||||
require.Equal(t, 0, n)
|
||||
require.Equal(t, io.EOF, err)
|
||||
})
|
||||
|
||||
t.Run("Seek forward from start", func(t *testing.T) {
|
||||
data := []byte("hello world")
|
||||
reader := bytes.NewReader(data)
|
||||
seeker := &fwSeeker{r: reader}
|
||||
|
||||
// Seek forward 6 bytes
|
||||
pos, err := seeker.Seek(6, io.SeekStart)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(6), pos)
|
||||
|
||||
// Seeking from the same position should work
|
||||
pos, err = seeker.Seek(6, io.SeekStart)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(6), pos)
|
||||
|
||||
// Seeking again from start should work.
|
||||
pos, err = seeker.Seek(7, io.SeekStart)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(7), pos)
|
||||
|
||||
// Seeking backwards should not be supported
|
||||
_, err = seeker.Seek(6, io.SeekStart)
|
||||
require.EqualError(t, err, "seeking backwards is not supported")
|
||||
|
||||
// Read the remaining data
|
||||
buf := make([]byte, 4)
|
||||
n, err := seeker.Read(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 4, n)
|
||||
require.Equal(t, []byte("orld"), buf)
|
||||
})
|
||||
|
||||
t.Run("Seek forward from current", func(t *testing.T) {
|
||||
data := []byte("hello world")
|
||||
reader := bytes.NewReader(data)
|
||||
seeker := &fwSeeker{r: reader}
|
||||
|
||||
// Read first 6 bytes
|
||||
buf := make([]byte, 6)
|
||||
n, err := seeker.Read(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 6, n)
|
||||
require.Equal(t, []byte("hello "), buf)
|
||||
|
||||
// Seek forward 2 more bytes from current position
|
||||
pos, err := seeker.Seek(2, io.SeekCurrent)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(8), pos)
|
||||
|
||||
// Read the remaining data
|
||||
buf = make([]byte, 3)
|
||||
n, err = seeker.Read(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, n)
|
||||
require.Equal(t, []byte("rld"), buf)
|
||||
})
|
||||
|
||||
t.Run("Seek backward not supported", func(t *testing.T) {
|
||||
data := []byte("hello world")
|
||||
reader := bytes.NewReader(data)
|
||||
seeker := &fwSeeker{r: reader}
|
||||
|
||||
// Try to seek backward
|
||||
_, err := seeker.Seek(-1, io.SeekCurrent)
|
||||
require.EqualError(t, err, "seeking backwards is not supported")
|
||||
|
||||
// Try to seek from end
|
||||
_, err = seeker.Seek(0, io.SeekEnd)
|
||||
require.EqualError(t, err, "seeking backwards is not supported")
|
||||
})
|
||||
|
||||
t.Run("Seek beyond EOF", func(t *testing.T) {
|
||||
data := []byte("hello")
|
||||
reader := bytes.NewReader(data)
|
||||
seeker := &fwSeeker{r: reader}
|
||||
|
||||
// Try to seek beyond EOF
|
||||
n, err := seeker.Seek(10, io.SeekStart)
|
||||
require.EqualError(t, err, "failed to seek: EOF")
|
||||
require.Equal(t, int64(5), n) // Should have read until EOF (5 bytes)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetImageOrientation(t *testing.T) {
|
||||
imgDir, ok := fileutils.FindDir("tests/exif_samples")
|
||||
require.True(t, ok, "Failed to find exif samples directory")
|
||||
|
||||
// Define orientations and their corresponding file prefixes
|
||||
orientations := map[string]int{
|
||||
"up": Upright,
|
||||
"up-mirrored": UprightMirrored,
|
||||
"down": UpsideDown,
|
||||
"down-mirrored": UpsideDownMirrored,
|
||||
"left": RotatedCCW,
|
||||
"left-mirrored": RotatedCWMirrored,
|
||||
"right": RotatedCW,
|
||||
"right-mirrored": RotatedCCWMirrored,
|
||||
}
|
||||
|
||||
// Define supported formats
|
||||
formats := []string{"jpg", "png", "tiff", "webp"}
|
||||
|
||||
// Generate test cases for all combinations
|
||||
var testCases []struct {
|
||||
name string
|
||||
fileName string
|
||||
expectedOrientation int
|
||||
}
|
||||
|
||||
for prefix, orientation := range orientations {
|
||||
for _, format := range formats {
|
||||
testCases = append(testCases, struct {
|
||||
name string
|
||||
fileName string
|
||||
expectedOrientation int
|
||||
}{
|
||||
name: fmt.Sprintf("%s (%s)", prefix, format),
|
||||
fileName: fmt.Sprintf("%s.%s", prefix, format),
|
||||
expectedOrientation: orientation,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
dec, err := NewDecoder(DecoderOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, tc := range testCases {
|
||||
var orientation int
|
||||
imgPath := filepath.Join(imgDir, tc.fileName)
|
||||
file, err := os.Open(imgPath)
|
||||
require.NoError(t, err)
|
||||
defer file.Close()
|
||||
|
||||
_, format, err := dec.DecodeConfig(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run(tc.name+"_file", func(t *testing.T) {
|
||||
_, err = file.Seek(0, io.SeekStart)
|
||||
require.NoError(t, err)
|
||||
|
||||
orientation, err = GetImageOrientation(file, format)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectedOrientation, orientation, "Incorrect orientation detected for %s", tc.fileName)
|
||||
})
|
||||
|
||||
t.Run(tc.name+"_reader", func(t *testing.T) {
|
||||
_, err = file.Seek(0, io.SeekStart)
|
||||
require.NoError(t, err)
|
||||
|
||||
orientation, err = GetImageOrientation(&io.LimitedReader{R: file, N: 1024 * 1024}, format)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectedOrientation, orientation, "Incorrect orientation detected for %s", tc.fileName)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -948,7 +948,7 @@ func (api *PluginAPI) SetTeamIcon(teamID string, data []byte) *model.AppError {
|
||||
return err
|
||||
}
|
||||
|
||||
return api.app.SetTeamIconFromFile(team, bytes.NewReader(data))
|
||||
return api.app.SetTeamIconFromFile(api.ctx, team, bytes.NewReader(data))
|
||||
}
|
||||
|
||||
func (api *PluginAPI) OpenInteractiveDialog(dialog model.OpenDialogRequest) *model.AppError {
|
||||
@@ -1103,12 +1103,15 @@ func (api *PluginAPI) UpdateUserRoles(userID string, newRoles string) (*model.Us
|
||||
func (api *PluginAPI) LogDebug(msg string, keyValuePairs ...any) {
|
||||
api.logger.Debugw(msg, keyValuePairs...)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) LogInfo(msg string, keyValuePairs ...any) {
|
||||
api.logger.Infow(msg, keyValuePairs...)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) LogError(msg string, keyValuePairs ...any) {
|
||||
api.logger.Errorw(msg, keyValuePairs...)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) LogWarn(msg string, keyValuePairs ...any) {
|
||||
api.logger.Warnw(msg, keyValuePairs...)
|
||||
}
|
||||
@@ -1320,7 +1323,8 @@ func (api *PluginAPI) DeleteOAuthApp(appID string) *model.AppError {
|
||||
// PublishPluginClusterEvent broadcasts a plugin event to all other running instances of
|
||||
// the calling plugin.
|
||||
func (api *PluginAPI) PublishPluginClusterEvent(ev model.PluginClusterEvent,
|
||||
opts model.PluginClusterEventSendOptions) error {
|
||||
opts model.PluginClusterEventSendOptions,
|
||||
) error {
|
||||
if api.app.Cluster() == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1177,7 +1177,7 @@ func TestPluginAPIGetTeamIcon(t *testing.T) {
|
||||
fileReader := bytes.NewReader(dataBytes)
|
||||
|
||||
// Set the Team Icon
|
||||
appErr := th.App.SetTeamIconFromFile(th.BasicTeam, fileReader)
|
||||
appErr := th.App.SetTeamIconFromFile(th.Context, th.BasicTeam, fileReader)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Get the team icon to check
|
||||
@@ -1239,7 +1239,7 @@ func TestPluginAPIRemoveTeamIcon(t *testing.T) {
|
||||
fileReader := bytes.NewReader(dataBytes)
|
||||
|
||||
// Set the Team Icon
|
||||
err := th.App.SetTeamIconFromFile(th.BasicTeam, fileReader)
|
||||
err := th.App.SetTeamIconFromFile(th.Context, th.BasicTeam, fileReader)
|
||||
require.Nil(t, err)
|
||||
err = api.RemoveTeamIcon(th.BasicTeam.Id)
|
||||
require.Nil(t, err)
|
||||
|
||||
@@ -650,7 +650,6 @@ func (a *App) getLinkMetadata(c request.CTX, requestURL string, timestamp int64,
|
||||
var err error
|
||||
if looksLikeAPermalink(requestURL, a.GetSiteURL()) && *a.Config().ServiceSettings.EnablePermalinkPreviews {
|
||||
permalink, err = a.getLinkMetadataForPermalink(c, requestURL)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
@@ -784,7 +783,7 @@ func (a *App) getLinkMetadataForURL(c request.CTX, requestURL string) (*opengrap
|
||||
|
||||
if err == nil {
|
||||
// Parse the data
|
||||
og, image, err = a.parseLinkMetadata(requestURL, body, contentType)
|
||||
og, image, err = a.parseLinkMetadata(c, requestURL, body, contentType)
|
||||
}
|
||||
og = model.TruncateOpenGraph(og) // remove unwanted length of texts
|
||||
|
||||
@@ -878,7 +877,7 @@ func peekContentType(p *bufio.Reader) string {
|
||||
return http.DetectContentType(byt)
|
||||
}
|
||||
|
||||
func (a *App) parseLinkMetadata(requestURL string, body io.Reader, contentType string) (*opengraph.OpenGraph, *model.PostImage, error) {
|
||||
func (a *App) parseLinkMetadata(rctx request.CTX, requestURL string, body io.Reader, contentType string) (*opengraph.OpenGraph, *model.PostImage, error) {
|
||||
if contentType == "" {
|
||||
bufRd := bufio.NewReader(body)
|
||||
// If the content-type is missing we try to detect it from the actual data.
|
||||
@@ -893,7 +892,7 @@ func (a *App) parseLinkMetadata(requestURL string, body io.Reader, contentType s
|
||||
|
||||
return nil, image, nil
|
||||
} else if strings.HasPrefix(contentType, "image") {
|
||||
image, err := parseImages(io.LimitReader(body, MaxMetadataImageSize))
|
||||
image, err := parseImages(rctx, requestURL, io.LimitReader(body, MaxMetadataImageSize))
|
||||
return nil, image, err
|
||||
} else if strings.HasPrefix(contentType, "text/html") {
|
||||
og := a.parseOpenGraphMetadata(requestURL, body, contentType)
|
||||
@@ -909,7 +908,7 @@ func (a *App) parseLinkMetadata(requestURL string, body io.Reader, contentType s
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func parseImages(body io.Reader) (*model.PostImage, error) {
|
||||
func parseImages(rctx request.CTX, requestURL string, body io.Reader) (*model.PostImage, error) {
|
||||
// Store any data that is read for the config for any further processing
|
||||
buf := &bytes.Buffer{}
|
||||
t := io.TeeReader(body, buf)
|
||||
@@ -927,12 +926,14 @@ func parseImages(body io.Reader) (*model.PostImage, error) {
|
||||
}
|
||||
|
||||
if format == "jpeg" {
|
||||
if imageOrientation, err := imaging.GetImageOrientation(io.MultiReader(buf, body)); err == nil &&
|
||||
if imageOrientation, err := imaging.GetImageOrientation(io.MultiReader(buf, body), format); err == nil &&
|
||||
(imageOrientation == imaging.RotatedCWMirrored ||
|
||||
imageOrientation == imaging.RotatedCCW ||
|
||||
imageOrientation == imaging.RotatedCCWMirrored ||
|
||||
imageOrientation == imaging.RotatedCW) {
|
||||
image.Width, image.Height = image.Height, image.Width
|
||||
} else if err != nil {
|
||||
rctx.Logger().Warn("Failed to get image orientation", mlog.Err(err), mlog.String("request_url", requestURL))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ import (
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/httpservice"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app/platform"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils/testutils"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/services/imageproxy"
|
||||
@@ -2673,7 +2675,7 @@ func TestParseLinkMetadata(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("image", func(t *testing.T) {
|
||||
og, dimensions, err := th.App.parseLinkMetadata(imageURL, makeImageReader(), "image/png")
|
||||
og, dimensions, err := th.App.parseLinkMetadata(th.Context, imageURL, makeImageReader(), "image/png")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Nil(t, og)
|
||||
@@ -2685,7 +2687,7 @@ func TestParseLinkMetadata(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("image with no content-type given", func(t *testing.T) {
|
||||
og, dimensions, err := th.App.parseLinkMetadata(imageURL, makeImageReader(), "")
|
||||
og, dimensions, err := th.App.parseLinkMetadata(th.Context, imageURL, makeImageReader(), "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Nil(t, og)
|
||||
@@ -2697,7 +2699,7 @@ func TestParseLinkMetadata(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("malformed image", func(t *testing.T) {
|
||||
og, dimensions, err := th.App.parseLinkMetadata(imageURL, makeOpenGraphReader(), "image/png")
|
||||
og, dimensions, err := th.App.parseLinkMetadata(th.Context, imageURL, makeOpenGraphReader(), "image/png")
|
||||
assert.Error(t, err)
|
||||
|
||||
assert.Nil(t, og)
|
||||
@@ -2705,7 +2707,7 @@ func TestParseLinkMetadata(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("opengraph", func(t *testing.T) {
|
||||
og, dimensions, err := th.App.parseLinkMetadata(ogURL, makeOpenGraphReader(), "text/html; charset=utf-8")
|
||||
og, dimensions, err := th.App.parseLinkMetadata(th.Context, ogURL, makeOpenGraphReader(), "text/html; charset=utf-8")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NotNil(t, og)
|
||||
@@ -2716,7 +2718,7 @@ func TestParseLinkMetadata(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("malformed opengraph", func(t *testing.T) {
|
||||
og, dimensions, err := th.App.parseLinkMetadata(ogURL, makeImageReader(), "text/html; charset=utf-8")
|
||||
og, dimensions, err := th.App.parseLinkMetadata(th.Context, ogURL, makeImageReader(), "text/html; charset=utf-8")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Nil(t, og)
|
||||
@@ -2724,7 +2726,7 @@ func TestParseLinkMetadata(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("neither", func(t *testing.T) {
|
||||
og, dimensions, err := th.App.parseLinkMetadata("http://example.com/test.wad", strings.NewReader("garbage"), "application/x-doom")
|
||||
og, dimensions, err := th.App.parseLinkMetadata(th.Context, "http://example.com/test.wad", strings.NewReader("garbage"), "application/x-doom")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Nil(t, og)
|
||||
@@ -2732,7 +2734,7 @@ func TestParseLinkMetadata(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("svg", func(t *testing.T) {
|
||||
og, dimensions, err := th.App.parseLinkMetadata("http://example.com/image.svg", nil, "image/svg+xml")
|
||||
og, dimensions, err := th.App.parseLinkMetadata(th.Context, "http://example.com/image.svg", nil, "image/svg+xml")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Nil(t, og)
|
||||
@@ -2820,6 +2822,14 @@ func TestParseImages(t *testing.T) {
|
||||
Format: "jpeg",
|
||||
},
|
||||
},
|
||||
"jpg-9": {
|
||||
FileName: "orientation_test_9.jpeg",
|
||||
Expected: &model.PostImage{
|
||||
Width: 4000,
|
||||
Height: 2667,
|
||||
Format: "jpeg",
|
||||
},
|
||||
},
|
||||
"animated gif": {
|
||||
FileName: "testgif.gif",
|
||||
Expected: &model.PostImage{
|
||||
@@ -2842,7 +2852,7 @@ func TestParseImages(t *testing.T) {
|
||||
file, err := testutils.ReadTestFile(testCase.FileName)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := parseImages(bytes.NewReader(file))
|
||||
result, err := parseImages(request.EmptyContext(mlog.CreateConsoleTestLogger(t)), "", bytes.NewReader(file))
|
||||
if testCase.ExpectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
|
||||
@@ -1944,16 +1944,16 @@ func (a *App) GetTeamIcon(team *model.Team) ([]byte, *model.AppError) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (a *App) SetTeamIcon(teamID string, imageData *multipart.FileHeader) *model.AppError {
|
||||
func (a *App) SetTeamIcon(rctx request.CTX, teamID string, imageData *multipart.FileHeader) *model.AppError {
|
||||
file, err := imageData.Open()
|
||||
if err != nil {
|
||||
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.open.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
}
|
||||
defer file.Close()
|
||||
return a.SetTeamIconFromMultiPartFile(teamID, file)
|
||||
return a.SetTeamIconFromMultiPartFile(rctx, teamID, file)
|
||||
}
|
||||
|
||||
func (a *App) SetTeamIconFromMultiPartFile(teamID string, file multipart.File) *model.AppError {
|
||||
func (a *App) SetTeamIconFromMultiPartFile(rctx request.CTX, teamID string, file multipart.File) *model.AppError {
|
||||
team, getTeamErr := a.GetTeam(teamID)
|
||||
|
||||
if getTeamErr != nil {
|
||||
@@ -1969,17 +1969,21 @@ func (a *App) SetTeamIconFromMultiPartFile(teamID string, file multipart.File) *
|
||||
nil, "", http.StatusBadRequest).Wrap(limitErr)
|
||||
}
|
||||
|
||||
return a.SetTeamIconFromFile(team, file)
|
||||
return a.SetTeamIconFromFile(rctx, team, file)
|
||||
}
|
||||
|
||||
func (a *App) SetTeamIconFromFile(team *model.Team, file io.Reader) *model.AppError {
|
||||
func (a *App) SetTeamIconFromFile(rctx request.CTX, team *model.Team, file io.ReadSeeker) *model.AppError {
|
||||
// Decode image into Image object
|
||||
img, _, err := image.Decode(file)
|
||||
img, format, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.decode.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
}
|
||||
|
||||
orientation, _ := imaging.GetImageOrientation(file)
|
||||
orientation, err := imaging.GetImageOrientation(file, format)
|
||||
if err != nil {
|
||||
rctx.Logger().Warn("Failed to get image orientation", mlog.Err(err))
|
||||
}
|
||||
|
||||
img = imaging.MakeImageUpright(img, orientation)
|
||||
|
||||
// Scale team icon
|
||||
|
||||
@@ -882,14 +882,18 @@ func (a *App) SetProfileImageFromMultiPartFile(c request.CTX, userID string, fil
|
||||
return a.SetProfileImageFromFile(c, userID, file)
|
||||
}
|
||||
|
||||
func (a *App) AdjustImage(file io.Reader) (*bytes.Buffer, *model.AppError) {
|
||||
func (a *App) AdjustImage(rctx request.CTX, file io.ReadSeeker) (*bytes.Buffer, *model.AppError) {
|
||||
// Decode image into Image object
|
||||
img, _, err := a.ch.imgDecoder.Decode(file)
|
||||
img, format, err := a.ch.imgDecoder.Decode(file)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("SetProfileImage", "api.user.upload_profile_user.decode.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
}
|
||||
|
||||
orientation, _ := imaging.GetImageOrientation(file)
|
||||
orientation, err := imaging.GetImageOrientation(file, format)
|
||||
if err != nil {
|
||||
rctx.Logger().Warn("Failed to get image orientation", mlog.Err(err))
|
||||
}
|
||||
|
||||
img = imaging.MakeImageUpright(img, orientation)
|
||||
|
||||
// Scale profile image
|
||||
@@ -904,8 +908,8 @@ func (a *App) AdjustImage(file io.Reader) (*bytes.Buffer, *model.AppError) {
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func (a *App) SetProfileImageFromFile(c request.CTX, userID string, file io.Reader) *model.AppError {
|
||||
buf, err := a.AdjustImage(file)
|
||||
func (a *App) SetProfileImageFromFile(c request.CTX, userID string, file io.ReadSeeker) *model.AppError {
|
||||
buf, err := a.AdjustImage(c, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1363,7 +1367,6 @@ func (a *App) UpdateUser(c request.CTX, user *model.User, sendNotifications bool
|
||||
|
||||
func (a *App) UpdateUserActive(c request.CTX, userID string, active bool) *model.AppError {
|
||||
user, err := a.GetUser(userID)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1671,7 +1674,6 @@ func (a *App) GetPasswordRecoveryToken(token string) (*model.Token, *model.AppEr
|
||||
|
||||
func (a *App) GetTokenById(token string) (*model.Token, *model.AppError) {
|
||||
rtoken, err := a.Srv().Store().Token().GetByToken(token)
|
||||
|
||||
if err != nil {
|
||||
var status int
|
||||
|
||||
@@ -2043,7 +2045,6 @@ func (a *App) VerifyUserEmail(userID, email string) *model.AppError {
|
||||
a.InvalidateCacheForUser(userID)
|
||||
|
||||
user, err := a.GetUser(userID)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -2788,7 +2789,6 @@ func (a *App) UpdateThreadFollowForUserFromChannelAdd(c request.CTX, userID, tea
|
||||
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventThreadUpdated, teamID, "", userID, nil, "")
|
||||
userThread, err := a.Srv().Store().Thread().GetThreadForUser(tm, true, a.IsPostPriorityEnabled())
|
||||
|
||||
if err != nil {
|
||||
var errNotFound *store.ErrNotFound
|
||||
if errors.As(err, &errNotFound) {
|
||||
|
||||
@@ -112,14 +112,14 @@ func TestAdjustProfileImage(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
_, appErr := th.App.AdjustImage(bytes.NewReader([]byte{}))
|
||||
_, appErr := th.App.AdjustImage(th.Context, bytes.NewReader([]byte{}))
|
||||
require.NotNil(t, appErr)
|
||||
|
||||
// test image isn't the correct dimensions
|
||||
// it should be adjusted
|
||||
testjpg, err := testutils.ReadTestFile("testjpg.jpg")
|
||||
require.NoError(t, err)
|
||||
adjusted, appErr := th.App.AdjustImage(bytes.NewReader(testjpg))
|
||||
adjusted, appErr := th.App.AdjustImage(th.Context, bytes.NewReader(testjpg))
|
||||
require.Nil(t, appErr)
|
||||
assert.True(t, adjusted.Len() > 0)
|
||||
assert.NotEqual(t, testjpg, adjusted)
|
||||
@@ -128,7 +128,7 @@ func TestAdjustProfileImage(t *testing.T) {
|
||||
user := th.BasicUser
|
||||
image, appErr := th.App.GetDefaultProfileImage(user)
|
||||
require.Nil(t, appErr)
|
||||
image2, appErr := th.App.AdjustImage(bytes.NewReader(image))
|
||||
image2, appErr := th.App.AdjustImage(th.Context, bytes.NewReader(image))
|
||||
require.Nil(t, appErr)
|
||||
assert.Equal(t, image, image2.Bytes())
|
||||
}
|
||||
@@ -317,7 +317,8 @@ func TestCreateUser(t *testing.T) {
|
||||
func main() {
|
||||
plugin.ClientMain(&MyPlugin{})
|
||||
}
|
||||
`}, th.App, th.NewPluginAPI)
|
||||
`,
|
||||
}, th.App, th.NewPluginAPI)
|
||||
defer tearDown()
|
||||
|
||||
user := &model.User{
|
||||
@@ -1924,7 +1925,7 @@ func TestUpdateUserRolesWithUser(t *testing.T) {
|
||||
_, err = th.App.UpdateUserRolesWithUser(th.Context, user, "does not exist", false)
|
||||
require.NotNil(t, err)
|
||||
|
||||
//Test reset to User role
|
||||
// Test reset to User role
|
||||
user, err = th.App.UpdateUserRolesWithUser(th.Context, user, model.SystemUserRoleId, false)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, user.Roles, model.SystemUserRoleId)
|
||||
|
||||
@@ -10,6 +10,7 @@ require (
|
||||
github.com/anthonynsimon/bild v0.14.0
|
||||
github.com/avct/uasurfer v0.0.0-20240501094946-ca0c4d1e541b
|
||||
github.com/aws/aws-sdk-go v1.55.6
|
||||
github.com/bep/imagemeta v0.10.0
|
||||
github.com/blang/semver/v4 v4.0.0
|
||||
github.com/blevesearch/bleve/v2 v2.4.4-0.20250115090822-cbafdca08538
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
@@ -59,7 +60,6 @@ require (
|
||||
github.com/reflog/dateconstraints v0.2.1
|
||||
github.com/rs/cors v1.11.1
|
||||
github.com/rudderlabs/analytics-go v3.3.3+incompatible
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/viper v1.19.0
|
||||
@@ -75,7 +75,7 @@ require (
|
||||
golang.org/x/crypto v0.32.0
|
||||
golang.org/x/image v0.23.0
|
||||
golang.org/x/net v0.34.0
|
||||
golang.org/x/sync v0.10.0
|
||||
golang.org/x/sync v0.12.0
|
||||
golang.org/x/term v0.28.0
|
||||
gopkg.in/mail.v2 v2.3.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
@@ -222,7 +222,7 @@ require (
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 // indirect
|
||||
google.golang.org/grpc v1.70.0 // indirect
|
||||
|
||||
@@ -56,6 +56,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bep/imagemeta v0.10.0 h1:kbQxe8SLHTUEEOT/DvLMGslIJy9Z1R+ZAckkIUnUBw4=
|
||||
github.com/bep/imagemeta v0.10.0/go.mod h1:23AF6O+4fUi9avjiydpKLStUNtJr5hJB4rarG18JpN8=
|
||||
github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU=
|
||||
@@ -758,8 +760,9 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -821,8 +824,9 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
|
||||
Двоичные данные
server/tests/exif_samples/down-mirrored.jpg
Обычный файл
|
После Ширина: | Высота: | Размер: 15 KiB |
Двоичные данные
server/tests/exif_samples/down-mirrored.png
Обычный файл
|
После Ширина: | Высота: | Размер: 15 KiB |
Двоичные данные
server/tests/exif_samples/down-mirrored.tiff
Обычный файл
Двоичные данные
server/tests/exif_samples/down-mirrored.webp
Обычный файл
|
После Ширина: | Высота: | Размер: 4.5 KiB |
Двоичные данные
server/tests/exif_samples/down.jpg
Обычный файл
|
После Ширина: | Высота: | Размер: 6.8 KiB |
Двоичные данные
server/tests/exif_samples/down.png
Обычный файл
|
После Ширина: | Высота: | Размер: 7.6 KiB |
Двоичные данные
server/tests/exif_samples/down.tiff
Обычный файл
Двоичные данные
server/tests/exif_samples/down.webp
Обычный файл
|
После Ширина: | Высота: | Размер: 2.3 KiB |
Двоичные данные
server/tests/exif_samples/left-mirrored.jpg
Обычный файл
|
После Ширина: | Высота: | Размер: 13 KiB |
Двоичные данные
server/tests/exif_samples/left-mirrored.png
Обычный файл
|
После Ширина: | Высота: | Размер: 12 KiB |
Двоичные данные
server/tests/exif_samples/left-mirrored.tiff
Обычный файл
Двоичные данные
server/tests/exif_samples/left-mirrored.webp
Обычный файл
|
После Ширина: | Высота: | Размер: 3.3 KiB |
Двоичные данные
server/tests/exif_samples/left.jpg
Обычный файл
|
После Ширина: | Высота: | Размер: 3.8 KiB |
Двоичные данные
server/tests/exif_samples/left.png
Обычный файл
|
После Ширина: | Высота: | Размер: 3.6 KiB |
Двоичные данные
server/tests/exif_samples/left.tiff
Обычный файл
Двоичные данные
server/tests/exif_samples/left.webp
Обычный файл
|
После Ширина: | Высота: | Размер: 1.2 KiB |
Двоичные данные
server/tests/exif_samples/right-mirrored.jpg
Обычный файл
|
После Ширина: | Высота: | Размер: 15 KiB |
Двоичные данные
server/tests/exif_samples/right-mirrored.png
Обычный файл
|
После Ширина: | Высота: | Размер: 15 KiB |
Двоичные данные
server/tests/exif_samples/right-mirrored.tiff
Обычный файл
Двоичные данные
server/tests/exif_samples/right-mirrored.webp
Обычный файл
|
После Ширина: | Высота: | Размер: 3.8 KiB |
Двоичные данные
server/tests/exif_samples/right.jpg
Обычный файл
|
После Ширина: | Высота: | Размер: 5.8 KiB |
Двоичные данные
server/tests/exif_samples/right.png
Обычный файл
|
После Ширина: | Высота: | Размер: 6.1 KiB |
Двоичные данные
server/tests/exif_samples/right.tiff
Обычный файл
Двоичные данные
server/tests/exif_samples/right.webp
Обычный файл
|
После Ширина: | Высота: | Размер: 1.8 KiB |
Двоичные данные
server/tests/exif_samples/up-mirrored.jpg
Обычный файл
|
После Ширина: | Высота: | Размер: 12 KiB |
Двоичные данные
server/tests/exif_samples/up-mirrored.png
Обычный файл
|
После Ширина: | Высота: | Размер: 11 KiB |
Двоичные данные
server/tests/exif_samples/up-mirrored.tiff
Обычный файл
Двоичные данные
server/tests/exif_samples/up-mirrored.webp
Обычный файл
|
После Ширина: | Высота: | Размер: 3.4 KiB |
Двоичные данные
server/tests/exif_samples/up.jpg
Обычный файл
|
После Ширина: | Высота: | Размер: 3.2 KiB |
Двоичные данные
server/tests/exif_samples/up.png
Обычный файл
|
После Ширина: | Высота: | Размер: 3.9 KiB |
Двоичные данные
server/tests/exif_samples/up.tiff
Обычный файл
Двоичные данные
server/tests/exif_samples/up.webp
Обычный файл
|
После Ширина: | Высота: | Размер: 1.2 KiB |
Двоичные данные
server/tests/orientation_test_9.jpeg
Обычный файл
|
После Ширина: | Высота: | Размер: 6.8 MiB |
Двоичные данные
server/tests/orientation_test_9_expected_preview.jpeg
Обычный файл
|
После Ширина: | Высота: | Размер: 688 KiB |
Двоичные данные
server/tests/orientation_test_9_expected_thumb.jpeg
Обычный файл
|
После Ширина: | Высота: | Размер: 5.1 KiB |