rack_resize 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 89ff24cb927e939e377fa9de0e4af8d736e026d2536a5721a94c8884c745de16
4
+ data.tar.gz: 13e4ca927f5ef1f058a528ded9158cd324e2eaf0e34c2797b21e1791028a31fc
5
+ SHA512:
6
+ metadata.gz: 58f951cd5354359a5a6261a739ded0e7136d346a91572c4bc8ba856530feb57644b1825959acc4ecdc79f91a15ab93e1e38c58d3578d82bd4c803a70ac015fdc
7
+ data.tar.gz: 462e57a07904d0e4c845b40611a2d7bc601e4c2fb207b8b7f2cccac9de934d6471bc222550e770ba19e0e58235d977d86b4ed8df0771761ac4b09a884495f106
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .idea/
2
+ Gemfile.lock
3
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'mini_magick'
6
+ gem 'ruby-vips'
7
+ gem 'rszr'
data/README.md ADDED
@@ -0,0 +1,6 @@
1
+
2
+ RESIZE IMAGE mini_magick samples/big.jpg - {:width=>"150"} - 0.949s
3
+ RESIZE IMAGE sips samples/big.jpg - {:width=>"150"} - 0.121s
4
+ RESIZE IMAGE vips samples/big.jpg - {:width=>"150"} - 0.116s
5
+
6
+ s.add_runtime_dependency "image_processing", ["> 1.0", "< 3.0"]
data/config.ru ADDED
@@ -0,0 +1,10 @@
1
+ require_relative "lib/rack_resize"
2
+ require "rack/static"
3
+
4
+ use RackResize::RackApp, processor: :imlib2, assets_folder: "samples" # save_resized: false,
5
+
6
+ use Rack::Static, urls: [""], root: "samples", index: "index.html"
7
+
8
+ run lambda { |env|
9
+ [200, {'Content-Type' => 'text/html'}, ['Hello from Rack!']]
10
+ }
@@ -0,0 +1,47 @@
1
+ require 'pathname'
2
+
3
+ class RackResize::Configuration
4
+ PROCESSORS = %i[sips vips mini_magick imlib2].freeze
5
+
6
+ attr_reader :processor
7
+ attr_accessor :save_resized, :default_quality, :cache_folder, :assets_folder
8
+
9
+ alias_method :save_resized?, :save_resized
10
+
11
+ def initialize(options = {})
12
+ @processor = options[:processor] || RUBY_PLATFORM.include?('darwin') ? :sips : :mini_magick
13
+ @save_resized = options.key?(:save_resized) ? options[:save_resized] : true
14
+ @default_quality = options[:default_quality] || 95
15
+ @cache_folder = options[:cache_folder]
16
+ @assets_folder = options[:assets_folder] ? Pathname.new(options[:assets_folder]) : nil
17
+
18
+ if defined?(Rails)
19
+ @cache_folder ||= Rails.root.join('tmp', 'rack_resize_cache')
20
+ @assets_folder ||= Rails.root.join('app', 'assets', 'images')
21
+ end
22
+
23
+ if options[:processor]
24
+ self.processor = options[:processor]
25
+ end
26
+ end
27
+
28
+ def processor=(value)
29
+ value = value.to_sym
30
+ unless PROCESSORS.include?(value)
31
+ raise ArgumentError, "Unknown processor #{value.inspect}. Must be one of: #{PROCESSORS.join(', ')}"
32
+ end
33
+ @processor = value
34
+ @processor_instance = nil
35
+ end
36
+
37
+ def processor_instance
38
+ @processor_instance ||= case @processor
39
+ when :mini_magick then RackResize::MiniMagick.new
40
+ when :vips then RackResize::Processors::Vips.new
41
+ when :sips then RackResize::Processors::Sips.new
42
+ when :imlib2 then RackResize::Processors::Imlib2.new
43
+ else
44
+ raise "RackResize - Unknow image processor #{@processor.inspect}"
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Cloudflare
4
+ end
@@ -0,0 +1,49 @@
1
+ class RackResize::Processing
2
+ attr_reader :config
3
+
4
+ def initialize(config:)
5
+ @config = config
6
+ end
7
+
8
+ def process!(source_file:, req_params:)
9
+ if config.save_resized?
10
+ tmp_file_name = config.cache_folder.join(Digest::MD5.hexdigest(source_file.to_s) + File.extname(source_file))
11
+ FileUtils.mkdir_p(tmp_file_name.dirname)
12
+
13
+ unless tmp_file_name.exist?
14
+ process_file(req_params:, source_file:, target_file: tmp_file_name.to_s)
15
+ end
16
+
17
+ logger.info("Serving cached file #{tmp_file_name}")
18
+ return StringIO.new(File.open(tmp_file_name.to_s, "rb", &:read))
19
+ else
20
+ file_content = process_file(req_params:, source_file:, target_file: nil)
21
+ return StringIO.new(file_content)
22
+ end
23
+ end
24
+
25
+ def process_file(source_file:, req_params:, target_file: nil)
26
+ dpr = req_params[:dpr]&.to_f || 1.0
27
+ target_width = (req_params[:w]&.to_i || req_params[:width]&.to_i)&.*(dpr)
28
+ target_height = (req_params[:h]&.to_i || req_params[:height]&.to_i)&.*(dpr)
29
+
30
+ start_time = Time.now
31
+ begin
32
+ return config.processor_instance.resize(source_file:, target_file:, target_width:, target_height:)
33
+ ensure
34
+ processing_time = (Time.now.to_f - start_time.to_f).round(3)
35
+ logger.info("RESIZE IMAGE #{config.processor} #{source_file} - #{req_params} - #{processing_time}s")
36
+ end
37
+ end
38
+
39
+ def logger
40
+ @logger ||= begin
41
+ if defined?(Rails) && Rails.logger
42
+ Rails.logger
43
+ else
44
+ require 'logger'
45
+ Logger.new(STDOUT)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,20 @@
1
+ begin
2
+ require "rszr"
3
+ rescue LoadError
4
+ raise LoadError, "RackResize::Processors::Imlib2 requires the rszr gem. Please add `gem \"rszr\"` to your Gemfile."
5
+ end
6
+
7
+ class RackResize::Processors::Imlib2
8
+
9
+ def resize(source_file:, target_file:, target_width:, target_height:)
10
+ image = Rszr::Image.load(source_file)
11
+ image.resize!(target_width || :auto, target_height || :auto) if target_width || target_height
12
+
13
+ if target_file
14
+ image.call(destination: target_file)
15
+ image.save(target_file)
16
+ else
17
+ image.save_data(format: source_file.to_s =~ /\.png$/ ? :png : :jpeg)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,53 @@
1
+ begin
2
+ require "image_processing"
3
+ rescue LoadError
4
+ raise LoadError, "RackResize::Processors::MiniMagick requires the image_processing gem. Please add `gem \"image_processing\"` to your Gemfile."
5
+ end
6
+
7
+ # require 'mini_magick'
8
+ # MiniMagick.logger.level = :debug
9
+
10
+ class RackResize::Processors::MiniMagick
11
+
12
+ def resize(source_file:, target_width:, target_height:, target_file: nil)
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)
16
+
17
+ if target_file
18
+ image.call(destination: target_file)
19
+ else
20
+ begin
21
+ tmp_file = image.call
22
+ return tmp_file.read
23
+ ensure
24
+ tmp_file.unlink
25
+ end
26
+ end
27
+ 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
+ end
48
+
49
+ # magick /Users/pavel/Work/resume-io/app/assets/images/templates/vancouver.jpg -auto-orient -resize 426.0x> -quality 95 /Users/pavel/Work/resume-io/tmp/cdn_cgi_cache/60f2727cce350874040b321219f05766.jpg
50
+
51
+
52
+ # magick mogrify -resize 426.0x> /var/folders/n9/b__lxh7j1_xcwg4k_8j2_39m0000gp/T/mini_magick20260709-58478-qowydk.jpg
53
+ # magick mogrify -quality 95 /var/folders/n9/b__lxh7j1_xcwg4k_8j2_39m0000gp/T/mini_magick20260709-58478-qowydk.jpg
@@ -0,0 +1,37 @@
1
+ require "shellwords"
2
+
3
+ class RackResize::Processors::Sips
4
+
5
+ def resize(source_file:, target_width:, target_height:, target_file: nil)
6
+ # args = ["sips", "--deleteColorManagementProperties", "--optimizeColorForSharing"]
7
+ args = ["sips", "--deleteColorManagementProperties", "--debug"]
8
+ args += ["-s formatOptions", RackResize.config.default_quality]
9
+ args << "--resampleWidth" << target_width.to_i if target_width
10
+ args << "--resampleHeight" << target_height.to_i if target_height
11
+
12
+ if target_file
13
+ args << "-o" << Shellwords.escape(target_file)
14
+ else
15
+ tmp_file = Tempfile.new(["result", File.extname(source_file)])
16
+ args << "-o" << tmp_file.path
17
+ end
18
+
19
+ args << Shellwords.escape(source_file)
20
+
21
+ pp [:args, args, args.join(" ")]
22
+
23
+ result = `#{args.join(" ")}`
24
+
25
+ puts "sips command result: #{result}"
26
+
27
+ unless target_file
28
+ begin
29
+ return File.open(tmp_file.path, 'rb', &:read)
30
+ ensure
31
+ tmp_file.unlink
32
+ end
33
+ end
34
+
35
+ nil
36
+ end
37
+ end
@@ -0,0 +1,27 @@
1
+ begin
2
+ require "image_processing"
3
+ rescue LoadError
4
+ raise LoadError, "RackResize::Processors::Vips requires the image_processing gem. Please add `gem \"image_processing\"` to your Gemfile."
5
+ end
6
+
7
+ class RackResize::Processors::Vips
8
+
9
+ def resize(source_file:, target_file:, target_width:, target_height:)
10
+ 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)
13
+
14
+ # image.call(destination: target_file)
15
+
16
+ if target_file
17
+ image.call(destination: target_file)
18
+ else
19
+ begin
20
+ tmp_file = image.call
21
+ return tmp_file.read
22
+ ensure
23
+ tmp_file.unlink
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+
5
+ class RackResize::RackApp
6
+
7
+ attr_reader :config
8
+
9
+ def initialize(*args, **options)
10
+ if args.first
11
+ @app = args.first
12
+ end
13
+
14
+ @config = options == {} ? RackResize.config : RackResize::Configuration.new(options)
15
+ @processing = RackResize::Processing.new(config: @config)
16
+ @path_prefix = options[:path_prefix] || "/cdn-cgi/image"
17
+ end
18
+
19
+ def call(env)
20
+ # process string like this
21
+ # /cdn-cgi/image/width=426,format=auto/assets/templates/vancouver-27c47f55.jpg
22
+
23
+ request = Rack::Request.new(env)
24
+ fullpath = request.path_info
25
+
26
+ unless fullpath.start_with?(@path_prefix)
27
+ return @app.call(env)
28
+ end
29
+
30
+ fullpath = fullpath.delete_prefix("/cdn-cgi").delete_prefix("/image").delete_prefix("/")
31
+ file_path_match = fullpath.match(%r{(?<params>[^\/]+)(?<file>\/.+?)(-[\da-f]{8})?(?<ext>\.\w{2,})$})
32
+
33
+ return error_resp("can't parse file path") unless file_path_match
34
+
35
+ req_params = file_path_match[:params].split(",").map {|s| s.split("=") }.to_h.transform_keys(&:to_sym)
36
+ asset_path = "#{file_path_match[:file]}#{file_path_match[:ext]}"
37
+
38
+ if defined?(Rails)
39
+ asset_path = asset_path.delete_prefix("/assets/")
40
+ else
41
+ asset_path = asset_path.delete_prefix("/#{config.assets_folder}/")
42
+ end
43
+
44
+ asset_file = config.assets_folder.join(asset_path.sub(%r{^/assets/}, ''))
45
+
46
+ return error_resp(".. is not allowed in image path") if asset_path.include?("..")
47
+
48
+ return error_resp("invalid file path") unless asset_file.to_s.start_with?(config.assets_folder.to_s)
49
+ return error_resp("file not exists on a server") unless asset_file.exist?
50
+
51
+ file_content = @processing.process!(source_file: asset_file, req_params:)
52
+ return send_file(asset_file:, file_content:)
53
+ end
54
+
55
+ def error_resp(message, http_code: 404)
56
+ [http_code, {}, [message]]
57
+ end
58
+
59
+ def send_file(asset_file:, file_content: nil)
60
+ content_type = Rack::Mime.mime_type(File.extname(asset_file), "application/octet-stream")
61
+ # file = file_path ? File.open(file_path, "rb") : StringIO.new(file_content)
62
+
63
+ [
64
+ 200,
65
+ {
66
+ "content-type" => content_type,
67
+ "content-length" => file_content.size.to_s,
68
+ "content-disposition" => "inline",
69
+ "cache-control" => "max-age=86400" # 1 day
70
+ },
71
+ file_content
72
+ ]
73
+ end
74
+ end
@@ -0,0 +1,10 @@
1
+ require "rack_resize"
2
+ require "rails"
3
+
4
+ module MyGem
5
+ class Railtie < Rails::Railtie
6
+ initializer "rack_resize.auto_register_itself" do |app|
7
+ app.config.middleware.insert_before 0, RackResize::RackApp
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,27 @@
1
+ module RackResize
2
+ autoload :Configuration, "#{__dir__}/rack_resize/configuration"
3
+ autoload :RackApp, "#{__dir__}/rack_resize/rack_app"
4
+ autoload :Processing, "#{__dir__}/rack_resize/processing"
5
+ # autoload :ImageController, "#{__dir__}/rack_resize/image_controller"
6
+
7
+ module Processors
8
+ autoload :Sips, "#{__dir__}/rack_resize/processors/sips"
9
+ autoload :Vips, "#{__dir__}/rack_resize/processors/vips"
10
+ autoload :MiniMagic, "#{__dir__}/rack_resize/processors/mini_magic"
11
+ autoload :Imlib2, "#{__dir__}/rack_resize/processors/imlib2"
12
+ end
13
+
14
+ class << self
15
+ def configuration
16
+ @configuration ||= Configuration.new
17
+ end
18
+
19
+ def config
20
+ configuration
21
+ end
22
+
23
+ def configure
24
+ yield configuration
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "rack_resize"
3
+ s.version = "0.1.0"
4
+ s.author = ["Pavel Evstigneev"]
5
+ s.email = ["pavel.evst@gmail.com"]
6
+ s.homepage = "https://github.com/paxa/rack_resize"
7
+ s.summary = %q{Image resizing on a fly}
8
+ s.license = 'MIT'
9
+ s.required_ruby_version = ['>= 3.0']
10
+
11
+ s.files = `git ls-files`.split("\n")
12
+ s.test_files = []
13
+
14
+ s.require_paths = ["lib"]
15
+
16
+ s.add_runtime_dependency "rack", ["> 2.0", "< 4.0"]
17
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack_resize
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Pavel Evstigneev
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-07-15 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rack
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '4.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">"
27
+ - !ruby/object:Gem::Version
28
+ version: '2.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '4.0'
32
+ email:
33
+ - pavel.evst@gmail.com
34
+ executables: []
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - ".gitignore"
39
+ - Gemfile
40
+ - README.md
41
+ - config.ru
42
+ - lib/rack_resize.rb
43
+ - lib/rack_resize/configuration.rb
44
+ - lib/rack_resize/input_parsers/cloudflare.rb
45
+ - lib/rack_resize/processing.rb
46
+ - lib/rack_resize/processors/imlib2.rb
47
+ - lib/rack_resize/processors/mini_magick.rb
48
+ - lib/rack_resize/processors/sips.rb
49
+ - lib/rack_resize/processors/vips.rb
50
+ - lib/rack_resize/rack_app.rb
51
+ - lib/rack_resize/rails_autoload.rb
52
+ - rack_resize.gemspec
53
+ homepage: https://github.com/paxa/rack_resize
54
+ licenses:
55
+ - MIT
56
+ metadata: {}
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '3.0'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubygems_version: 3.6.5
72
+ specification_version: 4
73
+ summary: Image resizing on a fly
74
+ test_files: []