rack_resize 0.1.3 → 0.1.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b93d3745135960f5c03fdec90f2104d5cf399d1bd67c248f16c8dd3061429c15
4
- data.tar.gz: c228aa468fce0696ec89889b616b326a28d92e0e411937167f94578de242e802
3
+ metadata.gz: aaecfcc4b1f54da6b128ae9fe7939db1676ab6069a0c15bbedaab5545c74a413
4
+ data.tar.gz: f7c136233b7a6bbed40c40fca10399139f30ab3393857851690f05593d2b44b4
5
5
  SHA512:
6
- metadata.gz: 4dfbd29c0d898694cb4f239a03a812d937dce5fd76b829faaea9587b12acb5c6406f926ec310bc9c26a2b8fc123e7bb3c2a5fa788132a21f3d84253a6165f0e4
7
- data.tar.gz: fa392a88e4dd71fc7b4d9f07e200adc67dea702c944d914f4c6808d871a5c785b73509f967dbe76eed0b00f3ca4d4a5c20f86591d9ec381bcbe17482317225c7
6
+ metadata.gz: 66afe23035cedbf3b32723375b16c75351e1b9d2c6907fa7989b80452181d102b76c719cea27261587e67331739ece84c68c62682791769d4a2c053c36e7d404
7
+ data.tar.gz: 0b6a7229f59709ca20b55ebeb0efd17cbf9ef8269410a1ae25a6479edb43e90a35294a5930c9d748aba4e5bc8f1bafb313e5a35fc3fd9dd2ce51544ba4a71bef
@@ -19,7 +19,7 @@ jobs:
19
19
  - name: Install package
20
20
  run: |
21
21
  sudo apt-get update
22
- sudo apt-get install -y libvips42 libvips-tools imagemagick libimlib2 libimlib2-dev
22
+ sudo apt-get install -y libvips42 libvips-tools imagemagick libimlib2 libimlib2-dev libheif-plugin-aomenc libavif-dev
23
23
 
24
24
  - uses: ruby/setup-ruby@v1
25
25
  with:
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 4.0.5
1
+ 4.0.6
data/Gemfile CHANGED
@@ -12,3 +12,6 @@ gem 'railties'
12
12
  gem 'minitest'
13
13
  gem 'minitest-reporters'
14
14
  gem 'rake'
15
+
16
+ gem 'benchmark-ips'
17
+ gem 'webrick'
data/README.md CHANGED
@@ -13,7 +13,7 @@ end
13
13
  How to use with Rack
14
14
 
15
15
  ```ruby
16
- use RackResize::RackApp, processor: :imlib2, assets_folder: "samples"
16
+ use RackResize::RackApp, processor: :mini_magick, assets_folders: ["samples"]
17
17
  use Rack::Static, urls: [""], root: "samples", index: "index.html" # optional
18
18
  ```
19
19
 
@@ -23,16 +23,77 @@ Cloudflare format: (can be used with helpers from `carrierwave-cloudflare` gem)
23
23
  ```
24
24
  /cdn-cgi/image/width=426,format=auto/assets/pets/dog.jpg
25
25
  ```
26
- Fastly and bunny.net: (tbd)
26
+ Fastly / bunny.net format (query string params):
27
27
  ```
28
- /assets/pets/dog.jpg?width=300
28
+ /assets/pets/dog.jpg?width=300&height=200&fit=cover&format=webp&quality=85
29
+ ```
30
+
31
+ ### Supported Parameters:
32
+
33
+ | Parameter | Shortcut | Type | Description |
34
+ |------------|----------|------|-------------|
35
+ | `width` | `w` | integer | Target width in pixels. Scales proportionally if `height` is omitted. |
36
+ | `height` | `h` | integer | Target height in pixels. Scales proportionally if `width` is omitted. |
37
+ | `fit` | — | string | Resize mode: `contain` (default), `cover`, `crop`. See below. |
38
+ | `format` | `f` | string | Output format: `jpeg`, `png`, `webp`, `avif`, `gif`. Use `auto` to keep original. |
39
+ | `quality` | `q` | integer (1–100) | Compression quality. Defaults to `default_quality` config value (95). |
40
+ | `dpr` | — | float (0.1–10) | Device pixel ratio. Multiplies `width` and `height` before processing. |
41
+ | `bg-color` | `bg` | color | Background color for flattening transparency. Also aliased as `background`. See formats below. |
42
+
43
+ **Background color formats:**
44
+ - CSS named color: `white`, `red`, `cornflowerblue` (all 148 CSS colors supported)
45
+ - 3-digit hex: `#a84`
46
+ - 6-digit hex: `#aa8844`
47
+ - 8-digit hex: `#aa884480` (last two digits = alpha 0–255)
48
+ - Decimal RGB: `0,255,0`
49
+ - Decimal RGBA: `0,255,0,0.5` (alpha 0.0–1.0)
50
+
51
+ Supported by: `vips`, `mini_magick`. Accepted but ignored by `sips` and `imlib2`.
52
+
53
+ **Fit modes:**
54
+ - `contain` — resizes to fit within the given box, preserving aspect ratio (default)
55
+ - `cover` / `crop` — resizes and center-crops to fill the exact box
56
+
57
+ **Example URLs (query string format):**
58
+ ```
59
+ # Resize to width only
60
+ /samples/image_1.jpeg?w=400
61
+
62
+ # Resize to fit within 400×300 box
63
+ /samples/image_1.jpeg?width=400&height=300
64
+
65
+ # Cover-crop to exact 400×300
66
+ /samples/image_1.jpeg?width=400&height=300&fit=cover
67
+
68
+ # Convert to WebP at 80% quality
69
+ /samples/image_1.jpeg?w=400&f=webp&q=80
70
+
71
+ # Retina (2×) resize
72
+ /samples/image_1.jpeg?w=200&dpr=2
73
+
74
+ # Flatten transparency with a background color
75
+ /samples/image.png?w=400&f=jpeg&bg-color=white
76
+ /samples/image.png?w=400&f=jpeg&bg=%23ff0000
77
+ /samples/image.png?w=400&f=jpeg&background=0,255,0,0.5
78
+ ```
79
+
80
+ **Example URLs (Cloudflare format):**
81
+ ```
82
+ # Resize to width
83
+ /cdn-cgi/image/width=400/samples/image_1.jpeg
84
+
85
+ # Cover-crop with format conversion
86
+ /cdn-cgi/image/width=400,height=300,fit=cover,format=webp/samples/image_1.jpeg
87
+
88
+ # Quality + format
89
+ /cdn-cgi/image/width=400,format=avif,quality=80/samples/image_1.jpeg
29
90
  ```
30
91
 
31
92
  ### Configuration:
32
93
 
33
94
  ```ruby
34
95
  RackResize.configure do |config|
35
- config.assets_folders = { "assets" => Rails.root.join('app', 'assets', 'images') }
96
+ config.assets_folders = { assets: Rails.root.join('app', 'assets', 'images') }
36
97
  config.processor = :sips / :vips / :mini_magick / :imlib2
37
98
  config.default_quality = 95
38
99
  config.save_resized = false
@@ -45,12 +106,25 @@ end
45
106
 
46
107
  <table>
47
108
  <tr>
48
- <th>libaray</th>
49
- <th>dependency</th>
109
+ <th>processor</th>
50
110
  <th>config</th>
111
+ <th>system library</th>
112
+ <th>gems</th>
113
+ </tr>
114
+ <tr>
115
+ <td>sips (macOS only)</td>
116
+ <td><code>processor: :sips</code></td>
117
+ <td>built-in on macOS</td>
118
+ <td>none</td>
51
119
  </tr>
52
120
  <tr>
53
121
  <td>mini_magick</td>
122
+ <td><code>processor: :mini_magick</code></td>
123
+ <td>
124
+
125
+ ImageMagick — `brew install imagemagick`
126
+
127
+ </td>
54
128
  <td>
55
129
 
56
130
  ```ruby
@@ -59,14 +133,15 @@ gem "mini_magick"
59
133
  ```
60
134
 
61
135
  </td>
136
+ </tr>
137
+ <tr>
138
+ <td>vips</td>
139
+ <td><code>processor: :vips</code></td>
62
140
  <td>
63
141
 
64
- `processor: :mini_magick`
142
+ libvips — `brew install vips`
65
143
 
66
144
  </td>
67
- </tr>
68
- <tr>
69
- <td>vips</td>
70
145
  <td>
71
146
 
72
147
  ```ruby
@@ -74,35 +149,64 @@ gem "image_processing"
74
149
  gem "ruby-vips"
75
150
  ```
76
151
 
77
- </td>
78
- <td>
79
-
80
- `processor: :vips`
81
-
82
152
  </td>
83
153
  </tr>
84
154
  <tr>
85
- <td>sips (MacOS only)</td>
86
- <td>none</td>
155
+ <td>imlib2</td>
156
+ <td><code>processor: :imlib2</code></td>
87
157
  <td>
88
158
 
89
- `processor: :sips`
159
+ Imlib2 — `brew install imlib2`
90
160
 
91
161
  </td>
92
- </tr>
93
- <tr>
94
- <td>imlib2</td>
95
162
  <td>
96
163
 
97
164
  ```ruby
98
- gem "rszr"
165
+ gem "rszr"
99
166
  ```
100
167
 
101
- </td>
102
- <td>
103
-
104
- `processor: :imlib2`
105
-
106
168
  </td>
107
169
  </tr>
108
170
  </table>
171
+
172
+ ### Supported Image Formats:
173
+
174
+ | Format | sips | mini_magick | vips | imlib2 |
175
+ |--------|:----:|:-----------:|:----:|:------:|
176
+ | JPEG | ✅ | ✅ | ✅ | ✅ |
177
+ | PNG | ✅ | ✅ | ✅ | ✅ |
178
+ | GIF | ✅ | ✅ | ✅ | ✅ |
179
+ | WebP | ❌ | ✅ ¹ | ✅ ¹ | ❌ |
180
+ | AVIF | ✅ ² | ✅ ³ | ✅ ⁴ | ❌ |
181
+ | HEIC | ✅ | ✅ ³ | ✅ ⁴ | ❌ |
182
+ | SVG | ❌ | ✅ ⁵ | ✅ ⁶ | ❌ |
183
+
184
+ ¹ Requires ImageMagick / libvips built with **libwebp** support (`brew install webp`)
185
+ ² macOS 13 (Ventura) or later
186
+ ³ Requires ImageMagick built with **libheif** support (`brew install libheif`)
187
+ ⁴ Requires libvips built with **libheif** support (`brew install libheif`)
188
+ ⁵ Requires **Inkscape** or **librsvg** (`brew install librsvg`)
189
+ ⁶ Requires libvips built with **librsvg** support (`brew install librsvg`)
190
+
191
+ ### Performance Benchmarks:
192
+
193
+ Measured on an Apple M1 Pro with Ruby 4.0.5. Output size: 300×200 px.
194
+ Run with `ruby benchmark/processor_benchmark.rb`
195
+
196
+ **JPEG — image_1.jpeg (51 KB)**
197
+
198
+ | Processor | i/s | ms/i | vs fastest |
199
+ |-------------|------:|------:|:----------:|
200
+ | imlib2 | 613.0 | 1.63 | — |
201
+ | vips | 216.9 | 4.61 | 2.83× |
202
+ | mini_magick | 36.4 | 27.49 | 16.85× |
203
+ | sips | 20.1 | 49.68 | 30.45× |
204
+
205
+ **PNG — sample.png (2 KB)**
206
+
207
+ | Processor | i/s | ms/i | vs fastest |
208
+ |-------------|------:|-----:|:----------:|
209
+ | vips | 431.1 | 2.32 | — |
210
+ | imlib2 | 221.7 | 4.51 | 1.94× |
211
+ | mini_magick | 36.9 | 27.12| 11.69× |
212
+ | sips | 19.4 | 51.62| 22.25× |
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # HTTP benchmark: starts a WEBrick server in a background thread, mounts one
4
+ # RackResize::RackApp per available processor at a distinct path prefix, then
5
+ # measures throughput with benchmark-ips over persistent HTTP connections.
6
+ #
7
+ # Usage:
8
+ # bundle exec ruby benchmark/http_benchmark.rb
9
+
10
+ require 'bundler/setup'
11
+ require 'rack'
12
+ require 'webrick'
13
+ require 'net/http'
14
+ require 'benchmark/ips'
15
+ require 'logger'
16
+ require 'stringio'
17
+ require 'timeout'
18
+ require_relative '../lib/rack_resize'
19
+
20
+ SAMPLES_DIR = File.expand_path('../samples', __dir__)
21
+ BENCH_PORT = 14_569
22
+ BENCH_IMAGE = 'image_1.jpeg'
23
+ BENCH_PARAMS = 'width=300&height=200'
24
+ NULL_LOGGER = Logger.new(IO::NULL)
25
+
26
+ # ── One RackApp per available processor ──────────────────────────────────────
27
+
28
+ available_apps = {}
29
+
30
+ puts "Loading processors..."
31
+ %i[sips vips mini_magick imlib2].each do |name|
32
+ begin
33
+ app = RackResize::RackApp.new(
34
+ processor: name,
35
+ assets_folders: { '/' => SAMPLES_DIR },
36
+ save_resized: false,
37
+ logger: NULL_LOGGER,
38
+ )
39
+ available_apps[name] = app
40
+ puts " ✓ #{name}"
41
+ rescue => e
42
+ puts " ✗ #{name}: #{e.message}"
43
+ end
44
+ end
45
+
46
+ abort "\nNo processors available." if available_apps.empty?
47
+
48
+ # ── URLMap: /sips → sips_app, /vips → vips_app, etc. ────────────────────────
49
+
50
+ rack_app = Rack::Builder.new do
51
+ available_apps.each do |name, app|
52
+ map("/#{name}") { run app }
53
+ end
54
+ end.to_app
55
+
56
+ # ── WEBrick with a minimal Rack env bridge ───────────────────────────────────
57
+
58
+ webrick = WEBrick::HTTPServer.new(
59
+ Port: BENCH_PORT,
60
+ Logger: WEBrick::Log.new(IO::NULL),
61
+ AccessLog: [],
62
+ )
63
+
64
+ webrick.mount_proc('/') do |req, res|
65
+ env = {
66
+ 'REQUEST_METHOD' => req.request_method,
67
+ 'PATH_INFO' => req.path,
68
+ 'QUERY_STRING' => req.query_string.to_s,
69
+ 'SERVER_NAME' => 'localhost',
70
+ 'SERVER_PORT' => BENCH_PORT.to_s,
71
+ 'HTTP_HOST' => "localhost:#{BENCH_PORT}",
72
+ 'SCRIPT_NAME' => '',
73
+ 'rack.version' => Rack::VERSION,
74
+ 'rack.input' => StringIO.new(''),
75
+ 'rack.errors' => $stderr,
76
+ 'rack.multithread' => true,
77
+ 'rack.multiprocess' => false,
78
+ 'rack.run_once' => false,
79
+ 'rack.url_scheme' => 'http',
80
+ }
81
+ req.header.each do |key, values|
82
+ env["HTTP_#{key.upcase.tr('-', '_')}"] = values.join(', ')
83
+ end
84
+
85
+ status, headers, body = rack_app.call(env)
86
+
87
+ res.status = status
88
+ headers.each { |k, v| res[k] = v }
89
+ body_str = +''
90
+ body.each { |chunk| body_str << chunk }
91
+ res.body = body_str
92
+ end
93
+
94
+ server_thread = Thread.new { webrick.start }
95
+
96
+ Timeout.timeout(10) do
97
+ loop do
98
+ TCPSocket.new('localhost', BENCH_PORT).close
99
+ break
100
+ rescue Errno::ECONNREFUSED
101
+ sleep 0.05
102
+ end
103
+ end
104
+
105
+ puts "\nServer listening on port #{BENCH_PORT}"
106
+
107
+ # ── Smoke-test each endpoint before benchmarking ─────────────────────────────
108
+
109
+ puts "\nVerifying endpoints..."
110
+ available_apps.keys.each do |name|
111
+ uri = URI("http://localhost:#{BENCH_PORT}/#{name}/#{BENCH_IMAGE}?#{BENCH_PARAMS}")
112
+ resp = Net::HTTP.get_response(uri)
113
+ if resp.is_a?(Net::HTTPOK)
114
+ puts " ✓ /#{name}/#{BENCH_IMAGE} → 200 (#{resp.body.bytesize} B)"
115
+ else
116
+ warn " ✗ /#{name}/#{BENCH_IMAGE} → #{resp.code}: #{resp.body}"
117
+ available_apps.delete(name)
118
+ end
119
+ end
120
+
121
+ abort "\nNo processors passed smoke test." if available_apps.empty?
122
+
123
+ # ── Persistent HTTP connections, one per processor ───────────────────────────
124
+
125
+ connections = available_apps.keys.to_h do |name|
126
+ http = Net::HTTP.new('localhost', BENCH_PORT)
127
+ http.start
128
+ [name, http]
129
+ end
130
+
131
+ puts "\n── HTTP rack server benchmark ───────────────────────────────────────────────"
132
+ puts " image : #{BENCH_IMAGE}"
133
+ puts " params: #{BENCH_PARAMS}"
134
+ puts
135
+
136
+ Benchmark.ips do |x|
137
+ x.config(time: 10, warmup: 3)
138
+
139
+ connections.each do |name, http|
140
+ path = "/#{name}/#{BENCH_IMAGE}?#{BENCH_PARAMS}"
141
+ x.report(name.to_s) { http.get(path) }
142
+ end
143
+
144
+ x.compare!
145
+ end
146
+
147
+ # ── Cleanup ───────────────────────────────────────────────────────────────────
148
+
149
+ connections.each_value(&:finish)
150
+ webrick.shutdown
151
+ server_thread.join
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Direct processor benchmark: instantiates each available RackResize processor
4
+ # and calls resize() directly on images from the samples/ folder.
5
+ #
6
+ # Usage:
7
+ # bundle exec ruby benchmark/processor_benchmark.rb
8
+
9
+ require 'bundler/setup'
10
+ require 'benchmark/ips'
11
+ require_relative '../lib/rack_resize'
12
+
13
+ SAMPLES_DIR = File.expand_path('../samples', __dir__)
14
+ BENCH_IMAGES = Dir[File.join(SAMPLES_DIR, '{image_1,sample}.{jpg,jpeg,png,webp,heic,avif}')].sort
15
+ BENCH_WIDTH = 300
16
+ BENCH_HEIGHT = 200
17
+
18
+ abort 'No sample images found in samples/.' if BENCH_IMAGES.empty?
19
+
20
+ # ── Collect available processor instances ────────────────────────────────────
21
+
22
+ PROCESSOR_CLASSES = {
23
+ sips: RackResize::Processors::Sips,
24
+ vips: RackResize::Processors::Vips,
25
+ mini_magick: RackResize::Processors::MiniMagick,
26
+ imlib2: RackResize::Processors::Imlib2,
27
+ }.freeze
28
+
29
+ puts "Loading processors..."
30
+ processors = {}
31
+
32
+ PROCESSOR_CLASSES.each do |name, klass|
33
+ begin
34
+ instance = klass.new
35
+ # Verify the processor actually works before including it
36
+ instance.resize(
37
+ source_file: BENCH_IMAGES.first,
38
+ target_file: nil,
39
+ target_width: BENCH_WIDTH,
40
+ target_height: BENCH_HEIGHT,
41
+ )
42
+ processors[name] = instance
43
+ puts " ✓ #{name}"
44
+ rescue => e
45
+ puts " ✗ #{name}: #{e.message}"
46
+ end
47
+ end
48
+
49
+ abort "\nNo processors available." if processors.empty?
50
+
51
+ puts "\nSample images: #{BENCH_IMAGES.map { |f| File.basename(f) }.join(', ')}"
52
+
53
+ # ── Benchmark each sample image independently ─────────────────────────────────
54
+
55
+ BENCH_IMAGES.each do |image_path|
56
+ image_name = File.basename(image_path)
57
+ image_size = File.size(image_path)
58
+
59
+ puts "\n── Direct processor benchmark ───────────────────────────────────────────────"
60
+ puts " image : #{image_name} (#{image_size / 1024} KB)"
61
+ puts " output: #{BENCH_WIDTH}x#{BENCH_HEIGHT}"
62
+ puts
63
+
64
+ Benchmark.ips do |x|
65
+ x.config(time: 5, warmup: 2)
66
+
67
+ processors.each do |name, processor|
68
+ next if name.to_s == "imlib2" && image_name =~ /heic|webp|avif/
69
+ next if name.to_s == "sips" && image_name =~ /webp/
70
+
71
+ x.report(name.to_s) do
72
+ processor.resize(
73
+ source_file: image_path,
74
+ target_file: nil,
75
+ target_width: BENCH_WIDTH,
76
+ target_height: BENCH_HEIGHT,
77
+ )
78
+ end
79
+ end
80
+
81
+ x.compare!
82
+ end
83
+ end
data/config.ru CHANGED
@@ -7,7 +7,7 @@
7
7
  require_relative "lib/rack_resize"
8
8
  require "rack/static"
9
9
 
10
- use RackResize::RackApp, processor: :imlib2, assets_folder: "samples"
10
+ use RackResize::RackApp, processor: :vips, assets_folders: ["samples"]
11
11
 
12
12
  use Rack::Static, urls: [""], root: "samples", index: "index.html"
13
13
 
@@ -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
@@ -64,7 +64,7 @@ class RackResize::Configuration
64
64
  when :sips then RackResize::Processors::Sips.new
65
65
  when :imlib2 then RackResize::Processors::Imlib2.new
66
66
  else
67
- raise "RackResize - Unknow image processor #{@processor.inspect}"
67
+ raise "RackResize - Unknown image processor #{@processor.inspect}"
68
68
  end
69
69
  end
70
70
 
@@ -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,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
@@ -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
@@ -1,20 +1,32 @@
1
1
  require "open3"
2
+ require "tempfile"
2
3
 
3
4
  class RackResize::Processors::Sips
4
5
 
5
- def resize(source_file:, target_width:, target_height:, target_file: nil)
6
+ def resize(source_file:, target_width:, target_height:, target_file: nil, fit: nil, format: nil, quality: nil, bg_color: nil)
7
+ quality ||= RackResize.config.default_quality
8
+ cover = (fit == 'cover' || fit == 'crop') && target_width && target_height
9
+
6
10
  args = %w[sips --deleteColorManagementProperties]
7
- args += ["-s", "formatOptions", RackResize.config.default_quality.to_s]
11
+ args += ["-s", "format", sips_format(format)] if format
12
+ args += ["-s", "formatOptions", quality.to_s]
8
13
 
9
14
  if target_width && target_height
10
- info, status = Open3.capture2("sips", "-g", "pixelWidth", "-g", "pixelHeight", source_file.to_s)
11
- raise "sips failed to read dimensions for #{source_file}" unless status.success?
12
- src_w = info[/pixelWidth: (\d+)/, 1]&.to_f
13
- src_h = info[/pixelHeight: (\d+)/, 1]&.to_f
14
- if src_w && src_h && (target_width.to_f / src_w) <= (target_height.to_f / src_h)
15
- args += ["--resampleWidth", target_width.to_i.to_s]
16
- else
17
- args += ["--resampleHeight", target_height.to_i.to_s]
15
+ src_w, src_h = sips_dimensions(source_file)
16
+ if cover
17
+ if src_w.nil? || src_h.nil?
18
+ args += ["--resampleWidth", target_width.to_i.to_s]
19
+ elsif target_width.to_f / src_w >= target_height.to_f / src_h
20
+ args += ["--resampleWidth", target_width.to_i.to_s]
21
+ else
22
+ args += ["--resampleHeight", target_height.to_i.to_s]
23
+ end
24
+ elsif src_w && src_h
25
+ if (target_width.to_f / src_w) <= (target_height.to_f / src_h)
26
+ args += ["--resampleWidth", target_width.to_i.to_s]
27
+ else
28
+ args += ["--resampleHeight", target_height.to_i.to_s]
29
+ end
18
30
  end
19
31
  else
20
32
  args += ["--resampleWidth", target_width.to_i.to_s] if target_width
@@ -22,20 +34,40 @@ class RackResize::Processors::Sips
22
34
  end
23
35
 
24
36
  if target_file
25
- _, status = Open3.capture2(*args, "-o", target_file.to_s, source_file.to_s)
26
- raise "sips failed (exit #{status.exitstatus}) for #{source_file}" unless status.success?
27
- return nil
37
+ run_sips(*args, "-o", target_file.to_s, source_file.to_s)
38
+ sips_crop!(target_file.to_s, target_width, target_height) if cover
39
+ nil
40
+ else
41
+ tmp = Tempfile.new(["result", File.extname(source_file)])
42
+ begin
43
+ run_sips(*args, "-o", tmp.path, source_file.to_s)
44
+ sips_crop!(tmp.path, target_width, target_height) if cover
45
+ File.binread(tmp.path)
46
+ ensure
47
+ tmp.close
48
+ tmp.unlink
49
+ end
28
50
  end
51
+ end
29
52
 
30
- tmp = Tempfile.new(["result", File.extname(source_file)])
31
- tmp_path = tmp.path || raise("tempfile has no path")
32
- begin
33
- _, status = Open3.capture2(*args, "-o", tmp_path, source_file.to_s)
34
- raise "sips failed (exit #{status.exitstatus}) for #{source_file}" unless status.success?
35
- File.binread(tmp_path)
36
- ensure
37
- tmp.close
38
- tmp.unlink
39
- end
53
+ private
54
+
55
+ def sips_dimensions(source_file)
56
+ info, status = Open3.capture2("sips", "-g", "pixelWidth", "-g", "pixelHeight", source_file.to_s)
57
+ raise "sips failed to read dimensions for #{source_file}" unless status.success?
58
+ [info[/pixelWidth: (\d+)/, 1]&.to_f, info[/pixelHeight: (\d+)/, 1]&.to_f]
59
+ end
60
+
61
+ def run_sips(*args)
62
+ _, status = Open3.capture2(*args)
63
+ raise "sips failed (exit #{status.exitstatus}): #{args.join(' ')}" unless status.success?
64
+ end
65
+
66
+ def sips_crop!(path, width, height)
67
+ run_sips("sips", "--cropToHeightWidth", height.to_i.to_s, width.to_i.to_s, path)
68
+ end
69
+
70
+ def sips_format(format)
71
+ format.to_s.downcase == 'jpg' ? 'jpeg' : format.to_s.downcase
40
72
  end
41
73
  end
@@ -6,13 +6,28 @@ end
6
6
 
7
7
  class RackResize::Processors::Vips
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 = ImageProcessing::Vips.source(source_file)
11
- image = image.resize_to_limit(target_width, target_height) if target_width || target_height
12
- 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
+ r, g, b, _a = bg_color
23
+ image = image.flatten(background: [r, g, b])
24
+ end
25
+
26
+ image = image.saver(quality: quality)
13
27
 
14
28
  if target_file
15
29
  image.call(destination: target_file)
30
+ nil
16
31
  else
17
32
  begin
18
33
  tmp_file = image.call
@@ -18,8 +18,9 @@ class RackResize::RackApp
18
18
  request = Rack::Request.new(env)
19
19
  fullpath = request.path_info
20
20
 
21
- RackResize::InputParsers::Cloudflare.parse_input(fullpath, cf_path_prefix: @cf_path_prefix) =>
22
- {route_matched:, req_params:, asset_path:}
21
+ result = RackResize::InputParsers::Cloudflare.parse_input(fullpath, cf_path_prefix: @cf_path_prefix)
22
+ result = RackResize::InputParsers::QueryString.parse_input(fullpath, request.query_string) unless result[:route_matched]
23
+ result => {route_matched:, req_params:, asset_path:}
23
24
 
24
25
  return @app.call(env) unless route_matched
25
26
  return error_resp("can't parse file path") unless asset_path
@@ -31,7 +32,7 @@ class RackResize::RackApp
31
32
  config.assets_folders.each do |prefix, folder|
32
33
  if asset_path.start_with?(prefix)
33
34
  asset_file = folder.join(asset_path.delete_prefix(prefix + (prefix.end_with?("/") ? "" : "/")))
34
- if asset_file.expand_path.to_s.start_with?(folder.to_s)
35
+ if asset_file.expand_path.to_s.start_with?(folder.expand_path.to_s)
35
36
  has_matched = true
36
37
  end
37
38
  break
@@ -47,20 +48,22 @@ class RackResize::RackApp
47
48
  return error_resp("invalid file path")
48
49
  end
49
50
  unless asset_file.exist?
50
- @processing.logger.info("RackResize::RackApp - File path fond #{asset_path} => #{asset_file}")
51
+ @processing.logger.info("RackResize::RackApp - File path not found #{asset_path} => #{asset_file}")
51
52
  return error_resp("file not exists on a server")
52
53
  end
53
54
 
54
55
  file_content = @processing.process!(source_file: asset_file, req_params:)
55
- return send_file(asset_file:, file_content:)
56
+ output_format = req_params[:format].then { |f| (f && f != 'auto') ? f : nil }
57
+ return send_file(asset_file:, file_content:, output_format:)
56
58
  end
57
59
 
58
60
  def error_resp(message, http_code: 404)
59
61
  [http_code, {}, [message]]
60
62
  end
61
63
 
62
- def send_file(asset_file:, file_content: nil)
63
- content_type = Rack::Mime.mime_type(File.extname(asset_file), "application/octet-stream")
64
+ def send_file(asset_file:, file_content: nil, output_format: nil)
65
+ ext = (output_format && output_format != 'auto') ? ".#{output_format}" : File.extname(asset_file)
66
+ content_type = Rack::Mime.mime_type(ext, "application/octet-stream")
64
67
 
65
68
  [
66
69
  200,
data/lib/rack_resize.rb CHANGED
@@ -2,17 +2,18 @@ module RackResize
2
2
  autoload :Configuration, "#{__dir__}/rack_resize/configuration"
3
3
  autoload :RackApp, "#{__dir__}/rack_resize/rack_app"
4
4
  autoload :Processing, "#{__dir__}/rack_resize/processing"
5
- # autoload :ImageController, "#{__dir__}/rack_resize/image_controller"
5
+ autoload :ColorUtils, "#{__dir__}/rack_resize/color_utils"
6
6
 
7
7
  module Processors
8
- autoload :Sips, "#{__dir__}/rack_resize/processors/sips"
9
- autoload :Vips, "#{__dir__}/rack_resize/processors/vips"
8
+ autoload :Sips, "#{__dir__}/rack_resize/processors/sips"
9
+ autoload :Vips, "#{__dir__}/rack_resize/processors/vips"
10
10
  autoload :MiniMagick, "#{__dir__}/rack_resize/processors/mini_magick"
11
- autoload :Imlib2, "#{__dir__}/rack_resize/processors/imlib2"
11
+ autoload :Imlib2, "#{__dir__}/rack_resize/processors/imlib2"
12
12
  end
13
13
 
14
14
  module InputParsers
15
- autoload :Cloudflare, "#{__dir__}/rack_resize/input_parsers/cloudflare"
15
+ autoload :Cloudflare, "#{__dir__}/rack_resize/input_parsers/cloudflare"
16
+ autoload :QueryString, "#{__dir__}/rack_resize/input_parsers/query_string"
16
17
  end
17
18
 
18
19
  class << self
data/rack_resize.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "rack_resize"
3
- s.version = "0.1.3"
3
+ s.version = "0.1.4"
4
4
  s.author = ["Pavel Evstigneev"]
5
5
  s.email = ["pavel.evst@gmail.com"]
6
6
  s.homepage = "https://github.com/paxa/rack_resize"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack_resize
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pavel Evstigneev
@@ -41,10 +41,14 @@ files:
41
41
  - Gemfile
42
42
  - README.md
43
43
  - Rakefile
44
+ - benchmark/http_benchmark.rb
45
+ - benchmark/processor_benchmark.rb
44
46
  - config.ru
45
47
  - lib/rack_resize.rb
48
+ - lib/rack_resize/color_utils.rb
46
49
  - lib/rack_resize/configuration.rb
47
50
  - lib/rack_resize/input_parsers/cloudflare.rb
51
+ - lib/rack_resize/input_parsers/query_string.rb
48
52
  - lib/rack_resize/processing.rb
49
53
  - lib/rack_resize/processors/imlib2.rb
50
54
  - lib/rack_resize/processors/mini_magick.rb
@@ -71,7 +75,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
71
75
  - !ruby/object:Gem::Version
72
76
  version: '0'
73
77
  requirements: []
74
- rubygems_version: 4.0.10
78
+ rubygems_version: 4.0.16
75
79
  specification_version: 4
76
80
  summary: Image resizing on a fly
77
81
  test_files: []