├── .circleci └── config.yml ├── .gitignore ├── .goreleaser.yml ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── README.md ├── bin ├── release ├── setup └── test ├── client ├── client.go ├── client_suite_test.go ├── client_test.go └── clientfakes │ └── fake_client_interface.go ├── cmd ├── cmd_suite_test.go ├── root.go ├── root_test.go └── zip2png.go ├── composite ├── composite.go ├── composite_suite_test.go ├── composite_test.go └── compositefakes │ └── fake_compositor_interface.go ├── fixtures ├── background.jpg ├── nested │ └── plant.png ├── nomatch.txt ├── person-in-field.jpg └── zip │ ├── example-cat.zip │ ├── example-missing-alpha.zip │ ├── example-missing-color.zip │ └── reference-example-cat.png ├── integration_suite_test.go ├── integration_test.go ├── main.go ├── processor ├── determine_output_path.go ├── determine_output_path_test.go ├── notifier.go ├── notifier_test.go ├── processor.go ├── processor_suite_test.go ├── processor_test.go ├── processorfakes │ ├── fake_notifier_interface.go │ └── fake_prompt_interface.go └── prompt.go └── storage ├── storage.go ├── storage_suite_test.go ├── storage_test.go └── storagefakes └── fake_storage_interface.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | build: 5 | docker: 6 | - image: circleci/golang:1.14 7 | working_directory: /go/src/github.com/remove-bg/go 8 | steps: 9 | - checkout 10 | - run: echo 'export PATH=$GOPATH/bin:$PATH' >> "$BASH_ENV" 11 | - restore_cache: 12 | keys: 13 | - vendor-{{ checksum "Gopkg.lock" }} 14 | - run: bin/setup 15 | - run: bin/test 16 | - save_cache: 17 | key: vendor-{{ checksum "Gopkg.lock" }} 18 | paths: 19 | - vendor 20 | 21 | workflows: 22 | version: 2 23 | build: 24 | jobs: 25 | - build 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | *.coverprofile 3 | .idea 4 | tmp 5 | removebg 6 | dist -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - dep ensure 4 | 5 | project_name: removebg 6 | 7 | builds: 8 | - 9 | env: 10 | - CGO_ENABLED=0 11 | goos: 12 | - darwin 13 | - linux 14 | - windows 15 | 16 | archives: 17 | - 18 | replacements: 19 | darwin: Darwin 20 | linux: Linux 21 | windows: Windows 22 | 386: i386 23 | amd64: x86_64 24 | 25 | checksum: 26 | name_template: 'checksums.txt' 27 | 28 | snapshot: 29 | name_template: "{{ .Tag }}-next" 30 | 31 | changelog: 32 | skip: true 33 | 34 | brews: 35 | - 36 | name: removebg 37 | github: 38 | owner: remove-bg 39 | name: homebrew-tap 40 | homepage: "https://www.remove.bg/" 41 | description: "Remove image background - 100% automatically" 42 | 43 | nfpms: 44 | - 45 | vendor: Kaleido AI GmbH 46 | homepage: "https://www.remove.bg/" 47 | description: "Remove image background - 100% automatically" 48 | formats: 49 | - deb 50 | - rpm 51 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | digest = "1:e64ecbfe2c3f8d74763af8dff5f20084556bef9c7a5653eb183527a9d8b82503" 6 | name = "github.com/bmatcuk/doublestar" 7 | packages = ["."] 8 | pruneopts = "UT" 9 | revision = "39df92f7399bdaa74c821d606e9322dd1332074a" 10 | version = "v1.3.0" 11 | 12 | [[projects]] 13 | digest = "1:80057945464ffb5b0da1f026beb8df0e8dbd098eaf771a349291bed2cd29a83e" 14 | name = "github.com/fsnotify/fsnotify" 15 | packages = ["."] 16 | pruneopts = "UT" 17 | revision = "45d7d09e39ef4ac08d493309fa031790c15bfe8a" 18 | version = "v1.4.9" 19 | 20 | [[projects]] 21 | digest = "1:574f4e0c57e045c9d109d23c60c586b2198eaead53daa97ed5f65c616e22a9b0" 22 | name = "github.com/h2non/parth" 23 | packages = ["."] 24 | pruneopts = "UT" 25 | revision = "b4df798d65426f8c8ab5ca5f9987aec5575d26c9" 26 | version = "v2.0.1" 27 | 28 | [[projects]] 29 | digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be" 30 | name = "github.com/inconshreveable/mousetrap" 31 | packages = ["."] 32 | pruneopts = "UT" 33 | revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" 34 | version = "v1.0" 35 | 36 | [[projects]] 37 | branch = "master" 38 | digest = "1:400e113a367b511b9b09ca642ee11d9885485a93838526d697033af334a2fdde" 39 | name = "github.com/kballard/go-shellquote" 40 | packages = ["."] 41 | pruneopts = "UT" 42 | revision = "95032a82bc518f77982ea72343cc1ade730072f0" 43 | 44 | [[projects]] 45 | digest = "1:09cb61dc19af93deae01587e2fdb1c081e0bf48f1a5ad5fa24f48750dc57dce8" 46 | name = "github.com/konsorten/go-windows-terminal-sequences" 47 | packages = ["."] 48 | pruneopts = "UT" 49 | revision = "edb144dfd453055e1e49a3d8b410a660b5a87613" 50 | version = "v1.0.3" 51 | 52 | [[projects]] 53 | digest = "1:0109cf4321a15313ec895f42e723e1f76121c6975ea006abfa20012272ec0937" 54 | name = "github.com/mattn/go-colorable" 55 | packages = ["."] 56 | pruneopts = "UT" 57 | revision = "68e95eba382c972aafde02ead2cd2426a8a92480" 58 | version = "v0.1.6" 59 | 60 | [[projects]] 61 | digest = "1:0c58d31abe2a2ccb429c559b6292e7df89dcda675456fecc282fa90aa08273eb" 62 | name = "github.com/mattn/go-isatty" 63 | packages = ["."] 64 | pruneopts = "UT" 65 | revision = "7b513a986450394f7bbf1476909911b3aa3a55ce" 66 | version = "v0.0.12" 67 | 68 | [[projects]] 69 | branch = "master" 70 | digest = "1:2b32af4d2a529083275afc192d1067d8126b578c7a9613b26600e4df9c735155" 71 | name = "github.com/mgutz/ansi" 72 | packages = ["."] 73 | pruneopts = "UT" 74 | revision = "9520e82c474b0a04dd04f8a40959027271bab992" 75 | 76 | [[projects]] 77 | digest = "1:a9f00de9b605c251f5f1eba5d34c238131f0a7d38f6e7126c6bacc073f314865" 78 | name = "github.com/nxadm/tail" 79 | packages = [ 80 | ".", 81 | "ratelimiter", 82 | "util", 83 | "watch", 84 | "winfile", 85 | ] 86 | pruneopts = "UT" 87 | revision = "327c577245448d8192115e77a76ea3d6aee88202" 88 | version = "v1.4.4" 89 | 90 | [[projects]] 91 | digest = "1:0aeeb8b2bdb9d59c74b92b32a4751209e615f6c26aea01fe1c4675d26ff0a72f" 92 | name = "github.com/onsi/ginkgo" 93 | packages = [ 94 | ".", 95 | "config", 96 | "internal/codelocation", 97 | "internal/containernode", 98 | "internal/failer", 99 | "internal/leafnodes", 100 | "internal/remote", 101 | "internal/spec", 102 | "internal/spec_iterator", 103 | "internal/specrunner", 104 | "internal/suite", 105 | "internal/testingtproxy", 106 | "internal/writer", 107 | "reporters", 108 | "reporters/stenographer", 109 | "reporters/stenographer/support/go-colorable", 110 | "reporters/stenographer/support/go-isatty", 111 | "types", 112 | ] 113 | pruneopts = "UT" 114 | revision = "cc0216944b25a88d3259699a029d4e601fb8a222" 115 | version = "v1.12.1" 116 | 117 | [[projects]] 118 | digest = "1:c218ecde80086a2c5427e431d84409ca2ed07d36e4fce140a42ccb74944e2a28" 119 | name = "github.com/onsi/gomega" 120 | packages = [ 121 | ".", 122 | "format", 123 | "gbytes", 124 | "gexec", 125 | "internal/assertion", 126 | "internal/asyncassertion", 127 | "internal/oraclematcher", 128 | "internal/testingtsupport", 129 | "matchers", 130 | "matchers/support/goraph/bipartitegraph", 131 | "matchers/support/goraph/edge", 132 | "matchers/support/goraph/node", 133 | "matchers/support/goraph/util", 134 | "types", 135 | ] 136 | pruneopts = "UT" 137 | revision = "1a3d249459a44387a05ca2d2c2b3d5f3db596dcb" 138 | version = "v1.10.0" 139 | 140 | [[projects]] 141 | digest = "1:1705716d33e69b9567ec6fd8e318a6e60869838b60643e54d0301bed8ac427d1" 142 | name = "github.com/sirupsen/logrus" 143 | packages = [ 144 | ".", 145 | "hooks/test", 146 | ] 147 | pruneopts = "UT" 148 | revision = "60c74ad9be0d874af0ab0daef6ab07c5c5911f0d" 149 | version = "v1.6.0" 150 | 151 | [[projects]] 152 | digest = "1:f6adcf4df6c030a53a14a8fdbb3fade7cac3d014ff78a4e0de500b4c0cf5154f" 153 | name = "github.com/spf13/cobra" 154 | packages = ["."] 155 | pruneopts = "UT" 156 | revision = "a684a6d7f5e37385d954dd3b5a14fc6912c6ab9d" 157 | version = "v1.0.0" 158 | 159 | [[projects]] 160 | digest = "1:524b71991fc7d9246cc7dc2d9e0886ccb97648091c63e30eef619e6862c955dd" 161 | name = "github.com/spf13/pflag" 162 | packages = ["."] 163 | pruneopts = "UT" 164 | revision = "2e9d26c8c37aae03e3f9d4e90b7116f5accb7cab" 165 | version = "v1.0.5" 166 | 167 | [[projects]] 168 | branch = "master" 169 | digest = "1:8c294aabd4396170f5bcbb763d558e4c8d21ded4b2a1618dfb664e58a4fffeef" 170 | name = "golang.org/x/net" 171 | packages = [ 172 | "html", 173 | "html/atom", 174 | "html/charset", 175 | ] 176 | pruneopts = "UT" 177 | revision = "d87ec0cfa47603df72c7e24116080bdcd7788ba7" 178 | 179 | [[projects]] 180 | branch = "master" 181 | digest = "1:020620a097c2bfd056c8db7d31a69ea2cfed874ce985763dcd9ae00f9fa5f74b" 182 | name = "golang.org/x/sys" 183 | packages = [ 184 | "internal/unsafeheader", 185 | "unix", 186 | ] 187 | pruneopts = "UT" 188 | revision = "fe76b779f299728f3bd63f77ea2c815504229c3b" 189 | 190 | [[projects]] 191 | digest = "1:8a0baffd5559acaa560f854d7d525c02f4fec2d4f8a214398556fb661a10f6e0" 192 | name = "golang.org/x/text" 193 | packages = [ 194 | "encoding", 195 | "encoding/charmap", 196 | "encoding/htmlindex", 197 | "encoding/internal", 198 | "encoding/internal/identifier", 199 | "encoding/japanese", 200 | "encoding/korean", 201 | "encoding/simplifiedchinese", 202 | "encoding/traditionalchinese", 203 | "encoding/unicode", 204 | "internal/gen", 205 | "internal/language", 206 | "internal/language/compact", 207 | "internal/tag", 208 | "internal/utf8internal", 209 | "language", 210 | "runes", 211 | "transform", 212 | "unicode/cldr", 213 | ] 214 | pruneopts = "UT" 215 | revision = "342b2e1fbaa52c93f31447ad2c6abc048c63e475" 216 | version = "v0.3.2" 217 | 218 | [[projects]] 219 | branch = "master" 220 | digest = "1:918a46e4a2fb83df33f668f5a6bd51b2996775d073fce1800d3ec01b0a5ddd2b" 221 | name = "golang.org/x/xerrors" 222 | packages = [ 223 | ".", 224 | "internal", 225 | ] 226 | pruneopts = "UT" 227 | revision = "9bdfabe68543c54f90421aeb9a60ef8061b5b544" 228 | 229 | [[projects]] 230 | digest = "1:fea33829eb537e8926ec996b1a65d3d2735ace0a85b053d016023fc7613d45d7" 231 | name = "gopkg.in/AlecAivazis/survey.v1" 232 | packages = [ 233 | ".", 234 | "core", 235 | "terminal", 236 | ] 237 | pruneopts = "UT" 238 | revision = "da50ccef2fd74048e26b7846a731da4254035115" 239 | version = "v1.8.8" 240 | 241 | [[projects]] 242 | digest = "1:80d8747c77f6b6c8e77232cd04aa93cbf4489836d47e487566d94e6ecbe23082" 243 | name = "gopkg.in/h2non/gock.v1" 244 | packages = ["."] 245 | pruneopts = "UT" 246 | revision = "3ffff9b1aa8200275a5eb219c5f9c62bd27acb31" 247 | version = "v1.0.15" 248 | 249 | [[projects]] 250 | branch = "v1" 251 | digest = "1:0caa92e17bc0b65a98c63e5bc76a9e844cd5e56493f8fdbb28fad101a16254d9" 252 | name = "gopkg.in/tomb.v1" 253 | packages = ["."] 254 | pruneopts = "UT" 255 | revision = "dd632973f1e7218eb1089048e0798ec9ae7dceb8" 256 | 257 | [[projects]] 258 | digest = "1:d7f1bd887dc650737a421b872ca883059580e9f8314d601f88025df4f4802dce" 259 | name = "gopkg.in/yaml.v2" 260 | packages = ["."] 261 | pruneopts = "UT" 262 | revision = "0b1645d91e851e735d3e23330303ce81f70adbe3" 263 | version = "v2.3.0" 264 | 265 | [solve-meta] 266 | analyzer-name = "dep" 267 | analyzer-version = 1 268 | input-imports = [ 269 | "github.com/bmatcuk/doublestar", 270 | "github.com/mattn/go-colorable", 271 | "github.com/onsi/ginkgo", 272 | "github.com/onsi/ginkgo/config", 273 | "github.com/onsi/gomega", 274 | "github.com/onsi/gomega/gbytes", 275 | "github.com/onsi/gomega/gexec", 276 | "github.com/sirupsen/logrus", 277 | "github.com/sirupsen/logrus/hooks/test", 278 | "github.com/spf13/cobra", 279 | "gopkg.in/AlecAivazis/survey.v1", 280 | "gopkg.in/h2non/gock.v1", 281 | ] 282 | solver-name = "gps-cdcl" 283 | solver-version = 1 284 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[override]] 29 | name = "gopkg.in/fsnotify.v1" 30 | source = "https://github.com/fsnotify/fsnotify.git" 31 | 32 | [prune] 33 | go-tests = true 34 | unused-packages = true 35 | 36 | [[constraint]] 37 | name = "gopkg.in/h2non/gock.v1" 38 | version = "1.0.14" 39 | 40 | [[constraint]] 41 | name = "gopkg.in/AlecAivazis/survey.v1" 42 | version = "1.8.3" 43 | 44 | [[constraint]] 45 | name = "github.com/sirupsen/logrus" 46 | version = "1.4.1" 47 | 48 | [[constraint]] 49 | name = "github.com/bmatcuk/doublestar" 50 | version = "1.1.1" 51 | 52 | [[constraint]] 53 | name = "github.com/spf13/cobra" 54 | version = "1.0.0" 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kaleido AI GmbH 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # remove.bg CLI 2.0 2 | 3 | ⚠️ _This repository is archived! Please read the message below._ 4 | 5 | We released a new version of the remove.bg CLI. You can download it here: https://github.com/remove-bg/remove-bg-cli 6 | The go version of our CLI is no longer maintained nor supported. 7 | 8 | --- 9 | 10 | ## remove.bg CLI 11 | 12 | ### Installation 13 | 14 | #### Download 15 | 16 | You can **[download latest stable release][releases]** (Windows, Mac, and Linux supported) 17 | 18 | #### Homebrew 19 | 20 | ``` 21 | brew install remove-bg/homebrew-tap/removebg 22 | ``` 23 | 24 | #### deb / rpm 25 | 26 | Download the .deb or .rpm from the [releases page][releases] and install with 27 | `dpkg -i` and `rpm -i`. 28 | 29 | For the latest `deb` package supporting `x86-64` you can also run: 30 | 31 | ``` 32 | curl -LO $(curl https://api.github.com/repos/remove-bg/go/releases/latest | grep -o "https://github.com/remove-bg/go/releases/download/.*linux_amd64.deb") 33 | sudo dpkg -i removebg*.deb 34 | ``` 35 | 36 | [releases]: https://github.com/remove-bg/go/releases 37 | 38 | ### Usage 39 | 40 | ``` 41 | removebg [options] ... 42 | ``` 43 | 44 | #### API key 45 | 46 | To process images you'll need your [remove.bg API key][api-key]. 47 | 48 | [api-key]: https://www.remove.bg/profile#api-key 49 | 50 | To use the API key for all requests you can export the following environment 51 | variable in your shell profile (e.g. `~/.bashrc` / `~/.zshrc`): 52 | 53 | ```sh 54 | export REMOVE_BG_API_KEY=xyz 55 | ``` 56 | 57 | Alternatively you can specify the API key per command: 58 | 59 | ```sh 60 | removebg --api-key xyz images/image1.jpg 61 | ``` 62 | 63 | ### Processing a directory of images 64 | 65 | #### Saving to the same directory (default) 66 | 67 | If you want to remove the background from all the PNG and JPG images in a 68 | directory, and save the transparent images in the same directory: 69 | 70 | ```sh 71 | removebg images/*.{png,jpg} 72 | ``` 73 | 74 | Given the following input: 75 | 76 | ``` 77 | images/ 78 | ├── dog.jpg 79 | └── cat.png 80 | ``` 81 | 82 | The result would be: 83 | 84 | ``` 85 | images/ 86 | ├── dog.jpg 87 | ├── cat.png 88 | ├── dog-removebg.png 89 | └── cat-removebg.png 90 | ``` 91 | 92 | ##### Saving to a different directory (`--output-directory`) 93 | 94 | If you want to remove the background from all the PNG and JPG images in a 95 | directory, and save the transparent images in a different directory: 96 | 97 | ```sh 98 | mkdir processed 99 | removebg --output-directory processed originals/*.{png,jpg} 100 | ``` 101 | 102 | Given the following input: 103 | 104 | ``` 105 | originals/ 106 | ├── dog.jpg 107 | └── cat.png 108 | ``` 109 | 110 | The result would be: 111 | 112 | ``` 113 | originals/ 114 | ├── dog.jpg 115 | └── cat.png 116 | 117 | processed/ 118 | ├── dog.png 119 | └── cat.png 120 | ``` 121 | 122 | #### CLI options 123 | 124 | - `--api-key` or `REMOVE_BG_API_KEY` environment variable (required). 125 | 126 | - `--output-directory` (optional) - The output directory for processed images. 127 | 128 | - `--reprocess-existing` - Images which have already been processed are skipped 129 | by default to save credits. Specify this flag to force reprocessing. 130 | 131 | - `--confirm-batch-over` (default `50`) - Prompt for confirmation before 132 | processing batches over this size. Specify `-1` to disable this safeguard. 133 | 134 | ##### Image processing options 135 | 136 | Please see the [API documentation][api-docs] for further details. 137 | 138 | [api-docs]: https://www.remove.bg/api#operations-tag-Background%20Removal 139 | 140 | - `--size` (default `auto`) 141 | - `--type` 142 | - `--channels` 143 | - `--bg-color` 144 | - `--format` (default: `png`) 145 | - `--extra-api-options` for forwarding any unlisted/new options to the API 146 | - Formatted as a URI encoded string (`=` between key/value, delimited with `&`) 147 | - e.g. `--extra-api-options 'crop=true&add_shadow=true'` 148 | 149 | ### Examples 150 | 151 | ```sh 152 | # Producing a JPG with a grey background at the path: processed/subject.jpg 153 | removebg subject.jpg --format jpg --bg-color 7a7a7a --output-directory processed 154 | 155 | # Producing a large transparent PNG image up to 25 megapixels 156 | removebg large.jpg --size full --format png 157 | 158 | # Processing a car image with additional API options 159 | removebg car.jpg --type car --extra-api-options 'add_shadow=true&semitransparency=true' 160 | ``` 161 | 162 | ### Development 163 | 164 | Prerequisites: 165 | 166 | - `go 1.14` 167 | - [`dep`](https://golang.github.io/dep/) 168 | 169 | Getting started: 170 | 171 | ``` 172 | git clone git@github.com:remove-bg/go.git $GOPATH/github.com/remove-bg/go 173 | cd $GOPATH/github.com/remove-bg/go 174 | bin/setup 175 | bin/test 176 | ``` 177 | 178 | To build & try out locally: 179 | 180 | ``` 181 | go build -o removebg main.go 182 | ./removebg --help 183 | ``` 184 | 185 | #### Releasing a new version 186 | 187 | - Install [goreleaser](https://goreleaser.com/install/) 188 | - [Create a Github token](https://github.com/settings/tokens/new) with repo access 189 | - Run the release script: 190 | 191 | ``` 192 | GITHUB_TOKEN=xyz bin/release vX.Y.Z 193 | ``` 194 | -------------------------------------------------------------------------------- /bin/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | version = ARGV.first 4 | 5 | unless version 6 | warn "Usage: bin/release \nE.g. bin/release v1.2.3" 7 | exit 1 8 | end 9 | 10 | unless ENV.key?("GITHUB_TOKEN") 11 | warn "Please set GITHUB_TOKEN environment variable" 12 | exit 1 13 | end 14 | 15 | unless system("which goreleaser > /dev/null") 16 | warn "Please install goreleaser:\nhttps://goreleaser.com/install/" 17 | exit 1 18 | end 19 | 20 | puts "Creating tag..." 21 | unless system("git tag -a #{version} -m '#{version}'") 22 | warn "Unable to create tag" 23 | exit 1 24 | end 25 | 26 | puts "Pushing tag..." 27 | unless system("git push origin #{version}") 28 | warn "Unable to push tag" 29 | exit 1 30 | end 31 | 32 | puts "Builing release..." 33 | exec("goreleaser --rm-dist") 34 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euox pipefail 4 | 5 | dep ensure 6 | 7 | go get -u github.com/onsi/ginkgo/ginkgo 8 | go get -u github.com/maxbrunsfeld/counterfeiter 9 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | ginkgo -r -cover -race -randomizeAllSpecs -p -requireSuite 6 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "mime/multipart" 11 | "net/http" 12 | "os" 13 | "path/filepath" 14 | "strings" 15 | ) 16 | 17 | const APIEndpoint = "https://api.remove.bg/v1.0/removebg" 18 | const imageFileParam = "image_file" 19 | const bgImageFileParam = "bg_image_file" 20 | 21 | //go:generate counterfeiter . ClientInterface 22 | type ClientInterface interface { 23 | RemoveFromFile(inputPath string, apiKey string, params map[string]string) ([]byte, string, error) 24 | } 25 | 26 | type Client struct { 27 | Version string 28 | HTTPClient http.Client 29 | } 30 | 31 | func (c Client) RemoveFromFile(inputPath string, apiKey string, params map[string]string) ([]byte, string, error) { 32 | request, err := c.buildRequest(APIEndpoint, apiKey, params, inputPath) 33 | if err != nil { 34 | return nil, "", err 35 | } 36 | 37 | resp, err := c.HTTPClient.Do(request) 38 | if err != nil { 39 | return nil, "", err 40 | } 41 | 42 | defer resp.Body.Close() 43 | 44 | statusCode := resp.StatusCode 45 | body, err := ioutil.ReadAll(resp.Body) 46 | contentType := resp.Header.Get("Content-Type") 47 | 48 | if statusCode == 200 { 49 | return body, contentType, err 50 | } else if statusCode >= 400 && statusCode < 500 { 51 | return nil, "", parseJsonErrors(statusCode, body) 52 | } else { 53 | return nil, "", fmt.Errorf("Unable to process image http_status=%d", statusCode) 54 | } 55 | } 56 | 57 | func (c Client) buildRequest(uri string, apiKey string, params map[string]string, inputPath string) (*http.Request, error) { 58 | body := &bytes.Buffer{} 59 | writer := multipart.NewWriter(body) 60 | 61 | err := attachFile(writer, imageFileParam, inputPath) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | if len(params[bgImageFileParam]) > 0 { 67 | err := attachFile(writer, bgImageFileParam, params[bgImageFileParam]) 68 | if err != nil { 69 | return nil, err 70 | } 71 | delete(params, bgImageFileParam) 72 | } 73 | 74 | for key, val := range params { 75 | _ = writer.WriteField(key, val) 76 | } 77 | 78 | err = writer.Close() 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | req, err := http.NewRequest("POST", uri, body) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | req.Header.Set("Content-Type", writer.FormDataContentType()) 89 | req.Header.Add("X-Api-Key", apiKey) 90 | req.Header.Add("User-Agent", c.userAgent()) 91 | return req, err 92 | } 93 | 94 | func attachFile(writer *multipart.Writer, paramName string, filePath string) error { 95 | file, err := os.Open(filePath) 96 | if err != nil { 97 | return errors.New("Unable to read file") 98 | } 99 | 100 | defer file.Close() 101 | 102 | part, err := writer.CreateFormFile(paramName, filepath.Base(filePath)) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | _, err = io.Copy(part, file) 108 | return err 109 | } 110 | 111 | func (c Client) userAgent() string { 112 | return fmt.Sprintf("remove-bg-go-%s", c.Version) 113 | } 114 | 115 | func parseJsonErrors(statusCode int, body []byte) error { 116 | parsedErrorResponse := jsonErrorResponse{} 117 | err := json.Unmarshal(body, &parsedErrorResponse) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | errorMessages := make([]string, len(parsedErrorResponse.Errors)) 123 | for i, e := range parsedErrorResponse.Errors { 124 | errorMessages[i] = e.Title 125 | } 126 | 127 | message := strings.Join(errorMessages, ", ") 128 | 129 | return &RequestError{ 130 | StatusCode: statusCode, 131 | Err: errors.New(message), 132 | } 133 | } 134 | 135 | type jsonErrorResponse struct { 136 | Errors []struct { 137 | Title string 138 | } 139 | } 140 | 141 | type RequestError struct { 142 | StatusCode int 143 | Err error 144 | } 145 | 146 | func (r *RequestError) Error() string { 147 | return fmt.Sprintf("%d: %s", r.StatusCode, r.Err.Error()) 148 | } 149 | 150 | func (r *RequestError) RateLimitExceeded() bool { 151 | return r.StatusCode == 429 152 | } 153 | -------------------------------------------------------------------------------- /client/client_suite_test.go: -------------------------------------------------------------------------------- 1 | package client_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestClient(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Client Suite") 13 | } 14 | -------------------------------------------------------------------------------- /client/client_test.go: -------------------------------------------------------------------------------- 1 | package client_test 2 | 3 | import ( 4 | "fmt" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | "github.com/remove-bg/go/client" 8 | "gopkg.in/h2non/gock.v1" 9 | "net/http" 10 | "path" 11 | "runtime" 12 | ) 13 | 14 | var _ = Describe("Client", func() { 15 | var ( 16 | fixtureFile string 17 | bgFixtureFile string 18 | subject client.Client 19 | ) 20 | 21 | BeforeEach(func() { 22 | _, testFile, _, _ := runtime.Caller(0) 23 | fixtureFile = path.Join(path.Dir(testFile), "../fixtures/person-in-field.jpg") 24 | bgFixtureFile = path.Join(path.Dir(testFile), "../fixtures/background.jpg") 25 | subject = client.Client{ 26 | Version: "x.y.z", 27 | HTTPClient: http.Client{}, 28 | } 29 | }) 30 | 31 | AfterEach(func() { 32 | gock.Off() 33 | }) 34 | 35 | It("requests the background removal", func() { 36 | gock.New("https://api.remove.bg"). 37 | Post("/v1.0/removebg"). 38 | MatchHeader("X-Api-Key", "^api-key$"). 39 | Reply(200). 40 | SetHeader("Content-Type", "image/png"). 41 | BodyString("data") 42 | 43 | result, contentType, err := subject.RemoveFromFile(fixtureFile, "api-key", map[string]string{}) 44 | 45 | Expect(err).To(Not(HaveOccurred())) 46 | Expect(result).To(Equal([]byte("data"))) 47 | Expect(contentType).To(Equal("image/png")) 48 | Expect(gock.IsDone()).To(BeTrue()) 49 | }) 50 | 51 | It("attaches the image file", func() { 52 | matcher := newMultipartAttachmentMatcher("image_file", "person-in-field.jpg") 53 | 54 | gock.New("https://api.remove.bg"). 55 | Post("/v1.0/removebg"). 56 | SetMatcher(matcher). 57 | Reply(200). 58 | BodyString("data") 59 | 60 | _, _, err := subject.RemoveFromFile(fixtureFile, "api-key", map[string]string{}) 61 | 62 | Expect(err).To(Not(HaveOccurred())) 63 | Expect(gock.IsDone()).To(BeTrue()) 64 | }) 65 | 66 | It("attaches a background image file if specified", func() { 67 | imageMatcher := newMultipartAttachmentMatcher("image_file", "person-in-field.jpg") 68 | bgImageMatcher := newMultipartAttachmentMatcher("bg_image_file", "background.jpg") 69 | 70 | gock.New("https://api.remove.bg"). 71 | Post("/v1.0/removebg"). 72 | SetMatcher(imageMatcher). 73 | SetMatcher(bgImageMatcher). 74 | Reply(200). 75 | BodyString("data") 76 | 77 | params := map[string]string{ 78 | "bg_image_file": bgFixtureFile, 79 | } 80 | 81 | _, _, err := subject.RemoveFromFile(fixtureFile, "api-key", params) 82 | 83 | Expect(err).To(Not(HaveOccurred())) 84 | Expect(gock.IsDone()).To(BeTrue()) 85 | }) 86 | 87 | It("includes the client version", func() { 88 | gock.New("https://api.remove.bg"). 89 | Post("/v1.0/removebg"). 90 | MatchHeader("User-Agent", "remove-bg-go-x.y.z"). 91 | Reply(200). 92 | BodyString("data") 93 | 94 | subject.RemoveFromFile(fixtureFile, "api-key", map[string]string{}) 95 | 96 | Expect(gock.IsDone()).To(BeTrue()) 97 | }) 98 | 99 | Context("server HTTP error", func() { 100 | It("returns a clear error", func() { 101 | gock.New("https://api.remove.bg"). 102 | Post("/v1.0/removebg"). 103 | Reply(500) 104 | 105 | result, _, err := subject.RemoveFromFile(fixtureFile, "api-key", map[string]string{}) 106 | 107 | Expect(result).To(BeNil()) 108 | Expect(err).To(MatchError("Unable to process image http_status=500")) 109 | }) 110 | }) 111 | 112 | Context("client HTTP error", func() { 113 | It("parses the JSON error messages", func() { 114 | jsonError := `{"errors": [{"title": "File too large"}, {"title": "Second error"}]}` 115 | 116 | gock.New("https://api.remove.bg"). 117 | Post("/v1.0/removebg"). 118 | Reply(400). 119 | BodyString(jsonError) 120 | 121 | result, _, err := subject.RemoveFromFile(fixtureFile, "api-key", map[string]string{}) 122 | 123 | Expect(result).To(BeNil()) 124 | 125 | re, ok := err.(*client.RequestError) 126 | Expect(ok).To(BeTrue()) 127 | 128 | Expect(re.Error()).To(Equal("400: File too large, Second error")) 129 | Expect(re.StatusCode).To(Equal(400)) 130 | }) 131 | }) 132 | 133 | Context("input file doesn't exist", func() { 134 | It("returns a clear error", func() { 135 | nonExistentFile := "/tmp/not-a-file" 136 | result, _, err := subject.RemoveFromFile(nonExistentFile, "api-key", map[string]string{}) 137 | 138 | Expect(result).To(BeNil()) 139 | Expect(err).To(MatchError("Unable to read file")) 140 | }) 141 | }) 142 | }) 143 | 144 | func newMultipartAttachmentMatcher(key string, expectedFilename string) *gock.MockMatcher { 145 | // Create a new custom matcher with HTTP headers only matchers 146 | matcher := gock.NewBasicMatcher() 147 | 148 | // Add a custom match function 149 | matcher.Add(func(req *http.Request, ereq *gock.Request) (bool, error) { 150 | _, header, err := req.FormFile(key) 151 | if err != nil { 152 | return false, err 153 | } 154 | 155 | if header.Size == 0 { 156 | return false, fmt.Errorf("Attachment is empty: %v", header.Size) 157 | } 158 | 159 | if header.Filename == expectedFilename { 160 | return true, nil 161 | } else { 162 | return false, fmt.Errorf("Image filename was: %s", header.Filename) 163 | } 164 | }) 165 | 166 | return matcher 167 | } 168 | -------------------------------------------------------------------------------- /client/clientfakes/fake_client_interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package clientfakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/remove-bg/go/client" 8 | ) 9 | 10 | type FakeClientInterface struct { 11 | RemoveFromFileStub func(string, string, map[string]string) ([]byte, string, error) 12 | removeFromFileMutex sync.RWMutex 13 | removeFromFileArgsForCall []struct { 14 | arg1 string 15 | arg2 string 16 | arg3 map[string]string 17 | } 18 | removeFromFileReturns struct { 19 | result1 []byte 20 | result2 string 21 | result3 error 22 | } 23 | removeFromFileReturnsOnCall map[int]struct { 24 | result1 []byte 25 | result2 string 26 | result3 error 27 | } 28 | invocations map[string][][]interface{} 29 | invocationsMutex sync.RWMutex 30 | } 31 | 32 | func (fake *FakeClientInterface) RemoveFromFile(arg1 string, arg2 string, arg3 map[string]string) ([]byte, string, error) { 33 | fake.removeFromFileMutex.Lock() 34 | ret, specificReturn := fake.removeFromFileReturnsOnCall[len(fake.removeFromFileArgsForCall)] 35 | fake.removeFromFileArgsForCall = append(fake.removeFromFileArgsForCall, struct { 36 | arg1 string 37 | arg2 string 38 | arg3 map[string]string 39 | }{arg1, arg2, arg3}) 40 | fake.recordInvocation("RemoveFromFile", []interface{}{arg1, arg2, arg3}) 41 | fake.removeFromFileMutex.Unlock() 42 | if fake.RemoveFromFileStub != nil { 43 | return fake.RemoveFromFileStub(arg1, arg2, arg3) 44 | } 45 | if specificReturn { 46 | return ret.result1, ret.result2, ret.result3 47 | } 48 | fakeReturns := fake.removeFromFileReturns 49 | return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 50 | } 51 | 52 | func (fake *FakeClientInterface) RemoveFromFileCallCount() int { 53 | fake.removeFromFileMutex.RLock() 54 | defer fake.removeFromFileMutex.RUnlock() 55 | return len(fake.removeFromFileArgsForCall) 56 | } 57 | 58 | func (fake *FakeClientInterface) RemoveFromFileCalls(stub func(string, string, map[string]string) ([]byte, string, error)) { 59 | fake.removeFromFileMutex.Lock() 60 | defer fake.removeFromFileMutex.Unlock() 61 | fake.RemoveFromFileStub = stub 62 | } 63 | 64 | func (fake *FakeClientInterface) RemoveFromFileArgsForCall(i int) (string, string, map[string]string) { 65 | fake.removeFromFileMutex.RLock() 66 | defer fake.removeFromFileMutex.RUnlock() 67 | argsForCall := fake.removeFromFileArgsForCall[i] 68 | return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 69 | } 70 | 71 | func (fake *FakeClientInterface) RemoveFromFileReturns(result1 []byte, result2 string, result3 error) { 72 | fake.removeFromFileMutex.Lock() 73 | defer fake.removeFromFileMutex.Unlock() 74 | fake.RemoveFromFileStub = nil 75 | fake.removeFromFileReturns = struct { 76 | result1 []byte 77 | result2 string 78 | result3 error 79 | }{result1, result2, result3} 80 | } 81 | 82 | func (fake *FakeClientInterface) RemoveFromFileReturnsOnCall(i int, result1 []byte, result2 string, result3 error) { 83 | fake.removeFromFileMutex.Lock() 84 | defer fake.removeFromFileMutex.Unlock() 85 | fake.RemoveFromFileStub = nil 86 | if fake.removeFromFileReturnsOnCall == nil { 87 | fake.removeFromFileReturnsOnCall = make(map[int]struct { 88 | result1 []byte 89 | result2 string 90 | result3 error 91 | }) 92 | } 93 | fake.removeFromFileReturnsOnCall[i] = struct { 94 | result1 []byte 95 | result2 string 96 | result3 error 97 | }{result1, result2, result3} 98 | } 99 | 100 | func (fake *FakeClientInterface) Invocations() map[string][][]interface{} { 101 | fake.invocationsMutex.RLock() 102 | defer fake.invocationsMutex.RUnlock() 103 | fake.removeFromFileMutex.RLock() 104 | defer fake.removeFromFileMutex.RUnlock() 105 | copiedInvocations := map[string][][]interface{}{} 106 | for key, value := range fake.invocations { 107 | copiedInvocations[key] = value 108 | } 109 | return copiedInvocations 110 | } 111 | 112 | func (fake *FakeClientInterface) recordInvocation(key string, args []interface{}) { 113 | fake.invocationsMutex.Lock() 114 | defer fake.invocationsMutex.Unlock() 115 | if fake.invocations == nil { 116 | fake.invocations = map[string][][]interface{}{} 117 | } 118 | if fake.invocations[key] == nil { 119 | fake.invocations[key] = [][]interface{}{} 120 | } 121 | fake.invocations[key] = append(fake.invocations[key], args) 122 | } 123 | 124 | var _ client.ClientInterface = new(FakeClientInterface) 125 | -------------------------------------------------------------------------------- /cmd/cmd_suite_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestCli(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Cmd Suite") 13 | } 14 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/remove-bg/go/processor" 7 | "github.com/spf13/cobra" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | const defaultLargeBatchSize = 50 13 | 14 | var ( 15 | apiKey string 16 | confirmBatchOver int 17 | outputDirectory string 18 | reprocessExisting bool 19 | skipPngFormatOptimization bool 20 | imageSize string 21 | imageType string 22 | imageFormat string 23 | imageChannels string 24 | bgColor string 25 | bgImageFile string 26 | extraApiOptions string 27 | ) 28 | 29 | // RootCmd is the entry point of command-line execution 30 | var RootCmd = &cobra.Command{ 31 | Short: "Remove image background - 100% automatically", 32 | Use: "removebg ...", 33 | Args: cobra.MinimumNArgs(1), 34 | RunE: func(cmd *cobra.Command, args []string) error { 35 | if len(apiKey) == 0 { 36 | return errors.New("API key must be specified") 37 | } 38 | 39 | if len(args) == 0 { 40 | return errors.New("please specify one or more files") 41 | } 42 | 43 | p := processor.NewProcessor(apiKey, cmd.Version) 44 | s := processor.Settings{ 45 | OutputDirectory: outputDirectory, 46 | ReprocessExisting: reprocessExisting, 47 | SkipPngFormatOptimization: skipPngFormatOptimization, 48 | LargeBatchConfirmThreshold: confirmBatchOver, 49 | ImageSettings: processor.ImageSettings{ 50 | Size: imageSize, 51 | Type: imageType, 52 | Channels: imageChannels, 53 | BgColor: bgColor, 54 | BgImageFile: bgImageFile, 55 | OutputFormat: strings.ToLower(imageFormat), 56 | ExtraApiOptions: extraApiOptions, 57 | }, 58 | } 59 | 60 | p.Process(args, s) 61 | 62 | return nil 63 | }, 64 | } 65 | 66 | func ConfigureVersion(version string, commit string) { 67 | RootCmd.Version = version 68 | RootCmd.SetVersionTemplate(fmt.Sprintf("%s\n%s\n", version, commit)) 69 | } 70 | 71 | func init() { 72 | RootCmd.Flags().StringVar(&apiKey, "api-key", "", "API key (required) or set REMOVE_BG_API_KEY environment variable") 73 | RootCmd.Flags().StringVar(&outputDirectory, "output-directory", "", "Output directory") 74 | RootCmd.Flags().BoolVar(&reprocessExisting, "reprocess-existing", false, "Reprocess and overwrite any already processed images") 75 | RootCmd.Flags().BoolVar(&skipPngFormatOptimization, "skip-png-format-optimization", false, "Skip optimizing PNG format as ZIP to save bandwidth (default false)") 76 | RootCmd.Flags().IntVar(&confirmBatchOver, "confirm-batch-over", defaultLargeBatchSize, "Confirm any batches over this size (-1 to disable)") 77 | RootCmd.Flags().StringVar(&imageSize, "size", "auto", "Image size") 78 | RootCmd.Flags().StringVar(&imageType, "type", "", "Image type") 79 | RootCmd.Flags().StringVar(&imageFormat, "format", "png", "Image format") 80 | RootCmd.Flags().StringVar(&imageChannels, "channels", "", "Image channels") 81 | RootCmd.Flags().StringVar(&bgColor, "bg-color", "", "Image background color") 82 | RootCmd.Flags().StringVar(&bgImageFile, "bg-image-file", "", "Adds a background image from a file") 83 | RootCmd.Flags().StringVar(&extraApiOptions, "extra-api-options", "", "Extra options to forward to the API (format: 'option1=val1&option2=val2')") 84 | 85 | if len(apiKey) == 0 { 86 | apiKey = os.Getenv("REMOVE_BG_API_KEY") 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /cmd/root_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | . "github.com/remove-bg/go/cmd" 7 | ) 8 | 9 | var _ = Describe("ConfigureVersion", func() { 10 | It("sets the version", func() { 11 | ConfigureVersion("x.y.z", "sha") 12 | 13 | Expect(RootCmd.Version).To(Equal("x.y.z")) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /cmd/zip2png.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/remove-bg/go/composite" 5 | "github.com/spf13/cobra" 6 | "log" 7 | ) 8 | 9 | var zip2pngCmd = &cobra.Command{ 10 | Short: "Converts a remove.bg ZIP to a PNG", 11 | Use: "zip2png ", 12 | Args: cobra.ExactArgs(2), 13 | RunE: func(cmd *cobra.Command, args []string) error { 14 | inputZipPath := args[0] 15 | outputImagePath := args[1] 16 | composite := composite.New() 17 | 18 | err := composite.Process(inputZipPath, outputImagePath) 19 | 20 | if err != nil { 21 | return err 22 | } 23 | 24 | log.Printf("Processed zip: %s -> %s\n", inputZipPath, outputImagePath) 25 | return nil 26 | }, 27 | } 28 | 29 | func init() { 30 | RootCmd.AddCommand(zip2pngCmd) 31 | } 32 | -------------------------------------------------------------------------------- /composite/composite.go: -------------------------------------------------------------------------------- 1 | package composite 2 | 3 | import ( 4 | "github.com/remove-bg/go/storage" 5 | 6 | "archive/zip" 7 | "bytes" 8 | "fmt" 9 | "image" 10 | "image/color" 11 | "image/jpeg" 12 | "image/png" 13 | "io" 14 | ) 15 | 16 | //go:generate counterfeiter . CompositorInterface 17 | type CompositorInterface interface { 18 | Process(inputZipPath string, outputImagePath string) error 19 | } 20 | 21 | type Compositor struct { 22 | Storage storage.StorageInterface 23 | } 24 | 25 | type imageDecoder = func(io.Reader) (image.Image, error) 26 | 27 | func New() Compositor { 28 | return Compositor{ 29 | Storage: storage.FileStorage{}, 30 | } 31 | } 32 | 33 | func (c Compositor) Process(inputZipPath string, outputImagePath string) error { 34 | if !c.Storage.FileExists(inputZipPath) { 35 | return fmt.Errorf("Could not locate zip: %s", inputZipPath) 36 | } 37 | 38 | rgb, alpha, err := extractImagesFromZip(inputZipPath) 39 | 40 | if err != nil { 41 | return err 42 | } 43 | 44 | composited := composite(rgb, alpha) 45 | 46 | c.savePng(composited, outputImagePath) 47 | 48 | return nil 49 | } 50 | 51 | func (c Compositor) savePng(image *image.NRGBA, outputPath string) { 52 | buf := new(bytes.Buffer) 53 | png.Encode(buf, image) 54 | c.Storage.Write(outputPath, buf.Bytes()) 55 | } 56 | 57 | const zipColorImageFileName = "color.jpg" 58 | const zipAlphaImageFileName = "alpha.png" 59 | 60 | func extractImagesFromZip(filename string) (rgb image.Image, alpha image.Image, err error) { 61 | archive, err := zip.OpenReader(filename) 62 | if err != nil { 63 | return nil, nil, err 64 | } 65 | 66 | defer archive.Close() 67 | 68 | alpha, err = decodeZipImage(archive, zipAlphaImageFileName, png.Decode) 69 | if err != nil { 70 | return nil, nil, err 71 | } 72 | 73 | rgb, err = decodeZipImage(archive, zipColorImageFileName, jpeg.Decode) 74 | if err != nil { 75 | return nil, nil, err 76 | } 77 | 78 | return rgb, alpha, nil 79 | } 80 | 81 | func decodeZipImage(archive *zip.ReadCloser, fileName string, decoder imageDecoder) (image.Image, error) { 82 | for _, f := range archive.File { 83 | if f.Name == fileName { 84 | rc, err := f.Open() 85 | defer rc.Close() 86 | 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | return decoder(rc) 92 | } 93 | } 94 | 95 | return nil, fmt.Errorf("Unable to find image in ZIP: %s", fileName) 96 | } 97 | 98 | func composite(rgb image.Image, alpha image.Image) *image.NRGBA { 99 | dimensions := rgb.Bounds().Max 100 | width := dimensions.X 101 | height := dimensions.Y 102 | 103 | composited := image.NewNRGBA(image.Rect(0, 0, width, height)) 104 | colorModel := composited.ColorModel() 105 | 106 | for x := 0; x < width; x++ { 107 | for y := 0; y < height; y++ { 108 | rgbColor := (colorModel.Convert(rgb.At(x, y))).(color.NRGBA) 109 | alphaColor := (alpha.At(x, y)).(color.Gray) 110 | rgbColor.A = alphaColor.Y 111 | 112 | composited.SetNRGBA(x, y, rgbColor) 113 | } 114 | } 115 | 116 | return composited 117 | } 118 | -------------------------------------------------------------------------------- /composite/composite_suite_test.go: -------------------------------------------------------------------------------- 1 | package composite_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestClient(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Composite Suite") 13 | } 14 | -------------------------------------------------------------------------------- /composite/composite_test.go: -------------------------------------------------------------------------------- 1 | package composite_test 2 | 3 | import ( 4 | "fmt" 5 | . "github.com/onsi/ginkgo" 6 | "github.com/onsi/ginkgo/config" 7 | . "github.com/onsi/gomega" 8 | "github.com/remove-bg/go/composite" 9 | "os" 10 | "path" 11 | "runtime" 12 | ) 13 | 14 | var _ = Describe("Composite", func() { 15 | var ( 16 | subject composite.Compositor 17 | exampleZip string 18 | outputPath string 19 | testDir string 20 | ) 21 | 22 | BeforeEach(func() { 23 | subject = composite.New() 24 | 25 | _, testFile, _, _ := runtime.Caller(0) 26 | testDir = path.Dir(testFile) 27 | 28 | exampleZip = path.Join(testDir, "../fixtures/zip/example-cat.zip") 29 | outputPath = path.Join(testDir, fmt.Sprintf("../tmp/composite-cat-%d.png", config.GinkgoConfig.ParallelNode)) 30 | 31 | // Remove stale state from any previous test runs 32 | os.Remove(outputPath) 33 | }) 34 | 35 | Context("when the input zip does not exist", func() { 36 | It("returns an error", func() { 37 | Expect(subject.Process("missing.zip", outputPath)).To(MatchError("Could not locate zip: missing.zip")) 38 | }) 39 | 40 | It("does not write any output", func() { 41 | Expect(subject.Process("missing.zip", outputPath)).To(HaveOccurred()) 42 | Expect(outputPath).ToNot(BeAnExistingFile()) 43 | }) 44 | }) 45 | 46 | Context("when color.jpg does not exist in the input zip", func() { 47 | It("returns an error", func() { 48 | exampleZip = path.Join(testDir, "../fixtures/zip/example-missing-color.zip") 49 | Expect(exampleZip).To(BeAnExistingFile()) 50 | 51 | Expect(subject.Process(exampleZip, outputPath)).To(MatchError("Unable to find image in ZIP: color.jpg")) 52 | }) 53 | }) 54 | 55 | Context("when alpha.png does not exist in the input zip", func() { 56 | It("returns an error", func() { 57 | exampleZip = path.Join(testDir, "../fixtures/zip/example-missing-alpha.zip") 58 | Expect(exampleZip).To(BeAnExistingFile()) 59 | 60 | Expect(subject.Process(exampleZip, outputPath)).To(MatchError("Unable to find image in ZIP: alpha.png")) 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /composite/compositefakes/fake_compositor_interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package compositefakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/remove-bg/go/composite" 8 | ) 9 | 10 | type FakeCompositorInterface struct { 11 | ProcessStub func(string, string) error 12 | processMutex sync.RWMutex 13 | processArgsForCall []struct { 14 | arg1 string 15 | arg2 string 16 | } 17 | processReturns struct { 18 | result1 error 19 | } 20 | processReturnsOnCall map[int]struct { 21 | result1 error 22 | } 23 | invocations map[string][][]interface{} 24 | invocationsMutex sync.RWMutex 25 | } 26 | 27 | func (fake *FakeCompositorInterface) Process(arg1 string, arg2 string) error { 28 | fake.processMutex.Lock() 29 | ret, specificReturn := fake.processReturnsOnCall[len(fake.processArgsForCall)] 30 | fake.processArgsForCall = append(fake.processArgsForCall, struct { 31 | arg1 string 32 | arg2 string 33 | }{arg1, arg2}) 34 | fake.recordInvocation("Process", []interface{}{arg1, arg2}) 35 | fake.processMutex.Unlock() 36 | if fake.ProcessStub != nil { 37 | return fake.ProcessStub(arg1, arg2) 38 | } 39 | if specificReturn { 40 | return ret.result1 41 | } 42 | fakeReturns := fake.processReturns 43 | return fakeReturns.result1 44 | } 45 | 46 | func (fake *FakeCompositorInterface) ProcessCallCount() int { 47 | fake.processMutex.RLock() 48 | defer fake.processMutex.RUnlock() 49 | return len(fake.processArgsForCall) 50 | } 51 | 52 | func (fake *FakeCompositorInterface) ProcessCalls(stub func(string, string) error) { 53 | fake.processMutex.Lock() 54 | defer fake.processMutex.Unlock() 55 | fake.ProcessStub = stub 56 | } 57 | 58 | func (fake *FakeCompositorInterface) ProcessArgsForCall(i int) (string, string) { 59 | fake.processMutex.RLock() 60 | defer fake.processMutex.RUnlock() 61 | argsForCall := fake.processArgsForCall[i] 62 | return argsForCall.arg1, argsForCall.arg2 63 | } 64 | 65 | func (fake *FakeCompositorInterface) ProcessReturns(result1 error) { 66 | fake.processMutex.Lock() 67 | defer fake.processMutex.Unlock() 68 | fake.ProcessStub = nil 69 | fake.processReturns = struct { 70 | result1 error 71 | }{result1} 72 | } 73 | 74 | func (fake *FakeCompositorInterface) ProcessReturnsOnCall(i int, result1 error) { 75 | fake.processMutex.Lock() 76 | defer fake.processMutex.Unlock() 77 | fake.ProcessStub = nil 78 | if fake.processReturnsOnCall == nil { 79 | fake.processReturnsOnCall = make(map[int]struct { 80 | result1 error 81 | }) 82 | } 83 | fake.processReturnsOnCall[i] = struct { 84 | result1 error 85 | }{result1} 86 | } 87 | 88 | func (fake *FakeCompositorInterface) Invocations() map[string][][]interface{} { 89 | fake.invocationsMutex.RLock() 90 | defer fake.invocationsMutex.RUnlock() 91 | fake.processMutex.RLock() 92 | defer fake.processMutex.RUnlock() 93 | copiedInvocations := map[string][][]interface{}{} 94 | for key, value := range fake.invocations { 95 | copiedInvocations[key] = value 96 | } 97 | return copiedInvocations 98 | } 99 | 100 | func (fake *FakeCompositorInterface) recordInvocation(key string, args []interface{}) { 101 | fake.invocationsMutex.Lock() 102 | defer fake.invocationsMutex.Unlock() 103 | if fake.invocations == nil { 104 | fake.invocations = map[string][][]interface{}{} 105 | } 106 | if fake.invocations[key] == nil { 107 | fake.invocations[key] = [][]interface{}{} 108 | } 109 | fake.invocations[key] = append(fake.invocations[key], args) 110 | } 111 | 112 | var _ composite.CompositorInterface = new(FakeCompositorInterface) 113 | -------------------------------------------------------------------------------- /fixtures/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remove-bg/go/1bed17cf684fdb1545444540ff60d7eefa83de89/fixtures/background.jpg -------------------------------------------------------------------------------- /fixtures/nested/plant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remove-bg/go/1bed17cf684fdb1545444540ff60d7eefa83de89/fixtures/nested/plant.png -------------------------------------------------------------------------------- /fixtures/nomatch.txt: -------------------------------------------------------------------------------- 1 | Used for file storage test 2 | -------------------------------------------------------------------------------- /fixtures/person-in-field.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remove-bg/go/1bed17cf684fdb1545444540ff60d7eefa83de89/fixtures/person-in-field.jpg -------------------------------------------------------------------------------- /fixtures/zip/example-cat.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remove-bg/go/1bed17cf684fdb1545444540ff60d7eefa83de89/fixtures/zip/example-cat.zip -------------------------------------------------------------------------------- /fixtures/zip/example-missing-alpha.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remove-bg/go/1bed17cf684fdb1545444540ff60d7eefa83de89/fixtures/zip/example-missing-alpha.zip -------------------------------------------------------------------------------- /fixtures/zip/example-missing-color.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remove-bg/go/1bed17cf684fdb1545444540ff60d7eefa83de89/fixtures/zip/example-missing-color.zip -------------------------------------------------------------------------------- /fixtures/zip/reference-example-cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remove-bg/go/1bed17cf684fdb1545444540ff60d7eefa83de89/fixtures/zip/reference-example-cat.png -------------------------------------------------------------------------------- /integration_suite_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | "github.com/onsi/gomega/gexec" 9 | ) 10 | 11 | func TestClient(t *testing.T) { 12 | RegisterFailHandler(Fail) 13 | RunSpecs(t, "Integration Suite") 14 | } 15 | 16 | var cliPath string 17 | 18 | var _ = SynchronizedBeforeSuite(func() []byte { 19 | var err error 20 | path, err := gexec.Build("github.com/remove-bg/go") 21 | Expect(err).ShouldNot(HaveOccurred()) 22 | return []byte(path) 23 | }, func(data []byte) { 24 | cliPath = string(data) 25 | }) 26 | 27 | var _ = SynchronizedAfterSuite(func() { 28 | gexec.CleanupBuildArtifacts() 29 | }, func() {}) 30 | -------------------------------------------------------------------------------- /integration_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "crypto/sha256" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | "github.com/onsi/gomega/gbytes" 8 | "github.com/onsi/gomega/gexec" 9 | "io" 10 | "io/ioutil" 11 | "os" 12 | "os/exec" 13 | "path" 14 | "runtime" 15 | ) 16 | 17 | var _ = Describe("Remove.bg CLI: zip2png command", func() { 18 | var ( 19 | exampleZip string 20 | referencePath string 21 | outputPath string 22 | testDir string 23 | tmpOutputDir string 24 | ) 25 | 26 | BeforeEach(func() { 27 | _, testFile, _, _ := runtime.Caller(0) 28 | testDir = path.Dir(testFile) 29 | 30 | exampleZip = path.Join(testDir, "fixtures/zip/example-cat.zip") 31 | Expect(exampleZip).To(BeAnExistingFile()) 32 | 33 | referencePath = path.Join(testDir, "fixtures/zip/reference-example-cat.png") 34 | Expect(referencePath).To(BeAnExistingFile()) 35 | 36 | tmpOutputDir, _ = ioutil.TempDir("", "removeBG-*") 37 | outputPath = path.Join(tmpOutputDir, "cat-composite.png") 38 | }) 39 | 40 | AfterEach(func() { 41 | os.RemoveAll(tmpOutputDir) 42 | }) 43 | 44 | It("combines the color.jpg and alpha.png into a transparent PNG", func() { 45 | command := exec.Command(cliPath, "zip2png", exampleZip, outputPath) 46 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) 47 | Expect(err).ShouldNot(HaveOccurred()) 48 | 49 | Eventually(session, 30).Should(gexec.Exit()) 50 | 51 | Expect(session.ExitCode()).To(Equal(0)) 52 | Expect(session.Err).To(gbytes.Say("Processed zip")) 53 | Expect(outputPath).To(BeAnExistingFile()) 54 | 55 | outputSha := fileSha(outputPath) 56 | referenceSha := fileSha(referencePath) 57 | 58 | Expect(outputSha).To(Equal(referenceSha), "Expected output composite to match reference composite") 59 | }) 60 | }) 61 | 62 | func fileSha(filepath string) []byte { 63 | Expect(filepath).To(BeAnExistingFile()) 64 | 65 | f, err := os.Open(filepath) 66 | Expect(err).To(BeNil()) 67 | 68 | defer f.Close() 69 | 70 | h := sha256.New() 71 | _, err = io.Copy(h, f) 72 | 73 | Expect(err).To(BeNil()) 74 | 75 | return h.Sum(nil) 76 | } 77 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/remove-bg/go/cmd" 8 | ) 9 | 10 | var ( 11 | version = "dev" 12 | commit = "unknown" 13 | ) 14 | 15 | func main() { 16 | cmd.ConfigureVersion(version, commit) 17 | 18 | err := cmd.RootCmd.Execute() 19 | if err != nil { 20 | fmt.Printf("Error: %s\n", err) 21 | os.Exit(1) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /processor/determine_output_path.go: -------------------------------------------------------------------------------- 1 | package processor 2 | 3 | import ( 4 | "path" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | const defaultOutputExtension = ".png" 10 | 11 | func DetermineOutputPath(inputPath string, settings Settings) string { 12 | outputDirectory := settings.OutputDirectory 13 | inputDirectory, fileName := filepath.Split(inputPath) 14 | extensionlessFileName := strings.TrimSuffix(fileName, path.Ext(fileName)) 15 | outputExtension := defaultOutputExtension 16 | 17 | if len(settings.ImageSettings.OutputFormat) > 0 { 18 | outputExtension = "." + settings.ImageSettings.OutputFormat 19 | } 20 | 21 | if len(outputDirectory) == 0 { 22 | return filepath.Join(inputDirectory, extensionlessFileName+"-removebg"+outputExtension) 23 | } 24 | 25 | return filepath.Join(outputDirectory, extensionlessFileName+outputExtension) 26 | } 27 | -------------------------------------------------------------------------------- /processor/determine_output_path_test.go: -------------------------------------------------------------------------------- 1 | package processor_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | . "github.com/remove-bg/go/processor" 8 | ) 9 | 10 | var _ = Describe("DetermineOutputPath", func() { 11 | Context("when output path is set", func() { 12 | It("joins the original filename with the output path", func() { 13 | settings := Settings{ 14 | OutputDirectory: "out", 15 | } 16 | 17 | result := DetermineOutputPath("in/nested/image.jpg", settings) 18 | 19 | Expect(result).To(Equal("out/image.png")) 20 | }) 21 | }) 22 | 23 | Context("when the output path isn't set", func() { 24 | It("writes to the original directory with a filename suffix", func() { 25 | settings := Settings{ 26 | OutputDirectory: "", 27 | } 28 | 29 | result := DetermineOutputPath("in/nested/image.jpg", settings) 30 | 31 | Expect(result).To(Equal("in/nested/image-removebg.png")) 32 | }) 33 | }) 34 | 35 | Context("when the output format is set", func() { 36 | It("is used as the file extension", func() { 37 | settings := Settings{ 38 | OutputDirectory: "out", 39 | ImageSettings: ImageSettings{ 40 | OutputFormat: "jpg", 41 | }, 42 | } 43 | 44 | result := DetermineOutputPath("in/nested/image.jpg", settings) 45 | 46 | Expect(result).To(Equal("out/image.jpg")) 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /processor/notifier.go: -------------------------------------------------------------------------------- 1 | package processor 2 | 3 | import ( 4 | "fmt" 5 | "github.com/mattn/go-colorable" 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | //go:generate counterfeiter . NotifierInterface 10 | type NotifierInterface interface { 11 | Success(path string, imageNumber int, totalImages int) 12 | Skip(input string, existing string, imageNumber int, totalImages int) 13 | Error(err error, path string, imageNumber int, totalImages int) 14 | } 15 | 16 | type Notifier struct { 17 | Logger *logrus.Logger 18 | } 19 | 20 | func NewNotifier() Notifier { 21 | l := logrus.New() 22 | logrus.SetOutput(colorable.NewColorableStdout()) 23 | 24 | return Notifier{ 25 | Logger: l, 26 | } 27 | } 28 | 29 | func (n Notifier) Success(path string, imageNumber int, totalImages int) { 30 | n.Logger.WithFields(logrus.Fields{ 31 | "image": fmt.Sprintf("%d/%d", imageNumber, totalImages), 32 | "input": path, 33 | }).Info("Processed image") 34 | } 35 | 36 | func (n Notifier) Error(err error, path string, imageNumber int, totalImages int) { 37 | n.Logger.WithFields(logrus.Fields{ 38 | "image": fmt.Sprintf("%d/%d", imageNumber, totalImages), 39 | "input": path, 40 | }).Error(err) 41 | } 42 | 43 | func (n Notifier) Skip(input string, existing string, imageNumber int, totalImages int) { 44 | n.Logger.WithFields(logrus.Fields{ 45 | "image": fmt.Sprintf("%d/%d", imageNumber, totalImages), 46 | "input": input, 47 | "existing": existing, 48 | }).Warn("Skipped image") 49 | } 50 | -------------------------------------------------------------------------------- /processor/notifier_test.go: -------------------------------------------------------------------------------- 1 | package processor_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "errors" 8 | "github.com/sirupsen/logrus/hooks/test" 9 | 10 | . "github.com/remove-bg/go/processor" 11 | ) 12 | 13 | var _ = Describe("Notifier", func() { 14 | Describe("Success", func() { 15 | It("logs the image details", func() { 16 | logger, hook := test.NewNullLogger() 17 | subject := Notifier{ 18 | Logger: logger, 19 | } 20 | 21 | subject.Success("input/image.jpg", 1, 2) 22 | 23 | logged := hook.LastEntry() 24 | 25 | Expect(logged).ToNot(BeNil()) 26 | Expect(logged.Message).To(Equal("Processed image")) 27 | Expect(logged.Data["image"]).To(Equal("1/2")) 28 | Expect(logged.Data["input"]).To(Equal("input/image.jpg")) 29 | }) 30 | }) 31 | 32 | Describe("Skip", func() { 33 | It("logs the image details", func() { 34 | logger, hook := test.NewNullLogger() 35 | subject := Notifier{ 36 | Logger: logger, 37 | } 38 | 39 | subject.Skip("input/image.jpg", "output/image.png", 1, 2) 40 | 41 | logged := hook.LastEntry() 42 | 43 | Expect(logged).ToNot(BeNil()) 44 | Expect(logged.Message).To(Equal("Skipped image")) 45 | Expect(logged.Data["image"]).To(Equal("1/2")) 46 | Expect(logged.Data["input"]).To(Equal("input/image.jpg")) 47 | Expect(logged.Data["existing"]).To(Equal("output/image.png")) 48 | }) 49 | }) 50 | 51 | Describe("Error", func() { 52 | It("logs the error and image details", func() { 53 | logger, hook := test.NewNullLogger() 54 | subject := Notifier{ 55 | Logger: logger, 56 | } 57 | 58 | err := errors.New("boom") 59 | subject.Error(err, "input/image.jpg", 1, 2) 60 | 61 | logged := hook.LastEntry() 62 | 63 | Expect(logged).ToNot(BeNil()) 64 | Expect(logged.Message).To(Equal("boom")) 65 | Expect(logged.Data["image"]).To(Equal("1/2")) 66 | Expect(logged.Data["input"]).To(Equal("input/image.jpg")) 67 | }) 68 | }) 69 | 70 | Describe("NewNotifier", func() { 71 | It("builds a notifier", func() { 72 | n := NewNotifier() 73 | Expect(n.Logger).ToNot(BeNil()) 74 | }) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /processor/processor.go: -------------------------------------------------------------------------------- 1 | package processor 2 | 3 | import ( 4 | "fmt" 5 | "github.com/remove-bg/go/client" 6 | "github.com/remove-bg/go/composite" 7 | "github.com/remove-bg/go/storage" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "path/filepath" 14 | "strings" 15 | ) 16 | 17 | type Processor struct { 18 | APIKey string 19 | Client client.ClientInterface 20 | Storage storage.StorageInterface 21 | Prompt PromptInterface 22 | Notifier NotifierInterface 23 | Compositor composite.CompositorInterface 24 | } 25 | 26 | type Settings struct { 27 | OutputDirectory string 28 | ReprocessExisting bool 29 | SkipPngFormatOptimization bool 30 | LargeBatchConfirmThreshold int 31 | ImageSettings ImageSettings 32 | } 33 | 34 | type ImageSettings struct { 35 | Size string 36 | Type string 37 | Channels string 38 | BgColor string 39 | BgImageFile string 40 | OutputFormat string 41 | ExtraApiOptions string 42 | transferFormat string 43 | } 44 | 45 | func NewProcessor(apiKey string, version string) Processor { 46 | return Processor{ 47 | APIKey: apiKey, 48 | Client: client.Client{ 49 | Version: version, 50 | HTTPClient: http.Client{}, 51 | }, 52 | Storage: storage.FileStorage{}, 53 | Prompt: Prompt{}, 54 | Notifier: NewNotifier(), 55 | Compositor: composite.New(), 56 | } 57 | } 58 | 59 | func (p Processor) Process(rawInputPaths []string, settings Settings) { 60 | err := p.Storage.MkdirP(settings.OutputDirectory) 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | 65 | inputPaths, err := p.Storage.ExpandPaths(rawInputPaths) 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | 70 | confirmation := p.confirmLargeBatch(inputPaths, settings) 71 | if !confirmation { 72 | return 73 | } 74 | 75 | settings.setTransferFormat() 76 | 77 | totalImages := len(inputPaths) 78 | 79 | for index, inputPath := range inputPaths { 80 | outputPath := DetermineOutputPath(inputPath, settings) 81 | skipImage := p.Storage.FileExists(outputPath) && !settings.ReprocessExisting 82 | 83 | if skipImage { 84 | p.Notifier.Skip(inputPath, outputPath, index+1, totalImages) 85 | continue 86 | } 87 | 88 | err := p.processFile(inputPath, outputPath, settings.ImageSettings) 89 | 90 | if err == nil { 91 | p.Notifier.Success(inputPath, index+1, totalImages) 92 | } else { 93 | p.Notifier.Error(err, inputPath, index+1, totalImages) 94 | 95 | clientErr, ok := err.(*client.RequestError) 96 | if ok && clientErr.RateLimitExceeded() { 97 | return // Halt processing loop 98 | } 99 | } 100 | } 101 | } 102 | 103 | const FormatPng = "png" 104 | const FormatZip = "zip" 105 | const MimeZip = "application/zip" 106 | 107 | func (s *Settings) setTransferFormat() { 108 | // Save network bandwidth by requesting ZIP format (output will still be a PNG) 109 | if !s.SkipPngFormatOptimization && s.ImageSettings.OutputFormat == FormatPng { 110 | s.ImageSettings.transferFormat = FormatZip 111 | } else { 112 | s.ImageSettings.transferFormat = s.ImageSettings.OutputFormat 113 | } 114 | } 115 | 116 | func (is *ImageSettings) TransferFormat() string { 117 | return is.transferFormat 118 | } 119 | 120 | func (p Processor) processFile(inputPath string, outputPath string, imageSettings ImageSettings) error { 121 | params := imageSettingsToParams(imageSettings) 122 | processedBytes, contentType, err := p.Client.RemoveFromFile(inputPath, p.APIKey, params) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | if strings.Contains(contentType, MimeZip) { 128 | return p.processCompositeFile(outputPath, processedBytes) 129 | } else { 130 | return p.Storage.Write(outputPath, processedBytes) 131 | } 132 | } 133 | 134 | func imageSettingsToParams(imageSettings ImageSettings) map[string]string { 135 | // TODO: Tidyup with reflection / struct tags? 136 | params := map[string]string{} 137 | 138 | if len(imageSettings.Size) > 0 { 139 | params["size"] = imageSettings.Size 140 | } 141 | 142 | if len(imageSettings.Type) > 0 { 143 | params["type"] = imageSettings.Type 144 | } 145 | 146 | if len(imageSettings.Channels) > 0 { 147 | params["channels"] = imageSettings.Channels 148 | } 149 | 150 | if len(imageSettings.BgColor) > 0 { 151 | params["bg_color"] = imageSettings.BgColor 152 | } 153 | 154 | if len(imageSettings.BgImageFile) > 0 { 155 | params["bg_image_file"] = imageSettings.BgImageFile 156 | } 157 | 158 | if len(imageSettings.TransferFormat()) > 0 { 159 | params["format"] = imageSettings.TransferFormat() 160 | } 161 | 162 | if len(imageSettings.ExtraApiOptions) > 0 { 163 | values, err := url.ParseQuery(imageSettings.ExtraApiOptions) 164 | 165 | if err == nil { 166 | for key := range values { 167 | params[key] = values.Get(key) 168 | } 169 | } else { 170 | fmt.Printf("Unable to parse extra api options: %s\n", err) 171 | } 172 | } 173 | 174 | return params 175 | } 176 | 177 | func (p Processor) confirmLargeBatch(inputPaths []string, settings Settings) bool { 178 | batchSize := len(inputPaths) 179 | skipConfirm := settings.LargeBatchConfirmThreshold < 0 180 | 181 | if skipConfirm || batchSize < settings.LargeBatchConfirmThreshold { 182 | return true 183 | } 184 | 185 | return p.Prompt.ConfirmLargeBatch(batchSize) 186 | } 187 | 188 | func (p Processor) processCompositeFile(outputPath string, processedBytes []byte) error { 189 | file, err := ioutil.TempFile("", "removebg.*.zip") 190 | if err != nil { 191 | return err 192 | } 193 | 194 | defer os.Remove(file.Name()) 195 | 196 | _, err = file.Write(processedBytes) 197 | if err != nil { 198 | return err 199 | } 200 | 201 | // Convert output/foo.zip -> output/foo.png 202 | pngOutputPath := strings.TrimSuffix(outputPath, filepath.Ext(outputPath)) + ".png" 203 | 204 | return p.Compositor.Process(file.Name(), pngOutputPath) 205 | } 206 | -------------------------------------------------------------------------------- /processor/processor_suite_test.go: -------------------------------------------------------------------------------- 1 | package processor_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestProcessor(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Processor Suite") 13 | } 14 | -------------------------------------------------------------------------------- /processor/processor_test.go: -------------------------------------------------------------------------------- 1 | package processor_test 2 | 3 | import ( 4 | "errors" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | "github.com/remove-bg/go/client" 8 | "github.com/remove-bg/go/client/clientfakes" 9 | "github.com/remove-bg/go/composite/compositefakes" 10 | "github.com/remove-bg/go/processor" 11 | "github.com/remove-bg/go/processor/processorfakes" 12 | "github.com/remove-bg/go/storage/storagefakes" 13 | ) 14 | 15 | const mimePng = "image/png" 16 | 17 | var _ = Describe("Processor", func() { 18 | var ( 19 | fakeClient *clientfakes.FakeClientInterface 20 | fakeStorage *storagefakes.FakeStorageInterface 21 | fakePrompt *processorfakes.FakePromptInterface 22 | fakeNotifier *processorfakes.FakeNotifierInterface 23 | fakeCompositor *compositefakes.FakeCompositorInterface 24 | subject processor.Processor 25 | testSettings processor.Settings 26 | ) 27 | 28 | BeforeEach(func() { 29 | fakeClient = &clientfakes.FakeClientInterface{} 30 | fakeStorage = &storagefakes.FakeStorageInterface{} 31 | fakePrompt = &processorfakes.FakePromptInterface{} 32 | fakeNotifier = &processorfakes.FakeNotifierInterface{} 33 | fakeCompositor = &compositefakes.FakeCompositorInterface{} 34 | fakePrompt.ConfirmLargeBatchReturns(true) 35 | fakeStorage.ExpandPathsStub = func(input []string) ([]string, error) { 36 | return input, nil 37 | } 38 | 39 | subject = processor.Processor{ 40 | APIKey: "api-key", 41 | Client: fakeClient, 42 | Storage: fakeStorage, 43 | Prompt: fakePrompt, 44 | Notifier: fakeNotifier, 45 | Compositor: fakeCompositor, 46 | } 47 | 48 | testSettings = processor.Settings{ 49 | OutputDirectory: "output-dir", 50 | LargeBatchConfirmThreshold: 50, 51 | ReprocessExisting: false, 52 | SkipPngFormatOptimization: false, 53 | } 54 | }) 55 | 56 | It("expands globs in the input paths", func() { 57 | fakeStorage.ExpandPathsStub = func(input []string) ([]string, error) { 58 | return []string{"dir/image1.jpg"}, nil 59 | } 60 | 61 | subject.Process([]string{"dir/*.jpg"}, testSettings) 62 | 63 | Expect(fakeClient.RemoveFromFileCallCount()).To(Equal(1)) 64 | Expect(fakeStorage.ExpandPathsCallCount()).To(Equal(1)) 65 | 66 | clientArg1, _, _ := fakeClient.RemoveFromFileArgsForCall(0) 67 | Expect(clientArg1).To(Equal("dir/image1.jpg")) 68 | }) 69 | 70 | It("create the output directory", func() { 71 | subject.Process([]string{"dir/*.jpg"}, testSettings) 72 | 73 | Expect(fakeStorage.MkdirPCallCount()).To(Equal(1)) 74 | Expect(fakeStorage.MkdirPArgsForCall(0)).To(Equal(testSettings.OutputDirectory)) 75 | }) 76 | 77 | It("coordinates the HTTP request and writing the result", func() { 78 | fakeClient.RemoveFromFileReturnsOnCall(0, []byte("Processed1"), mimePng, nil) 79 | fakeClient.RemoveFromFileReturnsOnCall(1, []byte("Processed2"), mimePng, nil) 80 | 81 | inputPaths := []string{"dir/image1.jpg", "dir/image2.jpg"} 82 | 83 | subject.Process(inputPaths, testSettings) 84 | 85 | Expect(fakeClient.RemoveFromFileCallCount()).To(Equal(2)) 86 | 87 | clientArg1, clientArg2, params := fakeClient.RemoveFromFileArgsForCall(0) 88 | Expect(clientArg1).To(Equal("dir/image1.jpg")) 89 | Expect(clientArg2).To(Equal("api-key")) 90 | Expect(len(params)).To(Equal(0)) 91 | 92 | Expect(fakeStorage.WriteCallCount()).To(Equal(2)) 93 | 94 | writerArg1, writerArg2 := fakeStorage.WriteArgsForCall(0) 95 | Expect(writerArg1).To(Equal("output-dir/image1.png")) 96 | Expect(writerArg2).To(Equal([]byte("Processed1"))) 97 | }) 98 | 99 | Context("zip format requested", func() { 100 | It("delegates to the compositor", func() { 101 | fakeCompositor.ProcessReturns(nil) 102 | fakeClient.RemoveFromFileReturnsOnCall(0, []byte("Zip1"), processor.MimeZip, nil) 103 | fakeClient.RemoveFromFileReturnsOnCall(1, []byte("Zip2"), processor.MimeZip, nil) 104 | 105 | inputPaths := []string{"dir/image1.jpg", "dir/image2.jpg"} 106 | testSettings.OutputDirectory = "out-dir" 107 | testSettings.ImageSettings.OutputFormat = processor.FormatZip 108 | 109 | subject.Process(inputPaths, testSettings) 110 | 111 | Expect(fakeCompositor.ProcessCallCount()).To(Equal(2)) 112 | 113 | zipFileName, outputPath := fakeCompositor.ProcessArgsForCall(0) 114 | Expect(zipFileName).To(ContainSubstring(".zip")) 115 | Expect(outputPath).To(Equal("out-dir/image1.png")) 116 | }) 117 | }) 118 | 119 | Context("png format requested", func() { 120 | BeforeEach(func() { 121 | fakeCompositor.ProcessReturns(nil) 122 | fakeClient.RemoveFromFileReturnsOnCall(0, []byte("Zip1"), processor.MimeZip, nil) 123 | fakeClient.RemoveFromFileReturnsOnCall(1, []byte("Zip2"), processor.MimeZip, nil) 124 | }) 125 | 126 | It("upgrades the format to zip behind the scenes", func() { 127 | inputPaths := []string{"dir/image1.jpg", "dir/image2.jpg"} 128 | testSettings.OutputDirectory = "out-dir" 129 | testSettings.ImageSettings.OutputFormat = processor.FormatPng 130 | 131 | subject.Process(inputPaths, testSettings) 132 | 133 | Expect(fakeClient.RemoveFromFileCallCount()).To(Equal(2)) 134 | _, _, params := fakeClient.RemoveFromFileArgsForCall(0) 135 | Expect(params["format"]).To(Equal(processor.FormatZip)) 136 | }) 137 | 138 | It("delegates to the compositor", func() { 139 | inputPaths := []string{"dir/image1.jpg", "dir/image2.jpg"} 140 | testSettings.OutputDirectory = "out-dir" 141 | testSettings.ImageSettings.OutputFormat = processor.FormatPng 142 | 143 | subject.Process(inputPaths, testSettings) 144 | 145 | Expect(fakeCompositor.ProcessCallCount()).To(Equal(2)) 146 | 147 | zipFileName, outputPath := fakeCompositor.ProcessArgsForCall(0) 148 | Expect(zipFileName).To(ContainSubstring(".zip")) 149 | Expect(outputPath).To(Equal("out-dir/image1.png")) 150 | }) 151 | 152 | It("allows the optimization to be skipped", func() { 153 | inputPaths := []string{"dir/image1.jpg", "dir/image2.jpg"} 154 | testSettings.OutputDirectory = "out-dir" 155 | testSettings.ImageSettings.OutputFormat = processor.FormatPng 156 | testSettings.SkipPngFormatOptimization = true 157 | 158 | subject.Process(inputPaths, testSettings) 159 | 160 | Expect(fakeClient.RemoveFromFileCallCount()).To(Equal(2)) 161 | _, _, params := fakeClient.RemoveFromFileArgsForCall(0) 162 | Expect(params["format"]).To(Equal(processor.FormatPng)) 163 | }) 164 | 165 | It("skips processing if the output PNG file exists", func() { 166 | testSettings.OutputDirectory = "" 167 | testSettings.ImageSettings.OutputFormat = processor.FormatPng 168 | 169 | inputPaths := []string{"dir/image1.jpg"} 170 | fakeStorage.FileExistsReturnsOnCall(0, true) 171 | 172 | subject.Process(inputPaths, testSettings) 173 | 174 | Expect(fakeNotifier.SkipCallCount()).To(Equal(1)) 175 | Expect(fakeClient.RemoveFromFileCallCount()).To(Equal(0)) 176 | Expect(fakeStorage.FileExistsArgsForCall(0)).To(Equal("dir/image1-removebg.png")) 177 | }) 178 | }) 179 | 180 | Describe("image options", func() { 181 | It("passes non-empty image options to the client", func() { 182 | fakeClient.RemoveFromFileReturnsOnCall(0, []byte("Processed1"), mimePng, nil) 183 | inputPaths := []string{"dir/image1.jpg"} 184 | 185 | testSettings.ImageSettings = processor.ImageSettings{ 186 | Size: "size-value", 187 | Type: "type-value", 188 | Channels: "channels-value", 189 | BgColor: "bg-color-value", 190 | BgImageFile: "bg-image-file-value", 191 | OutputFormat: "format-value", 192 | } 193 | 194 | subject.Process(inputPaths, testSettings) 195 | 196 | Expect(fakeClient.RemoveFromFileCallCount()).To(Equal(1)) 197 | _, _, params := fakeClient.RemoveFromFileArgsForCall(0) 198 | 199 | Expect(params["size"]).To(Equal("size-value")) 200 | Expect(params["type"]).To(Equal("type-value")) 201 | Expect(params["channels"]).To(Equal("channels-value")) 202 | Expect(params["bg_color"]).To(Equal("bg-color-value")) 203 | Expect(params["bg_image_file"]).To(Equal("bg-image-file-value")) 204 | Expect(params["format"]).To(Equal("format-value")) 205 | }) 206 | 207 | It("parses any extra API options into params", func() { 208 | fakeClient.RemoveFromFileReturnsOnCall(0, []byte("Processed1"), mimePng, nil) 209 | inputPaths := []string{"dir/image1.jpg"} 210 | 211 | testSettings.ImageSettings = processor.ImageSettings{ 212 | Size: "size-value", 213 | ExtraApiOptions: "option1=val1&option2=val2", 214 | } 215 | 216 | subject.Process(inputPaths, testSettings) 217 | 218 | Expect(fakeClient.RemoveFromFileCallCount()).To(Equal(1)) 219 | _, _, params := fakeClient.RemoveFromFileArgsForCall(0) 220 | 221 | Expect(params["size"]).To(Equal("size-value")) 222 | Expect(params["option1"]).To(Equal("val1")) 223 | Expect(params["option2"]).To(Equal("val2")) 224 | }) 225 | }) 226 | 227 | Context("client error", func() { 228 | It("keeps processing images", func() { 229 | fakeClient.RemoveFromFileReturnsOnCall(0, nil, "", errors.New("boom")) 230 | fakeClient.RemoveFromFileReturnsOnCall(1, []byte("Processed2"), mimePng, nil) 231 | inputPaths := []string{"dir/image1.jpg", "dir/image2.jpg"} 232 | 233 | subject.Process(inputPaths, testSettings) 234 | 235 | Expect(fakeClient.RemoveFromFileCallCount()).To(Equal(2)) 236 | Expect(fakeStorage.WriteCallCount()).To(Equal(1)) 237 | Expect(fakeNotifier.ErrorCallCount()).To(Equal(1)) 238 | Expect(fakeNotifier.SuccessCallCount()).To(Equal(1)) 239 | 240 | _, writerArg2 := fakeStorage.WriteArgsForCall(0) 241 | Expect(writerArg2).To(Equal([]byte("Processed2"))) 242 | }) 243 | 244 | Context("rate limit exceeded", func() { 245 | It("stops processing images", func() { 246 | rateLimitedExceeded := client.RequestError{ 247 | StatusCode: 429, 248 | Err: errors.New("rate limit exceeded"), 249 | } 250 | 251 | fakeClient.RemoveFromFileReturnsOnCall(0, nil, "", &rateLimitedExceeded) 252 | inputPaths := []string{"dir/image1.jpg", "dir/image2.jpg"} 253 | 254 | subject.Process(inputPaths, testSettings) 255 | 256 | Expect(fakeClient.RemoveFromFileCallCount()).To(Equal(1)) 257 | Expect(fakeNotifier.ErrorCallCount()).To(Equal(1)) 258 | Expect(fakeStorage.WriteCallCount()).To(Equal(0)) 259 | }) 260 | }) 261 | 262 | It("passes the error details to the notifier", func() { 263 | err := errors.New("boom") 264 | fakeClient.RemoveFromFileReturnsOnCall(0, nil, "", err) 265 | fakeClient.RemoveFromFileReturnsOnCall(1, []byte("Processed2"), mimePng, nil) 266 | inputPaths := []string{"dir/image1.jpg", "dir/image2.jpg"} 267 | 268 | subject.Process(inputPaths, testSettings) 269 | 270 | Expect(fakeNotifier.ErrorCallCount()).To(Equal(1)) 271 | 272 | notifiedErr, notifiedPath, notifiedImageNumber, notifiedTotal := fakeNotifier.ErrorArgsForCall(0) 273 | 274 | Expect(notifiedErr).To(Equal(err)) 275 | Expect(notifiedPath).To(Equal("dir/image1.jpg")) 276 | Expect(notifiedImageNumber).To(Equal(1)) 277 | Expect(notifiedTotal).To(Equal(2)) 278 | }) 279 | }) 280 | 281 | Context("writer error", func() { 282 | It("keeps processing images", func() { 283 | fakeClient.RemoveFromFileReturnsOnCall(0, []byte("Processed1"), mimePng, nil) 284 | fakeClient.RemoveFromFileReturnsOnCall(1, []byte("Processed2"), mimePng, nil) 285 | fakeStorage.WriteReturnsOnCall(0, errors.New("boom")) 286 | inputPaths := []string{"dir/image1.jpg", "dir/image2.jpg"} 287 | 288 | subject.Process(inputPaths, testSettings) 289 | 290 | Expect(fakeClient.RemoveFromFileCallCount()).To(Equal(2)) 291 | Expect(fakeNotifier.ErrorCallCount()).To(Equal(1)) 292 | Expect(fakeNotifier.SuccessCallCount()).To(Equal(1)) 293 | }) 294 | 295 | It("passes the error details to the notifier", func() { 296 | err := errors.New("boom") 297 | fakeClient.RemoveFromFileReturnsOnCall(0, []byte("Processed1"), mimePng, nil) 298 | fakeClient.RemoveFromFileReturnsOnCall(1, []byte("Processed2"), mimePng, nil) 299 | fakeStorage.WriteReturnsOnCall(0, err) 300 | inputPaths := []string{"dir/image1.jpg", "dir/image2.jpg"} 301 | 302 | subject.Process(inputPaths, testSettings) 303 | 304 | Expect(fakeNotifier.ErrorCallCount()).To(Equal(1)) 305 | 306 | notifiedErr, notifiedPath, notifiedImageNumber, notifiedTotal := fakeNotifier.ErrorArgsForCall(0) 307 | 308 | Expect(notifiedErr).To(Equal(err)) 309 | Expect(notifiedPath).To(Equal("dir/image1.jpg")) 310 | Expect(notifiedImageNumber).To(Equal(1)) 311 | Expect(notifiedTotal).To(Equal(2)) 312 | }) 313 | }) 314 | 315 | Describe("skipping already processed files", func() { 316 | It("skips processing if the output file exists", func() { 317 | testSettings.OutputDirectory = "" 318 | inputPaths := []string{"dir/image1.jpg", "dir/image2.jpg"} 319 | fakeStorage.FileExistsReturnsOnCall(0, true) 320 | fakeStorage.FileExistsReturnsOnCall(1, false) 321 | 322 | subject.Process(inputPaths, testSettings) 323 | 324 | Expect(fakeNotifier.SkipCallCount()).To(Equal(1)) 325 | Expect(fakeClient.RemoveFromFileCallCount()).To(Equal(1)) 326 | 327 | notifiedInput, notifiedOutput, notifiedImageNumber, notifiedTotal := fakeNotifier.SkipArgsForCall(0) 328 | Expect(notifiedInput).To(Equal("dir/image1.jpg")) 329 | Expect(notifiedOutput).To(Equal("dir/image1-removebg.png")) 330 | Expect(notifiedImageNumber).To(Equal(1)) 331 | Expect(notifiedTotal).To(Equal(2)) 332 | 333 | processedPath, _, _ := fakeClient.RemoveFromFileArgsForCall(0) 334 | Expect(processedPath).To(Equal("dir/image2.jpg")) 335 | }) 336 | 337 | It("can be configured to force re-processing of existing files", func() { 338 | testSettings.ReprocessExisting = true 339 | fakeStorage.FileExistsReturns(true) 340 | 341 | subject.Process([]string{"dir/image1.jpg"}, testSettings) 342 | 343 | Expect(fakeClient.RemoveFromFileCallCount()).To(Equal(1)) 344 | Expect(fakeStorage.WriteCallCount()).To(Equal(1)) 345 | Expect(fakeNotifier.SuccessCallCount()).To(Equal(1)) 346 | }) 347 | }) 348 | 349 | Describe("large batch confirmation", func() { 350 | It("doesn't prompt under the limit", func() { 351 | inputPaths := []string{"dir/image1.jpg"} 352 | testSettings.LargeBatchConfirmThreshold = 50 353 | 354 | subject.Process(inputPaths, testSettings) 355 | 356 | Expect(fakePrompt.ConfirmLargeBatchCallCount()).To(Equal(0)) 357 | }) 358 | 359 | It("delegates to the prompt", func() { 360 | fakePrompt.ConfirmLargeBatchReturns(true) 361 | inputPaths := make([]string, 50) 362 | testSettings.LargeBatchConfirmThreshold = 50 363 | 364 | subject.Process(inputPaths, testSettings) 365 | 366 | Expect(fakePrompt.ConfirmLargeBatchCallCount()).To(Equal(1)) 367 | Expect(fakeClient.RemoveFromFileCallCount()).To(Equal(50)) 368 | }) 369 | 370 | It("can be skipped with a negative value", func() { 371 | inputPaths := make([]string, 50) 372 | testSettings.LargeBatchConfirmThreshold = -1 373 | 374 | subject.Process(inputPaths, testSettings) 375 | 376 | Expect(fakePrompt.ConfirmLargeBatchCallCount()).To(Equal(0)) 377 | Expect(fakeClient.RemoveFromFileCallCount()).To(Equal(50)) 378 | }) 379 | 380 | It("can allows configuration of the threshold", func() { 381 | fakePrompt.ConfirmLargeBatchReturns(true) 382 | inputPaths := make([]string, 25) 383 | testSettings.LargeBatchConfirmThreshold = 25 384 | 385 | subject.Process(inputPaths, testSettings) 386 | 387 | Expect(fakePrompt.ConfirmLargeBatchCallCount()).To(Equal(1)) 388 | Expect(fakeClient.RemoveFromFileCallCount()).To(Equal(25)) 389 | }) 390 | 391 | It("doesn't process if the confirmation is rejected", func() { 392 | fakePrompt.ConfirmLargeBatchReturns(false) 393 | inputPaths := make([]string, 50) 394 | testSettings.LargeBatchConfirmThreshold = 50 395 | 396 | subject.Process(inputPaths, testSettings) 397 | 398 | Expect(fakePrompt.ConfirmLargeBatchCallCount()).To(Equal(1)) 399 | Expect(fakeClient.RemoveFromFileCallCount()).To(Equal(0)) 400 | }) 401 | }) 402 | 403 | Describe("NewProcessor", func() { 404 | It("builds a processor", func() { 405 | p := processor.NewProcessor("api-key", "1.0.0") 406 | 407 | Expect(p.APIKey).To(Equal("api-key")) 408 | Expect(p.Client).ToNot(BeNil()) 409 | Expect(p.Storage).ToNot(BeNil()) 410 | Expect(p.Prompt).ToNot(BeNil()) 411 | Expect(p.Notifier).ToNot(BeNil()) 412 | Expect(p.Compositor).ToNot(BeNil()) 413 | }) 414 | }) 415 | }) 416 | -------------------------------------------------------------------------------- /processor/processorfakes/fake_notifier_interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package processorfakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/remove-bg/go/processor" 8 | ) 9 | 10 | type FakeNotifierInterface struct { 11 | ErrorStub func(error, string, int, int) 12 | errorMutex sync.RWMutex 13 | errorArgsForCall []struct { 14 | arg1 error 15 | arg2 string 16 | arg3 int 17 | arg4 int 18 | } 19 | SkipStub func(string, string, int, int) 20 | skipMutex sync.RWMutex 21 | skipArgsForCall []struct { 22 | arg1 string 23 | arg2 string 24 | arg3 int 25 | arg4 int 26 | } 27 | SuccessStub func(string, int, int) 28 | successMutex sync.RWMutex 29 | successArgsForCall []struct { 30 | arg1 string 31 | arg2 int 32 | arg3 int 33 | } 34 | invocations map[string][][]interface{} 35 | invocationsMutex sync.RWMutex 36 | } 37 | 38 | func (fake *FakeNotifierInterface) Error(arg1 error, arg2 string, arg3 int, arg4 int) { 39 | fake.errorMutex.Lock() 40 | fake.errorArgsForCall = append(fake.errorArgsForCall, struct { 41 | arg1 error 42 | arg2 string 43 | arg3 int 44 | arg4 int 45 | }{arg1, arg2, arg3, arg4}) 46 | fake.recordInvocation("Error", []interface{}{arg1, arg2, arg3, arg4}) 47 | fake.errorMutex.Unlock() 48 | if fake.ErrorStub != nil { 49 | fake.ErrorStub(arg1, arg2, arg3, arg4) 50 | } 51 | } 52 | 53 | func (fake *FakeNotifierInterface) ErrorCallCount() int { 54 | fake.errorMutex.RLock() 55 | defer fake.errorMutex.RUnlock() 56 | return len(fake.errorArgsForCall) 57 | } 58 | 59 | func (fake *FakeNotifierInterface) ErrorCalls(stub func(error, string, int, int)) { 60 | fake.errorMutex.Lock() 61 | defer fake.errorMutex.Unlock() 62 | fake.ErrorStub = stub 63 | } 64 | 65 | func (fake *FakeNotifierInterface) ErrorArgsForCall(i int) (error, string, int, int) { 66 | fake.errorMutex.RLock() 67 | defer fake.errorMutex.RUnlock() 68 | argsForCall := fake.errorArgsForCall[i] 69 | return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 70 | } 71 | 72 | func (fake *FakeNotifierInterface) Skip(arg1 string, arg2 string, arg3 int, arg4 int) { 73 | fake.skipMutex.Lock() 74 | fake.skipArgsForCall = append(fake.skipArgsForCall, struct { 75 | arg1 string 76 | arg2 string 77 | arg3 int 78 | arg4 int 79 | }{arg1, arg2, arg3, arg4}) 80 | fake.recordInvocation("Skip", []interface{}{arg1, arg2, arg3, arg4}) 81 | fake.skipMutex.Unlock() 82 | if fake.SkipStub != nil { 83 | fake.SkipStub(arg1, arg2, arg3, arg4) 84 | } 85 | } 86 | 87 | func (fake *FakeNotifierInterface) SkipCallCount() int { 88 | fake.skipMutex.RLock() 89 | defer fake.skipMutex.RUnlock() 90 | return len(fake.skipArgsForCall) 91 | } 92 | 93 | func (fake *FakeNotifierInterface) SkipCalls(stub func(string, string, int, int)) { 94 | fake.skipMutex.Lock() 95 | defer fake.skipMutex.Unlock() 96 | fake.SkipStub = stub 97 | } 98 | 99 | func (fake *FakeNotifierInterface) SkipArgsForCall(i int) (string, string, int, int) { 100 | fake.skipMutex.RLock() 101 | defer fake.skipMutex.RUnlock() 102 | argsForCall := fake.skipArgsForCall[i] 103 | return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 104 | } 105 | 106 | func (fake *FakeNotifierInterface) Success(arg1 string, arg2 int, arg3 int) { 107 | fake.successMutex.Lock() 108 | fake.successArgsForCall = append(fake.successArgsForCall, struct { 109 | arg1 string 110 | arg2 int 111 | arg3 int 112 | }{arg1, arg2, arg3}) 113 | fake.recordInvocation("Success", []interface{}{arg1, arg2, arg3}) 114 | fake.successMutex.Unlock() 115 | if fake.SuccessStub != nil { 116 | fake.SuccessStub(arg1, arg2, arg3) 117 | } 118 | } 119 | 120 | func (fake *FakeNotifierInterface) SuccessCallCount() int { 121 | fake.successMutex.RLock() 122 | defer fake.successMutex.RUnlock() 123 | return len(fake.successArgsForCall) 124 | } 125 | 126 | func (fake *FakeNotifierInterface) SuccessCalls(stub func(string, int, int)) { 127 | fake.successMutex.Lock() 128 | defer fake.successMutex.Unlock() 129 | fake.SuccessStub = stub 130 | } 131 | 132 | func (fake *FakeNotifierInterface) SuccessArgsForCall(i int) (string, int, int) { 133 | fake.successMutex.RLock() 134 | defer fake.successMutex.RUnlock() 135 | argsForCall := fake.successArgsForCall[i] 136 | return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 137 | } 138 | 139 | func (fake *FakeNotifierInterface) Invocations() map[string][][]interface{} { 140 | fake.invocationsMutex.RLock() 141 | defer fake.invocationsMutex.RUnlock() 142 | fake.errorMutex.RLock() 143 | defer fake.errorMutex.RUnlock() 144 | fake.skipMutex.RLock() 145 | defer fake.skipMutex.RUnlock() 146 | fake.successMutex.RLock() 147 | defer fake.successMutex.RUnlock() 148 | copiedInvocations := map[string][][]interface{}{} 149 | for key, value := range fake.invocations { 150 | copiedInvocations[key] = value 151 | } 152 | return copiedInvocations 153 | } 154 | 155 | func (fake *FakeNotifierInterface) recordInvocation(key string, args []interface{}) { 156 | fake.invocationsMutex.Lock() 157 | defer fake.invocationsMutex.Unlock() 158 | if fake.invocations == nil { 159 | fake.invocations = map[string][][]interface{}{} 160 | } 161 | if fake.invocations[key] == nil { 162 | fake.invocations[key] = [][]interface{}{} 163 | } 164 | fake.invocations[key] = append(fake.invocations[key], args) 165 | } 166 | 167 | var _ processor.NotifierInterface = new(FakeNotifierInterface) 168 | -------------------------------------------------------------------------------- /processor/processorfakes/fake_prompt_interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package processorfakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/remove-bg/go/processor" 8 | ) 9 | 10 | type FakePromptInterface struct { 11 | ConfirmLargeBatchStub func(int) bool 12 | confirmLargeBatchMutex sync.RWMutex 13 | confirmLargeBatchArgsForCall []struct { 14 | arg1 int 15 | } 16 | confirmLargeBatchReturns struct { 17 | result1 bool 18 | } 19 | confirmLargeBatchReturnsOnCall map[int]struct { 20 | result1 bool 21 | } 22 | invocations map[string][][]interface{} 23 | invocationsMutex sync.RWMutex 24 | } 25 | 26 | func (fake *FakePromptInterface) ConfirmLargeBatch(arg1 int) bool { 27 | fake.confirmLargeBatchMutex.Lock() 28 | ret, specificReturn := fake.confirmLargeBatchReturnsOnCall[len(fake.confirmLargeBatchArgsForCall)] 29 | fake.confirmLargeBatchArgsForCall = append(fake.confirmLargeBatchArgsForCall, struct { 30 | arg1 int 31 | }{arg1}) 32 | fake.recordInvocation("ConfirmLargeBatch", []interface{}{arg1}) 33 | fake.confirmLargeBatchMutex.Unlock() 34 | if fake.ConfirmLargeBatchStub != nil { 35 | return fake.ConfirmLargeBatchStub(arg1) 36 | } 37 | if specificReturn { 38 | return ret.result1 39 | } 40 | fakeReturns := fake.confirmLargeBatchReturns 41 | return fakeReturns.result1 42 | } 43 | 44 | func (fake *FakePromptInterface) ConfirmLargeBatchCallCount() int { 45 | fake.confirmLargeBatchMutex.RLock() 46 | defer fake.confirmLargeBatchMutex.RUnlock() 47 | return len(fake.confirmLargeBatchArgsForCall) 48 | } 49 | 50 | func (fake *FakePromptInterface) ConfirmLargeBatchCalls(stub func(int) bool) { 51 | fake.confirmLargeBatchMutex.Lock() 52 | defer fake.confirmLargeBatchMutex.Unlock() 53 | fake.ConfirmLargeBatchStub = stub 54 | } 55 | 56 | func (fake *FakePromptInterface) ConfirmLargeBatchArgsForCall(i int) int { 57 | fake.confirmLargeBatchMutex.RLock() 58 | defer fake.confirmLargeBatchMutex.RUnlock() 59 | argsForCall := fake.confirmLargeBatchArgsForCall[i] 60 | return argsForCall.arg1 61 | } 62 | 63 | func (fake *FakePromptInterface) ConfirmLargeBatchReturns(result1 bool) { 64 | fake.confirmLargeBatchMutex.Lock() 65 | defer fake.confirmLargeBatchMutex.Unlock() 66 | fake.ConfirmLargeBatchStub = nil 67 | fake.confirmLargeBatchReturns = struct { 68 | result1 bool 69 | }{result1} 70 | } 71 | 72 | func (fake *FakePromptInterface) ConfirmLargeBatchReturnsOnCall(i int, result1 bool) { 73 | fake.confirmLargeBatchMutex.Lock() 74 | defer fake.confirmLargeBatchMutex.Unlock() 75 | fake.ConfirmLargeBatchStub = nil 76 | if fake.confirmLargeBatchReturnsOnCall == nil { 77 | fake.confirmLargeBatchReturnsOnCall = make(map[int]struct { 78 | result1 bool 79 | }) 80 | } 81 | fake.confirmLargeBatchReturnsOnCall[i] = struct { 82 | result1 bool 83 | }{result1} 84 | } 85 | 86 | func (fake *FakePromptInterface) Invocations() map[string][][]interface{} { 87 | fake.invocationsMutex.RLock() 88 | defer fake.invocationsMutex.RUnlock() 89 | fake.confirmLargeBatchMutex.RLock() 90 | defer fake.confirmLargeBatchMutex.RUnlock() 91 | copiedInvocations := map[string][][]interface{}{} 92 | for key, value := range fake.invocations { 93 | copiedInvocations[key] = value 94 | } 95 | return copiedInvocations 96 | } 97 | 98 | func (fake *FakePromptInterface) recordInvocation(key string, args []interface{}) { 99 | fake.invocationsMutex.Lock() 100 | defer fake.invocationsMutex.Unlock() 101 | if fake.invocations == nil { 102 | fake.invocations = map[string][][]interface{}{} 103 | } 104 | if fake.invocations[key] == nil { 105 | fake.invocations[key] = [][]interface{}{} 106 | } 107 | fake.invocations[key] = append(fake.invocations[key], args) 108 | } 109 | 110 | var _ processor.PromptInterface = new(FakePromptInterface) 111 | -------------------------------------------------------------------------------- /processor/prompt.go: -------------------------------------------------------------------------------- 1 | package processor 2 | 3 | import ( 4 | "fmt" 5 | "gopkg.in/AlecAivazis/survey.v1" 6 | ) 7 | 8 | //go:generate counterfeiter . PromptInterface 9 | type PromptInterface interface { 10 | ConfirmLargeBatch(size int) bool 11 | } 12 | 13 | type Prompt struct { 14 | } 15 | 16 | func (Prompt) ConfirmLargeBatch(size int) bool { 17 | confirmation := false 18 | message := fmt.Sprintf("Do you want to process %d images?", size) 19 | prompt := &survey.Confirm{ 20 | Message: message, 21 | } 22 | survey.AskOne(prompt, &confirmation, nil) 23 | return confirmation 24 | } 25 | -------------------------------------------------------------------------------- /storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/bmatcuk/doublestar" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | //go:generate counterfeiter . StorageInterface 10 | type StorageInterface interface { 11 | Write(path string, data []byte) error 12 | FileExists(path string) bool 13 | ExpandPaths(originalPaths []string) ([]string, error) 14 | MkdirP(path string) error 15 | } 16 | 17 | type FileStorage struct { 18 | } 19 | 20 | func (FileStorage) Write(path string, data []byte) error { 21 | out, _ := os.Create(path) 22 | defer out.Close() 23 | 24 | _, err := out.Write(data) 25 | return err 26 | } 27 | 28 | func (FileStorage) FileExists(path string) bool { 29 | if _, err := os.Stat(path); os.IsNotExist(err) { 30 | return false 31 | } 32 | 33 | return true 34 | } 35 | 36 | func (FileStorage) ExpandPaths(originalPaths []string) ([]string, error) { 37 | resolvedPaths := []string{} 38 | 39 | for _, originalPath := range originalPaths { 40 | if !strings.Contains(originalPath, "*") { 41 | resolvedPaths = append(resolvedPaths, originalPath) 42 | continue 43 | } 44 | 45 | expanded, err := doublestar.Glob(originalPath) 46 | 47 | if err != nil { 48 | return []string{}, err 49 | } else { 50 | resolvedPaths = append(resolvedPaths, expanded...) 51 | } 52 | } 53 | 54 | return resolvedPaths, nil 55 | } 56 | 57 | func (FileStorage) MkdirP(path string) error { 58 | if len(path) == 0 { 59 | return nil 60 | } 61 | 62 | return os.MkdirAll(path, 0755) 63 | } 64 | -------------------------------------------------------------------------------- /storage/storage_suite_test.go: -------------------------------------------------------------------------------- 1 | package storage_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestClient(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Storage Suite") 13 | } 14 | -------------------------------------------------------------------------------- /storage/storage_test.go: -------------------------------------------------------------------------------- 1 | package storage_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | "runtime" 10 | 11 | . "github.com/remove-bg/go/storage" 12 | ) 13 | 14 | var _ = Describe("FileStorage", func() { 15 | var ( 16 | subject FileStorage 17 | testDir string 18 | ) 19 | 20 | BeforeEach(func() { 21 | subject = FileStorage{} 22 | 23 | _, testFile, _, _ := runtime.Caller(0) 24 | testDir = path.Dir(testFile) 25 | }) 26 | 27 | Describe("FileExists", func() { 28 | It("is true when the file is present", func() { 29 | fixtureFile := path.Join(testDir, "../fixtures/person-in-field.jpg") 30 | 31 | Expect(subject.FileExists(fixtureFile)).To(BeTrue()) 32 | }) 33 | 34 | It("is false when the file doesn't exist", func() { 35 | missing := path.Join(testDir, "../fixtures/missing.jpg") 36 | 37 | Expect(subject.FileExists(missing)).To(BeFalse()) 38 | }) 39 | 40 | It("is false when the directory doesn't exist", func() { 41 | missingDir := path.Join(testDir, "../missing") 42 | 43 | Expect(subject.FileExists(missingDir)).To(BeFalse()) 44 | }) 45 | }) 46 | 47 | Describe("ExpandPaths", func() { 48 | It("expands any star (*) globs in the inputs paths", func() { 49 | glob := path.Join(testDir, "../fixtures/*.jpg") 50 | expanded, err := subject.ExpandPaths([]string{glob}) 51 | 52 | Expect(err).ToNot(HaveOccurred()) 53 | Expect(expanded).To(ContainElement(MatchRegexp(`fixtures\/person-in-field\.jpg$`))) 54 | }) 55 | 56 | It("expands any double-star (**) globs in the input paths", func() { 57 | glob := path.Join(testDir, "../fixtures/**/*.png") 58 | expanded, err := subject.ExpandPaths([]string{glob}) 59 | 60 | Expect(err).ToNot(HaveOccurred()) 61 | Expect(expanded).To(ContainElement(MatchRegexp(`nested\/plant\.png$`))) 62 | }) 63 | 64 | It("expands any alternative patterns in the input paths", func() { 65 | glob := path.Join(testDir, "../fixtures/**/*.{jpg,png}") 66 | expanded, err := subject.ExpandPaths([]string{glob}) 67 | 68 | Expect(err).ToNot(HaveOccurred()) 69 | Expect(expanded).To(ContainElement(MatchRegexp(`fixtures\/person-in-field\.jpg$`))) 70 | Expect(expanded).To(ContainElement(MatchRegexp(`nested\/plant\.png$`))) 71 | Expect(expanded).ToNot(ContainElement(MatchRegexp(`nomatch\.txt$`))) 72 | }) 73 | 74 | It("returns non-glob paths as-is", func() { 75 | fixtureFile := path.Join(testDir, "../fixtures/person-in-field.jpg") 76 | originals := []string{fixtureFile} 77 | expanded, err := subject.ExpandPaths(originals) 78 | 79 | Expect(err).ToNot(HaveOccurred()) 80 | Expect(expanded).To(Equal(originals)) 81 | }) 82 | 83 | Context("input path isn't a glob", func() { 84 | // We want non-existent paths to remain, so we don't fail silently 85 | It("doesn't strip non-existent files", func() { 86 | inputPath := "missing/foo/bar.jpg" 87 | originals := []string{inputPath} 88 | expanded, err := subject.ExpandPaths(originals) 89 | 90 | Expect(err).ToNot(HaveOccurred()) 91 | Expect(expanded).To(Equal(originals)) 92 | }) 93 | }) 94 | }) 95 | 96 | Describe("MkdirP", func() { 97 | var tmpDir string 98 | 99 | BeforeEach(func() { 100 | dir, err := ioutil.TempDir("", "mkdirp-spec") 101 | Expect(err).ToNot(HaveOccurred()) 102 | 103 | tmpDir = dir 104 | }) 105 | 106 | AfterEach(func() { 107 | os.RemoveAll(tmpDir) 108 | }) 109 | 110 | It("creates deeply nested directories, if they don't exist", func() { 111 | outputDir := path.Join(tmpDir, "nested1/nested2") 112 | 113 | Expect(outputDir).ToNot(BeADirectory()) 114 | Expect(subject.MkdirP(outputDir)).Should(Succeed()) 115 | Expect(outputDir).To(BeADirectory()) 116 | }) 117 | }) 118 | }) 119 | -------------------------------------------------------------------------------- /storage/storagefakes/fake_storage_interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package storagefakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/remove-bg/go/storage" 8 | ) 9 | 10 | type FakeStorageInterface struct { 11 | ExpandPathsStub func([]string) ([]string, error) 12 | expandPathsMutex sync.RWMutex 13 | expandPathsArgsForCall []struct { 14 | arg1 []string 15 | } 16 | expandPathsReturns struct { 17 | result1 []string 18 | result2 error 19 | } 20 | expandPathsReturnsOnCall map[int]struct { 21 | result1 []string 22 | result2 error 23 | } 24 | FileExistsStub func(string) bool 25 | fileExistsMutex sync.RWMutex 26 | fileExistsArgsForCall []struct { 27 | arg1 string 28 | } 29 | fileExistsReturns struct { 30 | result1 bool 31 | } 32 | fileExistsReturnsOnCall map[int]struct { 33 | result1 bool 34 | } 35 | MkdirPStub func(string) error 36 | mkdirPMutex sync.RWMutex 37 | mkdirPArgsForCall []struct { 38 | arg1 string 39 | } 40 | mkdirPReturns struct { 41 | result1 error 42 | } 43 | mkdirPReturnsOnCall map[int]struct { 44 | result1 error 45 | } 46 | WriteStub func(string, []byte) error 47 | writeMutex sync.RWMutex 48 | writeArgsForCall []struct { 49 | arg1 string 50 | arg2 []byte 51 | } 52 | writeReturns struct { 53 | result1 error 54 | } 55 | writeReturnsOnCall map[int]struct { 56 | result1 error 57 | } 58 | invocations map[string][][]interface{} 59 | invocationsMutex sync.RWMutex 60 | } 61 | 62 | func (fake *FakeStorageInterface) ExpandPaths(arg1 []string) ([]string, error) { 63 | var arg1Copy []string 64 | if arg1 != nil { 65 | arg1Copy = make([]string, len(arg1)) 66 | copy(arg1Copy, arg1) 67 | } 68 | fake.expandPathsMutex.Lock() 69 | ret, specificReturn := fake.expandPathsReturnsOnCall[len(fake.expandPathsArgsForCall)] 70 | fake.expandPathsArgsForCall = append(fake.expandPathsArgsForCall, struct { 71 | arg1 []string 72 | }{arg1Copy}) 73 | fake.recordInvocation("ExpandPaths", []interface{}{arg1Copy}) 74 | fake.expandPathsMutex.Unlock() 75 | if fake.ExpandPathsStub != nil { 76 | return fake.ExpandPathsStub(arg1) 77 | } 78 | if specificReturn { 79 | return ret.result1, ret.result2 80 | } 81 | fakeReturns := fake.expandPathsReturns 82 | return fakeReturns.result1, fakeReturns.result2 83 | } 84 | 85 | func (fake *FakeStorageInterface) ExpandPathsCallCount() int { 86 | fake.expandPathsMutex.RLock() 87 | defer fake.expandPathsMutex.RUnlock() 88 | return len(fake.expandPathsArgsForCall) 89 | } 90 | 91 | func (fake *FakeStorageInterface) ExpandPathsCalls(stub func([]string) ([]string, error)) { 92 | fake.expandPathsMutex.Lock() 93 | defer fake.expandPathsMutex.Unlock() 94 | fake.ExpandPathsStub = stub 95 | } 96 | 97 | func (fake *FakeStorageInterface) ExpandPathsArgsForCall(i int) []string { 98 | fake.expandPathsMutex.RLock() 99 | defer fake.expandPathsMutex.RUnlock() 100 | argsForCall := fake.expandPathsArgsForCall[i] 101 | return argsForCall.arg1 102 | } 103 | 104 | func (fake *FakeStorageInterface) ExpandPathsReturns(result1 []string, result2 error) { 105 | fake.expandPathsMutex.Lock() 106 | defer fake.expandPathsMutex.Unlock() 107 | fake.ExpandPathsStub = nil 108 | fake.expandPathsReturns = struct { 109 | result1 []string 110 | result2 error 111 | }{result1, result2} 112 | } 113 | 114 | func (fake *FakeStorageInterface) ExpandPathsReturnsOnCall(i int, result1 []string, result2 error) { 115 | fake.expandPathsMutex.Lock() 116 | defer fake.expandPathsMutex.Unlock() 117 | fake.ExpandPathsStub = nil 118 | if fake.expandPathsReturnsOnCall == nil { 119 | fake.expandPathsReturnsOnCall = make(map[int]struct { 120 | result1 []string 121 | result2 error 122 | }) 123 | } 124 | fake.expandPathsReturnsOnCall[i] = struct { 125 | result1 []string 126 | result2 error 127 | }{result1, result2} 128 | } 129 | 130 | func (fake *FakeStorageInterface) FileExists(arg1 string) bool { 131 | fake.fileExistsMutex.Lock() 132 | ret, specificReturn := fake.fileExistsReturnsOnCall[len(fake.fileExistsArgsForCall)] 133 | fake.fileExistsArgsForCall = append(fake.fileExistsArgsForCall, struct { 134 | arg1 string 135 | }{arg1}) 136 | fake.recordInvocation("FileExists", []interface{}{arg1}) 137 | fake.fileExistsMutex.Unlock() 138 | if fake.FileExistsStub != nil { 139 | return fake.FileExistsStub(arg1) 140 | } 141 | if specificReturn { 142 | return ret.result1 143 | } 144 | fakeReturns := fake.fileExistsReturns 145 | return fakeReturns.result1 146 | } 147 | 148 | func (fake *FakeStorageInterface) FileExistsCallCount() int { 149 | fake.fileExistsMutex.RLock() 150 | defer fake.fileExistsMutex.RUnlock() 151 | return len(fake.fileExistsArgsForCall) 152 | } 153 | 154 | func (fake *FakeStorageInterface) FileExistsCalls(stub func(string) bool) { 155 | fake.fileExistsMutex.Lock() 156 | defer fake.fileExistsMutex.Unlock() 157 | fake.FileExistsStub = stub 158 | } 159 | 160 | func (fake *FakeStorageInterface) FileExistsArgsForCall(i int) string { 161 | fake.fileExistsMutex.RLock() 162 | defer fake.fileExistsMutex.RUnlock() 163 | argsForCall := fake.fileExistsArgsForCall[i] 164 | return argsForCall.arg1 165 | } 166 | 167 | func (fake *FakeStorageInterface) FileExistsReturns(result1 bool) { 168 | fake.fileExistsMutex.Lock() 169 | defer fake.fileExistsMutex.Unlock() 170 | fake.FileExistsStub = nil 171 | fake.fileExistsReturns = struct { 172 | result1 bool 173 | }{result1} 174 | } 175 | 176 | func (fake *FakeStorageInterface) FileExistsReturnsOnCall(i int, result1 bool) { 177 | fake.fileExistsMutex.Lock() 178 | defer fake.fileExistsMutex.Unlock() 179 | fake.FileExistsStub = nil 180 | if fake.fileExistsReturnsOnCall == nil { 181 | fake.fileExistsReturnsOnCall = make(map[int]struct { 182 | result1 bool 183 | }) 184 | } 185 | fake.fileExistsReturnsOnCall[i] = struct { 186 | result1 bool 187 | }{result1} 188 | } 189 | 190 | func (fake *FakeStorageInterface) MkdirP(arg1 string) error { 191 | fake.mkdirPMutex.Lock() 192 | ret, specificReturn := fake.mkdirPReturnsOnCall[len(fake.mkdirPArgsForCall)] 193 | fake.mkdirPArgsForCall = append(fake.mkdirPArgsForCall, struct { 194 | arg1 string 195 | }{arg1}) 196 | fake.recordInvocation("MkdirP", []interface{}{arg1}) 197 | fake.mkdirPMutex.Unlock() 198 | if fake.MkdirPStub != nil { 199 | return fake.MkdirPStub(arg1) 200 | } 201 | if specificReturn { 202 | return ret.result1 203 | } 204 | fakeReturns := fake.mkdirPReturns 205 | return fakeReturns.result1 206 | } 207 | 208 | func (fake *FakeStorageInterface) MkdirPCallCount() int { 209 | fake.mkdirPMutex.RLock() 210 | defer fake.mkdirPMutex.RUnlock() 211 | return len(fake.mkdirPArgsForCall) 212 | } 213 | 214 | func (fake *FakeStorageInterface) MkdirPCalls(stub func(string) error) { 215 | fake.mkdirPMutex.Lock() 216 | defer fake.mkdirPMutex.Unlock() 217 | fake.MkdirPStub = stub 218 | } 219 | 220 | func (fake *FakeStorageInterface) MkdirPArgsForCall(i int) string { 221 | fake.mkdirPMutex.RLock() 222 | defer fake.mkdirPMutex.RUnlock() 223 | argsForCall := fake.mkdirPArgsForCall[i] 224 | return argsForCall.arg1 225 | } 226 | 227 | func (fake *FakeStorageInterface) MkdirPReturns(result1 error) { 228 | fake.mkdirPMutex.Lock() 229 | defer fake.mkdirPMutex.Unlock() 230 | fake.MkdirPStub = nil 231 | fake.mkdirPReturns = struct { 232 | result1 error 233 | }{result1} 234 | } 235 | 236 | func (fake *FakeStorageInterface) MkdirPReturnsOnCall(i int, result1 error) { 237 | fake.mkdirPMutex.Lock() 238 | defer fake.mkdirPMutex.Unlock() 239 | fake.MkdirPStub = nil 240 | if fake.mkdirPReturnsOnCall == nil { 241 | fake.mkdirPReturnsOnCall = make(map[int]struct { 242 | result1 error 243 | }) 244 | } 245 | fake.mkdirPReturnsOnCall[i] = struct { 246 | result1 error 247 | }{result1} 248 | } 249 | 250 | func (fake *FakeStorageInterface) Write(arg1 string, arg2 []byte) error { 251 | var arg2Copy []byte 252 | if arg2 != nil { 253 | arg2Copy = make([]byte, len(arg2)) 254 | copy(arg2Copy, arg2) 255 | } 256 | fake.writeMutex.Lock() 257 | ret, specificReturn := fake.writeReturnsOnCall[len(fake.writeArgsForCall)] 258 | fake.writeArgsForCall = append(fake.writeArgsForCall, struct { 259 | arg1 string 260 | arg2 []byte 261 | }{arg1, arg2Copy}) 262 | fake.recordInvocation("Write", []interface{}{arg1, arg2Copy}) 263 | fake.writeMutex.Unlock() 264 | if fake.WriteStub != nil { 265 | return fake.WriteStub(arg1, arg2) 266 | } 267 | if specificReturn { 268 | return ret.result1 269 | } 270 | fakeReturns := fake.writeReturns 271 | return fakeReturns.result1 272 | } 273 | 274 | func (fake *FakeStorageInterface) WriteCallCount() int { 275 | fake.writeMutex.RLock() 276 | defer fake.writeMutex.RUnlock() 277 | return len(fake.writeArgsForCall) 278 | } 279 | 280 | func (fake *FakeStorageInterface) WriteCalls(stub func(string, []byte) error) { 281 | fake.writeMutex.Lock() 282 | defer fake.writeMutex.Unlock() 283 | fake.WriteStub = stub 284 | } 285 | 286 | func (fake *FakeStorageInterface) WriteArgsForCall(i int) (string, []byte) { 287 | fake.writeMutex.RLock() 288 | defer fake.writeMutex.RUnlock() 289 | argsForCall := fake.writeArgsForCall[i] 290 | return argsForCall.arg1, argsForCall.arg2 291 | } 292 | 293 | func (fake *FakeStorageInterface) WriteReturns(result1 error) { 294 | fake.writeMutex.Lock() 295 | defer fake.writeMutex.Unlock() 296 | fake.WriteStub = nil 297 | fake.writeReturns = struct { 298 | result1 error 299 | }{result1} 300 | } 301 | 302 | func (fake *FakeStorageInterface) WriteReturnsOnCall(i int, result1 error) { 303 | fake.writeMutex.Lock() 304 | defer fake.writeMutex.Unlock() 305 | fake.WriteStub = nil 306 | if fake.writeReturnsOnCall == nil { 307 | fake.writeReturnsOnCall = make(map[int]struct { 308 | result1 error 309 | }) 310 | } 311 | fake.writeReturnsOnCall[i] = struct { 312 | result1 error 313 | }{result1} 314 | } 315 | 316 | func (fake *FakeStorageInterface) Invocations() map[string][][]interface{} { 317 | fake.invocationsMutex.RLock() 318 | defer fake.invocationsMutex.RUnlock() 319 | fake.expandPathsMutex.RLock() 320 | defer fake.expandPathsMutex.RUnlock() 321 | fake.fileExistsMutex.RLock() 322 | defer fake.fileExistsMutex.RUnlock() 323 | fake.mkdirPMutex.RLock() 324 | defer fake.mkdirPMutex.RUnlock() 325 | fake.writeMutex.RLock() 326 | defer fake.writeMutex.RUnlock() 327 | copiedInvocations := map[string][][]interface{}{} 328 | for key, value := range fake.invocations { 329 | copiedInvocations[key] = value 330 | } 331 | return copiedInvocations 332 | } 333 | 334 | func (fake *FakeStorageInterface) recordInvocation(key string, args []interface{}) { 335 | fake.invocationsMutex.Lock() 336 | defer fake.invocationsMutex.Unlock() 337 | if fake.invocations == nil { 338 | fake.invocations = map[string][][]interface{}{} 339 | } 340 | if fake.invocations[key] == nil { 341 | fake.invocations[key] = [][]interface{}{} 342 | } 343 | fake.invocations[key] = append(fake.invocations[key], args) 344 | } 345 | 346 | var _ storage.StorageInterface = new(FakeStorageInterface) 347 | --------------------------------------------------------------------------------