imgproxy 1.0.4 → 2.0.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +236 -92
  3. data/lib/imgproxy.rb +78 -38
  4. data/lib/imgproxy/builder.rb +56 -39
  5. data/lib/imgproxy/config.rb +96 -25
  6. data/lib/imgproxy/extensions/active_storage.rb +10 -0
  7. data/lib/imgproxy/extensions/shrine.rb +10 -0
  8. data/lib/imgproxy/options.rb +87 -72
  9. data/lib/imgproxy/options_aliases.rb +43 -0
  10. data/lib/imgproxy/options_casters/adjust.rb +22 -0
  11. data/lib/imgproxy/options_casters/array.rb +12 -0
  12. data/lib/imgproxy/options_casters/base64.rb +12 -0
  13. data/lib/imgproxy/options_casters/bool.rb +12 -0
  14. data/lib/imgproxy/options_casters/crop.rb +23 -0
  15. data/lib/imgproxy/options_casters/extend.rb +26 -0
  16. data/lib/imgproxy/options_casters/float.rb +16 -0
  17. data/lib/imgproxy/options_casters/gif_options.rb +21 -0
  18. data/lib/imgproxy/options_casters/gravity.rb +23 -0
  19. data/lib/imgproxy/options_casters/group.rb +21 -0
  20. data/lib/imgproxy/options_casters/integer.rb +10 -0
  21. data/lib/imgproxy/options_casters/jpeg_options.rb +26 -0
  22. data/lib/imgproxy/options_casters/png_options.rb +23 -0
  23. data/lib/imgproxy/options_casters/resize.rb +21 -0
  24. data/lib/imgproxy/options_casters/size.rb +24 -0
  25. data/lib/imgproxy/options_casters/string.rb +10 -0
  26. data/lib/imgproxy/options_casters/trim.rb +28 -0
  27. data/lib/imgproxy/options_casters/watermark.rb +30 -0
  28. data/lib/imgproxy/trim_array.rb +11 -0
  29. data/lib/imgproxy/url_adapters.rb +0 -4
  30. data/lib/imgproxy/url_adapters/active_storage.rb +25 -0
  31. data/lib/imgproxy/url_adapters/shrine.rb +15 -5
  32. data/lib/imgproxy/version.rb +1 -1
  33. metadata +69 -24
  34. data/lib/imgproxy/url_adapters/active_storage_gcs.rb +0 -31
  35. data/lib/imgproxy/url_adapters/active_storage_s3.rb +0 -23
  36. data/lib/imgproxy/url_adapters/shrine_s3.rb +0 -20
@@ -1,8 +1,9 @@
1
1
  require "openssl"
2
2
  require "base64"
3
- require "uri"
3
+ require "erb"
4
4
 
5
5
  require "imgproxy/options"
6
+ require "imgproxy/options_aliases"
6
7
 
7
8
  module Imgproxy
8
9
  # Builds imgproxy URL
@@ -22,10 +23,10 @@ module Imgproxy
22
23
  def initialize(options = {})
23
24
  options = options.dup
24
25
 
25
- @use_short_options = options.delete(:use_short_options)
26
- @use_short_options = config.use_short_options if @use_short_options.nil?
26
+ extract_builder_options(options)
27
27
 
28
28
  @options = Imgproxy::Options.new(options)
29
+ @format = @options.delete(:format)
29
30
  end
30
31
 
31
32
  # Genrates imgproxy URL
@@ -35,56 +36,68 @@ module Imgproxy
35
36
  # the configured URL adapters
36
37
  # @see Imgproxy.url_for
37
38
  def url_for(image)
38
- path = [*processing_options, "plain", url(image)].join("/")
39
- path = "#{path}@#{@options[:format]}" if @options[:format]
39
+ path = [*processing_options, url(image, ext: @format)].join("/")
40
+ signature = sign_path(path)
41
+
42
+ File.join(Imgproxy.config.endpoint.to_s, signature, path)
43
+ end
40
44
 
45
+ # Genrates imgproxy info URL
46
+ #
47
+ # @return [String] imgproxy info URL
48
+ # @param [String,URI, Object] image Source image URL or object applicable for
49
+ # the configured URL adapters
50
+ # @see Imgproxy.info_url_for
51
+ def info_url_for(image)
52
+ path = url(image)
41
53
  signature = sign_path(path)
42
54
 
43
- URI.join(Imgproxy.config.endpoint.to_s, "#{signature}/#{path}").to_s
55
+ File.join(Imgproxy.config.endpoint.to_s, "info", signature, path)
44
56
  end
45
57
 
46
58
  private
47
59
 
48
- OPTIONS_ALIASES = {
49
- resize: :rs,
50
- size: :s,
51
- resizing_type: :rt,
52
- width: :w,
53
- height: :h,
54
- enlarge: :en,
55
- extend: :ex,
56
- gravity: :g,
57
- quality: :q,
58
- background: :bg,
59
- blur: :bl,
60
- sharpen: :sh,
61
- watermark: :wm,
62
- preset: :pr,
63
- cachebuster: :cb,
64
- }.freeze
65
-
66
- NEED_ESCAPE_RE = /[@?%]/.freeze
60
+ NEED_ESCAPE_RE = /[@?% ]|[^\p{Ascii}]/.freeze
61
+
62
+ def extract_builder_options(options)
63
+ @use_short_options = not_nil_or(options.delete(:use_short_options), config.use_short_options)
64
+ @base64_encode_url = not_nil_or(options.delete(:base64_encode_url), config.base64_encode_urls)
65
+ @escape_plain_url =
66
+ not_nil_or(options.delete(:escape_plain_url), config.always_escape_plain_urls)
67
+ end
67
68
 
68
69
  def processing_options
69
- @processing_options ||=
70
- @options.reject { |k, _| k == :format }.map do |key, value|
71
- "#{option_alias(key)}:#{wrap_array(value).join(':')}"
72
- end
70
+ @processing_options ||= @options.map do |key, value|
71
+ [option_alias(key), value].join(":")
72
+ end
73
73
  end
74
74
 
75
- def option_alias(name)
76
- return name unless config.use_short_options
75
+ def url(image, ext: nil)
76
+ url = config.url_adapters.url_of(image)
77
77
 
78
- OPTIONS_ALIASES.fetch(name, name)
78
+ @base64_encode_url ? base64_url_for(url, ext: ext) : plain_url_for(url, ext: ext)
79
79
  end
80
80
 
81
- def wrap_array(value)
82
- value.is_a?(Array) ? value : [value]
81
+ def plain_url_for(url, ext: nil)
82
+ escaped_url = need_escape_url?(url) ? ERB::Util.url_encode(url) : url
83
+
84
+ ext ? "plain/#{escaped_url}@#{ext}" : "plain/#{escaped_url}"
83
85
  end
84
86
 
85
- def url(image)
86
- url = config.url_adapters.url_of(image)
87
- url.match?(NEED_ESCAPE_RE) ? CGI.escape(url) : url
87
+ def base64_url_for(url, ext: nil)
88
+ encoded_url = Base64.urlsafe_encode64(url).tr("=", "").scan(/.{1,16}/).join("/")
89
+
90
+ ext ? "#{encoded_url}.#{ext}" : encoded_url
91
+ end
92
+
93
+ def need_escape_url?(url)
94
+ @escape_plain_url || url.match?(NEED_ESCAPE_RE)
95
+ end
96
+
97
+ def option_alias(name)
98
+ return name unless @use_short_options
99
+
100
+ Imgproxy::OPTIONS_ALIASES.fetch(name, name)
88
101
  end
89
102
 
90
103
  def sign_path(path)
@@ -105,11 +118,11 @@ module Imgproxy
105
118
  end
106
119
 
107
120
  def signature_key
108
- config.key
121
+ config.raw_key
109
122
  end
110
123
 
111
124
  def signature_salt
112
- config.salt
125
+ config.raw_salt
113
126
  end
114
127
 
115
128
  def signature_size
@@ -119,5 +132,9 @@ module Imgproxy
119
132
  def config
120
133
  Imgproxy.config
121
134
  end
135
+
136
+ def not_nil_or(value, fallback)
137
+ value.nil? ? fallback : value
138
+ end
122
139
  end
123
140
  end
@@ -1,39 +1,110 @@
1
+ require "anyway_config"
2
+
1
3
  require "imgproxy/url_adapters"
2
4
 
3
5
  module Imgproxy
4
6
  # Imgproxy config
7
+ #
8
+ # @!attribute endpoint
9
+ # imgproxy endpoint
10
+ # @return [String]
11
+ # @!attribute key
12
+ # imgproxy hex-encoded signature key
13
+ # @return [String]
14
+ # @!attribute salt
15
+ # imgproxy hex-encoded signature salt
16
+ # @return [String]
17
+ # @!attribute raw_key
18
+ # Decoded signature key
19
+ # @return [String]
20
+ # @!attribute raw_salt
21
+ # Decoded signature salt
22
+ # @return [String]
23
+ # @!attribute signature_size
24
+ # imgproxy signature size. Defaults to 32
25
+ # @return [String]
26
+ # @!attribute use_short_options
27
+ # Use short processing option names (+rs+ for +resize+, +g+ for +gravity+, etc).
28
+ # Defaults to true
29
+ # @return [String]
30
+ # @!attribute base64_encode_urls
31
+ # Base64 encode the URL. Defaults to false
32
+ # @return [String]
33
+ # @!attribute always_escape_plain_urls
34
+ # Always escape plain URLs. Defaults to false
35
+ # @return [String]
36
+ # @!attribute use_s3_urls
37
+ # Use short S3 urls (s3://...) when possible. Defaults to false
38
+ # @return [String]
39
+ # @!attribute use_gcs_urls
40
+ # Use short Google Cloud Storage urls (gs://...) when possible. Defaults to false
41
+ # @return [String]
42
+ # @!attribute gcs_bucket
43
+ # Google Cloud Storage bucket name
44
+ # @return [String]
45
+ # @!attribute shrine_host
46
+ # Shrine host
47
+ # @return [String]
48
+ #
5
49
  # @see Imgproxy.configure
6
- class Config
7
- # @return [String] imgproxy endpoint
8
- attr_accessor :endpoint
9
- # @return [String] imgproxy signature key
10
- attr_accessor :key
11
- # @return [String] imgproxy signature salt
12
- attr_accessor :salt
13
- # @return [Integer] imgproxy signature size. Defaults to 32
14
- attr_accessor :signature_size
15
- # @return [Boolean] use short processing option names
16
- # (`rs` for `resize`, `g` for `gravity`, etc).
17
- # Defaults to true
18
- attr_accessor :use_short_options
19
-
20
- def initialize
21
- self.signature_size = 32
22
- self.use_short_options = true
50
+ # @see https://github.com/palkan/anyway_config anyway_config
51
+ class Config < Anyway::Config
52
+ attr_config(
53
+ :endpoint,
54
+ :key,
55
+ :salt,
56
+ :raw_key,
57
+ :raw_salt,
58
+ signature_size: 32,
59
+ use_short_options: true,
60
+ base64_encode_urls: false,
61
+ always_escape_plain_urls: false,
62
+ use_s3_urls: false,
63
+ use_gcs_urls: false,
64
+ gcs_bucket: nil,
65
+ shrine_host: nil,
66
+ )
67
+
68
+ alias_method :set_key, :key=
69
+ alias_method :set_raw_key, :raw_key=
70
+ alias_method :set_salt, :salt=
71
+ alias_method :set_raw_salt, :raw_salt=
72
+ private :set_key, :set_raw_key, :set_salt, :set_raw_salt
73
+
74
+ def key=(value)
75
+ value = value&.to_s
76
+ super(value)
77
+ set_raw_key(value && [value].pack("H*"))
23
78
  end
24
79
 
25
- # Decodes hex-encoded key and sets it to {#key}
26
- #
27
- # @param value [String] hex-encoded signature key
80
+ def raw_key=(value)
81
+ value = value&.to_s
82
+ super(value)
83
+ set_key(value&.unpack("H*")&.first)
84
+ end
85
+
86
+ def salt=(value)
87
+ value = value&.to_s
88
+ super(value)
89
+ set_raw_salt(value && [value].pack("H*"))
90
+ end
91
+
92
+ def raw_salt=(value)
93
+ value = value&.to_s
94
+ super(value)
95
+ set_salt(value&.unpack("H*")&.first)
96
+ end
97
+
98
+ # @deprecated Please use {#key} instead
28
99
  def hex_key=(value)
29
- self.key = value.nil? ? nil : [value].pack("H*")
100
+ warn "[DEPRECATION] #hex_key is deprecated. Please use #key instead."
101
+ self.key = value
30
102
  end
31
103
 
32
- # Decodes hex-encoded salt and sets it to {#salt}
33
- #
34
- # @param value [String] hex-encoded signature salt
104
+ # @deprecated Please use {#salt} instead
35
105
  def hex_salt=(value)
36
- self.salt = value.nil? ? nil : [value].pack("H*")
106
+ warn "[DEPRECATION] #hex_salt is deprecated. Please use #salt instead."
107
+ self.salt = value
37
108
  end
38
109
 
39
110
  # URL adapters config. Allows to use this gem with ActiveStorage, Shrine, etc.
@@ -12,6 +12,16 @@ module Imgproxy
12
12
  return options.url_for(self) if options.is_a?(Imgproxy::Builder)
13
13
  Imgproxy.url_for(self, options)
14
14
  end
15
+
16
+ # Returns imgproxy info URL for an attachment
17
+ #
18
+ # @return [String]
19
+ # @param options [Hash, Imgproxy::Builder]
20
+ # @see Imgproxy.info_url_for
21
+ def imgproxy_info_url(options = {})
22
+ return options.info_url_for(self) if options.is_a?(Imgproxy::Builder)
23
+ Imgproxy.info_url_for(self, options)
24
+ end
15
25
  end
16
26
  end
17
27
  end
@@ -12,6 +12,16 @@ module Imgproxy
12
12
  return options.url_for(self) if options.is_a?(Imgproxy::Builder)
13
13
  Imgproxy.url_for(self, options)
14
14
  end
15
+
16
+ # Returns imgproxy info URL for a Shrine::UploadedFile instance
17
+ #
18
+ # @return [String]
19
+ # @param options [Hash, Imgproxy::Builder]
20
+ # @see Imgproxy.info_url_for
21
+ def imgproxy_info_url(options = {})
22
+ return options.info_url_for(self) if options.is_a?(Imgproxy::Builder)
23
+ Imgproxy.info_url_for(self, options)
24
+ end
15
25
  end
16
26
  end
17
27
  end
@@ -1,100 +1,115 @@
1
+ require "imgproxy/trim_array"
2
+ require "imgproxy/options_casters/string"
3
+ require "imgproxy/options_casters/integer"
4
+ require "imgproxy/options_casters/float"
5
+ require "imgproxy/options_casters/bool"
6
+ require "imgproxy/options_casters/array"
7
+ require "imgproxy/options_casters/base64"
8
+ require "imgproxy/options_casters/resize"
9
+ require "imgproxy/options_casters/size"
10
+ require "imgproxy/options_casters/extend"
11
+ require "imgproxy/options_casters/gravity"
12
+ require "imgproxy/options_casters/crop"
13
+ require "imgproxy/options_casters/trim"
14
+ require "imgproxy/options_casters/adjust"
15
+ require "imgproxy/options_casters/watermark"
16
+ require "imgproxy/options_casters/jpeg_options"
17
+ require "imgproxy/options_casters/png_options"
18
+ require "imgproxy/options_casters/gif_options"
19
+
1
20
  module Imgproxy
2
21
  # Formats and regroups processing options
3
22
  class Options < Hash
4
- STRING_OPTS = %i[resizing_type gravity watermark_position style cachebuster format].freeze
5
- INT_OPTS = %i[width height quality watermark_x_offset watermark_y_offset].freeze
6
- FLOAT_OPTS = %i[dpr gravity_x gravity_y blur sharpen watermark_opacity watermark_scale].freeze
7
- BOOL_OPTS = %i[enlarge extend].freeze
8
- ARRAY_OPTS = %i[background preset].freeze
9
- ALL_OPTS = (STRING_OPTS + INT_OPTS + FLOAT_OPTS + BOOL_OPTS + ARRAY_OPTS).freeze
10
-
11
- OPTS_PRIORITY = %i[ resize size resizing_type width height dpr enlarge extend gravity quality
12
- background blur sharpen watermark preset cachebuster ].freeze
23
+ using TrimArray
24
+
25
+ CASTERS = {
26
+ resize: Imgproxy::OptionsCasters::Resize,
27
+ size: Imgproxy::OptionsCasters::Size,
28
+ resizing_type: Imgproxy::OptionsCasters::String,
29
+ resizing_algorithm: Imgproxy::OptionsCasters::String,
30
+ width: Imgproxy::OptionsCasters::Integer,
31
+ height: Imgproxy::OptionsCasters::Integer,
32
+ dpr: Imgproxy::OptionsCasters::Float,
33
+ enlarge: Imgproxy::OptionsCasters::Bool,
34
+ extend: Imgproxy::OptionsCasters::Extend,
35
+ gravity: Imgproxy::OptionsCasters::Gravity,
36
+ crop: Imgproxy::OptionsCasters::Crop,
37
+ padding: Imgproxy::OptionsCasters::Array,
38
+ trim: Imgproxy::OptionsCasters::Trim,
39
+ rotate: Imgproxy::OptionsCasters::Integer,
40
+ quality: Imgproxy::OptionsCasters::Integer,
41
+ max_bytes: Imgproxy::OptionsCasters::Integer,
42
+ background: Imgproxy::OptionsCasters::Array,
43
+ background_alpha: Imgproxy::OptionsCasters::Float,
44
+ adjust: Imgproxy::OptionsCasters::Adjust,
45
+ brightness: Imgproxy::OptionsCasters::Integer,
46
+ contrast: Imgproxy::OptionsCasters::Float,
47
+ saturation: Imgproxy::OptionsCasters::Float,
48
+ blur: Imgproxy::OptionsCasters::Float,
49
+ sharpen: Imgproxy::OptionsCasters::Float,
50
+ pixelate: Imgproxy::OptionsCasters::Integer,
51
+ unsharpening: Imgproxy::OptionsCasters::String,
52
+ watermark: Imgproxy::OptionsCasters::Watermark,
53
+ watermark_url: Imgproxy::OptionsCasters::Base64,
54
+ style: Imgproxy::OptionsCasters::Base64,
55
+ jpeg_options: Imgproxy::OptionsCasters::JpegOptions,
56
+ png_options: Imgproxy::OptionsCasters::PngOptions,
57
+ gif_options: Imgproxy::OptionsCasters::GifOptions,
58
+ page: Imgproxy::OptionsCasters::Integer,
59
+ video_thumbnail_second: Imgproxy::OptionsCasters::Integer,
60
+ preset: Imgproxy::OptionsCasters::Array,
61
+ cachebuster: Imgproxy::OptionsCasters::String,
62
+ strip_metadata: Imgproxy::OptionsCasters::Bool,
63
+ strip_color_profile: Imgproxy::OptionsCasters::Bool,
64
+ auto_rotate: Imgproxy::OptionsCasters::Bool,
65
+ filename: Imgproxy::OptionsCasters::String,
66
+ format: Imgproxy::OptionsCasters::String,
67
+ }.freeze
68
+
69
+ META = %i[size resize adjust].freeze
13
70
 
14
71
  # @param options [Hash] raw processing options
15
72
  def initialize(options)
16
- merge!(options.slice(*ALL_OPTS))
73
+ # Options order hack: initialize known and meta options with nil value to preserve order
74
+ CASTERS.each_key { |n| self[n] = nil if options.key?(n) || META.include?(n) }
17
75
 
18
- typecast
76
+ options.each do |name, value|
77
+ caster = CASTERS[name]
78
+ self[name] = caster ? caster.cast(value) : unwrap_hash(value)
79
+ end
19
80
 
20
81
  group_resizing_opts
21
- group_gravity_opts
22
- group_watermark_opts
23
-
24
- encode_style
82
+ group_adjust_opts
25
83
 
26
- replace(Hash[sort_by { |k, _| OPTS_PRIORITY.index(k) || 99 }])
27
-
28
- freeze
84
+ compact!
29
85
  end
30
86
 
31
87
  private
32
88
 
33
- def typecast
34
- compact.each do |key, value|
35
- self[key] =
36
- case key
37
- when *STRING_OPTS then value.to_s
38
- when *INT_OPTS then value.to_i
39
- when *FLOAT_OPTS then value.to_f
40
- when *BOOL_OPTS then bool(value)
41
- when *ARRAY_OPTS then wrap_array(value)
42
- end
43
- end
44
- end
45
-
46
- def bool(value)
47
- value && value != 0 && value != "0" ? 1 : 0
48
- end
89
+ def unwrap_hash(raw)
90
+ return raw unless raw.is_a?(Hash)
49
91
 
50
- def wrap_array(value)
51
- value.is_a?(Array) ? value : [value]
92
+ raw.flat_map do |_key, val|
93
+ unwrap_hash(val)
94
+ end
52
95
  end
53
96
 
54
97
  def group_resizing_opts
55
- return unless self[:width] && self[:height]
56
-
57
- self[:size] = trim_nils(
58
- [delete(:width), delete(:height), delete(:enlarge), delete(:extend)],
59
- )
98
+ return unless self[:width] && self[:height] && !self[:size] && !self[:resize]
60
99
 
100
+ self[:size] = extract_and_trim_nils(:width, :height, :enlarge, :extend)
61
101
  self[:resize] = [delete(:resizing_type), *delete(:size)] if self[:resizing_type]
62
102
  end
63
103
 
64
- def group_gravity_opts
65
- gravity = trim_nils(
66
- [
67
- delete(:gravity),
68
- delete(:gravity_x),
69
- delete(:gravity_y),
70
- ],
71
- )
72
-
73
- self[:gravity] = gravity unless gravity[0].nil?
74
- end
75
-
76
- def group_watermark_opts
77
- watermark = trim_nils(
78
- [
79
- delete(:watermark_opacity),
80
- delete(:watermark_position),
81
- delete(:watermark_x_offset),
82
- delete(:watermark_y_offset),
83
- delete(:watermark_scale),
84
- ],
85
- )
86
-
87
- self[:watermark] = watermark unless watermark[0].nil?
88
- end
104
+ def group_adjust_opts
105
+ return if self[:adjust]
106
+ return unless values_at(:brightness, :contrast, :saturation).count { |o| o } > 1
89
107
 
90
- def encode_style
91
- return if self[:style].nil?
92
- self[:style] = Base64.urlsafe_encode64(self[:style]).tr("=", "")
108
+ self[:adjust] = extract_and_trim_nils(:brightness, :contrast, :saturation)
93
109
  end
94
110
 
95
- def trim_nils(value)
96
- value.delete_at(-1) while !value.empty? && value[-1].nil?
97
- value
111
+ def extract_and_trim_nils(*keys)
112
+ keys.map { |k| delete(k) }.trim!
98
113
  end
99
114
  end
100
115
  end