imgix 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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