remove_bg 1.2.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +17 -6
- data/.gitignore +2 -0
- data/Appraisals +12 -6
- data/CHANGELOG.md +26 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +70 -36
- data/README.md +112 -11
- data/gemfiles/faraday_0_15.gemfile +5 -1
- data/gemfiles/faraday_0_16.gemfile +11 -0
- data/gemfiles/faraday_0_17.gemfile +11 -0
- data/gemfiles/faraday_1_x.gemfile +11 -0
- data/lib/remove_bg.rb +22 -0
- data/lib/remove_bg/account_info.rb +34 -0
- data/lib/remove_bg/api.rb +3 -9
- data/lib/remove_bg/api_client.rb +105 -17
- data/lib/remove_bg/base_request_options.rb +32 -0
- data/lib/remove_bg/composite_result.rb +65 -0
- data/lib/remove_bg/configuration.rb +14 -3
- data/lib/remove_bg/error.rb +19 -1
- data/lib/remove_bg/http_connection.rb +2 -0
- data/lib/remove_bg/image_composer.rb +60 -0
- data/lib/remove_bg/rate_limit_info.rb +34 -0
- data/lib/remove_bg/request_options.rb +25 -15
- data/lib/remove_bg/result.rb +42 -7
- data/lib/remove_bg/result_metadata.rb +14 -0
- data/lib/remove_bg/upload.rb +6 -2
- data/lib/remove_bg/version.rb +1 -1
- data/remove_bg.gemspec +9 -4
- metadata +106 -17
- data/gemfiles/faraday_0_14.gemfile +0 -7
- data/gemfiles/faraday_1rc1.gemfile +0 -7
data/lib/remove_bg.rb
CHANGED
@@ -4,16 +4,38 @@ require "remove_bg/configuration"
|
|
4
4
|
require "remove_bg/request_options"
|
5
5
|
|
6
6
|
module RemoveBg
|
7
|
+
# Removes the background from an image on the local file system
|
8
|
+
# @param image_path [String] Path to the input image
|
9
|
+
# @param options [Hash<Symbol, Object>] Image processing options (see API docs)
|
10
|
+
# @return [RemoveBg::Result|RemoveBg::CompositeResult] a processed image result
|
11
|
+
#
|
7
12
|
def self.from_file(image_path, raw_options = {})
|
8
13
|
options = RemoveBg::RequestOptions.new(raw_options)
|
9
14
|
ApiClient.new.remove_from_file(image_path, options)
|
10
15
|
end
|
11
16
|
|
17
|
+
# Removes the background from the image at the URL specified
|
18
|
+
# @param image_url [String] Absolute URL of the input image
|
19
|
+
# @param options [Hash<Symbol, Object>] Image processing options (see API docs)
|
20
|
+
# @return [RemoveBg::Result|RemoveBg::CompositeResult] A processed image result
|
21
|
+
#
|
12
22
|
def self.from_url(image_url, raw_options = {})
|
13
23
|
options = RemoveBg::RequestOptions.new(raw_options)
|
14
24
|
ApiClient.new.remove_from_url(image_url, options)
|
15
25
|
end
|
16
26
|
|
27
|
+
# Fetches account information for the globally configured API key, or a
|
28
|
+
# specific API key if provided
|
29
|
+
# @param options [Hash<Symbol, Object>]
|
30
|
+
# @return [RemoveBg::AccountInfo]
|
31
|
+
#
|
32
|
+
def self.account_info(raw_options = {})
|
33
|
+
options = RemoveBg::BaseRequestOptions.new(raw_options)
|
34
|
+
ApiClient.new.account_info(options)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Yields the global Remove.bg configuration
|
38
|
+
# @yield [RemoveBg::Configuration]
|
17
39
|
def self.configure
|
18
40
|
yield RemoveBg::Configuration.configuration
|
19
41
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module RemoveBg
|
2
|
+
class AccountInfo
|
3
|
+
# @return [RemoveBg::AccountInfo::ApiInfo]
|
4
|
+
attr_reader :api
|
5
|
+
|
6
|
+
# @return [RemoveBg::AccountInfo::CreditsInfo]
|
7
|
+
attr_reader :credits
|
8
|
+
|
9
|
+
def initialize(attributes)
|
10
|
+
@api = ApiInfo.new(**attributes.fetch(:api))
|
11
|
+
@credits = CreditsInfo.new(**attributes.fetch(:credits))
|
12
|
+
end
|
13
|
+
|
14
|
+
class ApiInfo
|
15
|
+
attr_reader :free_calls, :sizes
|
16
|
+
|
17
|
+
def initialize(free_calls:, sizes:)
|
18
|
+
@free_calls = free_calls
|
19
|
+
@sizes = sizes
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class CreditsInfo
|
24
|
+
attr_reader :total, :subscription, :payg, :enterprise
|
25
|
+
|
26
|
+
def initialize(total:, subscription:, payg:, enterprise:)
|
27
|
+
@total = total
|
28
|
+
@subscription = subscription
|
29
|
+
@payg = payg
|
30
|
+
@enterprise = enterprise
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/remove_bg/api.rb
CHANGED
@@ -2,19 +2,13 @@ module RemoveBg
|
|
2
2
|
module Api
|
3
3
|
URL = "https://api.remove.bg"
|
4
4
|
|
5
|
+
V1_ACCOUNT = "/v1.0/account"
|
6
|
+
private_constant :V1_ACCOUNT
|
7
|
+
|
5
8
|
V1_REMOVE_BG = "/v1.0/removebg"
|
6
9
|
private_constant :V1_REMOVE_BG
|
7
10
|
|
8
11
|
HEADER_API_KEY = "X-Api-Key"
|
9
12
|
private_constant :HEADER_API_KEY
|
10
|
-
|
11
|
-
HEADER_WIDTH = "X-Width"
|
12
|
-
private_constant :HEADER_WIDTH
|
13
|
-
|
14
|
-
HEADER_HEIGHT = "X-Height"
|
15
|
-
private_constant :HEADER_HEIGHT
|
16
|
-
|
17
|
-
HEADER_CREDITS_CHARGED = "X-Credits-Charged"
|
18
|
-
private_constant :HEADER_CREDITS_CHARGED
|
19
13
|
end
|
20
14
|
end
|
data/lib/remove_bg/api_client.rb
CHANGED
@@ -1,7 +1,13 @@
|
|
1
1
|
require "json"
|
2
|
+
require "tempfile"
|
3
|
+
|
4
|
+
require_relative "account_info"
|
2
5
|
require_relative "api"
|
6
|
+
require_relative "composite_result"
|
3
7
|
require_relative "error"
|
4
8
|
require_relative "http_connection"
|
9
|
+
require_relative "rate_limit_info"
|
10
|
+
require_relative "result_metadata"
|
5
11
|
require_relative "result"
|
6
12
|
require_relative "upload"
|
7
13
|
require_relative "url_validator"
|
@@ -10,59 +16,141 @@ module RemoveBg
|
|
10
16
|
class ApiClient
|
11
17
|
include RemoveBg::Api
|
12
18
|
|
19
|
+
# @param connection [Faraday::Connection]
|
20
|
+
#
|
13
21
|
def initialize(connection: RemoveBg::HttpConnection.build)
|
14
22
|
@connection = connection
|
15
23
|
end
|
16
24
|
|
25
|
+
# Removes the background from an image on the local file system
|
26
|
+
# @param image_path [String]
|
27
|
+
# @param options [RemoveBg::RequestOptions]
|
28
|
+
# @return [RemoveBg::Result|RemoveBg::CompositeResult]
|
29
|
+
# @raise [RemoveBg::Error]
|
30
|
+
#
|
17
31
|
def remove_from_file(image_path, options)
|
18
32
|
data = options.data.merge(image_file: Upload.for_file(image_path))
|
19
33
|
request_remove_bg(data, options.api_key)
|
20
34
|
end
|
21
35
|
|
36
|
+
# Removes the background from the image at the URL specified
|
37
|
+
# @param image_url [String]
|
38
|
+
# @param options [RemoveBg::RequestOptions]
|
39
|
+
# @return [RemoveBg::Result|RemoveBg::CompositeResult]
|
40
|
+
# @raise [RemoveBg::Error]
|
41
|
+
#
|
22
42
|
def remove_from_url(image_url, options)
|
23
43
|
RemoveBg::UrlValidator.validate(image_url)
|
24
44
|
data = options.data.merge(image_url: image_url)
|
25
45
|
request_remove_bg(data, options.api_key)
|
26
46
|
end
|
27
47
|
|
48
|
+
# Fetches account information
|
49
|
+
# @param options [RemoveBg::BaseRequestOptions]
|
50
|
+
# @return [RemoveBg::AccountInfo]
|
51
|
+
# @raise [RemoveBg::Error]
|
52
|
+
#
|
53
|
+
def account_info(options)
|
54
|
+
request_account_info(options.api_key)
|
55
|
+
end
|
56
|
+
|
28
57
|
private
|
29
58
|
|
30
59
|
attr_reader :connection
|
31
60
|
|
32
61
|
def request_remove_bg(data, api_key)
|
62
|
+
download = Tempfile.new("remove-bg-download")
|
63
|
+
download.binmode
|
64
|
+
|
65
|
+
streaming = false
|
66
|
+
|
33
67
|
response = connection.post(V1_REMOVE_BG, data) do |req|
|
34
68
|
req.headers[HEADER_API_KEY] = api_key
|
69
|
+
|
70
|
+
# Faraday v0.16 & v1.0+ support streaming, v0.17 did not (rollback release)
|
71
|
+
if req.options.respond_to?(:on_data)
|
72
|
+
streaming = true
|
73
|
+
req.options.on_data = Proc.new do |chunk, _|
|
74
|
+
download.write(chunk)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Faraday v0.15 / v0.17
|
80
|
+
if !streaming
|
81
|
+
download.write(response.body)
|
82
|
+
end
|
83
|
+
|
84
|
+
download.rewind
|
85
|
+
|
86
|
+
if response.status == 200
|
87
|
+
parse_image_result(headers: response.headers, download: download)
|
88
|
+
else
|
89
|
+
response_body = download.read
|
90
|
+
download.close
|
91
|
+
download.unlink
|
92
|
+
handle_http_error(response: response, body: response_body)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def request_account_info(api_key)
|
97
|
+
response = connection.get(V1_ACCOUNT) do |req|
|
98
|
+
req.headers[HEADER_API_KEY] = api_key
|
35
99
|
end
|
36
100
|
|
101
|
+
if response.status == 200
|
102
|
+
parse_account_result(response)
|
103
|
+
else
|
104
|
+
handle_http_error(response: response, body: response.body)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def handle_http_error(response:, body:)
|
109
|
+
error_message = parse_error_message(body)
|
110
|
+
|
37
111
|
case response.status
|
38
|
-
when
|
39
|
-
|
112
|
+
when 429
|
113
|
+
rate_limit = RateLimitInfo.new(response.headers)
|
114
|
+
raise RemoveBg::RateLimitError.new(error_message, response, body, rate_limit)
|
40
115
|
when 400..499
|
41
|
-
error_message
|
42
|
-
raise RemoveBg::ClientHttpError.new(error_message, response)
|
116
|
+
raise RemoveBg::ClientHttpError.new(error_message, response, body)
|
43
117
|
when 500..599
|
44
|
-
error_message
|
45
|
-
raise RemoveBg::ServerHttpError.new(error_message, response)
|
118
|
+
raise RemoveBg::ServerHttpError.new(error_message, response, body)
|
46
119
|
else
|
47
|
-
raise RemoveBg::HttpError.new("An unknown error occurred", response)
|
120
|
+
raise RemoveBg::HttpError.new("An unknown error occurred", response, body)
|
48
121
|
end
|
49
122
|
end
|
50
123
|
|
51
|
-
def
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
credits_charged: response.headers[HEADER_CREDITS_CHARGED]&.to_f,
|
124
|
+
def parse_image_result(headers:, download:)
|
125
|
+
result_for_content_type(headers["Content-Type"]).new(
|
126
|
+
download: download,
|
127
|
+
metadata: ResultMetadata.new(headers),
|
128
|
+
rate_limit: RateLimitInfo.new(headers)
|
57
129
|
)
|
58
130
|
end
|
59
131
|
|
60
|
-
def
|
61
|
-
|
132
|
+
def result_for_content_type(content_type)
|
133
|
+
if content_type&.include?("application/zip")
|
134
|
+
CompositeResult
|
135
|
+
else
|
136
|
+
Result
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def parse_account_result(response)
|
141
|
+
attributes = JSON.parse(response.body, symbolize_names: true)
|
142
|
+
.fetch(:data)
|
143
|
+
.fetch(:attributes)
|
144
|
+
|
145
|
+
RemoveBg::AccountInfo.new(attributes)
|
146
|
+
end
|
147
|
+
|
148
|
+
def parse_error_message(response_body)
|
149
|
+
parse_errors(response_body).first["title"]
|
62
150
|
end
|
63
151
|
|
64
|
-
def parse_errors(
|
65
|
-
JSON.parse(
|
152
|
+
def parse_errors(response_body)
|
153
|
+
JSON.parse(response_body)["errors"] || []
|
66
154
|
rescue JSON::ParserError
|
67
155
|
[{ "title" => "Unable to parse response" }]
|
68
156
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative "error"
|
2
|
+
|
3
|
+
module RemoveBg
|
4
|
+
class BaseRequestOptions
|
5
|
+
attr_reader :api_key, :data
|
6
|
+
|
7
|
+
def initialize(raw_options = {})
|
8
|
+
options = raw_options.dup
|
9
|
+
@api_key = resolve_api_key(options.delete(:api_key))
|
10
|
+
@data = options
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def resolve_api_key(request_api_key)
|
16
|
+
api_key = request_api_key || global_api_key
|
17
|
+
|
18
|
+
if api_key.nil? || api_key.empty?
|
19
|
+
raise RemoveBg::Error, <<~MSG
|
20
|
+
Please configure an API key or specify one per request. API key was:
|
21
|
+
#{api_key.inspect}
|
22
|
+
MSG
|
23
|
+
end
|
24
|
+
|
25
|
+
api_key
|
26
|
+
end
|
27
|
+
|
28
|
+
def global_api_key
|
29
|
+
RemoveBg::Configuration.configuration.api_key
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require_relative "result"
|
2
|
+
|
3
|
+
module RemoveBg
|
4
|
+
# Handles image composition for larger images (over 10MP) where transparency is
|
5
|
+
# required.
|
6
|
+
# @see RemoveBg::Result
|
7
|
+
#
|
8
|
+
class CompositeResult < Result
|
9
|
+
# Saves the ZIP archive containing the alpha.png and color.jpg files
|
10
|
+
# (useful if you want to handle composition yourself)
|
11
|
+
# @param file_path [string]
|
12
|
+
# @param overwrite [boolean] Overwrite any existing file at the specified path
|
13
|
+
# @return [nil]
|
14
|
+
#
|
15
|
+
def save_zip(file_path, overwrite: false)
|
16
|
+
if File.exist?(file_path) && !overwrite
|
17
|
+
raise FileOverwriteError.new(file_path)
|
18
|
+
end
|
19
|
+
|
20
|
+
FileUtils.cp(download, file_path)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def image_file
|
26
|
+
composite_file
|
27
|
+
end
|
28
|
+
|
29
|
+
def composite_file
|
30
|
+
@composite_file ||= begin
|
31
|
+
binary_tempfile(["composed", ".png"])
|
32
|
+
.tap { |file| compose_to_file(file) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def color_file
|
37
|
+
@color_file ||= binary_tempfile(["color", ".jpg"])
|
38
|
+
end
|
39
|
+
|
40
|
+
def alpha_file
|
41
|
+
@alpha_file ||= binary_tempfile(["alpha", ".png"])
|
42
|
+
end
|
43
|
+
|
44
|
+
def compose_to_file(destination)
|
45
|
+
extract_parts
|
46
|
+
|
47
|
+
ImageComposer.new.compose(
|
48
|
+
color_file: color_file,
|
49
|
+
alpha_file: alpha_file,
|
50
|
+
destination_path: destination.path
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
def extract_parts
|
55
|
+
Zip::File.open(download) do |zf|
|
56
|
+
zf.find_entry("color.jpg").extract(color_file.path) { true }
|
57
|
+
zf.find_entry("alpha.png").extract(alpha_file.path) { true }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def binary_tempfile(basename)
|
62
|
+
Tempfile.new(basename).tap { |file| file.binmode }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -1,13 +1,24 @@
|
|
1
|
+
require_relative "image_composer"
|
2
|
+
|
1
3
|
module RemoveBg
|
2
4
|
class Configuration
|
3
|
-
attr_accessor :api_key
|
5
|
+
attr_accessor :api_key, :image_processor, :auto_upgrade_png_to_zip
|
4
6
|
|
5
7
|
def self.configuration
|
6
|
-
@configuration ||= Configuration.new
|
8
|
+
@configuration ||= Configuration.new.tap do |config|
|
9
|
+
config.image_processor = ImageComposer.detect_image_processor
|
10
|
+
|
11
|
+
# Upgrade to ZIP where possible to save bandwith
|
12
|
+
config.auto_upgrade_png_to_zip = true
|
13
|
+
end
|
7
14
|
end
|
8
15
|
|
9
16
|
def self.reset
|
10
|
-
@configuration =
|
17
|
+
@configuration = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def can_process_images?
|
21
|
+
!image_processor.nil?
|
11
22
|
end
|
12
23
|
end
|
13
24
|
end
|
data/lib/remove_bg/error.rb
CHANGED
@@ -2,17 +2,35 @@ module RemoveBg
|
|
2
2
|
class Error < StandardError; end
|
3
3
|
|
4
4
|
class HttpError < Error
|
5
|
+
# @return [Faraday::Response]
|
5
6
|
attr_reader :http_response
|
6
7
|
|
7
|
-
|
8
|
+
# @return [String]
|
9
|
+
attr_reader :http_response_body
|
10
|
+
|
11
|
+
def initialize(message, http_response, http_response_body)
|
8
12
|
@http_response = http_response
|
13
|
+
@http_response_body = http_response_body
|
9
14
|
super(message)
|
10
15
|
end
|
11
16
|
end
|
12
17
|
|
18
|
+
# Raised for all HTTP 4XX errors
|
13
19
|
class ClientHttpError < HttpError; end
|
20
|
+
|
21
|
+
# Raised for all HTTP 5XX errors
|
14
22
|
class ServerHttpError < HttpError; end
|
15
23
|
|
24
|
+
# Raised for HTTP 429 status code
|
25
|
+
class RateLimitError < ClientHttpError
|
26
|
+
attr_reader :rate_limit
|
27
|
+
|
28
|
+
def initialize(message, http_response, http_response_body, rate_limit)
|
29
|
+
@rate_limit = rate_limit
|
30
|
+
super(message, http_response, http_response_body)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
16
34
|
class FileError < Error
|
17
35
|
attr_reader :file_path
|
18
36
|
|