Class: Cloudinary::Utils

Inherits:
Object show all
Defined in:
lib/cloudinary/utils.rb

Constant Summary collapse

MODE_DOWNLOAD =
"download"
DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION =
{:width => :auto, :crop => :limit}
CONDITIONAL_OPERATORS =
{
  "=" => 'eq',
  "!=" => 'ne',
  "<" => 'lt',
  ">" => 'gt',
  "<=" => 'lte',
  ">=" => 'gte',
  "&&" => 'and',
  "||" => 'or',
  "*" => 'mul',
  "/" => 'div',
  "+" => 'add',
  "-" => 'sub',
  "^" => 'pow'
}
PREDEFINED_VARS =
{
  "aspect_ratio"         => "ar",
  "aspectRatio"          => "ar",
  "current_page"         => "cp",
  "currentPage"          => "cp",
  "face_count"           => "fc",
  "faceCount"            => "fc",
  "height"               => "h",
  "initial_aspect_ratio" => "iar",
  "initialAspectRatio"   => "iar",
  "trimmed_aspect_ratio" => "tar",
  "trimmedAspectRatio"   => "tar",
  "initial_height"       => "ih",
  "initialHeight"        => "ih",
  "initial_width"        => "iw",
  "initialWidth"         => "iw",
  "page_count"           => "pc",
  "pageCount"            => "pc",
  "page_x"               => "px",
  "pageX"                => "px",
  "page_y"               => "py",
  "pageY"                => "py",
  "tags"                 => "tags",
  "initial_duration"     => "idu",
  "initialDuration"      => "idu",
  "duration"             => "du",
  "width"                => "w",
  "illustration_score"   => "ils",
  "illustrationScore"    => "ils",
  "context"              => "ctx"
}
SIMPLE_TRANSFORMATION_PARAMS =
{
  :ac => :audio_codec,
  :af => :audio_frequency,
  :br => :bit_rate,
  :cs => :color_space,
  :d  => :default_image,
  :dl => :delay,
  :dn => :density,
  :du => :duration,
  :eo => :end_offset,
  :f  => :fetch_format,
  :g  => :gravity,
  :ki => :keyframe_interval,
  :p  => :prefix,
  :pg => :page,
  :so => :start_offset,
  :sp => :streaming_profile,
  :vc => :video_codec,
  :vs => :video_sampling
}.freeze
URL_KEYS =
%w[
    api_secret
    auth_token
    cdn_subdomain
    cloud_name
    cname
    format
    private_cdn
    resource_type
    secure
    secure_cdn_subdomain
    secure_distribution
    shorten
    sign_url
    type
    url_suffix
    use_root_path
    version
].map(&:to_sym)
TRANSFORMATION_PARAMS =
%w[
    angle
    aspect_ratio
    audio_codec
    audio_frequency
    background
    bit_rate
    border
    color
    color_space
    crop
    custom_function
    default_image
    delay
    density
    dpr
    duration
    effect
    end_offset
    fetch_format
    flags
    fps
    gravity
    height
    if
    keyframe_interval
    offset
    opacity
    overlay
    page
    prefix
    quality
    radius
    raw_transformation
    responsive_width
    size
    start_offset
    streaming_profile
    transformation
    underlay
    variables
    video_codec
    video_sampling
    width
    x
    y
    zoom
].map(&:to_sym)
REMOTE_URL_REGEX =
%r(^ftp:|^https?:|^s3:|^gs:|^data:([\w-]+\/[\w-]+(\.[\w-]+)*(\+[\w-]+)?)?(;[\w-]+=[\w-]+)*;base64,([a-zA-Z0-9\/+\n=]+)$)
LONG_URL_SIGNATURE_LENGTH =
32
SHORT_URL_SIGNATURE_LENGTH =
8
UPLOAD_PREFIX =
'https://api.cloudinary.com'
ALGO_SHA1 =
:sha1
ALGO_SHA256 =
:sha256
ALGORITHM_SIGNATURE =
{
  ALGO_SHA1 => Digest::SHA1,
  ALGO_SHA256 => Digest::SHA256,
}
EXP_REGEXP =
Regexp.new('(\$_*[^_ ]+)|(?<![\$:])('+PREDEFINED_VARS.keys.join("|")+')'+'|('+CONDITIONAL_OPERATORS.keys.reverse.map { |k| Regexp.escape(k) }.join('|')+')(?=[ _])')
EXP_REPLACEMENT =
PREDEFINED_VARS.merge(CONDITIONAL_OPERATORS)
LAYER_KEYWORD_PARAMS =
[
  [:font_weight     ,"normal"],
  [:font_style      ,"normal"],
  [:text_decoration ,"none"],
  [:text_align      ,nil],
  [:stroke          ,"none"],
]
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 )
AUDIO_FORMATS =
%w(aac aifc aiff flac m4a mp3 ogg wav)
VIDEO_FORMATS =
%w(3g2 3gp asf avi flv h264 m2t m2v m3u8 mka mov mp4 mpeg ogv ts webm wmv )
@@json_decode =
false

Class Method Summary collapse

Class Method Details

.api_sign_request(params_to_sign, api_secret, signature_algorithm = nil, signature_version = nil) ⇒ String

Signs API request parameters

Parameters:

  • params_to_sign (Hash)

    Parameters to include in the signature

  • api_secret (String)

    API secret for signing

  • signature_algorithm (Symbol|nil) (defaults to: nil)

    Hash algorithm to use (:sha1 or :sha256)

  • signature_version (Integer|nil) (defaults to: nil)

    Version of signature algorithm to use:

    • Version 1: Original behavior without parameter encoding

    • Version 2+ (default): Includes parameter encoding to prevent parameter smuggling

Returns:

  • (String)

    Hexadecimal signature

[View source]

513
514
515
516
517
# File 'lib/cloudinary/utils.rb', line 513

def self.api_sign_request(params_to_sign, api_secret, signature_algorithm = nil, signature_version = nil)
  signature_version ||= Cloudinary.config.signature_version || 2
  to_sign = api_string_to_sign(params_to_sign, signature_version)
  hash("#{to_sign}#{api_secret}", signature_algorithm, :hexdigest)
end

.api_string_to_sign(params_to_sign, signature_version = 2) ⇒ String

Generates a string to be signed for API requests

Parameters:

  • params_to_sign (Hash)

    Parameters to include in the signature

  • signature_version (Integer) (defaults to: 2)

    Version of signature algorithm to use:

    • Version 1: Original behavior without parameter encoding

    • Version 2+ (default): Includes parameter encoding to prevent parameter smuggling

Returns:

  • (String)

    String to be signed

[View source]

492
493
494
495
496
497
498
499
500
501
# File 'lib/cloudinary/utils.rb', line 492

def self.api_string_to_sign(params_to_sign, signature_version = 2)
  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| 
                  param_string = "#{k}=#{v}"
                  signature_version >= 2 ? encode_param(param_string) : param_string
                }
                .join("&")
end

.archive_params(options = {}) ⇒ Object

Returns a Hash of parameters used to create an archive

Parameters:

  • options (Hash) (defaults to: {})
[View source]

1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
# File 'lib/cloudinary/utils.rb', line 1085

def self.archive_params(options = {})
  options = Cloudinary::Utils.symbolize_keys options
  {
    :timestamp=>(options[:timestamp] || Time.now.to_i),
    :type=>options[:type],
    :mode => options[:mode],
    :target_format => options[:target_format],
    :target_public_id=> options[:target_public_id],
    :flatten_folders=>Cloudinary::Utils.as_safe_bool(options[:flatten_folders]),
    :flatten_transformations=>Cloudinary::Utils.as_safe_bool(options[:flatten_transformations]),
    :use_original_filename=>Cloudinary::Utils.as_safe_bool(options[:use_original_filename]),
    :async=>Cloudinary::Utils.as_safe_bool(options[:async]),
    :notification_url=>options[:notification_url],
    :target_tags=>options[:target_tags] && Cloudinary::Utils.build_array(options[:target_tags]),
    :keep_derived=>Cloudinary::Utils.as_safe_bool(options[:keep_derived]),
    :tags=>options[:tags] && Cloudinary::Utils.build_array(options[:tags]),
    :public_ids=>options[:public_ids] && Cloudinary::Utils.build_array(options[:public_ids]),
    :fully_qualified_public_ids=>options[:fully_qualified_public_ids] && Cloudinary::Utils.build_array(options[:fully_qualified_public_ids]),
    :prefixes=>options[:prefixes] && Cloudinary::Utils.build_array(options[:prefixes]),
    :expires_at=>options[:expires_at],
    :transformations => build_eager(options[:transformations]),
    :skip_transformation_name=>Cloudinary::Utils.as_safe_bool(options[:skip_transformation_name]),
    :allow_missing=>Cloudinary::Utils.as_safe_bool(options[:allow_missing])
  }
end

.as_bool(value) ⇒ Object

[View source]

1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
# File 'lib/cloudinary/utils.rb', line 1019

def self.as_bool(value)
  case value
  when nil then nil
  when String then value.downcase == "true" || value == "1"
  when TrueClass then true
  when FalseClass then false
  when Integer then value != 0
  when Symbol then value == :true
  else
    raise "Invalid boolean value #{value} of type #{value.class}"
  end
end

.as_safe_bool(value) ⇒ Object

[View source]

1032
1033
1034
1035
1036
1037
1038
# File 'lib/cloudinary/utils.rb', line 1032

def self.as_safe_bool(value)
  case as_bool(value)
  when nil then nil
  when TrueClass then 1
  when FalseClass then 0
  end
end

.asset_file_name(path) ⇒ Object

[View source]

893
894
895
896
897
898
899
# File 'lib/cloudinary/utils.rb', line 893

def self.asset_file_name(path)
  data = Cloudinary.app_root.join(path).read(:mode=>"rb")
  ext = path.extname
  md5 = Digest::MD5.hexdigest(data)
  public_id = "#{path.basename(ext)}-#{md5}"
  "#{public_id}#{ext}"
end

.base_api_url(path, options = {}) ⇒ String

Creates a base URL for the cloudinary api

Parameters:

  • path (Object)

    Resource name

  • options (Hash) (defaults to: {})

    Additional options

Returns:

[View source]

769
770
771
772
773
774
775
# File 'lib/cloudinary/utils.rb', line 769

def self.base_api_url(path, options = {})
  cloudinary = options[:upload_prefix] || Cloudinary.config.upload_prefix || UPLOAD_PREFIX
  cloud_name = options[:cloud_name] || Cloudinary.config.cloud_name || raise(CloudinaryException, 'Must supply cloud_name')
  api_version = options[:api_version] || Cloudinary.config.api_version || 'v1_1'

  [cloudinary, api_version, cloud_name, path].join('/')
end

.build_array(array) ⇒ Object

[View source]

939
940
941
942
943
944
945
# File 'lib/cloudinary/utils.rb', line 939

def self.build_array(array)
  case array
    when Array then array
    when nil then []
    else [array]
  end
end

.build_distribution_domain(options = {}) ⇒ Object

[View source]

749
750
751
752
753
754
755
756
757
758
759
760
761
# File 'lib/cloudinary/utils.rb', line 749

def self.build_distribution_domain(options = {})
  cloud_name = config_option_consume(options, :cloud_name) || raise(CloudinaryException, "Must supply cloud_name in tag or in configuration")

  source = options.delete(:source)
  secure = config_option_consume(options, :secure, true)
  private_cdn = config_option_consume(options, :private_cdn)
  secure_distribution = config_option_consume(options, :secure_distribution)
  cname = config_option_consume(options, :cname)
  cdn_subdomain = config_option_consume(options, :cdn_subdomain)
  secure_cdn_subdomain = config_option_consume(options, :secure_cdn_subdomain)

  unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution)
end

.build_eager(eager) ⇒ Object

Parameters:

  • eager (String|Hash|Array)

    an transformation as a string or hash, with or without a format. The parameter also accepts an array of eager transformations.

[View source]

1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
# File 'lib/cloudinary/utils.rb', line 1114

def self.build_eager(eager)
  return nil if eager.nil?
  Cloudinary::Utils.build_array(eager).map do
  |transformation, format|
    unless transformation.is_a? String
      transformation = transformation.clone
      if transformation.respond_to?(:delete)
        format = transformation.delete(:format) || format
      end
      transformation = Cloudinary::Utils.generate_transformation_string(transformation, true)
    end
    [transformation, format].compact.join("/")
  end.join("|")
end

.chain_transformation(options, *transformation) ⇒ Object

[View source]

179
180
181
182
183
184
# File 'lib/cloudinary/utils.rb', line 179

def self.chain_transformation(options, *transformation)
  base_options = extract_config_params(options)
  transformation = transformation.reject(&:nil?)
  base_options[:transformation] = build_array(extract_transformation_params(options)).concat(transformation)
  base_options
end

.cloudinary_api_url(action = 'upload', options = {}) ⇒ Object

[View source]

777
778
779
780
781
# File 'lib/cloudinary/utils.rb', line 777

def self.cloudinary_api_url(action = 'upload', options = {})
  resource_type = options[:resource_type] || 'image'

  base_api_url([resource_type, action], options)
end

.cloudinary_url(source, options = {}) ⇒ Object

Warning: options are being destructively updated!

[View source]

552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
# File 'lib/cloudinary/utils.rb', line 552

def self.cloudinary_url(source, options = {})

  patch_fetch_format(options)
  type = options.delete(:type)

  transformation = self.generate_transformation_string(options)

  resource_type = options.delete(:resource_type)
  version = options.delete(:version)
  force_version = config_option_consume(options, :force_version, true)
  format = options.delete(:format)

  shorten = config_option_consume(options, :shorten)
  force_remote = options.delete(:force_remote)

  sign_url = config_option_consume(options, :sign_url)
  secret = config_option_consume(options, :api_secret)
  url_suffix = options.delete(:url_suffix)
  use_root_path = config_option_consume(options, :use_root_path)
  auth_token = config_option_consume(options, :auth_token)
  long_url_signature = config_option_consume(options, :long_url_signature)
  signature_algorithm = config_option_consume(options, :signature_algorithm)
  unless auth_token == false
    auth_token = Cloudinary::AuthToken.merge_auth_token(Cloudinary.config.auth_token, auth_token)
  end

  original_source = source
  return original_source if source.blank?
  if defined?(CarrierWave::Uploader::Base) && source.is_a?(CarrierWave::Uploader::Base)
    resource_type ||= source.resource_type
    type ||= source.storage_type
    source = format.blank? ? source.filename : source.full_public_id
  end
  type = type.to_s unless type.nil?
  resource_type ||= "image"
  source = source.to_s
  unless force_remote
    static_support = Cloudinary.config.static_file_support || Cloudinary.config.static_image_support
    return original_source if !static_support && type == "asset"
    return original_source if (type.nil? || type == "asset") && source.match(%r(^https?:/)i)
    return original_source if source.match(%r(^/(?!images/).*)) # starts with / but not /images/

    source = source.sub(%r(^/images/), '') # remove /images/ prefix  - backwards compatibility
    if type == "asset"
      source, resource_type = Cloudinary::Static.public_id_and_resource_type_from_path(source)
      return original_source unless source # asset not found in Static
      source += File.extname(original_source) unless format
    end
  end

  resource_type, type = finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten)
  source, source_to_sign = finalize_source(source, format, url_suffix)

  if version.nil? && force_version &&
       source_to_sign.include?("/") &&
       !source_to_sign.match(/^v[0-9]+/) &&
       !source_to_sign.match(/^https?:\//)
    version = 1
  end
  version &&= "v#{version}"

  transformation = transformation.gsub(%r(([^:])//), '\1/')
  if sign_url && ( !auth_token || auth_token.empty?)
    raise(CloudinaryException, "Must supply api_secret") if (secret.nil? || secret.empty?)
    to_sign = [transformation, source_to_sign].reject(&:blank?).join("/")
    to_sign = fully_unescape(to_sign)
    signature_algorithm = long_url_signature ? ALGO_SHA256 : signature_algorithm
    hash = hash("#{to_sign}#{secret}", signature_algorithm)
    signature = Base64.urlsafe_encode64(hash)
    signature = "s--#{signature[0, long_url_signature ? LONG_URL_SIGNATURE_LENGTH : SHORT_URL_SIGNATURE_LENGTH ]}--"
  end

  options[:source] = source
  prefix = build_distribution_domain(options)

  source = [prefix, resource_type, type, signature, transformation, version, source].reject(&:blank?).join("/")

  token = nil
  if sign_url && auth_token && !auth_token.empty?
    auth_token[:url] = URI.parse(source).path
    token = Cloudinary::AuthToken.generate auth_token
  end

  analytics = config_option_consume(options, :analytics, true)
  analytics_token = nil
  if analytics && ! original_source.include?("?") # Disable analytics for public IDs containing query params.
    analytics_token = Cloudinary::Analytics.sdk_analytics_query_param
  end

  query_params = [token, analytics_token].compact.join("&")

  source += "?#{query_params}" unless query_params.empty?
  source
end

.config_option_consume(options, option_name, default_value = nil) ⇒ Object

[View source]

1007
1008
1009
1010
1011
# File 'lib/cloudinary/utils.rb', line 1007

def self.config_option_consume(options, option_name, default_value = nil)
  return options.delete(option_name) if options.include?(option_name)
  option_value = Cloudinary.config.send(option_name)
  option_value.nil? ? default_value : option_value
end

.config_option_fetch(options, option_name, default_value = nil) ⇒ Object

[View source]

1013
1014
1015
1016
1017
# File 'lib/cloudinary/utils.rb', line 1013

def self.config_option_fetch(options, option_name, default_value = nil)
  return options.fetch(option_name) if options.include?(option_name)
  option_value = Cloudinary.config.send(option_name)
  option_value.nil? ? default_value : option_value
end

.deep_symbolize_keys(object) ⇒ Object

[View source]

1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
# File 'lib/cloudinary/utils.rb', line 1066

def self.deep_symbolize_keys(object)
  case object
  when Hash
    result = {}
    object.each do |key, value|
      key = key.to_sym rescue key
      result[key] = deep_symbolize_keys(value)
    end
    result
  when Array
    object.map{|e| deep_symbolize_keys(e)}
  else
    object
  end
end

.download_archive_url(options = {}) ⇒ String

Returns a URL that when invokes creates an archive and returns it.

Parameters:

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :resource_type (String|Symbol)

    The resource type of files to include in the archive. Must be one of :image | :video | :raw

  • :type (String|Symbol) — default: :upload

    The specific file type of resources: :upload|:private|:authenticated

  • :tags (String|Symbol|Array) — default: nil

    list of tags to include in the archive

  • :public_ids (String|Array<String>) — default: nil

    list of public_ids to include in the archive

  • :prefixes (String|Array<String>) — default: nil

    Optional list of prefixes of public IDs (e.g., folders).

  • :transformations (String|Array<String>)

    Optional list of transformations. The derived images of the given transformations are included in the archive. Using the string representation of multiple chained transformations as we use for the ‘eager’ upload parameter.

  • :mode (String|Symbol) — default: :create

    return the generated archive file or to store it as a raw resource and return a JSON with URLs for accessing the archive. Possible values: :download, :create

  • :target_format (String|Symbol) — default: :zip
  • :target_public_id (String)

    Optional public ID of the generated raw resource. Relevant only for the create mode. If not specified, random public ID is generated.

  • :flatten_folders (boolean) — default: false

    If true, flatten public IDs with folders to be in the root of the archive. Add numeric counter to the file name in case of a name conflict.

  • :flatten_transformations (boolean) — default: false

    If true, and multiple transformations are given, flatten the folder structure of derived images and store the transformation details on the file name instead.

  • :use_original_filename (boolean)

    Use the original file name of included images (if available) instead of the public ID.

  • :async (boolean) — default: false

    If true, return immediately and perform the archive creation in the background. Relevant only for the create mode.

  • :notification_url (String)

    Optional URL to send an HTTP post request (webhook) when the archive creation is completed.

  • String|Array (String|Array<String] :target_tags Optional array. Allows assigning one or more tag to the generated archive file (for later housekeeping via the admin API).)

    :target_tags Optional array. Allows assigning one or more tag to the generated archive file (for later housekeeping via the admin API).

  • :keep_derived (String) — default: false

    keep the derived images used for generating the archive

Returns:

[View source]

870
871
872
873
# File 'lib/cloudinary/utils.rb', line 870

def self.download_archive_url(options = {})
  params = Cloudinary::Utils.archive_params(options)
  cloudinary_api_download_url("generate_archive", params, options)
end

.download_folder(folder_path, options = {}) ⇒ String

Creates and returns a URL that when invoked creates an archive of a folder.

Parameters:

  • folder_path (Object)

    Full path (from the root) of the folder to download.

  • options (Hash) (defaults to: {})

    Additional options.

Returns:

[View source]

887
888
889
890
891
# File 'lib/cloudinary/utils.rb', line 887

def self.download_folder(folder_path, options = {})
  resource_type = options[:resource_type] || "all"

  download_archive_url(options.merge(:resource_type => resource_type, :prefixes => folder_path))
end

.download_generated_sprite(tag, options = {}) ⇒ String

Return a signed URL to the ‘generate_sprite’ endpoint with ‘mode=download’.

Parameters:

  • tag (String|Hash)

    Treated as additional options when hash is passed, otherwise as a tag

  • options (Hash) (defaults to: {})

    Additional options. Should be omitted when tag_or_options is a Hash

Returns:

  • (String)

    The signed URL to download sprite

[View source]

815
816
817
818
# File 'lib/cloudinary/utils.rb', line 815

def self.download_generated_sprite(tag, options = {})
  params = build_multi_and_sprite_params(tag, options)
  cloudinary_api_download_url("sprite", params, options)
end

.download_multi(tag, options = {}) ⇒ String

Return a signed URL to the ‘multi’ endpoint with ‘mode=download’.

Parameters:

  • tag (String|Hash)

    Treated as additional options when hash is passed, otherwise as a tag

  • options (Hash) (defaults to: {})

    Additional options. Should be omitted when tag_or_options is a Hash

Returns:

  • (String)

    The signed URL to download multi

[View source]

826
827
828
829
# File 'lib/cloudinary/utils.rb', line 826

def self.download_multi(tag, options = {})
  params = build_multi_and_sprite_params(tag, options)
  cloudinary_api_download_url("multi", params, options)
end

.download_zip_url(options = {}) ⇒ Object

Returns a URL that when invokes creates an zip archive and returns it.

[View source]

877
878
879
# File 'lib/cloudinary/utils.rb', line 877

def self.download_zip_url(options = {})
  download_archive_url(options.merge(:target_format => "zip"))
end

.encode_context(hash) ⇒ String

Same like encode_hash, with additional escaping of | and = characters

Returns:

  • (String)

    a joined string of all keys and values properly escaped and separated by a pipe character

[View source]

963
964
965
966
967
968
969
# File 'lib/cloudinary/utils.rb', line 963

def self.encode_context(hash)
  case hash
    when Hash then hash.map{|k,v| "#{k}=#{v.to_s.gsub(/([=|])/, '\\\\\1')}"}.join("|")
    when nil then ""
    else hash
  end
end

.encode_double_array(array) ⇒ Object

[View source]

971
972
973
974
975
976
977
978
# File 'lib/cloudinary/utils.rb', line 971

def self.encode_double_array(array)
  array = build_array(array)
  if array.length > 0 && array[0].is_a?(Array)
    return array.map{|a| build_array(a).join(",")}.join("|")
  else
    return array.join(",")
  end
end

.encode_hash(hash) ⇒ String

encodes a hash into pipe-delimited key-value pairs string

Returns:

  • (String)

    a joined string of all keys and values separated by a pipe character

[View source]

951
952
953
954
955
956
957
# File 'lib/cloudinary/utils.rb', line 951

def self.encode_hash(hash)
  case hash
    when Hash then hash.map{|k,v| "#{k}=#{v}"}.join("|")
    when nil then ""
    else hash
  end
end

.encode_param(value) ⇒ String

Encodes a parameter for safe inclusion in URL query strings.

Specifically replaces “&” characters with their percent-encoded equivalent “%26” to prevent them from being interpreted as parameter separators in URL query strings.

Parameters:

  • value (Object)

    The parameter to encode

Returns:

  • (String)

    Encoded parameter

[View source]

480
481
482
# File 'lib/cloudinary/utils.rb', line 480

def self.encode_param(value)
  value.to_s.gsub("&", "%26")
end

.extract_config_params(options) ⇒ Object

[View source]

171
172
173
# File 'lib/cloudinary/utils.rb', line 171

def self.extract_config_params(options)
    options.select{|k,v| URL_KEYS.include?(k)}
end

.extract_transformation_params(options) ⇒ Object

[View source]

175
176
177
# File 'lib/cloudinary/utils.rb', line 175

def self.extract_transformation_params(options)
  options.select{|k,v| TRANSFORMATION_PARAMS.include?(k)}
end

.finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten) ⇒ Object

[View source]

667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
# File 'lib/cloudinary/utils.rb', line 667

def self.finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten)
  type ||= :upload
  if !url_suffix.blank?
    case
    when resource_type.to_s == "image" && type.to_s == "upload"
      resource_type = "images"
      type = nil
    when resource_type.to_s == "image" && type.to_s == "private"
      resource_type = "private_images"
      type = nil
    when resource_type.to_s == "image" && type.to_s == "authenticated"
      resource_type = "authenticated_images"
      type = nil
    when resource_type.to_s == "raw" && type.to_s == "upload"
      resource_type = "files"
      type = nil
    when resource_type.to_s == "video" && type.to_s == "upload"
      resource_type = "videos"
      type = nil
    else
      raise(CloudinaryException, "URL Suffix only supported for image/upload, image/private, image/authenticated, video/upload and raw/upload")
    end
  end
  if use_root_path
    if (resource_type.to_s == "image" && type.to_s == "upload") || (resource_type.to_s == "images" && type.blank?)
      resource_type = nil
      type = nil
    else
      raise(CloudinaryException, "Root path only supported for image/upload")
    end
  end
  if shorten && resource_type.to_s == "image" && type.to_s == "upload"
    resource_type = "iu"
    type = nil
  end
  [resource_type, type]
end

.finalize_source(source, format, url_suffix) ⇒ Object

[View source]

647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
# File 'lib/cloudinary/utils.rb', line 647

def self.finalize_source(source, format, url_suffix)
  source = source.gsub(%r(([^:])//), '\1/')
  if source.match(%r(^https?:/)i)
    source = smart_escape(source)
    source_to_sign = source
  else
    source = smart_escape(smart_unescape(source))
    source_to_sign = source
    unless url_suffix.blank?
      raise(CloudinaryException, "url_suffix should not include . or /") if url_suffix.match(%r([\./]))
      source = "#{source}/#{url_suffix}"
    end
    if !format.blank?
      source = "#{source}.#{format}"
      source_to_sign = "#{source_to_sign}.#{format}"
    end
  end
  [source, source_to_sign]
end

.generate_auth_token(options) ⇒ Object

[View source]

1129
1130
1131
1132
1133
# File 'lib/cloudinary/utils.rb', line 1129

def self.generate_auth_token(options)
  options = Cloudinary::AuthToken.merge_auth_token Cloudinary.config.auth_token, options
  Cloudinary::AuthToken.generate options

end

.generate_responsive_breakpoints_string(breakpoints) ⇒ Object

[View source]

533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
# File 'lib/cloudinary/utils.rb', line 533

def self.generate_responsive_breakpoints_string(breakpoints)
  return nil if breakpoints.nil?
  breakpoints = build_array(breakpoints)

  breakpoints.map do |breakpoint_settings|
    unless breakpoint_settings.nil?
      breakpoint_settings = breakpoint_settings.clone
      transformation =  breakpoint_settings.delete(:transformation) || breakpoint_settings.delete("transformation")
      format =  breakpoint_settings.delete(:format) || breakpoint_settings.delete("format")
      if transformation
        transformation = Cloudinary::Utils.generate_transformation_string(transformation.clone, true)
      end
      breakpoint_settings[:transformation] = [transformation, format].compact.join("/")
    end
    breakpoint_settings
  end.to_json
end

.generate_transformation_string(options = {}, allow_implicit_crop_mode = false) ⇒ Object

Warning: options are being destructively updated!

[View source]

188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/cloudinary/utils.rb', line 188

def self.generate_transformation_string(options={}, allow_implicit_crop_mode = false)
  # allow_implicit_crop_mode was added to support height and width parameters without specifying a crop mode.
  # This only apply to this (cloudinary_gem) SDK

  if options.is_a?(Array)
    return options.map{|base_transformation| generate_transformation_string(base_transformation.clone, allow_implicit_crop_mode)}.reject(&:blank?).join("/")
  end

  symbolize_keys!(options)

  responsive_width = config_option_consume(options, :responsive_width)
  size = options.delete(:size)
  options[:width], options[:height] = size.split("x") if size
  width = options[:width]
  width = width.to_s if width.is_a?(Symbol)
  height = options[:height]
  has_layer = options[:overlay].present? || options[:underlay].present?

  crop = options.delete(:crop)
  angle = build_array(options.delete(:angle)).join(".")

  no_html_sizes = has_layer || angle.present? || crop.to_s == "fit" || crop.to_s == "limit" || crop.to_s == "lfill"
  options.delete(:width) if width && (width.to_f < 1 || no_html_sizes || width.to_s.start_with?("auto") || responsive_width)
  options.delete(:height) if height && (height.to_f < 1 || no_html_sizes || responsive_width)

  width=height=nil if crop.nil? && !has_layer && !width.to_s.start_with?("auto") && !allow_implicit_crop_mode

  background = options.delete(:background)
  background = background.sub(/^#/, 'rgb:') if background

  color = options.delete(:color)
  color = color.sub(/^#/, 'rgb:') if color

  base_transformations = build_array(options.delete(:transformation))
  if base_transformations.any?{|base_transformation| base_transformation.is_a?(Hash)}
    base_transformations = base_transformations.map do
      |base_transformation|
      base_transformation.is_a?(Hash) ? generate_transformation_string(base_transformation.clone, allow_implicit_crop_mode) : generate_transformation_string({:transformation=>base_transformation}, allow_implicit_crop_mode)
    end
  else
    named_transformation = base_transformations.join(".")
    base_transformations = []
  end

  effect = options.delete(:effect)
  effect = Array(effect).flatten.join(":") if effect.is_a?(Array) || effect.is_a?(Hash)

  border = options.delete(:border)
  if border.is_a?(Hash)
    border = "#{border[:width] || 2}px_solid_#{(border[:color] || "black").sub(/^#/, 'rgb:')}"
  elsif border.to_s =~ /^\d+$/ # fallback to html border attribute
    options[:border] = border
    border = nil
  end
  flags = build_array(options.delete(:flags)).join(".")
  dpr = config_option_consume(options, :dpr)

  if options.include? :offset
    options[:start_offset], options[:end_offset] = split_range options.delete(:offset)
  end

  fps = options.delete(:fps)
  fps = fps.join('-') if fps.is_a? Array

  overlay = process_layer(options.delete(:overlay))
  underlay = process_layer(options.delete(:underlay))
  ifValue = process_if(options.delete(:if))
  custom_function = process_custom_function(options.delete(:custom_function))
  custom_pre_function = process_custom_pre_function(options.delete(:custom_pre_function))

  params = {
    :a   => normalize_expression(angle),
    :ar => normalize_expression(options.delete(:aspect_ratio)),
    :b   => background,
    :bo  => border,
    :c   => crop,
    :co  => color,
    :dpr => normalize_expression(dpr),
    :e   => normalize_expression(effect),
    :fl  => flags,
    :fn  => custom_function || custom_pre_function,
    :fps => fps,
    :h   => normalize_expression(height),
    :l  => overlay,
    :o => normalize_expression(options.delete(:opacity)),
    :q => normalize_expression(options.delete(:quality)),
    :r => process_radius(options.delete(:radius)),
    :t   => named_transformation,
    :u  => underlay,
    :w   => normalize_expression(width),
    :x => normalize_expression(options.delete(:x)),
    :y => normalize_expression(options.delete(:y)),
    :z => normalize_expression(options.delete(:zoom))
  }
  SIMPLE_TRANSFORMATION_PARAMS.each do
    |param, option|
    params[param] = options.delete(option)
  end

  params[:vc] = process_video_params params[:vc] if params[:vc].present?
  [:so, :eo, :du].each do |range_value|
    params[range_value] = norm_range_value params[range_value] if params[range_value].present?
  end

  variables = options.delete(:variables)
  var_params = []
  options.each_pair do |key, value|
    if key =~ /^\$/
      var_params.push "#{key}_#{normalize_expression(value.to_s)}"
    end
  end
  var_params.sort!
  unless variables.nil? || variables.empty?
    for name, value in variables
      var_params.push "#{name}_#{normalize_expression(value.to_s)}"
    end
  end
  variables = var_params.join(',')

  raw_transformation = options.delete(:raw_transformation)
  transformation = params.reject{|_k,v| v.blank?}.map{|k,v| "#{k}_#{v}"}.sort
  transformation = transformation.join(",")
  transformation = [ifValue, variables, transformation, raw_transformation].reject(&:blank?).join(",")

  transformations = base_transformations << transformation
  if responsive_width
    responsive_width_transformation = Cloudinary.config.responsive_width_transformation || DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION
    transformations << generate_transformation_string(responsive_width_transformation.clone, allow_implicit_crop_mode)
  end

  if width.to_s.start_with?( "auto") || responsive_width
    options[:responsive] = true
  end
  if dpr.to_s == "auto"
    options[:hidpi] = true
  end

  transformations.reject(&:blank?).join("/")
end

.json_array_param(data) ⇒ String|nil

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a JSON array as String. Yields the array before it is converted to JSON format

Parameters:

Returns:

  • (String|nil)

    a JSON array string or ‘nil` if data is `nil`

[View source]

524
525
526
527
528
529
530
531
# File 'lib/cloudinary/utils.rb', line 524

def self.json_array_param(data)
  return nil if data.nil?

  data = JSON.parse(data) if data.is_a?(String)
  data = [data] unless data.is_a?(Array)
  data = yield data if block_given?
  JSON.generate(data)
end

.json_decode(str) ⇒ Object

[View source]

923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
# File 'lib/cloudinary/utils.rb', line 923

def self.json_decode(str)
  if !@@json_decode
    @@json_decode = true
    begin
      require 'json'
    rescue LoadError
      begin
        require 'active_support/json'
      rescue LoadError
        raise LoadError, "Please add the json gem or active_support to your Gemfile"
      end
    end
  end
  defined?(JSON) ? JSON.parse(str) : ActiveSupport::JSON.decode(str)
end

.normalize_expression(expression) ⇒ Object

[View source]

339
340
341
342
343
344
345
346
347
# File 'lib/cloudinary/utils.rb', line 339

def self.normalize_expression(expression)
  if expression.nil?
    nil
  elsif expression.is_a?( String) && expression =~ /^!.+!$/ # quoted string
    expression
  else
    expression.to_s.gsub(EXP_REGEXP) { |match| EXP_REPLACEMENT[match] || match }.gsub(/[ _]+/, "_")
  end
end

.private_download_url(public_id, format, options = {}) ⇒ Object

[View source]

831
832
833
834
835
836
837
838
839
840
841
842
# File 'lib/cloudinary/utils.rb', line 831

def self.private_download_url(public_id, format, options = {})
  cloudinary_params = sign_request({
      :timestamp=>Time.now.to_i,
      :public_id=>public_id,
      :format=>format,
      :type=>options[:type],
      :attachment=>options[:attachment],
      :expires_at=>options[:expires_at] && options[:expires_at].to_i
    }, options)

  return Cloudinary::Utils.cloudinary_api_url("download", options) + "?" + hash_query_params(cloudinary_params)
end

.process_if(if_value) ⇒ string

Parse “if” parameter Translates the condition if provided.

Returns:

  • (string)

    “if_” + ifValue

[View source]

332
333
334
# File 'lib/cloudinary/utils.rb', line 332

def self.process_if(if_value)
  "if_" + normalize_expression(if_value) unless if_value.to_s.empty?
end

.random_public_idObject

[View source]

913
914
915
916
# File 'lib/cloudinary/utils.rb', line 913

def self.random_public_id
  sr = defined?(ActiveSupport::SecureRandom) ? ActiveSupport::SecureRandom : SecureRandom
  sr.base64(20).downcase.gsub(/[^a-z0-9]/, "").sub(/^[0-9]+/, '')[0,20]
end

.resource_type_for_format(format) ⇒ Object

[View source]

996
997
998
999
1000
1001
1002
1003
1004
1005
# File 'lib/cloudinary/utils.rb', line 996

def self.resource_type_for_format(format)
  case
  when self.supported_format?(format, IMAGE_FORMATS)
    'image'
  when self.supported_format?(format, VIDEO_FORMATS), self.supported_format?(format, AUDIO_FORMATS)
    'video'
  else
    'raw'
  end
end

.safe_blank?(value) ⇒ Boolean

Returns:

  • (Boolean)
[View source]

1040
1041
1042
# File 'lib/cloudinary/utils.rb', line 1040

def self.safe_blank?(value)
  value.nil? || value == "" || value == []
end

.sign_request(params, options = {}) ⇒ Object

[View source]

783
784
785
786
787
788
789
790
791
792
# File 'lib/cloudinary/utils.rb', line 783

def self.sign_request(params, options={})
  api_key = options[:api_key] || Cloudinary.config.api_key || raise(CloudinaryException, "Must supply api_key")
  api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise(CloudinaryException, "Must supply api_secret")
  signature_algorithm = options[:signature_algorithm]
  signature_version = options[:signature_version]
  params = params.reject{|k, v| self.safe_blank?(v)}
  params[:signature] = api_sign_request(params, api_secret, signature_algorithm, signature_version)
  params[:api_key] = api_key
  params
end

.signed_preloaded_image(result) ⇒ Object

[View source]

918
919
920
# File 'lib/cloudinary/utils.rb', line 918

def self.signed_preloaded_image(result)
  "#{result["resource_type"]}/#{result["type"] || "upload"}/v#{result["version"]}/#{[result["public_id"], result["format"]].reject(&:blank?).join(".")}##{result["signature"]}"
end

.smart_escape(string, unsafe = /([^a-zA-Z0-9_.\-\/:]+)/) ⇒ Object

Based on CGI::escape. In addition does not escape / :

[View source]

902
903
904
905
906
# File 'lib/cloudinary/utils.rb', line 902

def self.smart_escape(string, unsafe = /([^a-zA-Z0-9_.\-\/:]+)/)
  string.gsub(unsafe) do |m|
    '%' + m.unpack('H2' * m.bytesize).join('%').upcase
  end
end

.smart_unescape(string) ⇒ Object

Based on CGI::unescape. In addition keeps ‘+’ character as is

[View source]

909
910
911
# File 'lib/cloudinary/utils.rb', line 909

def self.smart_unescape(string)
  CGI.unescape(string.gsub('+', '%2B'))
end

.supported_format?(format, formats) ⇒ Boolean

Returns:

  • (Boolean)
[View source]

990
991
992
993
994
# File 'lib/cloudinary/utils.rb', line 990

def self.supported_format?( format, formats)
  format = format.to_s.downcase
  extension = format =~ /\./ ? format.split('.').last : format
  formats.include?(extension)
end

.supported_image_format?(format) ⇒ Boolean

Returns:

  • (Boolean)
[View source]

986
987
988
# File 'lib/cloudinary/utils.rb', line 986

def self.supported_image_format?(format)
  supported_format? format, IMAGE_FORMATS
end

.symbolize_keys(h) ⇒ Object

[View source]

1044
1045
1046
1047
1048
1049
1050
1051
1052
# File 'lib/cloudinary/utils.rb', line 1044

def self.symbolize_keys(h)
  new_h = Hash.new
  if (h.respond_to? :keys)
    h.keys.each do |key|
      new_h[(key.to_sym rescue key)] = h[key]
    end
  end
  new_h
end

.symbolize_keys!(h) ⇒ Object

[View source]

1055
1056
1057
1058
1059
1060
1061
1062
1063
# File 'lib/cloudinary/utils.rb', line 1055

def self.symbolize_keys!(h)
  if (h.respond_to? :keys) && (h.respond_to? :delete)
    h.keys.each do |key|
      value = h.delete(key)
      h[(key.to_sym rescue key)] = value
    end
  end
  h
end

.text_style(layer) ⇒ Object

[View source]

445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
# File 'lib/cloudinary/utils.rb', line 445

def self.text_style(layer)
  return layer[:text_style] if layer[:text_style].present?

  font_family = layer[:font_family]
  font_size   = layer[:font_size]
  keywords    = []
  LAYER_KEYWORD_PARAMS.each do |attr, default_value|
    attr_value = layer[attr] || default_value
    keywords.push(attr_value) unless attr_value == default_value
  end
  letter_spacing = layer[:letter_spacing]
  keywords.push("letter_spacing_#{letter_spacing}") unless letter_spacing.blank?
  line_spacing = layer[:line_spacing]
  keywords.push("line_spacing_#{line_spacing}") unless line_spacing.blank?
  font_antialiasing = layer[:font_antialiasing]
  keywords.push("antialias_#{font_antialiasing}") unless font_antialiasing.blank?
  font_hinting = layer[:font_hinting]
  keywords.push("hinting_#{font_hinting}") unless font_hinting.blank?
  if !font_size.blank? || !font_family.blank? || !keywords.empty?
    raise(CloudinaryException, "Must supply font_family for text in overlay/underlay") if font_family.blank?
    raise(CloudinaryException, "Must supply font_size for text in overlay/underlay") if font_size.blank?
    keywords.unshift(font_size)
    keywords.unshift(font_family)
    keywords.reject(&:blank?).join("_")
  end
end

.unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution) ⇒ Object

Creates the URL prefix for the cloudinary resource URL

cdn_subdomain and secure_cdn_subdomain

  1. Customers in shared distribution (e.g. res.cloudinary.com)

    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.

  2. Customers with private cdn

    if cdn_domain is true uses cloudname-res-[1-5 ].cloudinary.com for http

    if secure_cdn_domain is true uses cloudname-res-[1-5 ].cloudinary.com for https (please contact support if you require this)

  3. Customers with cname

    if cdn_domain is true uses a.cname for http. For https, uses the same naming scheme as 1 for shared distribution and as 2 for private distribution.

[View source]

720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
# File 'lib/cloudinary/utils.rb', line 720

def self.unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution)
  return "/res#{cloud_name}" if cloud_name.start_with?("/") # For development

  shared_domain = !private_cdn

  if secure
    if secure_distribution.nil?
      secure_distribution = private_cdn ? "#{cloud_name}-res.cloudinary.com" : Cloudinary::SHARED_CDN
    end
    shared_domain ||= secure_distribution == Cloudinary::SHARED_CDN
    secure_cdn_subdomain = cdn_subdomain if secure_cdn_subdomain.nil? && shared_domain

    if secure_cdn_subdomain
      secure_distribution = secure_distribution.gsub('res.cloudinary.com', "res-#{(Zlib::crc32(source) % 5) + 1}.cloudinary.com")
    end

    prefix = "https://#{secure_distribution}"
  elsif cname
    subdomain = cdn_subdomain ? "a#{(Zlib::crc32(source) % 5) + 1}." : ""
    prefix = "http://#{subdomain}#{cname}"
  else
    host = [private_cdn ? "#{cloud_name}-" : "", "res", cdn_subdomain ? "-#{(Zlib::crc32(source) % 5) + 1}" : "", ".cloudinary.com"].join
    prefix = "http://#{host}"
  end
  prefix += "/#{cloud_name}" if shared_domain

  prefix
end