├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── benchmark_test.go ├── cutter.go ├── cutter └── main.go ├── cutter_test.go ├── example_test.go └── fixtures ├── dark.png └── gopher.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.0 5 | - 1.1 6 | - tip 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Olivier Amblet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cutter 2 | ====== 3 | 4 | A Go library to crop images. 5 | 6 | [![Build Status](https://travis-ci.org/oliamb/cutter.png?branch=master)](https://travis-ci.org/oliamb/cutter) 7 | [![GoDoc](https://godoc.org/github.com/oliamb/cutter?status.png)](https://godoc.org/github.com/oliamb/cutter) 8 | 9 | Cutter was initially developped to be able 10 | to crop image resized using github.com/nfnt/resize. 11 | 12 | Usage 13 | ----- 14 | 15 | Read the doc on https://godoc.org/github.com/oliamb/cutter 16 | 17 | Import package with 18 | 19 | ```go 20 | import "github.com/oliamb/cutter" 21 | ``` 22 | 23 | Package cutter provides a function to crop image. 24 | 25 | By default, the original image will be cropped at the 26 | given size from the top left corner. 27 | 28 | ```go 29 | croppedImg, err := cutter.Crop(img, cutter.Config{ 30 | Width: 250, 31 | Height: 500, 32 | }) 33 | ``` 34 | 35 | Most of the time, the cropped image will share some memory 36 | with the original, so it should be used read only. You must 37 | ask explicitely for a copy if nedded. 38 | 39 | ```go 40 | croppedImg, err := cutter.Crop(img, cutter.Config{ 41 | Width: 250, 42 | Height: 500, 43 | Options: cutter.Copy, 44 | }) 45 | ``` 46 | 47 | It is possible to specify the top left position: 48 | 49 | ```go 50 | croppedImg, err := cutter.Crop(img, cutter.Config{ 51 | Width: 250, 52 | Height: 500, 53 | Anchor: image.Point{100, 100}, 54 | Mode: cutter.TopLeft, // optional, default value 55 | }) 56 | ``` 57 | 58 | The Anchor property can represents the center of the cropped image 59 | instead of the top left corner: 60 | 61 | ```go 62 | croppedImg, err := cutter.Crop(img, cutter.Config{ 63 | Width: 250, 64 | Height: 500, 65 | Mode: cutter.Centered, 66 | }) 67 | ``` 68 | 69 | The default crop use the specified dimension, but it is possible 70 | to use Width and Heigth as a ratio instead. In this case, 71 | the resulting image will be as big as possible to fit the asked ratio 72 | from the anchor position. 73 | 74 | ```go 75 | croppedImg, err := cutter.Crop(baseImage, cutter.Config{ 76 | Width: 4, 77 | Height: 3, 78 | Mode: cutter.Centered, 79 | Options: cutter.Ratio&cutter.Copy, // Copy is useless here 80 | }) 81 | ``` 82 | 83 | About resize 84 | ------------ 85 | This lib only manage crop and won't resize image, but it works great in combination with [github.com/nfnt/resize](https://github.com/nfnt/resize) 86 | 87 | Contributing 88 | ------------ 89 | I'd love to see your contributions to Cutter. If you'd like to hack on it: 90 | 91 | - fork the project, 92 | - hack on it, 93 | - ensure tests pass, 94 | - make a pull request 95 | 96 | If you plan to modify the API, let's disscuss it first. 97 | 98 | Licensing 99 | --------- 100 | MIT License, Please see the file called LICENSE. 101 | 102 | Credits 103 | ------- 104 | Test Picture: Gopher picture from Heidi Schuyt, http://www.flickr.com/photos/hschuyt/7674222278/, 105 | © copyright Creative Commons(http://creativecommons.org/licenses/by-nc-sa/2.0/) 106 | 107 | Thanks to Urturn(http://www.urturn.com) for the time allocated to develop the library. 108 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package cutter 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | ) 7 | 8 | /* 9 | BenchmarkCrop is used to track the Crop with sharing memory. 10 | 11 | Result on my laptop: 2000000 948 ns/op 12 | */ 13 | func BenchmarkCrop(b *testing.B) { 14 | img := getImage() 15 | 16 | c := Config{ 17 | Width: 1000, 18 | Height: 1000, 19 | Mode: TopLeft, 20 | Anchor: image.Point{100, 100}, 21 | } 22 | b.ResetTimer() 23 | for i := 0; i < b.N; i++ { 24 | Crop(img, c) 25 | } 26 | } 27 | 28 | /* 29 | BenchmarkCropCopy is used to track the Crop with copy performance. 30 | 31 | Below are the actual result on my laptop given each 32 | optimization suggested by Nigel Tao: https://groups.google.com/forum/#!topic/golang-nuts/qxSpOOp1QOk 33 | 34 | 1. initial time on my Laptop: 10 210332414 ns/op 35 | 2. after inverting x and y in copy loop: 10 195377177 ns/op 36 | 3. after removing useless call to ColorModel().Convert(): 10 193589075 ns/op 37 | 4. after replacing the two 'pixel' loops by a call to draw.Draw 38 | to obtains the cropped image: 20 84960510 ns/op 39 | */ 40 | func BenchmarkCropCopy(b *testing.B) { 41 | img := getImage() 42 | 43 | c := Config{ 44 | Width: 1000, 45 | Height: 1000, 46 | Mode: TopLeft, 47 | Anchor: image.Point{100, 100}, 48 | Options: Copy, 49 | } 50 | b.ResetTimer() 51 | for i := 0; i < b.N; i++ { 52 | Crop(img, c) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /cutter.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package cutter provides a function to crop image. 3 | 4 | By default, the original image will be cropped at the 5 | given size from the top left corner. 6 | 7 | croppedImg, err := cutter.Crop(img, cutter.Config{ 8 | Width: 250, 9 | Height: 500, 10 | }) 11 | 12 | Most of the time, the cropped image will share some memory 13 | with the original, so it should be used read only. You must 14 | ask explicitely for a copy if nedded. 15 | 16 | croppedImg, err := cutter.Crop(img, cutter.Config{ 17 | Width: 250, 18 | Height: 500, 19 | Options: Copy, 20 | }) 21 | 22 | It is possible to specify the top left position: 23 | 24 | croppedImg, err := cutter.Crop(img, cutter.Config{ 25 | Width: 250, 26 | Height: 500, 27 | Anchor: image.Point{100, 100}, 28 | Mode: TopLeft, // optional, default value 29 | }) 30 | 31 | The Anchor property can represents the center of the cropped image 32 | instead of the top left corner: 33 | 34 | 35 | croppedImg, err := cutter.Crop(img, cutter.Config{ 36 | Width: 250, 37 | Height: 500, 38 | Mode: Centered, 39 | }) 40 | 41 | The default crop use the specified dimension, but it is possible 42 | to use Width and Heigth as a ratio instead. In this case, 43 | the resulting image will be as big as possible to fit the asked ratio 44 | from the anchor position. 45 | 46 | croppedImg, err := cutter.Crop(baseImage, cutter.Config{ 47 | Width: 4, 48 | Height: 3, 49 | Mode: Centered, 50 | Options: Ratio, 51 | }) 52 | */ 53 | package cutter 54 | 55 | import ( 56 | "image" 57 | "image/draw" 58 | ) 59 | 60 | // Config is used to defined 61 | // the way the crop should be realized. 62 | type Config struct { 63 | Width, Height int 64 | Anchor image.Point // The Anchor Point in the source image 65 | Mode AnchorMode // Which point in the resulting image the Anchor Point is referring to 66 | Options Option 67 | } 68 | 69 | // AnchorMode is an enumeration of the position an anchor can represent. 70 | type AnchorMode int 71 | 72 | const ( 73 | // TopLeft defines the Anchor Point 74 | // as the top left of the cropped picture. 75 | TopLeft AnchorMode = iota 76 | // Centered defines the Anchor Point 77 | // as the center of the cropped picture. 78 | Centered = iota 79 | ) 80 | 81 | // Option flags to modify the way the crop is done. 82 | type Option int 83 | 84 | const ( 85 | // Ratio flag is use when Width and Height 86 | // must be used to compute a ratio rather 87 | // than absolute size in pixels. 88 | Ratio Option = 1 << iota 89 | // Copy flag is used to enforce the function 90 | // to retrieve a copy of the selected pixels. 91 | // This disable the use of SubImage method 92 | // to compute the result. 93 | Copy = 1 << iota 94 | ) 95 | 96 | // An interface that is 97 | // image.Image + SubImage method. 98 | type subImageSupported interface { 99 | SubImage(r image.Rectangle) image.Image 100 | } 101 | 102 | // Crop retrieves an image that is a 103 | // cropped copy of the original img. 104 | // 105 | // The crop is made given the informations provided in config. 106 | func Crop(img image.Image, c Config) (image.Image, error) { 107 | maxBounds := c.maxBounds(img.Bounds()) 108 | size := c.computeSize(maxBounds, image.Point{c.Width, c.Height}) 109 | cr := c.computedCropArea(img.Bounds(), size) 110 | cr = img.Bounds().Intersect(cr) 111 | 112 | if c.Options&Copy == Copy { 113 | return cropWithCopy(img, cr) 114 | } 115 | if dImg, ok := img.(subImageSupported); ok { 116 | return dImg.SubImage(cr), nil 117 | } 118 | return cropWithCopy(img, cr) 119 | } 120 | 121 | func cropWithCopy(img image.Image, cr image.Rectangle) (image.Image, error) { 122 | result := image.NewRGBA(cr) 123 | draw.Draw(result, cr, img, cr.Min, draw.Src) 124 | return result, nil 125 | } 126 | 127 | func (c Config) maxBounds(bounds image.Rectangle) (r image.Rectangle) { 128 | if c.Mode == Centered { 129 | anchor := c.centeredMin(bounds) 130 | w := min(anchor.X-bounds.Min.X, bounds.Max.X-anchor.X) 131 | h := min(anchor.Y-bounds.Min.Y, bounds.Max.Y-anchor.Y) 132 | r = image.Rect(anchor.X-w, anchor.Y-h, anchor.X+w, anchor.Y+h) 133 | } else { 134 | r = image.Rect(c.Anchor.X, c.Anchor.Y, bounds.Max.X, bounds.Max.Y) 135 | } 136 | return 137 | } 138 | 139 | // computeSize retrieve the effective size of the cropped image. 140 | // It is defined by Height, Width, and Ratio option. 141 | func (c Config) computeSize(bounds image.Rectangle, ratio image.Point) (p image.Point) { 142 | if c.Options&Ratio == Ratio { 143 | // Ratio option is on, so we take the biggest size available that fit the given ratio. 144 | if float64(ratio.X)/float64(bounds.Dx()) > float64(ratio.Y)/float64(bounds.Dy()) { 145 | p = image.Point{bounds.Dx(), (bounds.Dx() / ratio.X) * ratio.Y} 146 | } else { 147 | p = image.Point{(bounds.Dy() / ratio.Y) * ratio.X, bounds.Dy()} 148 | } 149 | } else { 150 | p = image.Point{ratio.X, ratio.Y} 151 | } 152 | return 153 | } 154 | 155 | // computedCropArea retrieve the theorical crop area. 156 | // It is defined by Height, Width, Mode and 157 | func (c Config) computedCropArea(bounds image.Rectangle, size image.Point) (r image.Rectangle) { 158 | min := bounds.Min 159 | switch c.Mode { 160 | case Centered: 161 | rMin := c.centeredMin(bounds) 162 | r = image.Rect(rMin.X-size.X/2, rMin.Y-size.Y/2, rMin.X-size.X/2+size.X, rMin.Y-size.Y/2+size.Y) 163 | default: // TopLeft 164 | rMin := image.Point{min.X + c.Anchor.X, min.Y + c.Anchor.Y} 165 | r = image.Rect(rMin.X, rMin.Y, rMin.X+size.X, rMin.Y+size.Y) 166 | } 167 | return 168 | } 169 | 170 | func (c *Config) centeredMin(bounds image.Rectangle) (rMin image.Point) { 171 | if c.Anchor.X == 0 && c.Anchor.Y == 0 { 172 | rMin = image.Point{ 173 | X: bounds.Dx() / 2, 174 | Y: bounds.Dy() / 2, 175 | } 176 | } else { 177 | rMin = image.Point{ 178 | X: c.Anchor.X, 179 | Y: c.Anchor.Y, 180 | } 181 | } 182 | return 183 | } 184 | 185 | func min(a, b int) (r int) { 186 | if a < b { 187 | r = a 188 | } else { 189 | r = b 190 | } 191 | return 192 | } 193 | -------------------------------------------------------------------------------- /cutter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "image" 8 | "image/jpeg" 9 | "image/png" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | 14 | "github.com/oliamb/cutter" 15 | ) 16 | 17 | func main() { 18 | s := len(os.Args) 19 | if s < 3 { 20 | usage() 21 | return 22 | } 23 | fmt.Println("Args", os.Args) 24 | 25 | inPath := os.Args[s-2] 26 | fi, err := os.Open(inPath) 27 | if err != nil { 28 | log.Fatal("Cannot open input file '", inPath, "':", err) 29 | } 30 | 31 | outPath := os.Args[s-1] 32 | fo, err := os.Create(outPath) 33 | if err != nil { 34 | log.Fatal("Cannot create output file '", outPath, "':", err) 35 | } 36 | 37 | img, _, err := image.Decode(fi) 38 | if err != nil { 39 | log.Fatal("Cannot decode image at '", inPath, "':", err) 40 | } 41 | 42 | cImg, err := cutter.Crop(img, cutter.Config{ 43 | Height: 1000, // height in pixel or Y ratio(see Ratio Option below) 44 | Width: 1000, // width in pixel or X ratio 45 | Mode: cutter.TopLeft, // Accepted Mode: TopLeft, Centered 46 | Anchor: image.Point{100, 100}, // Position of the top left point 47 | Options: 0, // Accepted Option: Ratio 48 | }) 49 | if err != nil { 50 | log.Fatal("Cannot crop image:", err) 51 | } 52 | 53 | switch filepath.Ext(outPath) { 54 | case ".png": 55 | err = png.Encode(fo, cImg) 56 | case ".jpg": 57 | err = jpeg.Encode(fo, cImg, &jpeg.Options{}) 58 | default: 59 | err = errors.New("Unsupported format: " + filepath.Ext(outPath)) 60 | } 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | fmt.Println("Image saved to", outPath) 65 | } 66 | 67 | func usage() { 68 | flag.Usage() 69 | } 70 | -------------------------------------------------------------------------------- /cutter_test.go: -------------------------------------------------------------------------------- 1 | package cutter 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | ) 7 | 8 | func TestCrop(t *testing.T) { 9 | img := getImage() 10 | 11 | c := Config{ 12 | Width: 512, 13 | Height: 400, 14 | } 15 | r, err := Crop(img, c) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | if r.Bounds().Dx() != 512 { 20 | t.Error("Bad width should be 512 but is", r.Bounds().Dx()) 21 | } 22 | if r.Bounds().Dy() != 400 { 23 | t.Error("Bad width should be 400 but is", r.Bounds().Dy()) 24 | } 25 | if r.Bounds().Min.X != 0 { 26 | t.Error("Invalid Bounds Min X", r.Bounds().Min.X) 27 | } 28 | if r.Bounds().Min.Y != 0 { 29 | t.Error("Invalid Bounds Min Y", r.Bounds().Min.Y) 30 | } 31 | } 32 | 33 | func TestCrop_Centered(t *testing.T) { 34 | img := getImage() 35 | 36 | c := Config{ 37 | Width: 512, 38 | Height: 400, 39 | Mode: Centered, 40 | } 41 | r, err := Crop(img, c) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | if r.Bounds().Dx() != 512 { 46 | t.Error("Bad width should be 512 but is", r.Bounds().Dx()) 47 | } 48 | if r.Bounds().Dy() != 400 { 49 | t.Error("Bad width should be 512 but is", r.Bounds().Dy()) 50 | } 51 | if r.Bounds().Min.X != 544 { 52 | t.Error("Invalid Bounds Min X", r.Bounds().Min.X) 53 | } 54 | if r.Bounds().Min.Y != 518 { 55 | t.Error("Invalid Bounds Min Y", r.Bounds().Min.Y) 56 | } 57 | } 58 | 59 | func TestCrop_Centered_Ratio_WithoutAnchorPosition(t *testing.T) { 60 | // (0,0)-(64,64) 32 64 (32,32)-(32,32) 61 | img := image.NewGray(image.Rect(0, 0, 64, 64)) 62 | c := Config{ 63 | Width: 32, 64 | Height: 64, 65 | Mode: Centered, 66 | Options: Ratio, 67 | } 68 | r, err := Crop(img, c) 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | if r.Bounds().Dx() != 32 { 73 | t.Error("Bad Width", r.Bounds().Dx()) 74 | } 75 | if r.Bounds().Dy() != 64 { 76 | t.Error("Bad Height", r.Bounds().Dy()) 77 | } 78 | if r.Bounds().Min.X != 16 { 79 | t.Error("Invalid Bounds Min X", r.Bounds().Min.X) 80 | } 81 | if r.Bounds().Min.Y != 0 { 82 | t.Error("Invalid Bounds Min Y", r.Bounds().Min.Y) 83 | } 84 | } 85 | 86 | func TestCrop_OddCenteredSource(t *testing.T) { 87 | img := image.NewRGBA(image.Rect(0, 0, 10, 5)) 88 | point := image.Point{X: 4, Y: 2} 89 | r, err := Crop(img, Config{ 90 | Width: 3, Height: 3, 91 | Anchor: point, 92 | Mode: Centered, 93 | }) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | if r.Bounds().Dx() != 3 { 98 | t.Error("Bad width should be 3 but is", r.Bounds().Dx()) 99 | } 100 | if r.Bounds().Dy() != 3 { 101 | t.Error("Bad width should be 3 but is", r.Bounds().Dy()) 102 | } 103 | if r.Bounds().Min.X != 3 { 104 | t.Error("Invalid Bounds Min X", r.Bounds().Min.X) 105 | } 106 | if r.Bounds().Min.Y != 1 { 107 | t.Error("Invalid Bounds Min Y", r.Bounds().Min.Y) 108 | } 109 | } 110 | 111 | func TestCutter_Crop_TooBigArea(t *testing.T) { 112 | img := getImage() 113 | 114 | c := Config{ 115 | Width: 2000, 116 | Height: 2000, 117 | Anchor: image.Point{100, 100}, 118 | } 119 | r, err := Crop(img, c) 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | if r.Bounds().Dx() != 1500 { 124 | t.Error("Bad width should be 1500 but is", r.Bounds().Dx()) 125 | } 126 | if r.Bounds().Dy() != 1337 { 127 | t.Error("Bad width should be 1337 but is", r.Bounds().Dy()) 128 | } 129 | if r.Bounds().Min.X != 100 { 130 | t.Error("Invalid Bounds Min X", r.Bounds().Min.X) 131 | } 132 | if r.Bounds().Min.Y != 100 { 133 | t.Error("Invalid Bounds Min Y", r.Bounds().Min.Y) 134 | } 135 | } 136 | 137 | func TestCrop_TooBigAreaFromCenter(t *testing.T) { 138 | img := getImage() 139 | 140 | c := Config{ 141 | Width: 1000, 142 | Height: 2000, 143 | Anchor: image.Point{1200, 100}, 144 | Mode: Centered, 145 | } 146 | r, err := Crop(img, c) 147 | if err != nil { 148 | t.Fatal(err) 149 | } 150 | if r.Bounds().Dx() != 900 { 151 | t.Error("Bad width should be 900 but is", r.Bounds().Dx()) 152 | } 153 | if r.Bounds().Dy() != 1100 { 154 | t.Error("Bad width should be 1100 but is", r.Bounds().Dy(), r.Bounds()) 155 | } 156 | if r.Bounds().Min.X != 700 { 157 | t.Error("Invalid Bounds Min X", r.Bounds().Min.X) 158 | } 159 | if r.Bounds().Min.Y != 0 { 160 | t.Error("Invalid Bounds Min Y", r.Bounds().Min.Y) 161 | } 162 | } 163 | 164 | func TestCrop_OptionRatio(t *testing.T) { 165 | img := getImage() 166 | 167 | c := Config{ 168 | Width: 4, 169 | Height: 3, 170 | Anchor: image.Point{}, 171 | Mode: TopLeft, 172 | Options: Ratio, 173 | } 174 | 175 | r, err := Crop(img, c) 176 | if err != nil { 177 | t.Error(err) 178 | } 179 | if r.Bounds().Min.X != 0 { 180 | t.Error("Invalid Bounds Min X", r.Bounds().Min.X) 181 | } 182 | if r.Bounds().Min.Y != 0 { 183 | t.Error("Invalid Bounds Min Y", r.Bounds().Min.Y) 184 | } 185 | if r.Bounds().Dx() != 1600 { 186 | t.Error("Bad Width", r.Bounds().Dx()) 187 | } 188 | if r.Bounds().Dy() != 1200 { 189 | t.Error("Bad Height", r.Bounds().Dy(), r.Bounds()) 190 | } 191 | } 192 | 193 | func TestCutter_Crop_OptionRatio_Inverted(t *testing.T) { 194 | img := getImage() 195 | 196 | c := Config{ 197 | Width: 3, 198 | Height: 4, 199 | Anchor: image.Point{}, 200 | Mode: TopLeft, 201 | Options: Ratio, 202 | } 203 | 204 | r, err := Crop(img, c) 205 | if err != nil { 206 | t.Error(err) 207 | } 208 | if r.Bounds().Min.X != 0 { 209 | t.Error("Invalid Bounds Min X", r.Bounds().Min.X) 210 | } 211 | if r.Bounds().Min.Y != 0 { 212 | t.Error("Invalid Bounds Min Y", r.Bounds().Min.Y) 213 | } 214 | if r.Bounds().Dy() != 1437 { 215 | t.Error("Bad Height", r.Bounds().Dy(), r.Bounds()) 216 | } 217 | if r.Bounds().Dx() != 1077 { 218 | t.Error("Bad Width", r.Bounds().Dx()) 219 | } 220 | } 221 | 222 | func TestCutter_Crop_OptionRatio_DecentredAnchor_Overflow(t *testing.T) { 223 | img := getImage() 224 | c := Config{ 225 | Width: 3, 226 | Height: 4, 227 | Anchor: image.Point{100, 80}, 228 | Mode: Centered, 229 | Options: Ratio, 230 | } 231 | 232 | r, err := Crop(img, c) 233 | if err != nil { 234 | t.Error(err) 235 | } 236 | if r.Bounds().Min.X != 40 { 237 | t.Error("Invalid Bounds Min X", r.Bounds().Min.X) 238 | } 239 | if r.Bounds().Min.Y != 0 { 240 | t.Error("Invalid Bounds Min Y", r.Bounds().Min.Y) 241 | } 242 | if r.Bounds().Dy() != 160 { 243 | t.Error("Bad Height", r.Bounds().Dy(), r.Bounds()) 244 | } 245 | if r.Bounds().Dx() != 120 { 246 | t.Error("Bad Width", r.Bounds().Dx()) 247 | } 248 | } 249 | 250 | func TestCropForceCopy(t *testing.T) { 251 | img := getImage() 252 | 253 | c := Config{ 254 | Width: 512, 255 | Height: 400, 256 | Options: Copy, 257 | } 258 | r, err := Crop(img, c) 259 | if err != nil { 260 | t.Fatal(err) 261 | } 262 | if r.Bounds().Dx() != 512 { 263 | t.Error("Bad width should be 512 but is", r.Bounds().Dx()) 264 | } 265 | if r.Bounds().Dy() != 400 { 266 | t.Error("Bad width should be 400 but is", r.Bounds().Dy()) 267 | } 268 | if r.Bounds().Min.X != 0 { 269 | t.Error("Invalid Bounds Min X", r.Bounds().Min.X) 270 | } 271 | if r.Bounds().Min.Y != 0 { 272 | t.Error("Invalid Bounds Min Y", r.Bounds().Min.Y) 273 | } 274 | } 275 | 276 | func getImage() image.Image { 277 | return image.NewGray(image.Rect(0, 0, 1600, 1437)) 278 | } 279 | 280 | func getOddImage() image.Image { 281 | return image.NewGray(image.Rect(0, 0, 999, 999)) 282 | } 283 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package cutter 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | _ "image/jpeg" 7 | _ "image/png" 8 | "log" 9 | "os" 10 | _ "testing" 11 | ) 12 | 13 | func ExampleCrop() { 14 | f, err := os.Open("fixtures/gopher.jpg") 15 | if err != nil { 16 | log.Fatal("Cannot open file", err) 17 | } 18 | img, _, err := image.Decode(f) 19 | if err != nil { 20 | log.Fatal("Cannot decode image:", err) 21 | } 22 | 23 | cImg, err := Crop(img, Config{ 24 | Height: 500, // height in pixel or Y ratio(see Ratio Option below) 25 | Width: 500, // width in pixel or X ratio 26 | Mode: TopLeft, // Accepted Mode: TopLeft, Centered 27 | Anchor: image.Point{10, 10}, // Position of the top left point 28 | Options: 0, // Accepted Option: Ratio 29 | }) 30 | 31 | if err != nil { 32 | log.Fatal("Cannot crop image:", err) 33 | } 34 | fmt.Println("cImg dimension:", cImg.Bounds()) 35 | // Output: cImg dimension: (10,10)-(510,510) 36 | } 37 | 38 | func ExampleCropPng() { 39 | f, err := os.Open("fixtures/dark.png") 40 | if err != nil { 41 | log.Fatal("Cannot open file", err) 42 | } 43 | img, _, err := image.Decode(f) 44 | if err != nil { 45 | log.Fatal("Cannot decode image:", err) 46 | } 47 | 48 | cImg, err := Crop(img, Config{ 49 | Height: 500, // height in pixel or Y ratio(see Ratio Option below) 50 | Width: 500, // width in pixel or X ratio 51 | Mode: TopLeft, // Accepted Mode: TopLeft, Centered 52 | Anchor: image.Point{10, 10}, // Position of the top left point 53 | Options: 0, // Accepted Option: Ratio 54 | }) 55 | 56 | if err != nil { 57 | log.Fatal("Cannot crop image:", err) 58 | } 59 | // The image is smaller than the Crop call 60 | fmt.Println("cImg dimension:", cImg.Bounds()) 61 | // Output: cImg dimension: (10,10)-(312,288) 62 | } 63 | -------------------------------------------------------------------------------- /fixtures/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oliamb/cutter/8d58efb5046ae961797a076be851b73182558d05/fixtures/dark.png -------------------------------------------------------------------------------- /fixtures/gopher.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oliamb/cutter/8d58efb5046ae961797a076be851b73182558d05/fixtures/gopher.jpg --------------------------------------------------------------------------------