cloudinary 1.0.82 → 1.0.83

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +58 -7
  3. data/CHANGELOG +13 -0
  4. data/Gemfile +1 -1
  5. data/cloudinary.gemspec +13 -3
  6. data/lib/cloudinary/active_support/core_ext/hash/keys.rb +166 -0
  7. data/lib/cloudinary/active_support/core_ext/hash/readme.md +1 -0
  8. data/lib/cloudinary/api.rb +2 -1
  9. data/lib/cloudinary/carrier_wave/preloaded.rb +1 -1
  10. data/lib/cloudinary/helper.rb +38 -21
  11. data/lib/cloudinary/missing.rb +11 -10
  12. data/lib/cloudinary/uploader.rb +18 -20
  13. data/lib/cloudinary/utils.rb +187 -75
  14. data/lib/cloudinary/version.rb +1 -1
  15. data/lib/cloudinary/video_helper.rb +126 -0
  16. data/spec/api_spec.rb +23 -22
  17. data/spec/cloudinary_helper_spec.rb +34 -20
  18. data/spec/spec_helper.rb +75 -5
  19. data/spec/uploader_spec.rb +32 -3
  20. data/spec/utils_methods_spec.rb +18 -0
  21. data/spec/utils_spec.rb +73 -78
  22. data/spec/video_tag_spec.rb +186 -0
  23. data/spec/video_url_spec.rb +169 -0
  24. data/vendor/assets/html/cloudinary_cors.html +47 -0
  25. data/vendor/assets/javascripts/cloudinary/canvas-to-blob.min.js +1 -0
  26. data/vendor/assets/javascripts/cloudinary/index.js +4 -0
  27. data/vendor/assets/javascripts/cloudinary/jquery.cloudinary.js +898 -0
  28. data/vendor/assets/javascripts/cloudinary/jquery.fileupload-image.js +315 -0
  29. data/vendor/assets/javascripts/cloudinary/jquery.fileupload-process.js +172 -0
  30. data/vendor/assets/javascripts/cloudinary/jquery.fileupload-validate.js +119 -0
  31. data/vendor/assets/javascripts/cloudinary/jquery.fileupload.js +1460 -0
  32. data/vendor/assets/javascripts/cloudinary/jquery.iframe-transport.js +214 -0
  33. data/vendor/assets/javascripts/cloudinary/jquery.ui.widget.js +558 -0
  34. data/vendor/assets/javascripts/cloudinary/load-image.min.js +1 -0
  35. data/vendor/assets/javascripts/cloudinary/processing.js +5 -0
  36. metadata +44 -7
@@ -71,7 +71,7 @@ class Cloudinary::Uploader
71
71
  params = build_upload_params(options)
72
72
  if file.is_a?(Pathname)
73
73
  params[:file] = File.open(file, "rb")
74
- elsif file.respond_to?(:read) || file =~ /^https?:|^s3:|^data:[^;]*;base64,([a-zA-Z0-9\/+\n=]+)$/
74
+ elsif file.respond_to?(:read) || file =~ /^ftp:|^https?:|^s3:|^data:[^;]*;base64,([a-zA-Z0-9\/+\n=]+)$/
75
75
  params[:file] = file
76
76
  else
77
77
  params[:file] = File.open(file, "rb")
@@ -80,7 +80,7 @@ class Cloudinary::Uploader
80
80
  end
81
81
  end
82
82
 
83
- # Upload large raw files. Note that public_id should include an extension for best results.
83
+ # Upload large files. Note that public_id should include an extension for best results.
84
84
  def self.upload_large(file, public_id_or_options={}, old_options={})
85
85
  if public_id_or_options.is_a?(Hash)
86
86
  options = public_id_or_options
@@ -96,11 +96,13 @@ class Cloudinary::Uploader
96
96
  filename = "cloudinaryfile"
97
97
  end
98
98
  upload = upload_id = nil
99
- index = 1
99
+ index = 0
100
+ chunk_size = options[:chunk_size] || 20_000_000
100
101
  while !file.eof?
101
- buffer = file.read(20_000_000)
102
- upload = upload_large_part(Cloudinary::Blob.new(buffer, :original_filename=>filename), options.merge(:public_id=>public_id, :upload_id=>upload_id, :part_number=>index, :final=>file.eof?))
103
- upload_id = upload["upload_id"]
102
+ buffer = file.read(chunk_size)
103
+ current_loc = index*chunk_size
104
+ range = "bytes #{current_loc}-#{current_loc+buffer.size - 1}/#{file.size}"
105
+ upload = upload_large_part(Cloudinary::Blob.new(buffer, :original_filename=>filename), options.merge(:public_id=>public_id, :content_range=>range))
104
106
  public_id = upload["public_id"]
105
107
  index += 1
106
108
  end
@@ -108,19 +110,11 @@ class Cloudinary::Uploader
108
110
  end
109
111
 
110
112
 
111
- # Upload large raw files. Note that public_id should include an extension for best results.
113
+ # Upload large files. Note that public_id should include an extension for best results.
112
114
  def self.upload_large_part(file, options={})
113
- call_api("upload_large", options.merge(:resource_type=>:raw)) do
114
- params = {
115
- :timestamp=>(options[:timestamp] || Time.now.to_i),
116
- :type=>options[:type],
117
- :public_id=>options[:public_id],
118
- :backup=>options[:backup],
119
- :final=>options[:final],
120
- :part_number=>options[:part_number],
121
- :tags=>options[:tags] && Cloudinary::Utils.build_array(options[:tags]).join(","),
122
- :upload_id=>options[:upload_id]
123
- }
115
+ options[:resource_type] ||= :raw
116
+ call_api("upload_chunked", options) do
117
+ params = build_upload_params(options)
124
118
  if file.is_a?(Pathname) || !file.respond_to?(:read)
125
119
  params[:file] = File.open(file, "rb")
126
120
  else
@@ -171,6 +165,8 @@ class Cloudinary::Uploader
171
165
  :public_id=> public_id,
172
166
  :callback=> options[:callback],
173
167
  :eager=>build_eager(options[:eager]),
168
+ :eager_notification_url=>options[:eager_notification_url],
169
+ :eager_async=>Cloudinary::Utils.as_safe_bool(options[:eager_async]),
174
170
  :headers=>build_custom_headers(options[:headers]),
175
171
  :tags=>options[:tags] && Cloudinary::Utils.build_array(options[:tags]).join(","),
176
172
  :face_coordinates => options[:face_coordinates] && Cloudinary::Utils.encode_double_array(options[:face_coordinates])
@@ -277,12 +273,14 @@ class Cloudinary::Uploader
277
273
  params[:signature] = Cloudinary::Utils.api_sign_request(params.reject{|k,v| non_signable.include?(k)}, api_secret)
278
274
  params[:api_key] = api_key
279
275
  end
276
+ timeout = options[:timeout] || Cloudinary.config.timeout || 60
280
277
 
281
278
  result = nil
282
279
 
283
280
  api_url = Cloudinary::Utils.cloudinary_api_url(action, options)
284
-
285
- RestClient::Request.execute(:method => :post, :url => api_url, :payload => params.reject{|k, v| v.nil? || v==""}, :timeout=>60, :headers => {"User-Agent" => Cloudinary::USER_AGENT}) do
281
+ headers = {"User-Agent" => Cloudinary::USER_AGENT}
282
+ headers['Content-Range'] = options[:content_range] if options[:content_range]
283
+ RestClient::Request.execute(:method => :post, :url => api_url, :payload => params.reject{|k, v| v.nil? || v==""}, :timeout=> timeout, :headers => headers) do
286
284
  |response, request, tmpresult|
287
285
  raise CloudinaryException, "Server returned unexpected status code - #{response.code} - #{response.body}" if ![200,400,401,403,404,500].include?(response.code)
288
286
  begin
@@ -6,9 +6,9 @@ require 'aws_cf_signer'
6
6
 
7
7
  class Cloudinary::Utils
8
8
  # @deprecated Use Cloudinary::SHARED_CDN
9
- SHARED_CDN = Cloudinary::SHARED_CDN
9
+ SHARED_CDN = Cloudinary::SHARED_CDN
10
10
  DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION = {:width => :auto, :crop => :limit}
11
-
11
+
12
12
  # Warning: options are being destructively updated!
13
13
  def self.generate_transformation_string(options={})
14
14
  if options.is_a?(Array)
@@ -16,21 +16,21 @@ class Cloudinary::Utils
16
16
  end
17
17
  # Symbolize keys
18
18
  options.keys.each do |key|
19
- options[key.to_sym] = options.delete(key) if key.is_a?(String)
19
+ options[(key.to_sym rescue key)] = options.delete(key)
20
20
  end
21
21
 
22
22
  responsive_width = config_option_consume(options, :responsive_width)
23
23
  size = options.delete(:size)
24
- options[:width], options[:height] = size.split("x") if size
24
+ options[:width], options[:height] = size.split("x") if size
25
25
  width = options[:width]
26
26
  width = width.to_s if width.is_a?(Symbol)
27
27
  height = options[:height]
28
- has_layer = !options[:overlay].blank? || !options[:underlay].blank?
29
-
28
+ has_layer = options[:overlay].present? || options[:underlay].present?
29
+
30
30
  crop = options.delete(:crop)
31
31
  angle = build_array(options.delete(:angle)).join(".")
32
32
 
33
- no_html_sizes = has_layer || !angle.blank? || crop.to_s == "fit" || crop.to_s == "limit" || crop.to_s == "lfill"
33
+ no_html_sizes = has_layer || angle.present? || crop.to_s == "fit" || crop.to_s == "limit" || crop.to_s == "lfill"
34
34
  options.delete(:width) if width && (width.to_f < 1 || no_html_sizes || width == "auto" || responsive_width)
35
35
  options.delete(:height) if height && (height.to_f < 1 || no_html_sizes || responsive_width)
36
36
 
@@ -41,40 +41,84 @@ class Cloudinary::Utils
41
41
 
42
42
  color = options.delete(:color)
43
43
  color = color.sub(/^#/, 'rgb:') if color
44
-
44
+
45
45
  base_transformations = build_array(options.delete(:transformation))
46
46
  if base_transformations.any?{|base_transformation| base_transformation.is_a?(Hash)}
47
47
  base_transformations = base_transformations.map do
48
48
  |base_transformation|
49
49
  base_transformation.is_a?(Hash) ? generate_transformation_string(base_transformation.clone) : generate_transformation_string(:transformation=>base_transformation)
50
50
  end
51
- else
51
+ else
52
52
  named_transformation = base_transformations.join(".")
53
53
  base_transformations = []
54
54
  end
55
-
55
+
56
56
  effect = options.delete(:effect)
57
57
  effect = Array(effect).flatten.join(":") if effect.is_a?(Array) || effect.is_a?(Hash)
58
-
58
+
59
59
  border = options.delete(:border)
60
60
  if border.is_a?(Hash)
61
61
  border = "#{border[:width] || 2}px_solid_#{(border[:color] || "black").sub(/^#/, 'rgb:')}"
62
- elsif border.to_s =~ /^\d+$/ # fallback to html border attribute
62
+ elsif border.to_s =~ /^\d+$/ # fallback to html border attribute
63
63
  options[:border] = border
64
64
  border = nil
65
65
  end
66
66
  flags = build_array(options.delete(:flags)).join(".")
67
67
  dpr = config_option_consume(options, :dpr)
68
-
69
- params = {:w=>width, :h=>height, :t=>named_transformation, :c=>crop, :b=>background, :e=>effect, :a=>angle, :bo=>border, :fl=>flags, :co=>color, :dpr=>dpr}
70
- { :x=>:x, :y=>:y, :r=>:radius, :d=>:default_image, :g=>:gravity, :q=>:quality, :cs=>:color_space, :o=>:opacity,
71
- :p=>:prefix, :l=>:overlay, :u=>:underlay, :f=>:fetch_format, :dn=>:density, :pg=>:page, :dl=>:delay
68
+
69
+ if options.include? :offset
70
+ options[:start_offset], options[:end_offset] = split_range options.delete(:offset)
71
+ end
72
+
73
+ params = {
74
+ :a => angle,
75
+ :b => background,
76
+ :bo => border,
77
+ :c => crop,
78
+ :co => color,
79
+ :dpr => dpr,
80
+ :e => effect,
81
+ :fl => flags,
82
+ :h => height,
83
+ :t => named_transformation,
84
+ :w => width
85
+ }
86
+ {
87
+ :ac => :audio_codec,
88
+ :br => :bit_rate,
89
+ :cs => :color_space,
90
+ :d => :default_image,
91
+ :dl => :delay,
92
+ :dn => :density,
93
+ :du => :duration,
94
+ :eo => :end_offset,
95
+ :f => :fetch_format,
96
+ :g => :gravity,
97
+ :l => :overlay,
98
+ :o => :opacity,
99
+ :p => :prefix,
100
+ :pg => :page,
101
+ :q => :quality,
102
+ :r => :radius,
103
+ :af => :audio_frequency,
104
+ :so => :start_offset,
105
+ :u => :underlay,
106
+ :vc => :video_codec,
107
+ :vs => :video_sampling,
108
+ :x => :x,
109
+ :y => :y,
110
+ :z => :zoom
72
111
  }.each do
73
112
  |param, option|
74
113
  params[param] = options.delete(option)
75
- end
114
+ end
115
+
116
+ params[:vc] = process_video_params params[:vc] if params[:vc].present?
117
+ [:so, :eo, :du].each do |range_value|
118
+ params[range_value] = norm_range_value params[range_value] if params[range_value].present?
119
+ end
76
120
 
77
- transformation = params.reject{|k,v| v.blank?}.map{|k,v| [k.to_s, v]}.sort_by(&:first).map{|k,v| "#{k}_#{v}"}.join(",")
121
+ transformation = params.reject{|_k,v| v.blank?}.map{|k,v| "#{k}_#{v}"}.sort.join(",")
78
122
  raw_transformation = options.delete(:raw_transformation)
79
123
  transformation = [transformation, raw_transformation].reject(&:blank?).join(",")
80
124
  transformations = base_transformations << transformation
@@ -90,13 +134,13 @@ class Cloudinary::Utils
90
134
  options[:hidpi] = true
91
135
  end
92
136
 
93
- transformations.reject(&:blank?).join("/")
137
+ transformations.reject(&:blank?).join("/")
94
138
  end
95
-
139
+
96
140
  def self.api_string_to_sign(params_to_sign)
97
141
  params_to_sign.map{|k,v| [k.to_s, v.is_a?(Array) ? v.join(",") : v]}.reject{|k,v| v.nil? || v == ""}.sort_by(&:first).map{|k,v| "#{k}=#{v}"}.join("&")
98
142
  end
99
-
143
+
100
144
  def self.api_sign_request(params_to_sign, api_secret)
101
145
  to_sign = api_string_to_sign(params_to_sign)
102
146
  Digest::SHA1.hexdigest("#{to_sign}#{api_secret}")
@@ -107,24 +151,24 @@ class Cloudinary::Utils
107
151
 
108
152
  type = options.delete(:type)
109
153
 
110
- options[:fetch_format] ||= options.delete(:format) if type == :fetch
154
+ options[:fetch_format] ||= options.delete(:format) if type == :fetch
111
155
  transformation = self.generate_transformation_string(options)
112
156
 
113
157
  resource_type = options.delete(:resource_type) || "image"
114
158
  version = options.delete(:version)
115
159
  format = options.delete(:format)
116
160
  cloud_name = config_option_consume(options, :cloud_name) || raise(CloudinaryException, "Must supply cloud_name in tag or in configuration")
117
-
161
+
118
162
  secure = options.delete(:secure)
119
163
  ssl_detected = options.delete(:ssl_detected)
120
164
  secure = ssl_detected || Cloudinary.config.secure if secure.nil?
121
- private_cdn = config_option_consume(options, :private_cdn)
122
- secure_distribution = config_option_consume(options, :secure_distribution)
123
- cname = config_option_consume(options, :cname)
124
- shorten = config_option_consume(options, :shorten)
165
+ private_cdn = config_option_consume(options, :private_cdn)
166
+ secure_distribution = config_option_consume(options, :secure_distribution)
167
+ cname = config_option_consume(options, :cname)
168
+ shorten = config_option_consume(options, :shorten)
125
169
  force_remote = options.delete(:force_remote)
126
- cdn_subdomain = config_option_consume(options, :cdn_subdomain)
127
- secure_cdn_subdomain = config_option_consume(options, :secure_cdn_subdomain)
170
+ cdn_subdomain = config_option_consume(options, :cdn_subdomain)
171
+ secure_cdn_subdomain = config_option_consume(options, :secure_cdn_subdomain)
128
172
  sign_url = config_option_consume(options, :sign_url)
129
173
  secret = config_option_consume(options, :api_secret)
130
174
  sign_version = config_option_consume(options, :sign_version) # Deprecated behavior
@@ -136,33 +180,33 @@ class Cloudinary::Utils
136
180
  original_source = source
137
181
  return original_source if source.blank?
138
182
  if defined?(CarrierWave::Uploader::Base) && source.is_a?(CarrierWave::Uploader::Base)
139
- source = format.blank? ? source.filename : source.full_public_id
183
+ source = format.blank? ? source.filename : source.full_public_id
140
184
  end
141
185
  source = source.to_s
142
- if !force_remote
186
+ if !force_remote
143
187
  return original_source if (type.nil? || type == :asset) && source.match(%r(^https?:/)i)
144
- if source.start_with?("/")
188
+ if source.start_with?("/")
145
189
  if source.start_with?("/images/")
146
190
  source = source.sub(%r(/images/), '')
147
191
  else
148
192
  return original_source
149
193
  end
150
- end
194
+ end
151
195
  @metadata ||= defined?(Cloudinary::Static) ? Cloudinary::Static.metadata : {}
152
196
  if type == :asset && @metadata["images/#{source}"]
153
- return original_source if !Cloudinary.config.static_image_support
197
+ return original_source if !Cloudinary.config.static_image_support
154
198
  source = @metadata["images/#{source}"]["public_id"]
155
199
  source += File.extname(original_source) if !format
156
200
  elsif type == :asset
157
201
  return original_source # requested asset, but no metadata - probably local file. return.
158
202
  end
159
203
  end
160
-
204
+
161
205
  resource_type, type = finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten)
162
206
  source, source_to_sign = finalize_source(source, format, url_suffix)
163
-
164
- version ||= 1 if source_to_sign.include?("/") and !source_to_sign.match(/^v[0-9]+/) and !source_to_sign.match(/^https?:\//)
165
- version &&= "v#{version}"
207
+
208
+ version ||= 1 if source_to_sign.include?("/") and !source_to_sign.match(/^v[0-9]+/) and !source_to_sign.match(/^https?:\//)
209
+ version &&= "v#{version}"
166
210
 
167
211
  transformation = transformation.gsub(%r(([^:])//), '\1/')
168
212
  if sign_url
@@ -180,19 +224,19 @@ class Cloudinary::Utils
180
224
  source = smart_escape(source)
181
225
  source_to_sign = source
182
226
  else
183
- source = smart_escape(URI.decode(source))
227
+ source = smart_escape(URI.decode(source))
184
228
  source_to_sign = source
185
229
  unless url_suffix.blank?
186
230
  raise(CloudinaryException, "url_suffix should not include . or /") if url_suffix.match(%r([\./]))
187
- source = "#{source}/#{url_suffix}"
231
+ source = "#{source}/#{url_suffix}"
188
232
  end
189
233
  if !format.blank?
190
- source = "#{source}.#{format}"
191
- source_to_sign = "#{source_to_sign}.#{format}"
234
+ source = "#{source}.#{format}"
235
+ source_to_sign = "#{source_to_sign}.#{format}"
192
236
  end
193
237
  end
194
238
  [source, source_to_sign]
195
- end
239
+ end
196
240
 
197
241
  def self.finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten)
198
242
  type ||= :upload
@@ -200,7 +244,7 @@ class Cloudinary::Utils
200
244
  if resource_type.to_s == "image" && type.to_s == "upload"
201
245
  resource_type = "images"
202
246
  type = nil
203
- elsif resource_type.to_s == "raw" && type.to_s == "upload"
247
+ elsif resource_type.to_s == "raw" && type.to_s == "upload"
204
248
  resource_type = "files"
205
249
  type = nil
206
250
  else
@@ -220,12 +264,12 @@ class Cloudinary::Utils
220
264
  type = nil
221
265
  end
222
266
  [resource_type, type]
223
- end
224
-
267
+ end
268
+
225
269
  # cdn_subdomain and secure_cdn_subdomain
226
270
  # 1) Customers in shared distribution (e.g. res.cloudinary.com)
227
271
  # if cdn_domain is true uses res-[1-5].cloudinary.com for both http and https. Setting secure_cdn_subdomain to false disables this for https.
228
- # 2) Customers with private cdn
272
+ # 2) Customers with private cdn
229
273
  # if cdn_domain is true uses cloudname-res-[1-5].cloudinary.com for http
230
274
  # if secure_cdn_domain is true uses cloudname-res-[1-5].cloudinary.com for https (please contact support if you require this)
231
275
  # 3) Customers with cname
@@ -252,7 +296,7 @@ class Cloudinary::Utils
252
296
  prefix = "http://#{subdomain}#{cname}"
253
297
  else
254
298
  host = [private_cdn ? "#{cloud_name}-" : "", "res", cdn_subdomain ? "-#{(Zlib::crc32(source) % 5) + 1}" : "", ".cloudinary.com"].join
255
- prefix = "http://#{host}"
299
+ prefix = "http://#{host}"
256
300
  end
257
301
  prefix += "/#{cloud_name}" if shared_domain
258
302
 
@@ -274,23 +318,23 @@ class Cloudinary::Utils
274
318
  params[:api_key] = api_key
275
319
  params
276
320
  end
277
-
321
+
278
322
  def self.private_download_url(public_id, format, options = {})
279
323
  cloudinary_params = sign_request({
280
- :timestamp=>Time.now.to_i,
281
- :public_id=>public_id,
282
- :format=>format,
324
+ :timestamp=>Time.now.to_i,
325
+ :public_id=>public_id,
326
+ :format=>format,
283
327
  :type=>options[:type],
284
- :attachment=>options[:attachment],
328
+ :attachment=>options[:attachment],
285
329
  :expires_at=>options[:expires_at] && options[:expires_at].to_i
286
330
  }, options)
287
-
288
- return Cloudinary::Utils.cloudinary_api_url("download", options) + "?" + cloudinary_params.to_query
331
+
332
+ return Cloudinary::Utils.cloudinary_api_url("download", options) + "?" + cloudinary_params.to_query
289
333
  end
290
334
 
291
335
  def self.zip_download_url(tag, options = {})
292
336
  cloudinary_params = sign_request({:timestamp=>Time.now.to_i, :tag=>tag, :transformation=>generate_transformation_string(options)}, options)
293
- return Cloudinary::Utils.cloudinary_api_url("download_tag.zip", options) + "?" + cloudinary_params.to_query
337
+ return Cloudinary::Utils.cloudinary_api_url("download_tag.zip", options) + "?" + cloudinary_params.to_query
294
338
  end
295
339
 
296
340
  def self.signed_download_url(public_id, options = {})
@@ -303,7 +347,7 @@ class Cloudinary::Utils
303
347
  expires_at = options[:expires_at] || (Time.now+3600)
304
348
  signer.sign(url, :ending => expires_at)
305
349
  end
306
-
350
+
307
351
  def self.cloudinary_url(public_id, options = {})
308
352
  if options[:type].to_s == 'authenticated' && !options[:sign_url]
309
353
  result = signed_download_url(public_id, options)
@@ -318,16 +362,16 @@ class Cloudinary::Utils
318
362
  ext = path.extname
319
363
  md5 = Digest::MD5.hexdigest(data)
320
364
  public_id = "#{path.basename(ext)}-#{md5}"
321
- "#{public_id}#{ext}"
365
+ "#{public_id}#{ext}"
322
366
  end
323
-
324
- # Based on CGI::unescape. In addition does not escape / :
367
+
368
+ # Based on CGI::unescape. In addition does not escape / :
325
369
  def self.smart_escape(string)
326
370
  string.gsub(/([^a-zA-Z0-9_.\-\/:]+)/) do
327
371
  '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
328
372
  end
329
373
  end
330
-
374
+
331
375
  def self.random_public_id
332
376
  sr = defined?(ActiveSupport::SecureRandom) ? ActiveSupport::SecureRandom : SecureRandom
333
377
  sr.base64(20).downcase.gsub(/[^a-z0-9]/, "").sub(/^[0-9]+/, '')[0,20]
@@ -336,7 +380,7 @@ class Cloudinary::Utils
336
380
  def self.signed_preloaded_image(result)
337
381
  "#{result["resource_type"]}/#{result["type"] || "upload"}/v#{result["version"]}/#{[result["public_id"], result["format"]].reject(&:blank?).join(".")}##{result["signature"]}"
338
382
  end
339
-
383
+
340
384
  @@json_decode = false
341
385
  def self.json_decode(str)
342
386
  if !@@json_decode
@@ -347,7 +391,7 @@ class Cloudinary::Utils
347
391
  begin
348
392
  require 'active_support/json'
349
393
  rescue LoadError
350
- raise LoadError, "Please add the json gem or active_support to your Gemfile"
394
+ raise LoadError, "Please add the json gem or active_support to your Gemfile"
351
395
  end
352
396
  end
353
397
  end
@@ -361,7 +405,7 @@ class Cloudinary::Utils
361
405
  else [array]
362
406
  end
363
407
  end
364
-
408
+
365
409
  def self.encode_hash(hash)
366
410
  case hash
367
411
  when Hash then hash.map{|k,v| "#{k}=#{v}"}.join("|")
@@ -369,7 +413,7 @@ class Cloudinary::Utils
369
413
  else hash
370
414
  end
371
415
  end
372
-
416
+
373
417
  def self.encode_double_array(array)
374
418
  array = build_array(array)
375
419
  if array.length > 0 && array[0].is_a?(Array)
@@ -378,24 +422,24 @@ class Cloudinary::Utils
378
422
  return array.join(",")
379
423
  end
380
424
  end
381
-
382
- IMAGE_FORMATS = %w(bmp png tif tiff jpg jpeg gif pdf ico eps jpc jp2 psd)
383
-
425
+
426
+ IMAGE_FORMATS = %w(bmp png tif tiff jpg jpeg gif pdf ico eps jpc jp2 psd)
427
+
384
428
  def self.supported_image_format?(format)
385
429
  format = format.to_s.downcase
386
430
  extension = format =~ /\./ ? format.split('.').last : format
387
431
  IMAGE_FORMATS.include?(extension)
388
432
  end
389
-
433
+
390
434
  def self.resource_type_for_format(format)
391
435
  self.supported_image_format?(format) ? 'image' : 'raw'
392
436
  end
393
-
394
- def self.config_option_consume(options, option_name, default_value = nil)
437
+
438
+ def self.config_option_consume(options, option_name, default_value = nil)
395
439
  return options.delete(option_name) if options.include?(option_name)
396
- return Cloudinary.config.send(option_name) || default_value
440
+ return Cloudinary.config.send(option_name) || default_value
397
441
  end
398
-
442
+
399
443
  def self.as_bool(value)
400
444
  case value
401
445
  when nil then nil
@@ -408,7 +452,7 @@ class Cloudinary::Utils
408
452
  raise "Invalid boolean value #{value} of type #{value.class}"
409
453
  end
410
454
  end
411
-
455
+
412
456
  def self.as_safe_bool(value)
413
457
  case as_bool(value)
414
458
  when nil then nil
@@ -416,8 +460,76 @@ class Cloudinary::Utils
416
460
  when FalseClass then 0
417
461
  end
418
462
  end
419
-
463
+
420
464
  def self.safe_blank?(value)
421
465
  value.nil? || value == "" || value == []
422
466
  end
467
+
468
+ private
469
+ def self.number_pattern
470
+ "([0-9]*)\\.([0-9]+)|([0-9]+)"
471
+ end
472
+
473
+ def self.offset_any_pattern
474
+ "(#{number_pattern})([%pP])?"
475
+ end
476
+
477
+ def self.offset_any_pattern_re
478
+ /((([0-9]*)\.([0-9]+)|([0-9]+))([%pP])?)\.\.((([0-9]*)\.([0-9]+)|([0-9]+))([%pP])?)/
479
+ end
480
+
481
+ # Split a range into the start and end values
482
+ def self.split_range(range) # :nodoc:
483
+ case range
484
+ when Range
485
+ [range.first, range.last]
486
+ when String
487
+ range.split ".." if offset_any_pattern_re =~ range
488
+ when Array
489
+ [range.first, range.last]
490
+ else
491
+ nil
492
+ end
493
+ end
494
+
495
+ # Normalize an offset value
496
+ # @param [String] value a decimal value which may have a 'p' or '%' postfix. E.g. '35%', '0.4p'
497
+ # @return [Object|String] a normalized String of the input value if possible otherwise the value itself
498
+ def self.norm_range_value(value) # :nodoc:
499
+ offset = /^#{offset_any_pattern}$/.match( value.to_s)
500
+ if offset
501
+ modifier = offset[5].present? ? 'p' : ''
502
+ value = "#{offset[1]}#{modifier}"
503
+ end
504
+ value
505
+ end
506
+
507
+ # A video codec parameter can be either a String or a Hash.
508
+ #
509
+ # @param [Object] param <code>vc_<codec>[ : <profile> : [<level>]]</code>
510
+ # or <code>{ codec: 'h264', profile: 'basic', level: '3.1' }</code>
511
+ # @return [String] <code><codec> : <profile> : [<level>]]</code> if a Hash was provided
512
+ # or the param if a String was provided.
513
+ # Returns NIL if param is not a Hash or String
514
+ def self.process_video_params(param)
515
+ case param
516
+ when Hash
517
+ video = ""
518
+ if param.has_key? :codec
519
+ video = param[:codec]
520
+ if param.has_key? :profile
521
+ video.concat ":" + param[:profile]
522
+ if param.has_key? :level
523
+ video.concat ":" + param[:level]
524
+ end
525
+ end
526
+ end
527
+ video
528
+ when String
529
+ param
530
+ else
531
+ nil
532
+ end
533
+ end
534
+
423
535
  end