feat: filebrowser with encfs
This commit is contained in:
244
img/service.go
Normal file
244
img/service.go
Normal file
@@ -0,0 +1,244 @@
|
||||
//go:generate go-enum --sql --marshal --file $GOFILE
|
||||
package img
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/dsoprea/go-exif/v3"
|
||||
"github.com/marusama/semaphore/v2"
|
||||
|
||||
exifcommon "github.com/dsoprea/go-exif/v3/common"
|
||||
)
|
||||
|
||||
// ErrUnsupportedFormat means the given image format is not supported.
|
||||
var ErrUnsupportedFormat = errors.New("unsupported image format")
|
||||
|
||||
// Service
|
||||
type Service struct {
|
||||
sem semaphore.Semaphore
|
||||
}
|
||||
|
||||
func New(workers int) *Service {
|
||||
return &Service{
|
||||
sem: semaphore.New(workers),
|
||||
}
|
||||
}
|
||||
|
||||
// Format is an image file format.
|
||||
/*
|
||||
ENUM(
|
||||
jpeg
|
||||
png
|
||||
gif
|
||||
tiff
|
||||
bmp
|
||||
)
|
||||
*/
|
||||
type Format int
|
||||
|
||||
func (x Format) toImaging() imaging.Format {
|
||||
switch x {
|
||||
case FormatJpeg:
|
||||
return imaging.JPEG
|
||||
case FormatPng:
|
||||
return imaging.PNG
|
||||
case FormatGif:
|
||||
return imaging.GIF
|
||||
case FormatTiff:
|
||||
return imaging.TIFF
|
||||
case FormatBmp:
|
||||
return imaging.BMP
|
||||
default:
|
||||
return imaging.JPEG
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
ENUM(
|
||||
high
|
||||
medium
|
||||
low
|
||||
)
|
||||
*/
|
||||
type Quality int
|
||||
|
||||
func (x Quality) resampleFilter() imaging.ResampleFilter {
|
||||
switch x {
|
||||
case QualityHigh:
|
||||
return imaging.Lanczos
|
||||
case QualityMedium:
|
||||
return imaging.Box
|
||||
case QualityLow:
|
||||
return imaging.NearestNeighbor
|
||||
default:
|
||||
return imaging.Box
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
ENUM(
|
||||
fit
|
||||
fill
|
||||
)
|
||||
*/
|
||||
type ResizeMode int
|
||||
|
||||
func (s *Service) FormatFromExtension(ext string) (Format, error) {
|
||||
format, err := imaging.FormatFromExtension(ext)
|
||||
if err != nil {
|
||||
return -1, ErrUnsupportedFormat
|
||||
}
|
||||
switch format {
|
||||
case imaging.JPEG:
|
||||
return FormatJpeg, nil
|
||||
case imaging.PNG:
|
||||
return FormatPng, nil
|
||||
case imaging.GIF:
|
||||
return FormatGif, nil
|
||||
case imaging.TIFF:
|
||||
return FormatTiff, nil
|
||||
case imaging.BMP:
|
||||
return FormatBmp, nil
|
||||
}
|
||||
return -1, ErrUnsupportedFormat
|
||||
}
|
||||
|
||||
type resizeConfig struct {
|
||||
format Format
|
||||
resizeMode ResizeMode
|
||||
quality Quality
|
||||
}
|
||||
|
||||
type Option func(*resizeConfig)
|
||||
|
||||
func WithFormat(format Format) Option {
|
||||
return func(config *resizeConfig) {
|
||||
config.format = format
|
||||
}
|
||||
}
|
||||
|
||||
func WithMode(mode ResizeMode) Option {
|
||||
return func(config *resizeConfig) {
|
||||
config.resizeMode = mode
|
||||
}
|
||||
}
|
||||
|
||||
func WithQuality(quality Quality) Option {
|
||||
return func(config *resizeConfig) {
|
||||
config.quality = quality
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Resize(ctx context.Context, in io.Reader, width, height int, out io.Writer, options ...Option) error {
|
||||
if err := s.sem.Acquire(ctx, 1); err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.sem.Release(1)
|
||||
|
||||
format, wrappedReader, err := s.detectFormat(in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := resizeConfig{
|
||||
format: format,
|
||||
resizeMode: ResizeModeFit,
|
||||
quality: QualityMedium,
|
||||
}
|
||||
for _, option := range options {
|
||||
option(&config)
|
||||
}
|
||||
|
||||
if config.quality == QualityLow && format == FormatJpeg {
|
||||
thm, newWrappedReader, errThm := getEmbeddedThumbnail(wrappedReader)
|
||||
wrappedReader = newWrappedReader
|
||||
if errThm == nil {
|
||||
_, err = out.Write(thm)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img, err := imaging.Decode(wrappedReader, imaging.AutoOrientation(true))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch config.resizeMode {
|
||||
case ResizeModeFill:
|
||||
img = imaging.Fill(img, width, height, imaging.Center, config.quality.resampleFilter())
|
||||
case ResizeModeFit:
|
||||
fallthrough //nolint:gocritic
|
||||
default:
|
||||
img = imaging.Fit(img, width, height, config.quality.resampleFilter())
|
||||
}
|
||||
|
||||
return imaging.Encode(out, img, config.format.toImaging())
|
||||
}
|
||||
|
||||
func (s *Service) detectFormat(in io.Reader) (Format, io.Reader, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
r := io.TeeReader(in, buf)
|
||||
|
||||
_, imgFormat, err := image.DecodeConfig(r)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("%s: %w", err.Error(), ErrUnsupportedFormat)
|
||||
}
|
||||
|
||||
format, err := ParseFormat(imgFormat)
|
||||
if err != nil {
|
||||
return 0, nil, ErrUnsupportedFormat
|
||||
}
|
||||
|
||||
return format, io.MultiReader(buf, in), nil
|
||||
}
|
||||
|
||||
func getEmbeddedThumbnail(in io.Reader) ([]byte, io.Reader, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
r := io.TeeReader(in, buf)
|
||||
wrappedReader := io.MultiReader(buf, in)
|
||||
|
||||
offset := 0
|
||||
offsets := []int{12, 30}
|
||||
head := make([]byte, 0xffff) //nolint:gomnd
|
||||
|
||||
_, err := r.Read(head)
|
||||
if err != nil {
|
||||
return nil, wrappedReader, err
|
||||
}
|
||||
|
||||
for _, offset = range offsets {
|
||||
if _, err = exif.ParseExifHeader(head[offset:]); err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, wrappedReader, err
|
||||
}
|
||||
|
||||
im, err := exifcommon.NewIfdMappingWithStandard()
|
||||
if err != nil {
|
||||
return nil, wrappedReader, err
|
||||
}
|
||||
|
||||
_, index, err := exif.Collect(im, exif.NewTagIndex(), head[offset:])
|
||||
if err != nil {
|
||||
return nil, wrappedReader, err
|
||||
}
|
||||
|
||||
ifd := index.RootIfd.NextIfd()
|
||||
if ifd == nil {
|
||||
return nil, wrappedReader, exif.ErrNoThumbnail
|
||||
}
|
||||
|
||||
thm, err := ifd.Thumbnail()
|
||||
return thm, wrappedReader, err
|
||||
}
|
||||
259
img/service_enum.go
Normal file
259
img/service_enum.go
Normal file
@@ -0,0 +1,259 @@
|
||||
// Code generated by go-enum
|
||||
// DO NOT EDIT!
|
||||
|
||||
package img
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
// FormatJpeg is a Format of type Jpeg
|
||||
FormatJpeg Format = iota
|
||||
// FormatPng is a Format of type Png
|
||||
FormatPng
|
||||
// FormatGif is a Format of type Gif
|
||||
FormatGif
|
||||
// FormatTiff is a Format of type Tiff
|
||||
FormatTiff
|
||||
// FormatBmp is a Format of type Bmp
|
||||
FormatBmp
|
||||
)
|
||||
|
||||
const _FormatName = "jpegpnggiftiffbmp"
|
||||
|
||||
var _FormatMap = map[Format]string{
|
||||
0: _FormatName[0:4],
|
||||
1: _FormatName[4:7],
|
||||
2: _FormatName[7:10],
|
||||
3: _FormatName[10:14],
|
||||
4: _FormatName[14:17],
|
||||
}
|
||||
|
||||
// String implements the Stringer interface.
|
||||
func (x Format) String() string {
|
||||
if str, ok := _FormatMap[x]; ok {
|
||||
return str
|
||||
}
|
||||
return fmt.Sprintf("Format(%d)", x)
|
||||
}
|
||||
|
||||
var _FormatValue = map[string]Format{
|
||||
_FormatName[0:4]: 0,
|
||||
_FormatName[4:7]: 1,
|
||||
_FormatName[7:10]: 2,
|
||||
_FormatName[10:14]: 3,
|
||||
_FormatName[14:17]: 4,
|
||||
}
|
||||
|
||||
// ParseFormat attempts to convert a string to a Format
|
||||
func ParseFormat(name string) (Format, error) {
|
||||
if x, ok := _FormatValue[name]; ok {
|
||||
return x, nil
|
||||
}
|
||||
return Format(0), fmt.Errorf("%s is not a valid Format", name)
|
||||
}
|
||||
|
||||
// MarshalText implements the text marshaller method
|
||||
func (x Format) MarshalText() ([]byte, error) {
|
||||
return []byte(x.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the text unmarshaller method
|
||||
func (x *Format) UnmarshalText(text []byte) error {
|
||||
name := string(text)
|
||||
tmp, err := ParseFormat(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*x = tmp
|
||||
return nil
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (x *Format) Scan(value interface{}) error {
|
||||
var name string
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
name = v
|
||||
case []byte:
|
||||
name = string(v)
|
||||
case nil:
|
||||
*x = Format(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
tmp, err := ParseFormat(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*x = tmp
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (x Format) Value() (driver.Value, error) {
|
||||
return x.String(), nil
|
||||
}
|
||||
|
||||
const (
|
||||
// QualityHigh is a Quality of type High
|
||||
QualityHigh Quality = iota
|
||||
// QualityMedium is a Quality of type Medium
|
||||
QualityMedium
|
||||
// QualityLow is a Quality of type Low
|
||||
QualityLow
|
||||
)
|
||||
|
||||
const _QualityName = "highmediumlow"
|
||||
|
||||
var _QualityMap = map[Quality]string{
|
||||
0: _QualityName[0:4],
|
||||
1: _QualityName[4:10],
|
||||
2: _QualityName[10:13],
|
||||
}
|
||||
|
||||
// String implements the Stringer interface.
|
||||
func (x Quality) String() string {
|
||||
if str, ok := _QualityMap[x]; ok {
|
||||
return str
|
||||
}
|
||||
return fmt.Sprintf("Quality(%d)", x)
|
||||
}
|
||||
|
||||
var _QualityValue = map[string]Quality{
|
||||
_QualityName[0:4]: 0,
|
||||
_QualityName[4:10]: 1,
|
||||
_QualityName[10:13]: 2,
|
||||
}
|
||||
|
||||
// ParseQuality attempts to convert a string to a Quality
|
||||
func ParseQuality(name string) (Quality, error) {
|
||||
if x, ok := _QualityValue[name]; ok {
|
||||
return x, nil
|
||||
}
|
||||
return Quality(0), fmt.Errorf("%s is not a valid Quality", name)
|
||||
}
|
||||
|
||||
// MarshalText implements the text marshaller method
|
||||
func (x Quality) MarshalText() ([]byte, error) {
|
||||
return []byte(x.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the text unmarshaller method
|
||||
func (x *Quality) UnmarshalText(text []byte) error {
|
||||
name := string(text)
|
||||
tmp, err := ParseQuality(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*x = tmp
|
||||
return nil
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (x *Quality) Scan(value interface{}) error {
|
||||
var name string
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
name = v
|
||||
case []byte:
|
||||
name = string(v)
|
||||
case nil:
|
||||
*x = Quality(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
tmp, err := ParseQuality(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*x = tmp
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (x Quality) Value() (driver.Value, error) {
|
||||
return x.String(), nil
|
||||
}
|
||||
|
||||
const (
|
||||
// ResizeModeFit is a ResizeMode of type Fit
|
||||
ResizeModeFit ResizeMode = iota
|
||||
// ResizeModeFill is a ResizeMode of type Fill
|
||||
ResizeModeFill
|
||||
)
|
||||
|
||||
const _ResizeModeName = "fitfill"
|
||||
|
||||
var _ResizeModeMap = map[ResizeMode]string{
|
||||
0: _ResizeModeName[0:3],
|
||||
1: _ResizeModeName[3:7],
|
||||
}
|
||||
|
||||
// String implements the Stringer interface.
|
||||
func (x ResizeMode) String() string {
|
||||
if str, ok := _ResizeModeMap[x]; ok {
|
||||
return str
|
||||
}
|
||||
return fmt.Sprintf("ResizeMode(%d)", x)
|
||||
}
|
||||
|
||||
var _ResizeModeValue = map[string]ResizeMode{
|
||||
_ResizeModeName[0:3]: 0,
|
||||
_ResizeModeName[3:7]: 1,
|
||||
}
|
||||
|
||||
// ParseResizeMode attempts to convert a string to a ResizeMode
|
||||
func ParseResizeMode(name string) (ResizeMode, error) {
|
||||
if x, ok := _ResizeModeValue[name]; ok {
|
||||
return x, nil
|
||||
}
|
||||
return ResizeMode(0), fmt.Errorf("%s is not a valid ResizeMode", name)
|
||||
}
|
||||
|
||||
// MarshalText implements the text marshaller method
|
||||
func (x ResizeMode) MarshalText() ([]byte, error) {
|
||||
return []byte(x.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the text unmarshaller method
|
||||
func (x *ResizeMode) UnmarshalText(text []byte) error {
|
||||
name := string(text)
|
||||
tmp, err := ParseResizeMode(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*x = tmp
|
||||
return nil
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (x *ResizeMode) Scan(value interface{}) error {
|
||||
var name string
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
name = v
|
||||
case []byte:
|
||||
name = string(v)
|
||||
case nil:
|
||||
*x = ResizeMode(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
tmp, err := ParseResizeMode(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*x = tmp
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (x ResizeMode) Value() (driver.Value, error) {
|
||||
return x.String(), nil
|
||||
}
|
||||
446
img/service_test.go
Normal file
446
img/service_test.go
Normal file
@@ -0,0 +1,446 @@
|
||||
package img
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"image"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/image/bmp"
|
||||
"golang.org/x/image/tiff"
|
||||
)
|
||||
|
||||
func TestService_Resize(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
options []Option
|
||||
width int
|
||||
height int
|
||||
source func(t *testing.T) afero.File
|
||||
matcher func(t *testing.T, reader io.Reader)
|
||||
wantErr bool
|
||||
}{
|
||||
"fill upscale": {
|
||||
options: []Option{WithMode(ResizeModeFill)},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return newGrayJpeg(t, 50, 20)
|
||||
},
|
||||
matcher: sizeMatcher(100, 100),
|
||||
},
|
||||
"fill downscale": {
|
||||
options: []Option{WithMode(ResizeModeFill)},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return newGrayJpeg(t, 200, 150)
|
||||
},
|
||||
matcher: sizeMatcher(100, 100),
|
||||
},
|
||||
"fit upscale": {
|
||||
options: []Option{WithMode(ResizeModeFit)},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return newGrayJpeg(t, 50, 20)
|
||||
},
|
||||
matcher: sizeMatcher(50, 20),
|
||||
},
|
||||
"fit downscale": {
|
||||
options: []Option{WithMode(ResizeModeFit)},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return newGrayJpeg(t, 200, 150)
|
||||
},
|
||||
matcher: sizeMatcher(100, 75),
|
||||
},
|
||||
"keep original format": {
|
||||
options: []Option{},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return newGrayPng(t, 200, 150)
|
||||
},
|
||||
matcher: formatMatcher(FormatPng),
|
||||
},
|
||||
"convert to jpeg": {
|
||||
options: []Option{WithFormat(FormatJpeg)},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return newGrayJpeg(t, 200, 150)
|
||||
},
|
||||
matcher: formatMatcher(FormatJpeg),
|
||||
},
|
||||
"convert to png": {
|
||||
options: []Option{WithFormat(FormatPng)},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return newGrayJpeg(t, 200, 150)
|
||||
},
|
||||
matcher: formatMatcher(FormatPng),
|
||||
},
|
||||
"convert to gif": {
|
||||
options: []Option{WithFormat(FormatGif)},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return newGrayJpeg(t, 200, 150)
|
||||
},
|
||||
matcher: formatMatcher(FormatGif),
|
||||
},
|
||||
"convert to tiff": {
|
||||
options: []Option{WithFormat(FormatTiff)},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return newGrayJpeg(t, 200, 150)
|
||||
},
|
||||
matcher: formatMatcher(FormatTiff),
|
||||
},
|
||||
"convert to bmp": {
|
||||
options: []Option{WithFormat(FormatBmp)},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return newGrayJpeg(t, 200, 150)
|
||||
},
|
||||
matcher: formatMatcher(FormatBmp),
|
||||
},
|
||||
"convert to unknown": {
|
||||
options: []Option{WithFormat(Format(-1))},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return newGrayJpeg(t, 200, 150)
|
||||
},
|
||||
matcher: formatMatcher(FormatJpeg),
|
||||
},
|
||||
"resize png": {
|
||||
options: []Option{WithMode(ResizeModeFill)},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return newGrayPng(t, 200, 150)
|
||||
},
|
||||
matcher: sizeMatcher(100, 100),
|
||||
},
|
||||
"resize gif": {
|
||||
options: []Option{WithMode(ResizeModeFill)},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return newGrayGif(t, 200, 150)
|
||||
},
|
||||
matcher: sizeMatcher(100, 100),
|
||||
},
|
||||
"resize tiff": {
|
||||
options: []Option{WithMode(ResizeModeFill)},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return newGrayTiff(t, 200, 150)
|
||||
},
|
||||
matcher: sizeMatcher(100, 100),
|
||||
},
|
||||
"resize bmp": {
|
||||
options: []Option{WithMode(ResizeModeFill)},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return newGrayBmp(t, 200, 150)
|
||||
},
|
||||
matcher: sizeMatcher(100, 100),
|
||||
},
|
||||
"resize with high quality": {
|
||||
options: []Option{WithMode(ResizeModeFill), WithQuality(QualityHigh)},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return newGrayJpeg(t, 200, 150)
|
||||
},
|
||||
matcher: sizeMatcher(100, 100),
|
||||
},
|
||||
"resize with medium quality": {
|
||||
options: []Option{WithMode(ResizeModeFill), WithQuality(QualityMedium)},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return newGrayJpeg(t, 200, 150)
|
||||
},
|
||||
matcher: sizeMatcher(100, 100),
|
||||
},
|
||||
"resize with low quality": {
|
||||
options: []Option{WithMode(ResizeModeFill), WithQuality(QualityLow)},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return newGrayJpeg(t, 200, 150)
|
||||
},
|
||||
matcher: sizeMatcher(100, 100),
|
||||
},
|
||||
"resize with unknown quality": {
|
||||
options: []Option{WithMode(ResizeModeFill), WithQuality(Quality(-1))},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return newGrayJpeg(t, 200, 150)
|
||||
},
|
||||
matcher: sizeMatcher(100, 100),
|
||||
},
|
||||
"get thumbnail from file with APP0 JFIF": {
|
||||
options: []Option{WithMode(ResizeModeFill), WithQuality(QualityLow)},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return openFile(t, "testdata/gray-sample.jpg")
|
||||
},
|
||||
matcher: sizeMatcher(125, 128),
|
||||
},
|
||||
"get thumbnail from file without APP0 JFIF": {
|
||||
options: []Option{WithMode(ResizeModeFill), WithQuality(QualityLow)},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return openFile(t, "testdata/20130612_142406.jpg")
|
||||
},
|
||||
matcher: sizeMatcher(320, 240),
|
||||
},
|
||||
"resize from file without IFD1 thumbnail": {
|
||||
options: []Option{WithMode(ResizeModeFill), WithQuality(QualityLow)},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return openFile(t, "testdata/IMG_2578.JPG")
|
||||
},
|
||||
matcher: sizeMatcher(100, 100),
|
||||
},
|
||||
"resize for higher quality levels": {
|
||||
options: []Option{WithMode(ResizeModeFill), WithQuality(QualityMedium)},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return openFile(t, "testdata/gray-sample.jpg")
|
||||
},
|
||||
matcher: sizeMatcher(100, 100),
|
||||
},
|
||||
"broken file": {
|
||||
options: []Option{WithMode(ResizeModeFit)},
|
||||
width: 100,
|
||||
height: 100,
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
fs := afero.NewMemMapFs()
|
||||
file, err := fs.Create("image.jpg")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = file.WriteString("this is not an image")
|
||||
require.NoError(t, err)
|
||||
|
||||
return file
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
svc := New(1)
|
||||
source := test.source(t)
|
||||
defer source.Close()
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
err := svc.Resize(context.Background(), source, test.width, test.height, buf, test.options...)
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Fatalf("GetMarketSpecs() error = %v, wantErr %v", err, test.wantErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
test.matcher(t, buf)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func sizeMatcher(width, height int) func(t *testing.T, reader io.Reader) {
|
||||
return func(t *testing.T, reader io.Reader) {
|
||||
resizedImg, _, err := image.Decode(reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, width, resizedImg.Bounds().Dx())
|
||||
require.Equal(t, height, resizedImg.Bounds().Dy())
|
||||
}
|
||||
}
|
||||
|
||||
func formatMatcher(format Format) func(t *testing.T, reader io.Reader) {
|
||||
return func(t *testing.T, reader io.Reader) {
|
||||
_, decodedFormat, err := image.DecodeConfig(reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, format.String(), decodedFormat)
|
||||
}
|
||||
}
|
||||
|
||||
func newGrayJpeg(t *testing.T, width, height int) afero.File {
|
||||
fs := afero.NewMemMapFs()
|
||||
file, err := fs.Create("image.jpg")
|
||||
require.NoError(t, err)
|
||||
|
||||
img := image.NewGray(image.Rect(0, 0, width, height))
|
||||
err = jpeg.Encode(file, img, &jpeg.Options{Quality: 90})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = file.Seek(0, io.SeekStart)
|
||||
require.NoError(t, err)
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
func newGrayPng(t *testing.T, width, height int) afero.File {
|
||||
fs := afero.NewMemMapFs()
|
||||
file, err := fs.Create("image.png")
|
||||
require.NoError(t, err)
|
||||
|
||||
img := image.NewGray(image.Rect(0, 0, width, height))
|
||||
err = png.Encode(file, img)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = file.Seek(0, io.SeekStart)
|
||||
require.NoError(t, err)
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
func newGrayGif(t *testing.T, width, height int) afero.File {
|
||||
fs := afero.NewMemMapFs()
|
||||
file, err := fs.Create("image.gif")
|
||||
require.NoError(t, err)
|
||||
|
||||
img := image.NewGray(image.Rect(0, 0, width, height))
|
||||
err = gif.Encode(file, img, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = file.Seek(0, io.SeekStart)
|
||||
require.NoError(t, err)
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
func newGrayTiff(t *testing.T, width, height int) afero.File {
|
||||
fs := afero.NewMemMapFs()
|
||||
file, err := fs.Create("image.tiff")
|
||||
require.NoError(t, err)
|
||||
|
||||
img := image.NewGray(image.Rect(0, 0, width, height))
|
||||
err = tiff.Encode(file, img, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = file.Seek(0, io.SeekStart)
|
||||
require.NoError(t, err)
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
func newGrayBmp(t *testing.T, width, height int) afero.File {
|
||||
fs := afero.NewMemMapFs()
|
||||
file, err := fs.Create("image.bmp")
|
||||
require.NoError(t, err)
|
||||
|
||||
img := image.NewGray(image.Rect(0, 0, width, height))
|
||||
err = bmp.Encode(file, img)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = file.Seek(0, io.SeekStart)
|
||||
require.NoError(t, err)
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
func openFile(t *testing.T, name string) afero.File {
|
||||
appfs := afero.NewOsFs()
|
||||
file, err := appfs.Open(name)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
func TestService_FormatFromExtension(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
ext string
|
||||
want Format
|
||||
wantErr error
|
||||
}{
|
||||
"jpg": {
|
||||
ext: ".jpg",
|
||||
want: FormatJpeg,
|
||||
},
|
||||
"jpeg": {
|
||||
ext: ".jpeg",
|
||||
want: FormatJpeg,
|
||||
},
|
||||
"png": {
|
||||
ext: ".png",
|
||||
want: FormatPng,
|
||||
},
|
||||
"gif": {
|
||||
ext: ".gif",
|
||||
want: FormatGif,
|
||||
},
|
||||
"tiff": {
|
||||
ext: ".tiff",
|
||||
want: FormatTiff,
|
||||
},
|
||||
"bmp": {
|
||||
ext: ".bmp",
|
||||
want: FormatBmp,
|
||||
},
|
||||
"unknown": {
|
||||
ext: ".mov",
|
||||
wantErr: ErrUnsupportedFormat,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
svc := New(1)
|
||||
got, err := svc.FormatFromExtension(test.ext)
|
||||
require.ErrorIsf(t, err, test.wantErr, "error = %v, wantErr %v", err, test.wantErr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
require.Equal(t, test.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
BIN
img/testdata/20130612_142406.jpg
vendored
Normal file
BIN
img/testdata/20130612_142406.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
BIN
img/testdata/IMG_2578.JPG
vendored
Normal file
BIN
img/testdata/IMG_2578.JPG
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
img/testdata/gray-sample.jpg
vendored
Normal file
BIN
img/testdata/gray-sample.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Reference in New Issue
Block a user