diff --git a/previewer/application/query_test.go b/previewer/application/query_test.go new file mode 100644 index 0000000..d7a14ef --- /dev/null +++ b/previewer/application/query_test.go @@ -0,0 +1,57 @@ +package application + +import ( + "github.com/stretchr/testify/require" + "net/url" + "testing" +) + +func TestBuildQuery(t *testing.T) { + + urlParcer := func(u string) *url.URL { + res, _ := url.Parse(u) + return res + } + + table := []struct { + url *url.URL + expWidth int + expHeight int + expURL *url.URL + err bool + msg string + }{ + { + url: urlParcer("/fill/10/10/domain.me/some/pic.jpg"), expWidth: 10, expHeight: 10, expURL: urlParcer("http://domain.me/some/pic.jpg"), err: false, msg: "Normal request", + }, + { + url: urlParcer("/fill/10/10/pic.jpg"), expWidth: 10, expHeight: 10, expURL: urlParcer("http://pic.jpg"), err: false, msg: "Short URL", + }, + { + url: urlParcer("/fill/10"), expWidth: 0, expHeight: 0, expURL: nil, err: true, msg: "Only width", + }, + { + url: urlParcer("/fill/10/10"), expWidth: 0, expHeight: 0, expURL: nil, err: true, msg: "Only dimensions", + }, + { + url: urlParcer("/fill/qwew/qwew/domain.me/some/pic.jpg"), expWidth: 0, expHeight: 0, expURL: nil, err: true, msg: "Strings in dimensions", + }, + { + url: urlParcer("/fill/domain.me/some/pic.jpg"), expWidth: 0, expHeight: 0, expURL: nil, err: true, msg: "No dimensions", + }, + } + + for _, dat := range table { + t.Run(dat.msg, func(t *testing.T) { + i := false + query, err := buildQuery(dat.url) + if err != nil { + i = true + } + require.Equal(t, dat.err, i, dat.msg) + require.Equal(t, dat.expWidth, query.Width, dat.msg) + require.Equal(t, dat.expHeight, query.Height, dat.msg) + require.Equal(t, dat.expURL, query.URL, dat.msg) + }) + } +} diff --git a/previewer/converter/converter.go b/previewer/converter/converter.go index 7fad1bb..9d0d184 100644 --- a/previewer/converter/converter.go +++ b/previewer/converter/converter.go @@ -9,12 +9,14 @@ import ( "image/jpeg" "image/png" "net/http" + "sync" "github.com/nfnt/resize" ) type Image struct { image.Image + mx sync.Mutex } func SelectType(width int, height int, b []byte) ([]byte, error) { @@ -64,13 +66,17 @@ func SelectType(width int, height int, b []byte) ([]byte, error) { if err != nil { return nil, err } - m := Image{i} + m := NewImage(i) if err = m.convert(width, height); err != nil { return nil, err } return encode(m.Image) } +func NewImage(img image.Image) Image { + return Image{img, sync.Mutex{}} +} + func (img *Image) convert(width int, height int) error { widthOrig := img.Bounds().Max.X heightOrig := img.Bounds().Max.Y @@ -79,19 +85,22 @@ func (img *Image) convert(width int, height int) error { switch { case sfOriginal > sfNew: - // Ресайз по одной высоте и кроп по ширине следом - // Определение ширины кропа. calcWidth := int(float64(height) * sfOriginal) - img.resize(calcWidth, height) + if err := img.resize(calcWidth, height); err != nil { + return err + } if err := img.crop(image.Point{X: (calcWidth - width) / 2, Y: 0}, image.Point{X: (calcWidth-width)/2 + width, Y: height}); err != nil { return err } case sfOriginal == sfNew: - img.resize(width, height) + if err := img.resize(width, height); err != nil { + return err + } case sfOriginal < sfNew: - // Ресайз по одной ширине и кроп по высоте следом calcHeight := int(float64(width) / sfOriginal) - img.resize(width, calcHeight) + if err := img.resize(width, calcHeight); err != nil { + return err + } if err := img.crop(image.Point{X: 0, Y: (calcHeight - height) / 2}, image.Point{X: width, Y: (calcHeight-height)/2 + height}); err != nil { return err } @@ -99,15 +108,24 @@ func (img *Image) convert(width int, height int) error { return nil } -func (img *Image) resize(width, height int) { - img.Image = resize.Resize(uint(width), uint(height), img, resize.Bicubic) +func (img *Image) resize(width, height int) error { + img.mx.Lock() + defer img.mx.Unlock() + if width <= 0 || height <= 0 { + return errors.New("can't resize to zero or negative value") + } + tmpImg := resize.Resize(uint(width), uint(height), img, resize.Bicubic) + img.Image = tmpImg + return nil } func (img *Image) crop(p1 image.Point, p2 image.Point) error { - if img == nil { + img.mx.Lock() + defer img.mx.Unlock() + if img.Image == nil { return errors.New("corrupted image") } - if p1.X < 0 || p1.Y < 0 || p2.X < 0 || p2.Y < 0 { + if p1.X < 0 || p1.Y < 0 || p2.X > img.Image.Bounds().Max.X || p2.Y > img.Image.Bounds().Max.Y { return errors.New("not valid corner points") } b := image.Rect(0, 0, p2.X-p1.X, p2.Y-p1.Y) diff --git a/previewer/converter/converter_test.go b/previewer/converter/converter_test.go new file mode 100644 index 0000000..f0e335c --- /dev/null +++ b/previewer/converter/converter_test.go @@ -0,0 +1,159 @@ +package converter + +import ( + "github.com/stretchr/testify/require" + "image" + "image/color" + "testing" +) + +func TestResize(t *testing.T) { + table := []struct { + width int + height int + expectedX int + expectedY int + err bool + msg string + }{ + { + width: 300, height: 200, expectedX: 300, expectedY: 200, err: false, msg: "Reducing the image size", + }, + { + width: 1600, height: 1200, expectedX: 1600, expectedY: 1200, err: false, msg: "Increasing the image size", + }, + { + width: 0, height: 0, expectedX: 800, expectedY: 600, err: true, msg: "Resize to zero", + }, + { + width: -1000, height: -1200, expectedX: 800, expectedY: 600, err: true, msg: "Use negative values", + }, + } + + for _, dat := range table { + t.Run(dat.msg, func(t *testing.T) { + img := Image{Image: createImage(800, 600)} + i := false + err := img.resize(dat.width, dat.height) + if err != nil { + i = true + } + require.Equal(t, dat.err, i, dat.msg) + require.Equal(t, dat.expectedX, img.Bounds().Max.X, dat.msg) + require.Equal(t, dat.expectedY, img.Bounds().Max.Y, dat.msg) + }) + } +} + +func TestCrop(t *testing.T) { + table := []struct { + topLeft image.Point + bottomRight image.Point + expectedX int + expectedY int + err bool + msg string + }{ + { + topLeft: image.Point{X: 400, Y: 0}, bottomRight: image.Point{X: 600, Y: 1000}, expectedX: 200, expectedY: 1000, err: false, msg: "Vertical crop", + }, + { + topLeft: image.Point{X: 0, Y: 400}, bottomRight: image.Point{X: 1000, Y: 600}, expectedX: 1000, expectedY: 200, err: false, msg: "Horizontal crop", + }, + { + topLeft: image.Point{X: 100, Y: 100}, bottomRight: image.Point{X: 900, Y: 900}, expectedX: 800, expectedY: 800, err: false, msg: "Square crop", + }, + { + topLeft: image.Point{X: -100, Y: 0}, bottomRight: image.Point{X: 2000, Y: 1000}, expectedX: 1000, expectedY: 1000, err: true, msg: "Too wide crop with negative offset", + }, + { + topLeft: image.Point{X: 0, Y: -100}, bottomRight: image.Point{X: 1000, Y: 2000}, expectedX: 1000, expectedY: 1000, err: true, msg: "Too tall crop with negative offset", + }, + { + topLeft: image.Point{X: 100, Y: 0}, bottomRight: image.Point{X: 2000, Y: 1000}, expectedX: 1000, expectedY: 1000, err: true, msg: "Too wide crop with positive offset", + }, + { + topLeft: image.Point{X: 0, Y: 100}, bottomRight: image.Point{X: 1000, Y: 2000}, expectedX: 1000, expectedY: 1000, err: true, msg: "Too tall crop with positive offset", + }, + } + + for _, dat := range table { + t.Run(dat.msg, func(t *testing.T) { + img := Image{Image: createImage(1000, 1000)} + i := false + err := img.crop(dat.topLeft, dat.bottomRight) + if err != nil { + i = true + } + require.Equal(t, dat.err, i, dat.msg) + require.Equal(t, dat.expectedX, img.Image.Bounds().Max.X, dat.msg) + require.Equal(t, dat.expectedY, img.Image.Bounds().Max.Y, dat.msg) + }) + } +} + +/* +func TestConvert(t *testing.T) { + originalAspect := 800.0 / 600.0 + releasedValue := 3000 + table := []struct { + width int + height int + expectedX int + expectedY int + err bool + msg string + }{ + { + width: 400, height: 600, expectedX: 400, expectedY: int(400 / originalAspect), err: false, msg: "Reducing the image size by horizontal", + }, + { + width: 800, height: 400, expectedX: int(400 * originalAspect), expectedY: 400, err: false, msg: "Reducing the image size by vertical", + }, + { + width: 400, height: int(400 / originalAspect), expectedX: 400, expectedY: int(400 / originalAspect), err: false, msg: "Resize to original aspect ratio", + }, + { + width: 1000, height: releasedValue, expectedX: 1000, expectedY: int(1000 / originalAspect), err: false, msg: "Increasing the image size by horizontal", + }, + { + width: releasedValue, height: 1000, expectedX: int(1000 * originalAspect), expectedY: 1000, err: false, msg: "Increasing the image size by vertical", + }, + { + width: 0, height: 0, expectedX: 800, expectedY: 600, err: true, msg: "Resize to zero", + }, + { + width: -1000, height: -1200, expectedX: 800, expectedY: 600, err: true, msg: "Use negative values", + }, + } + + for _, dat := range table { + t.Run(dat.msg, func(t *testing.T) { + img := Image{Image: createImage(800, 600)} + i := false + err := img.convert(dat.width, dat.height) + if err != nil { + i = true + } + require.Equal(t, dat.err, i, dat.msg) + require.Equal(t, dat.expectedX, img.Image.Bounds().Max.X, dat.msg) + require.Equal(t, dat.expectedY, img.Image.Bounds().Max.Y, dat.msg) + }) + } +} +*/ + +func createImage(w, h int) image.Image { + res := image.NewRGBA(image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: w, Y: h}}) + for x := 0; x < w; x++ { + for y := 0; y < h; y++ { + switch { + case x%10 == 0 || y%10 == 00: // upper left quadrant + res.Set(x, y, color.Black) + default: + res.Set(x, y, color.White) + } + } + } + return res +}