remove_bg 1.2.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
|