imgix 3.3.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 +7 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +28 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +27 -0
- data/.github/ISSUE_TEMPLATE/question.md +17 -0
- data/.github/pull_request_template.md +73 -0
- data/.gitignore +18 -0
- data/.travis.yml +21 -0
- data/CHANGELOG.md +84 -0
- data/Contributing.markdown +19 -0
- data/Gemfile +9 -0
- data/LICENSE +22 -0
- data/README.md +258 -0
- data/Rakefile +16 -0
- data/imgix.gemspec +31 -0
- data/lib/imgix.rb +51 -0
- data/lib/imgix/client.rb +90 -0
- data/lib/imgix/param_helpers.rb +19 -0
- data/lib/imgix/path.rb +220 -0
- data/lib/imgix/version.rb +5 -0
- data/test/test_helper.rb +13 -0
- data/test/units/domains_test.rb +17 -0
- data/test/units/param_helpers_test.rb +23 -0
- data/test/units/path_test.rb +182 -0
- data/test/units/purge_test.rb +25 -0
- data/test/units/srcset_test.rb +755 -0
- data/test/units/url_test.rb +80 -0
- metadata +117 -0
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
|
3
|
+
require 'rake/testtask'
|
4
|
+
Rake::TestTask.new(:test) do |t|
|
5
|
+
t.libs << 'test'
|
6
|
+
t.pattern = 'test/**/*_test.rb'
|
7
|
+
end
|
8
|
+
task :default => :test
|
9
|
+
|
10
|
+
task :console do
|
11
|
+
require 'irb'
|
12
|
+
require 'irb/completion'
|
13
|
+
require 'imgix'
|
14
|
+
ARGV.clear
|
15
|
+
IRB.start
|
16
|
+
end
|
data/imgix.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'imgix/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'imgix'
|
8
|
+
spec.version = Imgix::VERSION
|
9
|
+
spec.authors = ['Kelly Sutton', 'Sam Soffes', 'Ryan LeFevre', 'Antony Denyer', 'Paul Straw', 'Sherwin Heydarbeygi']
|
10
|
+
spec.email = ['kelly@imgix.com', 'sam@soff.es', 'ryan@layervault.com', 'email@antonydenyer.co.uk', 'paul@imgix.com', 'sherwin@imgix.com']
|
11
|
+
spec.description = 'Easily create and sign imgix URLs.'
|
12
|
+
spec.summary = 'Official Ruby Gem for easily creating and signing imgix URLs.'
|
13
|
+
spec.homepage = 'https://github.com/imgix/imgix-rb'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.metadata = {
|
17
|
+
'bug_tracker_uri' => 'https://github.com/imgix/imgix-rb/issues',
|
18
|
+
'changelog_uri' => 'https://github.com/imgix/imgix-rb/blob/main/CHANGELOG.md',
|
19
|
+
'documentation_uri' => "https://www.rubydoc.info/gems/imgix/#{spec.version}",
|
20
|
+
'source_code_uri' => "https://github.com/imgix/imgix-rb/tree/#{spec.version}"
|
21
|
+
}
|
22
|
+
|
23
|
+
spec.files = `git ls-files`.split($/)
|
24
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
25
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
26
|
+
spec.require_paths = ['lib']
|
27
|
+
|
28
|
+
spec.required_ruby_version = '>= 1.9.0'
|
29
|
+
spec.add_dependency 'addressable'
|
30
|
+
spec.add_development_dependency 'webmock'
|
31
|
+
end
|
data/lib/imgix.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'imgix/version'
|
4
|
+
require 'imgix/client'
|
5
|
+
require 'imgix/path'
|
6
|
+
|
7
|
+
module Imgix
|
8
|
+
# regex pattern used to determine if a domain is valid
|
9
|
+
DOMAIN_REGEX = /^(?:[a-z\d\-_]{1,62}\.){0,125}(?:[a-z\d](?:\-(?=\-*[a-z\d])|[a-z]|\d){0,62}\.)[a-z\d]{1,63}$/i.freeze
|
10
|
+
|
11
|
+
# determines the growth rate when building out srcset pair widths
|
12
|
+
DEFAULT_WIDTH_TOLERANCE = 0.08
|
13
|
+
|
14
|
+
# the default minimum srcset width
|
15
|
+
MIN_WIDTH = 100
|
16
|
+
|
17
|
+
# the default maximum srcset width, also the max width supported by imgix
|
18
|
+
MAX_WIDTH = 8192
|
19
|
+
|
20
|
+
# returns an array of width values used during scrset generation
|
21
|
+
TARGET_WIDTHS = lambda { |tolerance, min, max|
|
22
|
+
increment_percentage = tolerance || DEFAULT_WIDTH_TOLERANCE
|
23
|
+
|
24
|
+
unless increment_percentage.is_a?(Numeric) && increment_percentage > 0
|
25
|
+
width_increment_error = 'error: `width_tolerance` must be a positive `Numeric` value'
|
26
|
+
raise ArgumentError, width_increment_error
|
27
|
+
end
|
28
|
+
|
29
|
+
max_size = max || MAX_WIDTH
|
30
|
+
resolutions = []
|
31
|
+
prev = min || MIN_WIDTH
|
32
|
+
|
33
|
+
while prev < max_size
|
34
|
+
# ensures that each width is even
|
35
|
+
resolutions.push(prev.round)
|
36
|
+
prev *= 1 + (increment_percentage * 2)
|
37
|
+
end
|
38
|
+
|
39
|
+
resolutions.push(max_size)
|
40
|
+
return resolutions
|
41
|
+
}
|
42
|
+
|
43
|
+
# hash of default quality parameter values mapped by each dpr srcset entry
|
44
|
+
DPR_QUALITY = {
|
45
|
+
1 => 75,
|
46
|
+
2 => 50,
|
47
|
+
3 => 35,
|
48
|
+
4 => 23,
|
49
|
+
5 => 20
|
50
|
+
}.freeze
|
51
|
+
end
|
data/lib/imgix/client.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest'
|
4
|
+
require 'addressable/uri'
|
5
|
+
require 'net/http'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
module Imgix
|
9
|
+
class Client
|
10
|
+
DEFAULTS = { use_https: true }.freeze
|
11
|
+
|
12
|
+
def initialize(options = {})
|
13
|
+
options = DEFAULTS.merge(options)
|
14
|
+
host, domain = options[:host], options[:domain]
|
15
|
+
|
16
|
+
host_deprecated = "Warning: The identifier `host' has been deprecated and " \
|
17
|
+
"will\nappear as `domain' in the next major version, e.g. " \
|
18
|
+
"`@host'\nbecomes `@domain', `options[:host]' becomes " \
|
19
|
+
"`options[:domain]'.\n"
|
20
|
+
|
21
|
+
if host
|
22
|
+
warn host_deprecated
|
23
|
+
@host = host
|
24
|
+
elsif domain
|
25
|
+
@host = domain
|
26
|
+
end
|
27
|
+
|
28
|
+
validate_host!
|
29
|
+
|
30
|
+
@secure_url_token = options[:secure_url_token]
|
31
|
+
@api_key = options[:api_key]
|
32
|
+
@use_https = options[:use_https]
|
33
|
+
@include_library_param = options.fetch(:include_library_param, true)
|
34
|
+
@library = options.fetch(:library_param, 'rb')
|
35
|
+
@version = options.fetch(:library_version, Imgix::VERSION)
|
36
|
+
end
|
37
|
+
|
38
|
+
def path(path)
|
39
|
+
p = Path.new(new_prefix, @secure_url_token, path)
|
40
|
+
p.ixlib("#{@library}-#{@version}") if @include_library_param
|
41
|
+
p
|
42
|
+
end
|
43
|
+
|
44
|
+
def purge(path)
|
45
|
+
token_error = 'Authentication token required'
|
46
|
+
raise token_error if @api_key.nil?
|
47
|
+
|
48
|
+
url = new_prefix + path
|
49
|
+
uri = URI.parse('https://api.imgix.com/v2/image/purger')
|
50
|
+
|
51
|
+
user_agent = { 'User-Agent' => "imgix #{@library}-#{@version}" }
|
52
|
+
|
53
|
+
req = Net::HTTP::Post.new(uri.path, user_agent)
|
54
|
+
req.basic_auth @api_key, ''
|
55
|
+
req.set_form_data({ url: url })
|
56
|
+
|
57
|
+
sock = Net::HTTP.new(uri.host, uri.port)
|
58
|
+
sock.use_ssl = true
|
59
|
+
res = sock.start { |http| http.request(req) }
|
60
|
+
|
61
|
+
res
|
62
|
+
end
|
63
|
+
|
64
|
+
def prefix(path)
|
65
|
+
msg = "Warning: `Client::prefix' will take zero arguments " \
|
66
|
+
"in the next major version.\n"
|
67
|
+
warn msg
|
68
|
+
new_prefix
|
69
|
+
end
|
70
|
+
|
71
|
+
def new_prefix
|
72
|
+
"#{@use_https ? 'https' : 'http'}://#{@host}"
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def validate_host!
|
78
|
+
host_error = 'The :host option must be specified'
|
79
|
+
raise ArgumentError, host_error if @host.nil?
|
80
|
+
|
81
|
+
domain_error = 'Domains must be passed in as fully-qualified'\
|
82
|
+
'domain names and should not include a protocol'\
|
83
|
+
'or any path element, i.e. "example.imgix.net"'\
|
84
|
+
|
85
|
+
if @host.match(DOMAIN_REGEX).nil?
|
86
|
+
raise ArgumentError, domain_error
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Imgix
|
4
|
+
module ParamHelpers
|
5
|
+
def rect(position)
|
6
|
+
warn "Warning: `ParamHelpers.rect` has been deprecated and will be removed in the next major version.\n"
|
7
|
+
@options[:rect] = position and return self if position.is_a?(String)
|
8
|
+
|
9
|
+
@options[:rect] = [
|
10
|
+
position[:x] || position[:left],
|
11
|
+
position[:y] || position[:top],
|
12
|
+
position[:width] || (position[:right] - position[:left]),
|
13
|
+
position[:height] || (position[:bottom] - position[:top])
|
14
|
+
].join(',')
|
15
|
+
|
16
|
+
return self
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/imgix/path.rb
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'cgi/util'
|
5
|
+
require 'erb'
|
6
|
+
require 'imgix/param_helpers'
|
7
|
+
|
8
|
+
module Imgix
|
9
|
+
class Path
|
10
|
+
include ParamHelpers
|
11
|
+
|
12
|
+
ALIASES = {
|
13
|
+
width: :w,
|
14
|
+
height: :h,
|
15
|
+
rotation: :rot,
|
16
|
+
noise_reduction: :nr,
|
17
|
+
sharpness: :sharp,
|
18
|
+
exposure: :exp,
|
19
|
+
vibrance: :vib,
|
20
|
+
saturation: :sat,
|
21
|
+
brightness: :bri,
|
22
|
+
contrast: :con,
|
23
|
+
highlight: :high,
|
24
|
+
shadow: :shad,
|
25
|
+
gamma: :gam,
|
26
|
+
pixelate: :px,
|
27
|
+
halftone: :htn,
|
28
|
+
watermark: :mark,
|
29
|
+
text: :txt,
|
30
|
+
format: :fm,
|
31
|
+
quality: :q
|
32
|
+
}.freeze
|
33
|
+
|
34
|
+
def initialize(prefix, secure_url_token, path = '/')
|
35
|
+
@prefix = prefix
|
36
|
+
@secure_url_token = secure_url_token
|
37
|
+
@path = path
|
38
|
+
@options = {}
|
39
|
+
|
40
|
+
@path = CGI.escape(@path) if /^https?/ =~ @path
|
41
|
+
@path = "/#{@path}" if @path[0] != '/'
|
42
|
+
@target_widths = TARGET_WIDTHS.call(DEFAULT_WIDTH_TOLERANCE, MIN_WIDTH, MAX_WIDTH)
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_url(opts = {})
|
46
|
+
prev_options = @options.dup
|
47
|
+
@options.merge!(opts)
|
48
|
+
|
49
|
+
url = @prefix + path_and_params
|
50
|
+
|
51
|
+
if @secure_url_token
|
52
|
+
url += (has_query? ? '&' : '?') + "s=#{signature}"
|
53
|
+
end
|
54
|
+
|
55
|
+
@options = prev_options
|
56
|
+
url
|
57
|
+
end
|
58
|
+
|
59
|
+
def defaults
|
60
|
+
@options = {}
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
def method_missing(method, *args, &block)
|
65
|
+
key = method.to_s.gsub('=', '')
|
66
|
+
if args.length == 0
|
67
|
+
return @options[key]
|
68
|
+
elsif args.first.nil? && @options.has_key?(key)
|
69
|
+
@options.delete(key) and return self
|
70
|
+
end
|
71
|
+
|
72
|
+
@options[key] = args.join(',')
|
73
|
+
self
|
74
|
+
end
|
75
|
+
|
76
|
+
ALIASES.each do |from, to|
|
77
|
+
define_method from do |*args|
|
78
|
+
warn "Warning: `Path.#{from}' has been deprecated and " \
|
79
|
+
"will be removed in the next major version (along " \
|
80
|
+
"with all parameter `ALIASES`).\n"
|
81
|
+
self.send(to, *args)
|
82
|
+
end
|
83
|
+
|
84
|
+
define_method "#{from}=" do |*args|
|
85
|
+
warn "Warning: `Path.#{from}=' has been deprecated and " \
|
86
|
+
"will be removed in the next major version (along " \
|
87
|
+
"with all parameter `ALIASES`).\n"
|
88
|
+
self.send("#{to}=", *args)
|
89
|
+
return self
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def to_srcset(options: {}, **params)
|
94
|
+
prev_options = @options.dup
|
95
|
+
@options.merge!(params)
|
96
|
+
|
97
|
+
width = @options[:w]
|
98
|
+
height = @options[:h]
|
99
|
+
aspect_ratio = @options[:ar]
|
100
|
+
|
101
|
+
srcset = if width || (height && aspect_ratio)
|
102
|
+
build_dpr_srcset(options: options, params: @options)
|
103
|
+
else
|
104
|
+
build_srcset_pairs(options: options, params: @options)
|
105
|
+
end
|
106
|
+
|
107
|
+
@options = prev_options
|
108
|
+
srcset
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def signature
|
114
|
+
Digest::MD5.hexdigest(@secure_url_token + path_and_params)
|
115
|
+
end
|
116
|
+
|
117
|
+
def path_and_params
|
118
|
+
has_query? ? "#{@path}?#{query}" : @path
|
119
|
+
end
|
120
|
+
|
121
|
+
def query
|
122
|
+
@options.map do |key, val|
|
123
|
+
escaped_key = ERB::Util.url_encode(key.to_s)
|
124
|
+
|
125
|
+
if escaped_key.end_with? '64'
|
126
|
+
escaped_key << "=" << Base64.urlsafe_encode64(val.to_s).delete('=')
|
127
|
+
else
|
128
|
+
escaped_key << "=" << ERB::Util.url_encode(val.to_s)
|
129
|
+
end
|
130
|
+
end.join('&')
|
131
|
+
end
|
132
|
+
|
133
|
+
def has_query?
|
134
|
+
query.length > 0
|
135
|
+
end
|
136
|
+
|
137
|
+
def build_srcset_pairs(options:, params:)
|
138
|
+
srcset = ''
|
139
|
+
|
140
|
+
widths = options[:widths] || []
|
141
|
+
width_tolerance = options[:width_tolerance] || DEFAULT_WIDTH_TOLERANCE
|
142
|
+
min_width = options[:min_width] || MIN_WIDTH
|
143
|
+
max_width = options[:max_width] || MAX_WIDTH
|
144
|
+
|
145
|
+
if !widths.empty?
|
146
|
+
validate_widths!(widths)
|
147
|
+
srcset_widths = widths
|
148
|
+
elsif width_tolerance != DEFAULT_WIDTH_TOLERANCE || min_width != MIN_WIDTH || max_width != MAX_WIDTH
|
149
|
+
validate_range!(min_width, max_width)
|
150
|
+
validate_width_tolerance!(width_tolerance)
|
151
|
+
srcset_widths = TARGET_WIDTHS.call(width_tolerance, min_width, max_width)
|
152
|
+
else
|
153
|
+
srcset_widths = @target_widths
|
154
|
+
end
|
155
|
+
|
156
|
+
for width in srcset_widths do
|
157
|
+
params[:w] = width
|
158
|
+
srcset += "#{to_url(params)} #{width}w,\n"
|
159
|
+
end
|
160
|
+
|
161
|
+
srcset[0..-3]
|
162
|
+
end
|
163
|
+
|
164
|
+
def build_dpr_srcset(options:, params:)
|
165
|
+
srcset = ''
|
166
|
+
|
167
|
+
disable_variable_quality = options[:disable_variable_quality] || false
|
168
|
+
validate_variable_qualities!(disable_variable_quality)
|
169
|
+
|
170
|
+
target_ratios = [1, 2, 3, 4, 5]
|
171
|
+
quality = params[:q]
|
172
|
+
|
173
|
+
for ratio in target_ratios do
|
174
|
+
params[:dpr] = ratio
|
175
|
+
|
176
|
+
unless disable_variable_quality
|
177
|
+
params[:q] = quality || DPR_QUALITY[ratio]
|
178
|
+
end
|
179
|
+
|
180
|
+
srcset += "#{to_url(params)} #{ratio}x,\n"
|
181
|
+
end
|
182
|
+
|
183
|
+
srcset[0..-3]
|
184
|
+
end
|
185
|
+
|
186
|
+
def validate_width_tolerance!(width_tolerance)
|
187
|
+
width_increment_error = 'error: `width_tolerance` must be a positive `Numeric` value'
|
188
|
+
|
189
|
+
if !width_tolerance.is_a?(Numeric) || width_tolerance <= 0
|
190
|
+
raise ArgumentError, width_increment_error
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def validate_widths!(widths)
|
195
|
+
widths_error = 'error: `widths` must be an array of positive `Numeric` values'
|
196
|
+
raise ArgumentError, widths_error unless widths.is_a?(Array)
|
197
|
+
|
198
|
+
all_positive_integers = widths.all? { |i| i.is_a?(Integer) && i > 0 }
|
199
|
+
raise ArgumentError, widths_error unless all_positive_integers
|
200
|
+
end
|
201
|
+
|
202
|
+
def validate_range!(min_srcset, max_srcset)
|
203
|
+
range_numeric_error = 'error: `min_width` and `max_width` must be positive `Numeric` values'
|
204
|
+
unless min_srcset.is_a?(Numeric) && max_srcset.is_a?(Numeric)
|
205
|
+
raise ArgumentError, range_numeric_error
|
206
|
+
end
|
207
|
+
|
208
|
+
unless min_srcset > 0 && max_srcset > 0
|
209
|
+
raise ArgumentError, range_numeric_error
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def validate_variable_qualities!(disable_quality)
|
214
|
+
disable_quality_error = 'error: `disable_quality` must be a Boolean value'
|
215
|
+
unless disable_quality.is_a?(TrueClass) || disable_quality.is_a?(FalseClass)
|
216
|
+
raise ArgumentError, disable_quality_error
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
data/test/test_helper.rb
ADDED