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.
@@ -0,0 +1,11 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "faraday", "~> 0.16.0"
6
+
7
+ group :development do
8
+ gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git", ref: "5868643"
9
+ end
10
+
11
+ gemspec path: "../"
@@ -0,0 +1,11 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "faraday", "~> 0.17.0"
6
+
7
+ group :development do
8
+ gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git", ref: "5868643"
9
+ end
10
+
11
+ gemspec path: "../"
@@ -0,0 +1,11 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "faraday", "~> 1.0"
6
+
7
+ group :development do
8
+ gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git", ref: "5868643"
9
+ end
10
+
11
+ gemspec path: "../"
@@ -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
@@ -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
@@ -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 200
39
- parse_result(response)
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 = parse_error_message(response)
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 = parse_error_message(response)
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 parse_result(response)
52
- RemoveBg::Result.new(
53
- data: response.body,
54
- width: response.headers[HEADER_WIDTH]&.to_i,
55
- height: response.headers[HEADER_HEIGHT]&.to_i,
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 parse_error_message(response)
61
- parse_errors(response).first["title"]
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(response)
65
- JSON.parse(response.body)["errors"] || []
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 = Configuration.new
17
+ @configuration = nil
18
+ end
19
+
20
+ def can_process_images?
21
+ !image_processor.nil?
11
22
  end
12
23
  end
13
24
  end
@@ -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
- def initialize(message, http_response)
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