rack_resize 0.1.3 → 0.1.6

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.
@@ -0,0 +1,200 @@
1
+ module RackResize::ColorUtils
2
+ extend self
3
+
4
+ CSS_COLORS = {
5
+ 'aliceblue' => [240, 248, 255],
6
+ 'antiquewhite' => [250, 235, 215],
7
+ 'aqua' => [0, 255, 255],
8
+ 'aquamarine' => [127, 255, 212],
9
+ 'azure' => [240, 255, 255],
10
+ 'beige' => [245, 245, 220],
11
+ 'bisque' => [255, 228, 196],
12
+ 'black' => [0, 0, 0],
13
+ 'blanchedalmond' => [255, 235, 205],
14
+ 'blue' => [0, 0, 255],
15
+ 'blueviolet' => [138, 43, 226],
16
+ 'brown' => [165, 42, 42],
17
+ 'burlywood' => [222, 184, 135],
18
+ 'cadetblue' => [95, 158, 160],
19
+ 'chartreuse' => [127, 255, 0],
20
+ 'chocolate' => [210, 105, 30],
21
+ 'coral' => [255, 127, 80],
22
+ 'cornflowerblue' => [100, 149, 237],
23
+ 'cornsilk' => [255, 248, 220],
24
+ 'crimson' => [220, 20, 60],
25
+ 'cyan' => [0, 255, 255],
26
+ 'darkblue' => [0, 0, 139],
27
+ 'darkcyan' => [0, 139, 139],
28
+ 'darkgoldenrod' => [184, 134, 11],
29
+ 'darkgray' => [169, 169, 169],
30
+ 'darkgreen' => [0, 100, 0],
31
+ 'darkgrey' => [169, 169, 169],
32
+ 'darkkhaki' => [189, 183, 107],
33
+ 'darkmagenta' => [139, 0, 139],
34
+ 'darkolivegreen' => [85, 107, 47],
35
+ 'darkorange' => [255, 140, 0],
36
+ 'darkorchid' => [153, 50, 204],
37
+ 'darkred' => [139, 0, 0],
38
+ 'darksalmon' => [233, 150, 122],
39
+ 'darkseagreen' => [143, 188, 143],
40
+ 'darkslateblue' => [72, 61, 139],
41
+ 'darkslategray' => [47, 79, 79],
42
+ 'darkslategrey' => [47, 79, 79],
43
+ 'darkturquoise' => [0, 206, 209],
44
+ 'darkviolet' => [148, 0, 211],
45
+ 'deeppink' => [255, 20, 147],
46
+ 'deepskyblue' => [0, 191, 255],
47
+ 'dimgray' => [105, 105, 105],
48
+ 'dimgrey' => [105, 105, 105],
49
+ 'dodgerblue' => [30, 144, 255],
50
+ 'firebrick' => [178, 34, 34],
51
+ 'floralwhite' => [255, 250, 240],
52
+ 'forestgreen' => [34, 139, 34],
53
+ 'fuchsia' => [255, 0, 255],
54
+ 'gainsboro' => [220, 220, 220],
55
+ 'ghostwhite' => [248, 248, 255],
56
+ 'gold' => [255, 215, 0],
57
+ 'goldenrod' => [218, 165, 32],
58
+ 'gray' => [128, 128, 128],
59
+ 'green' => [0, 128, 0],
60
+ 'greenyellow' => [173, 255, 47],
61
+ 'grey' => [128, 128, 128],
62
+ 'honeydew' => [240, 255, 240],
63
+ 'hotpink' => [255, 105, 180],
64
+ 'indianred' => [205, 92, 92],
65
+ 'indigo' => [75, 0, 130],
66
+ 'ivory' => [255, 255, 240],
67
+ 'khaki' => [240, 230, 140],
68
+ 'lavender' => [230, 230, 250],
69
+ 'lavenderblush' => [255, 240, 245],
70
+ 'lawngreen' => [124, 252, 0],
71
+ 'lemonchiffon' => [255, 250, 205],
72
+ 'lightblue' => [173, 216, 230],
73
+ 'lightcoral' => [240, 128, 128],
74
+ 'lightcyan' => [224, 255, 255],
75
+ 'lightgoldenrodyellow' => [250, 250, 210],
76
+ 'lightgray' => [211, 211, 211],
77
+ 'lightgreen' => [144, 238, 144],
78
+ 'lightgrey' => [211, 211, 211],
79
+ 'lightpink' => [255, 182, 193],
80
+ 'lightsalmon' => [255, 160, 122],
81
+ 'lightseagreen' => [32, 178, 170],
82
+ 'lightskyblue' => [135, 206, 250],
83
+ 'lightslategray' => [119, 136, 153],
84
+ 'lightslategrey' => [119, 136, 153],
85
+ 'lightsteelblue' => [176, 196, 222],
86
+ 'lightyellow' => [255, 255, 224],
87
+ 'lime' => [0, 255, 0],
88
+ 'limegreen' => [50, 205, 50],
89
+ 'linen' => [250, 240, 230],
90
+ 'magenta' => [255, 0, 255],
91
+ 'maroon' => [128, 0, 0],
92
+ 'mediumaquamarine' => [102, 205, 170],
93
+ 'mediumblue' => [0, 0, 205],
94
+ 'mediumorchid' => [186, 85, 211],
95
+ 'mediumpurple' => [147, 112, 219],
96
+ 'mediumseagreen' => [60, 179, 113],
97
+ 'mediumslateblue' => [123, 104, 238],
98
+ 'mediumspringgreen' => [0, 250, 154],
99
+ 'mediumturquoise' => [72, 209, 204],
100
+ 'mediumvioletred' => [199, 21, 133],
101
+ 'midnightblue' => [25, 25, 112],
102
+ 'mintcream' => [245, 255, 250],
103
+ 'mistyrose' => [255, 228, 225],
104
+ 'moccasin' => [255, 228, 181],
105
+ 'navajowhite' => [255, 222, 173],
106
+ 'navy' => [0, 0, 128],
107
+ 'oldlace' => [253, 245, 230],
108
+ 'olive' => [128, 128, 0],
109
+ 'olivedrab' => [107, 142, 35],
110
+ 'orange' => [255, 165, 0],
111
+ 'orangered' => [255, 69, 0],
112
+ 'orchid' => [218, 112, 214],
113
+ 'palegoldenrod' => [238, 232, 170],
114
+ 'palegreen' => [152, 251, 152],
115
+ 'paleturquoise' => [175, 238, 238],
116
+ 'palevioletred' => [219, 112, 147],
117
+ 'papayawhip' => [255, 239, 213],
118
+ 'peachpuff' => [255, 218, 185],
119
+ 'peru' => [205, 133, 63],
120
+ 'pink' => [255, 192, 203],
121
+ 'plum' => [221, 160, 221],
122
+ 'powderblue' => [176, 224, 230],
123
+ 'purple' => [128, 0, 128],
124
+ 'rebeccapurple' => [102, 51, 153],
125
+ 'red' => [255, 0, 0],
126
+ 'rosybrown' => [188, 143, 143],
127
+ 'royalblue' => [65, 105, 225],
128
+ 'saddlebrown' => [139, 69, 19],
129
+ 'salmon' => [250, 128, 114],
130
+ 'sandybrown' => [244, 164, 96],
131
+ 'seagreen' => [46, 139, 87],
132
+ 'seashell' => [255, 245, 238],
133
+ 'sienna' => [160, 82, 45],
134
+ 'silver' => [192, 192, 192],
135
+ 'skyblue' => [135, 206, 235],
136
+ 'slateblue' => [106, 90, 205],
137
+ 'slategray' => [112, 128, 144],
138
+ 'slategrey' => [112, 128, 144],
139
+ 'snow' => [255, 250, 250],
140
+ 'springgreen' => [0, 255, 127],
141
+ 'steelblue' => [70, 130, 180],
142
+ 'tan' => [210, 180, 140],
143
+ 'teal' => [0, 128, 128],
144
+ 'thistle' => [216, 191, 216],
145
+ 'tomato' => [255, 99, 71],
146
+ 'turquoise' => [64, 224, 208],
147
+ 'violet' => [238, 130, 238],
148
+ 'wheat' => [245, 222, 179],
149
+ 'white' => [255, 255, 255],
150
+ 'whitesmoke' => [248, 248, 248],
151
+ 'yellow' => [255, 255, 0],
152
+ 'yellowgreen' => [154, 205, 50],
153
+ }.freeze
154
+
155
+ # Returns [r, g, b, a] where r/g/b are 0-255 integers and a is a 0.0-1.0 float.
156
+ # Returns nil if the value cannot be parsed.
157
+ # Supported formats:
158
+ # CSS named color: "red", "white", "cornflowerblue"
159
+ # 3-digit hex: "#a84"
160
+ # 6-digit hex: "#aa8844"
161
+ # 8-digit hex: "#aa884480" (last two digits = alpha 0-255)
162
+ # decimal RGB: "0,255,0"
163
+ # decimal RGBA: "0,255,0,0.5" (alpha 0.0-1.0)
164
+ def parse_color(value)
165
+ return nil if value.nil?
166
+ v = value.to_s.strip.downcase
167
+ return nil if v.empty?
168
+
169
+ if (m = v.match(/\A#([0-9a-f])([0-9a-f])([0-9a-f])\z/))
170
+ return [m[1].hex * 17, m[2].hex * 17, m[3].hex * 17, 1.0]
171
+ end
172
+
173
+ if (m = v.match(/\A#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})\z/))
174
+ return [m[1].to_i(16), m[2].to_i(16), m[3].to_i(16), 1.0]
175
+ end
176
+
177
+ if (m = v.match(/\A#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})\z/))
178
+ return [m[1].to_i(16), m[2].to_i(16), m[3].to_i(16), m[4].to_i(16) / 255.0]
179
+ end
180
+
181
+ if CSS_COLORS.key?(v)
182
+ return CSS_COLORS[v] + [1.0]
183
+ end
184
+
185
+ if (m = v.match(/\A(\d+),(\d+),(\d+)(?:,(\d*\.?\d+))?\z/))
186
+ r = m[1].to_i.clamp(0, 255)
187
+ g = m[2].to_i.clamp(0, 255)
188
+ b = m[3].to_i.clamp(0, 255)
189
+ a = m[4] ? m[4].to_f.clamp(0.0, 1.0) : 1.0
190
+ return [r, g, b, a]
191
+ end
192
+
193
+ nil
194
+ end
195
+
196
+ def to_hex(bg_color)
197
+ r, g, b, _a = bg_color
198
+ "#%02x%02x%02x" % [r, g, b]
199
+ end
200
+ end
@@ -1,7 +1,7 @@
1
1
  require 'pathname'
2
2
 
3
3
  class RackResize::Configuration
4
- PROCESSORS = %i[sips vips mini_magick imlib2].freeze
4
+ PROCESSORS = %i[sips vips mini_magick imlib2 bun_image].freeze
5
5
 
6
6
  attr_reader :processor, :assets_folders
7
7
  attr_accessor :save_resized, :default_quality, :cache_folder, :http_cache_max_age, :max_dimension, :logger
@@ -63,8 +63,9 @@ class RackResize::Configuration
63
63
  when :vips then RackResize::Processors::Vips.new
64
64
  when :sips then RackResize::Processors::Sips.new
65
65
  when :imlib2 then RackResize::Processors::Imlib2.new
66
+ when :bun_image then RackResize::Processors::BunImage.new
66
67
  else
67
- raise "RackResize - Unknow image processor #{@processor.inspect}"
68
+ raise "RackResize - Unknown image processor #{@processor.inspect}"
68
69
  end
69
70
  end
70
71
 
@@ -9,7 +9,7 @@ module RackResize::InputParsers::Cloudflare
9
9
  return { route_matched: false, req_params: nil, asset_path: nil }
10
10
  end
11
11
 
12
- fullpath = fullpath.delete_prefix("/cdn-cgi").delete_prefix("/image").delete_prefix("/")
12
+ fullpath = fullpath.delete_prefix(cf_path_prefix).delete_prefix("/")
13
13
  file_path_match = fullpath.match(%r{(?<params>[^\/]+)(?<file>\/.+?)(-[\da-f]{8})?(?<ext>\.\w{2,})$})
14
14
 
15
15
  return { route_matched: true, req_params: nil, asset_path: nil } unless file_path_match
@@ -0,0 +1,71 @@
1
+ module RackResize::InputParsers::Imgproxy
2
+ extend self
3
+
4
+ # Supports imgproxy URL formats:
5
+ # /insecure/rs:fill:400:300/q:85/f:webp/plain/local:///assets/photo.jpg
6
+ # /insecure/rs:fit:200:150/plain/local:///assets/banner.jpg@webp
7
+ # /my-sig-123/w:400/h:300/q:80/plain/assets/photo.png
8
+ # /insecure/plain/local:///assets/photo.jpg@avif
9
+
10
+ REGEX = %r{\A/(?<signature>[^/]+)/(?:(?<options>(?:[a-z_]+:[^/]+/)*))plain/(?:local:///?|https?://[^/]+/)?(?<path>[^@\s]+?)(?:@(?<format>\w+))?\z}i
11
+
12
+ def parse_input(fullpath)
13
+ match = fullpath.to_s.match(REGEX)
14
+ return { route_matched: false, req_params: nil, asset_path: nil } unless match
15
+
16
+ req_params = {}
17
+
18
+ if match[:options]
19
+ match[:options].split("/").each do |part|
20
+ next if part.empty?
21
+ type, *args = part.split(":")
22
+ case type.downcase
23
+ when "rs", "resize"
24
+ # rs:<type>:<width>:<height>:<enlarge>:<extend>
25
+ req_params[:fit] = map_fit(args[0]) if args[0] && !args[0].empty?
26
+ req_params[:width] = args[1] if args[1] && !args[1].empty? && args[1] != "0"
27
+ req_params[:height] = args[2] if args[2] && !args[2].empty? && args[2] != "0"
28
+ when "w", "width"
29
+ req_params[:width] = args[0] if args[0] && !args[0].empty?
30
+ when "h", "height"
31
+ req_params[:height] = args[0] if args[0] && !args[0].empty?
32
+ when "dpr"
33
+ req_params[:dpr] = args[0] if args[0] && !args[0].empty?
34
+ when "q", "quality"
35
+ req_params[:quality] = args[0] if args[0] && !args[0].empty?
36
+ when "f", "format", "ext"
37
+ req_params[:format] = args[0] if args[0] && !args[0].empty?
38
+ when "bg", "background"
39
+ req_params[:background] = args.join(":") if args.any?
40
+ when "fit"
41
+ req_params[:fit] = map_fit(args[0]) if args[0] && !args[0].empty?
42
+ end
43
+ end
44
+ end
45
+
46
+ if match[:format]
47
+ req_params[:format] = match[:format]
48
+ end
49
+
50
+ raw_path = match[:path]
51
+ raw_path = "/" + raw_path unless raw_path.start_with?("/")
52
+ asset_path = raw_path.sub(/-[\da-f]{8}(?=\.\w{2,}$)/, "")
53
+
54
+ { route_matched: true, req_params: req_params, asset_path: asset_path }
55
+ end
56
+
57
+ private
58
+
59
+ def map_fit(val)
60
+ case val.to_s.downcase
61
+ when "fill", "fill-down", "cover"
62
+ "cover"
63
+ when "fit", "contain"
64
+ "contain"
65
+ when "force"
66
+ "fill"
67
+ else
68
+ val
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,30 @@
1
+ module RackResize::InputParsers::QueryString
2
+ extend self
3
+
4
+ # Supports Fastly and bunny.net style query params:
5
+ # /assets/photo.jpg?width=200&height=100&quality=85&fit=cover
6
+ # /assets/photo.jpg?w=200&h=100&dpr=2&format=webp
7
+ # /assets/photo.jpg?w=200&f=webp&q=80&fit=contain
8
+ #
9
+ # Shortcuts: f=format, q=quality
10
+
11
+ CANONICAL_PARAMS = %w[width height quality dpr format fit bg-color background].freeze
12
+ PARAM_ALIASES = { 'f' => :format, 'q' => :quality, 'h' => :height, 'w' => :width, 'bg' => :'bg-color' }.freeze
13
+ RESIZE_PARAMS = (CANONICAL_PARAMS + PARAM_ALIASES.keys).freeze
14
+
15
+ def parse_input(fullpath, query_string)
16
+ params = Rack::Utils.parse_query(query_string.to_s)
17
+
18
+ unless params.keys.any? { |k| RESIZE_PARAMS.include?(k) }
19
+ return { route_matched: false, req_params: nil, asset_path: nil }
20
+ end
21
+
22
+ # Aliases are applied first; canonical names overwrite them if both are present.
23
+ aliased = params.slice(*PARAM_ALIASES.keys).transform_keys { |k| PARAM_ALIASES[k] }
24
+ canonical = params.slice(*CANONICAL_PARAMS).transform_keys(&:to_sym)
25
+ req_params = aliased.merge(canonical)
26
+ asset_path = fullpath.sub(/-[\da-f]{8}(?=\.\w{2,}$)/, '')
27
+
28
+ { route_matched: true, req_params:, asset_path: }
29
+ end
30
+ end
@@ -1,4 +1,5 @@
1
1
  require 'digest'
2
+ require 'fileutils'
2
3
 
3
4
  class RackResize::Processing
4
5
  attr_reader :config
@@ -9,7 +10,9 @@ class RackResize::Processing
9
10
 
10
11
  def process!(source_file:, req_params:)
11
12
  if config.save_resized?
12
- tmp_file_name = config.cache_folder.join(Digest::MD5.hexdigest(source_file.to_s) + File.extname(source_file))
13
+ cache_key = Digest::MD5.hexdigest("#{source_file}:#{req_params.to_a.sort.to_s}")
14
+ output_ext = output_extension(source_file, req_params[:format])
15
+ tmp_file_name = config.cache_folder.join(cache_key + output_ext)
13
16
  FileUtils.mkdir_p(tmp_file_name.dirname)
14
17
 
15
18
  unless tmp_file_name.exist?
@@ -17,7 +20,12 @@ class RackResize::Processing
17
20
  end
18
21
 
19
22
  logger.info("Serving cached file #{tmp_file_name}")
20
- return StringIO.new(File.open(tmp_file_name.to_s, "rb", &:read))
23
+ begin
24
+ return StringIO.new(File.open(tmp_file_name.to_s, "rb", &:read))
25
+ rescue Errno::ENOENT
26
+ process_file(req_params:, source_file:, target_file: tmp_file_name.to_s)
27
+ return StringIO.new(File.open(tmp_file_name.to_s, "rb", &:read))
28
+ end
21
29
  else
22
30
  file_content = process_file(req_params:, source_file:, target_file: nil)
23
31
  return StringIO.new(file_content)
@@ -26,15 +34,20 @@ class RackResize::Processing
26
34
 
27
35
  def process_file(source_file:, req_params:, target_file: nil)
28
36
  dpr = req_params[:dpr]&.to_f || 1.0
29
- dpr = [[dpr, 0.1].max, 10.0].min
37
+ dpr = dpr.clamp(0.1, 10.0)
30
38
 
31
39
  max = config.max_dimension.to_f
32
40
  target_width = (req_params[:w]&.to_i || req_params[:width]&.to_i)&.*(dpr)&.clamp(1, max)
33
41
  target_height = (req_params[:h]&.to_i || req_params[:height]&.to_i)&.*(dpr)&.clamp(1, max)
34
42
 
43
+ fit = req_params[:fit]
44
+ format = req_params[:format].then { |f| f == 'auto' ? nil : f }
45
+ quality = req_params[:quality]&.to_i&.clamp(1, 100)
46
+ bg_color = RackResize::ColorUtils.parse_color(req_params[:"bg-color"] || req_params[:background])
47
+
35
48
  start_time = Time.now
36
49
  begin
37
- return config.processor_instance.resize(source_file:, target_file:, target_width:, target_height:)
50
+ return config.processor_instance.resize(source_file:, target_file:, target_width:, target_height:, fit:, format:, quality:, bg_color:)
38
51
  ensure
39
52
  processing_time = (Time.now.to_f - start_time.to_f).round(3)
40
53
  logger.info("RESIZE IMAGE #{config.processor} #{source_file} - #{req_params} - #{processing_time}s")
@@ -44,6 +57,13 @@ class RackResize::Processing
44
57
  def logger
45
58
  config.logger ||
46
59
  (defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger) ||
47
- (require('logger') && Logger.new(STDOUT))
60
+ (@_logger ||= (require 'logger'; Logger.new($stdout)))
61
+ end
62
+
63
+ private
64
+
65
+ def output_extension(source_file, format)
66
+ return File.extname(source_file) if format.nil?
67
+ ".#{format.to_s.downcase}"
48
68
  end
49
69
  end
@@ -0,0 +1,34 @@
1
+ const opts = JSON.parse(Bun.argv[2]);
2
+ const { source, target, width, height, format, quality, fit } = opts;
3
+
4
+ let img = Bun.file(source).image();
5
+
6
+ if (width && height) {
7
+ img = img.resize(width, height, { fit: fit === "fill" ? "fill" : "inside" });
8
+ } else if (width) {
9
+ img = img.resize(width);
10
+ } else if (height) {
11
+ // Bun.Image lacks height-only resize; derive width from source aspect ratio.
12
+ const meta = await img.metadata();
13
+ const w = Math.max(1, Math.round((meta.width * height) / meta.height));
14
+ img = new Bun.Image(source).resize(w, height, { fit: "inside" });
15
+ }
16
+
17
+ const q = quality ?? 85;
18
+ const outFmt = (format || source.split(".").pop()).toLowerCase();
19
+
20
+ switch (outFmt) {
21
+ case "jpg":
22
+ case "jpeg": img = img.jpeg({ quality: q }); break;
23
+ case "png": img = img.png(); break;
24
+ case "webp": img = img.webp({ quality: q }); break;
25
+ case "avif": img = img.avif({ quality: q }); break;
26
+ case "heic": img = img.heic({ quality: q }); break;
27
+ default: throw new Error(`unsupported format: ${outFmt}`);
28
+ }
29
+
30
+ if (target) {
31
+ await img.write(target);
32
+ } else {
33
+ process.stdout.write(await img.bytes());
34
+ }
@@ -0,0 +1,42 @@
1
+ require "open3"
2
+ require "json"
3
+ require "tempfile"
4
+
5
+ class RackResize::Processors::BunImage
6
+ SCRIPT = File.expand_path("bun_image.js", __dir__)
7
+
8
+ def resize(source_file:, target_width:, target_height:, target_file: nil, fit: nil, format: nil, quality: nil, bg_color: nil)
9
+ quality ||= RackResize.config.default_quality
10
+ cover = (fit == 'cover' || fit == 'crop') && target_width && target_height
11
+
12
+ # ponytail: Bun.Image has no crop/extract yet, so `cover` falls back to `fill`
13
+ # (stretches to exact dims). Upgrade when Bun ships extract() or fit:"cover".
14
+ bun_fit = cover ? 'fill' : 'inside'
15
+
16
+ payload = {
17
+ source: source_file.to_s,
18
+ target: target_file&.to_s,
19
+ width: target_width&.to_i,
20
+ height: target_height&.to_i,
21
+ format: format && (format.to_s.downcase == 'jpg' ? 'jpeg' : format.to_s.downcase),
22
+ quality: quality&.to_i,
23
+ fit: bun_fit,
24
+ }
25
+
26
+ if target_file
27
+ run_bun(payload)
28
+ nil
29
+ else
30
+ out, err, status = Open3.capture3("bun", SCRIPT, JSON.generate(payload))
31
+ raise "bun_image failed: #{err}" unless status.success?
32
+ out
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def run_bun(payload)
39
+ _, err, status = Open3.capture3("bun", SCRIPT, JSON.generate(payload))
40
+ raise "bun_image failed: #{err}" unless status.success?
41
+ end
42
+ end
@@ -6,14 +6,51 @@ end
6
6
 
7
7
  class RackResize::Processors::Imlib2
8
8
 
9
- def resize(source_file:, target_file:, target_width:, target_height:)
9
+ def resize(source_file:, target_file:, target_width:, target_height:, fit: nil, format: nil, quality: nil, bg_color: nil)
10
+ quality ||= RackResize.config.default_quality
11
+ cover = (fit == 'cover' || fit == 'crop') && target_width && target_height
12
+
10
13
  image = Rszr::Image.load(source_file)
11
- image.resize!(target_width || :auto, target_height || :auto) if target_width || target_height
14
+
15
+ if target_width || target_height
16
+ if cover
17
+ target_w = target_width.to_i
18
+ target_h = target_height.to_i
19
+ src_w = image.width
20
+ src_h = image.height
21
+
22
+ if target_w.to_f / src_w >= target_h.to_f / src_h
23
+ image.resize!(target_w, :auto)
24
+ crop_y = [(image.height - target_h) / 2, 0].max
25
+ image.crop!(0, crop_y, target_w, target_h)
26
+ else
27
+ image.resize!(:auto, target_h)
28
+ crop_x = [(image.width - target_w) / 2, 0].max
29
+ image.crop!(crop_x, 0, target_w, target_h)
30
+ end
31
+ else
32
+ image.resize!(target_width || :auto, target_height || :auto)
33
+ end
34
+ end
35
+
36
+ out_format = rszr_format(format || source_file.to_s)
12
37
 
13
38
  if target_file
14
39
  image.save(target_file)
40
+ nil
15
41
  else
16
- image.save_data(format: source_file.to_s =~ /\.png$/ ? :png : :jpeg)
42
+ image.save_data(format: out_format, quality: quality)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def rszr_format(format_or_path)
49
+ case format_or_path.to_s.downcase
50
+ when /\.png$/, 'png' then :png
51
+ when /\.webp$/, 'webp' then :webp
52
+ when /\.gif$/, 'gif' then :gif
53
+ else :jpeg
17
54
  end
18
55
  end
19
56
  end
@@ -4,18 +4,29 @@ rescue LoadError
4
4
  raise LoadError, "RackResize::Processors::MiniMagick requires the image_processing gem. Please add `gem \"image_processing\"` to your Gemfile."
5
5
  end
6
6
 
7
- # require 'mini_magick'
8
- # MiniMagick.logger.level = :debug
9
-
10
7
  class RackResize::Processors::MiniMagick
11
8
 
12
- def resize(source_file:, target_width:, target_height:, target_file: nil)
9
+ def resize(source_file:, target_width:, target_height:, target_file: nil, fit: nil, format: nil, quality: nil, bg_color: nil)
10
+ quality ||= RackResize.config.default_quality
11
+ cover = (fit == 'cover' || fit == 'crop') && target_width && target_height
12
+
13
13
  image = ImageProcessing::MiniMagick.source(source_file)
14
- image = image.resize_to_limit(target_width, target_height) if target_width || target_height
15
- image = image.saver(quality: RackResize.config.default_quality)
14
+ image = image.convert(format) if format
15
+
16
+ if target_width || target_height
17
+ image = cover ? image.resize_to_fill(target_width, target_height)
18
+ : image.resize_to_limit(target_width, target_height)
19
+ end
20
+
21
+ if bg_color
22
+ image = image.background(RackResize::ColorUtils.to_hex(bg_color)).flatten
23
+ end
24
+
25
+ image = image.saver(quality: quality)
16
26
 
17
27
  if target_file
18
28
  image.call(destination: target_file)
29
+ nil
19
30
  else
20
31
  begin
21
32
  tmp_file = image.call
@@ -25,23 +36,4 @@ class RackResize::Processors::MiniMagick
25
36
  end
26
37
  end
27
38
  end
28
-
29
- def resize_mm(source_file:, target_width:, target_height:, target_file: nil)
30
- image = MiniMagick::Image.open(source_file)
31
- image.combine_options do |img|
32
- img.resize("#{target_width}x#{target_height}>") if target_width || target_height
33
- img.quality(RackResize.config.default_quality)
34
-
35
- if target_file
36
- img.write(target_file)
37
- end
38
- end
39
-
40
- unless target_file
41
- return File.open(image.path, 'rb', &:read)
42
- end
43
-
44
- ensure
45
- image&.destroy!
46
- end
47
39
  end