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.
- checksums.yaml +4 -4
- data/README.md +236 -92
- data/lib/imgproxy.rb +78 -38
- data/lib/imgproxy/builder.rb +56 -39
- data/lib/imgproxy/config.rb +96 -25
- data/lib/imgproxy/extensions/active_storage.rb +10 -0
- data/lib/imgproxy/extensions/shrine.rb +10 -0
- data/lib/imgproxy/options.rb +87 -72
- data/lib/imgproxy/options_aliases.rb +43 -0
- data/lib/imgproxy/options_casters/adjust.rb +22 -0
- data/lib/imgproxy/options_casters/array.rb +12 -0
- data/lib/imgproxy/options_casters/base64.rb +12 -0
- data/lib/imgproxy/options_casters/bool.rb +12 -0
- data/lib/imgproxy/options_casters/crop.rb +23 -0
- data/lib/imgproxy/options_casters/extend.rb +26 -0
- data/lib/imgproxy/options_casters/float.rb +16 -0
- data/lib/imgproxy/options_casters/gif_options.rb +21 -0
- data/lib/imgproxy/options_casters/gravity.rb +23 -0
- data/lib/imgproxy/options_casters/group.rb +21 -0
- data/lib/imgproxy/options_casters/integer.rb +10 -0
- data/lib/imgproxy/options_casters/jpeg_options.rb +26 -0
- data/lib/imgproxy/options_casters/png_options.rb +23 -0
- data/lib/imgproxy/options_casters/resize.rb +21 -0
- data/lib/imgproxy/options_casters/size.rb +24 -0
- data/lib/imgproxy/options_casters/string.rb +10 -0
- data/lib/imgproxy/options_casters/trim.rb +28 -0
- data/lib/imgproxy/options_casters/watermark.rb +30 -0
- data/lib/imgproxy/trim_array.rb +11 -0
- data/lib/imgproxy/url_adapters.rb +0 -4
- data/lib/imgproxy/url_adapters/active_storage.rb +25 -0
- data/lib/imgproxy/url_adapters/shrine.rb +15 -5
- data/lib/imgproxy/version.rb +1 -1
- metadata +69 -24
- data/lib/imgproxy/url_adapters/active_storage_gcs.rb +0 -31
- data/lib/imgproxy/url_adapters/active_storage_s3.rb +0 -23
- data/lib/imgproxy/url_adapters/shrine_s3.rb +0 -20
data/lib/imgproxy/builder.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
require "openssl"
|
2
2
|
require "base64"
|
3
|
-
require "
|
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
|
-
|
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,
|
39
|
-
|
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
|
-
|
55
|
+
File.join(Imgproxy.config.endpoint.to_s, "info", signature, path)
|
44
56
|
end
|
45
57
|
|
46
58
|
private
|
47
59
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
71
|
-
|
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
|
76
|
-
|
75
|
+
def url(image, ext: nil)
|
76
|
+
url = config.url_adapters.url_of(image)
|
77
77
|
|
78
|
-
|
78
|
+
@base64_encode_url ? base64_url_for(url, ext: ext) : plain_url_for(url, ext: ext)
|
79
79
|
end
|
80
80
|
|
81
|
-
def
|
82
|
-
|
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
|
86
|
-
|
87
|
-
|
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.
|
121
|
+
config.raw_key
|
109
122
|
end
|
110
123
|
|
111
124
|
def signature_salt
|
112
|
-
config.
|
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
|
data/lib/imgproxy/config.rb
CHANGED
@@ -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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
100
|
+
warn "[DEPRECATION] #hex_key is deprecated. Please use #key instead."
|
101
|
+
self.key = value
|
30
102
|
end
|
31
103
|
|
32
|
-
#
|
33
|
-
#
|
34
|
-
# @param value [String] hex-encoded signature salt
|
104
|
+
# @deprecated Please use {#salt} instead
|
35
105
|
def hex_salt=(value)
|
36
|
-
|
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
|
data/lib/imgproxy/options.rb
CHANGED
@@ -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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
22
|
-
group_watermark_opts
|
23
|
-
|
24
|
-
encode_style
|
82
|
+
group_adjust_opts
|
25
83
|
|
26
|
-
|
27
|
-
|
28
|
-
freeze
|
84
|
+
compact!
|
29
85
|
end
|
30
86
|
|
31
87
|
private
|
32
88
|
|
33
|
-
def
|
34
|
-
|
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
|
-
|
51
|
-
|
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
|
65
|
-
|
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
|
-
|
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
|
96
|
-
|
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
|