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
Этот коммит содержится в:
Harrison Healey
2019-04-30 16:45:26 -04:00
коммит произвёл GitHub
родитель 9a9d5d4081
Коммит e50b642e43
7 изменённых файлов: 712 добавлений и 77 удалений

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

@@ -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.
;;
*)

509
utils/imgutils/gif.go Обычный файл
Просмотреть файл

@@ -0,0 +1,509 @@
// Copyright (c) 2011 The Go Authors.
// Modified work: Copyright (c) 2019 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package imgutils
// This contains a portion of Go's image/go library, modified to count the number of frames in a gif without loading
// the entire image into memory.
import (
"bufio"
"compress/lzw"
"errors"
"fmt"
"image"
"image/color"
"io"
)
var (
errNotEnough = errors.New("gif: not enough image data")
errTooMuch = errors.New("gif: too much image data")
)
// If the io.Reader does not also have ReadByte, then decode will introduce its own buffering.
type reader interface {
io.Reader
io.ByteReader
}
// Masks etc.
const (
// Fields.
fColorTable = 1 << 7
fInterlace = 1 << 6
fColorTableBitsMask = 7
// Graphic control flags.
gcTransparentColorSet = 1 << 0
gcDisposalMethodMask = 7 << 2
)
// Disposal Methods.
const (
DisposalNone = 0x01
DisposalBackground = 0x02
DisposalPrevious = 0x03
)
// Section indicators.
const (
sExtension = 0x21
sImageDescriptor = 0x2C
sTrailer = 0x3B
)
// Extensions.
const (
eText = 0x01 // Plain Text
eGraphicControl = 0xF9 // Graphic Control
eComment = 0xFE // Comment
eApplication = 0xFF // Application
)
func readFull(r io.Reader, b []byte) error {
_, err := io.ReadFull(r, b)
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
return err
}
func readByte(r io.ByteReader) (byte, error) {
b, err := r.ReadByte()
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
return b, err
}
// decoder is the type used to decode a GIF file.
type decoder struct {
r reader
// From header.
vers string
width int
height int
loopCount int
delayTime int
backgroundIndex byte
disposalMethod byte
// From image descriptor.
imageFields byte
// From graphics control.
transparentIndex byte
hasTransparentIndex bool
// Computed.
globalColorTable color.Palette
// Used when decoding.
imageCount int
tmp [1024]byte // must be at least 768 so we can read color table
}
// blockReader parses the block structure of GIF image data, which comprises
// (n, (n bytes)) blocks, with 1 <= n <= 255. It is the reader given to the
// LZW decoder, which is thus immune to the blocking. After the LZW decoder
// completes, there will be a 0-byte block remaining (0, ()), which is
// consumed when checking that the blockReader is exhausted.
//
// To avoid the allocation of a bufio.Reader for the lzw Reader, blockReader
// implements io.ReadByte and buffers blocks into the decoder's "tmp" buffer.
type blockReader struct {
d *decoder
i, j uint8 // d.tmp[i:j] contains the buffered bytes
err error
}
func (b *blockReader) fill() {
if b.err != nil {
return
}
b.j, b.err = readByte(b.d.r)
if b.j == 0 && b.err == nil {
b.err = io.EOF
}
if b.err != nil {
return
}
b.i = 0
b.err = readFull(b.d.r, b.d.tmp[:b.j])
if b.err != nil {
b.j = 0
}
}
func (b *blockReader) ReadByte() (byte, error) {
if b.i == b.j {
b.fill()
if b.err != nil {
return 0, b.err
}
}
c := b.d.tmp[b.i]
b.i++
return c, nil
}
// blockReader must implement io.Reader, but its Read shouldn't ever actually
// be called in practice. The compress/lzw package will only call ReadByte.
func (b *blockReader) Read(p []byte) (int, error) {
if len(p) == 0 || b.err != nil {
return 0, b.err
}
if b.i == b.j {
b.fill()
if b.err != nil {
return 0, b.err
}
}
n := copy(p, b.d.tmp[b.i:b.j])
b.i += uint8(n)
return n, nil
}
// close primarily detects whether or not a block terminator was encountered
// after reading a sequence of data sub-blocks. It allows at most one trailing
// sub-block worth of data. I.e., if some number of bytes exist in one sub-block
// following the end of LZW data, the very next sub-block must be the block
// terminator. If the very end of LZW data happened to fill one sub-block, at
// most one more sub-block of length 1 may exist before the block-terminator.
// These accommodations allow us to support GIFs created by less strict encoders.
// See https://golang.org/issue/16146.
func (b *blockReader) close() error {
if b.err == io.EOF {
// A clean block-sequence terminator was encountered while reading.
return nil
} else if b.err != nil {
// Some other error was encountered while reading.
return b.err
}
if b.i == b.j {
// We reached the end of a sub block reading LZW data. We'll allow at
// most one more sub block of data with a length of 1 byte.
b.fill()
if b.err == io.EOF {
return nil
} else if b.err != nil {
return b.err
} else if b.j > 1 {
return errTooMuch
}
}
// Part of a sub-block remains buffered. We expect that the next attempt to
// buffer a sub-block will reach the block terminator.
b.fill()
if b.err == io.EOF {
return nil
} else if b.err != nil {
return b.err
}
return errTooMuch
}
// decode reads a GIF image from r and stores the result in d.
func (d *decoder) decode(r io.Reader, configOnly, keepAllFrames bool) error {
// Add buffering if r does not provide ReadByte.
if rr, ok := r.(reader); ok {
d.r = rr
} else {
d.r = bufio.NewReader(r)
}
d.loopCount = -1
err := d.readHeaderAndScreenDescriptor()
if err != nil {
return err
}
if configOnly {
return nil
}
for {
c, err := readByte(d.r)
if err != nil {
return fmt.Errorf("gif: reading frames: %v", err)
}
switch c {
case sExtension:
if err = d.readExtension(); err != nil {
return err
}
case sImageDescriptor:
if err = d.readImageDescriptor(keepAllFrames); err != nil {
return err
}
case sTrailer:
if d.imageCount == 0 {
return fmt.Errorf("gif: missing image data")
}
return nil
default:
return fmt.Errorf("gif: unknown block type: 0x%.2x", c)
}
}
}
func (d *decoder) readHeaderAndScreenDescriptor() error {
err := readFull(d.r, d.tmp[:13])
if err != nil {
return fmt.Errorf("gif: reading header: %v", err)
}
d.vers = string(d.tmp[:6])
if d.vers != "GIF87a" && d.vers != "GIF89a" {
return fmt.Errorf("gif: can't recognize format %q", d.vers)
}
d.width = int(d.tmp[6]) + int(d.tmp[7])<<8
d.height = int(d.tmp[8]) + int(d.tmp[9])<<8
if fields := d.tmp[10]; fields&fColorTable != 0 {
d.backgroundIndex = d.tmp[11]
// readColorTable overwrites the contents of d.tmp, but that's OK.
if d.globalColorTable, err = d.readColorTable(fields); err != nil {
return err
}
}
// d.tmp[12] is the Pixel Aspect Ratio, which is ignored.
return nil
}
func (d *decoder) readColorTable(fields byte) (color.Palette, error) {
n := 1 << (1 + uint(fields&fColorTableBitsMask))
err := readFull(d.r, d.tmp[:3*n])
if err != nil {
return nil, fmt.Errorf("gif: reading color table: %s", err)
}
j, p := 0, make(color.Palette, n)
for i := range p {
p[i] = color.RGBA{d.tmp[j+0], d.tmp[j+1], d.tmp[j+2], 0xFF}
j += 3
}
return p, nil
}
func (d *decoder) readExtension() error {
extension, err := readByte(d.r)
if err != nil {
return fmt.Errorf("gif: reading extension: %v", err)
}
size := 0
switch extension {
case eText:
size = 13
case eGraphicControl:
return d.readGraphicControl()
case eComment:
// nothing to do but read the data.
case eApplication:
b, err := readByte(d.r)
if err != nil {
return fmt.Errorf("gif: reading extension: %v", err)
}
// The spec requires size be 11, but Adobe sometimes uses 10.
size = int(b)
default:
return fmt.Errorf("gif: unknown extension 0x%.2x", extension)
}
if size > 0 {
if err := readFull(d.r, d.tmp[:size]); err != nil {
return fmt.Errorf("gif: reading extension: %v", err)
}
}
// Application Extension with "NETSCAPE2.0" as string and 1 in data means
// this extension defines a loop count.
if extension == eApplication && string(d.tmp[:size]) == "NETSCAPE2.0" {
n, err := d.readBlock()
if err != nil {
return fmt.Errorf("gif: reading extension: %v", err)
}
if n == 0 {
return nil
}
if n == 3 && d.tmp[0] == 1 {
d.loopCount = int(d.tmp[1]) | int(d.tmp[2])<<8
}
}
for {
n, err := d.readBlock()
if err != nil {
return fmt.Errorf("gif: reading extension: %v", err)
}
if n == 0 {
return nil
}
}
}
func (d *decoder) readGraphicControl() error {
if err := readFull(d.r, d.tmp[:6]); err != nil {
return fmt.Errorf("gif: can't read graphic control: %s", err)
}
if d.tmp[0] != 4 {
return fmt.Errorf("gif: invalid graphic control extension block size: %d", d.tmp[0])
}
flags := d.tmp[1]
d.disposalMethod = (flags & gcDisposalMethodMask) >> 2
d.delayTime = int(d.tmp[2]) | int(d.tmp[3])<<8
if flags&gcTransparentColorSet != 0 {
d.transparentIndex = d.tmp[4]
d.hasTransparentIndex = true
}
if d.tmp[5] != 0 {
return fmt.Errorf("gif: invalid graphic control extension block terminator: %d", d.tmp[5])
}
return nil
}
func (d *decoder) readImageDescriptor(keepAllFrames bool) error {
m, err := d.newImageFromDescriptor()
if err != nil {
return err
}
useLocalColorTable := d.imageFields&fColorTable != 0
if useLocalColorTable {
m.Palette, err = d.readColorTable(d.imageFields)
if err != nil {
return err
}
} else {
if d.globalColorTable == nil {
return errors.New("gif: no color table")
}
m.Palette = d.globalColorTable
}
if d.hasTransparentIndex {
if !useLocalColorTable {
// Clone the global color table.
m.Palette = append(color.Palette(nil), d.globalColorTable...)
}
if ti := int(d.transparentIndex); ti < len(m.Palette) {
m.Palette[ti] = color.RGBA{}
} else {
// The transparentIndex is out of range, which is an error
// according to the spec, but Firefox and Google Chrome
// seem OK with this, so we enlarge the palette with
// transparent colors. See golang.org/issue/15059.
p := make(color.Palette, ti+1)
copy(p, m.Palette)
for i := len(m.Palette); i < len(p); i++ {
p[i] = color.RGBA{}
}
m.Palette = p
}
}
litWidth, err := readByte(d.r)
if err != nil {
return fmt.Errorf("gif: reading image data: %v", err)
}
if litWidth < 2 || litWidth > 8 {
return fmt.Errorf("gif: pixel size in decode out of range: %d", litWidth)
}
// A wonderfully Go-like piece of magic.
br := &blockReader{d: d}
lzwr := lzw.NewReader(br, lzw.LSB, int(litWidth))
defer lzwr.Close()
if err = readFull(lzwr, m.Pix); err != nil {
if err != io.ErrUnexpectedEOF {
return fmt.Errorf("gif: reading image data: %v", err)
}
return errNotEnough
}
// In theory, both lzwr and br should be exhausted. Reading from them
// should yield (0, io.EOF).
//
// The spec (Appendix F - Compression), says that "An End of
// Information code... must be the last code output by the encoder
// for an image". In practice, though, giflib (a widely used C
// library) does not enforce this, so we also accept lzwr returning
// io.ErrUnexpectedEOF (meaning that the encoded stream hit io.EOF
// before the LZW decoder saw an explicit end code), provided that
// the io.ReadFull call above successfully read len(m.Pix) bytes.
// See https://golang.org/issue/9856 for an example GIF.
if n, err := lzwr.Read(d.tmp[256:257]); n != 0 || (err != io.EOF && err != io.ErrUnexpectedEOF) {
if err != nil {
return fmt.Errorf("gif: reading image data: %v", err)
}
return errTooMuch
}
// In practice, some GIFs have an extra byte in the data sub-block
// stream, which we ignore. See https://golang.org/issue/16146.
if err := br.close(); err == errTooMuch {
return errTooMuch
} else if err != nil {
return fmt.Errorf("gif: reading image data: %v", err)
}
d.imageCount += 1
return nil
}
func (d *decoder) newImageFromDescriptor() (*image.Paletted, error) {
if err := readFull(d.r, d.tmp[:9]); err != nil {
return nil, fmt.Errorf("gif: can't read image descriptor: %s", err)
}
left := int(d.tmp[0]) + int(d.tmp[1])<<8
top := int(d.tmp[2]) + int(d.tmp[3])<<8
width := int(d.tmp[4]) + int(d.tmp[5])<<8
height := int(d.tmp[6]) + int(d.tmp[7])<<8
d.imageFields = d.tmp[8]
// The GIF89a spec, Section 20 (Image Descriptor) says: "Each image must
// fit within the boundaries of the Logical Screen, as defined in the
// Logical Screen Descriptor."
//
// This is conceptually similar to testing
// frameBounds := image.Rect(left, top, left+width, top+height)
// imageBounds := image.Rect(0, 0, d.width, d.height)
// if !frameBounds.In(imageBounds) { etc }
// but the semantics of the Go image.Rectangle type is that r.In(s) is true
// whenever r is an empty rectangle, even if r.Min.X > s.Max.X. Here, we
// want something stricter.
//
// Note that, by construction, left >= 0 && top >= 0, so we only have to
// explicitly compare frameBounds.Max (left+width, top+height) against
// imageBounds.Max (d.width, d.height) and not frameBounds.Min (left, top)
// against imageBounds.Min (0, 0).
if left+width > d.width || top+height > d.height {
return nil, errors.New("gif: frame bounds larger than image bounds")
}
return image.NewPaletted(image.Rectangle{
Min: image.Point{left, top},
Max: image.Point{left + width, top + height},
}, nil), nil
}
func (d *decoder) readBlock() (int, error) {
n, err := readByte(d.r)
if n == 0 || err != nil {
return 0, err
}
if err := readFull(d.r, d.tmp[:n]); err != nil {
return 0, err
}
return int(n), nil
}
func CountFrames(r io.Reader) (int, error) {
var d decoder
if err := d.decode(r, false, true); err != nil {
return -1, err
}
return d.imageCount, nil
}

80
utils/imgutils/gif_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,80 @@
// 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)
})
}