[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
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
6ffd31b6bd
Коммит
39c3b8ebf9
146
app/imaging/decode.go
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
|
||||
}
|
||||
58
app/imaging/svg.go
Обычный файл
58
app/imaging/svg.go
Обычный файл
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package imaging
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// SVGInfo holds information for a SVG image.
|
||||
type SVGInfo struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
// ParseSVG returns information for the given SVG input data.
|
||||
func ParseSVG(svgReader io.Reader) (SVGInfo, error) {
|
||||
var parsedSVG struct {
|
||||
Width string `xml:"width,attr,omitempty"`
|
||||
Height string `xml:"height,attr,omitempty"`
|
||||
ViewBox string `xml:"viewBox,attr,omitempty"`
|
||||
}
|
||||
svgInfo := SVGInfo{
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
}
|
||||
viewBoxPattern := regexp.MustCompile("^([0-9]+)[, ]+([0-9]+)[, ]+([0-9]+)[, ]+([0-9]+)$")
|
||||
dimensionPattern := regexp.MustCompile("(?i)^([0-9]+)(?:px)?$")
|
||||
|
||||
// decode provided SVG
|
||||
if err := xml.NewDecoder(svgReader).Decode(&parsedSVG); err != nil {
|
||||
return svgInfo, err
|
||||
}
|
||||
|
||||
// prefer viewbox for SVG dimensions over width/height
|
||||
if viewBoxMatches := viewBoxPattern.FindStringSubmatch(parsedSVG.ViewBox); len(viewBoxMatches) == 5 {
|
||||
svgInfo.Width, _ = strconv.Atoi(viewBoxMatches[3])
|
||||
svgInfo.Height, _ = strconv.Atoi(viewBoxMatches[4])
|
||||
} else if parsedSVG.Width != "" && parsedSVG.Height != "" {
|
||||
widthMatches := dimensionPattern.FindStringSubmatch(parsedSVG.Width)
|
||||
heightMatches := dimensionPattern.FindStringSubmatch(parsedSVG.Height)
|
||||
if len(widthMatches) == 2 && len(heightMatches) == 2 {
|
||||
svgInfo.Width, _ = strconv.Atoi(widthMatches[1])
|
||||
svgInfo.Height, _ = strconv.Atoi(heightMatches[1])
|
||||
}
|
||||
}
|
||||
|
||||
// if width and/or height are still zero, create new error
|
||||
if svgInfo.Width == 0 || svgInfo.Height == 0 {
|
||||
return svgInfo, errors.New("unable to extract SVG dimensions")
|
||||
}
|
||||
return svgInfo, nil
|
||||
}
|
||||
76
app/imaging/svg_test.go
Обычный файл
76
app/imaging/svg_test.go
Обычный файл
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package imaging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
//nolint:unparam
|
||||
func generateSVGData(width int, height int, useViewBox bool, useDimensions bool, useAlternateFormat bool) io.Reader {
|
||||
var (
|
||||
viewBoxAttribute = ""
|
||||
widthAttribute = ""
|
||||
heightAttribute = ""
|
||||
)
|
||||
if useViewBox == true {
|
||||
separator := " "
|
||||
if useAlternateFormat == true {
|
||||
separator = ", "
|
||||
}
|
||||
viewBoxAttribute = fmt.Sprintf(` viewBox="0%s0%s%d%s%d"`, separator, separator, width, separator, height)
|
||||
}
|
||||
if useDimensions == true && width > 0 && height > 0 {
|
||||
units := ""
|
||||
if useAlternateFormat == true {
|
||||
units = "px"
|
||||
}
|
||||
widthAttribute = fmt.Sprintf(` width="%d%s"`, width, units)
|
||||
heightAttribute = fmt.Sprintf(` height="%d%s"`, height, units)
|
||||
}
|
||||
svgString := fmt.Sprintf(`<svg%s%s%s></svg>`, widthAttribute, heightAttribute, viewBoxAttribute)
|
||||
return strings.NewReader(svgString)
|
||||
}
|
||||
|
||||
func TestParseValidSVGData(t *testing.T) {
|
||||
var width, height int = 300, 300
|
||||
validSVGs := []io.Reader{
|
||||
generateSVGData(width, height, true, true, false), // properly formed viewBox, width & height
|
||||
generateSVGData(width, height, true, true, true), // properly formed viewBox, width & height; alternate format
|
||||
generateSVGData(width, height, false, true, false), // missing viewBox, properly formed width & height
|
||||
generateSVGData(width, height, false, true, true), // missing viewBox, properly formed width & height; alternate format
|
||||
}
|
||||
for index, svg := range validSVGs {
|
||||
svgInfo, err := ParseSVG(svg)
|
||||
if err != nil {
|
||||
t.Errorf("Should be able to parse SVG attributes at index %d, but was not able to: err = %v", index, err)
|
||||
} else {
|
||||
if svgInfo.Width != width {
|
||||
t.Errorf("Expecting a width of %d for SVG at index %d, but it was %d instead.", width, index, svgInfo.Width)
|
||||
}
|
||||
|
||||
if svgInfo.Height != height {
|
||||
t.Errorf("Expecting a height of %d for SVG at index %d, but it was %d instead.", height, index, svgInfo.Height)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInvalidSVGData(t *testing.T) {
|
||||
var width, height int = 300, 300
|
||||
invalidSVGs := []io.Reader{
|
||||
generateSVGData(width, height, false, false, false), // missing viewBox, width & height
|
||||
generateSVGData(width, 0, false, true, false), // missing viewBox, malformed width & height
|
||||
generateSVGData(width, 0, false, true, false), // missing viewBox, malformed height, properly formed width
|
||||
}
|
||||
for index, svg := range invalidSVGs {
|
||||
_, err := ParseSVG(svg)
|
||||
if err == nil {
|
||||
t.Errorf("Should not be able to parse SVG attributes at index %d, but was definitely able to!", index)
|
||||
}
|
||||
}
|
||||
}
|
||||
147
app/imaging/utils.go
Обычный файл
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
Обычный файл
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
Обычный файл
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)
|
||||
})
|
||||
}
|
||||
Ссылка в новой задаче
Block a user