├── go.mod ├── testdata ├── src.jpg ├── src.png ├── dst_mean.png ├── dst_invert.png ├── dst_maximum.png ├── dst_median.png ├── dst_minimum.png ├── dst_resize.png ├── dst_sepia.png ├── dst_sigmoid.png ├── dst_colorize.png ├── dst_gamma_0.5.png ├── dst_gamma_1.5.png ├── dst_grayscale.png ├── dst_pixelate.png ├── dst_rotate_30.png ├── dst_color_func.png ├── dst_crop_to_size.png ├── dst_hue_rotate.png ├── dst_rotate_180.png ├── dst_unsharp_mask.png ├── dst_color_balance.png ├── dst_gaussian_blur.png ├── dst_contrast_decrease.png ├── dst_contrast_increase.png ├── dst_brightness_decrease.png ├── dst_brightness_increase.png ├── dst_convolution_emboss.png ├── dst_saturation_decrease.png └── dst_saturation_increase.png ├── .travis.yml ├── LICENSE ├── effects.go ├── effects_test.go ├── utils.go ├── rank.go ├── gift.go ├── utils_test.go ├── README.md ├── resize_test.go ├── pixels.go ├── resize.go ├── rank_test.go ├── colors.go ├── transform.go ├── convolution.go ├── transform_test.go ├── convolution_test.go ├── pixels_test.go └── gift_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/disintegration/gift 2 | -------------------------------------------------------------------------------- /testdata/src.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/src.jpg -------------------------------------------------------------------------------- /testdata/src.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/src.png -------------------------------------------------------------------------------- /testdata/dst_mean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_mean.png -------------------------------------------------------------------------------- /testdata/dst_invert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_invert.png -------------------------------------------------------------------------------- /testdata/dst_maximum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_maximum.png -------------------------------------------------------------------------------- /testdata/dst_median.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_median.png -------------------------------------------------------------------------------- /testdata/dst_minimum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_minimum.png -------------------------------------------------------------------------------- /testdata/dst_resize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_resize.png -------------------------------------------------------------------------------- /testdata/dst_sepia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_sepia.png -------------------------------------------------------------------------------- /testdata/dst_sigmoid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_sigmoid.png -------------------------------------------------------------------------------- /testdata/dst_colorize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_colorize.png -------------------------------------------------------------------------------- /testdata/dst_gamma_0.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_gamma_0.5.png -------------------------------------------------------------------------------- /testdata/dst_gamma_1.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_gamma_1.5.png -------------------------------------------------------------------------------- /testdata/dst_grayscale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_grayscale.png -------------------------------------------------------------------------------- /testdata/dst_pixelate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_pixelate.png -------------------------------------------------------------------------------- /testdata/dst_rotate_30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_rotate_30.png -------------------------------------------------------------------------------- /testdata/dst_color_func.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_color_func.png -------------------------------------------------------------------------------- /testdata/dst_crop_to_size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_crop_to_size.png -------------------------------------------------------------------------------- /testdata/dst_hue_rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_hue_rotate.png -------------------------------------------------------------------------------- /testdata/dst_rotate_180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_rotate_180.png -------------------------------------------------------------------------------- /testdata/dst_unsharp_mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_unsharp_mask.png -------------------------------------------------------------------------------- /testdata/dst_color_balance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_color_balance.png -------------------------------------------------------------------------------- /testdata/dst_gaussian_blur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_gaussian_blur.png -------------------------------------------------------------------------------- /testdata/dst_contrast_decrease.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_contrast_decrease.png -------------------------------------------------------------------------------- /testdata/dst_contrast_increase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_contrast_increase.png -------------------------------------------------------------------------------- /testdata/dst_brightness_decrease.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_brightness_decrease.png -------------------------------------------------------------------------------- /testdata/dst_brightness_increase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_brightness_increase.png -------------------------------------------------------------------------------- /testdata/dst_convolution_emboss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_convolution_emboss.png -------------------------------------------------------------------------------- /testdata/dst_saturation_decrease.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_saturation_decrease.png -------------------------------------------------------------------------------- /testdata/dst_saturation_increase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/gift/HEAD/testdata/dst_saturation_increase.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | arch: 4 | - AMD64 5 | - ppc64le 6 | 7 | go: 8 | - 1.13.x 9 | - 1.14.x 10 | - 1.15.x 11 | 12 | before_install: 13 | - go get github.com/mattn/goveralls 14 | 15 | script: 16 | - go test -v -race -cover 17 | - $GOPATH/bin/goveralls -service=travis-ci 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2018 Grigory Dryapak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /effects.go: -------------------------------------------------------------------------------- 1 | package gift 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | ) 7 | 8 | type pixelateFilter struct { 9 | size int 10 | } 11 | 12 | func (p *pixelateFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { 13 | dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy()) 14 | return 15 | } 16 | 17 | func (p *pixelateFilter) Draw(dst draw.Image, src image.Image, options *Options) { 18 | if options == nil { 19 | options = &defaultOptions 20 | } 21 | 22 | blockSize := p.size 23 | if blockSize <= 1 { 24 | copyimage(dst, src, options) 25 | return 26 | } 27 | 28 | srcb := src.Bounds() 29 | dstb := dst.Bounds() 30 | 31 | numBlocksX := srcb.Dx() / blockSize 32 | if srcb.Dx()%blockSize > 0 { 33 | numBlocksX++ 34 | } 35 | numBlocksY := srcb.Dy() / blockSize 36 | if srcb.Dy()%blockSize > 0 { 37 | numBlocksY++ 38 | } 39 | 40 | pixGetter := newPixelGetter(src) 41 | pixSetter := newPixelSetter(dst) 42 | 43 | parallelize(options.Parallelization, 0, numBlocksY, func(start, stop int) { 44 | for by := start; by < stop; by++ { 45 | for bx := 0; bx < numBlocksX; bx++ { 46 | // Calculate the block bounds. 47 | bb := image.Rect(bx*blockSize, by*blockSize, (bx+1)*blockSize, (by+1)*blockSize) 48 | bbSrc := bb.Add(srcb.Min).Intersect(srcb) 49 | bbDst := bbSrc.Sub(srcb.Min).Add(dstb.Min).Intersect(dstb) 50 | 51 | // Calculate the average color of the block. 52 | var r, g, b, a float32 53 | var cnt float32 54 | for y := bbSrc.Min.Y; y < bbSrc.Max.Y; y++ { 55 | for x := bbSrc.Min.X; x < bbSrc.Max.X; x++ { 56 | px := pixGetter.getPixel(x, y) 57 | r += px.r 58 | g += px.g 59 | b += px.b 60 | a += px.a 61 | cnt++ 62 | } 63 | } 64 | if cnt > 0 { 65 | r /= cnt 66 | g /= cnt 67 | b /= cnt 68 | a /= cnt 69 | } 70 | 71 | // Set the calculated color for all pixels in the block. 72 | for y := bbDst.Min.Y; y < bbDst.Max.Y; y++ { 73 | for x := bbDst.Min.X; x < bbDst.Max.X; x++ { 74 | pixSetter.setPixel(x, y, pixel{r, g, b, a}) 75 | } 76 | } 77 | } 78 | } 79 | }) 80 | } 81 | 82 | // Pixelate creates a filter that applies a pixelation effect to an image. 83 | func Pixelate(size int) Filter { 84 | return &pixelateFilter{ 85 | size: size, 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /effects_test.go: -------------------------------------------------------------------------------- 1 | package gift 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | ) 7 | 8 | func TestPixelate(t *testing.T) { 9 | testData := []struct { 10 | desc string 11 | size int 12 | srcb, dstb image.Rectangle 13 | srcPix, dstPix []uint8 14 | }{ 15 | { 16 | "pixelate (0)", 17 | 0, 18 | image.Rect(-1, -1, 4, 2), 19 | image.Rect(0, 0, 5, 3), 20 | []uint8{ 21 | 0x00, 0x40, 0x00, 0x40, 0x00, 22 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 23 | 0x00, 0x80, 0x00, 0x80, 0x00, 24 | }, 25 | []uint8{ 26 | 0x00, 0x40, 0x00, 0x40, 0x00, 27 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 28 | 0x00, 0x80, 0x00, 0x80, 0x00, 29 | }, 30 | }, 31 | { 32 | "pixelate (1)", 33 | 1, 34 | image.Rect(-1, -1, 4, 2), 35 | image.Rect(0, 0, 5, 3), 36 | []uint8{ 37 | 0x00, 0x40, 0x00, 0x40, 0x00, 38 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 39 | 0x00, 0x80, 0x00, 0x80, 0x00, 40 | }, 41 | []uint8{ 42 | 0x00, 0x40, 0x00, 0x40, 0x00, 43 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 44 | 0x00, 0x80, 0x00, 0x80, 0x00, 45 | }, 46 | }, 47 | { 48 | "pixelate (2)", 49 | 2, 50 | image.Rect(-1, -1, 4, 2), 51 | image.Rect(0, 0, 5, 3), 52 | []uint8{ 53 | 0x00, 0x40, 0x00, 0x40, 0x00, 54 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 55 | 0x00, 0x80, 0x00, 0x80, 0x00, 56 | }, 57 | []uint8{ 58 | 0x54, 0x54, 0x64, 0x64, 0x30, 59 | 0x54, 0x54, 0x64, 0x64, 0x30, 60 | 0x40, 0x40, 0x40, 0x40, 0x00, 61 | }, 62 | }, 63 | { 64 | "pixelate (3)", 65 | 3, 66 | image.Rect(-1, -1, 4, 2), 67 | image.Rect(0, 0, 5, 3), 68 | []uint8{ 69 | 0x00, 0x40, 0x00, 0x40, 0x00, 70 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 71 | 0x00, 0x80, 0x00, 0x80, 0x00, 72 | }, 73 | []uint8{ 74 | 0x45, 0x45, 0x45, 0x4d, 0x4d, 75 | 0x45, 0x45, 0x45, 0x4d, 0x4d, 76 | 0x45, 0x45, 0x45, 0x4d, 0x4d, 77 | }, 78 | }, 79 | { 80 | "pixelate (10)", 81 | 10, 82 | image.Rect(-1, -1, 4, 2), 83 | image.Rect(0, 0, 5, 3), 84 | []uint8{ 85 | 0x00, 0x40, 0x00, 0x40, 0x00, 86 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 87 | 0x00, 0x80, 0x00, 0x80, 0x00, 88 | }, 89 | []uint8{ 90 | 0x49, 0x49, 0x49, 0x49, 0x49, 91 | 0x49, 0x49, 0x49, 0x49, 0x49, 92 | 0x49, 0x49, 0x49, 0x49, 0x49, 93 | }, 94 | }, 95 | { 96 | "pixelate 0x0", 97 | 3, 98 | image.Rect(-1, -1, -1, -1), 99 | image.Rect(0, 0, 0, 0), 100 | []uint8{}, 101 | []uint8{}, 102 | }, 103 | } 104 | 105 | for _, d := range testData { 106 | src := image.NewGray(d.srcb) 107 | src.Pix = d.srcPix 108 | 109 | f := Pixelate(d.size) 110 | dst := image.NewGray(f.Bounds(src.Bounds())) 111 | f.Draw(dst, src, nil) 112 | 113 | if !checkBoundsAndPix(dst.Bounds(), d.dstb, dst.Pix, d.dstPix) { 114 | t.Errorf("test [%s] failed: %#v, %#v", d.desc, dst.Bounds(), dst.Pix) 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package gift 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | "math" 7 | "runtime" 8 | "sync" 9 | ) 10 | 11 | // parallelize parallelizes the data processing. 12 | func parallelize(enabled bool, start, stop int, fn func(start, stop int)) { 13 | procs := 1 14 | if enabled { 15 | procs = runtime.GOMAXPROCS(0) 16 | } 17 | var wg sync.WaitGroup 18 | splitRange(start, stop, procs, func(pstart, pstop int) { 19 | wg.Add(1) 20 | go func() { 21 | defer wg.Done() 22 | fn(pstart, pstop) 23 | }() 24 | }) 25 | wg.Wait() 26 | } 27 | 28 | // splitRange splits a range into n parts and calls a function for each of them. 29 | func splitRange(start, stop, n int, fn func(pstart, pstop int)) { 30 | count := stop - start 31 | if count < 1 { 32 | return 33 | } 34 | 35 | if n < 1 { 36 | n = 1 37 | } 38 | if n > count { 39 | n = count 40 | } 41 | 42 | div := count / n 43 | mod := count % n 44 | 45 | for i := 0; i < n; i++ { 46 | fn( 47 | start+i*div+minint(i, mod), 48 | start+(i+1)*div+minint(i+1, mod), 49 | ) 50 | } 51 | } 52 | 53 | func absf32(x float32) float32 { 54 | if x < 0 { 55 | return -x 56 | } 57 | return x 58 | } 59 | 60 | func minf32(x, y float32) float32 { 61 | if x < y { 62 | return x 63 | } 64 | return y 65 | } 66 | 67 | func maxf32(x, y float32) float32 { 68 | if x > y { 69 | return x 70 | } 71 | return y 72 | } 73 | 74 | func powf32(x, y float32) float32 { 75 | return float32(math.Pow(float64(x), float64(y))) 76 | } 77 | 78 | func logf32(x float32) float32 { 79 | return float32(math.Log(float64(x))) 80 | } 81 | 82 | func expf32(x float32) float32 { 83 | return float32(math.Exp(float64(x))) 84 | } 85 | 86 | func sincosf32(a float32) (float32, float32) { 87 | sin, cos := math.Sincos(math.Pi * float64(a) / 180) 88 | return float32(sin), float32(cos) 89 | } 90 | 91 | func floorf32(x float32) float32 { 92 | return float32(math.Floor(float64(x))) 93 | } 94 | 95 | func sqrtf32(x float32) float32 { 96 | return float32(math.Sqrt(float64(x))) 97 | } 98 | 99 | func minint(x, y int) int { 100 | if x < y { 101 | return x 102 | } 103 | return y 104 | } 105 | 106 | func maxint(x, y int) int { 107 | if x > y { 108 | return x 109 | } 110 | return y 111 | } 112 | 113 | func sort(data []float32) { 114 | n := len(data) 115 | 116 | if n < 2 { 117 | return 118 | } 119 | 120 | if n <= 20 { 121 | for i := 1; i < n; i++ { 122 | x := data[i] 123 | j := i - 1 124 | for ; j >= 0 && data[j] > x; j-- { 125 | data[j+1] = data[j] 126 | } 127 | data[j+1] = x 128 | } 129 | return 130 | } 131 | 132 | i := 0 133 | j := n - 1 134 | x := data[n/2] 135 | for i <= j { 136 | for data[i] < x { 137 | i++ 138 | } 139 | for data[j] > x { 140 | j-- 141 | } 142 | if i <= j { 143 | data[i], data[j] = data[j], data[i] 144 | i++ 145 | j-- 146 | } 147 | } 148 | if j > 0 { 149 | sort(data[:j+1]) 150 | } 151 | if i < n-1 { 152 | sort(data[i:]) 153 | } 154 | } 155 | 156 | // createTempImage creates a temporary image. 157 | func createTempImage(r image.Rectangle) draw.Image { 158 | return image.NewNRGBA64(r) 159 | } 160 | 161 | // isOpaque checks if the given image is opaque. 162 | func isOpaque(img image.Image) bool { 163 | type opaquer interface { 164 | Opaque() bool 165 | } 166 | if o, ok := img.(opaquer); ok { 167 | return o.Opaque() 168 | } 169 | return false 170 | } 171 | 172 | // genDisk generates a disk-shaped kernel. 173 | func genDisk(ksize int) []float32 { 174 | if ksize%2 == 0 { 175 | ksize-- 176 | } 177 | if ksize < 1 { 178 | return []float32{} 179 | } 180 | disk := make([]float32, ksize*ksize) 181 | kcenter := ksize / 2 182 | for i := 0; i < ksize; i++ { 183 | for j := 0; j < ksize; j++ { 184 | x := kcenter - i 185 | y := kcenter - j 186 | r := math.Sqrt(float64(x*x + y*y)) 187 | if r <= float64(ksize/2) { 188 | disk[j*ksize+i] = 1 189 | } 190 | } 191 | } 192 | return disk 193 | } 194 | 195 | // copyimage copies an image from src to dst. 196 | func copyimage(dst draw.Image, src image.Image, options *Options) { 197 | if options == nil { 198 | options = &defaultOptions 199 | } 200 | 201 | srcb := src.Bounds() 202 | dstb := dst.Bounds() 203 | pixGetter := newPixelGetter(src) 204 | pixSetter := newPixelSetter(dst) 205 | 206 | parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(start, stop int) { 207 | for srcy := start; srcy < stop; srcy++ { 208 | for srcx := srcb.Min.X; srcx < srcb.Max.X; srcx++ { 209 | dstx := dstb.Min.X + srcx - srcb.Min.X 210 | dsty := dstb.Min.Y + srcy - srcb.Min.Y 211 | pixSetter.setPixel(dstx, dsty, pixGetter.getPixel(srcx, srcy)) 212 | } 213 | } 214 | }) 215 | } 216 | 217 | type copyimageFilter struct{} 218 | 219 | func (p *copyimageFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { 220 | dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy()) 221 | return 222 | } 223 | 224 | func (p *copyimageFilter) Draw(dst draw.Image, src image.Image, options *Options) { 225 | copyimage(dst, src, options) 226 | } 227 | -------------------------------------------------------------------------------- /rank.go: -------------------------------------------------------------------------------- 1 | package gift 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | ) 7 | 8 | type rankMode int 9 | 10 | const ( 11 | rankMedian rankMode = iota 12 | rankMin 13 | rankMax 14 | ) 15 | 16 | type rankFilter struct { 17 | ksize int 18 | disk bool 19 | mode rankMode 20 | } 21 | 22 | func (p *rankFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { 23 | dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy()) 24 | return 25 | } 26 | 27 | func (p *rankFilter) Draw(dst draw.Image, src image.Image, options *Options) { 28 | if options == nil { 29 | options = &defaultOptions 30 | } 31 | 32 | srcb := src.Bounds() 33 | dstb := dst.Bounds() 34 | 35 | if srcb.Dx() <= 0 || srcb.Dy() <= 0 { 36 | return 37 | } 38 | 39 | ksize := p.ksize 40 | if ksize%2 == 0 { 41 | ksize-- 42 | } 43 | 44 | if ksize <= 1 { 45 | copyimage(dst, src, options) 46 | return 47 | } 48 | kradius := ksize / 2 49 | 50 | opaque := isOpaque(src) 51 | 52 | var disk []float32 53 | if p.disk { 54 | disk = genDisk(ksize) 55 | } 56 | 57 | pixGetter := newPixelGetter(src) 58 | pixSetter := newPixelSetter(dst) 59 | 60 | parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(start, stop int) { 61 | pxbuf := []pixel{} 62 | 63 | var rbuf, gbuf, bbuf, abuf []float32 64 | if p.mode == rankMedian { 65 | rbuf = make([]float32, 0, ksize*ksize) 66 | gbuf = make([]float32, 0, ksize*ksize) 67 | bbuf = make([]float32, 0, ksize*ksize) 68 | if !opaque { 69 | abuf = make([]float32, 0, ksize*ksize) 70 | } 71 | } 72 | 73 | for y := start; y < stop; y++ { 74 | // Init buffer. 75 | pxbuf = pxbuf[:0] 76 | for i := srcb.Min.X - kradius; i <= srcb.Min.X+kradius; i++ { 77 | for j := y - kradius; j <= y+kradius; j++ { 78 | kx, ky := i, j 79 | if kx < srcb.Min.X { 80 | kx = srcb.Min.X 81 | } else if kx > srcb.Max.X-1 { 82 | kx = srcb.Max.X - 1 83 | } 84 | if ky < srcb.Min.Y { 85 | ky = srcb.Min.Y 86 | } else if ky > srcb.Max.Y-1 { 87 | ky = srcb.Max.Y - 1 88 | } 89 | pxbuf = append(pxbuf, pixGetter.getPixel(kx, ky)) 90 | } 91 | } 92 | 93 | for x := srcb.Min.X; x < srcb.Max.X; x++ { 94 | var r, g, b, a float32 95 | if p.mode == rankMedian { 96 | rbuf = rbuf[:0] 97 | gbuf = gbuf[:0] 98 | bbuf = bbuf[:0] 99 | if !opaque { 100 | abuf = abuf[:0] 101 | } 102 | } else if p.mode == rankMin { 103 | r, g, b, a = 1, 1, 1, 1 104 | } else if p.mode == rankMax { 105 | r, g, b, a = 0, 0, 0, 0 106 | } 107 | 108 | sz := 0 109 | for i := 0; i < ksize; i++ { 110 | for j := 0; j < ksize; j++ { 111 | 112 | if p.disk { 113 | if disk[i*ksize+j] == 0 { 114 | continue 115 | } 116 | } 117 | 118 | px := pxbuf[i*ksize+j] 119 | if p.mode == rankMedian { 120 | rbuf = append(rbuf, px.r) 121 | gbuf = append(gbuf, px.g) 122 | bbuf = append(bbuf, px.b) 123 | if !opaque { 124 | abuf = append(abuf, px.a) 125 | } 126 | } else if p.mode == rankMin { 127 | r = minf32(r, px.r) 128 | g = minf32(g, px.g) 129 | b = minf32(b, px.b) 130 | if !opaque { 131 | a = minf32(a, px.a) 132 | } 133 | } else if p.mode == rankMax { 134 | r = maxf32(r, px.r) 135 | g = maxf32(g, px.g) 136 | b = maxf32(b, px.b) 137 | if !opaque { 138 | a = maxf32(a, px.a) 139 | } 140 | } 141 | sz++ 142 | } 143 | } 144 | 145 | if p.mode == rankMedian { 146 | sort(rbuf) 147 | sort(gbuf) 148 | sort(bbuf) 149 | if !opaque { 150 | sort(abuf) 151 | } 152 | 153 | idx := sz / 2 154 | r, g, b = rbuf[idx], gbuf[idx], bbuf[idx] 155 | if !opaque { 156 | a = abuf[idx] 157 | } 158 | } 159 | 160 | if opaque { 161 | a = 1 162 | } 163 | 164 | pixSetter.setPixel(dstb.Min.X+x-srcb.Min.X, dstb.Min.Y+y-srcb.Min.Y, pixel{r, g, b, a}) 165 | 166 | // Rotate buffer columns. 167 | if x < srcb.Max.X-1 { 168 | copy(pxbuf[0:], pxbuf[ksize:]) 169 | pxbuf = pxbuf[0 : ksize*(ksize-1)] 170 | kx := x + 1 + kradius 171 | if kx > srcb.Max.X-1 { 172 | kx = srcb.Max.X - 1 173 | } 174 | for j := y - kradius; j <= y+kradius; j++ { 175 | ky := j 176 | if ky < srcb.Min.Y { 177 | ky = srcb.Min.Y 178 | } else if ky > srcb.Max.Y-1 { 179 | ky = srcb.Max.Y - 1 180 | } 181 | pxbuf = append(pxbuf, pixGetter.getPixel(kx, ky)) 182 | } 183 | } 184 | } 185 | } 186 | }) 187 | } 188 | 189 | // Median creates a median image filter. 190 | // Picks a median value per channel in neighborhood for each pixel. 191 | // The ksize parameter is the kernel size. It must be an odd positive integer (for example: 3, 5, 7). 192 | // If the disk parameter is true, a disk-shaped neighborhood will be used instead of a square neighborhood. 193 | func Median(ksize int, disk bool) Filter { 194 | return &rankFilter{ 195 | ksize: ksize, 196 | disk: disk, 197 | mode: rankMedian, 198 | } 199 | } 200 | 201 | // Minimum creates a local minimum image filter. 202 | // Picks a minimum value per channel in neighborhood for each pixel. 203 | // The ksize parameter is the kernel size. It must be an odd positive integer (for example: 3, 5, 7). 204 | // If the disk parameter is true, a disk-shaped neighborhood will be used instead of a square neighborhood. 205 | func Minimum(ksize int, disk bool) Filter { 206 | return &rankFilter{ 207 | ksize: ksize, 208 | disk: disk, 209 | mode: rankMin, 210 | } 211 | } 212 | 213 | // Maximum creates a local maximum image filter. 214 | // Picks a maximum value per channel in neighborhood for each pixel. 215 | // The ksize parameter is the kernel size. It must be an odd positive integer (for example: 3, 5, 7). 216 | // If the disk parameter is true, a disk-shaped neighborhood will be used instead of a square neighborhood. 217 | func Maximum(ksize int, disk bool) Filter { 218 | return &rankFilter{ 219 | ksize: ksize, 220 | disk: disk, 221 | mode: rankMax, 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /gift.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package gift provides a set of useful image processing filters. 3 | 4 | Basic usage: 5 | 6 | // 1. Create a new filter list and add some filters. 7 | g := gift.New( 8 | gift.Resize(800, 0, gift.LanczosResampling), 9 | gift.UnsharpMask(1, 1, 0), 10 | ) 11 | 12 | // 2. Create a new image of the corresponding size. 13 | // dst is a new target image, src is the original image. 14 | dst := image.NewRGBA(g.Bounds(src.Bounds())) 15 | 16 | // 3. Use the Draw func to apply the filters to src and store the result in dst. 17 | g.Draw(dst, src) 18 | 19 | */ 20 | package gift 21 | 22 | import ( 23 | "image" 24 | "image/draw" 25 | ) 26 | 27 | // Filter is an image processing filter. 28 | type Filter interface { 29 | // Draw applies the filter to the src image and outputs the result to the dst image. 30 | Draw(dst draw.Image, src image.Image, options *Options) 31 | // Bounds calculates the appropriate bounds of an image after applying the filter. 32 | Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) 33 | } 34 | 35 | // Options is the parameters passed to image processing filters. 36 | type Options struct { 37 | Parallelization bool 38 | } 39 | 40 | var defaultOptions = Options{ 41 | Parallelization: true, 42 | } 43 | 44 | // GIFT is a list of image processing filters. 45 | type GIFT struct { 46 | Filters []Filter 47 | Options Options 48 | } 49 | 50 | // New creates a new filter list and initializes it with the given slice of filters. 51 | func New(filters ...Filter) *GIFT { 52 | return &GIFT{ 53 | Filters: filters, 54 | Options: defaultOptions, 55 | } 56 | } 57 | 58 | // SetParallelization enables or disables the image processing parallelization. 59 | // Parallelization is enabled by default. 60 | func (g *GIFT) SetParallelization(isEnabled bool) { 61 | g.Options.Parallelization = isEnabled 62 | } 63 | 64 | // Parallelization returns the current state of parallelization option. 65 | func (g *GIFT) Parallelization() bool { 66 | return g.Options.Parallelization 67 | } 68 | 69 | // Add appends the given filters to the list of filters. 70 | func (g *GIFT) Add(filters ...Filter) { 71 | g.Filters = append(g.Filters, filters...) 72 | } 73 | 74 | // Empty removes all the filters from the list. 75 | func (g *GIFT) Empty() { 76 | g.Filters = []Filter{} 77 | } 78 | 79 | // Bounds calculates the appropriate bounds for the result image after applying all the added filters. 80 | // Parameter srcBounds is the bounds of the source image. 81 | // 82 | // Example: 83 | // 84 | // src := image.NewRGBA(image.Rect(0, 0, 100, 200)) 85 | // g := gift.New(gift.Rotate90()) 86 | // 87 | // // calculate image bounds after applying rotation and create a new image of that size. 88 | // dst := image.NewRGBA(g.Bounds(src.Bounds())) // dst bounds: (0, 0, 200, 100) 89 | // 90 | // 91 | func (g *GIFT) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { 92 | b := srcBounds 93 | for _, f := range g.Filters { 94 | b = f.Bounds(b) 95 | } 96 | dstBounds = b 97 | return 98 | } 99 | 100 | // Draw applies all the added filters to the src image and outputs the result to the dst image. 101 | func (g *GIFT) Draw(dst draw.Image, src image.Image) { 102 | if len(g.Filters) == 0 { 103 | copyimage(dst, src, &g.Options) 104 | return 105 | } 106 | 107 | first, last := 0, len(g.Filters)-1 108 | var tmpIn image.Image 109 | var tmpOut draw.Image 110 | 111 | for i, f := range g.Filters { 112 | if i == first { 113 | tmpIn = src 114 | } else { 115 | tmpIn = tmpOut 116 | } 117 | 118 | if i == last { 119 | tmpOut = dst 120 | } else { 121 | tmpOut = createTempImage(f.Bounds(tmpIn.Bounds())) 122 | } 123 | 124 | f.Draw(tmpOut, tmpIn, &g.Options) 125 | } 126 | } 127 | 128 | // Operator is an image composition operator. 129 | type Operator int 130 | 131 | // Composition operators. 132 | const ( 133 | CopyOperator Operator = iota 134 | OverOperator 135 | ) 136 | 137 | // DrawAt applies all the added filters to the src image and outputs the result to the dst image 138 | // at the specified position pt using the specified composition operator op. 139 | func (g *GIFT) DrawAt(dst draw.Image, src image.Image, pt image.Point, op Operator) { 140 | switch op { 141 | case OverOperator: 142 | tb := g.Bounds(src.Bounds()) 143 | tb = tb.Sub(tb.Min).Add(pt) 144 | tmp := createTempImage(tb) 145 | g.Draw(tmp, src) 146 | pixGetterDst := newPixelGetter(dst) 147 | pixGetterTmp := newPixelGetter(tmp) 148 | pixSetterDst := newPixelSetter(dst) 149 | ib := tb.Intersect(dst.Bounds()) 150 | parallelize(g.Options.Parallelization, ib.Min.Y, ib.Max.Y, func(start, stop int) { 151 | for y := start; y < stop; y++ { 152 | for x := ib.Min.X; x < ib.Max.X; x++ { 153 | px0 := pixGetterDst.getPixel(x, y) 154 | px1 := pixGetterTmp.getPixel(x, y) 155 | c1 := px1.a 156 | c0 := (1 - c1) * px0.a 157 | cs := c0 + c1 158 | c0 /= cs 159 | c1 /= cs 160 | r := px0.r*c0 + px1.r*c1 161 | g := px0.g*c0 + px1.g*c1 162 | b := px0.b*c0 + px1.b*c1 163 | a := px0.a + px1.a*(1-px0.a) 164 | pixSetterDst.setPixel(x, y, pixel{r, g, b, a}) 165 | } 166 | } 167 | }) 168 | 169 | default: 170 | if pt.Eq(dst.Bounds().Min) { 171 | g.Draw(dst, src) 172 | return 173 | } 174 | if subimg, ok := getSubImage(dst, pt); ok { 175 | g.Draw(subimg, src) 176 | return 177 | } 178 | tb := g.Bounds(src.Bounds()) 179 | tb = tb.Sub(tb.Min).Add(pt) 180 | tmp := createTempImage(tb) 181 | g.Draw(tmp, src) 182 | pixGetter := newPixelGetter(tmp) 183 | pixSetter := newPixelSetter(dst) 184 | ib := tb.Intersect(dst.Bounds()) 185 | parallelize(g.Options.Parallelization, ib.Min.Y, ib.Max.Y, func(start, stop int) { 186 | for y := start; y < stop; y++ { 187 | for x := ib.Min.X; x < ib.Max.X; x++ { 188 | pixSetter.setPixel(x, y, pixGetter.getPixel(x, y)) 189 | } 190 | } 191 | }) 192 | } 193 | } 194 | 195 | func getSubImage(img draw.Image, pt image.Point) (draw.Image, bool) { 196 | if !pt.In(img.Bounds()) { 197 | return nil, false 198 | } 199 | switch img := img.(type) { 200 | case *image.Gray: 201 | return img.SubImage(image.Rectangle{pt, img.Bounds().Max}).(draw.Image), true 202 | case *image.Gray16: 203 | return img.SubImage(image.Rectangle{pt, img.Bounds().Max}).(draw.Image), true 204 | case *image.RGBA: 205 | return img.SubImage(image.Rectangle{pt, img.Bounds().Max}).(draw.Image), true 206 | case *image.RGBA64: 207 | return img.SubImage(image.Rectangle{pt, img.Bounds().Max}).(draw.Image), true 208 | case *image.NRGBA: 209 | return img.SubImage(image.Rectangle{pt, img.Bounds().Max}).(draw.Image), true 210 | case *image.NRGBA64: 211 | return img.SubImage(image.Rectangle{pt, img.Bounds().Max}).(draw.Image), true 212 | default: 213 | return nil, false 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package gift 2 | 3 | import ( 4 | "bytes" 5 | "image" 6 | "image/color" 7 | "runtime" 8 | "testing" 9 | ) 10 | 11 | func TestParallelize(t *testing.T) { 12 | for _, e := range []bool{true, false} { 13 | for _, n := range []int{0, 1, 5, 10, 50, 100, 500, 1000, 5000} { 14 | for _, p := range []int{1, 2, 4, 8, 16, 32, 64, 128} { 15 | if !testParallelizeN(e, n, p) { 16 | t.Fatalf("test [e=%v n=%d p=%d] failed", e, n, p) 17 | } 18 | } 19 | } 20 | } 21 | } 22 | 23 | func testParallelizeN(enabled bool, n, procs int) bool { 24 | defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(procs)) 25 | data := make([]int, n) 26 | parallelize(enabled, 0, n, func(start, stop int) { 27 | for i := start; i < stop; i++ { 28 | data[i]++ 29 | } 30 | }) 31 | for i := 0; i < n; i++ { 32 | if data[i] != 1 { 33 | return false 34 | } 35 | } 36 | return true 37 | } 38 | 39 | func TestSplitRange(t *testing.T) { 40 | for count := 0; count < 100; count++ { 41 | for procs := 0; procs < 100; procs++ { 42 | start := -55 43 | 44 | var parts [][2]int 45 | splitRange(start, start+count, procs, func(start, stop int) { 46 | parts = append(parts, [2]int{start, stop}) 47 | }) 48 | 49 | wantLen := procs 50 | if wantLen < 1 { 51 | wantLen = 1 52 | } 53 | if wantLen > count { 54 | wantLen = count 55 | } 56 | if len(parts) != wantLen { 57 | t.Fatalf("test [count=%d procs=%d] got len(parts) %d want %d", count, procs, len(parts), wantLen) 58 | } 59 | 60 | data := make([]int, count) 61 | for _, p := range parts { 62 | for i := p[0]; i < p[1]; i++ { 63 | data[i-start]++ 64 | } 65 | } 66 | for i := range data { 67 | if data[i] != 1 { 68 | t.Fatalf("test [count=%d procs=%d] got data[%d] == %d want 1", count, procs, i, data[i]) 69 | } 70 | } 71 | } 72 | } 73 | } 74 | 75 | func TestTempImageCopy(t *testing.T) { 76 | tmp1 := createTempImage(image.Rect(-1, -2, 1, 2)) 77 | if !tmp1.Bounds().Eq(image.Rect(-1, -2, 1, 2)) { 78 | t.Error("unexpected temp image bounds") 79 | } 80 | tmp2 := createTempImage(image.Rect(-3, -4, 3, 4)) 81 | if !tmp2.Bounds().Eq(image.Rect(-3, -4, 3, 4)) { 82 | t.Error("unexpected temp image bounds") 83 | } 84 | copyimage(tmp1, tmp2, nil) 85 | } 86 | 87 | func TestSort(t *testing.T) { 88 | testData := []struct { 89 | a, b []float32 90 | }{ 91 | { 92 | []float32{}, 93 | []float32{}, 94 | }, 95 | { 96 | []float32{0.1}, 97 | []float32{0.1}, 98 | }, 99 | { 100 | []float32{0.4, 0.2, 0.5, -0.5, 0.3, 0.0, 0.1}, 101 | []float32{-0.5, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5}, 102 | }, 103 | { 104 | []float32{-10, 10, -20, 20, -30, 30}, 105 | []float32{-30, -20, -10, 10, 20, 30}, 106 | }, 107 | { 108 | []float32{ 109 | 0.60, 0.94, 0.66, 0.44, 0.42, 0.69, 0.07, 0.16, 0.10, 0.30, 110 | 0.52, 0.81, 0.21, 0.38, 0.32, 0.47, 0.28, 0.29, 0.68, 0.22, 111 | 0.20, 0.36, 0.57, 0.86, 0.29, 0.30, 0.75, 0.21, 0.87, 0.70, 112 | }, 113 | []float32{ 114 | 0.07, 0.10, 0.16, 0.20, 0.21, 0.21, 0.22, 0.28, 0.29, 0.29, 115 | 0.30, 0.30, 0.32, 0.36, 0.38, 0.42, 0.44, 0.47, 0.52, 0.57, 116 | 0.60, 0.66, 0.68, 0.69, 0.70, 0.75, 0.81, 0.86, 0.87, 0.94, 117 | }, 118 | }, 119 | } 120 | 121 | for _, d := range testData { 122 | sort(d.a) 123 | for i := range d.a { 124 | if d.a[i] != d.b[i] { 125 | t.Errorf("sort failed: %#v", d.a) 126 | } 127 | } 128 | } 129 | } 130 | 131 | func TestDisk(t *testing.T) { 132 | testData := []struct { 133 | ksize int 134 | k []float32 135 | }{ 136 | { 137 | -5, 138 | []float32{}, 139 | }, 140 | { 141 | 0, 142 | []float32{}, 143 | }, 144 | { 145 | 1, 146 | []float32{1}, 147 | }, 148 | { 149 | 2, 150 | []float32{1}, 151 | }, 152 | { 153 | 3, 154 | []float32{ 155 | 0, 1, 0, 156 | 1, 1, 1, 157 | 0, 1, 0, 158 | }, 159 | }, 160 | { 161 | 4, 162 | []float32{ 163 | 0, 1, 0, 164 | 1, 1, 1, 165 | 0, 1, 0, 166 | }, 167 | }, 168 | { 169 | 5, 170 | []float32{ 171 | 0, 0, 1, 0, 0, 172 | 0, 1, 1, 1, 0, 173 | 1, 1, 1, 1, 1, 174 | 0, 1, 1, 1, 0, 175 | 0, 0, 1, 0, 0, 176 | }, 177 | }, 178 | { 179 | 6, 180 | []float32{ 181 | 0, 0, 1, 0, 0, 182 | 0, 1, 1, 1, 0, 183 | 1, 1, 1, 1, 1, 184 | 0, 1, 1, 1, 0, 185 | 0, 0, 1, 0, 0, 186 | }, 187 | }, 188 | { 189 | 7, 190 | []float32{ 191 | 0, 0, 0, 1, 0, 0, 0, 192 | 0, 1, 1, 1, 1, 1, 0, 193 | 0, 1, 1, 1, 1, 1, 0, 194 | 1, 1, 1, 1, 1, 1, 1, 195 | 0, 1, 1, 1, 1, 1, 0, 196 | 0, 1, 1, 1, 1, 1, 0, 197 | 0, 0, 0, 1, 0, 0, 0, 198 | }, 199 | }, 200 | } 201 | 202 | for _, d := range testData { 203 | disk := genDisk(d.ksize) 204 | for i := range disk { 205 | if disk[i] != d.k[i] { 206 | t.Errorf("gen disk failed: %d %#v", d.ksize, disk) 207 | } 208 | } 209 | } 210 | } 211 | 212 | type customImage struct{} 213 | 214 | func (customImage) ColorModel() color.Model { 215 | return color.GrayModel 216 | } 217 | func (customImage) Bounds() image.Rectangle { 218 | return image.Rectangle{} 219 | } 220 | func (customImage) At(x, y int) color.Color { 221 | return color.Gray{} 222 | } 223 | 224 | func TestIsOpaque(t *testing.T) { 225 | type opqt struct { 226 | img image.Image 227 | opaque bool 228 | } 229 | var testData []opqt 230 | 231 | testData = append(testData, opqt{customImage{}, false}) 232 | testData = append(testData, opqt{image.NewNRGBA(image.Rect(0, 0, 1, 1)), false}) 233 | testData = append(testData, opqt{image.NewNRGBA64(image.Rect(0, 0, 1, 1)), false}) 234 | testData = append(testData, opqt{image.NewRGBA(image.Rect(0, 0, 1, 1)), false}) 235 | testData = append(testData, opqt{image.NewRGBA64(image.Rect(0, 0, 1, 1)), false}) 236 | testData = append(testData, opqt{image.NewGray(image.Rect(0, 0, 1, 1)), true}) 237 | testData = append(testData, opqt{image.NewGray16(image.Rect(0, 0, 1, 1)), true}) 238 | testData = append(testData, opqt{image.NewYCbCr(image.Rect(0, 0, 1, 1), image.YCbCrSubsampleRatio444), true}) 239 | testData = append(testData, opqt{image.NewAlpha(image.Rect(0, 0, 1, 1)), false}) 240 | 241 | img1 := image.NewNRGBA(image.Rect(0, 0, 1, 1)) 242 | img1.Set(0, 0, color.NRGBA{0x00, 0x00, 0x00, 0xff}) 243 | testData = append(testData, opqt{img1, true}) 244 | img2 := image.NewNRGBA64(image.Rect(0, 0, 1, 1)) 245 | img2.Set(0, 0, color.NRGBA{0x00, 0x00, 0x00, 0xff}) 246 | testData = append(testData, opqt{img2, true}) 247 | img3 := image.NewRGBA(image.Rect(0, 0, 1, 1)) 248 | img3.Set(0, 0, color.NRGBA{0x00, 0x00, 0x00, 0xff}) 249 | testData = append(testData, opqt{img3, true}) 250 | img4 := image.NewRGBA64(image.Rect(0, 0, 1, 1)) 251 | img4.Set(0, 0, color.NRGBA{0x00, 0x00, 0x00, 0xff}) 252 | testData = append(testData, opqt{img4, true}) 253 | imgp1 := image.NewPaletted(image.Rect(0, 0, 1, 1), []color.Color{color.NRGBA{0x00, 0x00, 0x00, 0xff}}) 254 | imgp1.SetColorIndex(0, 0, 0) 255 | testData = append(testData, opqt{imgp1, true}) 256 | imgp2 := image.NewPaletted(image.Rect(0, 0, 1, 1), []color.Color{color.NRGBA{0x00, 0x00, 0x00, 0xfe}}) 257 | imgp2.SetColorIndex(0, 0, 0) 258 | testData = append(testData, opqt{imgp2, false}) 259 | 260 | for _, d := range testData { 261 | isop := isOpaque(d.img) 262 | if isop != d.opaque { 263 | t.Errorf("isOpaque failed %#v, %v", d.img, isop) 264 | } 265 | } 266 | } 267 | 268 | func checkBoundsAndPix(b1, b2 image.Rectangle, pix1, pix2 []uint8) bool { 269 | if !b1.Eq(b2) { 270 | return false 271 | } 272 | if !bytes.Equal(pix1, pix2) { 273 | return false 274 | } 275 | return true 276 | } 277 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GO IMAGE FILTERING TOOLKIT (GIFT) 2 | 3 | [![GoDoc](https://godoc.org/github.com/disintegration/gift?status.svg)](https://godoc.org/github.com/disintegration/gift) 4 | [![Build Status](https://travis-ci.org/disintegration/gift.svg?branch=master)](https://travis-ci.org/disintegration/gift) 5 | [![Coverage Status](https://coveralls.io/repos/github/disintegration/gift/badge.svg?branch=master)](https://coveralls.io/github/disintegration/gift?branch=master) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/disintegration/gift)](https://goreportcard.com/report/github.com/disintegration/gift) 7 | 8 | 9 | *Package gift provides a set of useful image processing filters.* 10 | 11 | Pure Go. No external dependencies outside of the Go standard library. 12 | 13 | 14 | ### INSTALLATION / UPDATING 15 | 16 | go get -u github.com/disintegration/gift 17 | 18 | 19 | ### DOCUMENTATION 20 | 21 | http://godoc.org/github.com/disintegration/gift 22 | 23 | 24 | ### QUICK START 25 | 26 | ```go 27 | // 1. Create a new filter list and add some filters. 28 | g := gift.New( 29 | gift.Resize(800, 0, gift.LanczosResampling), 30 | gift.UnsharpMask(1, 1, 0), 31 | ) 32 | 33 | // 2. Create a new image of the corresponding size. 34 | // dst is a new target image, src is the original image. 35 | dst := image.NewRGBA(g.Bounds(src.Bounds())) 36 | 37 | // 3. Use the Draw func to apply the filters to src and store the result in dst. 38 | g.Draw(dst, src) 39 | ``` 40 | 41 | ### USAGE 42 | 43 | To create a sequence of filters, the `New` function is used: 44 | ```go 45 | g := gift.New( 46 | gift.Grayscale(), 47 | gift.Contrast(10), 48 | ) 49 | ``` 50 | Filters also can be added using the `Add` method: 51 | ```go 52 | g.Add(GaussianBlur(2)) 53 | ``` 54 | 55 | The `Bounds` method takes the bounds of the source image and returns appropriate bounds for the destination image to fit the result (for example, after using `Resize` or `Rotate` filters). 56 | 57 | ```go 58 | dst := image.NewRGBA(g.Bounds(src.Bounds())) 59 | ``` 60 | 61 | There are two methods available to apply these filters to an image: 62 | 63 | - `Draw` applies all the added filters to the src image and outputs the result to the dst image starting from the top-left corner (Min point). 64 | ```go 65 | g.Draw(dst, src) 66 | ``` 67 | 68 | - `DrawAt` provides more control. It outputs the filtered src image to the dst image at the specified position using the specified image composition operator. This example is equivalent to the previous: 69 | ```go 70 | g.DrawAt(dst, src, dst.Bounds().Min, gift.CopyOperator) 71 | ``` 72 | 73 | Two image composition operators are supported by now: 74 | - `CopyOperator` - Replaces pixels of the dst image with pixels of the filtered src image. This mode is used by the Draw method. 75 | - `OverOperator` - Places the filtered src image on top of the dst image. This mode makes sence if the filtered src image has transparent areas. 76 | 77 | Empty filter list can be used to create a copy of an image or to paste one image to another. For example: 78 | ```go 79 | // Create a new image with dimensions of the bgImage. 80 | dstImage := image.NewRGBA(bgImage.Bounds()) 81 | // Copy the bgImage to the dstImage. 82 | gift.New().Draw(dstImage, bgImage) 83 | // Draw the fgImage over the dstImage at the (100, 100) position. 84 | gift.New().DrawAt(dstImage, fgImage, image.Pt(100, 100), gift.OverOperator) 85 | ``` 86 | 87 | 88 | ### SUPPORTED FILTERS 89 | 90 | + Transformations 91 | 92 | - Crop(rect image.Rectangle) 93 | - CropToSize(width, height int, anchor Anchor) 94 | - FlipHorizontal() 95 | - FlipVertical() 96 | - Resize(width, height int, resampling Resampling) 97 | - ResizeToFill(width, height int, resampling Resampling, anchor Anchor) 98 | - ResizeToFit(width, height int, resampling Resampling) 99 | - Rotate(angle float32, backgroundColor color.Color, interpolation Interpolation) 100 | - Rotate180() 101 | - Rotate270() 102 | - Rotate90() 103 | - Transpose() 104 | - Transverse() 105 | 106 | + Adjustments & effects 107 | 108 | - Brightness(percentage float32) 109 | - ColorBalance(percentageRed, percentageGreen, percentageBlue float32) 110 | - ColorFunc(fn func(r0, g0, b0, a0 float32) (r, g, b, a float32)) 111 | - Colorize(hue, saturation, percentage float32) 112 | - ColorspaceLinearToSRGB() 113 | - ColorspaceSRGBToLinear() 114 | - Contrast(percentage float32) 115 | - Convolution(kernel []float32, normalize, alpha, abs bool, delta float32) 116 | - Gamma(gamma float32) 117 | - GaussianBlur(sigma float32) 118 | - Grayscale() 119 | - Hue(shift float32) 120 | - Invert() 121 | - Maximum(ksize int, disk bool) 122 | - Mean(ksize int, disk bool) 123 | - Median(ksize int, disk bool) 124 | - Minimum(ksize int, disk bool) 125 | - Pixelate(size int) 126 | - Saturation(percentage float32) 127 | - Sepia(percentage float32) 128 | - Sigmoid(midpoint, factor float32) 129 | - Sobel() 130 | - Threshold(percentage float32) 131 | - UnsharpMask(sigma, amount, threshold float32) 132 | 133 | 134 | ### FILTER EXAMPLES 135 | 136 | The original image: 137 | 138 | ![](testdata/src.png) 139 | 140 | Resulting images after applying some of the filters: 141 | 142 | name / result | name / result | name / result | name / result 143 | --------------------------------------------|--------------------------------------------|--------------------------------------------|-------------------------------------------- 144 | resize | crop_to_size | rotate_180 | rotate_30 145 | ![](testdata/dst_resize.png) | ![](testdata/dst_crop_to_size.png) | ![](testdata/dst_rotate_180.png) | ![](testdata/dst_rotate_30.png) 146 | brightness_increase | brightness_decrease | contrast_increase | contrast_decrease 147 | ![](testdata/dst_brightness_increase.png) | ![](testdata/dst_brightness_decrease.png) | ![](testdata/dst_contrast_increase.png) | ![](testdata/dst_contrast_decrease.png) 148 | saturation_increase | saturation_decrease | gamma_1.5 | gamma_0.5 149 | ![](testdata/dst_saturation_increase.png) | ![](testdata/dst_saturation_decrease.png) | ![](testdata/dst_gamma_1.5.png) | ![](testdata/dst_gamma_0.5.png) 150 | gaussian_blur | unsharp_mask | sigmoid | pixelate 151 | ![](testdata/dst_gaussian_blur.png) | ![](testdata/dst_unsharp_mask.png) | ![](testdata/dst_sigmoid.png) | ![](testdata/dst_pixelate.png) 152 | colorize | grayscale | sepia | invert 153 | ![](testdata/dst_colorize.png) | ![](testdata/dst_grayscale.png) | ![](testdata/dst_sepia.png) | ![](testdata/dst_invert.png) 154 | mean | median | minimum | maximum 155 | ![](testdata/dst_mean.png) | ![](testdata/dst_median.png) | ![](testdata/dst_minimum.png) | ![](testdata/dst_maximum.png) 156 | hue_rotate | color_balance | color_func | convolution_emboss 157 | ![](testdata/dst_hue_rotate.png) | ![](testdata/dst_color_balance.png) | ![](testdata/dst_color_func.png) | ![](testdata/dst_convolution_emboss.png) 158 | 159 | Here's the code that produces the above images: 160 | 161 | ```go 162 | package main 163 | 164 | import ( 165 | "image" 166 | "image/color" 167 | "image/png" 168 | "log" 169 | "os" 170 | 171 | "github.com/disintegration/gift" 172 | ) 173 | 174 | func main() { 175 | src := loadImage("testdata/src.png") 176 | 177 | filters := map[string]gift.Filter{ 178 | "resize": gift.Resize(100, 0, gift.LanczosResampling), 179 | "crop_to_size": gift.CropToSize(100, 100, gift.LeftAnchor), 180 | "rotate_180": gift.Rotate180(), 181 | "rotate_30": gift.Rotate(30, color.Transparent, gift.CubicInterpolation), 182 | "brightness_increase": gift.Brightness(30), 183 | "brightness_decrease": gift.Brightness(-30), 184 | "contrast_increase": gift.Contrast(30), 185 | "contrast_decrease": gift.Contrast(-30), 186 | "saturation_increase": gift.Saturation(50), 187 | "saturation_decrease": gift.Saturation(-50), 188 | "gamma_1.5": gift.Gamma(1.5), 189 | "gamma_0.5": gift.Gamma(0.5), 190 | "gaussian_blur": gift.GaussianBlur(1), 191 | "unsharp_mask": gift.UnsharpMask(1, 1, 0), 192 | "sigmoid": gift.Sigmoid(0.5, 7), 193 | "pixelate": gift.Pixelate(5), 194 | "colorize": gift.Colorize(240, 50, 100), 195 | "grayscale": gift.Grayscale(), 196 | "sepia": gift.Sepia(100), 197 | "invert": gift.Invert(), 198 | "mean": gift.Mean(5, true), 199 | "median": gift.Median(5, true), 200 | "minimum": gift.Minimum(5, true), 201 | "maximum": gift.Maximum(5, true), 202 | "hue_rotate": gift.Hue(45), 203 | "color_balance": gift.ColorBalance(10, -10, -10), 204 | "color_func": gift.ColorFunc( 205 | func(r0, g0, b0, a0 float32) (r, g, b, a float32) { 206 | r = 1 - r0 // invert the red channel 207 | g = g0 + 0.1 // shift the green channel by 0.1 208 | b = 0 // set the blue channel to 0 209 | a = a0 // preserve the alpha channel 210 | return r, g, b, a 211 | }, 212 | ), 213 | "convolution_emboss": gift.Convolution( 214 | []float32{ 215 | -1, -1, 0, 216 | -1, 1, 1, 217 | 0, 1, 1, 218 | }, 219 | false, false, false, 0.0, 220 | ), 221 | } 222 | 223 | for name, filter := range filters { 224 | g := gift.New(filter) 225 | dst := image.NewNRGBA(g.Bounds(src.Bounds())) 226 | g.Draw(dst, src) 227 | saveImage("testdata/dst_"+name+".png", dst) 228 | } 229 | } 230 | 231 | func loadImage(filename string) image.Image { 232 | f, err := os.Open(filename) 233 | if err != nil { 234 | log.Fatalf("os.Open failed: %v", err) 235 | } 236 | defer f.Close() 237 | img, _, err := image.Decode(f) 238 | if err != nil { 239 | log.Fatalf("image.Decode failed: %v", err) 240 | } 241 | return img 242 | } 243 | 244 | func saveImage(filename string, img image.Image) { 245 | f, err := os.Create(filename) 246 | if err != nil { 247 | log.Fatalf("os.Create failed: %v", err) 248 | } 249 | defer f.Close() 250 | err = png.Encode(f, img) 251 | if err != nil { 252 | log.Fatalf("png.Encode failed: %v", err) 253 | } 254 | } 255 | ``` 256 | -------------------------------------------------------------------------------- /resize_test.go: -------------------------------------------------------------------------------- 1 | package gift 2 | 3 | import ( 4 | "bytes" 5 | "image" 6 | "testing" 7 | ) 8 | 9 | func TestResize(t *testing.T) { 10 | var img0, img1 *image.Gray 11 | 12 | // Testing various sizes and parallelization settings 13 | w, h := 10, 20 14 | img0 = image.NewGray(image.Rect(0, 0, w, h)) 15 | sz := []struct{ w0, h0, w1, h1 int }{ 16 | {w, h, w, h}, 17 | {w * 2, h, w * 2, h}, 18 | {w, h * 2, w, h * 2}, 19 | {w * 2, h * 2, w * 2, h * 2}, 20 | {w / 2, h, w / 2, h}, 21 | {w, 0, w, h}, 22 | {0, h, w, h}, 23 | {w * 2, 0, w * 2, h * 2}, 24 | {0, h / 2, w / 2, h / 2}, 25 | {0, 0, 0, 0}, 26 | {1, -1, 0, 0}, 27 | {-1, 1, 0, 0}, 28 | } 29 | rfilters := []Resampling{ 30 | NearestNeighborResampling, 31 | BoxResampling, 32 | LinearResampling, 33 | CubicResampling, 34 | LanczosResampling, 35 | } 36 | for _, prlz := range []bool{true, false} { 37 | for _, z := range sz { 38 | for _, f := range rfilters { 39 | g := New(Resize(z.w0, z.h0, f)) 40 | g.SetParallelization(prlz) 41 | img1 = image.NewGray(g.Bounds(img0.Bounds())) 42 | g.Draw(img1, img0) 43 | w2, h2 := img1.Bounds().Dx(), img1.Bounds().Dy() 44 | if w2 != z.w1 || h2 != z.h1 { 45 | t.Errorf("resize %s %dx%d: expected %dx%d got %dx%d", f, z.w0, z.h0, z.w1, z.h1, w2, h2) 46 | } 47 | } 48 | } 49 | } 50 | 51 | // Nearest filter resize 52 | img0 = image.NewGray(image.Rect(-1, -1, 4, 1)) 53 | img0.Pix = []uint8{ 54 | 1, 2, 3, 4, 5, 55 | 6, 7, 8, 0, 1, 56 | } 57 | img1Exp := image.NewGray(image.Rect(0, 0, 2, 2)) 58 | img1Exp.Pix = []uint8{ 59 | 2, 4, 60 | 7, 0, 61 | } 62 | f := Resize(2, 2, NearestNeighborResampling) 63 | img1 = image.NewGray(f.Bounds(img0.Bounds())) 64 | f.Draw(img1, img0, nil) 65 | if img1.Bounds().Size() != img1Exp.Bounds().Size() { 66 | t.Errorf("expected %v got %v", img1Exp.Bounds().Size(), img1.Bounds().Size()) 67 | } 68 | if !bytes.Equal(img1Exp.Pix, img1.Pix) { 69 | t.Errorf("expected %v got %v", img1Exp.Pix, img1.Pix) 70 | } 71 | 72 | // Box Filter resize 73 | img0 = image.NewGray(image.Rect(-1, -1, 3, 1)) 74 | img0.Pix = []uint8{ 75 | 1, 2, 2, 1, 76 | 4, 5, 8, 9, 77 | } 78 | img1Exp = image.NewGray(image.Rect(0, 0, 2, 1)) 79 | img1Exp.Pix = []uint8{ 80 | 3, 5, 81 | } 82 | f = Resize(2, 1, BoxResampling) 83 | img1 = image.NewGray(f.Bounds(img0.Bounds())) 84 | f.Draw(img1, img0, nil) 85 | if img1.Bounds().Size() != img1Exp.Bounds().Size() { 86 | t.Errorf("expected %v got %v", img1Exp.Bounds().Size(), img1.Bounds().Size()) 87 | } 88 | if !bytes.Equal(img1Exp.Pix, img1.Pix) { 89 | t.Errorf("expected %v got %v", img1Exp.Pix, img1.Pix) 90 | } 91 | 92 | // Empty image should remain empty and not panic 93 | img0 = &image.Gray{} 94 | f = Resize(100, 100, BoxResampling) 95 | img1 = image.NewGray(f.Bounds(img0.Bounds())) 96 | f.Draw(img1, img0, nil) 97 | if img1.Bounds().Dx() != 0 || img1.Bounds().Dy() != 0 { 98 | t.Errorf("empty image resized is not empty: %dx%d", img1.Bounds().Dx(), img1.Bounds().Dy()) 99 | } 100 | 101 | // Testing kernel values outside the window 102 | for _, f := range rfilters { 103 | if f.Kernel(f.Support()+0.000001) != 0 { 104 | t.Errorf("filter %s value outside support != 0", f) 105 | } 106 | } 107 | 108 | // Testing spline and sinc edge cases 109 | if sinc(0) != 1 { 110 | t.Errorf("sinc(0) != 1") 111 | } 112 | if bcspline(-2, 0, 0.5) != 0 { 113 | t.Errorf("bcspline(-2, ...) != 0") 114 | } 115 | 116 | if (resamp{name: "test"}).String() != "test" { 117 | t.Error("resamplingStruct String() fail") 118 | } 119 | 120 | testData := []struct { 121 | desc string 122 | w, h int 123 | r Resampling 124 | srcb, dstb image.Rectangle 125 | srcPix, dstPix []uint8 126 | }{ 127 | { 128 | "resize (1, 2 -> 1, 1; box; non-alpha)", 129 | 1, 1, BoxResampling, 130 | image.Rect(0, 0, 1, 2), 131 | image.Rect(0, 0, 1, 1), 132 | []uint8{ 133 | 0xff, 0x00, 0x00, 0xff, 134 | 0x00, 0xff, 0x00, 0xff, 135 | }, 136 | []uint8{ 137 | 0x80, 0x80, 0x00, 0xff, 138 | }, 139 | }, 140 | { 141 | "resize (1, 2 -> 1, 1; box; alpha)", 142 | 1, 1, BoxResampling, 143 | image.Rect(0, 0, 1, 2), 144 | image.Rect(0, 0, 1, 1), 145 | []uint8{ 146 | 0xff, 0x00, 0x00, 0xff, 147 | 0x00, 0xff, 0x00, 0x00, 148 | }, 149 | []uint8{ 150 | 0xff, 0x00, 0x00, 0x80, 151 | }, 152 | }, 153 | { 154 | "resize (1, 2 -> 1, 3; linear; alpha)", 155 | 1, 3, LinearResampling, 156 | image.Rect(0, 0, 1, 2), 157 | image.Rect(0, 0, 1, 3), 158 | []uint8{ 159 | 0xff, 0x00, 0x00, 0xff, 160 | 0x00, 0xff, 0x00, 0x00, 161 | }, 162 | []uint8{ 163 | 0xff, 0x00, 0x00, 0xff, 164 | 0xff, 0x00, 0x00, 0x80, 165 | 0x00, 0x00, 0x00, 0x00, 166 | }, 167 | }, 168 | } 169 | 170 | for _, d := range testData { 171 | src := image.NewNRGBA(d.srcb) 172 | src.Pix = d.srcPix 173 | 174 | f := Resize(d.w, d.h, d.r) 175 | dst := image.NewNRGBA(f.Bounds(src.Bounds())) 176 | f.Draw(dst, src, nil) 177 | 178 | if !checkBoundsAndPix(dst.Bounds(), d.dstb, dst.Pix, d.dstPix) { 179 | t.Errorf("test [%s] failed: %#v, %#v", d.desc, dst.Bounds(), dst.Pix) 180 | } 181 | } 182 | } 183 | 184 | func TestResizeToFit(t *testing.T) { 185 | testData := []struct { 186 | desc string 187 | w, h int 188 | r Resampling 189 | srcb, dstb image.Rectangle 190 | srcPix, dstPix []uint8 191 | }{ 192 | { 193 | "resize to fit (0, 0, nearest)", 194 | 0, 0, NearestNeighborResampling, 195 | image.Rect(-1, -1, 4, 4), 196 | image.Rect(0, 0, 0, 0), 197 | []uint8{ 198 | 0x00, 0x01, 0x02, 0x03, 0x04, 199 | 0x05, 0x06, 0x07, 0x08, 0x09, 200 | 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 201 | 0x0f, 0x10, 0x11, 0x12, 0x13, 202 | 0x14, 0x15, 0x16, 0x17, 0x18, 203 | }, 204 | []uint8{}, 205 | }, 206 | { 207 | "resize to fit (1, 1, nearest)", 208 | 1, 1, NearestNeighborResampling, 209 | image.Rect(-1, -1, 4, 4), 210 | image.Rect(0, 0, 1, 1), 211 | []uint8{ 212 | 0x00, 0x01, 0x02, 0x03, 0x04, 213 | 0x05, 0x06, 0x07, 0x08, 0x09, 214 | 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 215 | 0x0f, 0x10, 0x11, 0x12, 0x13, 216 | 0x14, 0x15, 0x16, 0x17, 0x18, 217 | }, 218 | []uint8{0x0c}, 219 | }, 220 | { 221 | "resize to fit (2, 3, box)", 222 | 2, 3, BoxResampling, 223 | image.Rect(-1, -1, 3, 1), 224 | image.Rect(0, 0, 2, 1), 225 | []uint8{ 226 | 0x00, 0x01, 0x02, 0x03, 227 | 0x05, 0x06, 0x07, 0x08, 228 | }, 229 | []uint8{0x03, 0x05}, 230 | }, 231 | { 232 | "resize to fit (3, 2, box)", 233 | 3, 2, BoxResampling, 234 | image.Rect(-1, -1, 1, 3), 235 | image.Rect(0, 0, 1, 2), 236 | []uint8{ 237 | 0x00, 0x01, 238 | 0x05, 0x06, 239 | 0x02, 0x03, 240 | 0x07, 0x08, 241 | }, 242 | []uint8{0x03, 0x05}, 243 | }, 244 | { 245 | "resize to fit (2, 4, box)", 246 | 2, 4, BoxResampling, 247 | image.Rect(-1, -1, 1, 3), 248 | image.Rect(0, 0, 2, 4), 249 | []uint8{ 250 | 0x00, 0x01, 251 | 0x05, 0x06, 252 | 0x02, 0x03, 253 | 0x07, 0x08, 254 | }, 255 | []uint8{ 256 | 0x00, 0x01, 257 | 0x05, 0x06, 258 | 0x02, 0x03, 259 | 0x07, 0x08, 260 | }, 261 | }, 262 | { 263 | "resize to fit (3, 10, box)", 264 | 3, 10, BoxResampling, 265 | image.Rect(-1, -1, 1, 3), 266 | image.Rect(0, 0, 2, 4), 267 | []uint8{ 268 | 0x00, 0x01, 269 | 0x05, 0x06, 270 | 0x02, 0x03, 271 | 0x07, 0x08, 272 | }, 273 | []uint8{ 274 | 0x00, 0x01, 275 | 0x05, 0x06, 276 | 0x02, 0x03, 277 | 0x07, 0x08, 278 | }, 279 | }, 280 | } 281 | 282 | for _, d := range testData { 283 | src := image.NewGray(d.srcb) 284 | src.Pix = d.srcPix 285 | 286 | f := ResizeToFit(d.w, d.h, d.r) 287 | dst := image.NewGray(f.Bounds(src.Bounds())) 288 | f.Draw(dst, src, nil) 289 | 290 | if !checkBoundsAndPix(dst.Bounds(), d.dstb, dst.Pix, d.dstPix) { 291 | t.Errorf("test [%s] failed: %#v, %#v", d.desc, dst.Bounds(), dst.Pix) 292 | } 293 | } 294 | } 295 | 296 | func TestResizeToFill(t *testing.T) { 297 | testData := []struct { 298 | desc string 299 | w, h int 300 | r Resampling 301 | anchor Anchor 302 | srcb, dstb image.Rectangle 303 | srcPix, dstPix []uint8 304 | }{ 305 | { 306 | "resize to fill (0, 0, nearest, center)", 307 | 0, 0, NearestNeighborResampling, CenterAnchor, 308 | image.Rect(-1, -1, 3, 1), 309 | image.Rect(0, 0, 0, 0), 310 | []uint8{ 311 | 0x00, 0x01, 0x02, 0x03, 312 | 0x04, 0x05, 0x06, 0x07, 313 | }, 314 | []uint8{}, 315 | }, 316 | { 317 | "resize to fill (4, 2, nearest, center)", 318 | 4, 2, NearestNeighborResampling, CenterAnchor, 319 | image.Rect(-1, -1, 3, 1), 320 | image.Rect(0, 0, 4, 2), 321 | []uint8{ 322 | 0x00, 0x01, 0x02, 0x03, 323 | 0x04, 0x05, 0x06, 0x07, 324 | }, 325 | []uint8{ 326 | 0x00, 0x01, 0x02, 0x03, 327 | 0x04, 0x05, 0x06, 0x07, 328 | }, 329 | }, 330 | { 331 | "resize to fill (4, 4, nearest, center)", 332 | 4, 4, NearestNeighborResampling, CenterAnchor, 333 | image.Rect(-1, -1, 3, 1), 334 | image.Rect(0, 0, 4, 4), 335 | []uint8{ 336 | 0x00, 0x01, 0x02, 0x03, 337 | 0x04, 0x05, 0x06, 0x07, 338 | }, 339 | []uint8{ 340 | 0x01, 0x01, 0x02, 0x02, 341 | 0x01, 0x01, 0x02, 0x02, 342 | 0x05, 0x05, 0x06, 0x06, 343 | 0x05, 0x05, 0x06, 0x06, 344 | }, 345 | }, 346 | { 347 | "resize to fill (4, 4, nearest, bottom-right)", 348 | 4, 4, NearestNeighborResampling, BottomRightAnchor, 349 | image.Rect(-1, -1, 1, 3), 350 | image.Rect(0, 0, 4, 4), 351 | []uint8{ 352 | 0x00, 0x01, 353 | 0x02, 0x03, 354 | 0x04, 0x05, 355 | 0x06, 0x07, 356 | }, 357 | []uint8{ 358 | 0x04, 0x04, 0x05, 0x05, 359 | 0x04, 0x04, 0x05, 0x05, 360 | 0x06, 0x06, 0x07, 0x07, 361 | 0x06, 0x06, 0x07, 0x07, 362 | }, 363 | }, 364 | { 365 | "resize to fill (2, 1, nearest, bottom)", 366 | 2, 1, NearestNeighborResampling, BottomAnchor, 367 | image.Rect(-1, -1, 5, 5), 368 | image.Rect(0, 0, 2, 1), 369 | []uint8{ 370 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 371 | 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 372 | 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 373 | 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 374 | 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 375 | 0xac, 0xad, 0xae, 0xaf, 0xb0, 0xb1, 376 | }, 377 | []uint8{ 378 | 0xa7, 0xaa, 379 | }, 380 | }, 381 | { 382 | "resize to fill (2, 1, nearest, top)", 383 | 2, 1, NearestNeighborResampling, TopAnchor, 384 | image.Rect(-1, -1, 5, 5), 385 | image.Rect(0, 0, 2, 1), 386 | []uint8{ 387 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 388 | 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 389 | 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 390 | 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 391 | 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 392 | 0xac, 0xad, 0xae, 0xaf, 0xb0, 0xb1, 393 | }, 394 | []uint8{ 395 | 0x07, 0x0a, 396 | }, 397 | }, 398 | } 399 | 400 | for _, d := range testData { 401 | src := image.NewGray(d.srcb) 402 | src.Pix = d.srcPix 403 | 404 | f := ResizeToFill(d.w, d.h, d.r, d.anchor) 405 | dst := image.NewGray(f.Bounds(src.Bounds())) 406 | f.Draw(dst, src, nil) 407 | 408 | if !checkBoundsAndPix(dst.Bounds(), d.dstb, dst.Pix, d.dstPix) { 409 | t.Errorf("test [%s] failed: %#v, %#v", d.desc, dst.Bounds(), dst.Pix) 410 | } 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /pixels.go: -------------------------------------------------------------------------------- 1 | package gift 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/draw" 7 | ) 8 | 9 | type pixel struct { 10 | r, g, b, a float32 11 | } 12 | 13 | type imageType int 14 | 15 | const ( 16 | itGeneric imageType = iota 17 | itNRGBA 18 | itNRGBA64 19 | itRGBA 20 | itRGBA64 21 | itYCbCr 22 | itGray 23 | itGray16 24 | itPaletted 25 | ) 26 | 27 | type pixelGetter struct { 28 | it imageType 29 | bounds image.Rectangle 30 | image image.Image 31 | nrgba *image.NRGBA 32 | nrgba64 *image.NRGBA64 33 | rgba *image.RGBA 34 | rgba64 *image.RGBA64 35 | gray *image.Gray 36 | gray16 *image.Gray16 37 | ycbcr *image.YCbCr 38 | paletted *image.Paletted 39 | palette []pixel 40 | } 41 | 42 | func newPixelGetter(img image.Image) *pixelGetter { 43 | switch img := img.(type) { 44 | case *image.NRGBA: 45 | return &pixelGetter{ 46 | it: itNRGBA, 47 | bounds: img.Bounds(), 48 | nrgba: img, 49 | } 50 | 51 | case *image.NRGBA64: 52 | return &pixelGetter{ 53 | it: itNRGBA64, 54 | bounds: img.Bounds(), 55 | nrgba64: img, 56 | } 57 | 58 | case *image.RGBA: 59 | return &pixelGetter{ 60 | it: itRGBA, 61 | bounds: img.Bounds(), 62 | rgba: img, 63 | } 64 | 65 | case *image.RGBA64: 66 | return &pixelGetter{ 67 | it: itRGBA64, 68 | bounds: img.Bounds(), 69 | rgba64: img, 70 | } 71 | 72 | case *image.Gray: 73 | return &pixelGetter{ 74 | it: itGray, 75 | bounds: img.Bounds(), 76 | gray: img, 77 | } 78 | 79 | case *image.Gray16: 80 | return &pixelGetter{ 81 | it: itGray16, 82 | bounds: img.Bounds(), 83 | gray16: img, 84 | } 85 | 86 | case *image.YCbCr: 87 | return &pixelGetter{ 88 | it: itYCbCr, 89 | bounds: img.Bounds(), 90 | ycbcr: img, 91 | } 92 | 93 | case *image.Paletted: 94 | return &pixelGetter{ 95 | it: itPaletted, 96 | bounds: img.Bounds(), 97 | paletted: img, 98 | palette: convertPalette(img.Palette), 99 | } 100 | 101 | default: 102 | return &pixelGetter{ 103 | it: itGeneric, 104 | bounds: img.Bounds(), 105 | image: img, 106 | } 107 | } 108 | } 109 | 110 | const ( 111 | qf8 = 1.0 / 0xff 112 | qf16 = 1.0 / 0xffff 113 | epal = qf16 * qf16 / 2 114 | ) 115 | 116 | func pixelFromColor(c color.Color) (px pixel) { 117 | r16, g16, b16, a16 := c.RGBA() 118 | switch a16 { 119 | case 0: 120 | px = pixel{0, 0, 0, 0} 121 | case 0xffff: 122 | r := float32(r16) * qf16 123 | g := float32(g16) * qf16 124 | b := float32(b16) * qf16 125 | px = pixel{r, g, b, 1} 126 | default: 127 | q := float32(1) / float32(a16) 128 | r := float32(r16) * q 129 | g := float32(g16) * q 130 | b := float32(b16) * q 131 | a := float32(a16) * qf16 132 | px = pixel{r, g, b, a} 133 | } 134 | return px 135 | } 136 | 137 | func convertPalette(p []color.Color) []pixel { 138 | pal := make([]pixel, len(p)) 139 | for i := 0; i < len(p); i++ { 140 | pal[i] = pixelFromColor(p[i]) 141 | } 142 | return pal 143 | } 144 | 145 | func getPaletteIndex(pal []pixel, px pixel) int { 146 | var k int 147 | var dmin float32 = 4 148 | for i, palpx := range pal { 149 | d := px.r - palpx.r 150 | dcur := d * d 151 | d = px.g - palpx.g 152 | dcur += d * d 153 | d = px.b - palpx.b 154 | dcur += d * d 155 | d = px.a - palpx.a 156 | dcur += d * d 157 | if dcur < epal { 158 | return i 159 | } 160 | if dcur < dmin { 161 | dmin = dcur 162 | k = i 163 | } 164 | } 165 | return k 166 | } 167 | 168 | func (p *pixelGetter) getPixel(x, y int) pixel { 169 | switch p.it { 170 | case itNRGBA: 171 | i := p.nrgba.PixOffset(x, y) 172 | r := float32(p.nrgba.Pix[i+0]) * qf8 173 | g := float32(p.nrgba.Pix[i+1]) * qf8 174 | b := float32(p.nrgba.Pix[i+2]) * qf8 175 | a := float32(p.nrgba.Pix[i+3]) * qf8 176 | return pixel{r, g, b, a} 177 | 178 | case itNRGBA64: 179 | i := p.nrgba64.PixOffset(x, y) 180 | r := float32(uint16(p.nrgba64.Pix[i+0])<<8|uint16(p.nrgba64.Pix[i+1])) * qf16 181 | g := float32(uint16(p.nrgba64.Pix[i+2])<<8|uint16(p.nrgba64.Pix[i+3])) * qf16 182 | b := float32(uint16(p.nrgba64.Pix[i+4])<<8|uint16(p.nrgba64.Pix[i+5])) * qf16 183 | a := float32(uint16(p.nrgba64.Pix[i+6])<<8|uint16(p.nrgba64.Pix[i+7])) * qf16 184 | return pixel{r, g, b, a} 185 | 186 | case itRGBA: 187 | i := p.rgba.PixOffset(x, y) 188 | a8 := p.rgba.Pix[i+3] 189 | switch a8 { 190 | case 0xff: 191 | r := float32(p.rgba.Pix[i+0]) * qf8 192 | g := float32(p.rgba.Pix[i+1]) * qf8 193 | b := float32(p.rgba.Pix[i+2]) * qf8 194 | return pixel{r, g, b, 1} 195 | case 0: 196 | return pixel{0, 0, 0, 0} 197 | default: 198 | q := float32(1) / float32(a8) 199 | r := float32(p.rgba.Pix[i+0]) * q 200 | g := float32(p.rgba.Pix[i+1]) * q 201 | b := float32(p.rgba.Pix[i+2]) * q 202 | a := float32(a8) * qf8 203 | return pixel{r, g, b, a} 204 | } 205 | 206 | case itRGBA64: 207 | i := p.rgba64.PixOffset(x, y) 208 | a16 := uint16(p.rgba64.Pix[i+6])<<8 | uint16(p.rgba64.Pix[i+7]) 209 | switch a16 { 210 | case 0xffff: 211 | r := float32(uint16(p.rgba64.Pix[i+0])<<8|uint16(p.rgba64.Pix[i+1])) * qf16 212 | g := float32(uint16(p.rgba64.Pix[i+2])<<8|uint16(p.rgba64.Pix[i+3])) * qf16 213 | b := float32(uint16(p.rgba64.Pix[i+4])<<8|uint16(p.rgba64.Pix[i+5])) * qf16 214 | return pixel{r, g, b, 1} 215 | case 0: 216 | return pixel{0, 0, 0, 0} 217 | default: 218 | q := float32(1) / float32(a16) 219 | r := float32(uint16(p.rgba64.Pix[i+0])<<8|uint16(p.rgba64.Pix[i+1])) * q 220 | g := float32(uint16(p.rgba64.Pix[i+2])<<8|uint16(p.rgba64.Pix[i+3])) * q 221 | b := float32(uint16(p.rgba64.Pix[i+4])<<8|uint16(p.rgba64.Pix[i+5])) * q 222 | a := float32(a16) * qf16 223 | return pixel{r, g, b, a} 224 | } 225 | 226 | case itGray: 227 | i := p.gray.PixOffset(x, y) 228 | v := float32(p.gray.Pix[i]) * qf8 229 | return pixel{v, v, v, 1} 230 | 231 | case itGray16: 232 | i := p.gray16.PixOffset(x, y) 233 | v := float32(uint16(p.gray16.Pix[i+0])<<8|uint16(p.gray16.Pix[i+1])) * qf16 234 | return pixel{v, v, v, 1} 235 | 236 | case itYCbCr: 237 | iy := (y-p.ycbcr.Rect.Min.Y)*p.ycbcr.YStride + (x - p.ycbcr.Rect.Min.X) 238 | 239 | var ic int 240 | switch p.ycbcr.SubsampleRatio { 241 | case image.YCbCrSubsampleRatio444: 242 | ic = (y-p.ycbcr.Rect.Min.Y)*p.ycbcr.CStride + (x - p.ycbcr.Rect.Min.X) 243 | case image.YCbCrSubsampleRatio422: 244 | ic = (y-p.ycbcr.Rect.Min.Y)*p.ycbcr.CStride + (x/2 - p.ycbcr.Rect.Min.X/2) 245 | case image.YCbCrSubsampleRatio420: 246 | ic = (y/2-p.ycbcr.Rect.Min.Y/2)*p.ycbcr.CStride + (x/2 - p.ycbcr.Rect.Min.X/2) 247 | case image.YCbCrSubsampleRatio440: 248 | ic = (y/2-p.ycbcr.Rect.Min.Y/2)*p.ycbcr.CStride + (x - p.ycbcr.Rect.Min.X) 249 | default: 250 | ic = p.ycbcr.COffset(x, y) 251 | } 252 | 253 | const ( 254 | max = 255 * 1e5 255 | inv = 1.0 / max 256 | ) 257 | 258 | y1 := int32(p.ycbcr.Y[iy]) * 1e5 259 | cb1 := int32(p.ycbcr.Cb[ic]) - 128 260 | cr1 := int32(p.ycbcr.Cr[ic]) - 128 261 | 262 | r1 := y1 + 140200*cr1 263 | g1 := y1 - 34414*cb1 - 71414*cr1 264 | b1 := y1 + 177200*cb1 265 | 266 | r := float32(clampi32(r1, 0, max)) * inv 267 | g := float32(clampi32(g1, 0, max)) * inv 268 | b := float32(clampi32(b1, 0, max)) * inv 269 | 270 | return pixel{r, g, b, 1} 271 | 272 | case itPaletted: 273 | i := p.paletted.PixOffset(x, y) 274 | k := p.paletted.Pix[i] 275 | return p.palette[k] 276 | } 277 | 278 | return pixelFromColor(p.image.At(x, y)) 279 | } 280 | 281 | func (p *pixelGetter) getPixelRow(y int, buf *[]pixel) { 282 | *buf = (*buf)[:0] 283 | for x := p.bounds.Min.X; x != p.bounds.Max.X; x++ { 284 | *buf = append(*buf, p.getPixel(x, y)) 285 | } 286 | } 287 | 288 | func (p *pixelGetter) getPixelColumn(x int, buf *[]pixel) { 289 | *buf = (*buf)[:0] 290 | for y := p.bounds.Min.Y; y != p.bounds.Max.Y; y++ { 291 | *buf = append(*buf, p.getPixel(x, y)) 292 | } 293 | } 294 | 295 | func f32u8(val float32) uint8 { 296 | x := int64(val + 0.5) 297 | if x > 0xff { 298 | return 0xff 299 | } 300 | if x > 0 { 301 | return uint8(x) 302 | } 303 | return 0 304 | } 305 | 306 | func f32u16(val float32) uint16 { 307 | x := int64(val + 0.5) 308 | if x > 0xffff { 309 | return 0xffff 310 | } 311 | if x > 0 { 312 | return uint16(x) 313 | } 314 | return 0 315 | } 316 | 317 | func clampi32(val, min, max int32) int32 { 318 | if val > max { 319 | return max 320 | } 321 | if val > min { 322 | return val 323 | } 324 | return 0 325 | } 326 | 327 | type pixelSetter struct { 328 | it imageType 329 | bounds image.Rectangle 330 | image draw.Image 331 | nrgba *image.NRGBA 332 | nrgba64 *image.NRGBA64 333 | rgba *image.RGBA 334 | rgba64 *image.RGBA64 335 | gray *image.Gray 336 | gray16 *image.Gray16 337 | paletted *image.Paletted 338 | palette []pixel 339 | } 340 | 341 | func newPixelSetter(img draw.Image) *pixelSetter { 342 | switch img := img.(type) { 343 | case *image.NRGBA: 344 | return &pixelSetter{ 345 | it: itNRGBA, 346 | bounds: img.Bounds(), 347 | nrgba: img, 348 | } 349 | 350 | case *image.NRGBA64: 351 | return &pixelSetter{ 352 | it: itNRGBA64, 353 | bounds: img.Bounds(), 354 | nrgba64: img, 355 | } 356 | 357 | case *image.RGBA: 358 | return &pixelSetter{ 359 | it: itRGBA, 360 | bounds: img.Bounds(), 361 | rgba: img, 362 | } 363 | 364 | case *image.RGBA64: 365 | return &pixelSetter{ 366 | it: itRGBA64, 367 | bounds: img.Bounds(), 368 | rgba64: img, 369 | } 370 | 371 | case *image.Gray: 372 | return &pixelSetter{ 373 | it: itGray, 374 | bounds: img.Bounds(), 375 | gray: img, 376 | } 377 | 378 | case *image.Gray16: 379 | return &pixelSetter{ 380 | it: itGray16, 381 | bounds: img.Bounds(), 382 | gray16: img, 383 | } 384 | 385 | case *image.Paletted: 386 | return &pixelSetter{ 387 | it: itPaletted, 388 | bounds: img.Bounds(), 389 | paletted: img, 390 | palette: convertPalette(img.Palette), 391 | } 392 | 393 | default: 394 | return &pixelSetter{ 395 | it: itGeneric, 396 | bounds: img.Bounds(), 397 | image: img, 398 | } 399 | } 400 | } 401 | 402 | func (p *pixelSetter) setPixel(x, y int, px pixel) { 403 | if !image.Pt(x, y).In(p.bounds) { 404 | return 405 | } 406 | switch p.it { 407 | case itNRGBA: 408 | i := p.nrgba.PixOffset(x, y) 409 | p.nrgba.Pix[i+0] = f32u8(px.r * 0xff) 410 | p.nrgba.Pix[i+1] = f32u8(px.g * 0xff) 411 | p.nrgba.Pix[i+2] = f32u8(px.b * 0xff) 412 | p.nrgba.Pix[i+3] = f32u8(px.a * 0xff) 413 | 414 | case itNRGBA64: 415 | r16 := f32u16(px.r * 0xffff) 416 | g16 := f32u16(px.g * 0xffff) 417 | b16 := f32u16(px.b * 0xffff) 418 | a16 := f32u16(px.a * 0xffff) 419 | i := p.nrgba64.PixOffset(x, y) 420 | p.nrgba64.Pix[i+0] = uint8(r16 >> 8) 421 | p.nrgba64.Pix[i+1] = uint8(r16 & 0xff) 422 | p.nrgba64.Pix[i+2] = uint8(g16 >> 8) 423 | p.nrgba64.Pix[i+3] = uint8(g16 & 0xff) 424 | p.nrgba64.Pix[i+4] = uint8(b16 >> 8) 425 | p.nrgba64.Pix[i+5] = uint8(b16 & 0xff) 426 | p.nrgba64.Pix[i+6] = uint8(a16 >> 8) 427 | p.nrgba64.Pix[i+7] = uint8(a16 & 0xff) 428 | 429 | case itRGBA: 430 | fa := px.a * 0xff 431 | i := p.rgba.PixOffset(x, y) 432 | p.rgba.Pix[i+0] = f32u8(px.r * fa) 433 | p.rgba.Pix[i+1] = f32u8(px.g * fa) 434 | p.rgba.Pix[i+2] = f32u8(px.b * fa) 435 | p.rgba.Pix[i+3] = f32u8(fa) 436 | 437 | case itRGBA64: 438 | fa := px.a * 0xffff 439 | r16 := f32u16(px.r * fa) 440 | g16 := f32u16(px.g * fa) 441 | b16 := f32u16(px.b * fa) 442 | a16 := f32u16(fa) 443 | i := p.rgba64.PixOffset(x, y) 444 | p.rgba64.Pix[i+0] = uint8(r16 >> 8) 445 | p.rgba64.Pix[i+1] = uint8(r16 & 0xff) 446 | p.rgba64.Pix[i+2] = uint8(g16 >> 8) 447 | p.rgba64.Pix[i+3] = uint8(g16 & 0xff) 448 | p.rgba64.Pix[i+4] = uint8(b16 >> 8) 449 | p.rgba64.Pix[i+5] = uint8(b16 & 0xff) 450 | p.rgba64.Pix[i+6] = uint8(a16 >> 8) 451 | p.rgba64.Pix[i+7] = uint8(a16 & 0xff) 452 | 453 | case itGray: 454 | i := p.gray.PixOffset(x, y) 455 | p.gray.Pix[i] = f32u8((0.299*px.r + 0.587*px.g + 0.114*px.b) * px.a * 0xff) 456 | 457 | case itGray16: 458 | i := p.gray16.PixOffset(x, y) 459 | y16 := f32u16((0.299*px.r + 0.587*px.g + 0.114*px.b) * px.a * 0xffff) 460 | p.gray16.Pix[i+0] = uint8(y16 >> 8) 461 | p.gray16.Pix[i+1] = uint8(y16 & 0xff) 462 | 463 | case itPaletted: 464 | px1 := pixel{ 465 | minf32(maxf32(px.r, 0), 1), 466 | minf32(maxf32(px.g, 0), 1), 467 | minf32(maxf32(px.b, 0), 1), 468 | minf32(maxf32(px.a, 0), 1), 469 | } 470 | i := p.paletted.PixOffset(x, y) 471 | k := getPaletteIndex(p.palette, px1) 472 | p.paletted.Pix[i] = uint8(k) 473 | 474 | case itGeneric: 475 | r16 := f32u16(px.r * 0xffff) 476 | g16 := f32u16(px.g * 0xffff) 477 | b16 := f32u16(px.b * 0xffff) 478 | a16 := f32u16(px.a * 0xffff) 479 | p.image.Set(x, y, color.NRGBA64{r16, g16, b16, a16}) 480 | } 481 | } 482 | 483 | func (p *pixelSetter) setPixelRow(y int, buf []pixel) { 484 | for i, x := 0, p.bounds.Min.X; i < len(buf); i, x = i+1, x+1 { 485 | p.setPixel(x, y, buf[i]) 486 | } 487 | } 488 | 489 | func (p *pixelSetter) setPixelColumn(x int, buf []pixel) { 490 | for i, y := 0, p.bounds.Min.Y; i < len(buf); i, y = i+1, y+1 { 491 | p.setPixel(x, y, buf[i]) 492 | } 493 | } 494 | -------------------------------------------------------------------------------- /resize.go: -------------------------------------------------------------------------------- 1 | package gift 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | "math" 7 | ) 8 | 9 | // Resampling is an interpolation algorithm used for image resizing. 10 | type Resampling interface { 11 | Support() float32 12 | Kernel(float32) float32 13 | } 14 | 15 | func bcspline(x, b, c float32) float32 { 16 | if x < 0 { 17 | x = -x 18 | } 19 | if x < 1 { 20 | return ((12-9*b-6*c)*x*x*x + (-18+12*b+6*c)*x*x + (6 - 2*b)) / 6 21 | } 22 | if x < 2 { 23 | return ((-b-6*c)*x*x*x + (6*b+30*c)*x*x + (-12*b-48*c)*x + (8*b + 24*c)) / 6 24 | } 25 | return 0 26 | } 27 | 28 | func sinc(x float32) float32 { 29 | if x == 0 { 30 | return 1 31 | } 32 | return float32(math.Sin(math.Pi*float64(x)) / (math.Pi * float64(x))) 33 | } 34 | 35 | type resamp struct { 36 | name string 37 | support float32 38 | kernel func(float32) float32 39 | } 40 | 41 | func (r resamp) String() string { 42 | return r.name 43 | } 44 | 45 | func (r resamp) Support() float32 { 46 | return r.support 47 | } 48 | 49 | func (r resamp) Kernel(x float32) float32 { 50 | return r.kernel(x) 51 | } 52 | 53 | // NearestNeighborResampling is a nearest neighbor resampling filter. 54 | var NearestNeighborResampling Resampling 55 | 56 | // BoxResampling is a box resampling filter (average of surrounding pixels). 57 | var BoxResampling Resampling 58 | 59 | // LinearResampling is a bilinear resampling filter. 60 | var LinearResampling Resampling 61 | 62 | // CubicResampling is a bicubic resampling filter (Catmull-Rom). 63 | var CubicResampling Resampling 64 | 65 | // LanczosResampling is a Lanczos resampling filter (3 lobes). 66 | var LanczosResampling Resampling 67 | 68 | type resampWeight struct { 69 | index int 70 | weight float32 71 | } 72 | 73 | func prepareResampWeights(dstSize, srcSize int, resampling Resampling) [][]resampWeight { 74 | delta := float32(srcSize) / float32(dstSize) 75 | scale := delta 76 | if scale < 1 { 77 | scale = 1 78 | } 79 | radius := float32(math.Ceil(float64(scale * resampling.Support()))) 80 | 81 | result := make([][]resampWeight, dstSize) 82 | tmp := make([]resampWeight, 0, dstSize*int(radius+2)*2) 83 | 84 | for i := 0; i < dstSize; i++ { 85 | center := (float32(i)+0.5)*delta - 0.5 86 | 87 | left := int(math.Ceil(float64(center - radius))) 88 | if left < 0 { 89 | left = 0 90 | } 91 | right := int(math.Floor(float64(center + radius))) 92 | if right > srcSize-1 { 93 | right = srcSize - 1 94 | } 95 | 96 | var sum float32 97 | for j := left; j <= right; j++ { 98 | weight := resampling.Kernel((float32(j) - center) / scale) 99 | if weight == 0 { 100 | continue 101 | } 102 | tmp = append(tmp, resampWeight{ 103 | index: j, 104 | weight: weight, 105 | }) 106 | sum += weight 107 | } 108 | 109 | for j := range tmp { 110 | tmp[j].weight /= sum 111 | } 112 | 113 | result[i] = tmp 114 | tmp = tmp[len(tmp):] 115 | } 116 | 117 | return result 118 | } 119 | 120 | func resizeLine(dst []pixel, src []pixel, weights [][]resampWeight) { 121 | for i := 0; i < len(dst); i++ { 122 | var r, g, b, a float32 123 | for _, w := range weights[i] { 124 | c := src[w.index] 125 | wa := c.a * w.weight 126 | r += c.r * wa 127 | g += c.g * wa 128 | b += c.b * wa 129 | a += wa 130 | } 131 | if a != 0 { 132 | r /= a 133 | g /= a 134 | b /= a 135 | } 136 | dst[i] = pixel{r, g, b, a} 137 | } 138 | } 139 | 140 | func resizeHorizontal(dst draw.Image, src image.Image, w int, resampling Resampling, options *Options) { 141 | srcb := src.Bounds() 142 | dstb := dst.Bounds() 143 | 144 | weights := prepareResampWeights(w, srcb.Dx(), resampling) 145 | 146 | pixGetter := newPixelGetter(src) 147 | pixSetter := newPixelSetter(dst) 148 | 149 | parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(start, stop int) { 150 | srcBuf := make([]pixel, srcb.Dx()) 151 | dstBuf := make([]pixel, w) 152 | for srcy := start; srcy < stop; srcy++ { 153 | pixGetter.getPixelRow(srcy, &srcBuf) 154 | resizeLine(dstBuf, srcBuf, weights) 155 | pixSetter.setPixelRow(dstb.Min.Y+srcy-srcb.Min.Y, dstBuf) 156 | } 157 | }) 158 | } 159 | 160 | func resizeVertical(dst draw.Image, src image.Image, h int, resampling Resampling, options *Options) { 161 | srcb := src.Bounds() 162 | dstb := dst.Bounds() 163 | 164 | weights := prepareResampWeights(h, srcb.Dy(), resampling) 165 | 166 | pixGetter := newPixelGetter(src) 167 | pixSetter := newPixelSetter(dst) 168 | 169 | parallelize(options.Parallelization, srcb.Min.X, srcb.Max.X, func(start, stop int) { 170 | srcBuf := make([]pixel, srcb.Dy()) 171 | dstBuf := make([]pixel, h) 172 | for srcx := start; srcx < stop; srcx++ { 173 | pixGetter.getPixelColumn(srcx, &srcBuf) 174 | resizeLine(dstBuf, srcBuf, weights) 175 | pixSetter.setPixelColumn(dstb.Min.X+srcx-srcb.Min.X, dstBuf) 176 | } 177 | }) 178 | } 179 | 180 | func resizeNearest(dst draw.Image, src image.Image, w, h int, options *Options) { 181 | srcb := src.Bounds() 182 | dstb := dst.Bounds() 183 | dx := float64(srcb.Dx()) / float64(w) 184 | dy := float64(srcb.Dy()) / float64(h) 185 | 186 | pixGetter := newPixelGetter(src) 187 | pixSetter := newPixelSetter(dst) 188 | 189 | parallelize(options.Parallelization, dstb.Min.Y, dstb.Min.Y+h, func(start, stop int) { 190 | for dsty := start; dsty < stop; dsty++ { 191 | for dstx := dstb.Min.X; dstx < dstb.Min.X+w; dstx++ { 192 | fx := math.Floor((float64(dstx-dstb.Min.X) + 0.5) * dx) 193 | fy := math.Floor((float64(dsty-dstb.Min.Y) + 0.5) * dy) 194 | srcx := srcb.Min.X + int(fx) 195 | srcy := srcb.Min.Y + int(fy) 196 | px := pixGetter.getPixel(srcx, srcy) 197 | pixSetter.setPixel(dstx, dsty, px) 198 | } 199 | } 200 | }) 201 | } 202 | 203 | type resizeFilter struct { 204 | width int 205 | height int 206 | resampling Resampling 207 | } 208 | 209 | func (p *resizeFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { 210 | w, h := p.width, p.height 211 | srcw, srch := srcBounds.Dx(), srcBounds.Dy() 212 | 213 | if (w == 0 && h == 0) || w < 0 || h < 0 || srcw <= 0 || srch <= 0 { 214 | dstBounds = image.Rect(0, 0, 0, 0) 215 | } else if w == 0 { 216 | fw := float64(h) * float64(srcw) / float64(srch) 217 | dstw := int(math.Max(1, math.Floor(fw+0.5))) 218 | dstBounds = image.Rect(0, 0, dstw, h) 219 | } else if h == 0 { 220 | fh := float64(w) * float64(srch) / float64(srcw) 221 | dsth := int(math.Max(1, math.Floor(fh+0.5))) 222 | dstBounds = image.Rect(0, 0, w, dsth) 223 | } else { 224 | dstBounds = image.Rect(0, 0, w, h) 225 | } 226 | 227 | return 228 | } 229 | 230 | func (p *resizeFilter) Draw(dst draw.Image, src image.Image, options *Options) { 231 | if options == nil { 232 | options = &defaultOptions 233 | } 234 | 235 | b := p.Bounds(src.Bounds()) 236 | w, h := b.Dx(), b.Dy() 237 | 238 | if w <= 0 || h <= 0 { 239 | return 240 | } 241 | 242 | if src.Bounds().Dx() == w && src.Bounds().Dy() == h { 243 | copyimage(dst, src, options) 244 | return 245 | } 246 | 247 | if p.resampling.Support() <= 0 { 248 | resizeNearest(dst, src, w, h, options) 249 | return 250 | } 251 | 252 | if src.Bounds().Dx() == w { 253 | resizeVertical(dst, src, h, p.resampling, options) 254 | return 255 | } 256 | 257 | if src.Bounds().Dy() == h { 258 | resizeHorizontal(dst, src, w, p.resampling, options) 259 | return 260 | } 261 | 262 | tmp := createTempImage(image.Rect(0, 0, w, src.Bounds().Dy())) 263 | resizeHorizontal(tmp, src, w, p.resampling, options) 264 | resizeVertical(dst, tmp, h, p.resampling, options) 265 | } 266 | 267 | // Resize creates a filter that resizes an image to the specified width and height using the specified resampling. 268 | // If one of width or height is 0, the image aspect ratio is preserved. 269 | // Supported resampling parameters: NearestNeighborResampling, BoxResampling, LinearResampling, CubicResampling, LanczosResampling. 270 | // 271 | // Example: 272 | // 273 | // // Resize the src image to width=300 preserving the aspect ratio. 274 | // g := gift.New( 275 | // gift.Resize(300, 0, gift.LanczosResampling), 276 | // ) 277 | // dst := image.NewRGBA(g.Bounds(src.Bounds())) 278 | // g.Draw(dst, src) 279 | // 280 | func Resize(width, height int, resampling Resampling) Filter { 281 | return &resizeFilter{ 282 | width: width, 283 | height: height, 284 | resampling: resampling, 285 | } 286 | } 287 | 288 | type resizeToFitFilter struct { 289 | width int 290 | height int 291 | resampling Resampling 292 | } 293 | 294 | func (p *resizeToFitFilter) Bounds(srcBounds image.Rectangle) image.Rectangle { 295 | w, h := p.width, p.height 296 | srcw, srch := srcBounds.Dx(), srcBounds.Dy() 297 | 298 | if w <= 0 || h <= 0 || srcw <= 0 || srch <= 0 { 299 | return image.Rect(0, 0, 0, 0) 300 | } 301 | 302 | if srcw <= w && srch <= h { 303 | return image.Rect(0, 0, srcw, srch) 304 | } 305 | 306 | wratio := float64(srcw) / float64(w) 307 | hratio := float64(srch) / float64(h) 308 | 309 | var dstw, dsth int 310 | if wratio > hratio { 311 | dstw = w 312 | dsth = minint(int(float64(srch)/wratio+0.5), h) 313 | } else { 314 | dsth = h 315 | dstw = minint(int(float64(srcw)/hratio+0.5), w) 316 | } 317 | 318 | return image.Rect(0, 0, dstw, dsth) 319 | } 320 | 321 | func (p *resizeToFitFilter) Draw(dst draw.Image, src image.Image, options *Options) { 322 | b := p.Bounds(src.Bounds()) 323 | Resize(b.Dx(), b.Dy(), p.resampling).Draw(dst, src, options) 324 | } 325 | 326 | // ResizeToFit creates a filter that resizes an image to fit within the specified dimensions while preserving the aspect ratio. 327 | // Supported resampling parameters: NearestNeighborResampling, BoxResampling, LinearResampling, CubicResampling, LanczosResampling. 328 | func ResizeToFit(width, height int, resampling Resampling) Filter { 329 | return &resizeToFitFilter{ 330 | width: width, 331 | height: height, 332 | resampling: resampling, 333 | } 334 | } 335 | 336 | type resizeToFillFilter struct { 337 | width int 338 | height int 339 | anchor Anchor 340 | resampling Resampling 341 | } 342 | 343 | func (p *resizeToFillFilter) Bounds(srcBounds image.Rectangle) image.Rectangle { 344 | w, h := p.width, p.height 345 | srcw, srch := srcBounds.Dx(), srcBounds.Dy() 346 | 347 | if w <= 0 || h <= 0 || srcw <= 0 || srch <= 0 { 348 | return image.Rect(0, 0, 0, 0) 349 | } 350 | 351 | return image.Rect(0, 0, w, h) 352 | } 353 | 354 | func (p *resizeToFillFilter) Draw(dst draw.Image, src image.Image, options *Options) { 355 | b := p.Bounds(src.Bounds()) 356 | w, h := b.Dx(), b.Dy() 357 | 358 | if w <= 0 || h <= 0 { 359 | return 360 | } 361 | 362 | srcw, srch := src.Bounds().Dx(), src.Bounds().Dy() 363 | 364 | wratio := float64(srcw) / float64(w) 365 | hratio := float64(srch) / float64(h) 366 | 367 | var tmpw, tmph int 368 | if wratio < hratio { 369 | tmpw = w 370 | tmph = maxint(int(float64(srch)/wratio+0.5), h) 371 | } else { 372 | tmph = h 373 | tmpw = maxint(int(float64(srcw)/hratio+0.5), w) 374 | } 375 | 376 | tmp := createTempImage(image.Rect(0, 0, tmpw, tmph)) 377 | Resize(tmpw, tmph, p.resampling).Draw(tmp, src, options) 378 | CropToSize(w, h, p.anchor).Draw(dst, tmp, options) 379 | } 380 | 381 | // ResizeToFill creates a filter that resizes an image to the smallest possible size that will cover the specified dimensions, 382 | // then crops the resized image to the specified dimensions using the specified anchor point. 383 | // Supported resampling parameters: NearestNeighborResampling, BoxResampling, LinearResampling, CubicResampling, LanczosResampling. 384 | func ResizeToFill(width, height int, resampling Resampling, anchor Anchor) Filter { 385 | return &resizeToFillFilter{ 386 | width: width, 387 | height: height, 388 | anchor: anchor, 389 | resampling: resampling, 390 | } 391 | } 392 | 393 | func init() { 394 | // Nearest neighbor resampling filter. 395 | NearestNeighborResampling = resamp{ 396 | name: "NearestNeighborResampling", 397 | support: 0, 398 | kernel: func(x float32) float32 { 399 | return 0 400 | }, 401 | } 402 | 403 | // Box resampling filter. 404 | BoxResampling = resamp{ 405 | name: "BoxResampling", 406 | support: 0.5, 407 | kernel: func(x float32) float32 { 408 | if x < 0 { 409 | x = -x 410 | } 411 | if x <= 0.5 { 412 | return 1 413 | } 414 | return 0 415 | }, 416 | } 417 | 418 | // Linear resampling filter. 419 | LinearResampling = resamp{ 420 | name: "LinearResampling", 421 | support: 1, 422 | kernel: func(x float32) float32 { 423 | if x < 0 { 424 | x = -x 425 | } 426 | if x < 1 { 427 | return 1 - x 428 | } 429 | return 0 430 | }, 431 | } 432 | 433 | // Cubic resampling filter (Catmull-Rom). 434 | CubicResampling = resamp{ 435 | name: "CubicResampling", 436 | support: 2, 437 | kernel: func(x float32) float32 { 438 | if x < 0 { 439 | x = -x 440 | } 441 | if x < 2 { 442 | return bcspline(x, 0, 0.5) 443 | } 444 | return 0 445 | }, 446 | } 447 | 448 | // Lanczos resampling filter (3 lobes). 449 | LanczosResampling = resamp{ 450 | name: "LanczosResampling", 451 | support: 3, 452 | kernel: func(x float32) float32 { 453 | if x < 0 { 454 | x = -x 455 | } 456 | if x < 3 { 457 | return sinc(x) * sinc(x/3) 458 | } 459 | return 0 460 | }, 461 | } 462 | } 463 | -------------------------------------------------------------------------------- /rank_test.go: -------------------------------------------------------------------------------- 1 | package gift 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | ) 7 | 8 | func TestMedian(t *testing.T) { 9 | testData := []struct { 10 | desc string 11 | ksize int 12 | disk bool 13 | srcb, dstb image.Rectangle 14 | srcPix, dstPix []uint8 15 | }{ 16 | { 17 | "median (0, false)", 18 | 0, false, 19 | image.Rect(-1, -1, 4, 2), 20 | image.Rect(0, 0, 5, 3), 21 | []uint8{ 22 | 0x11, 0x99, 0x55, 0x22, 0x66, 23 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 24 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 25 | }, 26 | []uint8{ 27 | 0x11, 0x99, 0x55, 0x22, 0x66, 28 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 29 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 30 | }, 31 | }, 32 | { 33 | "median (1, false)", 34 | 1, false, 35 | image.Rect(-1, -1, 4, 2), 36 | image.Rect(0, 0, 5, 3), 37 | []uint8{ 38 | 0x11, 0x99, 0x55, 0x22, 0x66, 39 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 40 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 41 | }, 42 | []uint8{ 43 | 0x11, 0x99, 0x55, 0x22, 0x66, 44 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 45 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 46 | }, 47 | }, 48 | { 49 | "median (2, false)", 50 | 2, false, 51 | image.Rect(-1, -1, 4, 2), 52 | image.Rect(0, 0, 5, 3), 53 | []uint8{ 54 | 0x11, 0x99, 0x55, 0x22, 0x66, 55 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 56 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 57 | }, 58 | []uint8{ 59 | 0x11, 0x99, 0x55, 0x22, 0x66, 60 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 61 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 62 | }, 63 | }, 64 | { 65 | "median (3, false)", 66 | 3, false, 67 | image.Rect(-1, -1, 4, 2), 68 | image.Rect(0, 0, 5, 3), 69 | []uint8{ 70 | 0x11, 0x99, 0x55, 0x22, 0x66, 71 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 72 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 73 | }, 74 | []uint8{ 75 | 0x44, 0x55, 0x55, 0x55, 0x66, 76 | 0x44, 0x77, 0x88, 0x88, 0x66, 77 | 0x44, 0x77, 0x88, 0xBB, 0xCC, 78 | }, 79 | }, 80 | { 81 | "median (3, true)", 82 | 3, true, 83 | image.Rect(-1, -1, 4, 2), 84 | image.Rect(0, 0, 5, 3), 85 | []uint8{ 86 | 0x11, 0x99, 0x55, 0x22, 0x66, 87 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 88 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 89 | }, 90 | []uint8{ 91 | 0x11, 0x55, 0x55, 0x55, 0x66, 92 | 0x44, 0x99, 0xBB, 0x88, 0x66, 93 | 0x33, 0x77, 0xBB, 0xBB, 0xEE, 94 | }, 95 | }, 96 | { 97 | "median (4, true)", 98 | 4, true, 99 | image.Rect(-1, -1, 4, 2), 100 | image.Rect(0, 0, 5, 3), 101 | []uint8{ 102 | 0x11, 0x99, 0x55, 0x22, 0x66, 103 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 104 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 105 | }, 106 | []uint8{ 107 | 0x11, 0x55, 0x55, 0x55, 0x66, 108 | 0x44, 0x99, 0xBB, 0x88, 0x66, 109 | 0x33, 0x77, 0xBB, 0xBB, 0xEE, 110 | }, 111 | }, 112 | } 113 | 114 | for _, d := range testData { 115 | src := image.NewGray(d.srcb) 116 | src.Pix = d.srcPix 117 | 118 | f := Median(d.ksize, d.disk) 119 | dst := image.NewGray(f.Bounds(src.Bounds())) 120 | f.Draw(dst, src, nil) 121 | 122 | if !checkBoundsAndPix(dst.Bounds(), d.dstb, dst.Pix, d.dstPix) { 123 | t.Errorf("test [%s] failed: %#v, %#v", d.desc, dst.Bounds(), dst.Pix) 124 | } 125 | } 126 | 127 | testDataNRGBA := []struct { 128 | desc string 129 | ksize int 130 | disk bool 131 | srcb, dstb image.Rectangle 132 | srcPix, dstPix []uint8 133 | }{ 134 | { 135 | "median nrgba (3, true)", 136 | 3, true, 137 | image.Rect(-1, -1, 4, 2), 138 | image.Rect(0, 0, 5, 3), 139 | []uint8{ 140 | 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x66, 141 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x00, 142 | 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0xBB, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0xEE, 143 | }, 144 | []uint8{ 145 | 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x66, 146 | 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0xBB, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x66, 147 | 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0xBB, 0x00, 0x00, 0x00, 0xBB, 0x00, 0x00, 0x00, 0xEE, 148 | }, 149 | }, 150 | } 151 | 152 | for _, d := range testDataNRGBA { 153 | src := image.NewNRGBA(d.srcb) 154 | src.Pix = d.srcPix 155 | 156 | f := Median(d.ksize, d.disk) 157 | dst := image.NewNRGBA(f.Bounds(src.Bounds())) 158 | f.Draw(dst, src, nil) 159 | 160 | if !checkBoundsAndPix(dst.Bounds(), d.dstb, dst.Pix, d.dstPix) { 161 | t.Errorf("test [%s] failed: %#v, %#v", d.desc, dst.Bounds(), dst.Pix) 162 | } 163 | } 164 | 165 | // check no panics 166 | Median(5, false).Draw(image.NewGray(image.Rect(0, 0, 1, 1)), image.NewGray(image.Rect(0, 0, 1, 1)), nil) 167 | Median(5, false).Draw(image.NewGray(image.Rect(0, 0, 0, 0)), image.NewGray(image.Rect(0, 0, 0, 0)), nil) 168 | } 169 | 170 | func TestMinimum(t *testing.T) { 171 | testData := []struct { 172 | desc string 173 | ksize int 174 | disk bool 175 | srcb, dstb image.Rectangle 176 | srcPix, dstPix []uint8 177 | }{ 178 | { 179 | "minimum (0, false)", 180 | 0, false, 181 | image.Rect(-1, -1, 4, 2), 182 | image.Rect(0, 0, 5, 3), 183 | []uint8{ 184 | 0x11, 0x99, 0x55, 0x22, 0x66, 185 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 186 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 187 | }, 188 | []uint8{ 189 | 0x11, 0x99, 0x55, 0x22, 0x66, 190 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 191 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 192 | }, 193 | }, 194 | { 195 | "minimum (1, false)", 196 | 1, false, 197 | image.Rect(-1, -1, 4, 2), 198 | image.Rect(0, 0, 5, 3), 199 | []uint8{ 200 | 0x11, 0x99, 0x55, 0x22, 0x66, 201 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 202 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 203 | }, 204 | []uint8{ 205 | 0x11, 0x99, 0x55, 0x22, 0x66, 206 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 207 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 208 | }, 209 | }, 210 | { 211 | "minimum (2, false)", 212 | 2, false, 213 | image.Rect(-1, -1, 4, 2), 214 | image.Rect(0, 0, 5, 3), 215 | []uint8{ 216 | 0x11, 0x99, 0x55, 0x22, 0x66, 217 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 218 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 219 | }, 220 | []uint8{ 221 | 0x11, 0x99, 0x55, 0x22, 0x66, 222 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 223 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 224 | }, 225 | }, 226 | { 227 | "minimum (3, false)", 228 | 3, false, 229 | image.Rect(-1, -1, 4, 2), 230 | image.Rect(0, 0, 5, 3), 231 | []uint8{ 232 | 0x11, 0x99, 0x55, 0x22, 0x66, 233 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 234 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 235 | }, 236 | []uint8{ 237 | 0x11, 0x11, 0x22, 0x00, 0x00, 238 | 0x11, 0x11, 0x22, 0x00, 0x00, 239 | 0x33, 0x33, 0x44, 0x00, 0x00, 240 | }, 241 | }, 242 | { 243 | "minimum (3, true)", 244 | 3, true, 245 | image.Rect(-1, -1, 4, 2), 246 | image.Rect(0, 0, 5, 3), 247 | []uint8{ 248 | 0x11, 0x99, 0x55, 0x22, 0x66, 249 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 250 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 251 | }, 252 | []uint8{ 253 | 0x11, 0x11, 0x22, 0x22, 0x00, 254 | 0x11, 0x44, 0x44, 0x00, 0x00, 255 | 0x33, 0x33, 0x77, 0x88, 0x00, 256 | }, 257 | }, 258 | { 259 | "minimum (4, true)", 260 | 4, true, 261 | image.Rect(-1, -1, 4, 2), 262 | image.Rect(0, 0, 5, 3), 263 | []uint8{ 264 | 0x11, 0x99, 0x55, 0x22, 0x66, 265 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 266 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 267 | }, 268 | []uint8{ 269 | 0x11, 0x11, 0x22, 0x22, 0x00, 270 | 0x11, 0x44, 0x44, 0x00, 0x00, 271 | 0x33, 0x33, 0x77, 0x88, 0x00, 272 | }, 273 | }, 274 | } 275 | 276 | for _, d := range testData { 277 | src := image.NewGray(d.srcb) 278 | src.Pix = d.srcPix 279 | 280 | f := Minimum(d.ksize, d.disk) 281 | dst := image.NewGray(f.Bounds(src.Bounds())) 282 | f.Draw(dst, src, nil) 283 | 284 | if !checkBoundsAndPix(dst.Bounds(), d.dstb, dst.Pix, d.dstPix) { 285 | t.Errorf("test [%s] failed: %#v, %#v", d.desc, dst.Bounds(), dst.Pix) 286 | } 287 | } 288 | 289 | testDataNRGBA := []struct { 290 | desc string 291 | ksize int 292 | disk bool 293 | srcb, dstb image.Rectangle 294 | srcPix, dstPix []uint8 295 | }{ 296 | { 297 | "minimum nrgba (3, true)", 298 | 3, true, 299 | image.Rect(-1, -1, 4, 2), 300 | image.Rect(0, 0, 5, 3), 301 | []uint8{ 302 | 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x66, 303 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x00, 304 | 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0xBB, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0xEE, 305 | }, 306 | []uint8{ 307 | 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 308 | 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 309 | 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x00, 310 | }, 311 | }, 312 | } 313 | 314 | for _, d := range testDataNRGBA { 315 | src := image.NewNRGBA(d.srcb) 316 | src.Pix = d.srcPix 317 | 318 | f := Minimum(d.ksize, d.disk) 319 | dst := image.NewNRGBA(f.Bounds(src.Bounds())) 320 | f.Draw(dst, src, nil) 321 | 322 | if !checkBoundsAndPix(dst.Bounds(), d.dstb, dst.Pix, d.dstPix) { 323 | t.Errorf("test [%s] failed: %#v, %#v", d.desc, dst.Bounds(), dst.Pix) 324 | } 325 | } 326 | } 327 | 328 | func TestMaximum(t *testing.T) { 329 | testData := []struct { 330 | desc string 331 | ksize int 332 | disk bool 333 | srcb, dstb image.Rectangle 334 | srcPix, dstPix []uint8 335 | }{ 336 | { 337 | "maximum (0, false)", 338 | 0, false, 339 | image.Rect(-1, -1, 4, 2), 340 | image.Rect(0, 0, 5, 3), 341 | []uint8{ 342 | 0x11, 0x99, 0x55, 0x22, 0x66, 343 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 344 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 345 | }, 346 | []uint8{ 347 | 0x11, 0x99, 0x55, 0x22, 0x66, 348 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 349 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 350 | }, 351 | }, 352 | { 353 | "maximum (1, false)", 354 | 1, false, 355 | image.Rect(-1, -1, 4, 2), 356 | image.Rect(0, 0, 5, 3), 357 | []uint8{ 358 | 0x11, 0x99, 0x55, 0x22, 0x66, 359 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 360 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 361 | }, 362 | []uint8{ 363 | 0x11, 0x99, 0x55, 0x22, 0x66, 364 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 365 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 366 | }, 367 | }, 368 | { 369 | "maximum (2, false)", 370 | 2, false, 371 | image.Rect(-1, -1, 4, 2), 372 | image.Rect(0, 0, 5, 3), 373 | []uint8{ 374 | 0x11, 0x99, 0x55, 0x22, 0x66, 375 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 376 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 377 | }, 378 | []uint8{ 379 | 0x11, 0x99, 0x55, 0x22, 0x66, 380 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 381 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 382 | }, 383 | }, 384 | { 385 | "maximum (3, false)", 386 | 3, false, 387 | image.Rect(-1, -1, 4, 2), 388 | image.Rect(0, 0, 5, 3), 389 | []uint8{ 390 | 0x11, 0x99, 0x55, 0x22, 0x66, 391 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 392 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 393 | }, 394 | []uint8{ 395 | 0xFF, 0xFF, 0xFF, 0xFF, 0xCC, 396 | 0xFF, 0xFF, 0xFF, 0xFF, 0xEE, 397 | 0xFF, 0xFF, 0xFF, 0xFF, 0xEE, 398 | }, 399 | }, 400 | { 401 | "maximum (3, true)", 402 | 3, true, 403 | image.Rect(-1, -1, 4, 2), 404 | image.Rect(0, 0, 5, 3), 405 | []uint8{ 406 | 0x11, 0x99, 0x55, 0x22, 0x66, 407 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 408 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 409 | }, 410 | []uint8{ 411 | 0xFF, 0x99, 0xFF, 0xCC, 0x66, 412 | 0xFF, 0xFF, 0xFF, 0xFF, 0xEE, 413 | 0xFF, 0xBB, 0xFF, 0xEE, 0xEE, 414 | }, 415 | }, 416 | { 417 | "maximum (4, true)", 418 | 4, true, 419 | image.Rect(-1, -1, 4, 2), 420 | image.Rect(0, 0, 5, 3), 421 | []uint8{ 422 | 0x11, 0x99, 0x55, 0x22, 0x66, 423 | 0xFF, 0x44, 0xFF, 0xCC, 0x00, 424 | 0x33, 0x77, 0xBB, 0x88, 0xEE, 425 | }, 426 | []uint8{ 427 | 0xFF, 0x99, 0xFF, 0xCC, 0x66, 428 | 0xFF, 0xFF, 0xFF, 0xFF, 0xEE, 429 | 0xFF, 0xBB, 0xFF, 0xEE, 0xEE, 430 | }, 431 | }, 432 | } 433 | 434 | for _, d := range testData { 435 | src := image.NewGray(d.srcb) 436 | src.Pix = d.srcPix 437 | 438 | f := Maximum(d.ksize, d.disk) 439 | dst := image.NewGray(f.Bounds(src.Bounds())) 440 | f.Draw(dst, src, nil) 441 | 442 | if !checkBoundsAndPix(dst.Bounds(), d.dstb, dst.Pix, d.dstPix) { 443 | t.Errorf("test [%s] failed: %#v, %#v", d.desc, dst.Bounds(), dst.Pix) 444 | } 445 | } 446 | 447 | testDataNRGBA := []struct { 448 | desc string 449 | ksize int 450 | disk bool 451 | srcb, dstb image.Rectangle 452 | srcPix, dstPix []uint8 453 | }{ 454 | { 455 | "maximum nrgba (3, true)", 456 | 3, true, 457 | image.Rect(-1, -1, 4, 2), 458 | image.Rect(0, 0, 5, 3), 459 | []uint8{ 460 | 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x66, 461 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x00, 462 | 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0xBB, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0xEE, 463 | }, 464 | []uint8{ 465 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x66, 466 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xEE, 467 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xBB, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xEE, 0x00, 0x00, 0x00, 0xEE, 468 | }, 469 | }, 470 | } 471 | 472 | for _, d := range testDataNRGBA { 473 | src := image.NewNRGBA(d.srcb) 474 | src.Pix = d.srcPix 475 | 476 | f := Maximum(d.ksize, d.disk) 477 | dst := image.NewNRGBA(f.Bounds(src.Bounds())) 478 | f.Draw(dst, src, nil) 479 | 480 | if !checkBoundsAndPix(dst.Bounds(), d.dstb, dst.Pix, d.dstPix) { 481 | t.Errorf("test [%s] failed: %#v, %#v", d.desc, dst.Bounds(), dst.Pix) 482 | } 483 | } 484 | } 485 | -------------------------------------------------------------------------------- /colors.go: -------------------------------------------------------------------------------- 1 | package gift 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | "math" 7 | ) 8 | 9 | func prepareLut(lutSize int, fn func(float32) float32) []float32 { 10 | lut := make([]float32, lutSize) 11 | q := 1 / float32(lutSize-1) 12 | for v := 0; v < lutSize; v++ { 13 | u := float32(v) * q 14 | lut[v] = fn(u) 15 | } 16 | return lut 17 | } 18 | 19 | func getFromLut(lut []float32, u float32) float32 { 20 | v := int(u*float32(len(lut)-1) + 0.5) 21 | return lut[v] 22 | } 23 | 24 | type colorchanFilter struct { 25 | fn func(float32) float32 26 | lut bool 27 | } 28 | 29 | func (p *colorchanFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { 30 | dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy()) 31 | return 32 | } 33 | 34 | func (p *colorchanFilter) Draw(dst draw.Image, src image.Image, options *Options) { 35 | if options == nil { 36 | options = &defaultOptions 37 | } 38 | 39 | srcb := src.Bounds() 40 | dstb := dst.Bounds() 41 | pixGetter := newPixelGetter(src) 42 | pixSetter := newPixelSetter(dst) 43 | 44 | var useLut bool 45 | var lut []float32 46 | 47 | useLut = false 48 | if p.lut { 49 | var lutSize int 50 | 51 | it := pixGetter.it 52 | if it == itNRGBA || it == itRGBA || it == itGray || it == itYCbCr { 53 | lutSize = 0xff + 1 54 | } else { 55 | lutSize = 0xffff + 1 56 | } 57 | 58 | numCalculations := srcb.Dx() * srcb.Dy() * 3 59 | if numCalculations > lutSize*2 { 60 | useLut = true 61 | lut = prepareLut(lutSize, p.fn) 62 | } 63 | } 64 | 65 | parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(start, stop int) { 66 | for y := start; y < stop; y++ { 67 | for x := srcb.Min.X; x < srcb.Max.X; x++ { 68 | px := pixGetter.getPixel(x, y) 69 | if useLut { 70 | px.r = getFromLut(lut, px.r) 71 | px.g = getFromLut(lut, px.g) 72 | px.b = getFromLut(lut, px.b) 73 | } else { 74 | px.r = p.fn(px.r) 75 | px.g = p.fn(px.g) 76 | px.b = p.fn(px.b) 77 | } 78 | pixSetter.setPixel(dstb.Min.X+x-srcb.Min.X, dstb.Min.Y+y-srcb.Min.Y, px) 79 | } 80 | } 81 | }) 82 | } 83 | 84 | // Invert creates a filter that negates the colors of an image. 85 | func Invert() Filter { 86 | return &colorchanFilter{ 87 | fn: func(x float32) float32 { 88 | return 1 - x 89 | }, 90 | lut: false, 91 | } 92 | } 93 | 94 | // ColorspaceSRGBToLinear creates a filter that converts the colors of an image from sRGB to linear RGB. 95 | func ColorspaceSRGBToLinear() Filter { 96 | return &colorchanFilter{ 97 | fn: func(x float32) float32 { 98 | if x <= 0.04045 { 99 | return x / 12.92 100 | } 101 | return float32(math.Pow(float64((x+0.055)/1.055), 2.4)) 102 | }, 103 | lut: true, 104 | } 105 | } 106 | 107 | // ColorspaceLinearToSRGB creates a filter that converts the colors of an image from linear RGB to sRGB. 108 | func ColorspaceLinearToSRGB() Filter { 109 | return &colorchanFilter{ 110 | fn: func(x float32) float32 { 111 | if x <= 0.0031308 { 112 | return x * 12.92 113 | } 114 | return float32(1.055*math.Pow(float64(x), 1/2.4) - 0.055) 115 | }, 116 | lut: true, 117 | } 118 | } 119 | 120 | // Gamma creates a filter that performs a gamma correction on an image. 121 | // The gamma parameter must be positive. Gamma = 1 gives the original image. 122 | // Gamma less than 1 darkens the image and gamma greater than 1 lightens it. 123 | func Gamma(gamma float32) Filter { 124 | e := 1 / maxf32(gamma, 1.0e-5) 125 | return &colorchanFilter{ 126 | fn: func(x float32) float32 { 127 | return powf32(x, e) 128 | }, 129 | lut: true, 130 | } 131 | } 132 | 133 | func sigmoid(a, b, x float32) float32 { 134 | return 1 / (1 + expf32(b*(a-x))) 135 | } 136 | 137 | // Sigmoid creates a filter that changes the contrast of an image using a sigmoidal function and returns the adjusted image. 138 | // It's a non-linear contrast change useful for photo adjustments as it preserves highlight and shadow detail. 139 | // The midpoint parameter is the midpoint of contrast that must be between 0 and 1, typically 0.5. 140 | // The factor parameter indicates how much to increase or decrease the contrast, typically in range (-10, 10). 141 | // If the factor parameter is positive the image contrast is increased otherwise the contrast is decreased. 142 | // 143 | // Example: 144 | // 145 | // g := gift.New( 146 | // gift.Sigmoid(0.5, 5), 147 | // ) 148 | // dst := image.NewRGBA(g.Bounds(src.Bounds())) 149 | // g.Draw(dst, src) 150 | // 151 | func Sigmoid(midpoint, factor float32) Filter { 152 | a := minf32(maxf32(midpoint, 0), 1) 153 | b := absf32(factor) 154 | sig0 := sigmoid(a, b, 0) 155 | sig1 := sigmoid(a, b, 1) 156 | e := float32(1.0e-5) 157 | 158 | return &colorchanFilter{ 159 | fn: func(x float32) float32 { 160 | if factor == 0 { 161 | return x 162 | } else if factor > 0 { 163 | sig := sigmoid(a, b, x) 164 | return (sig - sig0) / (sig1 - sig0) 165 | } else { 166 | arg := minf32(maxf32((sig1-sig0)*x+sig0, e), 1-e) 167 | return a - logf32(1/arg-1)/b 168 | } 169 | }, 170 | lut: true, 171 | } 172 | } 173 | 174 | // Contrast creates a filter that changes the contrast of an image. 175 | // The percentage parameter must be in range (-100, 100). The percentage = 0 gives the original image. 176 | // The percentage = -100 gives solid grey image. The percentage = 100 gives an overcontrasted image. 177 | func Contrast(percentage float32) Filter { 178 | if percentage == 0 { 179 | return ©imageFilter{} 180 | } 181 | 182 | p := 1 + minf32(maxf32(percentage, -100), 100)/100 183 | 184 | return &colorchanFilter{ 185 | fn: func(x float32) float32 { 186 | if 0 <= p && p <= 1 { 187 | return 0.5 + (x-0.5)*p 188 | } else if 1 < p && p < 2 { 189 | return 0.5 + (x-0.5)*(1/(2.0-p)) 190 | } else { 191 | if x < 0.5 { 192 | return 0 193 | } 194 | return 1 195 | } 196 | }, 197 | lut: false, 198 | } 199 | } 200 | 201 | // Brightness creates a filter that changes the brightness of an image. 202 | // The percentage parameter must be in range (-100, 100). The percentage = 0 gives the original image. 203 | // The percentage = -100 gives solid black image. The percentage = 100 gives solid white image. 204 | func Brightness(percentage float32) Filter { 205 | if percentage == 0 { 206 | return ©imageFilter{} 207 | } 208 | 209 | shift := minf32(maxf32(percentage, -100), 100) / 100 210 | 211 | return &colorchanFilter{ 212 | fn: func(x float32) float32 { 213 | return x + shift 214 | }, 215 | lut: false, 216 | } 217 | } 218 | 219 | type colorFilter struct { 220 | fn func(pixel) pixel 221 | } 222 | 223 | func (p *colorFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { 224 | dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy()) 225 | return 226 | } 227 | 228 | func (p *colorFilter) Draw(dst draw.Image, src image.Image, options *Options) { 229 | if options == nil { 230 | options = &defaultOptions 231 | } 232 | 233 | srcb := src.Bounds() 234 | dstb := dst.Bounds() 235 | pixGetter := newPixelGetter(src) 236 | pixSetter := newPixelSetter(dst) 237 | 238 | parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(start, stop int) { 239 | for y := start; y < stop; y++ { 240 | for x := srcb.Min.X; x < srcb.Max.X; x++ { 241 | px := pixGetter.getPixel(x, y) 242 | pixSetter.setPixel(dstb.Min.X+x-srcb.Min.X, dstb.Min.Y+y-srcb.Min.Y, p.fn(px)) 243 | } 244 | } 245 | }) 246 | } 247 | 248 | // Grayscale creates a filter that produces a grayscale version of an image. 249 | func Grayscale() Filter { 250 | return &colorFilter{ 251 | fn: func(px pixel) pixel { 252 | y := 0.299*px.r + 0.587*px.g + 0.114*px.b 253 | return pixel{y, y, y, px.a} 254 | }, 255 | } 256 | } 257 | 258 | // Sepia creates a filter that produces a sepia-toned version of an image. 259 | // The percentage parameter specifies how much the image should be adjusted. It must be in the range (0, 100) 260 | // 261 | // Example: 262 | // 263 | // g := gift.New( 264 | // gift.Sepia(100), 265 | // ) 266 | // dst := image.NewRGBA(g.Bounds(src.Bounds())) 267 | // g.Draw(dst, src) 268 | // 269 | func Sepia(percentage float32) Filter { 270 | adjustAmount := minf32(maxf32(percentage, 0), 100) / 100 271 | rr := 1 - 0.607*adjustAmount 272 | rg := 0.769 * adjustAmount 273 | rb := 0.189 * adjustAmount 274 | gr := 0.349 * adjustAmount 275 | gg := 1 - 0.314*adjustAmount 276 | gb := 0.168 * adjustAmount 277 | br := 0.272 * adjustAmount 278 | bg := 0.534 * adjustAmount 279 | bb := 1 - 0.869*adjustAmount 280 | return &colorFilter{ 281 | fn: func(px pixel) pixel { 282 | r := px.r*rr + px.g*rg + px.b*rb 283 | g := px.r*gr + px.g*gg + px.b*gb 284 | b := px.r*br + px.g*bg + px.b*bb 285 | return pixel{r, g, b, px.a} 286 | }, 287 | } 288 | } 289 | 290 | func convertHSLToRGB(h, s, l float32) (float32, float32, float32) { 291 | if s == 0 { 292 | return l, l, l 293 | } 294 | 295 | hueToRGB := func(p, q, t float32) float32 { 296 | if t < 0 { 297 | t++ 298 | } 299 | if t > 1 { 300 | t-- 301 | } 302 | if t < 1/6.0 { 303 | return p + (q-p)*6*t 304 | } 305 | if t < 1/2.0 { 306 | return q 307 | } 308 | if t < 2/3.0 { 309 | return p + (q-p)*(2/3.0-t)*6 310 | } 311 | return p 312 | } 313 | 314 | var p, q float32 315 | if l < 0.5 { 316 | q = l * (1 + s) 317 | } else { 318 | q = l + s - l*s 319 | } 320 | p = 2*l - q 321 | 322 | r := hueToRGB(p, q, h+1/3.0) 323 | g := hueToRGB(p, q, h) 324 | b := hueToRGB(p, q, h-1/3.0) 325 | 326 | return r, g, b 327 | } 328 | 329 | func convertRGBToHSL(r, g, b float32) (float32, float32, float32) { 330 | max := maxf32(r, maxf32(g, b)) 331 | min := minf32(r, minf32(g, b)) 332 | 333 | l := (max + min) / 2 334 | 335 | if max == min { 336 | return 0, 0, l 337 | } 338 | 339 | var h, s float32 340 | d := max - min 341 | if l > 0.5 { 342 | s = d / (2 - max - min) 343 | } else { 344 | s = d / (max + min) 345 | } 346 | 347 | if r == max { 348 | h = (g - b) / d 349 | if g < b { 350 | h += 6 351 | } 352 | } else if g == max { 353 | h = (b-r)/d + 2 354 | } else { 355 | h = (r-g)/d + 4 356 | } 357 | h /= 6 358 | 359 | return h, s, l 360 | } 361 | 362 | func normalizeHue(hue float32) float32 { 363 | hue = hue - float32(int(hue)) 364 | if hue < 0 { 365 | hue++ 366 | } 367 | return hue 368 | } 369 | 370 | // Hue creates a filter that rotates the hue of an image. 371 | // The shift parameter is the hue angle shift, typically in range (-180, 180). 372 | // The shift = 0 gives the original image. 373 | func Hue(shift float32) Filter { 374 | p := normalizeHue(shift / 360) 375 | if p == 0 { 376 | return ©imageFilter{} 377 | } 378 | 379 | return &colorFilter{ 380 | fn: func(px pixel) pixel { 381 | h, s, l := convertRGBToHSL(px.r, px.g, px.b) 382 | h = normalizeHue(h + p) 383 | r, g, b := convertHSLToRGB(h, s, l) 384 | return pixel{r, g, b, px.a} 385 | }, 386 | } 387 | } 388 | 389 | // Saturation creates a filter that changes the saturation of an image. 390 | // The percentage parameter must be in range (-100, 500). The percentage = 0 gives the original image. 391 | func Saturation(percentage float32) Filter { 392 | p := 1 + minf32(maxf32(percentage, -100), 500)/100 393 | if p == 1 { 394 | return ©imageFilter{} 395 | } 396 | 397 | return &colorFilter{ 398 | fn: func(px pixel) pixel { 399 | h, s, l := convertRGBToHSL(px.r, px.g, px.b) 400 | s *= p 401 | if s > 1 { 402 | s = 1 403 | } 404 | r, g, b := convertHSLToRGB(h, s, l) 405 | return pixel{r, g, b, px.a} 406 | }, 407 | } 408 | } 409 | 410 | // Colorize creates a filter that produces a colorized version of an image. 411 | // The hue parameter is the angle on the color wheel, typically in range (0, 360). 412 | // The saturation parameter must be in range (0, 100). 413 | // The percentage parameter specifies the strength of the effect, it must be in range (0, 100). 414 | // 415 | // Example: 416 | // 417 | // g := gift.New( 418 | // gift.Colorize(240, 50, 100), // blue colorization, 50% saturation 419 | // ) 420 | // dst := image.NewRGBA(g.Bounds(src.Bounds())) 421 | // g.Draw(dst, src) 422 | // 423 | func Colorize(hue, saturation, percentage float32) Filter { 424 | h := normalizeHue(hue / 360) 425 | s := minf32(maxf32(saturation, 0), 100) / 100 426 | p := minf32(maxf32(percentage, 0), 100) / 100 427 | if p == 0 { 428 | return ©imageFilter{} 429 | } 430 | 431 | return &colorFilter{ 432 | fn: func(px pixel) pixel { 433 | _, _, l := convertRGBToHSL(px.r, px.g, px.b) 434 | r, g, b := convertHSLToRGB(h, s, l) 435 | px.r += (r - px.r) * p 436 | px.g += (g - px.g) * p 437 | px.b += (b - px.b) * p 438 | return px 439 | }, 440 | } 441 | } 442 | 443 | // ColorBalance creates a filter that changes the color balance of an image. 444 | // The percentage parameters for each color channel (red, green, blue) must be in range (-100, 500). 445 | // 446 | // Example: 447 | // 448 | // g := gift.New( 449 | // gift.ColorBalance(20, -20, 0), // +20% red, -20% green 450 | // ) 451 | // dst := image.NewRGBA(g.Bounds(src.Bounds())) 452 | // g.Draw(dst, src) 453 | // 454 | func ColorBalance(percentageRed, percentageGreen, percentageBlue float32) Filter { 455 | pr := 1 + minf32(maxf32(percentageRed, -100), 500)/100 456 | pg := 1 + minf32(maxf32(percentageGreen, -100), 500)/100 457 | pb := 1 + minf32(maxf32(percentageBlue, -100), 500)/100 458 | 459 | return &colorFilter{ 460 | fn: func(px pixel) pixel { 461 | px.r *= pr 462 | px.g *= pg 463 | px.b *= pb 464 | return px 465 | }, 466 | } 467 | } 468 | 469 | // Threshold creates a filter that applies black/white thresholding to the image. 470 | // The percentage parameter must be in range (0, 100). 471 | func Threshold(percentage float32) Filter { 472 | p := minf32(maxf32(percentage, 0), 100) / 100 473 | return &colorFilter{ 474 | fn: func(px pixel) pixel { 475 | y := 0.299*px.r + 0.587*px.g + 0.114*px.b 476 | if y > p { 477 | return pixel{1, 1, 1, px.a} 478 | } 479 | return pixel{0, 0, 0, px.a} 480 | }, 481 | } 482 | } 483 | 484 | // ColorFunc creates a filter that changes the colors of an image using custom function. 485 | // The fn parameter specifies a function that takes red, green, blue and alpha channels of a pixel 486 | // as float32 values in range (0, 1) and returns the modified channel values. 487 | // 488 | // Example: 489 | // 490 | // g := gift.New( 491 | // gift.ColorFunc( 492 | // func(r0, g0, b0, a0 float32) (r, g, b, a float32) { 493 | // r = 1 - r0 // invert the red channel 494 | // g = g0 + 0.1 // shift the green channel by 0.1 495 | // b = 0 // set the blue channel to 0 496 | // a = a0 // preserve the alpha channel 497 | // return r, g, b, a 498 | // }, 499 | // ), 500 | // ) 501 | // dst := image.NewRGBA(g.Bounds(src.Bounds())) 502 | // g.Draw(dst, src) 503 | // 504 | func ColorFunc(fn func(r0, g0, b0, a0 float32) (r, g, b, a float32)) Filter { 505 | return &colorFilter{ 506 | fn: func(px pixel) pixel { 507 | r, g, b, a := fn(px.r, px.g, px.b, px.a) 508 | return pixel{r, g, b, a} 509 | }, 510 | } 511 | } 512 | -------------------------------------------------------------------------------- /transform.go: -------------------------------------------------------------------------------- 1 | package gift 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/draw" 7 | ) 8 | 9 | type transformType int 10 | 11 | const ( 12 | ttRotate90 transformType = iota 13 | ttRotate180 14 | ttRotate270 15 | ttFlipHorizontal 16 | ttFlipVertical 17 | ttTranspose 18 | ttTransverse 19 | ) 20 | 21 | type transformFilter struct { 22 | tt transformType 23 | } 24 | 25 | func (p *transformFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { 26 | if p.tt == ttRotate90 || p.tt == ttRotate270 || p.tt == ttTranspose || p.tt == ttTransverse { 27 | dstBounds = image.Rect(0, 0, srcBounds.Dy(), srcBounds.Dx()) 28 | } else { 29 | dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy()) 30 | } 31 | return 32 | } 33 | 34 | func (p *transformFilter) Draw(dst draw.Image, src image.Image, options *Options) { 35 | if options == nil { 36 | options = &defaultOptions 37 | } 38 | 39 | srcb := src.Bounds() 40 | dstb := dst.Bounds() 41 | 42 | pixGetter := newPixelGetter(src) 43 | pixSetter := newPixelSetter(dst) 44 | 45 | parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(start, stop int) { 46 | for srcy := start; srcy < stop; srcy++ { 47 | for srcx := srcb.Min.X; srcx < srcb.Max.X; srcx++ { 48 | var dstx, dsty int 49 | switch p.tt { 50 | case ttRotate90: 51 | dstx = dstb.Min.X + srcy - srcb.Min.Y 52 | dsty = dstb.Min.Y + srcb.Max.X - srcx - 1 53 | case ttRotate180: 54 | dstx = dstb.Min.X + srcb.Max.X - srcx - 1 55 | dsty = dstb.Min.Y + srcb.Max.Y - srcy - 1 56 | case ttRotate270: 57 | dstx = dstb.Min.X + srcb.Max.Y - srcy - 1 58 | dsty = dstb.Min.Y + srcx - srcb.Min.X 59 | case ttFlipHorizontal: 60 | dstx = dstb.Min.X + srcb.Max.X - srcx - 1 61 | dsty = dstb.Min.Y + srcy - srcb.Min.Y 62 | case ttFlipVertical: 63 | dstx = dstb.Min.X + srcx - srcb.Min.X 64 | dsty = dstb.Min.Y + srcb.Max.Y - srcy - 1 65 | case ttTranspose: 66 | dstx = dstb.Min.X + srcy - srcb.Min.Y 67 | dsty = dstb.Min.Y + srcx - srcb.Min.X 68 | case ttTransverse: 69 | dstx = dstb.Min.Y + srcb.Max.Y - srcy - 1 70 | dsty = dstb.Min.X + srcb.Max.X - srcx - 1 71 | } 72 | pixSetter.setPixel(dstx, dsty, pixGetter.getPixel(srcx, srcy)) 73 | } 74 | } 75 | }) 76 | } 77 | 78 | // Rotate90 creates a filter that rotates an image 90 degrees counter-clockwise. 79 | func Rotate90() Filter { 80 | return &transformFilter{ 81 | tt: ttRotate90, 82 | } 83 | } 84 | 85 | // Rotate180 creates a filter that rotates an image 180 degrees counter-clockwise. 86 | func Rotate180() Filter { 87 | return &transformFilter{ 88 | tt: ttRotate180, 89 | } 90 | } 91 | 92 | // Rotate270 creates a filter that rotates an image 270 degrees counter-clockwise. 93 | func Rotate270() Filter { 94 | return &transformFilter{ 95 | tt: ttRotate270, 96 | } 97 | } 98 | 99 | // FlipHorizontal creates a filter that flips an image horizontally. 100 | func FlipHorizontal() Filter { 101 | return &transformFilter{ 102 | tt: ttFlipHorizontal, 103 | } 104 | } 105 | 106 | // FlipVertical creates a filter that flips an image vertically. 107 | func FlipVertical() Filter { 108 | return &transformFilter{ 109 | tt: ttFlipVertical, 110 | } 111 | } 112 | 113 | // Transpose creates a filter that flips an image horizontally and rotates 90 degrees counter-clockwise. 114 | func Transpose() Filter { 115 | return &transformFilter{ 116 | tt: ttTranspose, 117 | } 118 | } 119 | 120 | // Transverse creates a filter that flips an image vertically and rotates 90 degrees counter-clockwise. 121 | func Transverse() Filter { 122 | return &transformFilter{ 123 | tt: ttTransverse, 124 | } 125 | } 126 | 127 | // Interpolation is an interpolation algorithm used for image transformation. 128 | type Interpolation int 129 | 130 | const ( 131 | // NearestNeighborInterpolation is a nearest-neighbor interpolation algorithm. 132 | NearestNeighborInterpolation Interpolation = iota 133 | // LinearInterpolation is a bilinear interpolation algorithm. 134 | LinearInterpolation 135 | // CubicInterpolation is a bicubic interpolation algorithm. 136 | CubicInterpolation 137 | ) 138 | 139 | func rotatePoint(x, y, asin, acos float32) (float32, float32) { 140 | newx := x*acos - y*asin 141 | newy := x*asin + y*acos 142 | return newx, newy 143 | } 144 | 145 | func calcRotatedSize(w, h int, angle float32) (int, int) { 146 | if w <= 0 || h <= 0 { 147 | return 0, 0 148 | } 149 | 150 | xoff := float32(w)/2 - 0.5 151 | yoff := float32(h)/2 - 0.5 152 | 153 | asin, acos := sincosf32(angle) 154 | x1, y1 := rotatePoint(0-xoff, 0-yoff, asin, acos) 155 | x2, y2 := rotatePoint(float32(w-1)-xoff, 0-yoff, asin, acos) 156 | x3, y3 := rotatePoint(float32(w-1)-xoff, float32(h-1)-yoff, asin, acos) 157 | x4, y4 := rotatePoint(0-xoff, float32(h-1)-yoff, asin, acos) 158 | 159 | minx := minf32(x1, minf32(x2, minf32(x3, x4))) 160 | maxx := maxf32(x1, maxf32(x2, maxf32(x3, x4))) 161 | miny := minf32(y1, minf32(y2, minf32(y3, y4))) 162 | maxy := maxf32(y1, maxf32(y2, maxf32(y3, y4))) 163 | 164 | neww := maxx - minx + 1 165 | if neww-floorf32(neww) > 0.01 { 166 | neww += 2 167 | } 168 | newh := maxy - miny + 1 169 | if newh-floorf32(newh) > 0.01 { 170 | newh += 2 171 | } 172 | return int(neww), int(newh) 173 | } 174 | 175 | type rotateFilter struct { 176 | angle float32 177 | bgcolor color.Color 178 | interpolation Interpolation 179 | } 180 | 181 | func (p *rotateFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { 182 | w, h := calcRotatedSize(srcBounds.Dx(), srcBounds.Dy(), p.angle) 183 | dstBounds = image.Rect(0, 0, w, h) 184 | return 185 | } 186 | 187 | func (p *rotateFilter) Draw(dst draw.Image, src image.Image, options *Options) { 188 | if options == nil { 189 | options = &defaultOptions 190 | } 191 | 192 | srcb := src.Bounds() 193 | dstb := dst.Bounds() 194 | 195 | w, h := calcRotatedSize(srcb.Dx(), srcb.Dy(), p.angle) 196 | if w <= 0 || h <= 0 { 197 | return 198 | } 199 | 200 | srcxoff := float32(srcb.Dx())/2 - 0.5 201 | srcyoff := float32(srcb.Dy())/2 - 0.5 202 | dstxoff := float32(w)/2 - 0.5 203 | dstyoff := float32(h)/2 - 0.5 204 | 205 | bgpx := pixelFromColor(p.bgcolor) 206 | asin, acos := sincosf32(p.angle) 207 | 208 | pixGetter := newPixelGetter(src) 209 | pixSetter := newPixelSetter(dst) 210 | 211 | parallelize(options.Parallelization, 0, h, func(start, stop int) { 212 | for y := start; y < stop; y++ { 213 | for x := 0; x < w; x++ { 214 | 215 | xf, yf := rotatePoint(float32(x)-dstxoff, float32(y)-dstyoff, asin, acos) 216 | xf, yf = float32(srcb.Min.X)+xf+srcxoff, float32(srcb.Min.Y)+yf+srcyoff 217 | var px pixel 218 | 219 | switch p.interpolation { 220 | case CubicInterpolation: 221 | px = interpolateCubic(xf, yf, srcb, pixGetter, bgpx) 222 | case LinearInterpolation: 223 | px = interpolateLinear(xf, yf, srcb, pixGetter, bgpx) 224 | default: 225 | px = interpolateNearest(xf, yf, srcb, pixGetter, bgpx) 226 | } 227 | 228 | pixSetter.setPixel(dstb.Min.X+x, dstb.Min.Y+y, px) 229 | } 230 | } 231 | }) 232 | } 233 | 234 | func interpolateCubic(xf, yf float32, bounds image.Rectangle, pixGetter *pixelGetter, bgpx pixel) pixel { 235 | var pxs [16]pixel 236 | var cfs [16]float32 237 | var px pixel 238 | 239 | x0, y0 := int(floorf32(xf)), int(floorf32(yf)) 240 | if !image.Pt(x0, y0).In(image.Rect(bounds.Min.X-1, bounds.Min.Y-1, bounds.Max.X, bounds.Max.Y)) { 241 | return bgpx 242 | } 243 | xq, yq := xf-float32(x0), yf-float32(y0) 244 | 245 | for i := 0; i < 4; i++ { 246 | for j := 0; j < 4; j++ { 247 | pt := image.Pt(x0+j-1, y0+i-1) 248 | if pt.In(bounds) { 249 | pxs[i*4+j] = pixGetter.getPixel(pt.X, pt.Y) 250 | } else { 251 | pxs[i*4+j] = bgpx 252 | } 253 | } 254 | } 255 | 256 | const ( 257 | k04 = 1 / 4.0 258 | k12 = 1 / 12.0 259 | k36 = 1 / 36.0 260 | ) 261 | 262 | cfs[0] = k36 * xq * yq * (xq - 1) * (xq - 2) * (yq - 1) * (yq - 2) 263 | cfs[1] = -k12 * yq * (xq - 1) * (xq - 2) * (xq + 1) * (yq - 1) * (yq - 2) 264 | cfs[2] = k12 * xq * yq * (xq + 1) * (xq - 2) * (yq - 1) * (yq - 2) 265 | cfs[3] = -k36 * xq * yq * (xq - 1) * (xq + 1) * (yq - 1) * (yq - 2) 266 | cfs[4] = -k12 * xq * (xq - 1) * (xq - 2) * (yq - 1) * (yq - 2) * (yq + 1) 267 | cfs[5] = k04 * (xq - 1) * (xq - 2) * (xq + 1) * (yq - 1) * (yq - 2) * (yq + 1) 268 | cfs[6] = -k04 * xq * (xq + 1) * (xq - 2) * (yq - 1) * (yq - 2) * (yq + 1) 269 | cfs[7] = k12 * xq * (xq - 1) * (xq + 1) * (yq - 1) * (yq - 2) * (yq + 1) 270 | cfs[8] = k12 * xq * yq * (xq - 1) * (xq - 2) * (yq + 1) * (yq - 2) 271 | cfs[9] = -k04 * yq * (xq - 1) * (xq - 2) * (xq + 1) * (yq + 1) * (yq - 2) 272 | cfs[10] = k04 * xq * yq * (xq + 1) * (xq - 2) * (yq + 1) * (yq - 2) 273 | cfs[11] = -k12 * xq * yq * (xq - 1) * (xq + 1) * (yq + 1) * (yq - 2) 274 | cfs[12] = -k36 * xq * yq * (xq - 1) * (xq - 2) * (yq - 1) * (yq + 1) 275 | cfs[13] = k12 * yq * (xq - 1) * (xq - 2) * (xq + 1) * (yq - 1) * (yq + 1) 276 | cfs[14] = -k12 * xq * yq * (xq + 1) * (xq - 2) * (yq - 1) * (yq + 1) 277 | cfs[15] = k36 * xq * yq * (xq - 1) * (xq + 1) * (yq - 1) * (yq + 1) 278 | 279 | for i := range pxs { 280 | wa := pxs[i].a * cfs[i] 281 | px.r += pxs[i].r * wa 282 | px.g += pxs[i].g * wa 283 | px.b += pxs[i].b * wa 284 | px.a += wa 285 | } 286 | 287 | if px.a != 0 { 288 | px.r /= px.a 289 | px.g /= px.a 290 | px.b /= px.a 291 | } 292 | 293 | return px 294 | } 295 | 296 | func interpolateLinear(xf, yf float32, bounds image.Rectangle, pixGetter *pixelGetter, bgpx pixel) pixel { 297 | var pxs [4]pixel 298 | var cfs [4]float32 299 | var px pixel 300 | 301 | x0, y0 := int(floorf32(xf)), int(floorf32(yf)) 302 | if !image.Pt(x0, y0).In(image.Rect(bounds.Min.X-1, bounds.Min.Y-1, bounds.Max.X, bounds.Max.Y)) { 303 | return bgpx 304 | } 305 | xq, yq := xf-float32(x0), yf-float32(y0) 306 | 307 | for i := 0; i < 2; i++ { 308 | for j := 0; j < 2; j++ { 309 | pt := image.Pt(x0+j, y0+i) 310 | if pt.In(bounds) { 311 | pxs[i*2+j] = pixGetter.getPixel(pt.X, pt.Y) 312 | } else { 313 | pxs[i*2+j] = bgpx 314 | } 315 | } 316 | } 317 | 318 | cfs[0] = (1 - xq) * (1 - yq) 319 | cfs[1] = xq * (1 - yq) 320 | cfs[2] = (1 - xq) * yq 321 | cfs[3] = xq * yq 322 | 323 | for i := range pxs { 324 | wa := pxs[i].a * cfs[i] 325 | px.r += pxs[i].r * wa 326 | px.g += pxs[i].g * wa 327 | px.b += pxs[i].b * wa 328 | px.a += wa 329 | } 330 | 331 | if px.a != 0 { 332 | px.r /= px.a 333 | px.g /= px.a 334 | px.b /= px.a 335 | } 336 | 337 | return px 338 | } 339 | 340 | func interpolateNearest(xf, yf float32, bounds image.Rectangle, pixGetter *pixelGetter, bgpx pixel) pixel { 341 | x0, y0 := int(floorf32(xf+0.5)), int(floorf32(yf+0.5)) 342 | if image.Pt(x0, y0).In(bounds) { 343 | return pixGetter.getPixel(x0, y0) 344 | } 345 | return bgpx 346 | } 347 | 348 | // Rotate creates a filter that rotates an image by the given angle counter-clockwise. 349 | // The angle parameter is the rotation angle in degrees. 350 | // The backgroundColor parameter specifies the color of the uncovered zone after the rotation. 351 | // The interpolation parameter specifies the interpolation method. 352 | // Supported interpolation methods: NearestNeighborInterpolation, LinearInterpolation, CubicInterpolation. 353 | // 354 | // Example: 355 | // 356 | // g := gift.New( 357 | // gift.Rotate(45, color.Black, gift.LinearInterpolation), 358 | // ) 359 | // dst := image.NewRGBA(g.Bounds(src.Bounds())) 360 | // g.Draw(dst, src) 361 | // 362 | func Rotate(angle float32, backgroundColor color.Color, interpolation Interpolation) Filter { 363 | return &rotateFilter{ 364 | angle: angle, 365 | bgcolor: backgroundColor, 366 | interpolation: interpolation, 367 | } 368 | } 369 | 370 | type cropFilter struct { 371 | rect image.Rectangle 372 | } 373 | 374 | func (p *cropFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { 375 | b := srcBounds.Intersect(p.rect) 376 | return b.Sub(b.Min) 377 | } 378 | 379 | func (p *cropFilter) Draw(dst draw.Image, src image.Image, options *Options) { 380 | if options == nil { 381 | options = &defaultOptions 382 | } 383 | 384 | srcb := src.Bounds().Intersect(p.rect) 385 | dstb := dst.Bounds() 386 | pixGetter := newPixelGetter(src) 387 | pixSetter := newPixelSetter(dst) 388 | 389 | parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(start, stop int) { 390 | for srcy := start; srcy < stop; srcy++ { 391 | for srcx := srcb.Min.X; srcx < srcb.Max.X; srcx++ { 392 | dstx := dstb.Min.X + srcx - srcb.Min.X 393 | dsty := dstb.Min.Y + srcy - srcb.Min.Y 394 | pixSetter.setPixel(dstx, dsty, pixGetter.getPixel(srcx, srcy)) 395 | } 396 | } 397 | }) 398 | } 399 | 400 | // Crop creates a filter that crops the specified rectangular region from an image. 401 | // 402 | // Example: 403 | // 404 | // g := gift.New( 405 | // gift.Crop(image.Rect(100, 100, 200, 200)), 406 | // ) 407 | // dst := image.NewRGBA(g.Bounds(src.Bounds())) 408 | // g.Draw(dst, src) 409 | // 410 | func Crop(rect image.Rectangle) Filter { 411 | return &cropFilter{ 412 | rect: rect, 413 | } 414 | } 415 | 416 | // Anchor is the anchor point for image cropping. 417 | type Anchor int 418 | 419 | // Anchor point positions. 420 | const ( 421 | CenterAnchor Anchor = iota 422 | TopLeftAnchor 423 | TopAnchor 424 | TopRightAnchor 425 | LeftAnchor 426 | RightAnchor 427 | BottomLeftAnchor 428 | BottomAnchor 429 | BottomRightAnchor 430 | ) 431 | 432 | func anchorPt(b image.Rectangle, w, h int, anchor Anchor) image.Point { 433 | var x, y int 434 | switch anchor { 435 | case TopLeftAnchor: 436 | x = b.Min.X 437 | y = b.Min.Y 438 | case TopAnchor: 439 | x = b.Min.X + (b.Dx()-w)/2 440 | y = b.Min.Y 441 | case TopRightAnchor: 442 | x = b.Max.X - w 443 | y = b.Min.Y 444 | case LeftAnchor: 445 | x = b.Min.X 446 | y = b.Min.Y + (b.Dy()-h)/2 447 | case RightAnchor: 448 | x = b.Max.X - w 449 | y = b.Min.Y + (b.Dy()-h)/2 450 | case BottomLeftAnchor: 451 | x = b.Min.X 452 | y = b.Max.Y - h 453 | case BottomAnchor: 454 | x = b.Min.X + (b.Dx()-w)/2 455 | y = b.Max.Y - h 456 | case BottomRightAnchor: 457 | x = b.Max.X - w 458 | y = b.Max.Y - h 459 | default: 460 | x = b.Min.X + (b.Dx()-w)/2 461 | y = b.Min.Y + (b.Dy()-h)/2 462 | } 463 | return image.Pt(x, y) 464 | } 465 | 466 | type cropToSizeFilter struct { 467 | w, h int 468 | anchor Anchor 469 | } 470 | 471 | func (p *cropToSizeFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { 472 | if p.w <= 0 || p.h <= 0 { 473 | return image.Rect(0, 0, 0, 0) 474 | } 475 | pt := anchorPt(srcBounds, p.w, p.h, p.anchor) 476 | r := image.Rect(0, 0, p.w, p.h).Add(pt) 477 | b := srcBounds.Intersect(r) 478 | return b.Sub(b.Min) 479 | } 480 | 481 | func (p *cropToSizeFilter) Draw(dst draw.Image, src image.Image, options *Options) { 482 | if p.w <= 0 || p.h <= 0 { 483 | return 484 | } 485 | pt := anchorPt(src.Bounds(), p.w, p.h, p.anchor) 486 | r := image.Rect(0, 0, p.w, p.h).Add(pt) 487 | b := src.Bounds().Intersect(r) 488 | Crop(b).Draw(dst, src, options) 489 | } 490 | 491 | // CropToSize creates a filter that crops an image to the specified size using the specified anchor point. 492 | func CropToSize(width, height int, anchor Anchor) Filter { 493 | return &cropToSizeFilter{ 494 | w: width, 495 | h: height, 496 | anchor: anchor, 497 | } 498 | } 499 | -------------------------------------------------------------------------------- /convolution.go: -------------------------------------------------------------------------------- 1 | package gift 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | "math" 7 | ) 8 | 9 | type uweight struct { 10 | u int 11 | weight float32 12 | } 13 | 14 | type uvweight struct { 15 | u int 16 | v int 17 | weight float32 18 | } 19 | 20 | func prepareConvolutionWeights(kernel []float32, normalize bool) (int, []uvweight) { 21 | size := int(math.Sqrt(float64(len(kernel)))) 22 | if size%2 == 0 { 23 | size-- 24 | } 25 | if size < 1 { 26 | return 0, []uvweight{} 27 | } 28 | center := size / 2 29 | 30 | weights := []uvweight{} 31 | for i := 0; i < size; i++ { 32 | for j := 0; j < size; j++ { 33 | k := j*size + i 34 | w := float32(0) 35 | if k < len(kernel) { 36 | w = kernel[k] 37 | } 38 | if w != 0 { 39 | weights = append(weights, uvweight{u: i - center, v: j - center, weight: w}) 40 | } 41 | } 42 | } 43 | 44 | if !normalize { 45 | return size, weights 46 | } 47 | 48 | var sum, sumpositive float32 49 | for _, w := range weights { 50 | sum += w.weight 51 | if w.weight > 0 { 52 | sumpositive += w.weight 53 | } 54 | } 55 | 56 | var div float32 57 | if sum != 0 { 58 | div = sum 59 | } else if sumpositive != 0 { 60 | div = sumpositive 61 | } else { 62 | return size, weights 63 | } 64 | 65 | for i := 0; i < len(weights); i++ { 66 | weights[i].weight /= div 67 | } 68 | 69 | return size, weights 70 | } 71 | 72 | type convolutionFilter struct { 73 | kernel []float32 74 | normalize bool 75 | alpha bool 76 | abs bool 77 | delta float32 78 | } 79 | 80 | func (p *convolutionFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { 81 | dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy()) 82 | return 83 | } 84 | 85 | func (p *convolutionFilter) Draw(dst draw.Image, src image.Image, options *Options) { 86 | if options == nil { 87 | options = &defaultOptions 88 | } 89 | 90 | srcb := src.Bounds() 91 | dstb := dst.Bounds() 92 | 93 | if srcb.Dx() <= 0 || srcb.Dy() <= 0 { 94 | return 95 | } 96 | 97 | ksize, weights := prepareConvolutionWeights(p.kernel, p.normalize) 98 | kcenter := ksize / 2 99 | 100 | if ksize < 1 { 101 | copyimage(dst, src, options) 102 | return 103 | } 104 | 105 | pixGetter := newPixelGetter(src) 106 | pixSetter := newPixelSetter(dst) 107 | 108 | parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(start, stop int) { 109 | // Init temporary rows. 110 | starty := start 111 | rows := make([][]pixel, ksize) 112 | for i := 0; i < ksize; i++ { 113 | rowy := starty + i - kcenter 114 | if rowy < srcb.Min.Y { 115 | rowy = srcb.Min.Y 116 | } else if rowy > srcb.Max.Y-1 { 117 | rowy = srcb.Max.Y - 1 118 | } 119 | row := make([]pixel, srcb.Dx()) 120 | pixGetter.getPixelRow(rowy, &row) 121 | rows[i] = row 122 | } 123 | 124 | for y := start; y < stop; y++ { 125 | // Calculate dst row. 126 | for x := srcb.Min.X; x < srcb.Max.X; x++ { 127 | var r, g, b, a float32 128 | for _, w := range weights { 129 | wx := x + w.u 130 | if wx < srcb.Min.X { 131 | wx = srcb.Min.X 132 | } else if wx > srcb.Max.X-1 { 133 | wx = srcb.Max.X - 1 134 | } 135 | rowsx := wx - srcb.Min.X 136 | rowsy := kcenter + w.v 137 | 138 | px := rows[rowsy][rowsx] 139 | r += px.r * w.weight 140 | g += px.g * w.weight 141 | b += px.b * w.weight 142 | if p.alpha { 143 | a += px.a * w.weight 144 | } 145 | } 146 | if p.abs { 147 | r = absf32(r) 148 | g = absf32(g) 149 | b = absf32(b) 150 | if p.alpha { 151 | a = absf32(a) 152 | } 153 | } 154 | if p.delta != 0 { 155 | r += p.delta 156 | g += p.delta 157 | b += p.delta 158 | if p.alpha { 159 | a += p.delta 160 | } 161 | } 162 | if !p.alpha { 163 | a = rows[kcenter][x-srcb.Min.X].a 164 | } 165 | pixSetter.setPixel(dstb.Min.X+x-srcb.Min.X, dstb.Min.Y+y-srcb.Min.Y, pixel{r, g, b, a}) 166 | } 167 | 168 | // Rotate temporary rows. 169 | if y < stop-1 { 170 | tmprow := rows[0] 171 | for i := 0; i < ksize-1; i++ { 172 | rows[i] = rows[i+1] 173 | } 174 | nextrowy := y + ksize/2 + 1 175 | if nextrowy > srcb.Max.Y-1 { 176 | nextrowy = srcb.Max.Y - 1 177 | } 178 | pixGetter.getPixelRow(nextrowy, &tmprow) 179 | rows[ksize-1] = tmprow 180 | } 181 | } 182 | }) 183 | } 184 | 185 | // Convolution creates a filter that applies a square convolution kernel to an image. 186 | // The length of the kernel slice must be the square of an odd kernel size (e.g. 9 for 3x3 kernel, 25 for 5x5 kernel). 187 | // Excessive slice members will be ignored. 188 | // If normalize parameter is true, the kernel will be normalized before applying the filter. 189 | // If alpha parameter is true, the alpha component of color will be filtered too. 190 | // If abs parameter is true, absolute values of color components will be taken after doing calculations. 191 | // If delta parameter is not zero, this value will be added to the filtered pixels. 192 | // 193 | // Example: 194 | // 195 | // // Apply the emboss filter to an image. 196 | // g := gift.New( 197 | // gift.Convolution( 198 | // []float32{ 199 | // -1, -1, 0, 200 | // -1, 1, 1, 201 | // 0, 1, 1, 202 | // }, 203 | // false, false, false, 0, 204 | // ), 205 | // ) 206 | // dst := image.NewRGBA(g.Bounds(src.Bounds())) 207 | // g.Draw(dst, src) 208 | // 209 | func Convolution(kernel []float32, normalize, alpha, abs bool, delta float32) Filter { 210 | return &convolutionFilter{ 211 | kernel: kernel, 212 | normalize: normalize, 213 | alpha: alpha, 214 | abs: abs, 215 | delta: delta, 216 | } 217 | } 218 | 219 | // prepareConvolutionWeights1d prepares pixel weights using a convolution kernel. 220 | // Weights equal to 0 are excluded. 221 | func prepareConvolutionWeights1d(kernel []float32) (int, []uweight) { 222 | size := len(kernel) 223 | if size%2 == 0 { 224 | size-- 225 | } 226 | if size < 1 { 227 | return 0, []uweight{} 228 | } 229 | center := size / 2 230 | weights := []uweight{} 231 | for i := 0; i < size; i++ { 232 | w := float32(0) 233 | if i < len(kernel) { 234 | w = kernel[i] 235 | } 236 | if w != 0 { 237 | weights = append(weights, uweight{i - center, w}) 238 | } 239 | } 240 | return size, weights 241 | } 242 | 243 | // convolveLine convolves a single line of pixels according to the given weights. 244 | func convolveLine(dstBuf []pixel, srcBuf []pixel, weights []uweight) { 245 | max := len(srcBuf) - 1 246 | if max < 0 { 247 | return 248 | } 249 | for dstu := 0; dstu < len(srcBuf); dstu++ { 250 | var r, g, b, a float32 251 | for _, w := range weights { 252 | k := dstu + w.u 253 | if k < 0 { 254 | k = 0 255 | } else if k > max { 256 | k = max 257 | } 258 | c := srcBuf[k] 259 | wa := c.a * w.weight 260 | r += c.r * wa 261 | g += c.g * wa 262 | b += c.b * wa 263 | a += wa 264 | } 265 | if a != 0 { 266 | r /= a 267 | g /= a 268 | b /= a 269 | } 270 | dstBuf[dstu] = pixel{r, g, b, a} 271 | } 272 | } 273 | 274 | // convolve1dv performs a fast vertical 1d convolution. 275 | func convolve1dv(dst draw.Image, src image.Image, kernel []float32, options *Options) { 276 | srcb := src.Bounds() 277 | dstb := dst.Bounds() 278 | if srcb.Dx() <= 0 || srcb.Dy() <= 0 { 279 | return 280 | } 281 | if kernel == nil || len(kernel) < 1 { 282 | copyimage(dst, src, options) 283 | return 284 | } 285 | _, weights := prepareConvolutionWeights1d(kernel) 286 | pixGetter := newPixelGetter(src) 287 | pixSetter := newPixelSetter(dst) 288 | parallelize(options.Parallelization, srcb.Min.X, srcb.Max.X, func(start, stop int) { 289 | srcBuf := make([]pixel, srcb.Dy()) 290 | dstBuf := make([]pixel, srcb.Dy()) 291 | for x := start; x < stop; x++ { 292 | pixGetter.getPixelColumn(x, &srcBuf) 293 | convolveLine(dstBuf, srcBuf, weights) 294 | pixSetter.setPixelColumn(dstb.Min.X+x-srcb.Min.X, dstBuf) 295 | } 296 | }) 297 | } 298 | 299 | // convolve1dh performs afast horizontal 1d convolution. 300 | func convolve1dh(dst draw.Image, src image.Image, kernel []float32, options *Options) { 301 | srcb := src.Bounds() 302 | dstb := dst.Bounds() 303 | if srcb.Dx() <= 0 || srcb.Dy() <= 0 { 304 | return 305 | } 306 | if kernel == nil || len(kernel) < 1 { 307 | copyimage(dst, src, options) 308 | return 309 | } 310 | _, weights := prepareConvolutionWeights1d(kernel) 311 | pixGetter := newPixelGetter(src) 312 | pixSetter := newPixelSetter(dst) 313 | parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(start, stop int) { 314 | srcBuf := make([]pixel, srcb.Dx()) 315 | dstBuf := make([]pixel, srcb.Dx()) 316 | for y := start; y < stop; y++ { 317 | pixGetter.getPixelRow(y, &srcBuf) 318 | convolveLine(dstBuf, srcBuf, weights) 319 | pixSetter.setPixelRow(dstb.Min.Y+y-srcb.Min.Y, dstBuf) 320 | } 321 | }) 322 | } 323 | 324 | func gaussianBlurKernel(x, sigma float32) float32 { 325 | return float32(math.Exp(-float64(x*x)/float64(2*sigma*sigma)) / (float64(sigma) * math.Sqrt(2*math.Pi))) 326 | } 327 | 328 | type gausssianBlurFilter struct { 329 | sigma float32 330 | } 331 | 332 | func (p *gausssianBlurFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { 333 | dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy()) 334 | return 335 | } 336 | 337 | func (p *gausssianBlurFilter) Draw(dst draw.Image, src image.Image, options *Options) { 338 | if options == nil { 339 | options = &defaultOptions 340 | } 341 | 342 | srcb := src.Bounds() 343 | if srcb.Dx() <= 0 || srcb.Dy() <= 0 { 344 | return 345 | } 346 | 347 | if p.sigma <= 0 { 348 | copyimage(dst, src, options) 349 | return 350 | } 351 | 352 | radius := int(math.Ceil(float64(p.sigma * 3))) 353 | size := 2*radius + 1 354 | center := radius 355 | kernel := make([]float32, size) 356 | 357 | kernel[center] = gaussianBlurKernel(0, p.sigma) 358 | sum := kernel[center] 359 | 360 | for i := 1; i <= radius; i++ { 361 | f := gaussianBlurKernel(float32(i), p.sigma) 362 | kernel[center-i] = f 363 | kernel[center+i] = f 364 | sum += 2 * f 365 | } 366 | 367 | for i := 0; i < len(kernel); i++ { 368 | kernel[i] /= sum 369 | } 370 | 371 | tmp := createTempImage(srcb) 372 | convolve1dh(tmp, src, kernel, options) 373 | convolve1dv(dst, tmp, kernel, options) 374 | } 375 | 376 | // GaussianBlur creates a filter that applies a gaussian blur to an image. 377 | // The sigma parameter must be positive and indicates how much the image will be blurred. 378 | // Blur affected radius roughly equals 3 * sigma. 379 | // 380 | // Example: 381 | // 382 | // g := gift.New( 383 | // gift.GaussianBlur(1.5), 384 | // ) 385 | // dst := image.NewRGBA(g.Bounds(src.Bounds())) 386 | // g.Draw(dst, src) 387 | // 388 | func GaussianBlur(sigma float32) Filter { 389 | return &gausssianBlurFilter{ 390 | sigma: sigma, 391 | } 392 | } 393 | 394 | type unsharpMaskFilter struct { 395 | sigma float32 396 | amount float32 397 | threshold float32 398 | } 399 | 400 | func (p *unsharpMaskFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { 401 | dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy()) 402 | return 403 | } 404 | 405 | func unsharp(orig, blurred, amount, threshold float32) float32 { 406 | dif := (orig - blurred) * amount 407 | if absf32(dif) > absf32(threshold) { 408 | return orig + dif 409 | } 410 | return orig 411 | } 412 | 413 | func (p *unsharpMaskFilter) Draw(dst draw.Image, src image.Image, options *Options) { 414 | if options == nil { 415 | options = &defaultOptions 416 | } 417 | 418 | srcb := src.Bounds() 419 | dstb := dst.Bounds() 420 | 421 | if srcb.Dx() <= 0 || srcb.Dy() <= 0 { 422 | return 423 | } 424 | 425 | blurred := createTempImage(srcb) 426 | blur := GaussianBlur(p.sigma) 427 | blur.Draw(blurred, src, options) 428 | 429 | pixGetterOrig := newPixelGetter(src) 430 | pixGetterBlur := newPixelGetter(blurred) 431 | pixelSetter := newPixelSetter(dst) 432 | 433 | parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(start, stop int) { 434 | for y := start; y < stop; y++ { 435 | for x := srcb.Min.X; x < srcb.Max.X; x++ { 436 | pxOrig := pixGetterOrig.getPixel(x, y) 437 | pxBlur := pixGetterBlur.getPixel(x, y) 438 | 439 | r := unsharp(pxOrig.r, pxBlur.r, p.amount, p.threshold) 440 | g := unsharp(pxOrig.g, pxBlur.g, p.amount, p.threshold) 441 | b := unsharp(pxOrig.b, pxBlur.b, p.amount, p.threshold) 442 | a := unsharp(pxOrig.a, pxBlur.a, p.amount, p.threshold) 443 | 444 | pixelSetter.setPixel(dstb.Min.X+x-srcb.Min.X, dstb.Min.Y+y-srcb.Min.Y, pixel{r, g, b, a}) 445 | } 446 | } 447 | }) 448 | } 449 | 450 | // UnsharpMask creates a filter that sharpens an image. 451 | // The sigma parameter is used in a gaussian function and affects the radius of effect. 452 | // Sigma must be positive. Sharpen radius roughly equals 3 * sigma. 453 | // The amount parameter controls how much darker and how much lighter the edge borders become. Typically between 0.5 and 1.5. 454 | // The threshold parameter controls the minimum brightness change that will be sharpened. Typically between 0 and 0.05. 455 | // 456 | // Example: 457 | // 458 | // g := gift.New( 459 | // gift.UnsharpMask(1, 1, 0), 460 | // ) 461 | // dst := image.NewRGBA(g.Bounds(src.Bounds())) 462 | // g.Draw(dst, src) 463 | // 464 | func UnsharpMask(sigma, amount, threshold float32) Filter { 465 | return &unsharpMaskFilter{ 466 | sigma: sigma, 467 | amount: amount, 468 | threshold: threshold, 469 | } 470 | } 471 | 472 | type meanFilter struct { 473 | ksize int 474 | disk bool 475 | } 476 | 477 | func (p *meanFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { 478 | dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy()) 479 | return 480 | } 481 | 482 | func (p *meanFilter) Draw(dst draw.Image, src image.Image, options *Options) { 483 | if options == nil { 484 | options = &defaultOptions 485 | } 486 | 487 | srcb := src.Bounds() 488 | if srcb.Dx() <= 0 || srcb.Dy() <= 0 { 489 | return 490 | } 491 | 492 | ksize := p.ksize 493 | if ksize%2 == 0 { 494 | ksize-- 495 | } 496 | 497 | if ksize <= 1 { 498 | copyimage(dst, src, options) 499 | return 500 | } 501 | 502 | if p.disk { 503 | diskKernel := genDisk(p.ksize) 504 | f := Convolution(diskKernel, true, true, false, 0) 505 | f.Draw(dst, src, options) 506 | } else { 507 | kernel := make([]float32, ksize*ksize) 508 | for i := range kernel { 509 | kernel[i] = 1 510 | } 511 | f := Convolution(kernel, true, true, false, 0) 512 | f.Draw(dst, src, options) 513 | } 514 | } 515 | 516 | // Mean creates a local mean image filter. 517 | // Takes an average across a neighborhood for each pixel. 518 | // The ksize parameter is the kernel size. It must be an odd positive integer (for example: 3, 5, 7). 519 | // If the disk parameter is true, a disk-shaped neighborhood will be used instead of a square neighborhood. 520 | func Mean(ksize int, disk bool) Filter { 521 | return &meanFilter{ 522 | ksize: ksize, 523 | disk: disk, 524 | } 525 | } 526 | 527 | type hvConvolutionFilter struct { 528 | hkernel, vkernel []float32 529 | } 530 | 531 | func (p *hvConvolutionFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { 532 | dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy()) 533 | return 534 | } 535 | 536 | func (p *hvConvolutionFilter) Draw(dst draw.Image, src image.Image, options *Options) { 537 | if options == nil { 538 | options = &defaultOptions 539 | } 540 | 541 | srcb := src.Bounds() 542 | dstb := dst.Bounds() 543 | 544 | if srcb.Dx() <= 0 || srcb.Dy() <= 0 { 545 | return 546 | } 547 | 548 | tmph := createTempImage(srcb) 549 | Convolution(p.hkernel, false, false, true, 0).Draw(tmph, src, options) 550 | pixGetterH := newPixelGetter(tmph) 551 | 552 | tmpv := createTempImage(srcb) 553 | Convolution(p.vkernel, false, false, true, 0).Draw(tmpv, src, options) 554 | pixGetterV := newPixelGetter(tmpv) 555 | 556 | pixSetter := newPixelSetter(dst) 557 | 558 | parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(start, stop int) { 559 | for y := start; y < stop; y++ { 560 | for x := srcb.Min.X; x < srcb.Max.X; x++ { 561 | pxh := pixGetterH.getPixel(x, y) 562 | pxv := pixGetterV.getPixel(x, y) 563 | r := sqrtf32(pxh.r*pxh.r + pxv.r*pxv.r) 564 | g := sqrtf32(pxh.g*pxh.g + pxv.g*pxv.g) 565 | b := sqrtf32(pxh.b*pxh.b + pxv.b*pxv.b) 566 | pixSetter.setPixel(dstb.Min.X+x-srcb.Min.X, dstb.Min.Y+y-srcb.Min.Y, pixel{r, g, b, pxh.a}) 567 | } 568 | } 569 | }) 570 | 571 | } 572 | 573 | // Sobel creates a filter that applies a sobel operator to an image. 574 | func Sobel() Filter { 575 | return &hvConvolutionFilter{ 576 | hkernel: []float32{-1, 0, 1, -2, 0, 2, -1, 0, 1}, 577 | vkernel: []float32{-1, -2, -1, 0, 0, 0, 1, 2, 1}, 578 | } 579 | } 580 | -------------------------------------------------------------------------------- /transform_test.go: -------------------------------------------------------------------------------- 1 | package gift 2 | 3 | import ( 4 | "bytes" 5 | "image" 6 | "image/color" 7 | "testing" 8 | ) 9 | 10 | func TestRotate90(t *testing.T) { 11 | img0 := image.NewGray(image.Rect(-1, -1, 3, 1)) 12 | img0.Pix = []uint8{ 13 | 1, 2, 3, 4, 14 | 5, 6, 7, 8, 15 | } 16 | img1Exp := image.NewGray(image.Rect(0, 0, 2, 4)) 17 | img1Exp.Pix = []uint8{ 18 | 4, 8, 19 | 3, 7, 20 | 2, 6, 21 | 1, 5, 22 | } 23 | 24 | f := Rotate90() 25 | img1 := image.NewGray(f.Bounds(img0.Bounds())) 26 | f.Draw(img1, img0, nil) 27 | 28 | if img1.Bounds().Size() != img1Exp.Bounds().Size() { 29 | t.Errorf("expected %v got %v", img1Exp.Bounds().Size(), img1.Bounds().Size()) 30 | } 31 | if !bytes.Equal(img1Exp.Pix, img1.Pix) { 32 | t.Errorf("expected %v got %v", img1Exp.Pix, img1.Pix) 33 | } 34 | } 35 | 36 | func TestRotate180(t *testing.T) { 37 | img0 := image.NewGray(image.Rect(-1, -1, 3, 1)) 38 | img0.Pix = []uint8{ 39 | 1, 2, 3, 4, 40 | 5, 6, 7, 8, 41 | } 42 | img1Exp := image.NewGray(image.Rect(0, 0, 4, 2)) 43 | img1Exp.Pix = []uint8{ 44 | 8, 7, 6, 5, 45 | 4, 3, 2, 1, 46 | } 47 | 48 | f := Rotate180() 49 | img1 := image.NewGray(f.Bounds(img0.Bounds())) 50 | f.Draw(img1, img0, nil) 51 | 52 | if img1.Bounds().Size() != img1Exp.Bounds().Size() { 53 | t.Errorf("expected %v got %v", img1Exp.Bounds().Size(), img1.Bounds().Size()) 54 | } 55 | if !bytes.Equal(img1Exp.Pix, img1.Pix) { 56 | t.Errorf("expected %v got %v", img1Exp.Pix, img1.Pix) 57 | } 58 | } 59 | 60 | func TestRotate270(t *testing.T) { 61 | img0 := image.NewGray(image.Rect(-1, -1, 3, 1)) 62 | img0.Pix = []uint8{ 63 | 1, 2, 3, 4, 64 | 5, 6, 7, 8, 65 | } 66 | img1Exp := image.NewGray(image.Rect(0, 0, 2, 4)) 67 | img1Exp.Pix = []uint8{ 68 | 5, 1, 69 | 6, 2, 70 | 7, 3, 71 | 8, 4, 72 | } 73 | 74 | f := Rotate270() 75 | img1 := image.NewGray(f.Bounds(img0.Bounds())) 76 | f.Draw(img1, img0, nil) 77 | 78 | if img1.Bounds().Size() != img1Exp.Bounds().Size() { 79 | t.Errorf("expected %v got %v", img1Exp.Bounds().Size(), img1.Bounds().Size()) 80 | } 81 | if !bytes.Equal(img1Exp.Pix, img1.Pix) { 82 | t.Errorf("expected %v got %v", img1Exp.Pix, img1.Pix) 83 | } 84 | } 85 | 86 | func TestFlipHorizontal(t *testing.T) { 87 | img0 := image.NewGray(image.Rect(-1, -1, 3, 1)) 88 | img0.Pix = []uint8{ 89 | 1, 2, 3, 4, 90 | 5, 6, 7, 8, 91 | } 92 | img1Exp := image.NewGray(image.Rect(0, 0, 4, 2)) 93 | img1Exp.Pix = []uint8{ 94 | 4, 3, 2, 1, 95 | 8, 7, 6, 5, 96 | } 97 | 98 | f := FlipHorizontal() 99 | img1 := image.NewGray(f.Bounds(img0.Bounds())) 100 | f.Draw(img1, img0, nil) 101 | 102 | if img1.Bounds().Size() != img1Exp.Bounds().Size() { 103 | t.Errorf("expected %v got %v", img1Exp.Bounds().Size(), img1.Bounds().Size()) 104 | } 105 | if !bytes.Equal(img1Exp.Pix, img1.Pix) { 106 | t.Errorf("expected %v got %v", img1Exp.Pix, img1.Pix) 107 | } 108 | } 109 | 110 | func TestFlipVertical(t *testing.T) { 111 | img0 := image.NewGray(image.Rect(-1, -1, 3, 1)) 112 | img0.Pix = []uint8{ 113 | 1, 2, 3, 4, 114 | 5, 6, 7, 8, 115 | } 116 | img1Exp := image.NewGray(image.Rect(0, 0, 4, 2)) 117 | img1Exp.Pix = []uint8{ 118 | 5, 6, 7, 8, 119 | 1, 2, 3, 4, 120 | } 121 | 122 | f := FlipVertical() 123 | img1 := image.NewGray(f.Bounds(img0.Bounds())) 124 | f.Draw(img1, img0, nil) 125 | 126 | if img1.Bounds().Size() != img1Exp.Bounds().Size() { 127 | t.Errorf("expected %v got %v", img1Exp.Bounds().Size(), img1.Bounds().Size()) 128 | } 129 | if !bytes.Equal(img1Exp.Pix, img1.Pix) { 130 | t.Errorf("expected %v got %v", img1Exp.Pix, img1.Pix) 131 | } 132 | } 133 | 134 | func TestTranspose(t *testing.T) { 135 | img0 := image.NewGray(image.Rect(-1, -1, 3, 1)) 136 | img0.Pix = []uint8{ 137 | 1, 2, 3, 4, 138 | 5, 6, 7, 8, 139 | } 140 | img1Exp := image.NewGray(image.Rect(0, 0, 2, 4)) 141 | img1Exp.Pix = []uint8{ 142 | 1, 5, 143 | 2, 6, 144 | 3, 7, 145 | 4, 8, 146 | } 147 | 148 | f := Transpose() 149 | img1 := image.NewGray(f.Bounds(img0.Bounds())) 150 | f.Draw(img1, img0, nil) 151 | 152 | if img1.Bounds().Size() != img1Exp.Bounds().Size() { 153 | t.Errorf("expected %v got %v", img1Exp.Bounds().Size(), img1.Bounds().Size()) 154 | } 155 | if !bytes.Equal(img1Exp.Pix, img1.Pix) { 156 | t.Errorf("expected %v got %v", img1Exp.Pix, img1.Pix) 157 | } 158 | } 159 | 160 | func TestTransverse(t *testing.T) { 161 | img0 := image.NewGray(image.Rect(-1, -1, 3, 1)) 162 | img0.Pix = []uint8{ 163 | 1, 2, 3, 4, 164 | 5, 6, 7, 8, 165 | } 166 | img1Exp := image.NewGray(image.Rect(0, 0, 2, 4)) 167 | img1Exp.Pix = []uint8{ 168 | 8, 4, 169 | 7, 3, 170 | 6, 2, 171 | 5, 1, 172 | } 173 | 174 | f := Transverse() 175 | img1 := image.NewGray(f.Bounds(img0.Bounds())) 176 | f.Draw(img1, img0, nil) 177 | 178 | if img1.Bounds().Size() != img1Exp.Bounds().Size() { 179 | t.Errorf("expected %v got %v", img1Exp.Bounds().Size(), img1.Bounds().Size()) 180 | } 181 | if !bytes.Equal(img1Exp.Pix, img1.Pix) { 182 | t.Errorf("expected %v got %v", img1Exp.Pix, img1.Pix) 183 | } 184 | } 185 | 186 | func TestCrop(t *testing.T) { 187 | testData := []struct { 188 | desc string 189 | r image.Rectangle 190 | srcb, dstb image.Rectangle 191 | srcPix, dstPix []uint8 192 | }{ 193 | { 194 | "crop (0, 0, 0, 0)", 195 | image.Rect(0, 0, 0, 0), 196 | image.Rect(-1, -1, 4, 2), 197 | image.Rect(0, 0, 0, 0), 198 | []uint8{ 199 | 0x00, 0x40, 0x00, 0x40, 0x00, 200 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 201 | 0x00, 0x80, 0x00, 0x80, 0x00, 202 | }, 203 | []uint8{}, 204 | }, 205 | { 206 | "crop (1, 1, -1, -1)", 207 | image.Rectangle{image.Pt(1, 1), image.Pt(-1, -1)}, 208 | image.Rect(-1, -1, 4, 2), 209 | image.Rect(0, 0, 0, 0), 210 | []uint8{ 211 | 0x00, 0x40, 0x00, 0x40, 0x00, 212 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 213 | 0x00, 0x80, 0x00, 0x80, 0x00, 214 | }, 215 | []uint8{}, 216 | }, 217 | { 218 | "crop (-1, 0, 3, 2)", 219 | image.Rect(-1, 0, 3, 2), 220 | image.Rect(-1, -1, 4, 2), 221 | image.Rect(0, 0, 4, 2), 222 | []uint8{ 223 | 0x00, 0x40, 0x00, 0x40, 0x00, 224 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 225 | 0x00, 0x80, 0x00, 0x80, 0x00, 226 | }, 227 | []uint8{ 228 | 0x60, 0xB0, 0xA0, 0xB0, 229 | 0x00, 0x80, 0x00, 0x80, 230 | }, 231 | }, 232 | { 233 | "crop (-100, -100, 2, 2)", 234 | image.Rect(-100, -100, 2, 2), 235 | image.Rect(-1, -1, 4, 2), 236 | image.Rect(0, 0, 3, 3), 237 | []uint8{ 238 | 0x00, 0x40, 0x00, 0x40, 0x00, 239 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 240 | 0x00, 0x80, 0x00, 0x80, 0x00, 241 | }, 242 | []uint8{ 243 | 0x00, 0x40, 0x00, 244 | 0x60, 0xB0, 0xA0, 245 | 0x00, 0x80, 0x00, 246 | }, 247 | }, 248 | { 249 | "crop (-100, -100, 100, 100)", 250 | image.Rect(-100, -100, 100, 100), 251 | image.Rect(-1, -1, 4, 2), 252 | image.Rect(0, 0, 5, 3), 253 | []uint8{ 254 | 0x00, 0x40, 0x00, 0x40, 0x00, 255 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 256 | 0x00, 0x80, 0x00, 0x80, 0x00, 257 | }, 258 | []uint8{ 259 | 0x00, 0x40, 0x00, 0x40, 0x00, 260 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 261 | 0x00, 0x80, 0x00, 0x80, 0x00, 262 | }, 263 | }, 264 | } 265 | 266 | for _, d := range testData { 267 | src := image.NewGray(d.srcb) 268 | src.Pix = d.srcPix 269 | 270 | f := Crop(d.r) 271 | dst := image.NewGray(f.Bounds(src.Bounds())) 272 | f.Draw(dst, src, nil) 273 | 274 | if !checkBoundsAndPix(dst.Bounds(), d.dstb, dst.Pix, d.dstPix) { 275 | t.Errorf("test [%s] failed: %#v, %#v", d.desc, dst.Bounds(), dst.Pix) 276 | } 277 | } 278 | } 279 | 280 | func TestCropToSize(t *testing.T) { 281 | testData := []struct { 282 | desc string 283 | w, h int 284 | anchor Anchor 285 | srcb, dstb image.Rectangle 286 | srcPix, dstPix []uint8 287 | }{ 288 | { 289 | "crop to size (0, 0, center)", 290 | 0, 0, CenterAnchor, 291 | image.Rect(-1, -1, 4, 4), 292 | image.Rect(0, 0, 0, 0), 293 | []uint8{ 294 | 0x00, 0x01, 0x02, 0x03, 0x04, 295 | 0x05, 0x06, 0x07, 0x08, 0x09, 296 | 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 297 | 0x0f, 0x10, 0x11, 0x12, 0x13, 298 | 0x14, 0x15, 0x16, 0x17, 0x18, 299 | }, 300 | []uint8{}, 301 | }, 302 | { 303 | "crop to size (3, 3, center)", 304 | 3, 3, CenterAnchor, 305 | image.Rect(-1, -1, 4, 4), 306 | image.Rect(0, 0, 3, 3), 307 | []uint8{ 308 | 0x00, 0x01, 0x02, 0x03, 0x04, 309 | 0x05, 0x06, 0x07, 0x08, 0x09, 310 | 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 311 | 0x0f, 0x10, 0x11, 0x12, 0x13, 312 | 0x14, 0x15, 0x16, 0x17, 0x18, 313 | }, 314 | []uint8{ 315 | 0x06, 0x07, 0x08, 316 | 0x0b, 0x0c, 0x0d, 317 | 0x10, 0x11, 0x12, 318 | }, 319 | }, 320 | { 321 | "crop to size (3, 3, top-left)", 322 | 3, 3, TopLeftAnchor, 323 | image.Rect(-1, -1, 4, 4), 324 | image.Rect(0, 0, 3, 3), 325 | []uint8{ 326 | 0x00, 0x01, 0x02, 0x03, 0x04, 327 | 0x05, 0x06, 0x07, 0x08, 0x09, 328 | 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 329 | 0x0f, 0x10, 0x11, 0x12, 0x13, 330 | 0x14, 0x15, 0x16, 0x17, 0x18, 331 | }, 332 | []uint8{ 333 | 0x00, 0x01, 0x02, 334 | 0x05, 0x06, 0x07, 335 | 0x0a, 0x0b, 0x0c, 336 | }, 337 | }, 338 | { 339 | "crop to size (3, 3, top)", 340 | 3, 3, TopAnchor, 341 | image.Rect(-1, -1, 4, 4), 342 | image.Rect(0, 0, 3, 3), 343 | []uint8{ 344 | 0x00, 0x01, 0x02, 0x03, 0x04, 345 | 0x05, 0x06, 0x07, 0x08, 0x09, 346 | 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 347 | 0x0f, 0x10, 0x11, 0x12, 0x13, 348 | 0x14, 0x15, 0x16, 0x17, 0x18, 349 | }, 350 | []uint8{ 351 | 0x01, 0x02, 0x03, 352 | 0x06, 0x07, 0x08, 353 | 0x0b, 0x0c, 0x0d, 354 | }, 355 | }, 356 | { 357 | "crop to size (3, 3,, top-right)", 358 | 3, 3, TopRightAnchor, 359 | image.Rect(-1, -1, 4, 4), 360 | image.Rect(0, 0, 3, 3), 361 | []uint8{ 362 | 0x00, 0x01, 0x02, 0x03, 0x04, 363 | 0x05, 0x06, 0x07, 0x08, 0x09, 364 | 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 365 | 0x0f, 0x10, 0x11, 0x12, 0x13, 366 | 0x14, 0x15, 0x16, 0x17, 0x18, 367 | }, 368 | []uint8{ 369 | 0x02, 0x03, 0x04, 370 | 0x07, 0x08, 0x09, 371 | 0x0c, 0x0d, 0x0e, 372 | }, 373 | }, 374 | { 375 | "crop to size (3, 3, left)", 376 | 3, 3, LeftAnchor, 377 | image.Rect(-1, -1, 4, 4), 378 | image.Rect(0, 0, 3, 3), 379 | []uint8{ 380 | 0x00, 0x01, 0x02, 0x03, 0x04, 381 | 0x05, 0x06, 0x07, 0x08, 0x09, 382 | 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 383 | 0x0f, 0x10, 0x11, 0x12, 0x13, 384 | 0x14, 0x15, 0x16, 0x17, 0x18, 385 | }, 386 | []uint8{ 387 | 0x05, 0x06, 0x07, 388 | 0x0a, 0x0b, 0x0c, 389 | 0x0f, 0x10, 0x11, 390 | }, 391 | }, 392 | { 393 | "crop to size (3, 3, right)", 394 | 3, 3, RightAnchor, 395 | image.Rect(-1, -1, 4, 4), 396 | image.Rect(0, 0, 3, 3), 397 | []uint8{ 398 | 0x00, 0x01, 0x02, 0x03, 0x04, 399 | 0x05, 0x06, 0x07, 0x08, 0x09, 400 | 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 401 | 0x0f, 0x10, 0x11, 0x12, 0x13, 402 | 0x14, 0x15, 0x16, 0x17, 0x18, 403 | }, 404 | []uint8{ 405 | 0x07, 0x08, 0x09, 406 | 0x0c, 0x0d, 0x0e, 407 | 0x11, 0x12, 0x13, 408 | }, 409 | }, 410 | { 411 | "crop to size (3, 3, bottom-left)", 412 | 3, 3, BottomLeftAnchor, 413 | image.Rect(-1, -1, 4, 4), 414 | image.Rect(0, 0, 3, 3), 415 | []uint8{ 416 | 0x00, 0x01, 0x02, 0x03, 0x04, 417 | 0x05, 0x06, 0x07, 0x08, 0x09, 418 | 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 419 | 0x0f, 0x10, 0x11, 0x12, 0x13, 420 | 0x14, 0x15, 0x16, 0x17, 0x18, 421 | }, 422 | []uint8{ 423 | 0x0a, 0x0b, 0x0c, 424 | 0x0f, 0x10, 0x11, 425 | 0x14, 0x15, 0x16, 426 | }, 427 | }, 428 | { 429 | "crop to size (3, 3, bottom)", 430 | 3, 3, BottomAnchor, 431 | image.Rect(-1, -1, 4, 4), 432 | image.Rect(0, 0, 3, 3), 433 | []uint8{ 434 | 0x00, 0x01, 0x02, 0x03, 0x04, 435 | 0x05, 0x06, 0x07, 0x08, 0x09, 436 | 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 437 | 0x0f, 0x10, 0x11, 0x12, 0x13, 438 | 0x14, 0x15, 0x16, 0x17, 0x18, 439 | }, 440 | []uint8{ 441 | 0x0b, 0x0c, 0x0d, 442 | 0x10, 0x11, 0x12, 443 | 0x15, 0x16, 0x17, 444 | }, 445 | }, 446 | { 447 | "crop to size (3, 3, bottom-right)", 448 | 3, 3, BottomRightAnchor, 449 | image.Rect(-1, -1, 4, 4), 450 | image.Rect(0, 0, 3, 3), 451 | []uint8{ 452 | 0x00, 0x01, 0x02, 0x03, 0x04, 453 | 0x05, 0x06, 0x07, 0x08, 0x09, 454 | 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 455 | 0x0f, 0x10, 0x11, 0x12, 0x13, 456 | 0x14, 0x15, 0x16, 0x17, 0x18, 457 | }, 458 | []uint8{ 459 | 0x0c, 0x0d, 0x0e, 460 | 0x11, 0x12, 0x13, 461 | 0x16, 0x17, 0x18, 462 | }, 463 | }, 464 | { 465 | "crop to size (100, 100, center)", 466 | 100, 100, CenterAnchor, 467 | image.Rect(-1, -1, 4, 4), 468 | image.Rect(0, 0, 5, 5), 469 | []uint8{ 470 | 0x00, 0x01, 0x02, 0x03, 0x04, 471 | 0x05, 0x06, 0x07, 0x08, 0x09, 472 | 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 473 | 0x0f, 0x10, 0x11, 0x12, 0x13, 474 | 0x14, 0x15, 0x16, 0x17, 0x18, 475 | }, 476 | []uint8{ 477 | 0x00, 0x01, 0x02, 0x03, 0x04, 478 | 0x05, 0x06, 0x07, 0x08, 0x09, 479 | 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 480 | 0x0f, 0x10, 0x11, 0x12, 0x13, 481 | 0x14, 0x15, 0x16, 0x17, 0x18, 482 | }, 483 | }, 484 | } 485 | 486 | for _, d := range testData { 487 | src := image.NewGray(d.srcb) 488 | src.Pix = d.srcPix 489 | 490 | f := CropToSize(d.w, d.h, d.anchor) 491 | dst := image.NewGray(f.Bounds(src.Bounds())) 492 | f.Draw(dst, src, nil) 493 | 494 | if !checkBoundsAndPix(dst.Bounds(), d.dstb, dst.Pix, d.dstPix) { 495 | t.Errorf("test [%s] failed: %#v, %#v", d.desc, dst.Bounds(), dst.Pix) 496 | } 497 | } 498 | } 499 | 500 | func TestRotate(t *testing.T) { 501 | testData := []struct { 502 | desc string 503 | a float32 504 | bg color.Color 505 | interp Interpolation 506 | srcb, dstb image.Rectangle 507 | srcPix, dstPix []uint8 508 | }{ 509 | { 510 | "rotate 0x0 90 white nearest", 511 | 90, color.White, NearestNeighborInterpolation, 512 | image.Rect(0, 0, 0, 0), 513 | image.Rect(0, 0, 0, 0), 514 | []uint8{}, 515 | []uint8{}, 516 | }, 517 | { 518 | "rotate 1x1 90 white nearest", 519 | 90, color.White, NearestNeighborInterpolation, 520 | image.Rect(-1, -1, 0, 0), 521 | image.Rect(0, 0, 1, 1), 522 | []uint8{0x80}, 523 | []uint8{0x80}, 524 | }, 525 | { 526 | "rotate 3x3 -90 white nearest", 527 | -90, color.White, NearestNeighborInterpolation, 528 | image.Rect(-1, -1, 2, 2), 529 | image.Rect(0, 0, 3, 3), 530 | []uint8{ 531 | 0x10, 0x20, 0x30, 532 | 0x40, 0x50, 0x60, 533 | 0x70, 0x80, 0x90, 534 | }, 535 | []uint8{ 536 | 0x70, 0x40, 0x10, 537 | 0x80, 0x50, 0x20, 538 | 0x90, 0x60, 0x30, 539 | }, 540 | }, 541 | { 542 | "rotate 3x3 -90 white linear", 543 | -90, color.White, LinearInterpolation, 544 | image.Rect(-1, -1, 2, 2), 545 | image.Rect(0, 0, 3, 3), 546 | []uint8{ 547 | 0x10, 0x20, 0x30, 548 | 0x40, 0x50, 0x60, 549 | 0x70, 0x80, 0x90, 550 | }, 551 | []uint8{ 552 | 0x70, 0x40, 0x10, 553 | 0x80, 0x50, 0x20, 554 | 0x90, 0x60, 0x30, 555 | }, 556 | }, 557 | { 558 | "rotate 3x3 45 black nearest", 559 | 45, color.Black, NearestNeighborInterpolation, 560 | image.Rect(-1, -1, 2, 2), 561 | image.Rect(0, 0, 5, 5), 562 | []uint8{ 563 | 0x10, 0x20, 0x30, 564 | 0x40, 0x50, 0x60, 565 | 0x70, 0x80, 0x90, 566 | }, 567 | []uint8{ 568 | 0x00, 0x00, 0x30, 0x00, 0x00, 569 | 0x00, 0x20, 0x30, 0x60, 0x00, 570 | 0x10, 0x10, 0x50, 0x90, 0x90, 571 | 0x00, 0x40, 0x70, 0x80, 0x00, 572 | 0x00, 0x00, 0x70, 0x00, 0x00, 573 | }, 574 | }, 575 | { 576 | "rotate 5x5 45 black linear", 577 | 45, color.Black, LinearInterpolation, 578 | image.Rect(-1, -1, 4, 4), 579 | image.Rect(0, 0, 8, 8), 580 | []uint8{ 581 | 0xff, 0xff, 0xff, 0xff, 0xff, 582 | 0xff, 0xff, 0xff, 0xff, 0xff, 583 | 0xff, 0xff, 0xff, 0xff, 0xff, 584 | 0xff, 0xff, 0xff, 0xff, 0xff, 585 | 0xff, 0xff, 0xff, 0xff, 0xff, 586 | }, 587 | []uint8{ 588 | 0x00, 0x00, 0x00, 0x26, 0x26, 0x00, 0x00, 0x00, 589 | 0x00, 0x00, 0x2c, 0xe0, 0xe0, 0x2c, 0x00, 0x00, 590 | 0x00, 0x2c, 0xe0, 0xff, 0xff, 0xe0, 0x2c, 0x00, 591 | 0x26, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x26, 592 | 0x26, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x26, 593 | 0x00, 0x2c, 0xe0, 0xff, 0xff, 0xe0, 0x2c, 0x00, 594 | 0x00, 0x00, 0x2c, 0xe0, 0xe0, 0x2c, 0x00, 0x00, 595 | 0x00, 0x00, 0x00, 0x26, 0x26, 0x00, 0x00, 0x00, 596 | }, 597 | }, 598 | { 599 | "rotate 5x5 45 black cubic", 600 | 45, color.Black, CubicInterpolation, 601 | image.Rect(-1, -1, 4, 4), 602 | image.Rect(0, 0, 8, 8), 603 | []uint8{ 604 | 0xff, 0xff, 0xff, 0xff, 0xff, 605 | 0xff, 0xff, 0xff, 0xff, 0xff, 606 | 0xff, 0xff, 0xff, 0xff, 0xff, 607 | 0xff, 0xff, 0xff, 0xff, 0xff, 608 | 0xff, 0xff, 0xff, 0xff, 0xff, 609 | }, 610 | []uint8{ 611 | 0x00, 0x00, 0x00, 0x23, 0x23, 0x00, 0x00, 0x00, 612 | 0x00, 0x00, 0x28, 0xf1, 0xf1, 0x28, 0x00, 0x00, 613 | 0x00, 0x28, 0xe3, 0xff, 0xff, 0xe3, 0x28, 0x00, 614 | 0x23, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xf1, 0x23, 615 | 0x23, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xf1, 0x23, 616 | 0x00, 0x28, 0xe3, 0xff, 0xff, 0xe3, 0x28, 0x00, 617 | 0x00, 0x00, 0x28, 0xf1, 0xf1, 0x28, 0x00, 0x00, 618 | 0x00, 0x00, 0x00, 0x23, 0x23, 0x00, 0x00, 0x00, 619 | }, 620 | }, 621 | } 622 | 623 | for _, d := range testData { 624 | src := image.NewGray(d.srcb) 625 | src.Pix = d.srcPix 626 | 627 | f := Rotate(d.a, d.bg, d.interp) 628 | dst := image.NewGray(f.Bounds(src.Bounds())) 629 | f.Draw(dst, src, nil) 630 | 631 | if !checkBoundsAndPix(dst.Bounds(), d.dstb, dst.Pix, d.dstPix) { 632 | t.Errorf("test [%s] failed: %#v, %#v", d.desc, dst.Bounds(), dst.Pix) 633 | } 634 | } 635 | 636 | } 637 | -------------------------------------------------------------------------------- /convolution_test.go: -------------------------------------------------------------------------------- 1 | package gift 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | ) 7 | 8 | func TestConvolution(t *testing.T) { 9 | testData := []struct { 10 | desc string 11 | kernel []float32 12 | normalize, alpha, abs bool 13 | delta float32 14 | srcb, dstb image.Rectangle 15 | srcPix, dstPix []uint8 16 | }{ 17 | { 18 | "convolution (0x0, false, false, false, 0)", 19 | []float32{}, 20 | false, false, false, 0, 21 | image.Rect(-1, -1, 2, 2), 22 | image.Rect(0, 0, 3, 3), 23 | []uint8{ 24 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x60, 0x80, 25 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 26 | 0x80, 0x60, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 27 | }, 28 | []uint8{ 29 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x60, 0x80, 30 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 31 | 0x80, 0x60, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 32 | }, 33 | }, 34 | { 35 | "convolution (3x3, false, false, false, 0)", 36 | []float32{ 37 | 0, 0, 1, 38 | 0, 0, 0, 39 | 1, 0, 0, 40 | }, 41 | false, false, false, 0, 42 | image.Rect(-1, -1, 2, 2), 43 | image.Rect(0, 0, 3, 3), 44 | []uint8{ 45 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x60, 0x80, 46 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 47 | 0x80, 0x60, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 48 | }, 49 | []uint8{ 50 | 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x60, 0x00, 0x20, 0x40, 0x60, 0x80, 51 | 0x80, 0x60, 0x40, 0x00, 0xA0, 0xA0, 0xA0, 0x00, 0x20, 0x40, 0x60, 0x00, 52 | 0x80, 0x60, 0x40, 0x20, 0x80, 0x60, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 53 | }, 54 | }, 55 | { 56 | "convolution (3x3, true, false, false, 0)", 57 | []float32{ 58 | 0, 0, 1, 59 | 0, 0, 0, 60 | 1, 0, 0, 61 | }, 62 | true, false, false, 0, 63 | image.Rect(-1, -1, 2, 2), 64 | image.Rect(0, 0, 3, 3), 65 | []uint8{ 66 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x60, 0x80, 67 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 68 | 0x80, 0x60, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 69 | }, 70 | []uint8{ 71 | 0x00, 0x00, 0x00, 0x00, 0x10, 0x20, 0x30, 0x00, 0x10, 0x20, 0x30, 0x80, 72 | 0x40, 0x30, 0x20, 0x00, 0x50, 0x50, 0x50, 0x00, 0x10, 0x20, 0x30, 0x00, 73 | 0x40, 0x30, 0x20, 0x20, 0x40, 0x30, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 74 | }, 75 | }, 76 | { 77 | "convolution (3x3, false, true, false, 0)", 78 | []float32{ 79 | 0, 0, 1, 80 | 0, 0, 0, 81 | 1, 0, 0, 82 | }, 83 | false, true, false, 0, 84 | image.Rect(-1, -1, 2, 2), 85 | image.Rect(0, 0, 3, 3), 86 | []uint8{ 87 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x60, 0x80, 88 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 89 | 0x80, 0x60, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 90 | }, 91 | []uint8{ 92 | 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x60, 0x80, 0x20, 0x40, 0x60, 0x80, 93 | 0x80, 0x60, 0x40, 0x20, 0xA0, 0xA0, 0xA0, 0xA0, 0x20, 0x40, 0x60, 0x80, 94 | 0x80, 0x60, 0x40, 0x20, 0x80, 0x60, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 95 | }, 96 | }, 97 | { 98 | "convolution (3x3, true, true, true, 0)", 99 | []float32{ 100 | 0, 0, 0, 101 | -0.5, 0, 0.5, 102 | 0, 0, 0, 103 | }, 104 | true, true, true, 0, 105 | image.Rect(-1, -1, 2, 2), 106 | image.Rect(0, 0, 3, 3), 107 | []uint8{ 108 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x60, 0x80, 109 | 0x80, 0x60, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x60, 0x80, 110 | 0x80, 0x60, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 111 | }, 112 | []uint8{ 113 | 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x60, 0x80, 0x20, 0x40, 0x60, 0x80, 114 | 0x80, 0x60, 0x40, 0x20, 0x60, 0x20, 0x20, 0x60, 0x20, 0x40, 0x60, 0x80, 115 | 0x80, 0x60, 0x40, 0x20, 0x80, 0x60, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 116 | }, 117 | }, 118 | { 119 | "convolution (3x3, false, true, false, 3 / 255.0)", 120 | []float32{ 121 | 0, 0, 1, 122 | 0, 0, 0, 123 | 1, 0, 0, 124 | }, 125 | false, true, false, 3 / 255.0, 126 | image.Rect(-1, -1, 2, 2), 127 | image.Rect(0, 0, 3, 3), 128 | []uint8{ 129 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x60, 0x80, 130 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 131 | 0x80, 0x60, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 132 | }, 133 | []uint8{ 134 | 0x03, 0x03, 0x03, 0x03, 0x23, 0x43, 0x63, 0x83, 0x23, 0x43, 0x63, 0x83, 135 | 0x83, 0x63, 0x43, 0x23, 0xA3, 0xA3, 0xA3, 0xA3, 0x23, 0x43, 0x63, 0x83, 136 | 0x83, 0x63, 0x43, 0x23, 0x83, 0x63, 0x43, 0x23, 0x03, 0x03, 0x03, 0x03, 137 | }, 138 | }, 139 | { 140 | "convolution (3x3, false, false, true, 0)", 141 | []float32{ 142 | 0, 0, -0.5, 143 | 0, 0, 0, 144 | 0, 0, 0, 145 | }, 146 | false, false, true, 0, 147 | image.Rect(-1, -1, 2, 2), 148 | image.Rect(0, 0, 3, 3), 149 | []uint8{ 150 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x60, 0x80, 151 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 152 | 0x80, 0x60, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 153 | }, 154 | []uint8{ 155 | 0x00, 0x00, 0x00, 0x00, 0x10, 0x20, 0x30, 0x00, 0x10, 0x20, 0x30, 0x80, 156 | 0x00, 0x00, 0x00, 0x00, 0x10, 0x20, 0x30, 0x00, 0x10, 0x20, 0x30, 0x00, 157 | 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 158 | }, 159 | }, 160 | { 161 | "convolution (7x7, false, true, false, 0)", 162 | []float32{ 163 | 0, 0, 0, 0, 0, 0, 1, 164 | 0, 0, 0, 0, 0, 0, 0, 165 | 0, 0, 0, 0, 0, 0, 0, 166 | 0, 0, 0, 0, 0, 0, 0, 167 | 0, 0, 0, 0, 0, 0, 0, 168 | 0, 0, 0, 0, 0, 0, 0, 169 | 1, 0, 0, 0, 0, 0, 0, 170 | }, 171 | false, true, false, 0, 172 | image.Rect(-1, -1, 2, 2), 173 | image.Rect(0, 0, 3, 3), 174 | []uint8{ 175 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x60, 0x80, 176 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 177 | 0x80, 0x60, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 178 | }, 179 | []uint8{ 180 | 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 181 | 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 182 | 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 183 | }, 184 | }, 185 | } 186 | 187 | for _, d := range testData { 188 | for _, parallel := range []bool{true, false} { 189 | src := image.NewNRGBA(d.srcb) 190 | src.Pix = d.srcPix 191 | 192 | f := Convolution(d.kernel, d.normalize, d.alpha, d.abs, d.delta) 193 | dst := image.NewNRGBA(f.Bounds(src.Bounds())) 194 | f.Draw(dst, src, &Options{Parallelization: parallel}) 195 | 196 | if !checkBoundsAndPix(dst.Bounds(), d.dstb, dst.Pix, d.dstPix) { 197 | t.Errorf("test [%s] failed: %#v, %#v", d.desc, dst.Bounds(), dst.Pix) 198 | } 199 | } 200 | } 201 | 202 | testKernelSizes := []struct { 203 | klen, size int 204 | }{ 205 | {0, 0}, 206 | {1, 1}, {3, 1}, {8, 1}, 207 | {9, 3}, {16, 3}, {24, 3}, 208 | {25, 5}, {40, 5}, {48, 5}, 209 | } 210 | 211 | for _, d := range testKernelSizes { 212 | tmp := make([]float32, d.klen) 213 | sz, _ := prepareConvolutionWeights(tmp, true) 214 | if sz != d.size { 215 | t.Errorf("unexpected kernel size: %d %d", d.klen, sz) 216 | } 217 | } 218 | 219 | // check no panics 220 | Convolution([]float32{}, true, true, true, 1).Draw(image.NewGray(image.Rect(0, 0, 0, 0)), image.NewGray(image.Rect(0, 0, 0, 0)), nil) 221 | convolve1dh(image.NewGray(image.Rect(0, 0, 1, 1)), image.NewGray(image.Rect(0, 0, 1, 1)), []float32{}, nil) 222 | convolve1dv(image.NewGray(image.Rect(0, 0, 1, 1)), image.NewGray(image.Rect(0, 0, 1, 1)), []float32{}, nil) 223 | convolve1dh(image.NewGray(image.Rect(0, 0, 0, 0)), image.NewGray(image.Rect(0, 0, 0, 0)), []float32{}, nil) 224 | convolve1dv(image.NewGray(image.Rect(0, 0, 0, 0)), image.NewGray(image.Rect(0, 0, 0, 0)), []float32{}, nil) 225 | convolveLine([]pixel{}, []pixel{}, []uweight{}) 226 | prepareConvolutionWeights1d([]float32{0, 0}) 227 | prepareConvolutionWeights1d([]float32{}) 228 | } 229 | 230 | func TestGaussianBlur(t *testing.T) { 231 | testData := []struct { 232 | desc string 233 | sigma float32 234 | srcb, dstb image.Rectangle 235 | srcPix, dstPix []uint8 236 | }{ 237 | { 238 | "blur (0)", 239 | 0, 240 | image.Rect(-1, -1, 4, 2), 241 | image.Rect(0, 0, 5, 3), 242 | []uint8{ 243 | 0x00, 0x00, 0x00, 0x00, 0x00, 244 | 0x00, 0xC0, 0x00, 0xC0, 0x00, 245 | 0x00, 0x00, 0x00, 0x00, 0x00, 246 | }, 247 | []uint8{ 248 | 0x00, 0x00, 0x00, 0x00, 0x00, 249 | 0x00, 0xC0, 0x00, 0xC0, 0x00, 250 | 0x00, 0x00, 0x00, 0x00, 0x00, 251 | }, 252 | }, 253 | { 254 | "blur (0.3)", 255 | 0.3, 256 | image.Rect(-1, -1, 4, 2), 257 | image.Rect(0, 0, 5, 3), 258 | []uint8{ 259 | 0x00, 0x00, 0x00, 0x00, 0x00, 260 | 0x00, 0xC0, 0x00, 0xC0, 0x00, 261 | 0x00, 0x00, 0x00, 0x00, 0x00, 262 | }, 263 | []uint8{ 264 | 0x00, 0x01, 0x00, 0x01, 0x00, 265 | 0x01, 0xBD, 0x01, 0xBD, 0x01, 266 | 0x00, 0x01, 0x00, 0x01, 0x00, 267 | }, 268 | }, 269 | { 270 | "blur (1)", 271 | 1, 272 | image.Rect(-1, -1, 4, 2), 273 | image.Rect(0, 0, 5, 3), 274 | []uint8{ 275 | 0x00, 0x00, 0x00, 0x00, 0x00, 276 | 0x00, 0xC0, 0x00, 0xC0, 0x00, 277 | 0x00, 0x00, 0x00, 0x00, 0x00, 278 | }, 279 | []uint8{ 280 | 0x0B, 0x15, 0x16, 0x15, 0x0B, 281 | 0x13, 0x23, 0x25, 0x23, 0x13, 282 | 0x0B, 0x15, 0x16, 0x15, 0x0B, 283 | }, 284 | }, 285 | { 286 | "blur (3)", 287 | 3, 288 | image.Rect(-1, -1, 4, 2), 289 | image.Rect(0, 0, 5, 3), 290 | []uint8{ 291 | 0x00, 0x00, 0x00, 0x00, 0x00, 292 | 0x00, 0xC0, 0x00, 0xC0, 0x00, 293 | 0x00, 0x00, 0x00, 0x00, 0x00, 294 | }, 295 | []uint8{ 296 | 0x05, 0x06, 0x06, 0x06, 0x05, 297 | 0x05, 0x06, 0x06, 0x06, 0x05, 298 | 0x05, 0x06, 0x06, 0x06, 0x05, 299 | }, 300 | }, 301 | } 302 | 303 | for _, d := range testData { 304 | src := image.NewGray(d.srcb) 305 | src.Pix = d.srcPix 306 | 307 | f := GaussianBlur(d.sigma) 308 | dst := image.NewGray(f.Bounds(src.Bounds())) 309 | f.Draw(dst, src, nil) 310 | 311 | if !checkBoundsAndPix(dst.Bounds(), d.dstb, dst.Pix, d.dstPix) { 312 | t.Errorf("test [%s] failed: %#v, %#v", d.desc, dst.Bounds(), dst.Pix) 313 | } 314 | } 315 | 316 | // check no panics 317 | GaussianBlur(0.5).Draw(image.NewGray(image.Rect(0, 0, 0, 0)), image.NewGray(image.Rect(0, 0, 0, 0)), nil) 318 | } 319 | 320 | func TestUnsharpMask(t *testing.T) { 321 | testData := []struct { 322 | desc string 323 | sigma, amount, threshold float32 324 | srcb, dstb image.Rectangle 325 | srcPix, dstPix []uint8 326 | }{ 327 | { 328 | "unsharp mask (0.3, 1, 0)", 329 | 0.3, 1, 0, 330 | image.Rect(-1, -1, 4, 2), 331 | image.Rect(0, 0, 5, 3), 332 | []uint8{ 333 | 0x00, 0x40, 0x00, 0x40, 0x00, 334 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 335 | 0x00, 0x80, 0x00, 0x80, 0x00, 336 | }, 337 | []uint8{ 338 | 0x00, 0x40, 0x00, 0x40, 0x00, 339 | 0x60, 0xB1, 0xA1, 0xB1, 0x60, 340 | 0x00, 0x81, 0x00, 0x81, 0x00, 341 | }, 342 | }, 343 | { 344 | "unsharp mask (1, 1, 0)", 345 | 1, 1, 0, 346 | image.Rect(-1, -1, 4, 2), 347 | image.Rect(0, 0, 5, 3), 348 | []uint8{ 349 | 0x00, 0x40, 0x00, 0x40, 0x00, 350 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 351 | 0x00, 0x80, 0x00, 0x80, 0x00, 352 | }, 353 | []uint8{ 354 | 0x00, 0x45, 0x00, 0x45, 0x00, 355 | 0x82, 0xFF, 0xE4, 0xFF, 0x82, 356 | 0x00, 0xB2, 0x00, 0xB2, 0x00, 357 | }, 358 | }, 359 | { 360 | "unsharp mask (1, 0.5, 0)", 361 | 1, 0.5, 0, 362 | image.Rect(-1, -1, 4, 2), 363 | image.Rect(0, 0, 5, 3), 364 | []uint8{ 365 | 0x00, 0x40, 0x00, 0x40, 0x00, 366 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 367 | 0x00, 0x80, 0x00, 0x80, 0x00, 368 | }, 369 | []uint8{ 370 | 0x00, 0x42, 0x00, 0x42, 0x00, 371 | 0x71, 0xDD, 0xC2, 0xDD, 0x71, 372 | 0x00, 0x99, 0x00, 0x99, 0x00, 373 | }, 374 | }, 375 | { 376 | "unsharp mask (1, 1, 0.05)", 377 | 1, 1, 0.05, 378 | image.Rect(-1, -1, 4, 2), 379 | image.Rect(0, 0, 5, 3), 380 | []uint8{ 381 | 0x00, 0x40, 0x00, 0x40, 0x00, 382 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 383 | 0x00, 0x80, 0x00, 0x80, 0x00, 384 | }, 385 | []uint8{ 386 | 0x00, 0x40, 0x00, 0x40, 0x00, 387 | 0x82, 0xFF, 0xE4, 0xFF, 0x82, 388 | 0x00, 0xB2, 0x00, 0xB2, 0x00, 389 | }, 390 | }, 391 | } 392 | 393 | for _, d := range testData { 394 | src := image.NewGray(d.srcb) 395 | src.Pix = d.srcPix 396 | 397 | f := UnsharpMask(d.sigma, d.amount, d.threshold) 398 | dst := image.NewGray(f.Bounds(src.Bounds())) 399 | f.Draw(dst, src, nil) 400 | 401 | if !checkBoundsAndPix(dst.Bounds(), d.dstb, dst.Pix, d.dstPix) { 402 | t.Errorf("test [%s] failed: %#v, %#v", d.desc, dst.Bounds(), dst.Pix) 403 | } 404 | } 405 | 406 | // check no panics 407 | UnsharpMask(0.5, 1, 0).Draw(image.NewGray(image.Rect(0, 0, 0, 0)), image.NewGray(image.Rect(0, 0, 0, 0)), nil) 408 | } 409 | 410 | func TestMean(t *testing.T) { 411 | testData := []struct { 412 | desc string 413 | ksize int 414 | disk bool 415 | srcb, dstb image.Rectangle 416 | srcPix, dstPix []uint8 417 | }{ 418 | { 419 | "mean (0x0 false)", 420 | 0, false, 421 | image.Rect(-1, -1, 4, 2), 422 | image.Rect(0, 0, 5, 3), 423 | []uint8{ 424 | 0x00, 0x40, 0x00, 0x40, 0x00, 425 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 426 | 0x00, 0x80, 0x00, 0x80, 0x00, 427 | }, 428 | []uint8{ 429 | 0x00, 0x40, 0x00, 0x40, 0x00, 430 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 431 | 0x00, 0x80, 0x00, 0x80, 0x00, 432 | }, 433 | }, 434 | { 435 | "mean (1x1 false)", 436 | 1, false, 437 | image.Rect(-1, -1, 4, 2), 438 | image.Rect(0, 0, 5, 3), 439 | []uint8{ 440 | 0x00, 0x40, 0x00, 0x40, 0x00, 441 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 442 | 0x00, 0x80, 0x00, 0x80, 0x00, 443 | }, 444 | []uint8{ 445 | 0x00, 0x40, 0x00, 0x40, 0x00, 446 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 447 | 0x00, 0x80, 0x00, 0x80, 0x00, 448 | }, 449 | }, 450 | { 451 | "mean (2x2 true)", 452 | 2, true, 453 | image.Rect(-1, -1, 4, 2), 454 | image.Rect(0, 0, 5, 3), 455 | []uint8{ 456 | 0x00, 0x40, 0x00, 0x40, 0x00, 457 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 458 | 0x00, 0x80, 0x00, 0x80, 0x00, 459 | }, 460 | []uint8{ 461 | 0x00, 0x40, 0x00, 0x40, 0x00, 462 | 0x60, 0xB0, 0xA0, 0xB0, 0x60, 463 | 0x00, 0x80, 0x00, 0x80, 0x00, 464 | }, 465 | }, 466 | { 467 | "mean (3x3 false)", 468 | 3, false, 469 | image.Rect(-1, -1, 4, 2), 470 | image.Rect(0, 0, 5, 3), 471 | []uint8{ 472 | 0x10, 0x40, 0x00, 0x40, 0x10, 473 | 0x20, 0x50, 0x00, 0x50, 0x20, 474 | 0x30, 0x60, 0x00, 0x60, 0x30, 475 | }, 476 | []uint8{ 477 | 0x25, 0x1E, 0x2E, 0x1E, 0x25, 478 | 0x30, 0x25, 0x35, 0x25, 0x30, 479 | 0x3B, 0x2C, 0x3C, 0x2C, 0x3B, 480 | }, 481 | }, 482 | { 483 | "mean (3x3 true)", 484 | 3, true, 485 | image.Rect(-1, -1, 4, 2), 486 | image.Rect(0, 0, 5, 3), 487 | []uint8{ 488 | 0x10, 0x40, 0x00, 0x40, 0x10, 489 | 0x20, 0x50, 0x00, 0x50, 0x20, 490 | 0x30, 0x60, 0x00, 0x60, 0x30, 491 | }, 492 | []uint8{ 493 | 0x1D, 0x2D, 0x1A, 0x2D, 0x1D, 494 | 0x2A, 0x36, 0x20, 0x36, 0x2A, 495 | 0x36, 0x40, 0x26, 0x40, 0x36, 496 | }, 497 | }, 498 | } 499 | 500 | for _, d := range testData { 501 | src := image.NewGray(d.srcb) 502 | src.Pix = d.srcPix 503 | 504 | f := Mean(d.ksize, d.disk) 505 | dst := image.NewGray(f.Bounds(src.Bounds())) 506 | f.Draw(dst, src, nil) 507 | 508 | if !checkBoundsAndPix(dst.Bounds(), d.dstb, dst.Pix, d.dstPix) { 509 | t.Errorf("test [%s] failed: %#v, %#v", d.desc, dst.Bounds(), dst.Pix) 510 | } 511 | } 512 | 513 | // check no panics 514 | Mean(5, false).Draw(image.NewGray(image.Rect(0, 0, 0, 0)), image.NewGray(image.Rect(0, 0, 0, 0)), nil) 515 | } 516 | 517 | func TestSobel(t *testing.T) { 518 | testData := []struct { 519 | desc string 520 | srcb, dstb image.Rectangle 521 | srcPix, dstPix []uint8 522 | }{ 523 | 524 | { 525 | "sobel 0x0", 526 | image.Rect(0, 0, 0, 0), 527 | image.Rect(0, 0, 0, 0), 528 | []uint8{}, 529 | []uint8{}, 530 | }, 531 | { 532 | "sobel 6x6", 533 | image.Rect(-1, -1, 5, 5), 534 | image.Rect(0, 0, 6, 6), 535 | []uint8{ 536 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 537 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 538 | 0x00, 0x00, 0x99, 0x99, 0x99, 0x99, 539 | 0x00, 0x00, 0x99, 0x99, 0x99, 0x99, 540 | 0x00, 0x00, 0x99, 0x99, 0x99, 0x99, 541 | 0x00, 0x00, 0x99, 0x99, 0x99, 0x99, 542 | }, 543 | []uint8{ 544 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 545 | 0x00, 0xd8, 0xff, 0xff, 0xff, 0xff, 546 | 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 547 | 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 548 | 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 549 | 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 550 | }, 551 | }, 552 | } 553 | 554 | for _, d := range testData { 555 | src := image.NewGray(d.srcb) 556 | src.Pix = d.srcPix 557 | 558 | f := Sobel() 559 | dst := image.NewGray(f.Bounds(src.Bounds())) 560 | f.Draw(dst, src, nil) 561 | 562 | if !checkBoundsAndPix(dst.Bounds(), d.dstb, dst.Pix, d.dstPix) { 563 | t.Errorf("test [%s] failed: %#v, %#v", d.desc, dst.Bounds(), dst.Pix) 564 | } 565 | } 566 | } 567 | -------------------------------------------------------------------------------- /pixels_test.go: -------------------------------------------------------------------------------- 1 | package gift 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/draw" 7 | "math" 8 | "testing" 9 | ) 10 | 11 | func TestNewPixelGetter(t *testing.T) { 12 | var img image.Image 13 | var pg *pixelGetter 14 | img = image.NewNRGBA(image.Rect(0, 0, 1, 1)) 15 | pg = newPixelGetter(img) 16 | if pg.it != itNRGBA || pg.nrgba == nil || !img.Bounds().Eq(pg.bounds) { 17 | t.Error("newPixelGetter NRGBA") 18 | } 19 | img = image.NewNRGBA64(image.Rect(0, 0, 1, 1)) 20 | pg = newPixelGetter(img) 21 | if pg.it != itNRGBA64 || pg.nrgba64 == nil || !img.Bounds().Eq(pg.bounds) { 22 | t.Error("newPixelGetter NRGBA64") 23 | } 24 | img = image.NewRGBA(image.Rect(0, 0, 1, 1)) 25 | pg = newPixelGetter(img) 26 | if pg.it != itRGBA || pg.rgba == nil || !img.Bounds().Eq(pg.bounds) { 27 | t.Error("newPixelGetter RGBA") 28 | } 29 | img = image.NewRGBA64(image.Rect(0, 0, 1, 1)) 30 | pg = newPixelGetter(img) 31 | if pg.it != itRGBA64 || pg.rgba64 == nil || !img.Bounds().Eq(pg.bounds) { 32 | t.Error("newPixelGetter RGBA64") 33 | } 34 | img = image.NewGray(image.Rect(0, 0, 1, 1)) 35 | pg = newPixelGetter(img) 36 | if pg.it != itGray || pg.gray == nil || !img.Bounds().Eq(pg.bounds) { 37 | t.Error("newPixelGetter Gray") 38 | } 39 | img = image.NewGray16(image.Rect(0, 0, 1, 1)) 40 | pg = newPixelGetter(img) 41 | if pg.it != itGray16 || pg.gray16 == nil || !img.Bounds().Eq(pg.bounds) { 42 | t.Error("newPixelGetter Gray16") 43 | } 44 | img = image.NewYCbCr(image.Rect(0, 0, 1, 1), image.YCbCrSubsampleRatio422) 45 | pg = newPixelGetter(img) 46 | if pg.it != itYCbCr || pg.ycbcr == nil || !img.Bounds().Eq(pg.bounds) { 47 | t.Error("newPixelGetter YCbCr") 48 | } 49 | img = image.NewUniform(color.NRGBA64{0, 0, 0, 0}) 50 | pg = newPixelGetter(img) 51 | if pg.it != itGeneric || pg.image == nil || !img.Bounds().Eq(pg.bounds) { 52 | t.Error("newPixelGetter Generic(Uniform)") 53 | } 54 | img = image.NewAlpha(image.Rect(0, 0, 1, 1)) 55 | pg = newPixelGetter(img) 56 | if pg.it != itGeneric || pg.image == nil || !img.Bounds().Eq(pg.bounds) { 57 | t.Error("newPixelGetter Generic(Alpha)") 58 | } 59 | } 60 | 61 | func comparePixels(px1, px2 pixel, dif float64) bool { 62 | if math.Abs(float64(px1.r)-float64(px2.r)) > dif { 63 | return false 64 | } 65 | if math.Abs(float64(px1.g)-float64(px2.g)) > dif { 66 | return false 67 | } 68 | if math.Abs(float64(px1.b)-float64(px2.b)) > dif { 69 | return false 70 | } 71 | if math.Abs(float64(px1.a)-float64(px2.a)) > dif { 72 | return false 73 | } 74 | return true 75 | 76 | } 77 | 78 | func compareColorsNRGBA(c1, c2 color.NRGBA, dif int) bool { 79 | if math.Abs(float64(c1.R)-float64(c2.R)) > float64(dif) { 80 | return false 81 | } 82 | if math.Abs(float64(c1.G)-float64(c2.G)) > float64(dif) { 83 | return false 84 | } 85 | if math.Abs(float64(c1.B)-float64(c2.B)) > float64(dif) { 86 | return false 87 | } 88 | if math.Abs(float64(c1.A)-float64(c2.A)) > float64(dif) { 89 | return false 90 | } 91 | return true 92 | } 93 | 94 | func TestGetPixel(t *testing.T) { 95 | var pg *pixelGetter 96 | 97 | // RGBA, NRGBA, RGBA64, NRGBA64 98 | 99 | palette := []color.Color{ 100 | color.NRGBA{0, 0, 0, 0}, 101 | color.NRGBA{255, 255, 255, 255}, 102 | color.NRGBA{50, 100, 150, 255}, 103 | color.NRGBA{150, 100, 50, 200}, 104 | } 105 | 106 | images1 := []draw.Image{ 107 | image.NewRGBA(image.Rect(-1, -2, 3, 4)), 108 | image.NewRGBA64(image.Rect(-1, -2, 3, 4)), 109 | image.NewNRGBA(image.Rect(-1, -2, 3, 4)), 110 | image.NewNRGBA64(image.Rect(-1, -2, 3, 4)), 111 | image.NewPaletted(image.Rect(-1, -2, 3, 4), palette), 112 | } 113 | 114 | colors1 := []struct { 115 | c color.NRGBA 116 | px pixel 117 | }{ 118 | {color.NRGBA{0, 0, 0, 0}, pixel{0, 0, 0, 0}}, 119 | {color.NRGBA{255, 255, 255, 255}, pixel{1, 1, 1, 1}}, 120 | {color.NRGBA{50, 100, 150, 255}, pixel{0.196, 0.392, 0.588, 1}}, 121 | {color.NRGBA{150, 100, 50, 200}, pixel{0.588, 0.392, 0.196, 0.784}}, 122 | } 123 | 124 | for _, img := range images1 { 125 | pg = newPixelGetter(img) 126 | for _, k := range colors1 { 127 | for _, x := range []int{-1, 0, 2} { 128 | for _, y := range []int{-2, 0, 3} { 129 | img.Set(x, y, k.c) 130 | px := pg.getPixel(x, y) 131 | if !comparePixels(k.px, px, 0.005) { 132 | t.Errorf("getPixel %T %v %dx%d %v %v", img, k.c, x, y, k.px, px) 133 | } 134 | } 135 | } 136 | } 137 | } 138 | 139 | // Uniform (Generic) 140 | 141 | for _, k := range colors1 { 142 | img := image.NewUniform(k.c) 143 | pg = newPixelGetter(img) 144 | for _, x := range []int{-1, 0, 2} { 145 | for _, y := range []int{-2, 0, 3} { 146 | px := pg.getPixel(x, y) 147 | if !comparePixels(k.px, px, 0.005) { 148 | t.Errorf("getPixel %T %v %dx%d %v %v", img, k.c, x, y, k.px, px) 149 | } 150 | } 151 | } 152 | } 153 | 154 | // YCbCr 155 | 156 | colors2 := []struct { 157 | c color.NRGBA 158 | px pixel 159 | }{ 160 | {color.NRGBA{0, 0, 0, 255}, pixel{0, 0, 0, 1}}, 161 | {color.NRGBA{255, 255, 255, 255}, pixel{1, 1, 1, 1}}, 162 | {color.NRGBA{50, 100, 150, 255}, pixel{0.196, 0.392, 0.588, 1}}, 163 | {color.NRGBA{150, 100, 50, 255}, pixel{0.588, 0.392, 0.196, 1}}, 164 | } 165 | 166 | for _, k := range colors2 { 167 | for _, sr := range []image.YCbCrSubsampleRatio{ 168 | image.YCbCrSubsampleRatio444, 169 | image.YCbCrSubsampleRatio422, 170 | image.YCbCrSubsampleRatio420, 171 | image.YCbCrSubsampleRatio440, 172 | image.YCbCrSubsampleRatio410, 173 | image.YCbCrSubsampleRatio411, 174 | } { 175 | img := image.NewYCbCr(image.Rect(-1, -2, 3, 4), sr) 176 | pg = newPixelGetter(img) 177 | for _, x := range []int{-1, 0, 2} { 178 | for _, y := range []int{-2, 0, 3} { 179 | iy := img.YOffset(x, y) 180 | ic := img.COffset(x, y) 181 | img.Y[iy], img.Cb[ic], img.Cr[ic] = color.RGBToYCbCr(k.c.R, k.c.G, k.c.B) 182 | px := pg.getPixel(x, y) 183 | if !comparePixels(k.px, px, 0.005) { 184 | t.Errorf("getPixel %T %v %dx%d %v %v", img, k.c, x, y, k.px, px) 185 | } 186 | } 187 | } 188 | } 189 | } 190 | 191 | // Gray, Gray16 192 | 193 | images2 := []draw.Image{ 194 | image.NewGray(image.Rect(-1, -2, 3, 4)), 195 | image.NewGray16(image.Rect(-1, -2, 3, 4)), 196 | } 197 | 198 | colors3 := []struct { 199 | c color.NRGBA 200 | px pixel 201 | }{ 202 | {color.NRGBA{0, 0, 0, 0}, pixel{0, 0, 0, 1}}, 203 | {color.NRGBA{255, 255, 255, 255}, pixel{1, 1, 1, 1}}, 204 | {color.NRGBA{50, 100, 150, 255}, pixel{0.356, 0.356, 0.356, 1}}, 205 | {color.NRGBA{150, 100, 50, 200}, pixel{0.337, 0.337, 0.337, 1}}, 206 | } 207 | 208 | for _, img := range images2 { 209 | pg = newPixelGetter(img) 210 | for _, k := range colors3 { 211 | for _, x := range []int{-1, 0, 2} { 212 | for _, y := range []int{-2, 0, 3} { 213 | img.Set(x, y, k.c) 214 | px := pg.getPixel(x, y) 215 | if !comparePixels(k.px, px, 0.005) { 216 | t.Errorf("getPixel %T %v %dx%d %v %v", img, k.c, x, y, k.px, px) 217 | } 218 | } 219 | } 220 | } 221 | } 222 | } 223 | 224 | func comparePixelSlices(s1, s2 []pixel, dif float64) bool { 225 | if len(s1) != len(s2) { 226 | return false 227 | } 228 | for i := 1; i < len(s1); i++ { 229 | if !comparePixels(s1[i], s2[i], dif) { 230 | return false 231 | } 232 | } 233 | return true 234 | } 235 | 236 | func TestGetPixelRow(t *testing.T) { 237 | colors := []color.NRGBA{ 238 | {0, 0, 0, 0}, 239 | {255, 255, 255, 255}, 240 | {50, 100, 150, 255}, 241 | {150, 100, 50, 200}, 242 | } 243 | pixels := []pixel{ 244 | {0, 0, 0, 0}, 245 | {1, 1, 1, 1}, 246 | {0.196, 0.392, 0.588, 1}, 247 | {0.588, 0.392, 0.196, 0.784}, 248 | } 249 | 250 | img := image.NewNRGBA(image.Rect(-1, -2, 3, 2)) 251 | pg := newPixelGetter(img) 252 | var row []pixel 253 | for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { 254 | for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { 255 | img.Set(x, y, colors[x-img.Bounds().Min.X]) 256 | } 257 | pg.getPixelRow(y, &row) 258 | if !comparePixelSlices(row, pixels, 0.005) { 259 | t.Errorf("getPixelRow y=%d %v %v", y, row, pixels) 260 | } 261 | } 262 | } 263 | 264 | func TestGetPixelColumn(t *testing.T) { 265 | colors := []color.NRGBA{ 266 | {0, 0, 0, 0}, 267 | {255, 255, 255, 255}, 268 | {50, 100, 150, 255}, 269 | {150, 100, 50, 200}, 270 | } 271 | pixels := []pixel{ 272 | {0, 0, 0, 0}, 273 | {1, 1, 1, 1}, 274 | {0.196, 0.392, 0.588, 1}, 275 | {0.588, 0.392, 0.196, 0.784}, 276 | } 277 | 278 | img := image.NewNRGBA(image.Rect(-1, -2, 3, 2)) 279 | pg := newPixelGetter(img) 280 | var column []pixel 281 | for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { 282 | for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { 283 | img.Set(x, y, colors[y-img.Bounds().Min.Y]) 284 | } 285 | pg.getPixelColumn(x, &column) 286 | if !comparePixelSlices(column, pixels, 0.005) { 287 | t.Errorf("getPixelColumn x=%d %v %v", x, column, pixels) 288 | } 289 | } 290 | } 291 | 292 | func TestF32u8(t *testing.T) { 293 | testData := []struct { 294 | x float32 295 | y uint8 296 | }{ 297 | {-1, 0}, 298 | {0, 0}, 299 | {100, 100}, 300 | {255, 255}, 301 | {256, 255}, 302 | } 303 | for _, p := range testData { 304 | v := f32u8(p.x) 305 | if v != p.y { 306 | t.Errorf("f32u8(%f) != %d: %d", p.x, p.y, v) 307 | } 308 | } 309 | } 310 | 311 | func TestF32u16(t *testing.T) { 312 | testData := []struct { 313 | x float32 314 | y uint16 315 | }{ 316 | {-1, 0}, 317 | {0, 0}, 318 | {1, 1}, 319 | {10000, 10000}, 320 | {65535, 65535}, 321 | {65536, 65535}, 322 | } 323 | for _, p := range testData { 324 | v := f32u16(p.x) 325 | if v != p.y { 326 | t.Errorf("f32u16(%f) != %d: %d", p.x, p.y, v) 327 | } 328 | } 329 | } 330 | 331 | func TestClampi32(t *testing.T) { 332 | testData := []struct { 333 | x int32 334 | y int32 335 | }{ 336 | {-1, 0}, 337 | {0, 0}, 338 | {1, 1}, 339 | {99, 99}, 340 | {100, 100}, 341 | {101, 100}, 342 | } 343 | for _, p := range testData { 344 | v := clampi32(p.x, 0, 100) 345 | if v != p.y { 346 | t.Errorf("clampi32(%d) != %d: %d", p.x, p.y, v) 347 | } 348 | } 349 | } 350 | 351 | func TestNewPixelSetter(t *testing.T) { 352 | var img draw.Image 353 | var pg *pixelSetter 354 | img = image.NewNRGBA(image.Rect(0, 0, 1, 1)) 355 | pg = newPixelSetter(img) 356 | if pg.it != itNRGBA || pg.nrgba == nil || !img.Bounds().Eq(pg.bounds) { 357 | t.Error("newPixelSetter NRGBA") 358 | } 359 | img = image.NewNRGBA64(image.Rect(0, 0, 1, 1)) 360 | pg = newPixelSetter(img) 361 | if pg.it != itNRGBA64 || pg.nrgba64 == nil || !img.Bounds().Eq(pg.bounds) { 362 | t.Error("newPixelSetter NRGBA64") 363 | } 364 | img = image.NewRGBA(image.Rect(0, 0, 1, 1)) 365 | pg = newPixelSetter(img) 366 | if pg.it != itRGBA || pg.rgba == nil || !img.Bounds().Eq(pg.bounds) { 367 | t.Error("newPixelSetter RGBA") 368 | } 369 | img = image.NewRGBA64(image.Rect(0, 0, 1, 1)) 370 | pg = newPixelSetter(img) 371 | if pg.it != itRGBA64 || pg.rgba64 == nil || !img.Bounds().Eq(pg.bounds) { 372 | t.Error("newPixelSetter RGBA64") 373 | } 374 | img = image.NewGray(image.Rect(0, 0, 1, 1)) 375 | pg = newPixelSetter(img) 376 | if pg.it != itGray || pg.gray == nil || !img.Bounds().Eq(pg.bounds) { 377 | t.Error("newPixelSetter Gray") 378 | } 379 | img = image.NewGray16(image.Rect(0, 0, 1, 1)) 380 | pg = newPixelSetter(img) 381 | if pg.it != itGray16 || pg.gray16 == nil || !img.Bounds().Eq(pg.bounds) { 382 | t.Error("newPixelSetter Gray16") 383 | } 384 | img = image.NewPaletted(image.Rect(0, 0, 1, 1), color.Palette{}) 385 | pg = newPixelSetter(img) 386 | if pg.it != itPaletted || pg.paletted == nil || !img.Bounds().Eq(pg.bounds) { 387 | t.Error("newPixelSetter Paletted") 388 | } 389 | img = image.NewAlpha(image.Rect(0, 0, 1, 1)) 390 | pg = newPixelSetter(img) 391 | if pg.it != itGeneric || pg.image == nil || !img.Bounds().Eq(pg.bounds) { 392 | t.Error("newPixelSetter Generic(Alpha)") 393 | } 394 | } 395 | 396 | func TestSetPixel(t *testing.T) { 397 | var ps *pixelSetter 398 | 399 | // RGBA, NRGBA, RGBA64, NRGBA64 400 | 401 | images1 := []draw.Image{ 402 | image.NewRGBA(image.Rect(-1, -2, 3, 4)), 403 | image.NewRGBA64(image.Rect(-1, -2, 3, 4)), 404 | image.NewNRGBA(image.Rect(-1, -2, 3, 4)), 405 | image.NewNRGBA64(image.Rect(-1, -2, 3, 4)), 406 | } 407 | 408 | colors1 := []struct { 409 | c color.NRGBA 410 | px pixel 411 | }{ 412 | {color.NRGBA{0, 0, 0, 0}, pixel{0, 0, 0, 0}}, 413 | {color.NRGBA{0, 0, 0, 255}, pixel{0, 0, 0, 1}}, 414 | {color.NRGBA{255, 255, 255, 255}, pixel{1, 1, 1, 1}}, 415 | {color.NRGBA{50, 100, 150, 255}, pixel{0.196, 0.392, 0.588, 1}}, 416 | {color.NRGBA{150, 100, 50, 200}, pixel{0.588, 0.392, 0.196, 0.784}}, 417 | } 418 | 419 | for _, img := range images1 { 420 | ps = newPixelSetter(img) 421 | for _, k := range colors1 { 422 | for _, x := range []int{-1, 0, 2} { 423 | for _, y := range []int{-2, 0, 3} { 424 | ps.setPixel(x, y, k.px) 425 | c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) 426 | if !compareColorsNRGBA(c, k.c, 1) { 427 | t.Errorf("setPixel %T %v %dx%d %v %v", img, k.px, x, y, k.c, c) 428 | } 429 | } 430 | } 431 | } 432 | } 433 | 434 | // Gray, Gray16 435 | 436 | images2 := []draw.Image{ 437 | image.NewGray(image.Rect(-1, -2, 3, 4)), 438 | image.NewGray16(image.Rect(-1, -2, 3, 4)), 439 | } 440 | 441 | colors2 := []struct { 442 | c color.NRGBA 443 | px pixel 444 | }{ 445 | {color.NRGBA{0, 0, 0, 255}, pixel{0, 0, 0, 1}}, 446 | {color.NRGBA{255, 255, 255, 255}, pixel{1, 1, 1, 1}}, 447 | {color.NRGBA{110, 110, 110, 255}, pixel{0.2, 0.5, 0.7, 1}}, 448 | {color.NRGBA{55, 55, 55, 255}, pixel{0.2, 0.5, 0.7, 0.5}}, 449 | } 450 | 451 | for _, img := range images2 { 452 | ps = newPixelSetter(img) 453 | for _, k := range colors2 { 454 | for _, x := range []int{-1, 0, 2} { 455 | for _, y := range []int{-2, 0, 3} { 456 | ps.setPixel(x, y, k.px) 457 | c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) 458 | if !compareColorsNRGBA(c, k.c, 1) { 459 | t.Errorf("setPixel %T %v %dx%d %v %v", img, k.px, x, y, k.c, c) 460 | } 461 | } 462 | } 463 | } 464 | } 465 | 466 | // Generic(Alpha) 467 | 468 | colors3 := []struct { 469 | c color.NRGBA 470 | px pixel 471 | }{ 472 | {color.NRGBA{255, 255, 255, 255}, pixel{0, 0, 0, 1}}, 473 | {color.NRGBA{255, 255, 255, 127}, pixel{0.2, 0.5, 0.7, 0.5}}, 474 | {color.NRGBA{255, 255, 255, 63}, pixel{0.1, 0.2, 0.3, 0.25}}, 475 | } 476 | 477 | img := image.NewAlpha(image.Rect(-1, -2, 3, 4)) 478 | ps = newPixelSetter(img) 479 | for _, k := range colors3 { 480 | for _, x := range []int{-1, 0, 2} { 481 | for _, y := range []int{-2, 0, 3} { 482 | ps.setPixel(x, y, k.px) 483 | c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) 484 | if !compareColorsNRGBA(c, k.c, 1) { 485 | t.Errorf("setPixel %T %v %dx%d %v %v", img, k.px, x, y, k.c, c) 486 | } 487 | } 488 | } 489 | } 490 | 491 | // Paletted 492 | 493 | images4 := []draw.Image{ 494 | image.NewPaletted( 495 | image.Rect(-1, -2, 3, 4), 496 | color.Palette{ 497 | color.NRGBA{0, 0, 0, 0}, 498 | color.NRGBA{0, 0, 0, 255}, 499 | color.NRGBA{255, 255, 255, 255}, 500 | color.NRGBA{50, 100, 150, 255}, 501 | color.NRGBA{150, 100, 50, 200}, 502 | color.NRGBA{1, 255, 255, 255}, 503 | color.NRGBA{2, 255, 255, 255}, 504 | color.NRGBA{3, 255, 255, 255}, 505 | }, 506 | ), 507 | } 508 | 509 | colors4 := []struct { 510 | c color.NRGBA 511 | px pixel 512 | }{ 513 | {color.NRGBA{0, 0, 0, 0}, pixel{0, 0, 0, 0}}, 514 | {color.NRGBA{0, 0, 0, 255}, pixel{0, 0, 0, 1}}, 515 | {color.NRGBA{255, 255, 255, 255}, pixel{1, 1, 1, 1}}, 516 | {color.NRGBA{50, 100, 150, 255}, pixel{0.196, 0.392, 0.588, 1}}, 517 | {color.NRGBA{150, 100, 50, 200}, pixel{0.588, 0.392, 0.196, 0.784}}, 518 | {color.NRGBA{0, 0, 0, 0}, pixel{0.1, 0.01, 0.001, 0.1}}, 519 | {color.NRGBA{0, 0, 0, 255}, pixel{0, 0, 0, 0.9}}, 520 | {color.NRGBA{255, 255, 255, 255}, pixel{1, 0.9, 1, 0.9}}, 521 | {color.NRGBA{1, 255, 255, 255}, pixel{0.001 / 255, 1, 1, 1}}, 522 | {color.NRGBA{1, 255, 255, 255}, pixel{1.001 / 255, 1, 1, 1}}, 523 | {color.NRGBA{2, 255, 255, 255}, pixel{2.001 / 255, 1, 1, 1}}, 524 | {color.NRGBA{3, 255, 255, 255}, pixel{3.001 / 255, 1, 1, 1}}, 525 | {color.NRGBA{3, 255, 255, 255}, pixel{4.001 / 255, 1, 1, 1}}, 526 | } 527 | 528 | for _, img := range images4 { 529 | ps = newPixelSetter(img) 530 | for _, k := range colors4 { 531 | for _, x := range []int{-1, 0, 2} { 532 | for _, y := range []int{-2, 0, 3} { 533 | ps.setPixel(x, y, k.px) 534 | c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) 535 | if !compareColorsNRGBA(c, k.c, 0) { 536 | t.Errorf("setPixel %T %v %dx%d %v %v", img, k.px, x, y, k.c, c) 537 | } 538 | } 539 | } 540 | } 541 | } 542 | 543 | } 544 | 545 | func TestSetPixelRow(t *testing.T) { 546 | colors := []color.NRGBA{ 547 | {0, 0, 0, 0}, 548 | {255, 255, 255, 255}, 549 | {50, 100, 150, 255}, 550 | {150, 100, 50, 200}, 551 | } 552 | pixels := []pixel{ 553 | {0, 0, 0, 0}, 554 | {1, 1, 1, 1}, 555 | {0.196, 0.392, 0.588, 1}, 556 | {0.588, 0.392, 0.196, 0.784}, 557 | } 558 | 559 | img := image.NewNRGBA(image.Rect(-1, -2, 3, 2)) 560 | ps := newPixelSetter(img) 561 | for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { 562 | ps.setPixelRow(y, pixels) 563 | for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { 564 | c := img.At(x, y).(color.NRGBA) 565 | wantedColor := colors[x-img.Bounds().Min.X] 566 | if !compareColorsNRGBA(wantedColor, c, 1) { 567 | t.Errorf("setPixelRow y=%d x=%d %v %v", y, x, wantedColor, c) 568 | } 569 | } 570 | } 571 | } 572 | 573 | func TestSetPixelColumn(t *testing.T) { 574 | colors := []color.NRGBA{ 575 | {0, 0, 0, 0}, 576 | {255, 255, 255, 255}, 577 | {50, 100, 150, 255}, 578 | {150, 100, 50, 200}, 579 | } 580 | pixels := []pixel{ 581 | {0, 0, 0, 0}, 582 | {1, 1, 1, 1}, 583 | {0.196, 0.392, 0.588, 1}, 584 | {0.588, 0.392, 0.196, 0.784}, 585 | } 586 | 587 | img := image.NewNRGBA(image.Rect(-1, -2, 3, 2)) 588 | ps := newPixelSetter(img) 589 | for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { 590 | ps.setPixelColumn(x, pixels) 591 | for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { 592 | c := img.At(x, y).(color.NRGBA) 593 | wantedColor := colors[y-img.Bounds().Min.Y] 594 | if !compareColorsNRGBA(wantedColor, c, 1) { 595 | t.Errorf("setPixelColumn x=%d y=%d %v %v", x, y, wantedColor, c) 596 | } 597 | } 598 | } 599 | } 600 | -------------------------------------------------------------------------------- /gift_test.go: -------------------------------------------------------------------------------- 1 | package gift 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/draw" 7 | _ "image/jpeg" 8 | _ "image/png" 9 | "os" 10 | "runtime" 11 | "testing" 12 | ) 13 | 14 | type testFilter struct { 15 | z int 16 | } 17 | 18 | func (p *testFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { 19 | dstBounds = image.Rect(0, 0, srcBounds.Dx()+p.z, srcBounds.Dy()+p.z*2) 20 | return 21 | } 22 | 23 | func (p *testFilter) Draw(dst draw.Image, src image.Image, options *Options) { 24 | dst.Set(dst.Bounds().Min.X, dst.Bounds().Min.Y, color.Gray{123}) 25 | } 26 | 27 | func TestGIFT(t *testing.T) { 28 | g := New() 29 | if g.Parallelization() != defaultOptions.Parallelization { 30 | t.Error("unexpected parallelization property") 31 | } 32 | g.SetParallelization(true) 33 | if !g.Parallelization() { 34 | t.Error("unexpected parallelization property") 35 | } 36 | g.SetParallelization(false) 37 | if g.Parallelization() { 38 | t.Error("unexpected parallelization property") 39 | } 40 | 41 | g = New( 42 | &testFilter{1}, 43 | &testFilter{2}, 44 | &testFilter{3}, 45 | ) 46 | if len(g.Filters) != 3 { 47 | t.Error("unexpected filters count") 48 | } 49 | 50 | g.Add( 51 | &testFilter{4}, 52 | &testFilter{5}, 53 | &testFilter{6}, 54 | ) 55 | if len(g.Filters) != 6 { 56 | t.Error("unexpected filters count") 57 | } 58 | b := g.Bounds(image.Rect(0, 0, 1, 2)) 59 | if !b.Eq(image.Rect(0, 0, 22, 44)) { 60 | t.Error("unexpected gift bounds") 61 | } 62 | 63 | g.Empty() 64 | if len(g.Filters) != 0 { 65 | t.Error("unexpected filters count") 66 | } 67 | b = g.Bounds(image.Rect(0, 0, 1, 2)) 68 | if !b.Eq(image.Rect(0, 0, 1, 2)) { 69 | t.Error("unexpected gift bounds") 70 | } 71 | 72 | g = &GIFT{} 73 | src := image.NewGray(image.Rect(-1, -1, 1, 1)) 74 | src.Pix = []uint8{ 75 | 1, 2, 76 | 3, 4, 77 | } 78 | dst := image.NewGray(g.Bounds(src.Bounds())) 79 | g.Draw(dst, src) 80 | if !dst.Bounds().Size().Eq(src.Bounds().Size()) { 81 | t.Error("unexpected dst bounds") 82 | } 83 | for i := range dst.Pix { 84 | if dst.Pix[i] != src.Pix[i] { 85 | t.Error("unexpected dst pix") 86 | } 87 | } 88 | 89 | g.Add(&testFilter{1}) 90 | g.Add(&testFilter{2}) 91 | dst = image.NewGray(g.Bounds(src.Bounds())) 92 | g.Draw(dst, src) 93 | if dst.Bounds().Dx() != src.Bounds().Dx()+3 || dst.Bounds().Dy() != src.Bounds().Dy()+6 { 94 | t.Error("unexpected dst bounds") 95 | } 96 | if dst.Pix[0] != 123 { 97 | t.Error("unexpected dst pix") 98 | } 99 | } 100 | 101 | func TestDrawAt(t *testing.T) { 102 | testDataGray := []struct { 103 | desc string 104 | filters []Filter 105 | pt image.Point 106 | op Operator 107 | srcb, dstb image.Rectangle 108 | srcPix, dstPix0, dstPix1 []uint8 109 | }{ 110 | { 111 | "draw at (Gray, [], -2, -2, copy)", 112 | []Filter{}, 113 | image.Pt(-2, -2), 114 | CopyOperator, 115 | image.Rect(-1, -1, 2, 2), 116 | image.Rect(-2, -2, 2, 2), 117 | []uint8{1, 2, 3, 4, 5, 6, 7, 8, 9}, 118 | []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 119 | []uint8{1, 2, 3, 0, 4, 5, 6, 0, 7, 8, 9, 0, 0, 0, 0, 0}, 120 | }, 121 | { 122 | "draw at (Gray, [], -1, -1, copy)", 123 | []Filter{}, 124 | image.Pt(-1, -1), 125 | CopyOperator, 126 | image.Rect(-1, -1, 2, 2), 127 | image.Rect(-2, -2, 2, 2), 128 | []uint8{1, 2, 3, 4, 5, 6, 7, 8, 9}, 129 | []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 130 | []uint8{0, 0, 0, 0, 0, 1, 2, 3, 0, 4, 5, 6, 0, 7, 8, 9}, 131 | }, 132 | { 133 | "draw at (Gray, [], 0, 0, copy)", 134 | []Filter{}, 135 | image.Pt(0, 0), 136 | CopyOperator, 137 | image.Rect(-1, -1, 2, 2), 138 | image.Rect(-2, -2, 2, 2), 139 | []uint8{1, 2, 3, 4, 5, 6, 7, 8, 9}, 140 | []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 141 | []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 4, 5}, 142 | }, 143 | { 144 | "draw at (Gray, [], 2, 2, copy)", 145 | []Filter{}, 146 | image.Pt(2, 2), 147 | CopyOperator, 148 | image.Rect(-1, -1, 2, 2), 149 | image.Rect(-2, -2, 2, 2), 150 | []uint8{1, 2, 3, 4, 5, 6, 7, 8, 9}, 151 | []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 152 | []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 153 | }, 154 | { 155 | "draw at (Gray, [], 0, -10, copy)", 156 | []Filter{}, 157 | image.Pt(0, -10), 158 | CopyOperator, 159 | image.Rect(-1, -1, 2, 2), 160 | image.Rect(-2, -2, 2, 2), 161 | []uint8{1, 2, 3, 4, 5, 6, 7, 8, 9}, 162 | []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 163 | []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 164 | }, 165 | { 166 | "draw at (Gray, [], -3, -3, copy)", 167 | []Filter{}, 168 | image.Pt(-3, -3), 169 | CopyOperator, 170 | image.Rect(-1, -1, 2, 2), 171 | image.Rect(-2, -2, 2, 2), 172 | []uint8{1, 2, 3, 4, 5, 6, 7, 8, 9}, 173 | []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 174 | []uint8{5, 6, 0, 0, 8, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 175 | }, 176 | { 177 | "draw at (Gray, [], -3, -3, over)", 178 | []Filter{}, 179 | image.Pt(-3, -3), 180 | OverOperator, 181 | image.Rect(-1, -1, 2, 2), 182 | image.Rect(-2, -2, 2, 2), 183 | []uint8{1, 2, 3, 4, 5, 6, 7, 8, 9}, 184 | []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 185 | []uint8{5, 6, 0, 0, 8, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 186 | }, 187 | { 188 | "draw at (Gray, [Resize], -2, -2, copy)", 189 | []Filter{Resize(6, 6, NearestNeighborResampling)}, 190 | image.Pt(-2, -2), 191 | CopyOperator, 192 | image.Rect(-1, -1, 2, 2), 193 | image.Rect(-2, -2, 2, 2), 194 | []uint8{1, 2, 3, 4, 5, 6, 7, 8, 9}, 195 | []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 196 | []uint8{1, 1, 2, 2, 1, 1, 2, 2, 4, 4, 5, 5, 4, 4, 5, 5}, 197 | }, 198 | { 199 | "draw at (Gray, [Resize], -3, -3, copy)", 200 | []Filter{Resize(6, 6, NearestNeighborResampling)}, 201 | image.Pt(-3, -3), 202 | CopyOperator, 203 | image.Rect(-1, -1, 2, 2), 204 | image.Rect(-2, -2, 2, 2), 205 | []uint8{1, 2, 3, 4, 5, 6, 7, 8, 9}, 206 | []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 207 | []uint8{1, 2, 2, 3, 4, 5, 5, 6, 4, 5, 5, 6, 7, 8, 8, 9}, 208 | }, 209 | { 210 | "draw at (Gray, [Resize], -1, -1, copy)", 211 | []Filter{Resize(6, 6, NearestNeighborResampling)}, 212 | image.Pt(-1, -1), 213 | CopyOperator, 214 | image.Rect(-1, -1, 2, 2), 215 | image.Rect(-2, -2, 2, 2), 216 | []uint8{1, 2, 3, 4, 5, 6, 7, 8, 9}, 217 | []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 218 | []uint8{0, 0, 0, 0, 0, 1, 1, 2, 0, 1, 1, 2, 0, 4, 4, 5}, 219 | }, 220 | { 221 | "draw at (Gray, [Resize], -1, -1, copy, empty)", 222 | []Filter{Resize(6, 6, NearestNeighborResampling)}, 223 | image.Pt(-1, -1), 224 | CopyOperator, 225 | image.Rect(0, 0, 0, 0), 226 | image.Rect(0, 0, 0, 0), 227 | []uint8{}, 228 | []uint8{}, 229 | []uint8{}, 230 | }, 231 | } 232 | 233 | for _, d := range testDataGray { 234 | src := image.NewGray(d.srcb) 235 | src.Pix = d.srcPix 236 | 237 | g := New(d.filters...) 238 | 239 | dst := image.NewGray(d.dstb) 240 | dst.Pix = d.dstPix0 241 | 242 | g.DrawAt(dst, src, d.pt, d.op) 243 | 244 | if !checkBoundsAndPix(dst.Bounds(), d.dstb, dst.Pix, d.dstPix1) { 245 | t.Errorf("test [%s] failed: %#v, %#v", d.desc, dst.Bounds(), dst.Pix) 246 | } 247 | } 248 | 249 | testDataNRGBA := []struct { 250 | desc string 251 | filters []Filter 252 | pt image.Point 253 | op Operator 254 | srcb, dstb image.Rectangle 255 | srcPix, dstPix0, dstPix1 []uint8 256 | }{ 257 | { 258 | "draw at (NRGBA, [], 1, 1, over, 0% 100% alpha)", 259 | []Filter{}, 260 | image.Pt(1, 1), 261 | OverOperator, 262 | image.Rect(0, 0, 2, 2), 263 | image.Rect(0, 0, 3, 3), 264 | []uint8{ 265 | 10, 20, 30, 255, 40, 50, 60, 255, 266 | 100, 200, 0, 255, 0, 250, 200, 255, 267 | }, 268 | []uint8{ 269 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 270 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 271 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 272 | }, 273 | []uint8{ 274 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 275 | 0, 0, 0, 0, 10, 20, 30, 255, 40, 50, 60, 255, 276 | 0, 0, 0, 0, 100, 200, 0, 255, 0, 250, 200, 255, 277 | }, 278 | }, 279 | { 280 | "draw at (NRGBA, [], 1, 1, over, 0% 50% alpha)", 281 | []Filter{}, 282 | image.Pt(1, 1), 283 | OverOperator, 284 | image.Rect(0, 0, 2, 2), 285 | image.Rect(0, 0, 3, 3), 286 | []uint8{ 287 | 10, 20, 30, 127, 40, 50, 60, 127, 288 | 100, 200, 0, 127, 0, 250, 200, 127, 289 | }, 290 | []uint8{ 291 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 292 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 293 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 294 | }, 295 | []uint8{ 296 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 297 | 0, 0, 0, 0, 10, 20, 30, 127, 40, 50, 60, 127, 298 | 0, 0, 0, 0, 100, 200, 0, 127, 0, 250, 200, 127, 299 | }, 300 | }, 301 | { 302 | "draw at (NRGBA, [], 1, 1, over, 100% 50% alpha)", 303 | []Filter{}, 304 | image.Pt(1, 1), 305 | OverOperator, 306 | image.Rect(0, 0, 2, 2), 307 | image.Rect(0, 0, 3, 3), 308 | []uint8{ 309 | 10, 20, 30, 128, 40, 50, 60, 128, 310 | 100, 200, 0, 128, 0, 250, 200, 128, 311 | }, 312 | []uint8{ 313 | 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 314 | 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 315 | 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 316 | }, 317 | []uint8{ 318 | 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 319 | 0, 0, 0, 255, 5, 10, 15, 255, 20, 25, 30, 255, 320 | 0, 0, 0, 255, 50, 100, 0, 255, 0, 125, 100, 255, 321 | }, 322 | }, 323 | { 324 | "draw at (NRGBA, [], 1, 1, over, 100% 25% alpha)", 325 | []Filter{}, 326 | image.Pt(1, 1), 327 | OverOperator, 328 | image.Rect(0, 0, 2, 2), 329 | image.Rect(0, 0, 3, 3), 330 | []uint8{ 331 | 20, 40, 80, 64, 40, 80, 120, 64, 332 | 100, 200, 0, 64, 0, 100, 200, 64, 333 | }, 334 | []uint8{ 335 | 0, 0, 0, 255, 1, 2, 3, 255, 0, 0, 0, 255, 336 | 0, 0, 0, 255, 40, 80, 120, 255, 40, 40, 40, 255, 337 | 0, 0, 0, 255, 200, 200, 12, 255, 0, 0, 0, 255, 338 | }, 339 | []uint8{ 340 | 0, 0, 0, 255, 1, 2, 3, 255, 0, 0, 0, 255, 341 | 0, 0, 0, 255, 35, 70, 110, 255, 40, 50, 60, 255, 342 | 0, 0, 0, 255, 175, 200, 9, 255, 0, 25, 50, 255, 343 | }, 344 | }, 345 | { 346 | "draw at (NRGBA, [], 1, 1, over, shape)", 347 | []Filter{}, 348 | image.Pt(1, 1), 349 | OverOperator, 350 | image.Rect(0, 0, 2, 2), 351 | image.Rect(0, 0, 3, 3), 352 | []uint8{ 353 | 100, 100, 100, 255, 100, 100, 100, 255, 354 | 100, 100, 100, 255, 100, 100, 100, 0, 355 | }, 356 | []uint8{ 357 | 10, 10, 10, 255, 10, 10, 10, 255, 10, 10, 10, 255, 358 | 10, 10, 10, 255, 10, 10, 10, 255, 10, 10, 10, 255, 359 | 10, 10, 10, 255, 10, 10, 10, 255, 10, 10, 10, 255, 360 | }, 361 | []uint8{ 362 | 10, 10, 10, 255, 10, 10, 10, 255, 10, 10, 10, 255, 363 | 10, 10, 10, 255, 100, 100, 100, 255, 100, 100, 100, 255, 364 | 10, 10, 10, 255, 100, 100, 100, 255, 10, 10, 10, 255, 365 | }, 366 | }, 367 | } 368 | 369 | for _, d := range testDataNRGBA { 370 | src := image.NewNRGBA(d.srcb) 371 | src.Pix = d.srcPix 372 | 373 | g := New(d.filters...) 374 | 375 | dst := image.NewNRGBA(d.dstb) 376 | dst.Pix = d.dstPix0 377 | 378 | g.DrawAt(dst, src, d.pt, d.op) 379 | 380 | if !checkBoundsAndPix(dst.Bounds(), d.dstb, dst.Pix, d.dstPix1) { 381 | t.Errorf("test [%s] failed: %#v, %#v", d.desc, dst.Bounds(), dst.Pix) 382 | } 383 | } 384 | 385 | } 386 | 387 | type fakeDrawImage struct { 388 | r image.Rectangle 389 | } 390 | 391 | func (p fakeDrawImage) Bounds() image.Rectangle { return p.r } 392 | func (p fakeDrawImage) At(x, y int) color.Color { return color.NRGBA{0, 0, 0, 0} } 393 | func (p fakeDrawImage) ColorModel() color.Model { return color.NRGBAModel } 394 | func (p fakeDrawImage) Set(x, y int, c color.Color) {} 395 | 396 | func TestSubImage(t *testing.T) { 397 | testData := []struct { 398 | desc string 399 | img draw.Image 400 | ok bool 401 | }{ 402 | { 403 | "sub image (Gray)", 404 | image.NewGray(image.Rect(0, 0, 10, 10)), 405 | true, 406 | }, 407 | { 408 | "sub image (Gray16)", 409 | image.NewGray16(image.Rect(0, 0, 10, 10)), 410 | true, 411 | }, 412 | { 413 | "sub image (RGBA)", 414 | image.NewRGBA(image.Rect(0, 0, 10, 10)), 415 | true, 416 | }, 417 | { 418 | "sub image (RGBA64)", 419 | image.NewRGBA64(image.Rect(0, 0, 10, 10)), 420 | true, 421 | }, 422 | { 423 | "sub image (NRGBA)", 424 | image.NewNRGBA(image.Rect(0, 0, 10, 10)), 425 | true, 426 | }, 427 | { 428 | "sub image (NRGBA64)", 429 | image.NewNRGBA64(image.Rect(0, 0, 10, 10)), 430 | true, 431 | }, 432 | { 433 | "sub image (fake)", 434 | fakeDrawImage{image.Rect(0, 0, 10, 10)}, 435 | false, 436 | }, 437 | } 438 | 439 | for _, d := range testData { 440 | simg, ok := getSubImage(d.img, image.Pt(3, 3)) 441 | if ok != d.ok { 442 | t.Errorf("test [%s] failed: expected %#v, got %#v", d.desc, d.ok, ok) 443 | } else if ok { 444 | simg.Set(5, 5, color.NRGBA{255, 255, 255, 255}) 445 | r, g, b, a := d.img.At(5, 5).RGBA() 446 | if r != 0xffff || g != 0xffff || b != 0xffff || a != 0xffff { 447 | t.Errorf("test [%s] failed: expected (0xffff, 0xffff, 0xffff, 0xffff), got (%d, %d, %d, %d)", d.desc, r, g, b, a) 448 | } 449 | } 450 | } 451 | 452 | } 453 | 454 | func TestDraw(t *testing.T) { 455 | filters := [][]Filter{ 456 | {}, 457 | {Resize(2, 2, NearestNeighborResampling), Crop(image.Rect(0, 0, 1, 1))}, 458 | {Resize(2, 2, NearestNeighborResampling), CropToSize(1, 1, CenterAnchor)}, 459 | {FlipHorizontal()}, 460 | {FlipVertical()}, 461 | {Resize(2, 2, NearestNeighborResampling), Resize(1, 1, NearestNeighborResampling)}, 462 | {Resize(2, 2, NearestNeighborResampling), ResizeToFit(1, 1, NearestNeighborResampling)}, 463 | {Resize(2, 2, NearestNeighborResampling), ResizeToFill(1, 1, NearestNeighborResampling, CenterAnchor)}, 464 | {Rotate(45, color.NRGBA{0, 0, 0, 0}, NearestNeighborInterpolation)}, 465 | {Rotate90()}, 466 | {Rotate180()}, 467 | {Rotate270()}, 468 | {Transpose()}, 469 | {Transverse()}, 470 | {Brightness(10)}, 471 | {ColorBalance(10, 10, 10)}, 472 | {ColorFunc(func(r0, g0, b0, a0 float32) (r, g, b, a float32) { return 1, 1, 1, 1 })}, 473 | {Colorize(240, 50, 100)}, 474 | {ColorspaceLinearToSRGB()}, 475 | {ColorspaceSRGBToLinear()}, 476 | {Contrast(10)}, 477 | {Convolution([]float32{-1, -1, 0, -1, 1, 1, 0, 1, 1}, false, false, false, 0)}, 478 | {Gamma(1.1)}, 479 | {GaussianBlur(3)}, 480 | {Grayscale()}, 481 | {Hue(90)}, 482 | {Invert()}, 483 | {Maximum(3, true)}, 484 | {Minimum(3, true)}, 485 | {Mean(3, true)}, 486 | {Median(3, true)}, 487 | {Pixelate(3)}, 488 | {Saturation(10)}, 489 | {Sepia(10)}, 490 | {Sigmoid(0.5, 5)}, 491 | {Sobel()}, 492 | {UnsharpMask(1, 1.5, 0.001)}, 493 | } 494 | 495 | for i, f := range filters { 496 | src := image.NewNRGBA(image.Rect(1, 1, 2, 2)) 497 | src.Pix = []uint8{255, 255, 255, 255} 498 | g := New(f...) 499 | dst := image.NewNRGBA(image.Rect(-100, -100, -95, -95)) 500 | g.Draw(dst, src) 501 | for x := dst.Bounds().Min.X; x < dst.Bounds().Max.X; x++ { 502 | for y := dst.Bounds().Min.Y; y < dst.Bounds().Max.Y; y++ { 503 | failed := false 504 | if x == -100 && y == -100 { 505 | if (color.NRGBAModel.Convert(dst.At(x, y)).(color.NRGBA) == color.NRGBA{0, 0, 0, 0}) { 506 | failed = true 507 | } 508 | } else { 509 | if (color.NRGBAModel.Convert(dst.At(x, y)).(color.NRGBA) != color.NRGBA{0, 0, 0, 0}) { 510 | failed = true 511 | } 512 | } 513 | if failed { 514 | t.Errorf("test draw pos failed: %d %#v %#v", i, f, dst.Pix) 515 | } 516 | } 517 | } 518 | } 519 | } 520 | 521 | func loadImage(t *testing.T, filename string) image.Image { 522 | f, err := os.Open(filename) 523 | if err != nil { 524 | t.Fatalf("os.Open (%q) failed: %v", filename, err) 525 | } 526 | img, _, err := image.Decode(f) 527 | if err != nil { 528 | t.Fatalf("image.Decode (%q) failed: %v", filename, err) 529 | } 530 | return img 531 | } 532 | 533 | func loadImageNRGBA(t *testing.T, filename string) *image.NRGBA { 534 | img := loadImage(t, filename) 535 | nrgba := image.NewNRGBA(img.Bounds()) 536 | New().Draw(nrgba, img) 537 | return nrgba 538 | } 539 | 540 | func TestGolden(t *testing.T) { 541 | filters := map[string]Filter{ 542 | "resize": Resize(100, 0, LanczosResampling), 543 | "crop_to_size": CropToSize(100, 100, LeftAnchor), 544 | "rotate_180": Rotate180(), 545 | "rotate_30": Rotate(30, color.Transparent, CubicInterpolation), 546 | "brightness_increase": Brightness(30), 547 | "brightness_decrease": Brightness(-30), 548 | "contrast_increase": Contrast(30), 549 | "contrast_decrease": Contrast(-30), 550 | "saturation_increase": Saturation(50), 551 | "saturation_decrease": Saturation(-50), 552 | "gamma_1.5": Gamma(1.5), 553 | "gamma_0.5": Gamma(0.5), 554 | "gaussian_blur": GaussianBlur(1), 555 | "unsharp_mask": UnsharpMask(1, 1, 0), 556 | "sigmoid": Sigmoid(0.5, 7), 557 | "pixelate": Pixelate(5), 558 | "colorize": Colorize(240, 50, 100), 559 | "grayscale": Grayscale(), 560 | "sepia": Sepia(100), 561 | "invert": Invert(), 562 | "mean": Mean(5, true), 563 | "median": Median(5, true), 564 | "minimum": Minimum(5, true), 565 | "maximum": Maximum(5, true), 566 | "hue_rotate": Hue(45), 567 | "color_balance": ColorBalance(10, -10, -10), 568 | "color_func": ColorFunc( 569 | func(r0, g0, b0, a0 float32) (r, g, b, a float32) { 570 | r = 1 - r0 571 | g = g0 + 0.1 572 | b = 0 573 | a = a0 574 | return r, g, b, a 575 | }, 576 | ), 577 | "convolution_emboss": Convolution( 578 | []float32{ 579 | -1, -1, 0, 580 | -1, 1, 1, 581 | 0, 1, 1, 582 | }, 583 | false, false, false, 0, 584 | ), 585 | } 586 | src := loadImage(t, "testdata/src.png") 587 | for name, filter := range filters { 588 | g := New(filter) 589 | dst := image.NewNRGBA(g.Bounds(src.Bounds())) 590 | g.Draw(dst, src) 591 | want := loadImageNRGBA(t, "testdata/dst_"+name+".png") 592 | if !goldenEqual(dst, want) { 593 | t.Errorf("resulting image differs from golden: %s", name) 594 | } 595 | } 596 | } 597 | 598 | // goldenEqual compares two NRGBA images. It is used in golden tests only. 599 | // All the golden images are generated on amd64 architecture. Due to differences 600 | // in floating-point rounding on different architectures, we need to add some 601 | // level of tolerance when comparing images on architectures other than amd64. 602 | // See https://golang.org/ref/spec#Floating_point_operators for information on 603 | // fused multiply and add (FMA) instruction. 604 | func goldenEqual(img1, img2 *image.NRGBA) bool { 605 | maxDiff := 0 606 | if runtime.GOARCH != "amd64" { 607 | maxDiff = 1 608 | } 609 | if !img1.Rect.Eq(img2.Rect) { 610 | return false 611 | } 612 | if len(img1.Pix) != len(img2.Pix) { 613 | return false 614 | } 615 | for i := 0; i < len(img1.Pix); i++ { 616 | diff := int(img1.Pix[i]) - int(img2.Pix[i]) 617 | if diff < 0 { 618 | diff = -diff 619 | } 620 | if diff > maxDiff { 621 | return false 622 | } 623 | } 624 | return true 625 | } 626 | 627 | func BenchmarkFilter(b *testing.B) { 628 | file, err := os.Open("testdata/src.jpg") 629 | if err != nil { 630 | b.Fatalf("failed to open test image: %v", err) 631 | } 632 | src, _, err := image.Decode(file) 633 | if err != nil { 634 | b.Fatalf("failed to decode test image: %v", err) 635 | } 636 | filters := []struct { 637 | name string 638 | filter Filter 639 | }{ 640 | {"Resize Lanczos", Resize(150, 0, LanczosResampling)}, 641 | {"Resize Cubic", Resize(150, 0, CubicResampling)}, 642 | {"Resize Linear", Resize(150, 0, LinearResampling)}, 643 | {"Resize Box", Resize(150, 0, BoxResampling)}, 644 | {"Resize Nearest", Resize(150, 0, NearestNeighborResampling)}, 645 | {"Crop", Crop(image.Rect(50, 50, 200, 200))}, 646 | {"CropToSize", CropToSize(150, 150, CenterAnchor)}, 647 | {"FlipHorizontal", FlipHorizontal()}, 648 | {"FlipVertical", FlipVertical()}, 649 | {"Transpose", Transpose()}, 650 | {"Transverse", Transverse()}, 651 | {"Rotate90", Rotate90()}, 652 | {"Rotate180", Rotate180()}, 653 | {"Rotate270", Rotate270()}, 654 | {"Rotate", Rotate(30, color.Transparent, CubicInterpolation)}, 655 | {"Brightness", Brightness(30)}, 656 | {"Contrast", Contrast(30)}, 657 | {"Saturation", Saturation(50)}, 658 | {"Gamma", Gamma(1.5)}, 659 | {"GaussianBlur", GaussianBlur(1)}, 660 | {"UnsharpMask", UnsharpMask(1, 1, 0)}, 661 | {"Sigmoid", Sigmoid(0.5, 7)}, 662 | {"Pixelate", Pixelate(5)}, 663 | {"Colorize", Colorize(240, 50, 100)}, 664 | {"ColorBalance", ColorBalance(10, -10, -10)}, 665 | {"Threshold", Threshold(50)}, 666 | {"Hue", Hue(45)}, 667 | {"Grayscale", Grayscale()}, 668 | {"Sepia", Sepia(100)}, 669 | {"Invert", Invert()}, 670 | {"ColorFunc", ColorFunc( 671 | func(r0, g0, b0, a0 float32) (r, g, b, a float32) { 672 | r = 1 - r0 673 | g = g0 + 0.1 674 | b = 0 675 | a = a0 676 | return r, g, b, a 677 | }, 678 | )}, 679 | {"ColorspaceSRGBToLinear", ColorspaceSRGBToLinear()}, 680 | {"ColorspaceLinearToSRGB", ColorspaceLinearToSRGB()}, 681 | {"Mean", Mean(5, true)}, 682 | {"Median", Median(5, true)}, 683 | {"Minimum", Minimum(5, true)}, 684 | {"Maximum", Maximum(5, true)}, 685 | {"Convolution", Convolution( 686 | []float32{ 687 | -1, -1, 0, 688 | -1, 1, 1, 689 | 0, 1, 1, 690 | }, 691 | false, false, false, 0, 692 | )}, 693 | {"Sobel", Sobel()}, 694 | } 695 | for _, f := range filters { 696 | b.Run(f.name, func(b *testing.B) { 697 | g := New(f.filter) 698 | dst := image.NewNRGBA(g.Bounds(src.Bounds())) 699 | b.ReportAllocs() 700 | b.ResetTimer() 701 | for i := 0; i < b.N; i++ { 702 | g.Draw(dst, src) 703 | } 704 | }) 705 | } 706 | } 707 | --------------------------------------------------------------------------------