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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +1 -1
- data/.ruby-version +1 -1
- data/Gemfile +3 -0
- data/README.md +185 -24
- data/benchmark/http_benchmark.rb +151 -0
- data/benchmark/processor_benchmark.rb +84 -0
- data/config.ru +1 -1
- data/lib/rack_resize/color_utils.rb +200 -0
- data/lib/rack_resize/configuration.rb +3 -2
- data/lib/rack_resize/input_parsers/cloudflare.rb +1 -1
- data/lib/rack_resize/input_parsers/imgproxy.rb +71 -0
- data/lib/rack_resize/input_parsers/query_string.rb +30 -0
- data/lib/rack_resize/processing.rb +25 -5
- data/lib/rack_resize/processors/bun_image.js +34 -0
- data/lib/rack_resize/processors/bun_image.rb +42 -0
- data/lib/rack_resize/processors/imlib2.rb +40 -3
- data/lib/rack_resize/processors/mini_magick.rb +17 -25
- data/lib/rack_resize/processors/sips.rb +55 -23
- data/lib/rack_resize/processors/vips.rb +18 -3
- data/lib/rack_resize/rack_app.rb +11 -7
- data/lib/rack_resize.rb +8 -5
- data/rack_resize.gemspec +1 -1
- metadata +9 -2
|
@@ -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 -
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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:
|
|
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.
|
|
15
|
-
|
|
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
|