photo-cook 1.1.2 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +16 -0
  3. data/Gemfile +3 -13
  4. data/README.md +2 -2
  5. data/Rakefile +2 -15
  6. data/app/assets/javascripts/photo-cook/photo-cook.coffee +90 -0
  7. data/app/helpers/photo_cook_helper.rb +14 -0
  8. data/lib/photo-cook/device-pixel-ratio-spy.rb +17 -0
  9. data/lib/photo-cook/device-pixel-ratio.rb +67 -0
  10. data/lib/photo-cook/engine.rb +15 -2
  11. data/lib/photo-cook/events.rb +15 -0
  12. data/lib/photo-cook/logger.rb +37 -0
  13. data/lib/photo-cook/optimization/__api__.rb +21 -0
  14. data/lib/photo-cook/optimization/carrierwave.rb +10 -0
  15. data/lib/photo-cook/optimization/image-optim.rb +131 -0
  16. data/lib/photo-cook/optimization/job.rb +12 -0
  17. data/lib/photo-cook/optimization/logging.rb +21 -0
  18. data/lib/photo-cook/pixels.rb +56 -0
  19. data/lib/photo-cook/resize/__api__.rb +93 -0
  20. data/lib/photo-cook/resize/assemble.rb +125 -0
  21. data/lib/photo-cook/resize/calculations.rb +63 -0
  22. data/lib/photo-cook/resize/carrierwave.rb +22 -0
  23. data/lib/photo-cook/resize/command.rb +28 -0
  24. data/lib/photo-cook/resize/logging.rb +25 -0
  25. data/lib/photo-cook/resize/magick-photo.rb +34 -0
  26. data/lib/photo-cook/resize/middleware.rb +108 -0
  27. data/lib/photo-cook/resize/mode.rb +36 -0
  28. data/lib/photo-cook/resize/resizer.rb +88 -0
  29. data/lib/photo-cook/utils.rb +71 -0
  30. data/lib/photo-cook/version.rb +3 -2
  31. data/lib/photo-cook.rb +32 -23
  32. data/photo-cook.gemspec +14 -8
  33. data/srcset.rb +34 -0
  34. data/test/fixtures/dog.png +0 -0
  35. data/test/helper.rb +12 -0
  36. data/test/test-photo-cook-assemble.rb +23 -0
  37. data/test/test-photo-cook-calculations.rb +45 -0
  38. data/test/test-photo-cook-resize.rb +62 -0
  39. metadata +86 -25
  40. data/app/assets/javascripts/photo-cook/photo-cook.js.erb +0 -85
  41. data/lib/photo-cook/abstract-optimizer.rb +0 -9
  42. data/lib/photo-cook/assemble.rb +0 -97
  43. data/lib/photo-cook/carrierwave.rb +0 -15
  44. data/lib/photo-cook/command.rb +0 -49
  45. data/lib/photo-cook/dimensions.rb +0 -36
  46. data/lib/photo-cook/dirs.rb +0 -8
  47. data/lib/photo-cook/image-optim.rb +0 -24
  48. data/lib/photo-cook/logging.rb +0 -85
  49. data/lib/photo-cook/magick-photo.rb +0 -6
  50. data/lib/photo-cook/middleware.rb +0 -139
  51. data/lib/photo-cook/optimization-api.rb +0 -20
  52. data/lib/photo-cook/optimization-job.rb +0 -9
  53. data/lib/photo-cook/pixel-ratio-spy.rb +0 -19
  54. data/lib/photo-cook/pixel-ratio.rb +0 -36
  55. data/lib/photo-cook/resize-api.rb +0 -94
  56. data/lib/photo-cook/resizer.rb +0 -111
  57. data/lib/photo-cook/size-formatting.rb +0 -13
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+ module PhotoCook
3
+ module Resize
4
+ class << self
5
+ attr_accessor :cache_dir
6
+ attr_accessor :multiplier
7
+ end
8
+ self.cache_dir = 'resize-cache'
9
+ self.multiplier = DevicePixelRatio::DEFAULT
10
+
11
+ class << self
12
+ # Performs photo resizing with ImageMagick:
13
+ # perform_resize('/application/public/uploads/car.png', '/application/public/resized_car.png', 280, 280)
14
+ #
15
+ # Source file: /application/public/uploads/car.png
16
+ # Result file: /application/public/resized_car.png
17
+ #
18
+ # NOTE: This method will perform validation
19
+ # NOTE: This method knows anything about resize cache
20
+ def perform(source_path, store_path, w, h, mode)
21
+ w, h, mode = parse_options(w, h, mode, multiplier: 1.0)
22
+ photo, msec = PhotoCook::Utils.measure do
23
+ resizer.resize(source_path, store_path, w, h, mode)
24
+ end
25
+ PhotoCook.notify('resize', photo, msec)
26
+ photo
27
+ end
28
+
29
+ # Builds URI which points to PhotoCook::Middleware:
30
+ # uri('/uploads/car.png', 280, 280)
31
+ # => /uploads/resize-cache/fit-280x280/car.png
32
+ #
33
+ # NOTE: This method will perform validation
34
+ def uri(uri, width, height, mode, options = {})
35
+ Assemble.assemble_resize_uri(uri, *parse_options(width, height, mode, options))
36
+ end
37
+
38
+ # Inverse of PhotoCook#resize (see ^):
39
+ # strip('/uploads/resize-cache/fit-280x280/car.png')
40
+ # => /uploads/car.png
41
+ #
42
+ # NOTE: This method will perform validation
43
+ def strip(uri, check = false)
44
+ # TODO Implement check
45
+ Assemble.disassemble_resize_uri(uri)
46
+ end
47
+
48
+ # TODO Change uri to source_path
49
+ def base64_uri(uri, width, height, mode, options = {})
50
+ w, h, m = parse_options(width, height, mode, options)
51
+ command = Command.assemble(w, h, m)
52
+ source_path = Assemble.assemble_source_path_from_normal_uri(PhotoCook.root_path, uri)
53
+ store_path = Assemble.assemble_store_path(PhotoCook.root_path, source_path, command)
54
+ photo = if File.readable?(store_path)
55
+ MagickPhoto.new(store_path)
56
+ else
57
+ Resize.perform(source_path, store_path, w, h, m)
58
+ end
59
+
60
+ "data:#{photo.mime_type};base64,#{Base64.strict_encode64(File.read(photo.path))}"
61
+ end
62
+
63
+ def base64_uri_from_source_path(source_path, store_path, width, height, mode, options = {})
64
+ w, h, m = parse_options(width, height, mode, options)
65
+ photo = if File.readable?(store_path)
66
+ MagickPhoto.new(store_path)
67
+ else
68
+ Resize.perform(source_path, store_path, w, h, m)
69
+ end
70
+
71
+ "data:#{photo.mime_type};base64,#{Base64.strict_encode64(File.read(photo.path))}"
72
+ end
73
+
74
+ # TODO Think about it. This can be very cool feature of PhotoCook
75
+ if defined?(Rails)
76
+ def static_asset_uri(uri, *rest)
77
+ # If production assets are precompiled and placed into public so we can resize as usually
78
+ Rails.application.config.serve_static_files ? uri : uri(uri, *rest)
79
+ end
80
+ end
81
+
82
+ def parse_options(width, height, mode, options)
83
+ multiplier = options.fetch(:multiplier, self.multiplier).to_f
84
+ mode = Mode.parse!(mode)
85
+ width = Pixels.round(Pixels.parse(width) * multiplier)
86
+ height = Pixels.round(Pixels.parse(height) * multiplier)
87
+ Pixels.check!(width)
88
+ Pixels.check!(height)
89
+ [width, height, mode]
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+ module PhotoCook
3
+ module Resize
4
+ module Assemble
5
+ class << self
6
+ # Returns URI which points to PhotoCook::Resize::Middleware
7
+ #
8
+ # Arguments:
9
+ # source_uri => /uploads/photos/1/car.png
10
+ # width => :auto
11
+ # height => 640
12
+ # mode => fit
13
+ #
14
+ # Returns /uploads/photos/1/resized/width=auto&height=640&mode=fit/car.png
15
+ #
16
+ # NOTE: This method performs no validation
17
+ # NOTE: This method is very hot
18
+ def assemble_resize_uri(source_uri, width, height, mode)
19
+ source_uri.split('/').insert(-2, PhotoCook::Resize.cache_dir, Command.assemble(width, height, mode)).join('/')
20
+ end
21
+
22
+ # Strips resize command from URI. Inverse of +assemble_resize_uri+
23
+ #
24
+ # Arguments:
25
+ # resize_uri => /uploads/photos/1/resized/width=auto&height=640&mode=fit/car.png
26
+ #
27
+ # Returns /uploads/photos/1/car.png
28
+ #
29
+ # NOTE: This method performs no validation
30
+ def disassemble_resize_uri(resize_uri)
31
+ # Take URI:
32
+ # /uploads/photos/1/resized/width=auto&height=640&mode=fit/car.png
33
+ #
34
+ # Split by separator:
35
+ # ["", "uploads", "photos", "1", "resized", "width=auto&height=640&mode=fit", "car.png"]
36
+ #
37
+ sections = resize_uri.split('/')
38
+
39
+ # Delete PhotoCook directory:
40
+ # ["", "uploads", "photos", "1", "width=auto&height=640&mode=fit", "car.png"]
41
+ sections.delete_at(-3)
42
+
43
+ # Delete command string:
44
+ # ["", "uploads", "photos", "1", "car.png"]
45
+ sections.delete_at(-2)
46
+
47
+ sections.join('/')
48
+ end
49
+
50
+ # Path where source photo is stored
51
+ #
52
+ # Arguments:
53
+ # root => /application
54
+ # resize_uri => /uploads/photos/1/resized/width=auto&height=640&mode=fit/car.png
55
+ #
56
+ # Returns /application/public/uploads/photos/1/car.png
57
+ #
58
+ # NOTE: This method performs no validation
59
+ def assemble_source_path_from_resize_uri(root, resize_uri)
60
+ assemble_source_path_from_normal_uri(root, disassemble_resize_uri(resize_uri))
61
+ end
62
+
63
+ def assemble_source_path_from_normal_uri(root, normal_uri)
64
+ File.join(assemble_public_path(root), normal_uri)
65
+ end
66
+
67
+ # Path where resized photo is stored
68
+ #
69
+ # Arguments:
70
+ # root => /application
71
+ # source_path => /application/public/uploads/photos/1/car.png
72
+ # assembled_command => width=auto&height=640&mode=fit
73
+ #
74
+ # Returns /application/public/resized/uploads/photos/1/width=auto&height=640&mode=fit/car.png
75
+ #
76
+ # NOTE: This method performs no validation
77
+ def assemble_store_path(root, source_path, assembled_command)
78
+ public = assemble_public_path(root)
79
+ photo_location = dirname_or_blank(source_path.split(public).last)
80
+ File.join(public, PhotoCook::Resize.cache_dir, photo_location, assembled_command, File.basename(source_path))
81
+ end
82
+
83
+ # Path to public directory
84
+ #
85
+ # Arguments:
86
+ # root => /application
87
+ #
88
+ # Returns /application/public
89
+ #
90
+ # NOTE: This method performs no validation
91
+ def assemble_public_path(root)
92
+ File.join(root, PhotoCook.public_dir)
93
+ end
94
+
95
+ def resize_uri?(uri)
96
+ sections = uri.split('/')
97
+
98
+ # Check if PhotoCook cache directory exists:
99
+ # sections[-3] => resized
100
+ sections[-3] == PhotoCook::Resize.cache_dir &&
101
+
102
+ # Check if valid resize command exists:
103
+ # sections[-2] => width=auto&height=640&mode=fit
104
+ matches_regex?(sections[-2], Command.regex)
105
+ end
106
+
107
+ private
108
+ def dirname_or_blank(path)
109
+ File.dirname(path).sub(/\A\.\z/, '')
110
+ end
111
+
112
+ # Ruby 2.4 Regexp#match?
113
+ if Regexp.instance_methods.include?(:match?)
114
+ def matches_regex?(string, regex)
115
+ regex.match?(string)
116
+ end
117
+ else
118
+ def matches_regex?(string, regex)
119
+ regex === string
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+ module PhotoCook
3
+ module Resize
4
+ module Calculations
5
+ class << self
6
+ def size_to_fit(maxw, maxh, reqw, reqh, round = true)
7
+ outw, outh = maxw, maxh
8
+
9
+ scale = outw > reqw ? reqw / convert(outw) : 1.0
10
+ outw, outh = outw * scale, outh * scale
11
+
12
+ scale = outh > reqh ? reqh / convert(outh) : 1.0
13
+ outw, outh = outw * scale, outh * scale
14
+
15
+ round ? [round(outw), round(outh)] : [outw, outh]
16
+ end
17
+
18
+ def size_to_fill(maxw, maxh, reqw, reqh, round = true)
19
+ outw, outh = reqw, reqh
20
+
21
+ if reqw > maxw && reqh > maxh
22
+ if maxw >= maxh
23
+ outw = (reqw * maxh) / reqh.to_f
24
+ outh = maxh
25
+
26
+ if outw > maxw
27
+ outh = (outh * maxw) / convert(outw)
28
+ outw = maxw
29
+ end
30
+ else
31
+ outw = maxw
32
+ outh = (maxw * reqh) / reqw
33
+
34
+ if outh > maxh
35
+ outw = (outw * maxh) / convert(outh)
36
+ outh = maxh
37
+ end
38
+ end
39
+
40
+ elsif reqw > maxw
41
+ outw = maxw
42
+ outh = (reqh * maxw) / convert(reqw)
43
+
44
+ elsif reqh > maxh
45
+ outw = (reqw * maxh) / convert(reqh)
46
+ outh = maxh
47
+ end
48
+
49
+ round ? [round(outw), round(outh)] : [outw, outh]
50
+ end
51
+
52
+ def round(x)
53
+ PhotoCook::Pixels.round(x)
54
+ end
55
+
56
+ private
57
+ def convert(x)
58
+ x.kind_of?(Float) ? x : x.to_f
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+ module PhotoCook
3
+ module Resize
4
+ module CarrierWave
5
+ def resize(w, h, mode = :fit, options = {})
6
+ PhotoCook::Resize.uri(url, w, h, mode, options)
7
+ end
8
+
9
+ def resize_to_fit(w, h, options = {})
10
+ PhotoCook::Resize.uri(url, w, h, :fit, options)
11
+ end
12
+
13
+ def resize_to_fill(w, h, options = {})
14
+ PhotoCook::Resize.uri(url, w, h, :fill, options)
15
+ end
16
+
17
+ def resize_inline(w, h, mode = :fit, options = {})
18
+ PhotoCook::Resize.base64_uri(url, w, h, mode, options)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ module PhotoCook
3
+ module Resize
4
+ module Command
5
+ class << self
6
+ # Proportional support
7
+ # http://stackoverflow.com/questions/7200909/imagemagick-convert-to-fixed-width-proportional-height
8
+ #
9
+ # Device pixel ratio collection
10
+ # http://dpi.lv/
11
+ # http://www.canbike.org/CSSpixels/
12
+ def regex
13
+ @regex ||= /\A(?<mode>fit|fill)-(?<width>\d+)x(?<height>\d+)\z/
14
+ end
15
+
16
+ # NOTE: This method performs no validation
17
+ # NOTE: This method is very hot
18
+ def assemble(width, height, mode)
19
+ "#{mode}-#{width}x#{height}"
20
+ end
21
+
22
+ def extract(resize_uri)
23
+ resize_uri.split('/')[-2].match(@regex)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+ PhotoCook.subscribe 'resize:middleware:match' do |uri|
3
+ PhotoCook.log do
4
+ log "PhotoCook::Resize::Middleware matched request for photo resize"
5
+ log "Request URI: #{uri}"
6
+ end
7
+ end
8
+
9
+ PhotoCook.subscribe 'resize' do |photo, msec|
10
+ PhotoCook.log do
11
+ log "Performed resize"
12
+ log "Source path: #{photo.source_path}"
13
+ log "Store path: #{photo.store_path}"
14
+ log "Max width: #{photo.max_width}px"
15
+ log "Max height: #{photo.max_height}px"
16
+ log "Desired width: #{photo.desired_width}px"
17
+ log "Desired height: #{photo.desired_height}px"
18
+ log "Desired aspect ratio: #{photo.desired_aspect_ratio.round(3)}"
19
+ log "Calculated width: #{photo.calculated_width}px"
20
+ log "Calculated height: #{photo.calculated_height}px"
21
+ log "Calculated aspect ratio: #{photo.calculated_aspect_ratio.round(3)}"
22
+ log "Resize mode: #{photo.resize_mode}"
23
+ log "Completed in: #{msec.round(1)}ms"
24
+ end
25
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+ module PhotoCook
3
+ module Resize
4
+ class MagickPhoto < MiniMagick::Image
5
+ attr_accessor :source_path
6
+ attr_accessor :store_path
7
+
8
+ attr_accessor :desired_width
9
+ attr_accessor :desired_height
10
+
11
+ attr_accessor :calculated_width
12
+ attr_accessor :calculated_height
13
+
14
+ attr_accessor :resize_mode
15
+
16
+ attr_reader :max_width
17
+ attr_reader :max_height
18
+
19
+ def initialize(*)
20
+ super
21
+ @max_width = self[:width]
22
+ @max_height = self[:height]
23
+ end
24
+
25
+ def desired_aspect_ratio
26
+ desired_width / desired_height.to_f
27
+ end
28
+
29
+ def calculated_aspect_ratio
30
+ calculated_width / calculated_height.to_f
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+ module PhotoCook
3
+ module Resize
4
+ class Middleware
5
+ class << self
6
+ attr_accessor :headers
7
+ end
8
+
9
+ self.headers = { 'Cache-Control' => 'public, max-age=31536000, no-transform' }
10
+
11
+ def initialize(app)
12
+ @app = app
13
+ end
14
+
15
+ # Consider we have car.png in /uploads/photos/1/car.png
16
+ # We want to resize it with the following params:
17
+ # Width: choose automatically
18
+ # Height: exactly 640px
19
+ # Mode: fill
20
+ #
21
+ # Middleware will handle this URI:
22
+ # /uploads/photos/1/resized/width=auto&height=640&mode=fill/car.png
23
+ #
24
+ def call(env)
25
+ uri = extract_uri(env)
26
+
27
+ # Check if URI contains PhotoCook resize indicators
28
+ return default_action(env) unless Assemble.resize_uri?(uri)
29
+
30
+ # If resized photo exists but nginx or apache didn't handle this request
31
+ return respond_with_file(env) if requested_file_exists?(uri)
32
+
33
+ # At this point we are sure that this request is targeting to resize photo
34
+ PhotoCook.notify('resize:middleware:match', uri)
35
+
36
+ # Matched data: width=auto&height=640&mode=fill
37
+ command = Command.extract(uri)
38
+
39
+ # Assemble path of the source photo:
40
+ # => /application/public/uploads/photos/1/car.png
41
+ source_path = Assemble.assemble_source_path_from_resize_uri(root_path, uri)
42
+
43
+ # Assemble path of the resized photo:
44
+ # => /application/public/resized/uploads/photos/1/COMMAND/car.png
45
+ store_path = Assemble.assemble_store_path(root_path, source_path, command.to_s)
46
+
47
+ if File.file?(store_path) && File.readable?(store_path)
48
+ symlink_cache_dir(source_path, store_path)
49
+ respond_with_file(env)
50
+
51
+ elsif File.file?(source_path) && File.readable?(source_path)
52
+ # Finally resize photo
53
+ # Resized photo will appear in resize directory
54
+ Resize.perform(source_path, store_path, command[:width], command[:height], command[:mode])
55
+ symlink_cache_dir(source_path, store_path)
56
+ respond_with_file(env)
57
+
58
+ else
59
+ default_action(env)
60
+ end
61
+ end
62
+
63
+ protected
64
+ def root_path
65
+ PhotoCook.root_path
66
+ end
67
+
68
+ def public_path
69
+ PhotoCook::Resize::Assemble.assemble_public_path(root_path)
70
+ end
71
+
72
+ def requested_file_exists?(uri)
73
+ # Check if file exists:
74
+ # /application/public/uploads/photos/1/resized/width=auto&height=640&mode=fill/car.png
75
+ path = File.join(public_path, uri)
76
+ File.file?(path) && File.readable?(path)
77
+ end
78
+
79
+ def extract_uri(env)
80
+ Rack::Utils.clean_path_info(Rack::Utils.unescape(env['PATH_INFO']))
81
+ end
82
+
83
+ def default_action(env)
84
+ @app.call(env)
85
+ end
86
+
87
+ def respond_with_file(env)
88
+ # http://rubylogs.com/writing-rails-middleware/
89
+ # https://viget.com/extend/refactoring-patterns-the-rails-middleware-response-handler
90
+ status, headers, body = Rack::File.new(public_path).call(env)
91
+
92
+ # Rack::File will set Last-Modified, Content-Type and Content-Length
93
+ # We will set Cache-Control. This is default behaviour.
94
+ # This is configurable in PhotoCook::Resize::Middleware.headers
95
+ headers.merge!(self.class.headers) if status == 200 || status == 304
96
+
97
+ response = Rack::Response.new(body, status, headers)
98
+ response.finish
99
+ end
100
+
101
+ def symlink_cache_dir(source_path, store_path)
102
+ PhotoCook::Utils.make_relative_symlink(
103
+ File.dirname(File.dirname(store_path)),
104
+ File.join(File.dirname(source_path), PhotoCook::Resize.cache_dir))
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+ module PhotoCook
3
+ module Resize
4
+ module Mode
5
+ class << self
6
+ def parse(mode)
7
+ case mode
8
+ when true then :fill
9
+ when false then :fit
10
+ when :fit, :fill then mode
11
+ when 'fit', 'fill' then mode.to_sym
12
+ end
13
+ end
14
+
15
+ def parse!(mode)
16
+ check!(mode = parse(mode))
17
+ mode
18
+ end
19
+
20
+ def check!(mode)
21
+ case mode
22
+ when :fill, :fit then true
23
+ else raise Unknown, mode
24
+ end
25
+ true
26
+ end
27
+ end
28
+
29
+ class Unknown < ::ArgumentError
30
+ def initialize(mode)
31
+ super "Mode #{mode} is unknown"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+ module PhotoCook
3
+ module Resize
4
+ class << self
5
+ def resizer
6
+ Resizer.instance
7
+ end
8
+ end
9
+
10
+ class Resizer
11
+ include Singleton
12
+
13
+ CENTER_GRAVITY = 'Center'.freeze
14
+ TRANSPARENT_BACKGROUND = 'rgba(255, 255, 255, 0.0)'.freeze
15
+
16
+ def resize(source_path, store_path, w, h, mode)
17
+ send("resize_to_#{mode}", source_path, store_path, w, h)
18
+ end
19
+
20
+ # Resize the photo to fit within the specified dimensions:
21
+ # - the original aspect ratio will be kept
22
+ # - new dimensions will be not larger then the specified
23
+ def resize_to_fit(source_path, store_path, width, height)
24
+ process photo: source_path, store: store_path do |photo|
25
+ outw, outh = Calculations.size_to_fit(*photo[:dimensions], width, height)
26
+ photo.resize "#{PhotoCook::Pixels.to_magick_dimensions(outw, outh)}>"
27
+
28
+ photo.resize_mode = :fit
29
+ photo.desired_width = width
30
+ photo.desired_height = height
31
+ photo.calculated_width = outw
32
+ photo.calculated_height = outh
33
+ end
34
+ end
35
+
36
+ # Resize the photo to fill within the specified dimensions:
37
+ # - the original aspect ratio will be kept
38
+ # - new dimensions may vary
39
+ def resize_to_fill(source_path, store_path, width, height)
40
+ process photo: source_path, store: store_path do |photo|
41
+ outw, outh = Calculations.size_to_fill(*photo[:dimensions], width, height)
42
+
43
+ photo.combine_options do |cmd|
44
+ cmd.resize PhotoCook::Pixels.to_magick_dimensions(outw, outh) + '^'
45
+ cmd.gravity CENTER_GRAVITY
46
+ cmd.background TRANSPARENT_BACKGROUND
47
+ cmd.crop PhotoCook::Pixels.to_magick_dimensions(outw, outh) + '+0+0'
48
+ cmd.repage.+
49
+ end
50
+
51
+ photo.resize_mode = :fill
52
+ photo.desired_width = width
53
+ photo.desired_height = height
54
+ photo.calculated_width = outw
55
+ photo.calculated_height = outh
56
+ end
57
+ end
58
+
59
+ protected
60
+
61
+ def process(photo:, store:)
62
+ photo = open(photo)
63
+ yield(photo)
64
+ store(photo, store)
65
+ end
66
+
67
+ def open(source_path)
68
+ # MiniMagick::Image.open creates a temporary file for us and protects original
69
+ photo = MagickPhoto.open(source_path)
70
+ photo.validate!
71
+ photo.source_path = source_path
72
+ photo
73
+ end
74
+
75
+ def store(resized_photo, store_path)
76
+ path_to_dir = File.dirname(store_path)
77
+
78
+ # Solution to broken symlinks.
79
+ # This added here because mkdir -p can't detect broken symlinks.
80
+ FileUtils.rm(path_to_dir) if !Dir.exists?(path_to_dir) && File.symlink?(path_to_dir)
81
+ FileUtils.mkdir_p(path_to_dir)
82
+ resized_photo.write(store_path)
83
+ resized_photo.store_path = store_path
84
+ resized_photo
85
+ end
86
+ end
87
+ end
88
+ end