├── testdata ├── cat.jpg ├── fry.gif └── grape.png ├── .golangci.yaml ├── go.mod ├── go.sum ├── .travis.yml ├── LICENSE.md ├── downsize_test.go ├── cmd └── downsize │ ├── main.go │ └── README.md ├── downsize.go └── README.md /testdata/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lelenanam/downsize/HEAD/testdata/cat.jpg -------------------------------------------------------------------------------- /testdata/fry.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lelenanam/downsize/HEAD/testdata/fry.gif -------------------------------------------------------------------------------- /testdata/grape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lelenanam/downsize/HEAD/testdata/grape.png -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - gofmt 4 | - megacheck 5 | - golint 6 | - misspell 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lelenanam/downsize 2 | 3 | go 1.13 4 | 5 | require github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= 2 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | go: 4 | - 1.x 5 | - master 6 | 7 | before_script: 8 | - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH) v1.21.0 9 | 10 | script: 11 | - env GO111MODULES=on $(go env GOPATH)/golangci-lint run 12 | - env GO111MODULES=on go test -v -race ./... 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Elena Morozova 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 | -------------------------------------------------------------------------------- /downsize_test.go: -------------------------------------------------------------------------------- 1 | package downsize 2 | 3 | import ( 4 | "bytes" 5 | "image" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func TestDownsize(t *testing.T) { 11 | table := []struct { 12 | img string 13 | maxSize int 14 | }{ 15 | { 16 | img: "./testdata/cat.jpg", 17 | maxSize: 20000, 18 | }, 19 | { 20 | img: "./testdata/grape.png", 21 | maxSize: 20000, 22 | }, 23 | { 24 | img: "./testdata/fry.gif", 25 | maxSize: 20000, 26 | }, 27 | } 28 | for _, test := range table { 29 | file, err := os.Open(test.img) 30 | if err != nil { 31 | t.Errorf("Error: %v, cannot open file %v\n", err, test.img) 32 | } 33 | defer func() { 34 | if err := file.Close(); err != nil { 35 | t.Errorf("Error: %v, cannot close file %v\n", err, test.img) 36 | } 37 | }() 38 | 39 | resBuf := bytes.NewBuffer(nil) 40 | img, format, err := image.Decode(file) 41 | if err != nil { 42 | t.Errorf("Error: %v, cannot decode file %v\n", err, test.img) 43 | } 44 | 45 | if err = Encode(resBuf, img, &Options{Size: test.maxSize, Format: format}); err != nil { 46 | t.Errorf("Error: %v, cannot downsize file %v\n", err, test.img) 47 | } 48 | resSize := resBuf.Len() 49 | resAccur := 1 - float64(resSize)/float64(test.maxSize) 50 | if resAccur > Accuracy { 51 | t.Errorf("[FAIL] For file: %v, size: %v, accuracy: %.4f, should be: %v\n", 52 | test.img, resSize, resAccur, Accuracy) 53 | } else { 54 | t.Logf("[OK] File: %v, size: %v, accuracy: %.4f\n", test.img, resSize, resAccur) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /cmd/downsize/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "image" 7 | _ "image/gif" 8 | "image/jpeg" 9 | _ "image/jpeg" 10 | _ "image/png" 11 | "log" 12 | "os" 13 | 14 | "github.com/lelenanam/downsize" 15 | ) 16 | 17 | // USAGE: downsize [-s=size] [-f=format] [-q=jpeg quality] [-i=infile] [-o=outfile] 18 | // USAGE: downsize [--help] 19 | 20 | func main() { 21 | flag.Usage = func() { 22 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 23 | fmt.Fprintf(os.Stderr, "%s [-s=size] [-f=format] [-q=jpeg quality] [-i=infile] [-o=outfile]\n", os.Args[0]) 24 | flag.PrintDefaults() 25 | } 26 | var size = flag.Int("s", 204800, "desired output file size in bytes") 27 | var format = flag.String("f", "", "format: jpeg, png or gif, by default the format of an image is determined during decoding") 28 | var quality = flag.Int("q", downsize.DefaultQuality, "desired output jpeg quality, ranges from 1 to 100 inclusive, higher is better") 29 | var infile = flag.String("i", "", "input file name, required") 30 | var outfile = flag.String("o", "", "output file name, required") 31 | flag.Parse() 32 | 33 | if *infile == "" || *outfile == "" { 34 | flag.Usage() 35 | return 36 | } 37 | 38 | file, err := os.Open(*infile) 39 | if err != nil { 40 | log.Fatalln("Cannot open input file: ", *infile, err) 41 | } 42 | defer func() { 43 | if err := file.Close(); err != nil { 44 | log.Println("Cannot close input file: ", *infile, err) 45 | } 46 | }() 47 | 48 | img, decodedFormat, err := image.Decode(file) 49 | if err != nil { 50 | log.Fatalln("Cannot decode file: ", file.Name(), err) 51 | } 52 | 53 | out, err := os.Create(*outfile) 54 | if err != nil { 55 | log.Fatalln("Cannot create output file: ", *outfile, err) 56 | } 57 | defer func() { 58 | if err := out.Close(); err != nil { 59 | log.Println("Cannot close output file: ", *outfile, err) 60 | } 61 | }() 62 | 63 | outFormat := *format 64 | if *format == "" { 65 | outFormat = decodedFormat 66 | log.Println("Output format:", decodedFormat) 67 | } 68 | if *format == "jpg" { 69 | outFormat = "jpeg" 70 | } 71 | 72 | opt := &downsize.Options{Size: *size, Format: outFormat} 73 | 74 | if *quality != 0 && outFormat == "jpeg" { 75 | opt.JpegOptions = &jpeg.Options{Quality: *quality} 76 | } 77 | 78 | if err = downsize.Encode(out, img, opt); err != nil { 79 | log.Fatalf("Cannot downsize image to size: %v, error: %q", opt.Size, err) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /cmd/downsize/README.md: -------------------------------------------------------------------------------- 1 | # downsize 2 | 3 | Reduces an image to a specified file size in bytes. 4 | 5 | # Installation 6 | 7 | ```bash 8 | $ go get -u github.com/lelenanam/downsize/... 9 | ``` 10 | 11 | # Usage 12 | 13 | You can specify the size in bytes and the format for the output file. For `jpeg` format, you can specify the quality. 14 | 15 | ``` 16 | Usage of downsize: 17 | downsize [-s=size] [-f=format] [-q=jpeg quality] [-i=infile] [-o=outfile] 18 | -f string 19 | format: jpeg, png or gif, by default the format of an image is determined during decoding 20 | -i string 21 | input file name, required 22 | -o string 23 | output file name, required 24 | -q int 25 | desired output jpeg quality, ranges from 1 to 100 inclusive, higher is better (default 80) 26 | -s int 27 | desired output file size in bytes (default 204800) 28 | ``` 29 | 30 | 31 | ## Example 32 | 33 | Resize the file `image.jpg` to size `1 MB` and save the result in `jpeg` format file `resized.jpg`. 34 | 35 | ```sh 36 | $ downsize -s=1048576 -f=jpeg image.jpg resized.jpg 37 | ``` 38 | 39 | ## Sample 1 40 | 41 | The original image `2.4 MB`: 42 | 43 | ![flower](https://cloud.githubusercontent.com/assets/4003503/24270582/f352a102-0fd2-11e7-852e-7ea77c4eae82.jpg) 44 | 45 | Downsize to `1 MB`, auto determine format for result image: 46 | 47 | ```sh 48 | $ downsize -s=1048576 flower.jpg flower1mb.jpg 49 | ``` 50 | 51 | Resized result: 52 | 53 | ![flower1mb](https://cloud.githubusercontent.com/assets/4003503/24625151/f6576e30-1862-11e7-89cd-aa6ebbc21e3f.jpg) 54 | 55 | Downsize to `200 KB`, `jpeg` format for result image: 56 | 57 | ```sh 58 | $ downsize -s=204800 -f=jpeg flower.jpg flower200kb.jpg 59 | ``` 60 | 61 | Resized result: 62 | 63 | ![flower200kb](https://cloud.githubusercontent.com/assets/4003503/24625184/120b66fe-1863-11e7-9cab-42af6bb2aa71.jpg) 64 | 65 | Downsize to `200 KB`, `png` format for result image: 66 | 67 | ```sh 68 | $ downsize -s=204800 -f=png flower.jpg flower200kb.png 69 | ``` 70 | 71 | Resized result: 72 | 73 | ![flower200kb](https://cloud.githubusercontent.com/assets/4003503/24625215/26a34bfe-1863-11e7-9d5f-3258a8aa71ce.png) 74 | 75 | 76 | ## Sample 2 77 | 78 | The original image `3.4 MB`: 79 | 80 | ![leaves](https://cloud.githubusercontent.com/assets/4003503/24270590/ffc8b070-0fd2-11e7-949f-3f76364ac252.jpg) 81 | 82 | Downsize to `200 KB`, auto determine format for result image, default quality: 83 | 84 | ```sh 85 | $ downsize -s=204800 leaves.jpg leaves200kb.jpg 86 | ``` 87 | 88 | Resized result: 89 | 90 | ![leaves200kb](https://cloud.githubusercontent.com/assets/4003503/24625297/690b42d0-1863-11e7-86f3-bb90358b009d.jpg) 91 | 92 | Downsize to `200 KB`, auto determine format for result image, quality `50`: 93 | 94 | ```sh 95 | $ downsize -s=204800 -q=50 leaves.jpg leaves200kbQ50.jpg 96 | ``` 97 | 98 | Resized result: 99 | 100 | ![leaves200kbq50](https://cloud.githubusercontent.com/assets/4003503/24625339/8c90db3e-1863-11e7-9a9d-227980e19464.jpg) 101 | 102 | Downsize to `100 KB`, auto determine format for result image: 103 | 104 | ```sh 105 | $ downsize -s=102400 leaves.jpg leaves100kb.jpg 106 | ``` 107 | 108 | Resized result: 109 | 110 | ![leaves100kb](https://cloud.githubusercontent.com/assets/4003503/24625357/9f83193c-1863-11e7-99c7-2cc912f5b723.jpg) -------------------------------------------------------------------------------- /downsize.go: -------------------------------------------------------------------------------- 1 | package downsize 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "image" 7 | "image/gif" 8 | "image/jpeg" 9 | "image/png" 10 | "io" 11 | "sync" 12 | 13 | "github.com/nfnt/resize" 14 | ) 15 | 16 | // Options are the encoding parameters. 17 | type Options struct { 18 | // Size is desired output file size in bytes 19 | Size int 20 | // Format is image format to encode 21 | Format string 22 | // JpegOptions are the options for jpeg format 23 | JpegOptions *jpeg.Options 24 | // GifOptions are the options for gif format 25 | GifOptions *gif.Options 26 | } 27 | 28 | // DefaultQuality is default quality to encode image 29 | const DefaultQuality = 80 30 | 31 | // defaultFormat is default output format 32 | var defaultFormat = "jpeg" 33 | 34 | // defaultJpegOptions is default options to encode jpeg format 35 | var defaultJpegOptions = &jpeg.Options{Quality: DefaultQuality} 36 | 37 | // defaultOptions is default options for downsize 38 | var defaultOptions = &Options{Format: defaultFormat, JpegOptions: defaultJpegOptions} 39 | 40 | //Accuracy for calculating specified file size Options.Size 41 | //for Options.Size result might be in range [Options.Size - Options.Size*Accuracy; Options.Size] 42 | const Accuracy = 0.05 43 | 44 | var bufPool = sync.Pool{ 45 | New: func() interface{} { 46 | return new(bytes.Buffer) 47 | }, 48 | } 49 | 50 | func setOptions(o *Options) *Options { 51 | opts := defaultOptions 52 | if o != nil { 53 | opts = o 54 | } 55 | if opts.Format == "" { 56 | opts.Format = defaultFormat 57 | if opts.GifOptions != nil { 58 | opts.Format = "gif" 59 | } 60 | } 61 | if opts.Format == defaultFormat && opts.JpegOptions == nil { 62 | opts.JpegOptions = defaultJpegOptions 63 | } 64 | return opts 65 | } 66 | 67 | // Encode changes size of Image img (result size<=o.Size) 68 | // and writes the Image img to w with the given options. 69 | // Default parameters are used if a nil *Options is passed. 70 | func Encode(w io.Writer, img image.Image, o *Options) error { 71 | buf := bufPool.Get().(*bytes.Buffer) 72 | buf.Reset() 73 | defer bufPool.Put(buf) 74 | 75 | opts := setOptions(o) 76 | 77 | if err := encode(buf, img, opts); err != nil { 78 | return err 79 | } 80 | originSize := buf.Len() 81 | 82 | if opts.Size <= 0 { 83 | opts.Size = originSize 84 | } 85 | 86 | if originSize <= opts.Size { 87 | _, err := io.Copy(w, buf) 88 | return err 89 | } 90 | 91 | min := 0 92 | max := img.Bounds().Dx() 93 | 94 | for min < max { 95 | buf.Reset() 96 | newWidth := (min + max) / 2 97 | newImg := resize.Resize(uint(newWidth), 0, img, resize.Lanczos3) 98 | if err := encode(buf, newImg, opts); err != nil { 99 | return err 100 | } 101 | newSize := buf.Len() 102 | if newSize > opts.Size { 103 | max = newWidth - 1 104 | } else { 105 | newAccur := 1 - float64(newSize)/float64(opts.Size) 106 | if newAccur <= Accuracy { 107 | break 108 | } 109 | min = newWidth + 1 110 | } 111 | } 112 | _, err := io.Copy(w, buf) 113 | return err 114 | } 115 | 116 | func encode(w io.Writer, img image.Image, o *Options) error { 117 | switch o.Format { 118 | case "jpeg": 119 | return jpeg.Encode(w, img, o.JpegOptions) 120 | case "png": 121 | return png.Encode(w, img) 122 | case "gif": 123 | return gif.Encode(w, img, o.GifOptions) 124 | default: 125 | return fmt.Errorf("Unknown image format %q", o.Format) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # downsize 2 | 3 | [![Build Status](https://travis-ci.org/lelenanam/downsize.svg?branch=master)](https://travis-ci.org/lelenanam/downsize) 4 | [![GoDoc](https://godoc.org/github.com/lelenanam/downsize?status.svg)](https://godoc.org/github.com/lelenanam/downsize) 5 | 6 | Reduces an image to a specified file size in bytes. 7 | Also [command line tool](https://github.com/lelenanam/downsize/tree/master/cmd/downsize) available. 8 | 9 | # Installation 10 | 11 | ```bash 12 | $ go get -u github.com/lelenanam/downsize 13 | ``` 14 | 15 | # Usage 16 | 17 | ```go 18 | import "github.com/lelenanam/downsize" 19 | ``` 20 | 21 | The `downsize` package provides a function `downsize.Encode`: 22 | 23 | ```go 24 | func Encode(w io.Writer, m image.Image, o *Options) error 25 | ``` 26 | 27 | This function: 28 | 29 | * takes any image type that implements `image.Image` interface as an input `m` 30 | * reduces an image's dimensions to achieve a specified file size `Options.Size` in bytes 31 | * writes result Image `m` to writer `w` with the given options 32 | * default parameters are used if a `nil` `*Options` is passed 33 | 34 | ```go 35 | // Options are the encoding parameters. 36 | type Options struct { 37 | // Size is desired output file size in bytes 38 | Size int 39 | // Format is image format to encode 40 | Format string 41 | // JpegOptions are the options for jpeg format 42 | JpegOptions *jpeg.Options 43 | // GifOptions are the options for gif format 44 | GifOptions *gif.Options 45 | } 46 | ``` 47 | 48 | By default an image encodes with `jpeg` format and with the quality `DefaultQuality = 80`. 49 | All metadata is stripped after encoding. 50 | 51 | ```go 52 | const DefaultQuality = 80 53 | var defaultFormat = "jpeg" 54 | var defaultJpegOptions = &jpeg.Options{Quality: DefaultQuality} 55 | var defaultOptions = &Options{Format: defaultFormat, JpegOptions: defaultJpegOptions} 56 | 57 | ``` 58 | 59 | # Example 60 | 61 | ```go 62 | package main 63 | 64 | import ( 65 | "image" 66 | _ "image/gif" 67 | _ "image/jpeg" 68 | _ "image/png" 69 | "log" 70 | "os" 71 | 72 | "github.com/lelenanam/downsize" 73 | ) 74 | 75 | func main() { 76 | file, err := os.Open("img.png") 77 | if err != nil { 78 | log.Fatal(err) 79 | } 80 | defer func() { 81 | if err := file.Close(); err != nil { 82 | log.Println("Cannot close input file: ", err) 83 | } 84 | }() 85 | 86 | img, format, err := image.Decode(file) 87 | if err != nil { 88 | log.Fatalf("Error: %v, cannot decode file %v", err, file.Name()) 89 | } 90 | 91 | out, err := os.Create("resized.png") 92 | if err != nil { 93 | log.Fatal(err) 94 | } 95 | defer func() { 96 | if err := out.Close(); err != nil { 97 | log.Println("Cannot close output file: ", err) 98 | } 99 | }() 100 | 101 | opt := &downsize.Options{Size: 1048576, Format: format} 102 | if err = downsize.Encode(out, img, opt); err != nil { 103 | log.Fatalf("Error: %v, cannot downsize image to size: %v", err, opt.Size) 104 | } 105 | } 106 | ``` 107 | 108 | # Sample 109 | 110 | The original jpeg image `2.4 MB`: 111 | 112 | ![flower](https://cloud.githubusercontent.com/assets/4003503/24624009/4fcd3962-185f-11e7-8b6b-a28e217cba27.jpg) 113 | 114 | Downsize to `200 KB`, `png` format and default quality for result image: 115 | 116 | ```go 117 | opt := &downsize.Options{Size: 204800, Format: "png"} 118 | err = downsize.Encode(out, img, opt) 119 | ``` 120 | 121 | Resized result `200 KB`: 122 | 123 | ![flower200kbpng](https://cloud.githubusercontent.com/assets/4003503/24624123/aa7f5f16-185f-11e7-9340-e896ee116bc3.png) 124 | 125 | Downsize to `200 KB`, `jpeg` format and default quality for result image: 126 | 127 | ```go 128 | opt := &downsize.Options{Size: 204800, Format: "jpeg"} 129 | err = downsize.Encode(out, img, opt) 130 | ``` 131 | 132 | Resized result `200 KB`: 133 | 134 | ![flower200kbjpegq80](https://cloud.githubusercontent.com/assets/4003503/24624188/de20d7b4-185f-11e7-931b-1b2eeb1ab0f0.jpg) 135 | 136 | Downsize to `200 KB`, `jpeg` format and quality `50` for result image: 137 | 138 | ```go 139 | opt := &downsize.Options{Size: 204800, Format: "jpeg", JpegOptions: &jpeg.Options{Quality: 50}} 140 | err = downsize.Encode(out, img, opt) 141 | ``` 142 | 143 | Resized result `200 KB`, quality `50`: 144 | 145 | ![flower200kbjpegq50](https://cloud.githubusercontent.com/assets/4003503/24624303/3edbcfbe-1860-11e7-947f-16954fd3a872.jpg) 146 | 147 | 148 | The original image `3.4 MB`: 149 | 150 | ![leaves](https://cloud.githubusercontent.com/assets/4003503/24270590/ffc8b070-0fd2-11e7-949f-3f76364ac252.jpg) 151 | 152 | Downsize to `100 KB`, auto determine format and default quality for result image: 153 | 154 | ```go 155 | opt := &downsize.Options{Size: 102400} 156 | err = downsize.Encode(out, img, opt) 157 | ``` 158 | 159 | Resized result `100 KB`: 160 | 161 | ![leaves100kb](https://cloud.githubusercontent.com/assets/4003503/24624461/c86e946e-1860-11e7-8059-c4bb25ad3c49.jpg) 162 | 163 | Downsize to `100 KB`, auto determine format and quality `50` for result image: 164 | 165 | ```go 166 | opt := &downsize.Options{Size: 102400, JpegOptions: &jpeg.Options{Quality: 50}} 167 | err = downsize.Encode(out, img, opt) 168 | ``` 169 | 170 | Resized result `100 KB`, quality `50`: 171 | 172 | ![leaves100kbjpegq50](https://cloud.githubusercontent.com/assets/4003503/24624590/38ccf520-1861-11e7-964e-7b3411a3fc11.jpg) 173 | 174 | Downsize to `50 KB`, auto determine format and default duality for result image: 175 | 176 | ```go 177 | opt := &downsize.Options{Size: 51200} 178 | err = downsize.Encode(out, img, opt) 179 | ``` 180 | 181 | Resized result `50 KB`: 182 | 183 | ![leaves50kbjpegq80](https://cloud.githubusercontent.com/assets/4003503/24624690/7b46c0ac-1861-11e7-93d0-b4c87b9765eb.jpg) 184 | 185 | # License 186 | 187 | [MIT License](LICENSE.md) 188 | --------------------------------------------------------------------------------