cloudinary 1.11.1 → 1.18.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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +42 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +21 -0
  4. data/.github/pull_request_template.md +24 -0
  5. data/.gitignore +6 -1
  6. data/.travis.yml +15 -5
  7. data/CHANGELOG.md +149 -0
  8. data/Rakefile +3 -45
  9. data/cloudinary.gemspec +20 -4
  10. data/lib/active_storage/blob_key.rb +20 -0
  11. data/lib/active_storage/service/cloudinary_service.rb +229 -0
  12. data/lib/cloudinary.rb +31 -22
  13. data/lib/cloudinary/api.rb +173 -5
  14. data/lib/cloudinary/auth_token.rb +6 -4
  15. data/lib/cloudinary/carrier_wave.rb +3 -1
  16. data/lib/cloudinary/carrier_wave/remote.rb +3 -2
  17. data/lib/cloudinary/carrier_wave/storage.rb +2 -1
  18. data/lib/cloudinary/cloudinary_controller.rb +2 -4
  19. data/lib/cloudinary/helper.rb +30 -3
  20. data/lib/cloudinary/railtie.rb +7 -3
  21. data/lib/cloudinary/uploader.rb +35 -6
  22. data/lib/cloudinary/utils.rb +112 -40
  23. data/lib/cloudinary/version.rb +1 -1
  24. data/lib/cloudinary/video_helper.rb +96 -22
  25. data/lib/tasks/cloudinary/fetch_assets.rake +48 -0
  26. data/lib/tasks/{cloudinary.rake → cloudinary/sync_static.rake} +0 -0
  27. data/tools/allocate_test_cloud.sh +9 -0
  28. data/tools/get_test_cloud.sh +9 -0
  29. data/tools/update_version +29 -11
  30. data/vendor/assets/javascripts/cloudinary/jquery.cloudinary.js +48 -14
  31. data/vendor/assets/javascripts/cloudinary/jquery.fileupload.js +24 -4
  32. data/vendor/assets/javascripts/cloudinary/jquery.ui.widget.js +741 -561
  33. data/vendor/assets/javascripts/cloudinary/load-image.all.min.js +1 -1
  34. metadata +62 -67
  35. data/spec/access_control_spec.rb +0 -102
  36. data/spec/api_spec.rb +0 -567
  37. data/spec/archive_spec.rb +0 -129
  38. data/spec/auth_token_spec.rb +0 -77
  39. data/spec/cache_spec.rb +0 -109
  40. data/spec/cloudinary_helper_spec.rb +0 -325
  41. data/spec/cloudinary_spec.rb +0 -32
  42. data/spec/data/sync_static/app/assets/javascripts/1.coffee +0 -1
  43. data/spec/data/sync_static/app/assets/javascripts/1.js +0 -1
  44. data/spec/data/sync_static/app/assets/stylesheets/1.css +0 -3
  45. data/spec/docx.docx +0 -0
  46. data/spec/favicon.ico +0 -0
  47. data/spec/image_spec.rb +0 -107
  48. data/spec/logo.png +0 -0
  49. data/spec/rake_spec.rb +0 -160
  50. data/spec/sample_asset_file.tsv +0 -4
  51. data/spec/search_spec.rb +0 -109
  52. data/spec/spec_helper.rb +0 -266
  53. data/spec/storage_spec.rb +0 -44
  54. data/spec/streaminig_profiles_api_spec.rb +0 -74
  55. data/spec/support/helpers/temp_file_helpers.rb +0 -22
  56. data/spec/support/shared_contexts/rake.rb +0 -19
  57. data/spec/uploader_spec.rb +0 -392
  58. data/spec/utils_methods_spec.rb +0 -54
  59. data/spec/utils_spec.rb +0 -970
  60. data/spec/video_tag_spec.rb +0 -253
  61. data/spec/video_url_spec.rb +0 -185
@@ -1,8 +1,12 @@
1
1
  class Cloudinary::Railtie < Rails::Railtie
2
2
  rake_tasks do
3
- Dir[File.join(File.dirname(__FILE__),'../tasks/*.rake')].each { |f| load f }
4
- end
3
+ Dir[File.join(File.dirname(__FILE__),'../tasks/**/*.rake')].each { |f| load f }
4
+ end
5
5
  config.after_initialize do |app|
6
6
  ActionView::Base.send :include, CloudinaryHelper
7
7
  end
8
- end
8
+
9
+ ActiveSupport.on_load(:action_controller_base) do
10
+ ActionController::Base.send :include, Cloudinary::CloudinaryController
11
+ end
12
+ end
@@ -5,8 +5,7 @@ require 'cloudinary/cache'
5
5
 
6
6
  class Cloudinary::Uploader
7
7
 
8
- REMOTE_URL_REGEX = %r(^ftp:|^https?:|^s3:|^data:[^;]*;base64,([a-zA-Z0-9\/+\n=]+)$)
9
-
8
+ REMOTE_URL_REGEX = Cloudinary::Utils::REMOTE_URL_REGEX
10
9
  # @deprecated use {Cloudinary::Utils.build_eager} instead
11
10
  def self.build_eager(eager)
12
11
  Cloudinary::Utils.build_eager(eager)
@@ -28,6 +27,7 @@ class Cloudinary::Uploader
28
27
  :backup => Cloudinary::Utils.as_safe_bool(options[:backup]),
29
28
  :callback => options[:callback],
30
29
  :categorization => options[:categorization],
30
+ :cinemagraph_analysis => Cloudinary::Utils.as_safe_bool(options[:cinemagraph_analysis]),
31
31
  :colors => Cloudinary::Utils.as_safe_bool(options[:colors]),
32
32
  :context => Cloudinary::Utils.encode_context(options[:context]),
33
33
  :custom_coordinates => Cloudinary::Utils.encode_double_array(options[:custom_coordinates]),
@@ -37,6 +37,7 @@ class Cloudinary::Uploader
37
37
  :eager_async => Cloudinary::Utils.as_safe_bool(options[:eager_async]),
38
38
  :eager_notification_url => options[:eager_notification_url],
39
39
  :exif => Cloudinary::Utils.as_safe_bool(options[:exif]),
40
+ :eval => options[:eval],
40
41
  :face_coordinates => Cloudinary::Utils.encode_double_array(options[:face_coordinates]),
41
42
  :faces => Cloudinary::Utils.as_safe_bool(options[:faces]),
42
43
  :folder => options[:folder],
@@ -64,6 +65,8 @@ class Cloudinary::Uploader
64
65
  :unique_filename => Cloudinary::Utils.as_safe_bool(options[:unique_filename]),
65
66
  :upload_preset => options[:upload_preset],
66
67
  :use_filename => Cloudinary::Utils.as_safe_bool(options[:use_filename]),
68
+ :accessibility_analysis => Cloudinary::Utils.as_safe_bool(options[:accessibility_analysis]),
69
+ :metadata => Cloudinary::Utils.encode_context(options[:metadata])
67
70
  }
68
71
  params
69
72
  end
@@ -77,7 +80,10 @@ class Cloudinary::Uploader
77
80
  params = build_upload_params(options)
78
81
  if file.is_a?(Pathname)
79
82
  params[:file] = File.open(file, "rb")
80
- elsif file.respond_to?(:read) || file.match(REMOTE_URL_REGEX)
83
+ elsif file.is_a?(StringIO)
84
+ file.rewind
85
+ params[:file] = Cloudinary::Blob.new(file.read, options)
86
+ elsif file.respond_to?(:read) || Cloudinary::Utils.is_remote?(file)
81
87
  params[:file] = file
82
88
  else
83
89
  params[:file] = File.open(file, "rb")
@@ -95,7 +101,7 @@ class Cloudinary::Uploader
95
101
  public_id = public_id_or_options
96
102
  options = old_options
97
103
  end
98
- if file.match(REMOTE_URL_REGEX)
104
+ if Cloudinary::Utils.is_remote?(file)
99
105
  return upload(file, options.merge(:public_id => public_id))
100
106
  elsif file.is_a?(Pathname) || !file.respond_to?(:read)
101
107
  filename = file
@@ -248,7 +254,7 @@ class Cloudinary::Uploader
248
254
  end
249
255
  end
250
256
 
251
- # options may include 'exclusive' (boolean) which causes clearing this tag from all other resources
257
+ # options may include 'exclusive' (boolean) which causes clearing this tag from all other resources
252
258
  def self.add_tag(tag, public_ids = [], options = {})
253
259
  exclusive = options.delete(:exclusive)
254
260
  command = exclusive ? "set_exclusive" : "add"
@@ -267,6 +273,29 @@ class Cloudinary::Uploader
267
273
  return self.call_tags_api(nil, "remove_all", public_ids, options)
268
274
  end
269
275
 
276
+ # Populates metadata fields with the given values. Existing values will be overwritten.
277
+ #
278
+ # Any metadata-value pairs given are merged with any existing metadata-value pairs
279
+ # (an empty value for an existing metadata field clears the value).
280
+ #
281
+ # @param [Hash] metadata A list of custom metadata fields (by external_id) and the values to assign to each of them.
282
+ # @param [Array] public_ids An array of Public IDs of assets uploaded to Cloudinary.
283
+ # @param [Hash] options
284
+ # @option options [String] :resource_type The type of file. Default: image. Valid values: image, raw, video.
285
+ # @option options [String] :type The storage type. Default: upload. Valid values: upload, private, authenticated
286
+ # @return mixed a list of public IDs that were updated
287
+ # @raise [Cloudinary::Api:Error]
288
+ def self.update_metadata(metadata, public_ids, options = {})
289
+ self.call_api("metadata", options) do
290
+ {
291
+ timestamp: (options[:timestamp] || Time.now.to_i),
292
+ type: options[:type],
293
+ public_ids: Cloudinary::Utils.build_array(public_ids),
294
+ metadata: Cloudinary::Utils.encode_context(metadata)
295
+ }
296
+ end
297
+ end
298
+
270
299
  private
271
300
 
272
301
  def self.call_tags_api(tag, command, public_ids = [], options = {})
@@ -317,7 +346,7 @@ class Cloudinary::Uploader
317
346
  params[:signature] = Cloudinary::Utils.api_sign_request(params.reject { |k, v| non_signable.include?(k) }, api_secret)
318
347
  params[:api_key] = api_key
319
348
  end
320
- timeout = options[:timeout] || Cloudinary.config.timeout || 60
349
+ timeout = options.fetch(:timeout) { Cloudinary.config.to_h.fetch(:timeout, 60) }
321
350
 
322
351
  result = nil
323
352
 
@@ -1,4 +1,6 @@
1
1
  # Copyright Cloudinary
2
+
3
+ # frozen_string_literal: true
2
4
  require 'digest/sha1'
3
5
  require 'zlib'
4
6
  require 'uri'
@@ -24,7 +26,8 @@ class Cloudinary::Utils
24
26
  "*" => 'mul',
25
27
  "/" => 'div',
26
28
  "+" => 'add',
27
- "-" => 'sub'
29
+ "-" => 'sub',
30
+ "^" => 'pow'
28
31
  }
29
32
 
30
33
  PREDEFINED_VARS = {
@@ -39,9 +42,32 @@ class Cloudinary::Utils
39
42
  "page_x" => "px",
40
43
  "page_y" => "py",
41
44
  "tags" => "tags",
45
+ "initial_duration" => "idu",
46
+ "duration" => "du",
42
47
  "width" => "w"
43
48
  }
44
49
 
50
+ SIMPLE_TRANSFORMATION_PARAMS = {
51
+ :ac => :audio_codec,
52
+ :af => :audio_frequency,
53
+ :br => :bit_rate,
54
+ :cs => :color_space,
55
+ :d => :default_image,
56
+ :dl => :delay,
57
+ :dn => :density,
58
+ :du => :duration,
59
+ :eo => :end_offset,
60
+ :f => :fetch_format,
61
+ :g => :gravity,
62
+ :ki => :keyframe_interval,
63
+ :p => :prefix,
64
+ :pg => :page,
65
+ :so => :start_offset,
66
+ :sp => :streaming_profile,
67
+ :vc => :video_codec,
68
+ :vs => :video_sampling
69
+ }.freeze
70
+
45
71
  URL_KEYS = %w[
46
72
  api_secret
47
73
  auth_token
@@ -113,6 +139,11 @@ class Cloudinary::Utils
113
139
  zoom
114
140
  ].map(&:to_sym)
115
141
 
142
+ REMOTE_URL_REGEX = %r(^ftp:|^https?:|^s3:|^gs:|^data:([\w-]+\/[\w-]+(\+[\w-]+)?)?(;[\w-]+=[\w-]+)*;base64,([a-zA-Z0-9\/+\n=]+)$)
143
+
144
+ LONG_URL_SIGNATURE_LENGTH = 32
145
+ SHORT_URL_SIGNATURE_LENGTH = 8
146
+
116
147
  def self.extract_config_params(options)
117
148
  options.select{|k,v| URL_KEYS.include?(k)}
118
149
  end
@@ -216,7 +247,7 @@ class Cloudinary::Utils
216
247
  :l => overlay,
217
248
  :o => normalize_expression(options.delete(:opacity)),
218
249
  :q => normalize_expression(options.delete(:quality)),
219
- :r => normalize_expression(options.delete(:radius)),
250
+ :r => process_radius(options.delete(:radius)),
220
251
  :t => named_transformation,
221
252
  :u => underlay,
222
253
  :w => normalize_expression(width),
@@ -224,26 +255,7 @@ class Cloudinary::Utils
224
255
  :y => normalize_expression(options.delete(:y)),
225
256
  :z => normalize_expression(options.delete(:zoom))
226
257
  }
227
- {
228
- :ac => :audio_codec,
229
- :af => :audio_frequency,
230
- :br => :bit_rate,
231
- :cs => :color_space,
232
- :d => :default_image,
233
- :dl => :delay,
234
- :dn => :density,
235
- :du => :duration,
236
- :eo => :end_offset,
237
- :f => :fetch_format,
238
- :g => :gravity,
239
- :ki => :keyframe_interval,
240
- :p => :prefix,
241
- :pg => :page,
242
- :so => :start_offset,
243
- :sp => :streaming_profile,
244
- :vc => :video_codec,
245
- :vs => :video_sampling
246
- }.each do
258
+ SIMPLE_TRANSFORMATION_PARAMS.each do
247
259
  |param, option|
248
260
  params[param] = options.delete(option)
249
261
  end
@@ -293,19 +305,17 @@ class Cloudinary::Utils
293
305
  # Translates the condition if provided.
294
306
  # @return [string] "if_" + ifValue
295
307
  # @private
296
- def self.process_if(ifValue)
297
- if ifValue
298
- ifValue = normalize_expression(ifValue)
299
-
300
- ifValue = "if_" + ifValue
301
- end
308
+ def self.process_if(if_value)
309
+ "if_" + normalize_expression(if_value) unless if_value.to_s.empty?
302
310
  end
303
311
 
304
- EXP_REGEXP = Regexp.new(PREDEFINED_VARS.keys.join("|")+'|('+CONDITIONAL_OPERATORS.keys.reverse.map { |k| Regexp.escape(k) }.join('|')+')(?=[ _])')
312
+ EXP_REGEXP = Regexp.new('(?<!\$)('+PREDEFINED_VARS.keys.join("|")+')'+'|('+CONDITIONAL_OPERATORS.keys.reverse.map { |k| Regexp.escape(k) }.join('|')+')(?=[ _])')
305
313
  EXP_REPLACEMENT = PREDEFINED_VARS.merge(CONDITIONAL_OPERATORS)
306
314
 
307
315
  def self.normalize_expression(expression)
308
- if expression =~ /^!.+!$/ # quoted string
316
+ if expression.nil?
317
+ ''
318
+ elsif expression.is_a?( String) && expression =~ /^!.+!$/ # quoted string
309
319
  expression
310
320
  else
311
321
  expression.to_s.gsub(EXP_REGEXP,EXP_REPLACEMENT).gsub(/[ _]+/, "_")
@@ -375,6 +385,17 @@ class Cloudinary::Utils
375
385
  end
376
386
  private_class_method :process_layer
377
387
 
388
+ # Parse radius options
389
+ # @return [string] radius transformation string
390
+ # @private
391
+ def self.process_radius(radius)
392
+ if radius.is_a?(Array) && !radius.length.between?(1, 4)
393
+ raise(CloudinaryException, "Invalid radius parameter")
394
+ end
395
+ Array(radius).map { |r| normalize_expression(r) }.join(":")
396
+ end
397
+ private_class_method :process_radius
398
+
378
399
  LAYER_KEYWORD_PARAMS =[
379
400
  [:font_weight ,"normal"],
380
401
  [:font_style ,"normal"],
@@ -395,6 +416,10 @@ class Cloudinary::Utils
395
416
  keywords.push("letter_spacing_#{letter_spacing}") unless letter_spacing.blank?
396
417
  line_spacing = layer[:line_spacing]
397
418
  keywords.push("line_spacing_#{line_spacing}") unless line_spacing.blank?
419
+ font_antialiasing = layer[:font_antialiasing]
420
+ keywords.push("antialias_#{font_antialiasing}") unless font_antialiasing.blank?
421
+ font_hinting = layer[:font_hinting]
422
+ keywords.push("hinting_#{font_hinting}") unless font_hinting.blank?
398
423
  if !font_size.blank? || !font_family.blank? || !keywords.empty?
399
424
  raise(CloudinaryException, "Must supply font_family for text in overlay/underlay") if font_family.blank?
400
425
  raise(CloudinaryException, "Must supply font_size for text in overlay/underlay") if font_size.blank?
@@ -455,6 +480,7 @@ class Cloudinary::Utils
455
480
 
456
481
  resource_type = options.delete(:resource_type)
457
482
  version = options.delete(:version)
483
+ force_version = config_option_consume(options, :force_version, true)
458
484
  format = options.delete(:format)
459
485
  cloud_name = config_option_consume(options, :cloud_name) || raise(CloudinaryException, "Must supply cloud_name in tag or in configuration")
460
486
 
@@ -474,6 +500,7 @@ class Cloudinary::Utils
474
500
  url_suffix = options.delete(:url_suffix)
475
501
  use_root_path = config_option_consume(options, :use_root_path)
476
502
  auth_token = config_option_consume(options, :auth_token)
503
+ long_url_signature = config_option_consume(options, :long_url_signature)
477
504
  unless auth_token == false
478
505
  auth_token = Cloudinary::AuthToken.merge_auth_token(Cloudinary.config.auth_token, auth_token)
479
506
  end
@@ -505,7 +532,12 @@ class Cloudinary::Utils
505
532
  resource_type, type = finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten)
506
533
  source, source_to_sign = finalize_source(source, format, url_suffix)
507
534
 
508
- version ||= 1 if source_to_sign.include?("/") and !source_to_sign.match(/^v[0-9]+/) and !source_to_sign.match(/^https?:\//)
535
+ if version.nil? && force_version &&
536
+ source_to_sign.include?("/") &&
537
+ !source_to_sign.match(/^v[0-9]+/) &&
538
+ !source_to_sign.match(/^https?:\//)
539
+ version = 1
540
+ end
509
541
  version &&= "v#{version}"
510
542
 
511
543
  transformation = transformation.gsub(%r(([^:])//), '\1/')
@@ -513,7 +545,7 @@ class Cloudinary::Utils
513
545
  raise(CloudinaryException, "Must supply api_secret") if (secret.nil? || secret.empty?)
514
546
  to_sign = [transformation, sign_version && version, source_to_sign].reject(&:blank?).join("/")
515
547
  to_sign = fully_unescape(to_sign)
516
- signature = 's--' + Base64.urlsafe_encode64(Digest::SHA1.digest(to_sign + secret))[0,8] + '--'
548
+ signature = compute_signature(to_sign, secret, long_url_signature)
517
549
  end
518
550
 
519
551
  prefix = unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution)
@@ -533,7 +565,7 @@ class Cloudinary::Utils
533
565
  source = smart_escape(source)
534
566
  source_to_sign = source
535
567
  else
536
- source = smart_escape(URI.decode(source))
568
+ source = smart_escape(smart_unescape(source))
537
569
  source_to_sign = source
538
570
  unless url_suffix.blank?
539
571
  raise(CloudinaryException, "url_suffix should not include . or /") if url_suffix.match(%r([\./]))
@@ -704,6 +736,18 @@ class Cloudinary::Utils
704
736
  download_archive_url(options.merge(:target_format => "zip"))
705
737
  end
706
738
 
739
+ # Creates and returns a URL that when invoked creates an archive of a folder.
740
+ #
741
+ # @param [Object] folder_path Full path (from the root) of the folder to download.
742
+ # @param [Hash] options Additional options.
743
+ #
744
+ # @return [String]
745
+ def self.download_folder(folder_path, options = {})
746
+ resource_type = options[:resource_type] || "all"
747
+
748
+ download_archive_url(options.merge(:resource_type => resource_type, :prefixes => folder_path))
749
+ end
750
+
707
751
  def self.signed_download_url(public_id, options = {})
708
752
  aws_private_key_path = options[:aws_private_key_path] || Cloudinary.config.aws_private_key_path
709
753
  if aws_private_key_path
@@ -737,13 +781,18 @@ class Cloudinary::Utils
737
781
  "#{public_id}#{ext}"
738
782
  end
739
783
 
740
- # Based on CGI::unescape. In addition does not escape / :
784
+ # Based on CGI::escape. In addition does not escape / :
741
785
  def self.smart_escape(string, unsafe = /([^a-zA-Z0-9_.\-\/:]+)/)
742
786
  string.gsub(unsafe) do |m|
743
787
  '%' + m.unpack('H2' * m.bytesize).join('%').upcase
744
788
  end
745
789
  end
746
790
 
791
+ # Based on CGI::unescape. In addition keeps '+' character as is
792
+ def self.smart_unescape(string)
793
+ CGI.unescape(string.sub('+', '%2B'))
794
+ end
795
+
747
796
  def self.random_public_id
748
797
  sr = defined?(ActiveSupport::SecureRandom) ? ActiveSupport::SecureRandom : SecureRandom
749
798
  sr.base64(20).downcase.gsub(/[^a-z0-9]/, "").sub(/^[0-9]+/, '')[0,20]
@@ -811,7 +860,7 @@ class Cloudinary::Utils
811
860
  end
812
861
  end
813
862
 
814
- IMAGE_FORMATS = %w(ai bmp bpg djvu eps eps3 flif gif hdp hpx ico j2k jp2 jpc jpe jpg miff pdf png psd svg tif tiff wdp webp zip )
863
+ IMAGE_FORMATS = %w(ai bmp bpg djvu eps eps3 flif gif hdp hpx ico j2k jp2 jpc jpe jpeg jpg miff pdf png psd svg tif tiff wdp webp zip )
815
864
 
816
865
  AUDIO_FORMATS = %w(aac aifc aiff flac m4a mp3 ogg wav)
817
866
 
@@ -831,10 +880,8 @@ class Cloudinary::Utils
831
880
  case
832
881
  when self.supported_format?(format, IMAGE_FORMATS)
833
882
  'image'
834
- when self.supported_format?(format, VIDEO_FORMATS)
883
+ when self.supported_format?(format, VIDEO_FORMATS), self.supported_format?(format, AUDIO_FORMATS)
835
884
  'video'
836
- when self.supported_format?(format, AUDIO_FORMATS)
837
- 'audio'
838
885
  else
839
886
  'raw'
840
887
  end
@@ -842,7 +889,8 @@ class Cloudinary::Utils
842
889
 
843
890
  def self.config_option_consume(options, option_name, default_value = nil)
844
891
  return options.delete(option_name) if options.include?(option_name)
845
- return Cloudinary.config.send(option_name) || default_value
892
+ option_value = Cloudinary.config.send(option_name)
893
+ option_value.nil? ? default_value : option_value
846
894
  end
847
895
 
848
896
  def self.as_bool(value)
@@ -851,7 +899,7 @@ class Cloudinary::Utils
851
899
  when String then value.downcase == "true" || value == "1"
852
900
  when TrueClass then true
853
901
  when FalseClass then false
854
- when Fixnum then value != 0
902
+ when Integer then value != 0
855
903
  when Symbol then value == :true
856
904
  else
857
905
  raise "Invalid boolean value #{value} of type #{value.class}"
@@ -928,6 +976,7 @@ class Cloudinary::Utils
928
976
  :keep_derived=>Cloudinary::Utils.as_safe_bool(options[:keep_derived]),
929
977
  :tags=>options[:tags] && Cloudinary::Utils.build_array(options[:tags]),
930
978
  :public_ids=>options[:public_ids] && Cloudinary::Utils.build_array(options[:public_ids]),
979
+ :fully_qualified_public_ids=>options[:fully_qualified_public_ids] && Cloudinary::Utils.build_array(options[:fully_qualified_public_ids]),
931
980
  :prefixes=>options[:prefixes] && Cloudinary::Utils.build_array(options[:prefixes]),
932
981
  :expires_at=>options[:expires_at],
933
982
  :transformations => build_eager(options[:transformations]),
@@ -1096,4 +1145,27 @@ class Cloudinary::Utils
1096
1145
  end
1097
1146
  end
1098
1147
 
1148
+ def self.is_remote?(url)
1149
+ REMOTE_URL_REGEX === url
1150
+ end
1151
+
1152
+ # Computes a short or long signature based on a message and secret
1153
+ # @param [String] message The string to sign
1154
+ # @param [String] secret A secret that will be added to the message when signing
1155
+ # @param [Boolean] long_signature Whether to create a short or long signature
1156
+ # @return [String] Properly formatted signature
1157
+ def self.compute_signature(message, secret, long_url_signature)
1158
+ combined_message_secret = message + secret
1159
+
1160
+ algo, signature_length =
1161
+ if long_url_signature
1162
+ [Digest::SHA256, LONG_URL_SIGNATURE_LENGTH]
1163
+ else
1164
+ [Digest::SHA1, SHORT_URL_SIGNATURE_LENGTH]
1165
+ end
1166
+
1167
+ "s--#{Base64.urlsafe_encode64(algo.digest(combined_message_secret))[0, signature_length]}--"
1168
+ end
1169
+
1170
+ private_class_method :compute_signature
1099
1171
  end
@@ -1,4 +1,4 @@
1
1
  # Copyright Cloudinary
2
2
  module Cloudinary
3
- VERSION = "1.11.1"
3
+ VERSION = "1.18.0"
4
4
  end
@@ -3,12 +3,33 @@ module CloudinaryHelper
3
3
  DEFAULT_POSTER_OPTIONS = { :format => 'jpg', :resource_type => 'video' }
4
4
  DEFAULT_SOURCE_TYPES = %w(webm mp4 ogv)
5
5
  DEFAULT_VIDEO_OPTIONS = { :resource_type => 'video' }
6
+ DEFAULT_SOURCES = [
7
+ {
8
+ :type => "mp4",
9
+ :codecs => "hev1",
10
+ :transformations => { :video_codec => "h265" }
11
+ },
12
+ {
13
+ :type => "webm",
14
+ :codecs => "vp9",
15
+ :transformations => { :video_codec => "vp9" }
16
+ },
17
+ {
18
+ :type => "mp4",
19
+ :transformations => { :video_codec => "auto" }
20
+ },
21
+ {
22
+ :type => "webm",
23
+ :transformations => { :video_codec => "auto" }
24
+ }
25
+ ]
6
26
 
7
27
  # Creates an HTML video tag for the provided +source+
8
28
  #
9
29
  # ==== Options
10
30
  # * <tt>:source_types</tt> - Specify which source type the tag should include. defaults to webm, mp4 and ogv.
11
31
  # * <tt>:source_transformation</tt> - specific transformations to use for a specific source type.
32
+ # * <tt>:sources</tt> - list of sources (overrides :source_types when present)
12
33
  # * <tt>:poster</tt> - override default thumbnail:
13
34
  # * url: provide an ad hoc url
14
35
  # * options: with specific poster transformations and/or Cloudinary +:public_id+
@@ -20,6 +41,17 @@ module CloudinaryHelper
20
41
  # cl_video_tag("mymovie.webm", :source_types => [:webm, :mp4], :poster => {:effect => 'sepia'}) do
21
42
  # content_tag( :span, "Cannot present video!")
22
43
  # end
44
+ # cl_video_tag("mymovie", :sources => [
45
+ # {
46
+ # :type => "mp4",
47
+ # :codecs => "hev1",
48
+ # :transformations => { :video_codec => "h265" }
49
+ # },
50
+ # {
51
+ # :type => "webm",
52
+ # :transformations => { :video_codec => "auto" }
53
+ # }
54
+ # ])
23
55
  def cl_video_tag(source, options = {}, &block)
24
56
  source = strip_known_ext(source)
25
57
  video_attributes = [:autoplay,:controls,:loop,:muted,:poster, :preload]
@@ -48,30 +80,12 @@ module CloudinaryHelper
48
80
  video_options[:poster] = cl_video_thumbnail_path(source, options)
49
81
  end
50
82
 
51
- source_transformation = options.delete(:source_transformation) || {}
52
- source_types = Array(options.delete(:source_types))
53
- fallback = (capture(&block) if block_given?) || options.delete(:fallback_content)
83
+ fallback = (capture(&block) if block_given?) || options.delete(:fallback_content)
54
84
 
55
- if source_types.size > 1
56
- cloudinary_tag(source, options) do |_source, tag_options|
57
- content_tag('video', tag_options.merge(video_options)) do
58
- source_tags = source_types.map do |type|
59
- transformation = source_transformation[type.to_sym] || {}
60
- cloudinary_tag("#{source}.#{type}", options.merge(transformation)) do |url, _tag_options|
61
- mime_type = "video/#{(type == 'ogv' ? 'ogg' : type)}"
62
- tag("source", :src => url, :type => mime_type)
63
- end
64
- end
65
- source_tags.push(fallback.html_safe) unless fallback.blank?
66
- safe_join(source_tags)
67
- end
68
- end
85
+ if options[:sources]
86
+ video_tag_from_sources(source, options, video_options, fallback)
69
87
  else
70
- transformation = source_transformation[source_types.first.to_sym] || {}
71
- video_options[:src] = cl_video_path("#{source}.#{source_types.first.to_sym}", transformation.merge(options))
72
- cloudinary_tag(source, options) do |_source, tag_options|
73
- content_tag('video', fallback, tag_options.merge(video_options))
74
- end
88
+ video_tag_from_source_types(source, options, video_options, fallback)
75
89
  end
76
90
  end
77
91
 
@@ -96,6 +110,66 @@ module CloudinaryHelper
96
110
  name.sub(/\.(#{DEFAULT_SOURCE_TYPES.join("|")})$/, '')
97
111
  end
98
112
 
113
+ private
114
+
115
+ def video_tag_from_source_types(source_name, options, video_options, fallback)
116
+ source_transformation = options.delete(:source_transformation) || {}
117
+ source_types = Array(options.delete(:source_types))
118
+
119
+ if source_types.size > 1
120
+ sources = source_types.map do |type|
121
+ {
122
+ :type => type,
123
+ :transformations => source_transformation[type.to_sym] || {}
124
+ }
125
+ end
126
+
127
+ generate_tag_from_sources(:source_name => source_name,
128
+ :sources => sources,
129
+ :options => options,
130
+ :video_options => video_options,
131
+ :fallback => fallback)
132
+ else
133
+ transformation = source_transformation[source_types.first.to_sym] || {}
134
+ video_options[:src] = cl_video_path("#{source_name}.#{source_types.first.to_sym}", transformation.merge(options))
135
+ cloudinary_tag(source_name, options) do |_source, tag_options|
136
+ content_tag('video', fallback, tag_options.merge(video_options))
137
+ end
138
+ end
139
+ end
140
+
141
+ def video_tag_from_sources(source_name, options, video_options, fallback)
142
+ sources = options.delete(:sources)
143
+
144
+ generate_tag_from_sources(:source_name => source_name,
145
+ :sources => sources,
146
+ :options => options,
147
+ :video_options => video_options,
148
+ :fallback => fallback)
149
+ end
150
+
151
+ def generate_tag_from_sources(params)
152
+ source_name, sources, options, video_options, fallback = params.values_at(:source_name, :sources, :options, :video_options, :fallback)
153
+
154
+ cloudinary_tag(source_name, options) do |_source, tag_options|
155
+ content_tag('video', tag_options.merge(video_options)) do
156
+ source_tags = sources.map do |source|
157
+ type = source[:type]
158
+ transformation = source[:transformations] || {}
159
+ cloudinary_tag("#{source_name}.#{type}", options.merge(transformation)) do |url, _tag_options|
160
+ mime_type = "video/#{(type == 'ogv' ? 'ogg' : type)}"
161
+ if source[:codecs]
162
+ codecs = source[:codecs].is_a?(Array) ? source[:codecs].join(", ") : source[:codecs]
163
+ mime_type = "#{mime_type}; codecs=#{codecs}"
164
+ end
165
+ tag("source", :src => url, :type => mime_type)
166
+ end
167
+ end
168
+ source_tags.push(fallback.html_safe) unless fallback.blank?
169
+ safe_join(source_tags)
170
+ end
171
+ end
172
+ end
99
173
  end
100
174
 
101
175