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.
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Imgix
4
+ VERSION = '3.3.0'
5
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ Bundler.require :test
6
+
7
+ require 'minitest/autorun'
8
+ require 'imgix'
9
+ require 'webmock/minitest'
10
+ include WebMock::API
11
+
12
+ class Imgix::Test < MiniTest::Test
13
+ end