diff --git a/carver.go b/carver.go index 04aa10e7..629850e5 100644 --- a/carver.go +++ b/carver.go @@ -7,6 +7,7 @@ import ( "image/draw" "math" + cimage "github.com/esimov/caire/image" "github.com/esimov/caire/utils" pigo "github.com/esimov/pigo/core" ) @@ -66,7 +67,7 @@ func (c *Carver) set(x, y int, px float64) { // with the minimum pixel value of the neighboring pixels from the previous row. func (c *Carver) ComputeSeams(p *Processor, img *image.NRGBA) (*image.NRGBA, error) { width, height := img.Bounds().Dx(), img.Bounds().Dy() - sobel := c.SobelDetector(img, float64(p.SobelThreshold)) + sobel := cimage.SobelDetector(img, float64(p.SobelThreshold)) dets := []pigo.Detection{} @@ -81,7 +82,7 @@ func (c *Carver) ComputeSeams(p *Processor, img *image.NRGBA) (*image.NRGBA, err minSize := float64(utils.Min(width, height)) * ratio / 3 // Transform the image to pixel array. - pixels := rgbToGrayscale(img) + pixels := cimage.RgbToGrayscale(img) cParams := pigo.CascadeParams{ MinSize: int(minSize), @@ -206,7 +207,7 @@ func (c *Carver) ComputeSeams(p *Processor, img *image.NRGBA) (*image.NRGBA, err var srcImg *image.NRGBA if p.BlurRadius > 0 { srcImg = image.NewNRGBA(img.Bounds()) - err := Stackblur(srcImg, sobel, uint32(p.BlurRadius)) + err := cimage.Stackblur(srcImg, sobel, uint32(p.BlurRadius)) if err != nil { return nil, fmt.Errorf("error bluring the image: %w", err) } diff --git a/carver_benchmark_test.go b/carver_benchmark_test.go index 7e581edf..932fee44 100644 --- a/carver_benchmark_test.go +++ b/carver_benchmark_test.go @@ -1,39 +1,40 @@ -package caire - -import ( - "image" - "os" - "path/filepath" - "testing" -) - -func Benchmark_Carver(b *testing.B) { - sampleImg := filepath.Join("./testdata", "sample.jpg") - f, err := os.Open(sampleImg) - if err != nil { - b.Fatalf("could not load sample image: %v", err) - } - defer f.Close() - - src, _, err := image.Decode(f) - if err != nil { - b.Fatalf("error decoding image: %v", err) - } - b.ResetTimer() - - img := imgToNRGBA(src) - - width, height := img.Bounds().Max.X, img.Bounds().Max.Y - c := NewCarver(width, height) - - for i := 0; i < b.N; i++ { - _, err := c.ComputeSeams(p, img) - if err != nil { - b.FailNow() - } - - seams := c.FindLowestEnergySeams(p) - img = c.RemoveSeam(img, seams, p.Debug) - } - -} +package caire + +import ( + "image" + "os" + "path/filepath" + "testing" + + cimage "github.com/esimov/caire/image" +) + +func Benchmark_Carver(b *testing.B) { + sampleImg := filepath.Join("./testdata", "sample.jpg") + f, err := os.Open(sampleImg) + if err != nil { + b.Fatalf("could not load sample image: %v", err) + } + defer f.Close() + + src, _, err := image.Decode(f) + if err != nil { + b.Fatalf("error decoding image: %v", err) + } + + img := cimage.ImgToNRGBA(src) + + width, height := img.Bounds().Max.X, img.Bounds().Max.Y + c := NewCarver(width, height) + + for b.Loop() { + _, err := c.ComputeSeams(p, img) + if err != nil { + b.FailNow() + } + + seams := c.FindLowestEnergySeams(p) + img = c.RemoveSeam(img, seams, p.Debug) + } + +} diff --git a/carver_test.go b/carver_test.go index 53070678..cac6f3aa 100644 --- a/carver_test.go +++ b/carver_test.go @@ -9,6 +9,7 @@ import ( "path/filepath" "testing" + cimage "github.com/esimov/caire/image" "github.com/esimov/caire/utils" pigo "github.com/esimov/pigo/core" "github.com/stretchr/testify/assert" @@ -134,7 +135,7 @@ func TestCarver_DetectVerticalEnergySeam(t *testing.T) { } var c = NewCarver(dx, dy) - img = rotateImage90(img) + img = cimage.RotateImage90(img) for x := 0; x < imgHeight; x++ { width, height := img.Bounds().Max.X, img.Bounds().Max.Y @@ -279,11 +280,11 @@ func TestCarver_ShouldDetectFace(t *testing.T) { if err != nil { t.Fatalf("error decoding image: %v", err) } - img := imgToNRGBA(src) + img := cimage.ImgToNRGBA(src) dx, dy := img.Bounds().Max.X, img.Bounds().Max.Y // Transform the image to a pixel array. - pixels := rgbToGrayscale(img) + pixels := cimage.RgbToGrayscale(img) cParams := pigo.CascadeParams{ MinSize: 100, @@ -329,16 +330,16 @@ func TestCarver_ShouldNotRemoveFaceZone(t *testing.T) { if err != nil { t.Fatalf("error decoding image: %v", err) } - img := imgToNRGBA(src) + img := cimage.ImgToNRGBA(src) dx, dy := img.Bounds().Max.X, img.Bounds().Max.Y c := NewCarver(dx, dy) // Transform the image to a pixel array. - pixels := rgbToGrayscale(img) + pixels := cimage.RgbToGrayscale(img) - sobel := c.SobelDetector(img, float64(p.SobelThreshold)) + sobel := cimage.SobelDetector(img, float64(p.SobelThreshold)) - err = Stackblur(img, sobel, uint32(p.BlurRadius)) + err = cimage.Stackblur(img, sobel, uint32(p.BlurRadius)) assert.NoError(t, err) cParams := pigo.CascadeParams{ @@ -410,11 +411,11 @@ func TestCarver_ShouldNotResizeWithFaceDistorsion(t *testing.T) { if err != nil { t.Fatalf("error decoding image: %v", err) } - img := imgToNRGBA(src) + img := cimage.ImgToNRGBA(src) dx, dy := img.Bounds().Max.X, img.Bounds().Max.Y // Transform the image to a pixel array. - pixels := rgbToGrayscale(img) + pixels := cimage.RgbToGrayscale(img) cParams := pigo.CascadeParams{ MinSize: 100, MaxSize: utils.Max(dx, dy), @@ -600,8 +601,7 @@ func TestCarver_SobelShouldDetectEdgesInEveryColorChannel(t *testing.T) { draw.Draw(img, image.Rect(0, 0, imgWidth/2, imgHeight), green, image.Point{}, draw.Src) draw.Draw(img, image.Rect(imgWidth/2, 0, imgWidth, imgHeight), blue, image.Point{}, draw.Src) - c := NewCarver(imgWidth, imgHeight) - sobel := c.SobelDetector(img, 0) + sobel := cimage.SobelDetector(img, 0) var energy int for i := 0; i < len(sobel.Pix); i += 4 { diff --git a/cmd/caire/main.go b/cmd/caire/main.go index 2c568b8a..42b9dbb9 100644 --- a/cmd/caire/main.go +++ b/cmd/caire/main.go @@ -53,7 +53,7 @@ func main() { log.SetFlags(0) flag.Usage = func() { - fmt.Fprintf(os.Stderr, fmt.Sprintf(HelpBanner, Version)) + fmt.Fprintf(os.Stderr, "%s", fmt.Sprintf(HelpBanner, Version)) flag.PrintDefaults() } flag.Parse() diff --git a/exec.go b/exec.go index f3ecb540..287cbf3b 100644 --- a/exec.go +++ b/exec.go @@ -16,6 +16,7 @@ import ( "slices" + cimage "github.com/esimov/caire/image" "github.com/esimov/caire/utils" "golang.org/x/term" ) @@ -41,6 +42,7 @@ type result struct { err error } +// Resize implements the interface method with the same name in the Processor struct. func Resize(s SeamCarver, img *image.NRGBA) (image.Image, error) { return s.Resize(img) } @@ -156,13 +158,13 @@ func (p *Processor) Execute(img *Image) { } if err = <-errc; err != nil { - fmt.Fprintf(os.Stderr, utils.DecorateText(err.Error(), utils.ErrorMessage)) + fmt.Fprintf(os.Stderr, "%s", utils.DecorateText(err.Error(), utils.ErrorMessage)) } case mode.IsRegular() || mode&os.ModeNamedPipe != 0: // check for regular files or pipe names ext := filepath.Ext(img.Dst) if !slices.Contains(validExtensions, ext) && img.Dst != img.PipeName { - log.Fatalf(utils.DecorateText(fmt.Sprintf("%v file type not supported", ext), utils.ErrorMessage)) + log.Fatalf("%s", utils.DecorateText(fmt.Sprintf("%v file type not supported", ext), utils.ErrorMessage)) } err = img.process(p, img.Src, img.Dst) @@ -258,20 +260,20 @@ func (img *Image) process(p *Processor, in, out string) error { }() if len(img.MaskPath) > 0 { - mask, err := decodeImg(img.MaskPath) + mask, err := cimage.DecodeContent(img.MaskPath) if err != nil { return fmt.Errorf("cannot decode image: %w", err) } - p.Mask = dither(imgToNRGBA(mask)) + p.Mask = cimage.Dither(mask) p.DebugMask = p.Mask } if len(img.RMaskPath) > 0 { - rmask, err := decodeImg(img.RMaskPath) + rmask, err := cimage.DecodeContent(img.RMaskPath) if err != nil { return fmt.Errorf("cannot decode image: %w", err) } - p.RMask = dither(imgToNRGBA(rmask)) + p.RMask = cimage.Dither(rmask) p.DebugMask = p.RMask } diff --git a/go.mod b/go.mod index 7e5b7767..28dd1b8c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/esimov/caire -go 1.22 +go 1.27 require ( gioui.org v0.8.0 diff --git a/gui.go b/gui.go index e88d7b4c..27a93980 100644 --- a/gui.go +++ b/gui.go @@ -22,6 +22,7 @@ import ( "gioui.org/unit" "gioui.org/widget" "gioui.org/widget/material" + cimage "github.com/esimov/caire/image" "github.com/esimov/caire/imop" "github.com/esimov/caire/utils" ) @@ -273,7 +274,7 @@ func (g *Gui) Run() error { } if g.proc.vRes { - g.process.img = rotateImage270(g.process.img.(*image.NRGBA)) + g.process.img = cimage.RotateImage270(g.process.img.(*image.NRGBA)) } w.Invalidate() default: diff --git a/image.go b/image/image.go similarity index 54% rename from image.go rename to image/image.go index 93c2a205..961eb09f 100644 --- a/image.go +++ b/image/image.go @@ -1,257 +1,228 @@ -package caire - -import ( - "errors" - "fmt" - "image" - "image/color" - "image/jpeg" - "image/png" - "io" - "os" - "path/filepath" - "strings" - - "github.com/esimov/caire/utils" - "golang.org/x/image/bmp" -) - -// decodeImg decodes an image file to type image.Image -func decodeImg(src string) (image.Image, error) { - file, err := os.Open(src) - if err != nil { - return nil, fmt.Errorf("could not open the mask file: %v", err) - } - - ctype, err := utils.DetectContentType(file.Name()) - if err != nil { - return nil, err - } - - if !strings.Contains(ctype.(string), "image") { - return nil, fmt.Errorf("the mask should be an image file") - } - - img, _, err := image.Decode(file) - if err != nil { - return nil, fmt.Errorf("could not decode the mask file: %v", err) - } - - return img, nil -} - -// encodeImg encodes an image to a destination of type io.Writer. -func encodeImg(p *Processor, w io.Writer, img *image.NRGBA) error { - switch w := w.(type) { - case *os.File: - ext := filepath.Ext(w.Name()) - switch ext { - case "", ".jpg", ".jpeg": - res, err := Resize(p, img) - if err != nil { - return err - } - return jpeg.Encode(w, res, &jpeg.Options{Quality: 100}) - case ".png": - res, err := Resize(p, img) - if err != nil { - return err - } - return png.Encode(w, res) - case ".bmp": - res, err := Resize(p, img) - if err != nil { - return err - } - return bmp.Encode(w, res) - default: - return errors.New("unsupported image format") - } - default: - res, err := Resize(p, img) - if err != nil { - return err - } - return jpeg.Encode(w, res, &jpeg.Options{Quality: 100}) - } -} - -// rotateImage90 rotate the image by 90 degree counter clockwise. -func rotateImage90(src *image.NRGBA) *image.NRGBA { - b := src.Bounds() - dst := image.NewNRGBA(image.Rect(0, 0, b.Max.Y, b.Max.X)) - for dstY := 0; dstY < b.Max.X; dstY++ { - for dstX := 0; dstX < b.Max.Y; dstX++ { - srcX := b.Max.X - dstY - 1 - srcY := dstX - - srcOff := srcY*src.Stride + srcX*4 - dstOff := dstY*dst.Stride + dstX*4 - copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4]) - } - } - return dst -} - -// rotateImage270 rotate the image by 270 degree counter clockwise. -func rotateImage270(src *image.NRGBA) *image.NRGBA { - b := src.Bounds() - dst := image.NewNRGBA(image.Rect(0, 0, b.Max.Y, b.Max.X)) - for dstY := 0; dstY < b.Max.X; dstY++ { - for dstX := 0; dstX < b.Max.Y; dstX++ { - srcX := dstY - srcY := b.Max.Y - dstX - 1 - - srcOff := srcY*src.Stride + srcX*4 - dstOff := dstY*dst.Stride + dstX*4 - copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4]) - } - } - - return dst -} - -// imgToNRGBA converts any image type to *image.NRGBA with min-point at (0, 0). -func imgToNRGBA(img image.Image) *image.NRGBA { - srcBounds := img.Bounds() - if srcBounds.Min.X == 0 && srcBounds.Min.Y == 0 { - if src0, ok := img.(*image.NRGBA); ok { - return src0 - } - } - srcMinX := srcBounds.Min.X - srcMinY := srcBounds.Min.Y - - dstBounds := srcBounds.Sub(srcBounds.Min) - dstW := dstBounds.Dx() - dstH := dstBounds.Dy() - dst := image.NewNRGBA(dstBounds) - - switch src := img.(type) { - case *image.NRGBA: - rowSize := srcBounds.Dx() * 4 - for dstY := 0; dstY < dstH; dstY++ { - di := dst.PixOffset(0, dstY) - si := src.PixOffset(srcMinX, srcMinY+dstY) - for dstX := 0; dstX < dstW; dstX++ { - copy(dst.Pix[di:di+rowSize], src.Pix[si:si+rowSize]) - } - } - case *image.YCbCr: - for dstY := 0; dstY < dstH; dstY++ { - di := dst.PixOffset(0, dstY) - for dstX := 0; dstX < dstW; dstX++ { - srcX := srcMinX + dstX - srcY := srcMinY + dstY - siy := src.YOffset(srcX, srcY) - sic := src.COffset(srcX, srcY) - r, g, b := color.YCbCrToRGB(src.Y[siy], src.Cb[sic], src.Cr[sic]) - dst.Pix[di+0] = r - dst.Pix[di+1] = g - dst.Pix[di+2] = b - dst.Pix[di+3] = 0xff - di += 4 - } - } - default: - for dstY := 0; dstY < dstH; dstY++ { - di := dst.PixOffset(0, dstY) - for dstX := 0; dstX < dstW; dstX++ { - c := color.NRGBAModel.Convert(img.At(srcMinX+dstX, srcMinY+dstY)).(color.NRGBA) - dst.Pix[di+0] = c.R - dst.Pix[di+1] = c.G - dst.Pix[di+2] = c.B - dst.Pix[di+3] = c.A - di += 4 - } - } - } - - return dst -} - -// imgToPix converts an image to a pixel array. -func imgToPix(src *image.NRGBA) []uint8 { - bounds := src.Bounds() - pixels := make([]uint8, 0, bounds.Max.X*bounds.Max.Y*4) - - for x := bounds.Min.X; x < bounds.Max.X; x++ { - for y := bounds.Min.Y; y < bounds.Max.Y; y++ { - r, g, b, _ := src.At(y, x).RGBA() - pixels = append(pixels, uint8(r>>8), uint8(g>>8), uint8(b>>8), 255) - } - } - - return pixels -} - -// pixToImage converts an array buffer to an image. -func pixToImage(pixels []uint8, width, height int) image.Image { - dst := image.NewNRGBA(image.Rect(0, 0, width, height)) - bounds := dst.Bounds() - dx, dy := bounds.Max.X, bounds.Max.Y - col := color.NRGBA{ - R: uint8(0), - G: uint8(0), - B: uint8(0), - A: uint8(255), - } - - for x := bounds.Min.X; x < dx; x++ { - for y := bounds.Min.Y; y < dy*4; y += 4 { - col.R = uint8(pixels[y+x*dy*4]) - col.G = uint8(pixels[y+x*dy*4+1]) - col.B = uint8(pixels[y+x*dy*4+2]) - col.A = uint8(pixels[y+x*dy*4+3]) - - dst.SetNRGBA(x, int(y/4), col) - } - } - - return dst -} - -// rgbToGrayscale converts an image to grayscale mode and -// returns the pixel values as an one dimensional array. -func rgbToGrayscale(src *image.NRGBA) []uint8 { - width, height := src.Bounds().Dx(), src.Bounds().Dy() - gray := make([]uint8, width*height) - - for y := 0; y < height; y++ { - for x := 0; x < width; x++ { - r, g, b, _ := src.At(x, y).RGBA() - gray[y*width+x] = uint8( - (0.299*float64(r) + - 0.587*float64(g) + - 0.114*float64(b)) / 256, - ) - } - } - - return gray -} - -// dither converts an image to black and white image, where the white is fully transparent. -func dither(src *image.NRGBA) *image.NRGBA { - var ( - bounds = src.Bounds() - dithered = image.NewNRGBA(bounds) - dx = bounds.Dx() - dy = bounds.Dy() - ) - - for x := 0; x < dx; x++ { - for y := 0; y < dy; y++ { - r, g, b, _ := src.At(x, y).RGBA() - threshold := func() color.Color { - if r > 127 && g > 127 && b > 127 { - return color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff} - } - return color.NRGBA{A: 0x00} - } - dithered.Set(x, y, threshold()) - } - } - - return dithered -} +// Package image can be perceived as an utilitary library for all kind of +// image processing operations, like image conversion, image decoding and encoding, +// pixel manipulation, color conversion etc. +package image + +import ( + "fmt" + "image" + "image/color" + "io" + "os" + "strings" + + "github.com/esimov/caire/utils" +) + +func DecodeContent(src string) (image.Image, error) { + file, err := os.Open(src) + if err != nil { + return nil, fmt.Errorf("could not open the mask file: %v", err) + } + + ctype, err := utils.DetectContentType(file.Name()) + if err != nil { + return nil, err + } + + if !strings.Contains(ctype.(string), "image") { + return nil, fmt.Errorf("the mask should be an image file") + } + + img, err := Decode(file) + if err != nil { + return nil, fmt.Errorf("cannot decode content: %w", err) + } + + return img, nil +} + +// Decode decodes an image file to type image.Image +func Decode(r io.Reader) (image.Image, error) { + img, _, err := image.Decode(r) + if err != nil { + return nil, fmt.Errorf("could not decode the mask file: %v", err) + } + + return img, nil +} + +// ImgToNRGBA converts any image type to *image.NRGBA with min-point at (0, 0). +func ImgToNRGBA(img image.Image) *image.NRGBA { + srcBounds := img.Bounds() + if srcBounds.Min.X == 0 && srcBounds.Min.Y == 0 { + if src0, ok := img.(*image.NRGBA); ok { + return src0 + } + } + srcMinX := srcBounds.Min.X + srcMinY := srcBounds.Min.Y + + dstBounds := srcBounds.Sub(srcBounds.Min) + dstW := dstBounds.Dx() + dstH := dstBounds.Dy() + dst := image.NewNRGBA(dstBounds) + + switch src := img.(type) { + case *image.NRGBA: + rowSize := srcBounds.Dx() * 4 + for dstY := 0; dstY < dstH; dstY++ { + di := dst.PixOffset(0, dstY) + si := src.PixOffset(srcMinX, srcMinY+dstY) + for dstX := 0; dstX < dstW; dstX++ { + copy(dst.Pix[di:di+rowSize], src.Pix[si:si+rowSize]) + } + } + case *image.YCbCr: + for dstY := 0; dstY < dstH; dstY++ { + di := dst.PixOffset(0, dstY) + for dstX := 0; dstX < dstW; dstX++ { + srcX := srcMinX + dstX + srcY := srcMinY + dstY + siy := src.YOffset(srcX, srcY) + sic := src.COffset(srcX, srcY) + r, g, b := color.YCbCrToRGB(src.Y[siy], src.Cb[sic], src.Cr[sic]) + dst.Pix[di+0] = r + dst.Pix[di+1] = g + dst.Pix[di+2] = b + dst.Pix[di+3] = 0xff + di += 4 + } + } + default: + for dstY := 0; dstY < dstH; dstY++ { + di := dst.PixOffset(0, dstY) + for dstX := 0; dstX < dstW; dstX++ { + c := color.NRGBAModel.Convert(img.At(srcMinX+dstX, srcMinY+dstY)).(color.NRGBA) + dst.Pix[di+0] = c.R + dst.Pix[di+1] = c.G + dst.Pix[di+2] = c.B + dst.Pix[di+3] = c.A + di += 4 + } + } + } + + return dst +} + +// RgbToGrayscale converts an image to grayscale mode and +// returns the pixel values as an one dimensional array. +func RgbToGrayscale(img *image.NRGBA) []uint8 { + width, height := img.Bounds().Dx(), img.Bounds().Dy() + gray := make([]uint8, width*height) + + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + r, g, b, _ := img.At(x, y).RGBA() + gray[y*width+x] = uint8( + (0.299*float64(r) + + 0.587*float64(g) + + 0.114*float64(b)) / 256, + ) + } + } + + return gray +} + +// Dither converts an image to black and white image, where the white is fully transparent. +func Dither(img image.Image) *image.NRGBA { + bounds := img.Bounds() + dithered := image.NewNRGBA(bounds) + dx, dy := bounds.Dx(), bounds.Dy() + + for x := 0; x < dx; x++ { + for y := 0; y < dy; y++ { + r, g, b, _ := img.At(x, y).RGBA() + threshold := func() color.Color { + if r > 127 && g > 127 && b > 127 { + return color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff} + } + return color.NRGBA{A: 0x00} + } + dithered.Set(x, y, threshold()) + } + } + + return dithered +} + +// ImageToPix converts an image to a pixel array. +func ImageToPix(img *image.NRGBA) []uint8 { + bounds := img.Bounds() + pixels := make([]uint8, 0, bounds.Max.X*bounds.Max.Y*4) + + for x := bounds.Min.X; x < bounds.Max.X; x++ { + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + r, g, b, _ := img.At(y, x).RGBA() + pixels = append(pixels, uint8(r>>8), uint8(g>>8), uint8(b>>8), 255) + } + } + + return pixels +} + +// PixToImage converts an array buffer to an image. +func PixToImage(pixels []uint8, width, height int) image.Image { + dst := image.NewNRGBA(image.Rect(0, 0, width, height)) + bounds := dst.Bounds() + dx, dy := bounds.Max.X, bounds.Max.Y + col := color.NRGBA{ + R: uint8(0), + G: uint8(0), + B: uint8(0), + A: uint8(255), + } + + for x := bounds.Min.X; x < dx; x++ { + for y := bounds.Min.Y; y < dy*4; y += 4 { + col.R = uint8(pixels[y+x*dy*4]) + col.G = uint8(pixels[y+x*dy*4+1]) + col.B = uint8(pixels[y+x*dy*4+2]) + col.A = uint8(pixels[y+x*dy*4+3]) + + dst.SetNRGBA(x, int(y/4), col) + } + } + + return dst +} + +// RotateImage90 rotate the image by 90 degree counter clockwise. +func RotateImage90(img *image.NRGBA) *image.NRGBA { + bounds := img.Bounds() + dst := image.NewNRGBA(image.Rect(0, 0, bounds.Max.Y, bounds.Max.X)) + + for dstY := 0; dstY < bounds.Max.X; dstY++ { + for dstX := 0; dstX < bounds.Max.Y; dstX++ { + srcX := bounds.Max.X - dstY - 1 + srcY := dstX + + srcOff := srcY*img.Stride + srcX*4 + dstOff := dstY*dst.Stride + dstX*4 + copy(dst.Pix[dstOff:dstOff+4], img.Pix[srcOff:srcOff+4]) + } + } + + return dst +} + +// RotateImage270 rotate the image by 270 degree counter clockwise. +func RotateImage270(img *image.NRGBA) *image.NRGBA { + bounds := img.Bounds() + dst := image.NewNRGBA(image.Rect(0, 0, bounds.Max.Y, bounds.Max.X)) + + for dstY := 0; dstY < bounds.Max.X; dstY++ { + for dstX := 0; dstX < bounds.Max.Y; dstX++ { + srcX := dstY + srcY := bounds.Max.Y - dstX - 1 + + srcOff := srcY*img.Stride + srcX*4 + dstOff := dstY*dst.Stride + dstX*4 + copy(dst.Pix[dstOff:dstOff+4], img.Pix[srcOff:srcOff+4]) + } + } + + return dst +} diff --git a/image_test.go b/image/image_test.go similarity index 95% rename from image_test.go rename to image/image_test.go index 09fb8242..9fee1aae 100644 --- a/image_test.go +++ b/image/image_test.go @@ -1,230 +1,220 @@ -package caire - -import ( - "image" - "image/color" - "image/color/palette" - "image/draw" - "os" - "path/filepath" - "testing" - - "github.com/esimov/caire/utils" -) - -func TestImage_ShouldGetSampleImage(t *testing.T) { - path := filepath.Join("./testdata", "sample.jpg") - _, err := os.ReadFile(path) - if err != nil { - t.Errorf("Should get the sample image") - } -} - -func TestImage_ImgToNRGBA(t *testing.T) { - rect := image.Rect(-1, -1, 15, 15) - colors := palette.Plan9 - testCases := []struct { - name string - img image.Image - }{ - { - name: "NRGBA", - img: makeNRGBAImage(rect, colors), - }, - { - name: "YCbCr-444", - img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio444), - }, - { - name: "YCbCr-422", - img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio422), - }, - { - name: "YCbCr-420", - img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio420), - }, - { - name: "YCbCr-440", - img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio440), - }, - { - name: "YCbCr-410", - img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio410), - }, - { - name: "YCbCr-411", - img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio411), - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - r := tc.img.Bounds() - for y := r.Min.Y; y < r.Max.Y; y++ { - buf := make([]byte, r.Dx()*4) - scan(tc.img, 0, y-r.Min.Y, r.Dx(), y+1-r.Min.Y, buf) - wantBuf := readRow(tc.img, y) - if !compareBytes(buf, wantBuf, 1) { - t.Errorf("scan horizontal line (y=%d): got %v want %v", y, buf, wantBuf) - } - } - for x := r.Min.X; x < r.Max.X; x++ { - buf := make([]byte, r.Dy()*4) - scan(tc.img, x-r.Min.X, 0, x+1-r.Min.X, r.Dy(), buf) - wantBuf := readColumn(tc.img, x) - if !compareBytes(buf, wantBuf, 1) { - t.Errorf("scan vertical line (x=%d): got %v want %v", x, buf, wantBuf) - } - } - }) - } -} - -func scan(img image.Image, x1, y1, x2, y2 int, dst []uint8) { - switch img := img.(type) { - case *image.NRGBA: - size := (x2 - x1) * 4 - j := 0 - i := y1*img.Stride + x1*4 - for y := y1; y < y2; y++ { - copy(dst[j:j+size], img.Pix[i:i+size]) - j += size - i += img.Stride - } - case *image.YCbCr: - j := 0 - x1 += img.Rect.Min.X - x2 += img.Rect.Min.X - y1 += img.Rect.Min.Y - y2 += img.Rect.Min.Y - for y := y1; y < y2; y++ { - iy := (y-img.Rect.Min.Y)*img.YStride + (x1 - img.Rect.Min.X) - for x := x1; x < x2; x++ { - var ic int - switch img.SubsampleRatio { - case image.YCbCrSubsampleRatio444: - ic = (y-img.Rect.Min.Y)*img.CStride + (x - img.Rect.Min.X) - case image.YCbCrSubsampleRatio422: - ic = (y-img.Rect.Min.Y)*img.CStride + (x/2 - img.Rect.Min.X/2) - case image.YCbCrSubsampleRatio420: - ic = (y/2-img.Rect.Min.Y/2)*img.CStride + (x/2 - img.Rect.Min.X/2) - case image.YCbCrSubsampleRatio440: - ic = (y/2-img.Rect.Min.Y/2)*img.CStride + (x - img.Rect.Min.X) - default: - ic = img.COffset(x, y) - } - - yy := int(img.Y[iy]) - cb := int(img.Cb[ic]) - 128 - cr := int(img.Cr[ic]) - 128 - - r := (yy<<16 + 91881*cr + 1<<15) >> 16 - if r > 0xff { - r = 0xff - } else if r < 0 { - r = 0 - } - - g := (yy<<16 - 22554*cb - 46802*cr + 1<<15) >> 16 - if g > 0xff { - g = 0xff - } else if g < 0 { - g = 0 - } - - b := (yy<<16 + 116130*cb + 1<<15) >> 16 - if b > 0xff { - b = 0xff - } else if b < 0 { - b = 0 - } - - dst[j+0] = uint8(r) - dst[j+1] = uint8(g) - dst[j+2] = uint8(b) - dst[j+3] = 0xff - - iy++ - j += 4 - } - } - } -} - -func makeYCbCrImage(rect image.Rectangle, colors []color.Color, sr image.YCbCrSubsampleRatio) *image.YCbCr { - img := image.NewYCbCr(rect, sr) - j := 0 - for y := rect.Min.Y; y < rect.Max.Y; y++ { - for x := rect.Min.X; x < rect.Max.X; x++ { - iy := img.YOffset(x, y) - ic := img.COffset(x, y) - c := color.NRGBAModel.Convert(colors[j]).(color.NRGBA) - img.Y[iy], img.Cb[ic], img.Cr[ic] = color.RGBToYCbCr(c.R, c.G, c.B) - j++ - } - } - return img -} - -func makeNRGBAImage(rect image.Rectangle, colors []color.Color) *image.NRGBA { - img := image.NewNRGBA(rect) - fillDrawImage(img, colors) - return img -} - -func fillDrawImage(img draw.Image, colors []color.Color) { - colorsNRGBA := make([]color.NRGBA, len(colors)) - for i, c := range colors { - nrgba := color.NRGBAModel.Convert(c).(color.NRGBA) - nrgba.A = uint8(i % 256) - colorsNRGBA[i] = nrgba - } - rect := img.Bounds() - i := 0 - for y := rect.Min.Y; y < rect.Max.Y; y++ { - for x := rect.Min.X; x < rect.Max.X; x++ { - img.Set(x, y, colorsNRGBA[i]) - i++ - } - } -} - -func readRow(img image.Image, y int) []uint8 { - row := make([]byte, img.Bounds().Dx()*4) - i := 0 - for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { - c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) - row[i+0] = c.R - row[i+1] = c.G - row[i+2] = c.B - row[i+3] = c.A - i += 4 - } - return row -} - -func readColumn(img image.Image, x int) []uint8 { - column := make([]byte, img.Bounds().Dy()*4) - i := 0 - for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { - c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) - column[i+0] = c.R - column[i+1] = c.G - column[i+2] = c.B - column[i+3] = c.A - i += 4 - } - return column -} - -func compareBytes(a, b []uint8, delta int) bool { - if len(a) != len(b) { - return false - } - for i := 0; i < len(a); i++ { - if utils.Abs(int(a[i])-int(b[i])) > delta { - return false - } - } - return true -} +package image + +import ( + "image" + "image/color" + "image/color/palette" + "image/draw" + "testing" + + "github.com/esimov/caire/utils" +) + +func TestImage_ImgToNRGBA(t *testing.T) { + rect := image.Rect(-1, -1, 15, 15) + colors := palette.Plan9 + testCases := []struct { + name string + img image.Image + }{ + { + name: "NRGBA", + img: makeNRGBAImage(rect, colors), + }, + { + name: "YCbCr-444", + img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio444), + }, + { + name: "YCbCr-422", + img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio422), + }, + { + name: "YCbCr-420", + img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio420), + }, + { + name: "YCbCr-440", + img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio440), + }, + { + name: "YCbCr-410", + img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio410), + }, + { + name: "YCbCr-411", + img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio411), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + r := tc.img.Bounds() + for y := r.Min.Y; y < r.Max.Y; y++ { + buf := make([]byte, r.Dx()*4) + scan(tc.img, 0, y-r.Min.Y, r.Dx(), y+1-r.Min.Y, buf) + wantBuf := readRow(tc.img, y) + if !compareBytes(buf, wantBuf, 1) { + t.Errorf("scan horizontal line (y=%d): got %v want %v", y, buf, wantBuf) + } + } + for x := r.Min.X; x < r.Max.X; x++ { + buf := make([]byte, r.Dy()*4) + scan(tc.img, x-r.Min.X, 0, x+1-r.Min.X, r.Dy(), buf) + wantBuf := readColumn(tc.img, x) + if !compareBytes(buf, wantBuf, 1) { + t.Errorf("scan vertical line (x=%d): got %v want %v", x, buf, wantBuf) + } + } + }) + } +} + +func scan(img image.Image, x1, y1, x2, y2 int, dst []uint8) { + switch img := img.(type) { + case *image.NRGBA: + size := (x2 - x1) * 4 + j := 0 + i := y1*img.Stride + x1*4 + for y := y1; y < y2; y++ { + copy(dst[j:j+size], img.Pix[i:i+size]) + j += size + i += img.Stride + } + case *image.YCbCr: + j := 0 + x1 += img.Rect.Min.X + x2 += img.Rect.Min.X + y1 += img.Rect.Min.Y + y2 += img.Rect.Min.Y + for y := y1; y < y2; y++ { + iy := (y-img.Rect.Min.Y)*img.YStride + (x1 - img.Rect.Min.X) + for x := x1; x < x2; x++ { + var ic int + switch img.SubsampleRatio { + case image.YCbCrSubsampleRatio444: + ic = (y-img.Rect.Min.Y)*img.CStride + (x - img.Rect.Min.X) + case image.YCbCrSubsampleRatio422: + ic = (y-img.Rect.Min.Y)*img.CStride + (x/2 - img.Rect.Min.X/2) + case image.YCbCrSubsampleRatio420: + ic = (y/2-img.Rect.Min.Y/2)*img.CStride + (x/2 - img.Rect.Min.X/2) + case image.YCbCrSubsampleRatio440: + ic = (y/2-img.Rect.Min.Y/2)*img.CStride + (x - img.Rect.Min.X) + default: + ic = img.COffset(x, y) + } + + yy := int(img.Y[iy]) + cb := int(img.Cb[ic]) - 128 + cr := int(img.Cr[ic]) - 128 + + r := (yy<<16 + 91881*cr + 1<<15) >> 16 + if r > 0xff { + r = 0xff + } else if r < 0 { + r = 0 + } + + g := (yy<<16 - 22554*cb - 46802*cr + 1<<15) >> 16 + if g > 0xff { + g = 0xff + } else if g < 0 { + g = 0 + } + + b := (yy<<16 + 116130*cb + 1<<15) >> 16 + if b > 0xff { + b = 0xff + } else if b < 0 { + b = 0 + } + + dst[j+0] = uint8(r) + dst[j+1] = uint8(g) + dst[j+2] = uint8(b) + dst[j+3] = 0xff + + iy++ + j += 4 + } + } + } +} + +func makeYCbCrImage(rect image.Rectangle, colors []color.Color, sr image.YCbCrSubsampleRatio) *image.YCbCr { + img := image.NewYCbCr(rect, sr) + j := 0 + for y := rect.Min.Y; y < rect.Max.Y; y++ { + for x := rect.Min.X; x < rect.Max.X; x++ { + iy := img.YOffset(x, y) + ic := img.COffset(x, y) + c := color.NRGBAModel.Convert(colors[j]).(color.NRGBA) + img.Y[iy], img.Cb[ic], img.Cr[ic] = color.RGBToYCbCr(c.R, c.G, c.B) + j++ + } + } + return img +} + +func makeNRGBAImage(rect image.Rectangle, colors []color.Color) *image.NRGBA { + img := image.NewNRGBA(rect) + fillDrawImage(img, colors) + return img +} + +func fillDrawImage(img draw.Image, colors []color.Color) { + colorsNRGBA := make([]color.NRGBA, len(colors)) + for i, c := range colors { + nrgba := color.NRGBAModel.Convert(c).(color.NRGBA) + nrgba.A = uint8(i % 256) + colorsNRGBA[i] = nrgba + } + rect := img.Bounds() + i := 0 + for y := rect.Min.Y; y < rect.Max.Y; y++ { + for x := rect.Min.X; x < rect.Max.X; x++ { + img.Set(x, y, colorsNRGBA[i]) + i++ + } + } +} + +func readRow(img image.Image, y int) []uint8 { + row := make([]byte, img.Bounds().Dx()*4) + i := 0 + for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { + c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) + row[i+0] = c.R + row[i+1] = c.G + row[i+2] = c.B + row[i+3] = c.A + i += 4 + } + return row +} + +func readColumn(img image.Image, x int) []uint8 { + column := make([]byte, img.Bounds().Dy()*4) + i := 0 + for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { + c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) + column[i+0] = c.R + column[i+1] = c.G + column[i+2] = c.B + column[i+3] = c.A + i += 4 + } + return column +} + +func compareBytes(a, b []uint8, delta int) bool { + if len(a) != len(b) { + return false + } + for i := 0; i < len(a); i++ { + if utils.Abs(int(a[i])-int(b[i])) > delta { + return false + } + } + return true +} diff --git a/sobel.go b/image/sobel.go similarity index 67% rename from sobel.go rename to image/sobel.go index a883b909..8171a84f 100644 --- a/sobel.go +++ b/image/sobel.go @@ -1,4 +1,4 @@ -package caire +package image import ( "image" @@ -23,11 +23,13 @@ var ( // SobelDetector uses the sobel filter operator for detecting image edges. // See https://en.wikipedia.org/wiki/Sobel_operator -func (c *Carver) SobelDetector(img *image.NRGBA, threshold float64) *image.NRGBA { +func SobelDetector(img *image.NRGBA, threshold float64) *image.NRGBA { var sumX, sumY int32 - dx, dy := img.Bounds().Max.X, img.Bounds().Max.Y - dst := image.NewNRGBA(img.Bounds()) - data := c.getImageData(img) + + bounds := img.Bounds() + dx, dy := bounds.Dx(), bounds.Dy() + dst := image.NewNRGBA(bounds) + grayscale := RgbToGrayscale(img) // at returns the brightness of the pixel at (x, y). Coordinates outside // the image are clamped to the nearest border pixel, so the borders @@ -35,7 +37,7 @@ func (c *Carver) SobelDetector(img *image.NRGBA, threshold float64) *image.NRGBA at := func(x, y int) int32 { x = min(max(x, 0), dx-1) y = min(max(y, 0), dy-1) - return int32(data[y*dx+x]) + return int32(grayscale[y*dx+x]) } for y := 0; y < dy; y++ { @@ -66,17 +68,6 @@ func (c *Carver) SobelDetector(img *image.NRGBA, threshold float64) *image.NRGBA dst.Pix[idx+3] = 0xff } } - return dst -} -// getImageData converts the image to grayscale and returns an array of pixel brightness values. -func (c *Carver) getImageData(img *image.NRGBA) []uint8 { - dx, dy := img.Bounds().Max.X, img.Bounds().Max.Y - pixels := make([]uint8, dx*dy) - - for i := range pixels { - r, g, b := uint32(img.Pix[i*4]), uint32(img.Pix[i*4+1]), uint32(img.Pix[i*4+2]) - pixels[i] = uint8((299*r + 587*g + 114*b) / 1000) - } - return pixels + return dst } diff --git a/sobel_test.go b/image/sobel_test.go similarity index 87% rename from sobel_test.go rename to image/sobel_test.go index afa36226..976db44b 100644 --- a/sobel_test.go +++ b/image/sobel_test.go @@ -1,4 +1,4 @@ -package caire +package image import ( "image" @@ -10,6 +10,11 @@ import ( "github.com/stretchr/testify/assert" ) +const ( + imgWidth = 10 + imgHeight = 10 +) + // energyRow returns one character per pixel of a row: "#" for an edge, "." otherwise. func energyRow(sobel *image.NRGBA, y int) string { var row strings.Builder @@ -27,8 +32,7 @@ func TestSobel_UniformImageShouldHaveNoEnergy(t *testing.T) { img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight)) draw.Draw(img, img.Bounds(), &image.Uniform{color.Gray{Y: 0x80}}, image.Point{}, draw.Src) - c := NewCarver(imgWidth, imgHeight) - sobel := c.SobelDetector(img, 0) + sobel := SobelDetector(img, 0) // The detector used to read past the end of the data in the last rows, // which marked the bottom of every image as an edge. @@ -42,14 +46,13 @@ func TestSobel_EdgeShouldBeCenteredOnTheColorStep(t *testing.T) { img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight)) draw.Draw(img, image.Rect(imgWidth/2, 0, imgWidth, imgHeight), image.White, image.Point{}, draw.Src) - c := NewCarver(imgWidth, imgHeight) - sobel := c.SobelDetector(img, 0) + out := SobelDetector(img, 0) // Only the two columns on each side of the step are edges. // The window used to be anchored at the top left corner, which moved // every edge one pixel up and to the left. want := strings.Repeat(".", imgWidth/2-1) + "##" + strings.Repeat(".", imgWidth/2-1) for y := 0; y < imgHeight; y++ { - assert.Equal(t, want, energyRow(sobel, y), "row %d", y) + assert.Equal(t, want, energyRow(out, y), "row %d", y) } } diff --git a/stackblur.go b/image/stackblur.go similarity index 95% rename from stackblur.go rename to image/stackblur.go index 3b6e5f66..309e661c 100644 --- a/stackblur.go +++ b/image/stackblur.go @@ -1,430 +1,430 @@ -// Go implementation of the StackBlur algorithm -// http://incubator.quasimondo.com/processing/fast_blur_deluxe.php - -package caire - -import ( - "errors" - "image" - "image/color" -) - -// blurStack is a linked list containing the color value and a pointer to the next struct. -type blurStack struct { - r, g, b, a uint32 - next *blurStack -} - -var mulTable = []uint32{ - 512, 512, 456, 512, 328, 456, 335, 512, 405, 328, 271, 456, 388, 335, 292, 512, - 454, 405, 364, 328, 298, 271, 496, 456, 420, 388, 360, 335, 312, 292, 273, 512, - 482, 454, 428, 405, 383, 364, 345, 328, 312, 298, 284, 271, 259, 496, 475, 456, - 437, 420, 404, 388, 374, 360, 347, 335, 323, 312, 302, 292, 282, 273, 265, 512, - 497, 482, 468, 454, 441, 428, 417, 405, 394, 383, 373, 364, 354, 345, 337, 328, - 320, 312, 305, 298, 291, 284, 278, 271, 265, 259, 507, 496, 485, 475, 465, 456, - 446, 437, 428, 420, 412, 404, 396, 388, 381, 374, 367, 360, 354, 347, 341, 335, - 329, 323, 318, 312, 307, 302, 297, 292, 287, 282, 278, 273, 269, 265, 261, 512, - 505, 497, 489, 482, 475, 468, 461, 454, 447, 441, 435, 428, 422, 417, 411, 405, - 399, 394, 389, 383, 378, 373, 368, 364, 359, 354, 350, 345, 341, 337, 332, 328, - 324, 320, 316, 312, 309, 305, 301, 298, 294, 291, 287, 284, 281, 278, 274, 271, - 268, 265, 262, 259, 257, 507, 501, 496, 491, 485, 480, 475, 470, 465, 460, 456, - 451, 446, 442, 437, 433, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388, - 385, 381, 377, 374, 370, 367, 363, 360, 357, 354, 350, 347, 344, 341, 338, 335, - 332, 329, 326, 323, 320, 318, 315, 312, 310, 307, 304, 302, 299, 297, 294, 292, - 289, 287, 285, 282, 280, 278, 275, 273, 271, 269, 267, 265, 263, 261, 259, -} - -var shgTable = []uint32{ - 9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, - 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, - 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, - 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, - 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, - 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, - 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, - 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, - 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, - 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, - 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, - 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, - 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, - 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, - 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, - 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, -} - -// Stackblur takes the source image and returns it's blurred version by applying the blur radius defined as parameter. The destination image must be a image.NRGBA. -func Stackblur(dst, src image.Image, radius uint32) error { - // Limit the maximum blur radius to 255 to avoid overflowing the multable. - if int(radius) >= len(mulTable) { - radius = uint32(len(mulTable) - 1) - } - - if radius < 1 { - return errors.New("blur radius must be greater than 0") - } - - img, ok := dst.(*image.NRGBA) - if !ok { - return errors.New("the destination image must be image.NRGBA") - } - - process(img, src, radius) - return nil -} - -func process(dst *image.NRGBA, src image.Image, radius uint32) { - srcBounds := src.Bounds() - srcMinX := srcBounds.Min.X - srcMinY := srcBounds.Min.Y - - dstBounds := srcBounds.Sub(srcBounds.Min) - dstW := dstBounds.Dx() - dstH := dstBounds.Dy() - - switch src0 := src.(type) { - case *image.NRGBA: - rowSize := srcBounds.Dx() * 4 - for dstY := 0; dstY < dstH; dstY++ { - di := src0.PixOffset(0, dstY) - si := src0.PixOffset(srcMinX, srcMinY+dstY) - for dstX := 0; dstX < dstW; dstX++ { - copy(dst.Pix[di:di+rowSize], src0.Pix[si:si+rowSize]) - } - } - case *image.YCbCr: - for dstY := 0; dstY < dstH; dstY++ { - di := dst.PixOffset(0, dstY) - for dstX := 0; dstX < dstW; dstX++ { - srcX := srcMinX + dstX - srcY := srcMinY + dstY - siy := src0.YOffset(srcX, srcY) - sic := src0.COffset(srcX, srcY) - r, g, b := color.YCbCrToRGB(src0.Y[siy], src0.Cb[sic], src0.Cr[sic]) - dst.Pix[di+0] = r - dst.Pix[di+1] = g - dst.Pix[di+2] = b - dst.Pix[di+3] = 0xff - di += 4 - } - } - default: - for dstY := 0; dstY < dstH; dstY++ { - di := dst.PixOffset(0, dstY) - for dstX := 0; dstX < dstW; dstX++ { - c := color.NRGBAModel.Convert(src.At(srcMinX+dstX, srcMinY+dstY)).(color.NRGBA) - dst.Pix[di+0] = c.R - dst.Pix[di+1] = c.G - dst.Pix[di+2] = c.B - dst.Pix[di+3] = c.A - di += 4 - } - } - } - - blurImage(dst, radius) -} - -func blurImage(src *image.NRGBA, radius uint32) { - var ( - stackEnd *blurStack - stackIn *blurStack - stackOut *blurStack - ) - - var width, height = uint32(src.Bounds().Dx()), uint32(src.Bounds().Dy()) - var ( - div, widthMinus1, heightMinus1, radiusPlus1, sumFactor uint32 - x, y, i, p, yp, yi, yw, - rSum, gSum, bSum, aSum, - rOutSum, gOutSum, bOutSum, aOutSum, - rInSum, gInSum, bInSum, aInSum, - pr, pg, pb, pa uint32 - ) - - div = radius + radius + 1 - widthMinus1 = width - 1 - heightMinus1 = height - 1 - radiusPlus1 = radius + 1 - sumFactor = radiusPlus1 * (radiusPlus1 + 1) / 2 - - stackStart := new(blurStack) - stack := stackStart - - for i = 1; i < div; i++ { - stack.next = new(blurStack) - stack = stack.next - if i == radiusPlus1 { - stackEnd = stack - } - } - stack.next = stackStart - - mulSum := mulTable[radius] - shgSum := shgTable[radius] - - for y = 0; y < height; y++ { - rInSum, gInSum, bInSum, aInSum, rSum, gSum, bSum, aSum = 0, 0, 0, 0, 0, 0, 0, 0 - - pr = uint32(src.Pix[yi]) - pg = uint32(src.Pix[yi+1]) - pb = uint32(src.Pix[yi+2]) - pa = uint32(src.Pix[yi+3]) - - rOutSum = radiusPlus1 * pr - gOutSum = radiusPlus1 * pg - bOutSum = radiusPlus1 * pb - aOutSum = radiusPlus1 * pa - - rSum += sumFactor * pr - gSum += sumFactor * pg - bSum += sumFactor * pb - aSum += sumFactor * pa - - stack = stackStart - - for i = 0; i < radiusPlus1; i++ { - stack.r = pr - stack.g = pg - stack.b = pb - stack.a = pa - stack = stack.next - } - - for i = 1; i < radiusPlus1; i++ { - var diff uint32 - if widthMinus1 < i { - diff = widthMinus1 - } else { - diff = i - } - p = yi + (diff << 2) - pr = uint32(src.Pix[p]) - pg = uint32(src.Pix[p+1]) - pb = uint32(src.Pix[p+2]) - pa = uint32(src.Pix[p+3]) - - stack.r = pr - stack.g = pg - stack.b = pb - stack.a = pa - - rSum += stack.r * (radiusPlus1 - i) - gSum += stack.g * (radiusPlus1 - i) - bSum += stack.b * (radiusPlus1 - i) - aSum += stack.a * (radiusPlus1 - i) - - rInSum += pr - gInSum += pg - bInSum += pb - aInSum += pa - - stack = stack.next - } - stackIn = stackStart - stackOut = stackEnd - - for x = 0; x < width; x++ { - pa = (aSum * mulSum) >> shgSum - src.Pix[yi+3] = uint8(pa) - - if pa != 0 { - src.Pix[yi] = uint8((rSum * mulSum) >> shgSum) - src.Pix[yi+1] = uint8((gSum * mulSum) >> shgSum) - src.Pix[yi+2] = uint8((bSum * mulSum) >> shgSum) - } else { - src.Pix[yi] = 0 - src.Pix[yi+1] = 0 - src.Pix[yi+2] = 0 - } - - rSum -= rOutSum - gSum -= gOutSum - bSum -= bOutSum - aSum -= aOutSum - - rOutSum -= stackIn.r - gOutSum -= stackIn.g - bOutSum -= stackIn.b - aOutSum -= stackIn.a - - p = x + radius + 1 - - if p > widthMinus1 { - p = widthMinus1 - } - p = (yw + p) << 2 - - stackIn.r = uint32(src.Pix[p]) - stackIn.g = uint32(src.Pix[p+1]) - stackIn.b = uint32(src.Pix[p+2]) - stackIn.a = uint32(src.Pix[p+3]) - - rInSum += stackIn.r - gInSum += stackIn.g - bInSum += stackIn.b - aInSum += stackIn.a - - rSum += rInSum - gSum += gInSum - bSum += bInSum - aSum += aInSum - - stackIn = stackIn.next - - pr = stackOut.r - pg = stackOut.g - pb = stackOut.b - pa = stackOut.a - - rOutSum += pr - gOutSum += pg - bOutSum += pb - aOutSum += pa - - rInSum -= pr - gInSum -= pg - bInSum -= pb - aInSum -= pa - - stackOut = stackOut.next - - yi += 4 - } - yw += width - } - - for x = 0; x < width; x++ { - rInSum, gInSum, bInSum, aInSum, rSum, gSum, bSum, aSum = 0, 0, 0, 0, 0, 0, 0, 0 - - yi = x << 2 - pr = uint32(src.Pix[yi]) - pg = uint32(src.Pix[yi+1]) - pb = uint32(src.Pix[yi+2]) - pa = uint32(src.Pix[yi+3]) - - rOutSum = radiusPlus1 * pr - gOutSum = radiusPlus1 * pg - bOutSum = radiusPlus1 * pb - aOutSum = radiusPlus1 * pa - - rSum += sumFactor * pr - gSum += sumFactor * pg - bSum += sumFactor * pb - aSum += sumFactor * pa - - stack = stackStart - - for i = 0; i < radiusPlus1; i++ { - stack.r = pr - stack.g = pg - stack.b = pb - stack.a = pa - stack = stack.next - } - - yp = width - - for i = 1; i <= radius; i++ { - yi = (yp + x) << 2 - pr = uint32(src.Pix[yi]) - pg = uint32(src.Pix[yi+1]) - pb = uint32(src.Pix[yi+2]) - pa = uint32(src.Pix[yi+3]) - - stack.r = pr - stack.g = pg - stack.b = pb - stack.a = pa - - rSum += stack.r * (radiusPlus1 - i) - gSum += stack.g * (radiusPlus1 - i) - bSum += stack.b * (radiusPlus1 - i) - aSum += stack.a * (radiusPlus1 - i) - - rInSum += pr - gInSum += pg - bInSum += pb - aInSum += pa - - stack = stack.next - - if i < heightMinus1 { - yp += width - } - } - - yi = x - stackIn = stackStart - stackOut = stackEnd - - for y = 0; y < height; y++ { - p = yi << 2 - pa = (aSum * mulSum) >> shgSum - src.Pix[p+3] = uint8(pa) - - if pa > 0 { - src.Pix[p] = uint8((rSum * mulSum) >> shgSum) - src.Pix[p+1] = uint8((gSum * mulSum) >> shgSum) - src.Pix[p+2] = uint8((bSum * mulSum) >> shgSum) - } else { - src.Pix[p] = 0 - src.Pix[p+1] = 0 - src.Pix[p+2] = 0 - } - - rSum -= rOutSum - gSum -= gOutSum - bSum -= bOutSum - aSum -= aOutSum - - rOutSum -= stackIn.r - gOutSum -= stackIn.g - bOutSum -= stackIn.b - aOutSum -= stackIn.a - - p = y + radiusPlus1 - - if p > heightMinus1 { - p = heightMinus1 - } - p = (x + (p * width)) << 2 - - stackIn.r = uint32(src.Pix[p]) - stackIn.g = uint32(src.Pix[p+1]) - stackIn.b = uint32(src.Pix[p+2]) - stackIn.a = uint32(src.Pix[p+3]) - - rInSum += stackIn.r - gInSum += stackIn.g - bInSum += stackIn.b - aInSum += stackIn.a - - rSum += rInSum - gSum += gInSum - bSum += bInSum - aSum += aInSum - - stackIn = stackIn.next - - pr = stackOut.r - pg = stackOut.g - pb = stackOut.b - pa = stackOut.a - - rOutSum += pr - gOutSum += pg - bOutSum += pb - aOutSum += pa - - rInSum -= pr - gInSum -= pg - bInSum -= pb - aInSum -= pa - - stackOut = stackOut.next - - yi += width - } - } -} +// Go implementation of the StackBlur algorithm +// http://incubator.quasimondo.com/processing/fast_blur_deluxe.php + +package image + +import ( + "errors" + "image" + "image/color" +) + +// blurStack is a linked list containing the color value and a pointer to the next struct. +type blurStack struct { + r, g, b, a uint32 + next *blurStack +} + +var mulTable = []uint32{ + 512, 512, 456, 512, 328, 456, 335, 512, 405, 328, 271, 456, 388, 335, 292, 512, + 454, 405, 364, 328, 298, 271, 496, 456, 420, 388, 360, 335, 312, 292, 273, 512, + 482, 454, 428, 405, 383, 364, 345, 328, 312, 298, 284, 271, 259, 496, 475, 456, + 437, 420, 404, 388, 374, 360, 347, 335, 323, 312, 302, 292, 282, 273, 265, 512, + 497, 482, 468, 454, 441, 428, 417, 405, 394, 383, 373, 364, 354, 345, 337, 328, + 320, 312, 305, 298, 291, 284, 278, 271, 265, 259, 507, 496, 485, 475, 465, 456, + 446, 437, 428, 420, 412, 404, 396, 388, 381, 374, 367, 360, 354, 347, 341, 335, + 329, 323, 318, 312, 307, 302, 297, 292, 287, 282, 278, 273, 269, 265, 261, 512, + 505, 497, 489, 482, 475, 468, 461, 454, 447, 441, 435, 428, 422, 417, 411, 405, + 399, 394, 389, 383, 378, 373, 368, 364, 359, 354, 350, 345, 341, 337, 332, 328, + 324, 320, 316, 312, 309, 305, 301, 298, 294, 291, 287, 284, 281, 278, 274, 271, + 268, 265, 262, 259, 257, 507, 501, 496, 491, 485, 480, 475, 470, 465, 460, 456, + 451, 446, 442, 437, 433, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388, + 385, 381, 377, 374, 370, 367, 363, 360, 357, 354, 350, 347, 344, 341, 338, 335, + 332, 329, 326, 323, 320, 318, 315, 312, 310, 307, 304, 302, 299, 297, 294, 292, + 289, 287, 285, 282, 280, 278, 275, 273, 271, 269, 267, 265, 263, 261, 259, +} + +var shgTable = []uint32{ + 9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, + 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, +} + +// Stackblur takes the source image and returns it's blurred version by applying the blur radius defined as parameter. The destination image must be a image.NRGBA. +func Stackblur(dst, src image.Image, radius uint32) error { + // Limit the maximum blur radius to 255 to avoid overflowing the multable. + if int(radius) >= len(mulTable) { + radius = uint32(len(mulTable) - 1) + } + + if radius < 1 { + return errors.New("blur radius must be greater than 0") + } + + img, ok := dst.(*image.NRGBA) + if !ok { + return errors.New("the destination image must be image.NRGBA") + } + + process(img, src, radius) + return nil +} + +func process(dst *image.NRGBA, src image.Image, radius uint32) { + srcBounds := src.Bounds() + srcMinX := srcBounds.Min.X + srcMinY := srcBounds.Min.Y + + dstBounds := srcBounds.Sub(srcBounds.Min) + dstW := dstBounds.Dx() + dstH := dstBounds.Dy() + + switch src0 := src.(type) { + case *image.NRGBA: + rowSize := srcBounds.Dx() * 4 + for dstY := 0; dstY < dstH; dstY++ { + di := src0.PixOffset(0, dstY) + si := src0.PixOffset(srcMinX, srcMinY+dstY) + for dstX := 0; dstX < dstW; dstX++ { + copy(dst.Pix[di:di+rowSize], src0.Pix[si:si+rowSize]) + } + } + case *image.YCbCr: + for dstY := 0; dstY < dstH; dstY++ { + di := dst.PixOffset(0, dstY) + for dstX := 0; dstX < dstW; dstX++ { + srcX := srcMinX + dstX + srcY := srcMinY + dstY + siy := src0.YOffset(srcX, srcY) + sic := src0.COffset(srcX, srcY) + r, g, b := color.YCbCrToRGB(src0.Y[siy], src0.Cb[sic], src0.Cr[sic]) + dst.Pix[di+0] = r + dst.Pix[di+1] = g + dst.Pix[di+2] = b + dst.Pix[di+3] = 0xff + di += 4 + } + } + default: + for dstY := 0; dstY < dstH; dstY++ { + di := dst.PixOffset(0, dstY) + for dstX := 0; dstX < dstW; dstX++ { + c := color.NRGBAModel.Convert(src.At(srcMinX+dstX, srcMinY+dstY)).(color.NRGBA) + dst.Pix[di+0] = c.R + dst.Pix[di+1] = c.G + dst.Pix[di+2] = c.B + dst.Pix[di+3] = c.A + di += 4 + } + } + } + + blurImage(dst, radius) +} + +func blurImage(src *image.NRGBA, radius uint32) { + var ( + stackEnd *blurStack + stackIn *blurStack + stackOut *blurStack + ) + + var width, height = uint32(src.Bounds().Dx()), uint32(src.Bounds().Dy()) + var ( + div, widthMinus1, heightMinus1, radiusPlus1, sumFactor uint32 + x, y, i, p, yp, yi, yw, + rSum, gSum, bSum, aSum, + rOutSum, gOutSum, bOutSum, aOutSum, + rInSum, gInSum, bInSum, aInSum, + pr, pg, pb, pa uint32 + ) + + div = radius + radius + 1 + widthMinus1 = width - 1 + heightMinus1 = height - 1 + radiusPlus1 = radius + 1 + sumFactor = radiusPlus1 * (radiusPlus1 + 1) / 2 + + stackStart := new(blurStack) + stack := stackStart + + for i = 1; i < div; i++ { + stack.next = new(blurStack) + stack = stack.next + if i == radiusPlus1 { + stackEnd = stack + } + } + stack.next = stackStart + + mulSum := mulTable[radius] + shgSum := shgTable[radius] + + for y = 0; y < height; y++ { + rInSum, gInSum, bInSum, aInSum, rSum, gSum, bSum, aSum = 0, 0, 0, 0, 0, 0, 0, 0 + + pr = uint32(src.Pix[yi]) + pg = uint32(src.Pix[yi+1]) + pb = uint32(src.Pix[yi+2]) + pa = uint32(src.Pix[yi+3]) + + rOutSum = radiusPlus1 * pr + gOutSum = radiusPlus1 * pg + bOutSum = radiusPlus1 * pb + aOutSum = radiusPlus1 * pa + + rSum += sumFactor * pr + gSum += sumFactor * pg + bSum += sumFactor * pb + aSum += sumFactor * pa + + stack = stackStart + + for i = 0; i < radiusPlus1; i++ { + stack.r = pr + stack.g = pg + stack.b = pb + stack.a = pa + stack = stack.next + } + + for i = 1; i < radiusPlus1; i++ { + var diff uint32 + if widthMinus1 < i { + diff = widthMinus1 + } else { + diff = i + } + p = yi + (diff << 2) + pr = uint32(src.Pix[p]) + pg = uint32(src.Pix[p+1]) + pb = uint32(src.Pix[p+2]) + pa = uint32(src.Pix[p+3]) + + stack.r = pr + stack.g = pg + stack.b = pb + stack.a = pa + + rSum += stack.r * (radiusPlus1 - i) + gSum += stack.g * (radiusPlus1 - i) + bSum += stack.b * (radiusPlus1 - i) + aSum += stack.a * (radiusPlus1 - i) + + rInSum += pr + gInSum += pg + bInSum += pb + aInSum += pa + + stack = stack.next + } + stackIn = stackStart + stackOut = stackEnd + + for x = 0; x < width; x++ { + pa = (aSum * mulSum) >> shgSum + src.Pix[yi+3] = uint8(pa) + + if pa != 0 { + src.Pix[yi] = uint8((rSum * mulSum) >> shgSum) + src.Pix[yi+1] = uint8((gSum * mulSum) >> shgSum) + src.Pix[yi+2] = uint8((bSum * mulSum) >> shgSum) + } else { + src.Pix[yi] = 0 + src.Pix[yi+1] = 0 + src.Pix[yi+2] = 0 + } + + rSum -= rOutSum + gSum -= gOutSum + bSum -= bOutSum + aSum -= aOutSum + + rOutSum -= stackIn.r + gOutSum -= stackIn.g + bOutSum -= stackIn.b + aOutSum -= stackIn.a + + p = x + radius + 1 + + if p > widthMinus1 { + p = widthMinus1 + } + p = (yw + p) << 2 + + stackIn.r = uint32(src.Pix[p]) + stackIn.g = uint32(src.Pix[p+1]) + stackIn.b = uint32(src.Pix[p+2]) + stackIn.a = uint32(src.Pix[p+3]) + + rInSum += stackIn.r + gInSum += stackIn.g + bInSum += stackIn.b + aInSum += stackIn.a + + rSum += rInSum + gSum += gInSum + bSum += bInSum + aSum += aInSum + + stackIn = stackIn.next + + pr = stackOut.r + pg = stackOut.g + pb = stackOut.b + pa = stackOut.a + + rOutSum += pr + gOutSum += pg + bOutSum += pb + aOutSum += pa + + rInSum -= pr + gInSum -= pg + bInSum -= pb + aInSum -= pa + + stackOut = stackOut.next + + yi += 4 + } + yw += width + } + + for x = 0; x < width; x++ { + rInSum, gInSum, bInSum, aInSum, rSum, gSum, bSum, aSum = 0, 0, 0, 0, 0, 0, 0, 0 + + yi = x << 2 + pr = uint32(src.Pix[yi]) + pg = uint32(src.Pix[yi+1]) + pb = uint32(src.Pix[yi+2]) + pa = uint32(src.Pix[yi+3]) + + rOutSum = radiusPlus1 * pr + gOutSum = radiusPlus1 * pg + bOutSum = radiusPlus1 * pb + aOutSum = radiusPlus1 * pa + + rSum += sumFactor * pr + gSum += sumFactor * pg + bSum += sumFactor * pb + aSum += sumFactor * pa + + stack = stackStart + + for i = 0; i < radiusPlus1; i++ { + stack.r = pr + stack.g = pg + stack.b = pb + stack.a = pa + stack = stack.next + } + + yp = width + + for i = 1; i <= radius; i++ { + yi = (yp + x) << 2 + pr = uint32(src.Pix[yi]) + pg = uint32(src.Pix[yi+1]) + pb = uint32(src.Pix[yi+2]) + pa = uint32(src.Pix[yi+3]) + + stack.r = pr + stack.g = pg + stack.b = pb + stack.a = pa + + rSum += stack.r * (radiusPlus1 - i) + gSum += stack.g * (radiusPlus1 - i) + bSum += stack.b * (radiusPlus1 - i) + aSum += stack.a * (radiusPlus1 - i) + + rInSum += pr + gInSum += pg + bInSum += pb + aInSum += pa + + stack = stack.next + + if i < heightMinus1 { + yp += width + } + } + + yi = x + stackIn = stackStart + stackOut = stackEnd + + for y = 0; y < height; y++ { + p = yi << 2 + pa = (aSum * mulSum) >> shgSum + src.Pix[p+3] = uint8(pa) + + if pa > 0 { + src.Pix[p] = uint8((rSum * mulSum) >> shgSum) + src.Pix[p+1] = uint8((gSum * mulSum) >> shgSum) + src.Pix[p+2] = uint8((bSum * mulSum) >> shgSum) + } else { + src.Pix[p] = 0 + src.Pix[p+1] = 0 + src.Pix[p+2] = 0 + } + + rSum -= rOutSum + gSum -= gOutSum + bSum -= bOutSum + aSum -= aOutSum + + rOutSum -= stackIn.r + gOutSum -= stackIn.g + bOutSum -= stackIn.b + aOutSum -= stackIn.a + + p = y + radiusPlus1 + + if p > heightMinus1 { + p = heightMinus1 + } + p = (x + (p * width)) << 2 + + stackIn.r = uint32(src.Pix[p]) + stackIn.g = uint32(src.Pix[p+1]) + stackIn.b = uint32(src.Pix[p+2]) + stackIn.a = uint32(src.Pix[p+3]) + + rInSum += stackIn.r + gInSum += stackIn.g + bInSum += stackIn.b + aInSum += stackIn.a + + rSum += rInSum + gSum += gInSum + bSum += bInSum + aSum += aInSum + + stackIn = stackIn.next + + pr = stackOut.r + pg = stackOut.g + pb = stackOut.b + pa = stackOut.a + + rOutSum += pr + gOutSum += pg + bOutSum += pb + aOutSum += pa + + rInSum -= pr + gInSum -= pg + bInSum -= pb + aInSum -= pa + + stackOut = stackOut.next + + yi += width + } + } +} diff --git a/imop/comp.go b/imop/comp.go index 268ffefd..f2aee309 100644 --- a/imop/comp.go +++ b/imop/comp.go @@ -1,472 +1,472 @@ -// Package imop implements the Porter-Duff composition operations -// used for mixing a graphic element with its backdrop. -// Porter and Duff presented in their paper 12 different composition operation, but the -// core image/draw core package implements only the source-over-destination and source. -// This package implements all of the 12 composite operation together with some blending modes. -package imop - -import ( - "fmt" - "image" - "image/color" - "math" - - "github.com/esimov/caire/utils" -) - -type CompType int - -const ( - Clear CompType = iota - Copy - Dst - SrcOver - DstOver - SrcIn - DstIn - SrcOut - DstOut - SrcAtop - DstAtop - Xor -) - -// Bitmap holds an image type as a placeholder for the Porter-Duff composition -// operations which can be used as a source or destination image. -type Bitmap struct { - Img *image.NRGBA -} - -// Composite struct contains the currently active composition operation and all the supported operations. -type Composite struct { - CurrentOp CompType - Ops []CompType -} - -// NewBitmap initializes a new Bitmap. -func NewBitmap(rect image.Rectangle) *Bitmap { - return &Bitmap{ - Img: image.NewNRGBA(rect), - } -} - -// InitOp initializes a new composition operation. -func InitOp() *Composite { - return &Composite{ - CurrentOp: SrcOver, - Ops: []CompType{ - Clear, - Copy, - Dst, - SrcOver, - DstOver, - SrcIn, - DstIn, - SrcOut, - DstOut, - SrcAtop, - DstAtop, - Xor, - }, - } -} - -// Set changes the current composition operation. -func (op *Composite) Set(compType CompType) error { - if utils.Contains(op.Ops, compType) { - op.CurrentOp = compType - return nil - } - - return fmt.Errorf("unsupported composition operation") -} - -// Set changes the current composition operation. -func (op *Composite) Get() CompType { - return op.CurrentOp -} - -// Draw applies the currently active Ported-Duff composition operation formula, -// taking as parameter the source and the destination image and draws the result into the bitmap. -// If a blend mode is activated it will plug in the alpha blending formula also into the equation. -func (op *Composite) Draw(bitmap *Bitmap, src, dst *image.NRGBA, blend *Blend) { - dx, dy := src.Bounds().Dx(), src.Bounds().Dy() - - var ( - r, g, b, a uint32 - rn, gn, bn, an float64 - ) - - for x := 0; x < dx; x++ { - for y := 0; y < dy; y++ { - r1, g1, b1, a1 := src.At(x, y).RGBA() - r2, g2, b2, a2 := dst.At(x, y).RGBA() - - rs, gs, bs, as := r1>>8, g1>>8, b1>>8, a1>>8 - rb, gb, bb, ab := r2>>8, g2>>8, b2>>8, a2>>8 - - // normalize the values. - rsn := float64(rs) / 255 - gsn := float64(gs) / 255 - bsn := float64(bs) / 255 - asn := float64(as) / 255 - - rbn := float64(rb) / 255 - gbn := float64(gb) / 255 - bbn := float64(bb) / 255 - abn := float64(ab) / 255 - - // applying the alpha composition formula - switch op.CurrentOp { - case Clear: - rn, gn, bn, an = 0, 0, 0, 0 - case Copy: - rn = asn * rsn - gn = asn * gsn - bn = asn * bsn - an = asn * asn - case Dst: - rn = abn * rbn - gn = abn * gbn - bn = abn * bbn - an = abn * abn - case SrcOver: - rn = asn*rsn + abn*rbn*(1-asn) - gn = asn*gsn + abn*gbn*(1-asn) - bn = asn*bsn + abn*bbn*(1-asn) - an = asn + abn*(1-asn) - case DstOver: - rn = asn*rsn*(1-abn) + abn*rbn - gn = asn*gsn*(1-abn) + abn*gbn - bn = asn*bsn*(1-abn) + abn*bbn - an = asn*(1-abn) + abn - case SrcIn: - rn = asn * rsn * abn - gn = asn * gsn * abn - bn = asn * bsn * abn - an = asn * abn - case DstIn: - rn = abn * rbn * asn - gn = abn * gbn * asn - bn = abn * bbn * asn - an = abn * asn - case SrcOut: - rn = asn * rsn * (1 - abn) - gn = asn * gsn * (1 - abn) - bn = asn * bsn * (1 - abn) - an = asn * (1 - abn) - case DstOut: - rn = abn * rbn * (1 - asn) - gn = abn * gbn * (1 - asn) - bn = abn * bbn * (1 - asn) - an = abn * (1 - asn) - case SrcAtop: - rn = asn*rsn*abn + (1-asn)*abn*rbn - gn = asn*gsn*abn + (1-asn)*abn*gbn - bn = asn*bsn*abn + (1-asn)*abn*bbn - an = asn*abn + abn*(1-asn) - case DstAtop: - rn = asn*rsn*(1-abn) + abn*rbn*asn - gn = asn*gsn*(1-abn) + abn*gbn*asn - bn = asn*bsn*(1-abn) + abn*bbn*asn - an = asn*(1-abn) + abn*asn - case Xor: - rn = asn*rsn*(1-abn) + abn*rbn*(1-asn) - gn = asn*gsn*(1-abn) + abn*gbn*(1-asn) - bn = asn*bsn*(1-abn) + abn*bbn*(1-asn) - an = asn*(1-abn) + abn*(1-asn) - } - - r = uint32(rn * 255) - g = uint32(gn * 255) - b = uint32(bn * 255) - a = uint32(an * 255) - - bitmap.Img.Set(x, y, color.NRGBA{ - R: uint8(r), - G: uint8(g), - B: uint8(b), - A: uint8(a), - }) - - // applying the blending mode - if blend != nil { - rn, gn, bn, an = 0, 0, 0, 0 // reset the colors - r1, g1, b1, a1 = src.At(x, y).RGBA() - r2, g2, b2, a2 = dst.At(x, y).RGBA() - - rs, gs, bs, as = r1>>8, g1>>8, b1>>8, a1>>8 - rb, gb, bb, ab = r2>>8, g2>>8, b2>>8, a2>>8 - - rsn = float64(rs) / 255 - gsn = float64(gs) / 255 - bsn = float64(bs) / 255 - asn = float64(as) / 255 - - rbn = float64(rb) / 255 - gbn = float64(gb) / 255 - bbn = float64(bb) / 255 - abn = float64(ab) / 255 - - foreground := Color{R: rsn, G: gsn, B: bsn} - background := Color{R: rbn, G: gbn, B: bbn} - - switch blend.CurrentOp { - case Normal: - rn, gn, bn, an = rsn, gsn, bsn, asn - case Darken: - rn = utils.Min(rsn, rbn) - gn = utils.Min(gsn, gbn) - bn = utils.Min(bsn, bbn) - an = utils.Min(asn, abn) - case Lighten: - rn = utils.Max(rsn, rbn) - gn = utils.Max(gsn, gbn) - bn = utils.Max(bsn, bbn) - an = utils.Max(asn, abn) - case Screen: - rn = 1 - (1-rsn)*(1-rbn) - gn = 1 - (1-gsn)*(1-gbn) - bn = 1 - (1-bsn)*(1-bbn) - an = 1 - (1-asn)*(1-abn) - case Multiply: - rn = rsn * rbn - gn = gsn * gbn - bn = bsn * bbn - an = asn * abn - case Overlay: - if rsn <= 0.5 { - rn = 2 * rsn * rbn - } else { - rn = 1 - 2*(1-rsn)*(1-rbn) - } - - if gsn <= 0.5 { - gn = 2 * gsn * gbn - } else { - gn = 1 - 2*(1-gsn)*(1-gbn) - } - - if bsn <= 0.5 { - bn = 2 * bsn * bbn - } else { - bn = 1 - 2*(1-bsn)*(1-bbn) - } - - if asn <= 0.5 { - an = 2 * asn * abn - } else { - an = 1 - 2*(1-asn)*(1-abn) - } - case SoftLight: - if rbn < 0.5 { - rn = rsn - (1-2*rbn)*rsn*(1-rsn) - } else { - var w3r float64 - if rsn < 0.25 { - w3r = ((16*rsn-12)*rsn + 4) * rsn - } else { - w3r = math.Sqrt(rsn) - } - rn = rsn + (2*rbn-1)*(w3r-rsn) - } - - if gbn < 0.5 { - gn = gsn - (1-2*gbn)*gsn*(1-gsn) - } else { - var w3g float64 - if gsn < 0.25 { - w3g = ((16*gsn-12)*gsn + 4) * gsn - } else { - w3g = math.Sqrt(gsn) - } - gn = gsn + (2*gbn-1)*(w3g-gsn) - } - - if bbn < 0.5 { - bn = bsn - (1-2*bbn)*bsn*(1-bsn) - } else { - var w3b float64 - if bsn < 0.25 { - w3b = ((16*bsn-12)*bsn + 4) * bsn - } else { - w3b = math.Sqrt(bsn) - } - bn = bsn + (2*bbn-1)*(w3b-bsn) - } - - if abn < 0.5 { - an = asn - (1-2*abn)*asn*(1-asn) - } else { - var w3a float64 - if asn < 0.25 { - w3a = ((16*asn-12)*asn + 4) * asn - } else { - w3a = math.Sqrt(asn) - } - an = asn + (2*abn-1)*(w3a-asn) - } - case HardLight: - if rbn < 0.5 { - rn = rbn - (1-2*rsn)*rbn*(1-rbn) - } else { - var w3r float64 - if rbn < 0.25 { - w3r = ((16*rbn-12)*rbn + 4) * rbn - } else { - w3r = math.Sqrt(rbn) - } - rn = rbn + (2*rsn-1)*(w3r-rbn) - } - - if gbn < 0.5 { - gn = gbn - (1-2*gsn)*gbn*(1-gbn) - } else { - var w3g float64 - if gbn < 0.25 { - w3g = ((16*gbn-12)*gbn + 4) * gbn - } else { - w3g = math.Sqrt(gbn) - } - gn = gbn + (2*gsn-1)*(w3g-gbn) - } - - if bbn < 0.5 { - bn = bbn - (1-2*bsn)*bbn*(1-bbn) - } else { - var w3b float64 - if bbn < 0.25 { - w3b = ((16*bbn-12)*bbn + 4) * bbn - } else { - w3b = math.Sqrt(bbn) - } - bn = bbn + (2*bsn-1)*(w3b-bbn) - } - - if abn < 0.5 { - an = abn - (1-2*asn)*abn*(1-abn) - } else { - var w3a float64 - if abn < 0.25 { - w3a = ((16*abn-12)*abn + 4) * abn - } else { - w3a = math.Sqrt(abn) - } - an = abn + (2*asn-1)*(w3a-abn) - } - case ColorDodge: - if rsn < 1 { - rn = utils.Min(1, rbn/(1-rsn)) - } else if rsn == 1 { - rn = 1 - } - - if gsn < 1 { - gn = utils.Min(1, gbn/(1-gsn)) - } else if gsn == 1 { - gn = 1 - } - - if bsn < 1 { - bn = utils.Min(1, bbn/(1-bsn)) - } else if bsn == 1 { - bn = 1 - } - - if asn < 1 { - an = utils.Min(1, abn/(1-asn)) - } else if asn == 1 { - an = 1 - } - case ColorBurn: - if rsn > 0 { - rn = 1 - utils.Min(1, (1-rbn)/rsn) - } else if rsn == 0 { - rn = 0 - } - - if gsn > 0 { - gn = 1 - utils.Min(1, (1-gbn)/gsn) - } else if gsn == 0 { - gn = 0 - } - - if bsn > 0 { - bn = 1 - utils.Min(1, (1-bbn)/bsn) - } else if bsn == 0 { - bn = 0 - } - - if asn > 0 { - an = 1 - utils.Min(1, (1-abn)/asn) - } else if asn == 0 { - an = 0 - } - case Difference: - rn = utils.Abs(rbn - rsn) - gn = utils.Abs(gbn - gsn) - bn = utils.Abs(bbn - bsn) - an = 1 - case Exclusion: - rn = rsn + rbn - 2*rsn*rbn - gn = gsn + gbn - 2*gsn*gbn - bn = bsn + bbn - 2*bsn*bbn - an = 1 - - // Non-separable blend modes - // https://www.w3.org/TR/compositing-1/#blendingnonseparable - case Hue: - sat := blend.SetSat(background, blend.Sat(foreground)) - rgb := blend.SetLum(sat, blend.Lum(foreground)) - - a := asn + abn - asn*abn - rn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255) - gn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255) - bn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255) - rn, gn, bn = rn/255, gn/255, bn/255 - an = a - case Saturation: - sat := blend.SetSat(foreground, blend.Sat(background)) - rgb := blend.SetLum(sat, blend.Lum(foreground)) - - a := asn + abn - asn*abn - rn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255) - gn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255) - bn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255) - rn, gn, bn = rn/255, gn/255, bn/255 - an = a - case ColorMode: - rgb := blend.SetLum(background, blend.Lum(foreground)) - - a := asn + abn - asn*abn - rn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255) - gn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255) - bn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255) - rn, gn, bn = rn/255, gn/255, bn/255 - an = a - case Luminosity: - rgb := blend.SetLum(foreground, blend.Lum(background)) - - a := asn + abn - asn*abn - rn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255) - gn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255) - bn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255) - rn, gn, bn = rn/255, gn/255, bn/255 - an = a - } - - r = uint32(rn * 255) - g = uint32(gn * 255) - b = uint32(bn * 255) - a = uint32(an * 255) - - bitmap.Img.Set(x, y, color.NRGBA{ - R: uint8(r), - G: uint8(g), - B: uint8(b), - A: uint8(a), - }) - } - } - } -} +// Package imop implements the Porter-Duff composition operations +// used for mixing a graphic element with its backdrop. +// Porter and Duff presented in their paper 12 different composition operation, but the +// core image/draw core package implements only the source-over-destination and source. +// This package implements all of the 12 composite operation together with some blending modes. +package imop + +import ( + "fmt" + "image" + "image/color" + "math" + + "github.com/esimov/caire/utils" +) + +type CompType int + +const ( + Clear CompType = iota + Copy + Dst + SrcOver + DstOver + SrcIn + DstIn + SrcOut + DstOut + SrcAtop + DstAtop + Xor +) + +// Bitmap holds an image type as a placeholder for the Porter-Duff composition +// operations which can be used as a source or destination image. +type Bitmap struct { + Img *image.NRGBA +} + +// Composite struct contains the currently active composition operation and all the supported operations. +type Composite struct { + CurrentOp CompType + Ops []CompType +} + +// NewBitmap initializes a new Bitmap. +func NewBitmap(rect image.Rectangle) *Bitmap { + return &Bitmap{ + Img: image.NewNRGBA(rect), + } +} + +// InitOp initializes a new composition operation. +func InitOp() *Composite { + return &Composite{ + CurrentOp: SrcOver, + Ops: []CompType{ + Clear, + Copy, + Dst, + SrcOver, + DstOver, + SrcIn, + DstIn, + SrcOut, + DstOut, + SrcAtop, + DstAtop, + Xor, + }, + } +} + +// Set changes the current composition operation. +func (op *Composite) Set(compType CompType) error { + if utils.Contains(op.Ops, compType) { + op.CurrentOp = compType + return nil + } + + return fmt.Errorf("unsupported composition operation") +} + +// Set changes the current composition operation. +func (op *Composite) Get() CompType { + return op.CurrentOp +} + +// Draw applies the currently active Ported-Duff composition operation formula, +// taking as parameter the source and the destination image and draws the result into the bitmap. +// If a blend mode is activated it will plug in the alpha blending formula also into the equation. +func (op *Composite) Draw(bitmap *Bitmap, src, dst image.Image, blend *Blend) { + dx, dy := src.Bounds().Dx(), src.Bounds().Dy() + + var ( + r, g, b, a uint32 + rn, gn, bn, an float64 + ) + + for x := 0; x < dx; x++ { + for y := 0; y < dy; y++ { + r1, g1, b1, a1 := src.At(x, y).RGBA() + r2, g2, b2, a2 := dst.At(x, y).RGBA() + + rs, gs, bs, as := r1>>8, g1>>8, b1>>8, a1>>8 + rb, gb, bb, ab := r2>>8, g2>>8, b2>>8, a2>>8 + + // normalize the values. + rsn := float64(rs) / 255 + gsn := float64(gs) / 255 + bsn := float64(bs) / 255 + asn := float64(as) / 255 + + rbn := float64(rb) / 255 + gbn := float64(gb) / 255 + bbn := float64(bb) / 255 + abn := float64(ab) / 255 + + // applying the alpha composition formula + switch op.CurrentOp { + case Clear: + rn, gn, bn, an = 0, 0, 0, 0 + case Copy: + rn = asn * rsn + gn = asn * gsn + bn = asn * bsn + an = asn * asn + case Dst: + rn = abn * rbn + gn = abn * gbn + bn = abn * bbn + an = abn * abn + case SrcOver: + rn = asn*rsn + abn*rbn*(1-asn) + gn = asn*gsn + abn*gbn*(1-asn) + bn = asn*bsn + abn*bbn*(1-asn) + an = asn + abn*(1-asn) + case DstOver: + rn = asn*rsn*(1-abn) + abn*rbn + gn = asn*gsn*(1-abn) + abn*gbn + bn = asn*bsn*(1-abn) + abn*bbn + an = asn*(1-abn) + abn + case SrcIn: + rn = asn * rsn * abn + gn = asn * gsn * abn + bn = asn * bsn * abn + an = asn * abn + case DstIn: + rn = abn * rbn * asn + gn = abn * gbn * asn + bn = abn * bbn * asn + an = abn * asn + case SrcOut: + rn = asn * rsn * (1 - abn) + gn = asn * gsn * (1 - abn) + bn = asn * bsn * (1 - abn) + an = asn * (1 - abn) + case DstOut: + rn = abn * rbn * (1 - asn) + gn = abn * gbn * (1 - asn) + bn = abn * bbn * (1 - asn) + an = abn * (1 - asn) + case SrcAtop: + rn = asn*rsn*abn + (1-asn)*abn*rbn + gn = asn*gsn*abn + (1-asn)*abn*gbn + bn = asn*bsn*abn + (1-asn)*abn*bbn + an = asn*abn + abn*(1-asn) + case DstAtop: + rn = asn*rsn*(1-abn) + abn*rbn*asn + gn = asn*gsn*(1-abn) + abn*gbn*asn + bn = asn*bsn*(1-abn) + abn*bbn*asn + an = asn*(1-abn) + abn*asn + case Xor: + rn = asn*rsn*(1-abn) + abn*rbn*(1-asn) + gn = asn*gsn*(1-abn) + abn*gbn*(1-asn) + bn = asn*bsn*(1-abn) + abn*bbn*(1-asn) + an = asn*(1-abn) + abn*(1-asn) + } + + r = uint32(rn * 255) + g = uint32(gn * 255) + b = uint32(bn * 255) + a = uint32(an * 255) + + bitmap.Img.Set(x, y, color.NRGBA{ + R: uint8(r), + G: uint8(g), + B: uint8(b), + A: uint8(a), + }) + + // applying the blending mode + if blend != nil { + rn, gn, bn, an = 0, 0, 0, 0 // reset the colors + r1, g1, b1, a1 = src.At(x, y).RGBA() + r2, g2, b2, a2 = dst.At(x, y).RGBA() + + rs, gs, bs, as = r1>>8, g1>>8, b1>>8, a1>>8 + rb, gb, bb, ab = r2>>8, g2>>8, b2>>8, a2>>8 + + rsn = float64(rs) / 255 + gsn = float64(gs) / 255 + bsn = float64(bs) / 255 + asn = float64(as) / 255 + + rbn = float64(rb) / 255 + gbn = float64(gb) / 255 + bbn = float64(bb) / 255 + abn = float64(ab) / 255 + + foreground := Color{R: rsn, G: gsn, B: bsn} + background := Color{R: rbn, G: gbn, B: bbn} + + switch blend.CurrentOp { + case Normal: + rn, gn, bn, an = rsn, gsn, bsn, asn + case Darken: + rn = utils.Min(rsn, rbn) + gn = utils.Min(gsn, gbn) + bn = utils.Min(bsn, bbn) + an = utils.Min(asn, abn) + case Lighten: + rn = utils.Max(rsn, rbn) + gn = utils.Max(gsn, gbn) + bn = utils.Max(bsn, bbn) + an = utils.Max(asn, abn) + case Screen: + rn = 1 - (1-rsn)*(1-rbn) + gn = 1 - (1-gsn)*(1-gbn) + bn = 1 - (1-bsn)*(1-bbn) + an = 1 - (1-asn)*(1-abn) + case Multiply: + rn = rsn * rbn + gn = gsn * gbn + bn = bsn * bbn + an = asn * abn + case Overlay: + if rsn <= 0.5 { + rn = 2 * rsn * rbn + } else { + rn = 1 - 2*(1-rsn)*(1-rbn) + } + + if gsn <= 0.5 { + gn = 2 * gsn * gbn + } else { + gn = 1 - 2*(1-gsn)*(1-gbn) + } + + if bsn <= 0.5 { + bn = 2 * bsn * bbn + } else { + bn = 1 - 2*(1-bsn)*(1-bbn) + } + + if asn <= 0.5 { + an = 2 * asn * abn + } else { + an = 1 - 2*(1-asn)*(1-abn) + } + case SoftLight: + if rbn < 0.5 { + rn = rsn - (1-2*rbn)*rsn*(1-rsn) + } else { + var w3r float64 + if rsn < 0.25 { + w3r = ((16*rsn-12)*rsn + 4) * rsn + } else { + w3r = math.Sqrt(rsn) + } + rn = rsn + (2*rbn-1)*(w3r-rsn) + } + + if gbn < 0.5 { + gn = gsn - (1-2*gbn)*gsn*(1-gsn) + } else { + var w3g float64 + if gsn < 0.25 { + w3g = ((16*gsn-12)*gsn + 4) * gsn + } else { + w3g = math.Sqrt(gsn) + } + gn = gsn + (2*gbn-1)*(w3g-gsn) + } + + if bbn < 0.5 { + bn = bsn - (1-2*bbn)*bsn*(1-bsn) + } else { + var w3b float64 + if bsn < 0.25 { + w3b = ((16*bsn-12)*bsn + 4) * bsn + } else { + w3b = math.Sqrt(bsn) + } + bn = bsn + (2*bbn-1)*(w3b-bsn) + } + + if abn < 0.5 { + an = asn - (1-2*abn)*asn*(1-asn) + } else { + var w3a float64 + if asn < 0.25 { + w3a = ((16*asn-12)*asn + 4) * asn + } else { + w3a = math.Sqrt(asn) + } + an = asn + (2*abn-1)*(w3a-asn) + } + case HardLight: + if rbn < 0.5 { + rn = rbn - (1-2*rsn)*rbn*(1-rbn) + } else { + var w3r float64 + if rbn < 0.25 { + w3r = ((16*rbn-12)*rbn + 4) * rbn + } else { + w3r = math.Sqrt(rbn) + } + rn = rbn + (2*rsn-1)*(w3r-rbn) + } + + if gbn < 0.5 { + gn = gbn - (1-2*gsn)*gbn*(1-gbn) + } else { + var w3g float64 + if gbn < 0.25 { + w3g = ((16*gbn-12)*gbn + 4) * gbn + } else { + w3g = math.Sqrt(gbn) + } + gn = gbn + (2*gsn-1)*(w3g-gbn) + } + + if bbn < 0.5 { + bn = bbn - (1-2*bsn)*bbn*(1-bbn) + } else { + var w3b float64 + if bbn < 0.25 { + w3b = ((16*bbn-12)*bbn + 4) * bbn + } else { + w3b = math.Sqrt(bbn) + } + bn = bbn + (2*bsn-1)*(w3b-bbn) + } + + if abn < 0.5 { + an = abn - (1-2*asn)*abn*(1-abn) + } else { + var w3a float64 + if abn < 0.25 { + w3a = ((16*abn-12)*abn + 4) * abn + } else { + w3a = math.Sqrt(abn) + } + an = abn + (2*asn-1)*(w3a-abn) + } + case ColorDodge: + if rsn < 1 { + rn = utils.Min(1, rbn/(1-rsn)) + } else if rsn == 1 { + rn = 1 + } + + if gsn < 1 { + gn = utils.Min(1, gbn/(1-gsn)) + } else if gsn == 1 { + gn = 1 + } + + if bsn < 1 { + bn = utils.Min(1, bbn/(1-bsn)) + } else if bsn == 1 { + bn = 1 + } + + if asn < 1 { + an = utils.Min(1, abn/(1-asn)) + } else if asn == 1 { + an = 1 + } + case ColorBurn: + if rsn > 0 { + rn = 1 - utils.Min(1, (1-rbn)/rsn) + } else if rsn == 0 { + rn = 0 + } + + if gsn > 0 { + gn = 1 - utils.Min(1, (1-gbn)/gsn) + } else if gsn == 0 { + gn = 0 + } + + if bsn > 0 { + bn = 1 - utils.Min(1, (1-bbn)/bsn) + } else if bsn == 0 { + bn = 0 + } + + if asn > 0 { + an = 1 - utils.Min(1, (1-abn)/asn) + } else if asn == 0 { + an = 0 + } + case Difference: + rn = utils.Abs(rbn - rsn) + gn = utils.Abs(gbn - gsn) + bn = utils.Abs(bbn - bsn) + an = 1 + case Exclusion: + rn = rsn + rbn - 2*rsn*rbn + gn = gsn + gbn - 2*gsn*gbn + bn = bsn + bbn - 2*bsn*bbn + an = 1 + + // Non-separable blend modes + // https://www.w3.org/TR/compositing-1/#blendingnonseparable + case Hue: + sat := blend.SetSat(background, blend.Sat(foreground)) + rgb := blend.SetLum(sat, blend.Lum(foreground)) + + a := asn + abn - asn*abn + rn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255) + gn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255) + bn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255) + rn, gn, bn = rn/255, gn/255, bn/255 + an = a + case Saturation: + sat := blend.SetSat(foreground, blend.Sat(background)) + rgb := blend.SetLum(sat, blend.Lum(foreground)) + + a := asn + abn - asn*abn + rn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255) + gn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255) + bn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255) + rn, gn, bn = rn/255, gn/255, bn/255 + an = a + case ColorMode: + rgb := blend.SetLum(background, blend.Lum(foreground)) + + a := asn + abn - asn*abn + rn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255) + gn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255) + bn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255) + rn, gn, bn = rn/255, gn/255, bn/255 + an = a + case Luminosity: + rgb := blend.SetLum(foreground, blend.Lum(background)) + + a := asn + abn - asn*abn + rn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255) + gn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255) + bn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255) + rn, gn, bn = rn/255, gn/255, bn/255 + an = a + } + + r = uint32(rn * 255) + g = uint32(gn * 255) + b = uint32(bn * 255) + a = uint32(an * 255) + + bitmap.Img.Set(x, y, color.NRGBA{ + R: uint8(r), + G: uint8(g), + B: uint8(b), + A: uint8(a), + }) + } + } + } +} diff --git a/imop/comp_test.go b/imop/comp_test.go index eb1f93ed..f19a871b 100644 --- a/imop/comp_test.go +++ b/imop/comp_test.go @@ -1,208 +1,208 @@ -package imop - -import ( - "image" - "image/color" - "image/draw" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestComp_Basic(t *testing.T) { - assert := assert.New(t) - - op := InitOp() - - op.Set(Clear) - assert.Equal(Clear, op.Get()) - assert.NotEqual("unsupported_composite_operation", op.Get()) - - op.Set(Dst) - assert.Equal(Dst, op.Get()) -} - -func TestComp_Ops(t *testing.T) { - assert := assert.New(t) - op := InitOp() - - transparent := color.NRGBA{R: 0, G: 0, B: 0, A: 0} - cyan := color.NRGBA{R: 33, G: 150, B: 243, A: 255} - magenta := color.NRGBA{R: 233, G: 30, B: 99, A: 255} - - rect := image.Rect(0, 0, 10, 10) - bmp := NewBitmap(rect) - source := image.NewNRGBA(rect) - backdrop := image.NewNRGBA(rect) - - // No composition operation applied. The SrcOver is the default one. - draw.Draw(source, image.Rect(0, 4, 6, 10), &image.Uniform{cyan}, image.Point{}, draw.Src) - draw.Draw(backdrop, image.Rect(4, 0, 10, 6), &image.Uniform{magenta}, image.Point{}, draw.Src) - op.Draw(bmp, source, backdrop, nil) - - // Pick three representative points/pixels from the generated image output. - // Depending on the applied composition operation the colors of the - // selected pixels should be the source color, the destination color or transparent. - topRight := bmp.Img.At(9, 0) - bottomLeft := bmp.Img.At(0, 9) - center := bmp.Img.At(5, 5) - - assert.EqualValues(topRight, magenta) - assert.EqualValues(bottomLeft, cyan) - assert.EqualValues(center, cyan) - - // Clear - op.Set(Clear) - op.Draw(bmp, source, backdrop, nil) - - topRight = bmp.Img.At(9, 0) - bottomLeft = bmp.Img.At(0, 9) - center = bmp.Img.At(5, 5) - - assert.EqualValues(topRight, transparent) - assert.EqualValues(bottomLeft, transparent) - assert.EqualValues(center, transparent) - - // Copy - op.Set(Copy) - op.Draw(bmp, source, backdrop, nil) - - topRight = bmp.Img.At(9, 0) - bottomLeft = bmp.Img.At(0, 9) - center = bmp.Img.At(5, 5) - - assert.EqualValues(topRight, transparent) - assert.EqualValues(bottomLeft, cyan) - assert.EqualValues(center, cyan) - - // Dst - op.Set(Dst) - op.Draw(bmp, source, backdrop, nil) - - topRight = bmp.Img.At(9, 0) - bottomLeft = bmp.Img.At(0, 9) - center = bmp.Img.At(5, 5) - - assert.EqualValues(topRight, magenta) - assert.EqualValues(bottomLeft, transparent) - assert.EqualValues(center, magenta) - - // SrcOver - op.Set(SrcOver) - op.Draw(bmp, source, backdrop, nil) - - topRight = bmp.Img.At(9, 0) - bottomLeft = bmp.Img.At(0, 9) - center = bmp.Img.At(5, 5) - - assert.EqualValues(topRight, magenta) - assert.EqualValues(bottomLeft, cyan) - assert.EqualValues(center, cyan) - - // DstOver - op.Set(DstOver) - op.Draw(bmp, source, backdrop, nil) - - topRight = bmp.Img.At(9, 0) - bottomLeft = bmp.Img.At(0, 9) - center = bmp.Img.At(5, 5) - - assert.EqualValues(topRight, magenta) - assert.EqualValues(bottomLeft, cyan) - assert.EqualValues(center, magenta) - - // SrcIn - op.Set(SrcIn) - op.Draw(bmp, source, backdrop, nil) - - topRight = bmp.Img.At(9, 0) - bottomLeft = bmp.Img.At(0, 9) - center = bmp.Img.At(5, 5) - - assert.EqualValues(topRight, transparent) - assert.EqualValues(bottomLeft, transparent) - assert.EqualValues(center, cyan) - - // DstIn - op.Set(DstIn) - op.Draw(bmp, source, backdrop, nil) - - topRight = bmp.Img.At(9, 0) - bottomLeft = bmp.Img.At(0, 9) - center = bmp.Img.At(5, 5) - - assert.EqualValues(topRight, transparent) - assert.EqualValues(bottomLeft, transparent) - assert.EqualValues(center, magenta) - - // SrcOut - op.Set(SrcOut) - op.Draw(bmp, source, backdrop, nil) - - topRight = bmp.Img.At(9, 0) - bottomLeft = bmp.Img.At(0, 9) - center = bmp.Img.At(5, 5) - - assert.EqualValues(topRight, transparent) - assert.EqualValues(bottomLeft, cyan) - assert.EqualValues(center, transparent) - - // DstOut - op.Set(DstOut) - op.Draw(bmp, source, backdrop, nil) - - topRight = bmp.Img.At(9, 0) - bottomLeft = bmp.Img.At(0, 9) - center = bmp.Img.At(5, 5) - - assert.EqualValues(topRight, magenta) - assert.EqualValues(bottomLeft, transparent) - assert.EqualValues(center, transparent) - - // SrcAtop - op.Set(SrcAtop) - op.Draw(bmp, source, backdrop, nil) - - topRight = bmp.Img.At(9, 0) - bottomLeft = bmp.Img.At(0, 9) - center = bmp.Img.At(5, 5) - - assert.EqualValues(topRight, magenta) - assert.EqualValues(bottomLeft, transparent) - assert.EqualValues(center, cyan) - - // DstAtop - op.Set(DstAtop) - op.Draw(bmp, source, backdrop, nil) - - topRight = bmp.Img.At(9, 0) - bottomLeft = bmp.Img.At(0, 9) - center = bmp.Img.At(5, 5) - - assert.EqualValues(topRight, transparent) - assert.EqualValues(bottomLeft, cyan) - assert.EqualValues(center, magenta) - - // Xor - op.Set(Xor) - op.Draw(bmp, source, backdrop, nil) - - topRight = bmp.Img.At(9, 0) - bottomLeft = bmp.Img.At(0, 9) - center = bmp.Img.At(5, 5) - - assert.EqualValues(topRight, magenta) - assert.EqualValues(bottomLeft, cyan) - assert.EqualValues(center, transparent) - // DstAtop - op.Set(DstAtop) - op.Draw(bmp, source, backdrop, nil) - - topRight = bmp.Img.At(9, 0) - bottomLeft = bmp.Img.At(0, 9) - center = bmp.Img.At(5, 5) - - assert.EqualValues(topRight, transparent) - assert.EqualValues(bottomLeft, cyan) - assert.EqualValues(center, magenta) -} +package imop + +import ( + "image" + "image/color" + "image/draw" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestComp_Basic(t *testing.T) { + assert := assert.New(t) + + op := InitOp() + + op.Set(Clear) + assert.Equal(Clear, op.Get()) + assert.NotEqual("unsupported_composite_operation", op.Get()) + + op.Set(Dst) + assert.Equal(Dst, op.Get()) +} + +func TestComp_Ops(t *testing.T) { + assert := assert.New(t) + op := InitOp() + + transparent := color.NRGBA{R: 0, G: 0, B: 0, A: 0} + cyan := color.NRGBA{R: 33, G: 150, B: 243, A: 255} + magenta := color.NRGBA{R: 233, G: 30, B: 99, A: 255} + + rect := image.Rect(0, 0, 10, 10) + bmp := NewBitmap(rect) + source := image.NewNRGBA(rect) + backdrop := image.NewNRGBA(rect) + + // No composition operation applied. The SrcOver is the default one. + draw.Draw(source, image.Rect(0, 4, 6, 10), &image.Uniform{cyan}, image.Point{}, draw.Src) + draw.Draw(backdrop, image.Rect(4, 0, 10, 6), &image.Uniform{magenta}, image.Point{}, draw.Src) + op.Draw(bmp, source, backdrop, nil) + + // Pick three representative points/pixels from the generated image output. + // Depending on the applied composition operation the colors of the + // selected pixels should be the source color, the destination color or transparent. + topRight := bmp.Img.At(9, 0) + bottomLeft := bmp.Img.At(0, 9) + center := bmp.Img.At(5, 5) + + assert.EqualValues(topRight, magenta) + assert.EqualValues(bottomLeft, cyan) + assert.EqualValues(center, cyan) + + // Clear + op.Set(Clear) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, transparent) + assert.EqualValues(bottomLeft, transparent) + assert.EqualValues(center, transparent) + + // Copy + op.Set(Copy) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, transparent) + assert.EqualValues(bottomLeft, cyan) + assert.EqualValues(center, cyan) + + // Dst + op.Set(Dst) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, magenta) + assert.EqualValues(bottomLeft, transparent) + assert.EqualValues(center, magenta) + + // SrcOver + op.Set(SrcOver) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, magenta) + assert.EqualValues(bottomLeft, cyan) + assert.EqualValues(center, cyan) + + // DstOver + op.Set(DstOver) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, magenta) + assert.EqualValues(bottomLeft, cyan) + assert.EqualValues(center, magenta) + + // SrcIn + op.Set(SrcIn) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, transparent) + assert.EqualValues(bottomLeft, transparent) + assert.EqualValues(center, cyan) + + // DstIn + op.Set(DstIn) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, transparent) + assert.EqualValues(bottomLeft, transparent) + assert.EqualValues(center, magenta) + + // SrcOut + op.Set(SrcOut) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, transparent) + assert.EqualValues(bottomLeft, cyan) + assert.EqualValues(center, transparent) + + // DstOut + op.Set(DstOut) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, magenta) + assert.EqualValues(bottomLeft, transparent) + assert.EqualValues(center, transparent) + + // SrcAtop + op.Set(SrcAtop) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, magenta) + assert.EqualValues(bottomLeft, transparent) + assert.EqualValues(center, cyan) + + // DstAtop + op.Set(DstAtop) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, transparent) + assert.EqualValues(bottomLeft, cyan) + assert.EqualValues(center, magenta) + + // Xor + op.Set(Xor) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, magenta) + assert.EqualValues(bottomLeft, cyan) + assert.EqualValues(center, transparent) + // DstAtop + op.Set(DstAtop) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, transparent) + assert.EqualValues(bottomLeft, cyan) + assert.EqualValues(center, magenta) +} diff --git a/preview.go b/preview.go deleted file mode 100644 index d9e84752..00000000 --- a/preview.go +++ /dev/null @@ -1,29 +0,0 @@ -package caire - -import ( - "os" -) - -// showPreview spawns a new Gio GUI window and updates its content with the resized image received from a channel. -func (p *Processor) showPreview( - imgWorker <-chan worker, - errChan chan<- error, - guiWindow struct { - width int - height int - }, -) { - var gui = NewGUI(guiWindow.width, guiWindow.height) - gui.proc = p - gui.process.worker = imgWorker - - // Run the Gio GUI app in a separate goroutine - go func() { - if err := gui.Run(); err != nil { - errChan <- err - } - // It's important to call os.Exit(0) in order to terminate - // the execution of the GUI app when pressing ESC key. - os.Exit(0) - }() -} diff --git a/processor.go b/processor.go index 27e2fd63..5602fed9 100644 --- a/processor.go +++ b/processor.go @@ -6,12 +6,18 @@ import ( "fmt" "image" "image/draw" + "image/jpeg" + "image/png" "io" "math" + "os" + "path/filepath" "github.com/disintegration/imaging" + cimage "github.com/esimov/caire/image" "github.com/esimov/caire/utils" pigo "github.com/esimov/pigo/core" + "golang.org/x/image/bmp" ) //go:embed data/facefinder @@ -19,7 +25,7 @@ var cascadeFile []byte var _ SeamCarver = (*Processor)(nil) -// worker struct contains all the information needed for transferring the resized image to the Gio GUI. +// Worker struct contains all the information needed for transferring the resized image to the Gio GUI. type worker struct { img *image.NRGBA mask *image.NRGBA @@ -213,8 +219,8 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) { // Otherwise we rotate the image only once, right before calling this function. if p.resizeXY { dx, dy = img.Bounds().Dy(), img.Bounds().Dx() - img = rotateImage90(img) - p.rotateMasks(rotateImage90) + img = cimage.RotateImage90(img) + p.rotateMasks(cimage.RotateImage90) } if dx > p.NewHeight { img, err = p.shrink(img) @@ -222,8 +228,8 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) { return nil, err } if p.resizeXY { - img = rotateImage270(img) - p.rotateMasks(rotateImage270) + img = cimage.RotateImage270(img) + p.rotateMasks(cimage.RotateImage270) } if p.NewWidth > 0 && p.NewWidth != dy { if p.NewWidth <= dy { @@ -245,8 +251,8 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) { } } else { if p.resizeXY { - img = rotateImage270(img) - p.rotateMasks(rotateImage270) + img = cimage.RotateImage270(img) + p.rotateMasks(cimage.RotateImage270) } } return img, nil @@ -259,8 +265,8 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) { if p.resizeXY { dx, dy = img.Bounds().Dy(), img.Bounds().Dx() - img = rotateImage90(img) - p.rotateMasks(rotateImage90) + img = cimage.RotateImage90(img) + p.rotateMasks(cimage.RotateImage90) } if dx < p.NewHeight { img, err = p.enlarge(img) @@ -268,8 +274,8 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) { return nil, err } if p.resizeXY { - img = rotateImage270(img) - p.rotateMasks(rotateImage270) + img = cimage.RotateImage270(img) + p.rotateMasks(cimage.RotateImage270) } if p.NewWidth > 0 && p.NewWidth != dy { if p.NewWidth <= dy { @@ -291,8 +297,8 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) { } } else { if p.resizeXY { - img = rotateImage270(img) - p.rotateMasks(rotateImage270) + img = cimage.RotateImage270(img) + p.rotateMasks(cimage.RotateImage270) } } return img, nil @@ -395,8 +401,8 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) { // Run the carver function if the desired image height is not identical with the rescaled image height. if newHeight > 0 && p.NewHeight != c.Height { if !p.resizeXY { - img = rotateImage90(img) - p.rotateMasks(rotateImage90) + img = cimage.RotateImage90(img) + p.rotateMasks(cimage.RotateImage90) } if p.NewHeight > c.Height { img, err = enlargeVertFn(img) @@ -410,8 +416,8 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) { } } if !p.resizeXY { - img = rotateImage270(img) - p.rotateMasks(rotateImage270) + img = cimage.RotateImage270(img) + p.rotateMasks(cimage.RotateImage270) } } @@ -490,7 +496,7 @@ func (p *Processor) calculateFitness(img *image.NRGBA, c *Carver) *image.NRGBA { // Process encodes the resized image into an io.Writer interface. // We are using the io package, since we can provide different input and output types, // as long as they implement the io.Reader and io.Writer interface. -func (p *Processor) Process(r io.Reader, w io.Writer) error { +func (p *Processor) Process(reader io.Reader, writer io.Writer) error { var err error if p.FaceDetect { @@ -508,12 +514,12 @@ func (p *Processor) Process(r io.Reader, w io.Writer) error { // Resize sets this flag as well, but the preview window size depends on it. p.resizeXY = p.NewWidth != 0 && p.NewHeight != 0 - src, _, err := image.Decode(r) + src, err := cimage.Decode(reader) if err != nil { - return err + return fmt.Errorf("cannot decode file: %w", err) } - img := imgToNRGBA(src) + img := cimage.ImgToNRGBA(src) if p.Preview { guiWidth := img.Bounds().Max.X @@ -544,7 +550,7 @@ func (p *Processor) Process(r io.Reader, w io.Writer) error { go p.showPreview(p.imgWorker, p.errs, guiWindow) } - return encodeImg(p, w, img) + return p.encodeImg(writer, img) } // shrink reduces the image dimension either horizontally or vertically. @@ -606,3 +612,52 @@ func (p *Processor) enlarge(img *image.NRGBA) (*image.NRGBA, error) { }) return img, nil } + +// showPreview spawns a new Gio GUI window and updates its content with the resized image received from a channel. +func (p *Processor) showPreview( + imgWorker <-chan worker, + errChan chan<- error, + guiWindow struct { + width int + height int + }, +) { + var gui = NewGUI(guiWindow.width, guiWindow.height) + gui.proc = p + gui.process.worker = imgWorker + + // Run the Gio GUI app in a separate goroutine + go func() { + if err := gui.Run(); err != nil { + errChan <- err + } + // It's important to call os.Exit(0) in order to terminate + // the execution of the GUI app when pressing ESC key. + os.Exit(0) + }() +} + +// encodeImg encodes an image to a destination of type io.Writer. +func (p *Processor) encodeImg(w io.Writer, img *image.NRGBA) error { + res, err := Resize(p, img) + if err != nil { + return err + } + + switch w := w.(type) { + case *os.File: + ext := filepath.Ext(w.Name()) + switch ext { + case "", ".jpg", ".jpeg": + return jpeg.Encode(w, res, &jpeg.Options{Quality: 100}) + case ".png": + return png.Encode(w, res) + case ".bmp": + return bmp.Encode(w, res) + default: + return errors.New("unsupported image format") + } + default: + return jpeg.Encode(w, res, &jpeg.Options{Quality: 100}) + } +} diff --git a/processor_test.go b/processor_test.go index 66304508..b7b2cea8 100644 --- a/processor_test.go +++ b/processor_test.go @@ -13,6 +13,7 @@ import ( "testing" "time" + cimage "github.com/esimov/caire/image" "github.com/esimov/caire/utils" "github.com/stretchr/testify/assert" ) @@ -48,7 +49,7 @@ func TestResize_ShrinkImageHeight(t *testing.T) { p.NewWidth = imgWidth p.NewHeight = newHeight - img = rotateImage90(img) + img = cimage.RotateImage90(img) for x := 0; x < newHeight; x++ { width, height := img.Bounds().Max.X, img.Bounds().Max.Y c = NewCarver(width, height) @@ -59,7 +60,7 @@ func TestResize_ShrinkImageHeight(t *testing.T) { seams := c.FindLowestEnergySeams(p) img = c.RemoveSeam(img, seams, p.Debug) } - img = rotateImage270(img) + img = cimage.RotateImage270(img) imgHeight := img.Bounds().Max.Y assert.Equal(t, imgHeight, newHeight) @@ -96,7 +97,7 @@ func TestResize_EnlargeImageHeight(t *testing.T) { p.NewWidth = imgWidth p.NewHeight = newHeight - img = rotateImage90(img) + img = cimage.RotateImage90(img) for x := 0; x < newHeight; x++ { width, height := img.Bounds().Max.X, img.Bounds().Max.Y c = NewCarver(width, height) @@ -107,7 +108,7 @@ func TestResize_EnlargeImageHeight(t *testing.T) { seams := c.FindLowestEnergySeams(p) img = c.AddSeam(img, seams, p.Debug) } - img = rotateImage270(img) + img = cimage.RotateImage270(img) imgHeight := img.Bounds().Max.Y - img.Bounds().Dy() assert.NotEqual(t, imgHeight, newHeight) diff --git a/utils/download.go b/utils/download.go index 2921cdcf..c51dce16 100644 --- a/utils/download.go +++ b/utils/download.go @@ -33,11 +33,6 @@ func DownloadImage(url string) (*os.File, error) { if err != nil { return nil, fmt.Errorf("unable to create temporary file: %w", err) } - defer func() { - if err := tmpfile.Close(); err != nil { - log.Printf("could not close the temporary file: %v", err) - } - }() // Copy the image binary data into the temporary file. _, err = io.Copy(tmpfile, bytes.NewBuffer(data)) diff --git a/utils/spinner.go b/utils/spinner.go index 124d82c0..d7ba79cc 100644 --- a/utils/spinner.go +++ b/utils/spinner.go @@ -53,7 +53,7 @@ func (s *Spinner) Start() { s.mu.Lock() output := fmt.Sprintf("\r%s%s %s%s", s.message, StatusColor, r, DefaultColor) - fmt.Fprintf(s.writer, output) + fmt.Fprintf(s.writer, "%s", output) s.lastOutput = output s.mu.Unlock() @@ -72,7 +72,7 @@ func (s *Spinner) Stop() { s.clear() s.RestoreCursor() if len(s.StopMsg) > 0 { - fmt.Fprintf(s.writer, s.StopMsg) + fmt.Fprintf(s.writer, "%s", s.StopMsg) } s.stopChan <- struct{}{} }