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.
- checksums.yaml +4 -4
- data/.travis.yml +16 -0
- data/Gemfile +3 -13
- data/README.md +2 -2
- data/Rakefile +2 -15
- data/app/assets/javascripts/photo-cook/photo-cook.coffee +90 -0
- data/app/helpers/photo_cook_helper.rb +14 -0
- data/lib/photo-cook/device-pixel-ratio-spy.rb +17 -0
- data/lib/photo-cook/device-pixel-ratio.rb +67 -0
- data/lib/photo-cook/engine.rb +15 -2
- data/lib/photo-cook/events.rb +15 -0
- data/lib/photo-cook/logger.rb +37 -0
- data/lib/photo-cook/optimization/__api__.rb +21 -0
- data/lib/photo-cook/optimization/carrierwave.rb +10 -0
- data/lib/photo-cook/optimization/image-optim.rb +131 -0
- data/lib/photo-cook/optimization/job.rb +12 -0
- data/lib/photo-cook/optimization/logging.rb +21 -0
- data/lib/photo-cook/pixels.rb +56 -0
- data/lib/photo-cook/resize/__api__.rb +93 -0
- data/lib/photo-cook/resize/assemble.rb +125 -0
- data/lib/photo-cook/resize/calculations.rb +63 -0
- data/lib/photo-cook/resize/carrierwave.rb +22 -0
- data/lib/photo-cook/resize/command.rb +28 -0
- data/lib/photo-cook/resize/logging.rb +25 -0
- data/lib/photo-cook/resize/magick-photo.rb +34 -0
- data/lib/photo-cook/resize/middleware.rb +108 -0
- data/lib/photo-cook/resize/mode.rb +36 -0
- data/lib/photo-cook/resize/resizer.rb +88 -0
- data/lib/photo-cook/utils.rb +71 -0
- data/lib/photo-cook/version.rb +3 -2
- data/lib/photo-cook.rb +32 -23
- data/photo-cook.gemspec +14 -8
- data/srcset.rb +34 -0
- data/test/fixtures/dog.png +0 -0
- data/test/helper.rb +12 -0
- data/test/test-photo-cook-assemble.rb +23 -0
- data/test/test-photo-cook-calculations.rb +45 -0
- data/test/test-photo-cook-resize.rb +62 -0
- metadata +86 -25
- data/app/assets/javascripts/photo-cook/photo-cook.js.erb +0 -85
- data/lib/photo-cook/abstract-optimizer.rb +0 -9
- data/lib/photo-cook/assemble.rb +0 -97
- data/lib/photo-cook/carrierwave.rb +0 -15
- data/lib/photo-cook/command.rb +0 -49
- data/lib/photo-cook/dimensions.rb +0 -36
- data/lib/photo-cook/dirs.rb +0 -8
- data/lib/photo-cook/image-optim.rb +0 -24
- data/lib/photo-cook/logging.rb +0 -85
- data/lib/photo-cook/magick-photo.rb +0 -6
- data/lib/photo-cook/middleware.rb +0 -139
- data/lib/photo-cook/optimization-api.rb +0 -20
- data/lib/photo-cook/optimization-job.rb +0 -9
- data/lib/photo-cook/pixel-ratio-spy.rb +0 -19
- data/lib/photo-cook/pixel-ratio.rb +0 -36
- data/lib/photo-cook/resize-api.rb +0 -94
- data/lib/photo-cook/resizer.rb +0 -111
- 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
|