cloudinary 1.0.82 → 1.0.83

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.
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