Commit 993b28db authored by Harrison Healey's avatar Harrison Healey

MM-14030 Add format and frame count to image metadata (#10757)

* MM-14030 Add initial CountFrames

* MM-14030 Add format and frame count to image metadata

* Fix tests and stop using image dimensions from OpenGraph

* Fix copyright header

* Move license to NOTICE.txt
parent f68493c0
......@@ -8,6 +8,45 @@ This document includes a list of open source components used in Mattermost Serve
-----
## Go
This product uses the Go programming language by the Go authors.
* HOMEPAGE:
* https://golang.org
* LICENSE: BSD-style
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---
## Masterminds/squirrel
This product contains 'squirrel' by GitHub user "Masterminds".
......
......@@ -4,6 +4,7 @@
package app
import (
"bytes"
"image"
"io"
"net/http"
......@@ -15,6 +16,7 @@ import (
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils"
"github.com/mattermost/mattermost-server/utils/imgutils"
"github.com/mattermost/mattermost-server/utils/markdown"
)
......@@ -180,16 +182,7 @@ func (a *App) getImagesForPost(post *model.Post, imageURLs []string, isNewPost b
continue
}
if image.Width != 0 || image.Height != 0 {
// The site has already told us the image dimensions
images[imageURL] = &model.PostImage{
Width: int(image.Width),
Height: int(image.Height),
}
} else {
// The site did not specify its image dimensions
imageURLs = append(imageURLs, imageURL)
}
imageURLs = append(imageURLs, imageURL)
}
}
}
......@@ -498,7 +491,12 @@ func (a *App) parseLinkMetadata(requestURL string, body io.Reader, contentType s
}
func parseImages(body io.Reader) (*model.PostImage, error) {
config, _, err := image.DecodeConfig(body)
// Store any data that is read for the config for any further processing
buf := &bytes.Buffer{}
t := io.TeeReader(body, buf)
// Read the image config to get the format and dimensions
config, format, err := image.DecodeConfig(t)
if err != nil {
return nil, err
}
......@@ -506,6 +504,17 @@ func parseImages(body io.Reader) (*model.PostImage, error) {
image := &model.PostImage{
Width: config.Width,
Height: config.Height,
Format: format,
}
if format == "gif" {
// Decoding the config may have read some of the image data, so re-read the data that has already been read first
frameCount, err := imgutils.CountFrames(io.MultiReader(buf, body))
if err != nil {
return nil, err
}
image.FrameCount = frameCount
}
return image, nil
......
......@@ -257,10 +257,12 @@ func TestPreparePostForClient(t *testing.T) {
imageDimensions := clientPost.Metadata.Images
require.Len(t, imageDimensions, 2)
assert.Equal(t, &model.PostImage{
Format: "png",
Width: 1068,
Height: 552,
}, imageDimensions["https://github.com/hmhealey/test-files/raw/master/logoVertical.png"])
assert.Equal(t, &model.PostImage{
Format: "png",
Width: 501,
Height: 501,
}, imageDimensions["https://github.com/hmhealey/test-files/raw/master/icon.png"])
......@@ -310,6 +312,7 @@ func TestPreparePostForClient(t *testing.T) {
imageDimensions := clientPost.Metadata.Images
require.Len(t, imageDimensions, 1)
assert.Equal(t, &model.PostImage{
Format: "png",
Width: 1068,
Height: 552,
}, imageDimensions["https://github.com/hmhealey/test-files/raw/master/logoVertical.png"])
......@@ -354,6 +357,7 @@ func TestPreparePostForClient(t *testing.T) {
imageDimensions := clientPost.Metadata.Images
require.Len(t, imageDimensions, 1)
assert.Equal(t, &model.PostImage{
Format: "png",
Width: 420,
Height: 420,
}, imageDimensions["https://avatars1.githubusercontent.com/u/3277310?s=400&v=4"])
......@@ -391,6 +395,7 @@ func TestPreparePostForClient(t *testing.T) {
imageDimensions := clientPost.Metadata.Images
require.Len(t, imageDimensions, 1)
assert.Equal(t, &model.PostImage{
Format: "png",
Width: 501,
Height: 501,
}, imageDimensions["https://github.com/hmhealey/test-files/raw/master/icon.png"])
......@@ -643,6 +648,7 @@ func TestGetImagesForPost(t *testing.T) {
assert.Equal(t, images, map[string]*model.PostImage{
imageURL: {
Format: "png",
Width: 408,
Height: 336,
},
......@@ -671,48 +677,7 @@ func TestGetImagesForPost(t *testing.T) {
assert.Equal(t, images, map[string]*model.PostImage{})
})
t.Run("for an OpenGraph image with dimensions", func(t *testing.T) {
th := Setup(t)
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
})
ogURL := "https://example.com/index.html"
imageURL := "https://example.com/image.png"
post := &model.Post{
Metadata: &model.PostMetadata{
Embeds: []*model.PostEmbed{
{
Type: model.POST_EMBED_OPENGRAPH,
URL: ogURL,
Data: &opengraph.OpenGraph{
Images: []*opengraph.Image{
{
URL: imageURL,
Width: 100,
Height: 200,
},
},
},
},
},
},
}
images := th.App.getImagesForPost(post, []string{}, false)
assert.Equal(t, images, map[string]*model.PostImage{
imageURL: {
Width: 100,
Height: 200,
},
})
})
t.Run("for an OpenGraph image without dimensions", func(t *testing.T) {
t.Run("for an OpenGraph image", func(t *testing.T) {
th := Setup(t)
defer th.TearDown()
......@@ -759,13 +724,14 @@ func TestGetImagesForPost(t *testing.T) {
assert.Equal(t, images, map[string]*model.PostImage{
imageURL: {
Format: "png",
Width: 200,
Height: 300,
},
})
})
t.Run("with an OpenGraph image with a secure_url and dimensions", func(t *testing.T) {
t.Run("with an OpenGraph image with a secure_url", func(t *testing.T) {
th := Setup(t)
defer th.TearDown()
......@@ -773,8 +739,22 @@ func TestGetImagesForPost(t *testing.T) {
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
})
ogURL := "https://example.com/index.html"
imageURL := "https://example.com/secure_image.png"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/secure_image.png" {
w.Header().Set("Content-Type", "image/png")
img := image.NewGray(image.Rect(0, 0, 300, 400))
var encoder png.Encoder
encoder.Encode(w, img)
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
ogURL := server.URL + "/index.html"
imageURL := server.URL + "/secure_image.png"
post := &model.Post{
Metadata: &model.PostMetadata{
......@@ -785,9 +765,7 @@ func TestGetImagesForPost(t *testing.T) {
Data: &opengraph.OpenGraph{
Images: []*opengraph.Image{
{
URL: imageURL,
Width: 300,
Height: 400,
SecureURL: imageURL,
},
},
},
......@@ -800,6 +778,7 @@ func TestGetImagesForPost(t *testing.T) {
assert.Equal(t, images, map[string]*model.PostImage{
imageURL: {
Format: "png",
Width: 300,
Height: 400,
},
......@@ -853,6 +832,7 @@ func TestGetImagesForPost(t *testing.T) {
assert.Equal(t, images, map[string]*model.PostImage{
imageURL: {
Format: "png",
Width: 400,
Height: 500,
},
......@@ -1948,6 +1928,7 @@ func TestParseLinkMetadata(t *testing.T) {
assert.Nil(t, og)
assert.Equal(t, &model.PostImage{
Format: "png",
Width: 408,
Height: 336,
}, dimensions)
......@@ -1991,20 +1972,34 @@ func TestParseLinkMetadata(t *testing.T) {
func TestParseImages(t *testing.T) {
for name, testCase := range map[string]struct {
FileName string
ExpectedWidth int
ExpectedHeight int
ExpectError bool
FileName string
Expected *model.PostImage
ExpectError bool
}{
"png": {
FileName: "test.png",
ExpectedWidth: 408,
ExpectedHeight: 336,
FileName: "test.png",
Expected: &model.PostImage{
Width: 408,
Height: 336,
Format: "png",
},
},
"animated gif": {
FileName: "testgif.gif",
ExpectedWidth: 118,
ExpectedHeight: 118,
FileName: "testgif.gif",
Expected: &model.PostImage{
Width: 118,
Height: 118,
Format: "gif",
FrameCount: 4,
},
},
"tiff": {
FileName: "test.tiff",
Expected: &model.PostImage{
Width: 701,
Height: 701,
Format: "tiff",
},
},
"not an image": {
FileName: "README.md",
......@@ -2015,15 +2010,12 @@ func TestParseImages(t *testing.T) {
file, err := testutils.ReadTestFile(testCase.FileName)
require.Nil(t, err)
dimensions, err := parseImages(bytes.NewReader(file))
result, err := parseImages(bytes.NewReader(file))
if testCase.ExpectError {
require.NotNil(t, err)
assert.NotNil(t, err)
} else {
require.Nil(t, err)
require.NotNil(t, dimensions)
require.Equal(t, testCase.ExpectedWidth, dimensions.Width)
require.Equal(t, testCase.ExpectedHeight, dimensions.Height)
assert.Nil(t, err)
assert.Equal(t, testCase.Expected, result)
}
})
}
......
......@@ -31,6 +31,12 @@ type PostMetadata struct {
type PostImage struct {
Width int `json:"width"`
Height int `json:"height"`
// Format is the name of the image format as used by image/go such as "png", "gif", or "jpeg".
Format string `json:"format"`
// FrameCount stores the number of frames in this image, if it is an animated gif. It will be 0 for other formats.
FrameCount int `json:"frame_count"`
}
func (o *PostImage) ToJson() string {
......
......@@ -5,7 +5,7 @@ count=0
for fileType in GoFiles; do
for file in `go list -f $'{{range .GoFiles}}{{$.Dir}}/{{.}}\n{{end}}' "$@"`; do
case $file in
*/utils/lru.go|*/store/storetest/mocks/*|*/services/*/mocks/*|*/app/plugin/jira/plugin_*|*/plugin/plugintest/*|*/app/plugin/zoom/plugin_*)
*/utils/lru.go|*/utils/imgutils/gif.go|*/store/storetest/mocks/*|*/services/*/mocks/*|*/app/plugin/jira/plugin_*|*/plugin/plugintest/*|*/app/plugin/zoom/plugin_*)
# Third-party, doesn't require a header.
;;
*)
......
This diff is collapsed.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package imgutils
import (
"bytes"
"testing"
"github.com/mattermost/mattermost-server/utils/testutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCountFrames(t *testing.T) {
header := []byte{
'G', 'I', 'F', '8', '9', 'a', // header
1, 0, 1, 0, // width and height of 1 by 1
128, 0, 0, // other header information
0, 0, 0, 1, 1, 1, // color table
}
frame := []byte{
0x2c, // block introducer
0, 0, 0, 0, 1, 0, 1, 0, // position and dimensions of the frame
0, // other frame information
0x2, 0x2, 0x4c, 0x1, 0, // encoded pixel data
}
trailer := []byte{0x3b}
t.Run("should count the frames of a static gif", func(t *testing.T) {
var b []byte
b = append(b, header...)
b = append(b, frame...)
b = append(b, trailer...)
count, err := CountFrames(bytes.NewReader(b))
assert.Nil(t, err)
assert.Equal(t, 1, count)
})
t.Run("should count the frames of an animated gif", func(t *testing.T) {
var b []byte
b = append(b, header...)
for i := 0; i < 100; i++ {
b = append(b, frame...)
}
b = append(b, trailer...)
count, err := CountFrames(bytes.NewReader(b))
assert.Nil(t, err)
assert.Equal(t, 100, count)
})
t.Run("should count the frames of an actual animated gif", func(t *testing.T) {
b, err := testutils.ReadTestFile("testgif.gif")
require.Nil(t, err)
count, err := CountFrames(bytes.NewReader(b))
assert.Nil(t, err)
assert.Equal(t, 4, count)
})
t.Run("should return an error for a non-gif image", func(t *testing.T) {
b, err := testutils.ReadTestFile("test.png")
require.Nil(t, err)
_, err = CountFrames(bytes.NewReader(b))
assert.NotNil(t, err)
})
t.Run("should return an error for garbage data", func(t *testing.T) {
_, err := CountFrames(bytes.NewReader([]byte("garbage data")))
assert.NotNil(t, err)
})
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment