rack_resize 0.1.4 → 0.1.7

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: aaecfcc4b1f54da6b128ae9fe7939db1676ab6069a0c15bbedaab5545c74a413
4
- data.tar.gz: f7c136233b7a6bbed40c40fca10399139f30ab3393857851690f05593d2b44b4
3
+ metadata.gz: 78c20cbac58c064a90e81fd8a88114da73f2e69f4ba56ded773fa47bf582349a
4
+ data.tar.gz: ab003436d413adfac50b26f80732e9c0e97fae5b4a7a3439f4f9ee8e9964d8d3
5
5
  SHA512:
6
- metadata.gz: 66afe23035cedbf3b32723375b16c75351e1b9d2c6907fa7989b80452181d102b76c719cea27261587e67331739ece84c68c62682791769d4a2c053c36e7d404
7
- data.tar.gz: 0b6a7229f59709ca20b55ebeb0efd17cbf9ef8269410a1ae25a6479edb43e90a35294a5930c9d748aba4e5bc8f1bafb313e5a35fc3fd9dd2ce51544ba4a71bef
6
+ metadata.gz: bc78d25f2d53166bed8c5dcf78367feb7056107c80dbffd700962b99b0472cf1009e04326c242570b96e626252711c818b1d5973a0e59e7f58092bcbdfc5e612
7
+ data.tar.gz: e37e06c0dfcdd96de655e6822594f63c2693764b66bac9b9c6b9fa185213f4b1352e08441a34c0c5883c2d559b0eae1821f0ac9a6ccfd4dc0ae4a6d74ccd9432
data/README.md CHANGED
@@ -27,18 +27,23 @@ Fastly / bunny.net format (query string params):
27
27
  ```
28
28
  /assets/pets/dog.jpg?width=300&height=200&fit=cover&format=webp&quality=85
29
29
  ```
30
+ Imgproxy format:
31
+ ```
32
+ /insecure/rs:fill:400:300/q:85/f:webp/plain/local:///assets/pets/dog.jpg
33
+ /insecure/rs:fit:200:150/plain/local:///assets/banner.jpg@webp
34
+ ```
30
35
 
31
36
  ### Supported Parameters:
32
37
 
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. |
38
+ | Parameter | Shortcut | Imgproxy Option | Type | Description |
39
+ |------------|----------|-----------------|------|-------------|
40
+ | `width` | `w` | `w:<val>` or in `rs:<mode>:<w>:<h>` | integer | Target width in pixels. Scales proportionally if `height` is omitted. |
41
+ | `height` | `h` | `h:<val>` or in `rs:<mode>:<w>:<h>` | integer | Target height in pixels. Scales proportionally if `width` is omitted. |
42
+ | `fit` | — | `rs:fill` (cover), `rs:fit` (contain) | string | Resize mode: `contain` (default), `cover`, `crop`. See below. |
43
+ | `format` | `f` | `f:<fmt>` or `@<fmt>` suffix | string | Output format: `jpeg`, `png`, `webp`, `avif`, `gif`. Use `auto` to keep original. |
44
+ | `quality` | `q` | `q:<val>` | integer (1–100) | Compression quality. Defaults to `default_quality` config value (95). |
45
+ | `dpr` | — | `dpr:<val>` | float (0.1–10) | Device pixel ratio. Multiplies `width` and `height` before processing. |
46
+ | `bg-color` | `bg` | `bg:<color>` | color | Background color for flattening transparency. Also aliased as `background`. See formats below. |
42
47
 
43
48
  **Background color formats:**
44
49
  - CSS named color: `white`, `red`, `cornflowerblue` (all 148 CSS colors supported)
@@ -51,8 +56,8 @@ Fastly / bunny.net format (query string params):
51
56
  Supported by: `vips`, `mini_magick`. Accepted but ignored by `sips` and `imlib2`.
52
57
 
53
58
  **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
59
+ - `contain` (`rs:fit`) — resizes to fit within the given box, preserving aspect ratio (default)
60
+ - `cover` / `crop` (`rs:fill`) — resizes and center-crops to fill the exact box
56
61
 
57
62
  **Example URLs (query string format):**
58
63
  ```
@@ -89,12 +94,36 @@ Supported by: `vips`, `mini_magick`. Accepted but ignored by `sips` and `imlib2`
89
94
  /cdn-cgi/image/width=400,format=avif,quality=80/samples/image_1.jpeg
90
95
  ```
91
96
 
97
+ **Example URLs (Imgproxy format):**
98
+ ```
99
+ # Resize to width and height with cover crop (rs:fill)
100
+ /insecure/rs:fill:400:300/plain/local:///samples/image_1.jpeg
101
+
102
+ # Resize to fit within 400×300 box (rs:fit)
103
+ /insecure/rs:fit:400:300/plain/local:///samples/image_1.jpeg
104
+
105
+ # Convert format and adjust quality
106
+ /insecure/rs:fill:400:300/q:85/f:webp/plain/local:///samples/image_1.jpeg
107
+
108
+ # Specify format via extension suffix (@webp)
109
+ /insecure/rs:fit:200:150/plain/samples/banner.jpg@webp
110
+
111
+ # Using separate width, height, and DPR
112
+ /insecure/w:400/h:300/dpr:2/plain/samples/image_1.jpeg
113
+
114
+ # Flatten transparency with background color
115
+ /insecure/rs:fill:200:200/bg:white/plain/samples/logo.png
116
+
117
+ # Rails fingerprint stripping (fingerprinted URL automatically resolves to original file)
118
+ /insecure/w:200/plain/assets/photo-1a2b3c4d.jpg
119
+ ```
120
+
92
121
  ### Configuration:
93
122
 
94
123
  ```ruby
95
124
  RackResize.configure do |config|
96
125
  config.assets_folders = { assets: Rails.root.join('app', 'assets', 'images') }
97
- config.processor = :sips / :vips / :mini_magick / :imlib2
126
+ config.processor = :sips / :vips / :mini_magick / :imlib2 / :bun_image
98
127
  config.default_quality = 95
99
128
  config.save_resized = false
100
129
  config.cache_folder = Rails.root.join('tmp', 'rack_resize_cache') # used if save_resized enabled
@@ -102,6 +131,21 @@ RackResize.configure do |config|
102
131
  end
103
132
  ```
104
133
 
134
+ ### Development vs. Production Architecture:
135
+
136
+ `rack_resize` lets you write standard CDN / imgproxy URLs in your application code without needing external services running in development:
137
+
138
+ ```
139
+ [ Local Development ]
140
+ Browser Request --> Rails / Rack App --> RackResize Middleware --> Ruby Processor (:vips, :mini_magick, etc.)
141
+
142
+ [ Production ]
143
+ Browser Request --> Nginx / CDN Cache --> imgproxy (Go service) --> Original Image Storage (S3 / disk)
144
+ ```
145
+
146
+ - **In Development**: Include `rack_resize` in your `:development` Gemfile group. It intercepts `/insecure/...`, `/cdn-cgi/image/...`, or query string parameters, processes images on the fly in Ruby, and serves them immediately.
147
+ - **In Production**: Route image paths directly at your reverse proxy (e.g. Nginx with `proxy_cache`) to your production `imgproxy` container or CDN edge, keeping your web application servers focused on dynamic application traffic.
148
+
105
149
  ### Supported Processing Backends:
106
150
 
107
151
  <table>
@@ -167,19 +211,31 @@ gem "rszr"
167
211
 
168
212
  </td>
169
213
  </tr>
214
+ <tr>
215
+ <td>bun_image</td>
216
+ <td><code>processor: :bun_image</code></td>
217
+ <td>
218
+
219
+ Bun ≥ 1.3.14 — `brew install oven-sh/bun/bun`
220
+
221
+ </td>
222
+ <td>none (shells out to <code>bun</code>)</td>
223
+ </tr>
170
224
  </table>
171
225
 
226
+ Note: <code>fit=cover</code>/<code>fit=crop</code> falls back to stretch (fill) with <code>bun_image</code> — Bun.Image has no crop yet.
227
+
172
228
  ### Supported Image Formats:
173
229
 
174
- | Format | sips | mini_magick | vips | imlib2 |
175
- |--------|:----:|:-----------:|:----:|:------:|
176
- | JPEG | ✅ | ✅ | ✅ | ✅ |
177
- | PNG | ✅ | ✅ | ✅ | ✅ |
178
- | GIF | ✅ | ✅ | ✅ | ✅ |
179
- | WebP | ❌ | ✅ ¹ | ✅ ¹ | ❌ |
180
- | AVIF | ✅ ² | ✅ ³ | ✅ ⁴ | ❌ |
181
- | HEIC | ✅ | ✅ ³ | ✅ ⁴ | ❌ |
182
- | SVG | ❌ | ✅ ⁵ | ✅ ⁶ | ❌ |
230
+ | Format | sips | mini_magick | vips | imlib2 | bun_image |
231
+ |--------|:----:|:-----------:|:----:|:------:|:---------:|
232
+ | JPEG | ✅ | ✅ | ✅ | ✅ | ✅ |
233
+ | PNG | ✅ | ✅ | ✅ | ✅ | ✅ |
234
+ | GIF | ✅ | ✅ | ✅ | ✅ | ❌ |
235
+ | WebP | ❌ | ✅ ¹ | ✅ ¹ | ❌ | ✅ |
236
+ | AVIF | ✅ ² | ✅ ³ | ✅ ⁴ | ❌ | ✅ ⁷ |
237
+ | HEIC | ✅ | ✅ ³ | ✅ ⁴ | ❌ | ✅ ⁷ |
238
+ | SVG | ❌ | ✅ ⁵ | ✅ ⁶ | ❌ | ❌ |
183
239
 
184
240
  ¹ Requires ImageMagick / libvips built with **libwebp** support (`brew install webp`)
185
241
  ² macOS 13 (Ventura) or later
@@ -187,6 +243,7 @@ gem "rszr"
187
243
  ⁴ Requires libvips built with **libheif** support (`brew install libheif`)
188
244
  ⁵ Requires **Inkscape** or **librsvg** (`brew install librsvg`)
189
245
  ⁶ Requires libvips built with **librsvg** support (`brew install librsvg`)
246
+ ⁷ AVIF/HEIC encode is macOS/Windows only in Bun.Image
190
247
 
191
248
  ### Performance Benchmarks:
192
249
 
@@ -28,7 +28,7 @@ NULL_LOGGER = Logger.new(IO::NULL)
28
28
  available_apps = {}
29
29
 
30
30
  puts "Loading processors..."
31
- %i[sips vips mini_magick imlib2].each do |name|
31
+ %i[sips vips mini_magick imlib2 bun_image].each do |name|
32
32
  begin
33
33
  app = RackResize::RackApp.new(
34
34
  processor: name,
@@ -24,6 +24,7 @@ PROCESSOR_CLASSES = {
24
24
  vips: RackResize::Processors::Vips,
25
25
  mini_magick: RackResize::Processors::MiniMagick,
26
26
  imlib2: RackResize::Processors::Imlib2,
27
+ bun_image: RackResize::Processors::BunImage,
27
28
  }.freeze
28
29
 
29
30
  puts "Loading processors..."
@@ -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,6 +63,7 @@ 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
68
  raise "RackResize - Unknown image processor #{@processor.inspect}"
68
69
  end
@@ -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
@@ -8,11 +8,15 @@ module RackResize::InputParsers::QueryString
8
8
  #
9
9
  # Shortcuts: f=format, q=quality
10
10
 
11
+ IMAGE_EXTENSIONS = %w[.jpg .jpeg .png .webp .gif .avif .svg .heic].freeze
11
12
  CANONICAL_PARAMS = %w[width height quality dpr format fit bg-color background].freeze
12
13
  PARAM_ALIASES = { 'f' => :format, 'q' => :quality, 'h' => :height, 'w' => :width, 'bg' => :'bg-color' }.freeze
13
14
  RESIZE_PARAMS = (CANONICAL_PARAMS + PARAM_ALIASES.keys).freeze
14
15
 
15
16
  def parse_input(fullpath, query_string)
17
+ ext = File.extname(fullpath.to_s.split('?').first.to_s).downcase
18
+ return { route_matched: false, req_params: nil, asset_path: nil } unless IMAGE_EXTENSIONS.include?(ext)
19
+
16
20
  params = Rack::Utils.parse_query(query_string.to_s)
17
21
 
18
22
  unless params.keys.any? { |k| RESIZE_PARAMS.include?(k) }
@@ -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
@@ -20,6 +20,7 @@ class RackResize::RackApp
20
20
 
21
21
  result = RackResize::InputParsers::Cloudflare.parse_input(fullpath, cf_path_prefix: @cf_path_prefix)
22
22
  result = RackResize::InputParsers::QueryString.parse_input(fullpath, request.query_string) unless result[:route_matched]
23
+ result = RackResize::InputParsers::Imgproxy.parse_input(fullpath) unless result[:route_matched]
23
24
  result => {route_matched:, req_params:, asset_path:}
24
25
 
25
26
  return @app.call(env) unless route_matched
data/lib/rack_resize.rb CHANGED
@@ -9,11 +9,13 @@ module RackResize
9
9
  autoload :Vips, "#{__dir__}/rack_resize/processors/vips"
10
10
  autoload :MiniMagick, "#{__dir__}/rack_resize/processors/mini_magick"
11
11
  autoload :Imlib2, "#{__dir__}/rack_resize/processors/imlib2"
12
+ autoload :BunImage, "#{__dir__}/rack_resize/processors/bun_image"
12
13
  end
13
14
 
14
15
  module InputParsers
15
16
  autoload :Cloudflare, "#{__dir__}/rack_resize/input_parsers/cloudflare"
16
17
  autoload :QueryString, "#{__dir__}/rack_resize/input_parsers/query_string"
18
+ autoload :Imgproxy, "#{__dir__}/rack_resize/input_parsers/imgproxy"
17
19
  end
18
20
 
19
21
  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.4"
3
+ s.version = "0.1.7"
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.4
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pavel Evstigneev
@@ -48,8 +48,11 @@ files:
48
48
  - lib/rack_resize/color_utils.rb
49
49
  - lib/rack_resize/configuration.rb
50
50
  - lib/rack_resize/input_parsers/cloudflare.rb
51
+ - lib/rack_resize/input_parsers/imgproxy.rb
51
52
  - lib/rack_resize/input_parsers/query_string.rb
52
53
  - lib/rack_resize/processing.rb
54
+ - lib/rack_resize/processors/bun_image.js
55
+ - lib/rack_resize/processors/bun_image.rb
53
56
  - lib/rack_resize/processors/imlib2.rb
54
57
  - lib/rack_resize/processors/mini_magick.rb
55
58
  - lib/rack_resize/processors/sips.rb