photo-cook 1.0.2 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/app/assets/javascripts/photo-cook/photo-cook.js.erb +114 -0
- data/app/assets/javascripts/photo-cook.js +1 -0
- data/lib/photo-cook/assemble.rb +27 -0
- data/lib/photo-cook/callbacks.rb +8 -0
- data/lib/photo-cook/carrierwave.rb +7 -1
- data/lib/photo-cook/command.rb +35 -0
- data/lib/photo-cook/dimensions.rb +36 -0
- data/lib/photo-cook/engine.rb +4 -2
- data/lib/photo-cook/logging.rb +18 -0
- data/lib/photo-cook/middleware.rb +61 -63
- data/lib/photo-cook/paths.rb +12 -0
- data/lib/photo-cook/pixel-ratio-spy.rb +19 -0
- data/lib/photo-cook/pixel-ratio.rb +29 -0
- data/lib/photo-cook/resizer.rb +51 -50
- data/lib/photo-cook/resizing.rb +68 -0
- data/lib/photo-cook/version.rb +1 -1
- data/lib/photo-cook.rb +20 -34
- metadata +13 -3
- data/vendor/assets/javascripts/photo-cook/photo-cook.js.erb +0 -49
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 30713968f97de16d9fa2a2d0c3b88bcb5435ac43
|
4
|
+
data.tar.gz: 6deed34ded832a9041bb6adf6bd5595acb2ae170
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5c76ef21599b1cb9d4c5e870b77afc2748aa7ba65613b7b46246a6850c9e3aee716182c45c4fdc100951746af0feb2a9ed910df90fd12cc68d3ad47e33cfd081
|
7
|
+
data.tar.gz: a87c0fdaf2d015ac0b9aafd5b551d64c18c864f21f23d514c6ddd9989759df01cc108cbeb9a6f447435f5fdb001889479cf329c29d07e46c84502716abd89ed2
|
data/Gemfile.lock
CHANGED
@@ -0,0 +1,114 @@
|
|
1
|
+
(function(root) {
|
2
|
+
var PhotoCook = {
|
3
|
+
resizeDir: <%= PhotoCook.cache_dir.to_json %>,
|
4
|
+
|
5
|
+
commandRegex: /\-(?:(?:\d+x\d+)|(?:\d+x)|(?:x\d+))(?:crop)?$/,
|
6
|
+
|
7
|
+
initialize: function() {
|
8
|
+
PhotoCook.persistPixelRatio();
|
9
|
+
},
|
10
|
+
|
11
|
+
pixelRatio: (function () {
|
12
|
+
// https://gist.github.com/marcedwards/3446599
|
13
|
+
var mediaQuery = [
|
14
|
+
'(-webkit-min-device-pixel-ratio: 1.3)',
|
15
|
+
'(-o-min-device-pixel-ratio: 13/10)',
|
16
|
+
'min-resolution: 120dpi'
|
17
|
+
].join(', ');
|
18
|
+
|
19
|
+
var ratio = window.devicePixelRatio;
|
20
|
+
|
21
|
+
// If no ratio found check if screen is retina
|
22
|
+
// and if so return 2x ratio
|
23
|
+
if (ratio == null && typeof window.matchMedia === 'function') {
|
24
|
+
if (window.matchMedia(mediaQuery).matches) {
|
25
|
+
ratio = 2;
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
return parseFloat(ratio == null ? 1 : ratio);
|
30
|
+
})(),
|
31
|
+
|
32
|
+
persistPixelRatio: function() {
|
33
|
+
var date = new Date();
|
34
|
+
|
35
|
+
// Expires in 1 year
|
36
|
+
date.setTime(date.getTime() + 365 * 24 * 60 * 60 * 1000);
|
37
|
+
var expires = 'expires=' + date.toUTCString();
|
38
|
+
document.cookie = "PhotoCookPixelRatio=" + PhotoCook.pixelRatio + '; ' + expires;
|
39
|
+
},
|
40
|
+
|
41
|
+
resize: function (path, width, height, options) {
|
42
|
+
var crop = typeof options === 'boolean' ? !!options : !!(options && options.crop);
|
43
|
+
var ratio = Math.max(0, options && options.pixelRatio) || PhotoCook.pixelRatio;
|
44
|
+
|
45
|
+
var command = '-' + (width && width !== 'auto' ? Math.round(width * ratio) : '')
|
46
|
+
+ 'x'
|
47
|
+
+ (height && height !== 'auto' ? Math.round(height * ratio) : '')
|
48
|
+
+ (crop ? 'crop' : '');
|
49
|
+
|
50
|
+
var index; index = path.lastIndexOf('.');
|
51
|
+
if (index < 0) { index = path.lastIndexOf('?'); }
|
52
|
+
if (index < 0) { index = path.lastIndexOf('#'); }
|
53
|
+
|
54
|
+
var leftPart = path.slice(0, Math.max(path.lastIndexOf('/'), 0));
|
55
|
+
var rightPart = index >= 0 ? path.slice(index) : '';
|
56
|
+
|
57
|
+
var basename = path.slice(
|
58
|
+
leftPart ? leftPart.length + 1 : 0,
|
59
|
+
rightPart ? 0 - rightPart.length : path.length
|
60
|
+
);
|
61
|
+
|
62
|
+
return (leftPart ? leftPart + '/' : leftPart)
|
63
|
+
+ PhotoCook.resizeDir
|
64
|
+
+ '/'
|
65
|
+
+ basename
|
66
|
+
+ command
|
67
|
+
+ rightPart;
|
68
|
+
},
|
69
|
+
|
70
|
+
strip: function (path) {
|
71
|
+
var token = PhotoCook.resizeDir + '/';
|
72
|
+
var leftIndex = path.lastIndexOf(token);
|
73
|
+
if (leftIndex < 0 || (leftIndex > 0 && path[leftIndex - 1] !== '/')) {
|
74
|
+
return path;
|
75
|
+
}
|
76
|
+
|
77
|
+
var rightIndex; rightIndex = path.lastIndexOf('.');
|
78
|
+
if (rightIndex < 0) { rightIndex = path.lastIndexOf('?'); }
|
79
|
+
if (rightIndex < 0) { rightIndex = path.lastIndexOf('#'); }
|
80
|
+
|
81
|
+
var basename = path.slice(
|
82
|
+
leftIndex + token.length,
|
83
|
+
rightIndex >= 0 ? 0 - path.length + rightIndex : path.length
|
84
|
+
);
|
85
|
+
|
86
|
+
var oldLength = basename.length;
|
87
|
+
|
88
|
+
basename = basename.replace(PhotoCook.commandRegex, '');
|
89
|
+
|
90
|
+
if (oldLength === basename.length) {
|
91
|
+
return path;
|
92
|
+
}
|
93
|
+
|
94
|
+
var leftPart = path.slice(0, leftIndex);
|
95
|
+
var rightPart = rightIndex >= 0 ? path.slice(rightIndex) : '';
|
96
|
+
|
97
|
+
return leftPart + basename + rightPart;
|
98
|
+
},
|
99
|
+
|
100
|
+
uriRegex: /^[-a-z]+:\/\/|^(?:cid|data):|^\/\//i,
|
101
|
+
|
102
|
+
// Returns true if given URL can produce request to PhotoCook middleware on server
|
103
|
+
isServableURL: function(url) {
|
104
|
+
// By default check that URL is relative
|
105
|
+
return !PhotoCook.uriRegex.test(url);
|
106
|
+
}
|
107
|
+
};
|
108
|
+
|
109
|
+
root.PhotoCook = PhotoCook;
|
110
|
+
})(this);
|
111
|
+
|
112
|
+
PhotoCook.initialize();
|
113
|
+
|
114
|
+
// navigator.cookieEnabled
|
@@ -0,0 +1 @@
|
|
1
|
+
//= require ./photo-cook/photo-cook
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module PhotoCook
|
2
|
+
module Assemble
|
3
|
+
|
4
|
+
# Edit URI so it will point to PhotoCook::Middleware
|
5
|
+
# NOTE: This method performs no validation
|
6
|
+
def assemble_uri(uri, width, height, pixel_ratio, crop)
|
7
|
+
result = File.join cache_dir, dirname_or_blank(uri),
|
8
|
+
assemble_command(width, height, pixel_ratio, crop), File.basename(uri)
|
9
|
+
uri.start_with?('/') ? '/' + result : result
|
10
|
+
end
|
11
|
+
|
12
|
+
# Edit path so it will point to place where resized photo stored
|
13
|
+
# NOTE: This method performs no validation
|
14
|
+
def assemble_store_path(path, width, height, pixel_ratio, crop)
|
15
|
+
rootless = path.split(File.join(root, public_dir)).second
|
16
|
+
File.join root, public_dir,
|
17
|
+
cache_dir, dirname_or_blank(rootless),
|
18
|
+
assemble_command(width, height, pixel_ratio, crop), File.basename(path)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def dirname_or_blank(path)
|
23
|
+
(dirname = File.dirname(path)) == '.' ? '' : dirname
|
24
|
+
end
|
25
|
+
end
|
26
|
+
extend Assemble
|
27
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module PhotoCook
|
2
|
+
module Command
|
3
|
+
# Proportional support
|
4
|
+
# http://stackoverflow.com/questions/7200909/imagemagick-convert-to-fixed-width-proportional-height
|
5
|
+
#
|
6
|
+
# Device pixel ratio collection
|
7
|
+
# http://dpi.lv/
|
8
|
+
# http://www.canbike.org/CSSpixels/
|
9
|
+
def command_regex
|
10
|
+
@command_regex ||= %r{
|
11
|
+
width= (?<width> auto|\d{1,4}) &
|
12
|
+
height= (?<height> auto|\d{1,4}) &
|
13
|
+
pixel_ratio= (?<pixel_ratio>[1234]) &
|
14
|
+
crop= (?<crop> yes|no)
|
15
|
+
}x
|
16
|
+
end
|
17
|
+
|
18
|
+
# NOTE: This method performs no validation
|
19
|
+
def assemble_command(width, height, pixel_ratio, crop)
|
20
|
+
"width=#{ width == 0 ? 'auto' : width}&" +
|
21
|
+
"height=#{ height == 0 ? 'auto' : height}&" +
|
22
|
+
"pixel_ratio=#{pixel_ratio.ceil}&" +
|
23
|
+
"crop=#{ bool_to_crop(crop)}&"
|
24
|
+
end
|
25
|
+
|
26
|
+
def crop_to_bool(crop)
|
27
|
+
crop == 'yes'
|
28
|
+
end
|
29
|
+
|
30
|
+
def bool_to_crop(crop)
|
31
|
+
crop ? 'yes' : 'no'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
extend Command
|
35
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module PhotoCook
|
2
|
+
module Dimensions
|
3
|
+
def parse_and_check_dimensions(unsafe_width, unsafe_height)
|
4
|
+
width = unsafe_width == :auto ? 0 : unsafe_width.to_i
|
5
|
+
height = unsafe_height == :auto ? 0 : unsafe_height.to_i
|
6
|
+
|
7
|
+
check_dimensions!(width, height)
|
8
|
+
[width, height]
|
9
|
+
end
|
10
|
+
|
11
|
+
def check_dimensions!(width, height)
|
12
|
+
raise WidthOutOfBoundsError if width < 0 || width > 9999
|
13
|
+
raise HeightOutOfBoundsError if height < 0 || height > 9999
|
14
|
+
raise NoConcreteDimensionsError if width + height == 0
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class WidthOutOfBoundsError < ArgumentError
|
19
|
+
def initialize
|
20
|
+
super 'Width must be positive integer number (0...9999)'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class HeightOutOfBoundsError < ArgumentError
|
25
|
+
def initialize
|
26
|
+
super 'Height must be positive integer number (0...9999)'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class NoConcreteDimensionsError < ArgumentError
|
31
|
+
def initialize
|
32
|
+
super "Both width and height specified as 'auto'"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
extend Dimensions
|
36
|
+
end
|
data/lib/photo-cook/engine.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
module PhotoCook
|
2
2
|
class Engine < ::Rails::Engine
|
3
|
+
config.before_initialize do
|
4
|
+
PhotoCook.root = Rails.root
|
5
|
+
end
|
3
6
|
|
4
|
-
initializer :
|
7
|
+
initializer :photo_cook_javascripts do |app|
|
5
8
|
app.config.assets.paths << File.join(PhotoCook::Engine.root, 'app/assets/javascripts')
|
6
9
|
end
|
7
|
-
|
8
10
|
end
|
9
11
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module PhotoCook
|
2
|
+
module Logging
|
3
|
+
def log_resize(photo, w, h, px_ratio, crop, msec)
|
4
|
+
msg = %{
|
5
|
+
[PhotoCook] Resized photo.
|
6
|
+
Source file: #{photo.source_path}
|
7
|
+
Resized file: #{photo.resized_path}
|
8
|
+
Width: #{w == 0 ? 'auto': "#{w}px"}
|
9
|
+
Height: #{h == 0 ? 'auto': "#{h}px"}
|
10
|
+
Crop: #{crop ? 'yes' : 'no'}
|
11
|
+
Pixel ratio: #{px_ratio}
|
12
|
+
Completed in: #{msec.round(1)}ms
|
13
|
+
}
|
14
|
+
rails_env? ? Rails.logger.info(msg) : print(msg)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
extend Logging
|
18
|
+
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# To use this middleware you should configure application:
|
2
|
-
#
|
3
|
-
require 'rack'
|
2
|
+
# application.config.middleware.insert_before(Rack::Sendfile, PhotoCook::Middleware, Rails.root)
|
4
3
|
|
5
4
|
module PhotoCook
|
6
5
|
class Middleware
|
@@ -9,99 +8,98 @@ module PhotoCook
|
|
9
8
|
@app, @root = app, root
|
10
9
|
end
|
11
10
|
|
12
|
-
#
|
13
|
-
#
|
11
|
+
# Consider we have car.png in /uploads/photos/1
|
12
|
+
# We want to resize it with the following params:
|
13
|
+
# Width: choose automatically
|
14
|
+
# Height: exactly 640px
|
15
|
+
# Crop: yes
|
16
|
+
# Pixel ratio: 1
|
17
|
+
#
|
18
|
+
# Middleware will handle this URI:
|
19
|
+
# /resized/uploads/photos/1/width:auto&height:640&crop:true&pixel_ratio:1/car.png
|
20
|
+
#
|
14
21
|
def call(env)
|
15
22
|
uri = extract_uri(env)
|
16
23
|
|
17
|
-
return default_actions(env)
|
24
|
+
return default_actions(env) if
|
18
25
|
|
19
|
-
#
|
20
|
-
|
21
|
-
# File::SEPARATOR + PhotoCook.resize_dir = /resized
|
22
|
-
# dir.chomp! = /uploads/photos
|
23
|
-
(dir = File.dirname(uri)).chomp!(File::SEPARATOR + PhotoCook.resize_dir) &&
|
26
|
+
# Check if uri starts with '/resized'
|
27
|
+
false == uri.start_with?(PhotoCook.resize_uri_indicator) ||
|
24
28
|
|
25
|
-
#
|
26
|
-
#
|
27
|
-
|
28
|
-
# photo_basename.sub! = car.png
|
29
|
-
(photo_name = File.basename(uri)) &&
|
30
|
-
(photo_basename = File.basename(photo_name, '.*')).sub!(command_regex, '')
|
29
|
+
# Check if uri has valid resize command.
|
30
|
+
# uri.split('/')[-2] => width:auto&height:640&crop:true&pixel_ratio:1
|
31
|
+
nil == (uri.split('/')[-2] =~ PhotoCook.command_regex) ||
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
# Regex match: _640x320crop
|
35
|
-
command = Regexp.last_match
|
33
|
+
# If for some reasons file exists but request went to Ruby app
|
34
|
+
true == requested_file_exists?(uri)
|
36
35
|
|
37
36
|
# At this point we are sure that this request is targeting to resize photo
|
38
37
|
|
39
|
-
#
|
40
|
-
|
38
|
+
# Assemble path of the source photo:
|
39
|
+
# => /uploads/photos/1/car.png
|
40
|
+
source_path = assemble_source_path(uri)
|
41
41
|
|
42
|
-
#
|
43
|
-
|
42
|
+
# Matched data: width:auto&height:640&crop:true&pixel_ratio:1
|
43
|
+
command = Regexp.last_match
|
44
44
|
|
45
|
-
#
|
46
|
-
|
45
|
+
# Map crop option values: 'yes' => true, 'no' => false
|
46
|
+
crop = PhotoCook.crop_to_bool(command[:crop])
|
47
47
|
|
48
|
-
|
49
|
-
|
48
|
+
# Finally resize photo
|
49
|
+
# Resized photo will appear in resize directory
|
50
|
+
photo = PhotoCook.resize_photo(source_path,
|
51
|
+
command[:width], command[:height], pixel_ratio: command[:pixel_ratio], crop: crop)
|
50
52
|
|
51
|
-
|
52
|
-
|
53
|
-
status, headers, body = Rack::File.new(File.join(@root, PhotoCook.public_dir)).call(env)
|
54
|
-
response = Rack::Response.new(body, status, headers)
|
55
|
-
response.finish
|
53
|
+
if photo
|
54
|
+
respond_with_file(env)
|
56
55
|
else
|
57
56
|
default_actions(env)
|
58
57
|
end
|
59
58
|
end
|
60
59
|
|
61
|
-
|
60
|
+
private
|
62
61
|
|
63
|
-
def assemble_source_path(
|
64
|
-
|
62
|
+
def assemble_source_path(resize_uri)
|
63
|
+
# Take URI:
|
64
|
+
# /resized/uploads/photos/1/width:auto&height:640&crop:true&pixel_ratio:1/car.png
|
65
|
+
#
|
66
|
+
# Split by file separator:
|
67
|
+
# ["", "resized", "uploads", "photos", "1", "width:auto&height:640&crop:true&pixel_ratio:1", "car.png"]
|
68
|
+
#
|
69
|
+
els = resize_uri.split('/')
|
70
|
+
|
71
|
+
# Delete PhotoCook directory:
|
72
|
+
# ["", "uploads", "photos", "1", "width:auto&height:640&crop:true&pixel_ratio:1", "car.png"]
|
73
|
+
els.delete_at(1)
|
74
|
+
|
75
|
+
# Delete command string:
|
76
|
+
# ["", "uploads", "photos", "1", "car.png"]
|
77
|
+
els.delete_at(-2)
|
78
|
+
|
79
|
+
URI.decode File.join(@root, PhotoCook.public_dir, els)
|
65
80
|
end
|
66
81
|
|
67
82
|
def requested_file_exists?(uri)
|
68
|
-
#
|
83
|
+
# Check if file exists:
|
84
|
+
# /application/public/resized/uploads/photos/1/width:auto&height:640&crop:true&pixel_ratio:1/car.png
|
69
85
|
File.exists? File.join(@root, PhotoCook.public_dir, uri)
|
70
86
|
end
|
71
87
|
|
72
88
|
def extract_uri(env)
|
73
|
-
|
89
|
+
# Remove query string and fragment
|
90
|
+
Rack::Utils.unescape(env['PATH_INFO'].gsub(/[\?#].*\z/, ''))
|
74
91
|
end
|
75
92
|
|
76
93
|
def default_actions(env)
|
77
94
|
@app.call(env)
|
78
95
|
end
|
79
96
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
@r_command = %r{
|
87
|
-
\- (?:(?:#{w}x#{h}) | (?:#{w}x) | (?:x#{h})) (?<crop>crop)? \z
|
88
|
-
}x
|
89
|
-
end
|
90
|
-
@r_command
|
91
|
-
end
|
92
|
-
|
93
|
-
def log(photo, source_path, resized_path, command)
|
94
|
-
w = command[:width].to_i
|
95
|
-
h = command[:height].to_i
|
96
|
-
crop = !!command[:crop]
|
97
|
-
Rails.logger.info %{
|
98
|
-
[PhotoCook] Resized photo.
|
99
|
-
Source file: "#{source_path}".
|
100
|
-
Resized file: "#{resized_path}".
|
101
|
-
Width: #{w == 0 ? 'auto': "#{w}px"}.
|
102
|
-
Height: #{h == 0 ? 'auto': "#{h}px"}.
|
103
|
-
Crop: #{crop ? 'yes' : 'no'}.
|
104
|
-
}
|
97
|
+
def respond_with_file(env)
|
98
|
+
# http://rubylogs.com/writing-rails-middleware/
|
99
|
+
# https://viget.com/extend/refactoring-patterns-the-rails-middleware-response-handler
|
100
|
+
status, headers, body = Rack::File.new(File.join(@root, PhotoCook.public_dir)).call(env)
|
101
|
+
response = Rack::Response.new(body, status, headers)
|
102
|
+
response.finish
|
105
103
|
end
|
106
104
|
end
|
107
105
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module PhotoCook
|
2
|
+
module PixelRatioSpy
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
before_action :pass_pixel_ratio
|
7
|
+
end
|
8
|
+
|
9
|
+
def pass_pixel_ratio
|
10
|
+
ratio = cookies[:PhotoCookPixelRatio].to_f
|
11
|
+
# Dont set pixel ratio if for some reasons it is invalid
|
12
|
+
PhotoCook.client_pixel_ratio = PhotoCook.valid_pixel_ratio?(ratio) ? ratio : nil
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class << self
|
17
|
+
attr_accessor :client_pixel_ratio
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module PhotoCook
|
2
|
+
module PixelRatio
|
3
|
+
def parse_and_check_pixel_ratio(unsafe_ratio)
|
4
|
+
pixel_ratio = unsafe_ratio.to_f
|
5
|
+
|
6
|
+
raise PixelRatioInvalidOrInfiniteError if pixel_ratio.nan? || pixel_ratio.infinite?
|
7
|
+
raise PixelRatioOutOfBoundsError if pixel_ratio < 1 || pixel_ratio > 4
|
8
|
+
|
9
|
+
pixel_ratio
|
10
|
+
end
|
11
|
+
|
12
|
+
def valid_pixel_ratio?(ratio)
|
13
|
+
!ratio.nan? && !ratio.infinite? && ratio >= 1 && ratio <= 4
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class PixelRatioInvalidOrInfiniteError < ArgumentError
|
18
|
+
def initialize
|
19
|
+
super 'Given pixel ratio is invalid number or infinite'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class PixelRatioOutOfBoundsError < ArgumentError
|
24
|
+
def initialize
|
25
|
+
super 'Pixel ratio must be positive number: 1 <= pixel_ratio <= 4'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
extend PixelRatio
|
29
|
+
end
|
data/lib/photo-cook/resizer.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'mini_magick'
|
2
|
-
|
3
1
|
module PhotoCook
|
4
2
|
class Resizer
|
5
3
|
include Singleton
|
@@ -7,11 +5,11 @@ module PhotoCook
|
|
7
5
|
CENTER_GRAVITY = 'Center'.freeze
|
8
6
|
TRANSPARENT_BACKGROUND = 'rgba(255,255,255,0.0)'.freeze
|
9
7
|
|
10
|
-
def resize(photo_path, width, height, crop = false)
|
8
|
+
def resize(photo_path, width, height, pixel_ratio = 1.0, crop = false)
|
11
9
|
if crop
|
12
|
-
resize_to_fill
|
10
|
+
resize_to_fill(photo_path, width, height, pixel_ratio)
|
13
11
|
else
|
14
|
-
resize_to_fit
|
12
|
+
resize_to_fit(photo_path, width, height, pixel_ratio)
|
15
13
|
end
|
16
14
|
end
|
17
15
|
|
@@ -20,19 +18,15 @@ module PhotoCook
|
|
20
18
|
# - new dimensions will be not larger then the specified
|
21
19
|
#
|
22
20
|
# https://github.com/carrierwaveuploader/carrierwave/blob/71cb18bba4a2078524d1ea683f267d3a97aa9bc8/lib/carrierwave/processing/mini_magick.rb#L131
|
23
|
-
def resize_to_fit(photo_path, width, height)
|
21
|
+
def resize_to_fit(photo_path, width, height, pixel_ratio)
|
24
22
|
|
25
23
|
# Do nothing if photo is not valid so exceptions will be not thrown
|
26
|
-
return unless (photo = open(photo_path)).
|
24
|
+
return unless (photo = open(photo_path)) && photo.valid?
|
27
25
|
|
28
|
-
width, height
|
29
|
-
|
26
|
+
store_path = assemble_store_path(photo_path, width, height, pixel_ratio, false)
|
27
|
+
width, height = multiply_dimensions(width, height, pixel_ratio)
|
30
28
|
|
31
|
-
|
32
|
-
photo.combine_options do |cmd|
|
33
|
-
cmd.resize "#{width == 0 ? nil : width}x#{height == 0 ? nil : height}>"
|
34
|
-
end
|
35
|
-
end
|
29
|
+
photo.combine_options { |cmd| cmd.resize "#{literal_dimensions(width, height)}>" }
|
36
30
|
|
37
31
|
store(photo, store_path)
|
38
32
|
end
|
@@ -42,36 +36,41 @@ module PhotoCook
|
|
42
36
|
# - the photo will be cropped if necessary
|
43
37
|
#
|
44
38
|
# https://github.com/carrierwaveuploader/carrierwave/blob/71cb18bba4a2078524d1ea683f267d3a97aa9bc8/lib/carrierwave/processing/mini_magick.rb#L176
|
45
|
-
def resize_to_fill(photo_path, width, height)
|
39
|
+
def resize_to_fill(photo_path, width, height, pixel_ratio)
|
46
40
|
|
47
41
|
# Do nothing if photo is not valid so exceptions will be not thrown
|
48
|
-
return unless (photo = open(photo_path)).
|
49
|
-
|
50
|
-
width, height
|
51
|
-
cols,
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
cmd.
|
42
|
+
return unless (photo = open(photo_path)) && photo.valid?
|
43
|
+
|
44
|
+
store_path = assemble_store_path(photo_path, width, height, pixel_ratio, true)
|
45
|
+
cols, rows = photo[:dimensions]
|
46
|
+
mwidth, mheight = multiply_dimensions(width, height, pixel_ratio)
|
47
|
+
|
48
|
+
# TODO
|
49
|
+
# Original dimensions are 1000x800. You want 640x640@1x. You will get 640x640
|
50
|
+
# Original dimensions are 1000x800. You want 640x640@2x. You will get 800x800
|
51
|
+
# Original dimensions are 1000x800. You want 640x640@3x. You will get 800x800
|
52
|
+
# Original dimensions are 1000x800. You want 1280x1280@1x. You will get ?
|
53
|
+
# Original dimensions are 1000x800. You want 1000x1280@1x. You will get ?
|
54
|
+
|
55
|
+
photo.combine_options do |cmd|
|
56
|
+
if width != cols || height != rows
|
57
|
+
scale_x = width / cols.to_f
|
58
|
+
scale_y = height / rows.to_f
|
59
|
+
if scale_x >= scale_y
|
60
|
+
cols = (scale_x * (cols + 0.5)).round
|
61
|
+
rows = (scale_x * (rows + 0.5)).round
|
62
|
+
cmd.resize "#{cols}>"
|
63
|
+
else
|
64
|
+
cols = (scale_y * (cols + 0.5)).round
|
65
|
+
rows = (scale_y * (rows + 0.5)).round
|
66
|
+
cmd.resize "x#{rows}>"
|
73
67
|
end
|
74
68
|
end
|
69
|
+
cmd.gravity CENTER_GRAVITY
|
70
|
+
cmd.background TRANSPARENT_BACKGROUND
|
71
|
+
if cols != width || rows != height
|
72
|
+
cmd.extent "#{literal_dimensions(width, height)}>"
|
73
|
+
end
|
75
74
|
end
|
76
75
|
|
77
76
|
store(photo, store_path)
|
@@ -91,22 +90,24 @@ module PhotoCook
|
|
91
90
|
end
|
92
91
|
|
93
92
|
def store(resized_photo, path_to_store_at)
|
94
|
-
dir = File.dirname
|
95
|
-
|
96
|
-
|
93
|
+
dir = File.dirname(path_to_store_at)
|
94
|
+
FileUtils.mkdir_p(dir) unless File.exists?(dir)
|
95
|
+
|
96
|
+
resized_photo.write(path_to_store_at)
|
97
97
|
resized_photo.resized_path = path_to_store_at
|
98
98
|
resized_photo
|
99
99
|
end
|
100
100
|
|
101
|
-
def
|
102
|
-
width
|
103
|
-
|
104
|
-
|
105
|
-
|
101
|
+
def literal_dimensions(width, height)
|
102
|
+
"#{width == 0 ? nil : width}x#{height == 0 ? nil : height}"
|
103
|
+
end
|
104
|
+
|
105
|
+
def assemble_store_path(path, width, height, pixel_ratio, crop)
|
106
|
+
PhotoCook.assemble_store_path(path, width, height, pixel_ratio, crop)
|
106
107
|
end
|
107
108
|
|
108
|
-
def
|
109
|
-
|
109
|
+
def multiply_dimensions(width, height, ratio)
|
110
|
+
[(width * ratio).round, (height * ratio).round]
|
110
111
|
end
|
111
112
|
end
|
112
113
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module PhotoCook
|
2
|
+
module Resizing
|
3
|
+
|
4
|
+
# Performs photo resizing:
|
5
|
+
# resize_photo('/application/public/uploads/car.png', 280, 280)
|
6
|
+
#
|
7
|
+
# NOTE: This method will perform validation
|
8
|
+
def resize_photo(path, width, height, options = {})
|
9
|
+
started = Time.now
|
10
|
+
|
11
|
+
width, height = parse_and_check_dimensions(width, height)
|
12
|
+
pixel_ratio, crop = open_options(options)
|
13
|
+
# Explicit # Default
|
14
|
+
pixel_ratio = unify_pixel_ratio(parse_and_check_pixel_ratio(pixel_ratio || 1))
|
15
|
+
photo = Resizer.instance.resize(path, width, height, pixel_ratio, !!crop)
|
16
|
+
|
17
|
+
finished = Time.now
|
18
|
+
log_resize(photo, width, height, pixel_ratio, !!crop, (finished - started) * 1000.0)
|
19
|
+
photo
|
20
|
+
end
|
21
|
+
|
22
|
+
# Builds URI for resizing:
|
23
|
+
# resize('/uploads/car.png', 280, 280, pixel_ratio: 2.5)
|
24
|
+
# => /resized/uploads/width:280&height:280&crop:0&pixel_ratio:3
|
25
|
+
#
|
26
|
+
# NOTE: This method will perform validation
|
27
|
+
def build_resize_uri(uri, width, height, options = {})
|
28
|
+
width, height = parse_and_check_dimensions(width, height)
|
29
|
+
pixel_ratio, crop = open_options(options)
|
30
|
+
# Explicit # From cookies # Default
|
31
|
+
pixel_ratio = parse_and_check_pixel_ratio(pixel_ratio || PhotoCook.client_pixel_ratio || 1)
|
32
|
+
assemble_uri(uri, width, height, unify_pixel_ratio(pixel_ratio), !!crop)
|
33
|
+
end
|
34
|
+
|
35
|
+
alias resize build_resize_uri
|
36
|
+
|
37
|
+
# Shorthand: hresize('/uploads/car.png', 280) <=> resize('/uploads/car.png', 280, nil)
|
38
|
+
def hresize(uri, width, options = {})
|
39
|
+
resize(uri, width, :auto, options)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Shorthand: vresize('/uploads/car.png', 280) <=> resize('/uploads/car.png', nil, 280)
|
43
|
+
def vresize(uri, height, options = {})
|
44
|
+
resize(uri, :auto, height, options)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def open_options(options)
|
49
|
+
case options
|
50
|
+
when Hash
|
51
|
+
[options[:pixel_ratio], options.fetch(:crop, false)]
|
52
|
+
when Symbol
|
53
|
+
[nil, options == :crop]
|
54
|
+
else
|
55
|
+
[nil, nil]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Do not produce various number of pixel ratios:
|
60
|
+
# 2.5 => 3
|
61
|
+
# 2.1 => 3
|
62
|
+
def unify_pixel_ratio(px_ratio)
|
63
|
+
px_ratio.ceil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
extend Resizing
|
68
|
+
end
|
data/lib/photo-cook/version.rb
CHANGED
data/lib/photo-cook.rb
CHANGED
@@ -1,41 +1,27 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
mattr_writer :resize_dir
|
5
|
-
|
6
|
-
def self.public_dir
|
7
|
-
@public_dir || 'public'
|
8
|
-
end
|
9
|
-
|
10
|
-
def self.resize_dir
|
11
|
-
@resize_dir || 'resized'
|
12
|
-
end
|
13
|
-
|
14
|
-
def self.resize(*args)
|
15
|
-
assemble_path(*args)
|
16
|
-
end
|
17
|
-
|
18
|
-
def self.assemble_path(path, width, height, crop = false)
|
19
|
-
File.join(assemble_dir(path), assemble_name(path, width, height, crop))
|
20
|
-
end
|
21
|
-
|
22
|
-
def self.assemble_dir(path)
|
23
|
-
File.join(File.dirname(path), resize_dir)
|
24
|
-
end
|
25
|
-
|
26
|
-
def self.assemble_name(path, width, height, crop = false)
|
27
|
-
File.basename(path, '.*') + assemble_command(width, height, crop) + File.extname(path)
|
28
|
-
end
|
1
|
+
require 'fileutils'
|
2
|
+
require 'rake'
|
3
|
+
require 'mini_magick'
|
29
4
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
prefix + (crop ? 'crop' : '')
|
5
|
+
module PhotoCook
|
6
|
+
def self.rails_env?
|
7
|
+
defined?(Rails)
|
34
8
|
end
|
35
9
|
end
|
36
10
|
|
37
|
-
require 'photo-cook/
|
11
|
+
require 'photo-cook/dimensions'
|
12
|
+
require 'photo-cook/pixel-ratio'
|
13
|
+
require 'photo-cook/paths'
|
14
|
+
require 'photo-cook/command'
|
15
|
+
require 'photo-cook/assemble'
|
16
|
+
require 'photo-cook/callbacks'
|
17
|
+
require 'photo-cook/logging'
|
38
18
|
require 'photo-cook/resizer'
|
39
19
|
require 'photo-cook/middleware'
|
40
|
-
require 'photo-cook/carrierwave'
|
41
20
|
require 'photo-cook/magick-photo'
|
21
|
+
require 'photo-cook/carrierwave'
|
22
|
+
require 'photo-cook/resizing'
|
23
|
+
|
24
|
+
if PhotoCook.rails_env?
|
25
|
+
require 'photo-cook/engine'
|
26
|
+
require 'photo-cook/pixel-ratio-spy'
|
27
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: photo-cook
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yaroslav Konoplov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-12-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -49,15 +49,25 @@ files:
|
|
49
49
|
- Gemfile
|
50
50
|
- Gemfile.lock
|
51
51
|
- Rakefile
|
52
|
+
- app/assets/javascripts/photo-cook.js
|
53
|
+
- app/assets/javascripts/photo-cook/photo-cook.js.erb
|
52
54
|
- lib/photo-cook.rb
|
55
|
+
- lib/photo-cook/assemble.rb
|
56
|
+
- lib/photo-cook/callbacks.rb
|
53
57
|
- lib/photo-cook/carrierwave.rb
|
58
|
+
- lib/photo-cook/command.rb
|
59
|
+
- lib/photo-cook/dimensions.rb
|
54
60
|
- lib/photo-cook/engine.rb
|
61
|
+
- lib/photo-cook/logging.rb
|
55
62
|
- lib/photo-cook/magick-photo.rb
|
56
63
|
- lib/photo-cook/middleware.rb
|
64
|
+
- lib/photo-cook/paths.rb
|
65
|
+
- lib/photo-cook/pixel-ratio-spy.rb
|
66
|
+
- lib/photo-cook/pixel-ratio.rb
|
57
67
|
- lib/photo-cook/resizer.rb
|
68
|
+
- lib/photo-cook/resizing.rb
|
58
69
|
- lib/photo-cook/version.rb
|
59
70
|
- photo-cook.gemspec
|
60
|
-
- vendor/assets/javascripts/photo-cook/photo-cook.js.erb
|
61
71
|
homepage: http://github.com/yivo/photo-cook
|
62
72
|
licenses:
|
63
73
|
- MIT
|
@@ -1,49 +0,0 @@
|
|
1
|
-
window.PhotoCook = {
|
2
|
-
resizeDir: function() {
|
3
|
-
return <%= PhotoCook.resize_dir.to_json %>;
|
4
|
-
},
|
5
|
-
|
6
|
-
pixelRatio: (function() {
|
7
|
-
var mediaQuery = '(-webkit-min-device-pixel-ratio: 1.5), (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (min-resolution: 1.5dppx)';
|
8
|
-
return function() {
|
9
|
-
var ratio = window.devicePixelRatio;
|
10
|
-
|
11
|
-
// If no ratio found check if screen is retina
|
12
|
-
// and if so return 2x ratio
|
13
|
-
if (ratio == null && typeof window.matchMedia === 'function') {
|
14
|
-
if (window.matchMedia(mediaQuery).matches) {
|
15
|
-
ratio = 2;
|
16
|
-
}
|
17
|
-
}
|
18
|
-
|
19
|
-
return ratio != null ? ratio : 1;
|
20
|
-
};
|
21
|
-
})()
|
22
|
-
};
|
23
|
-
|
24
|
-
window.PhotoCook.resize = function(path, width, height, crop) {
|
25
|
-
if (path == null) { return path; }
|
26
|
-
|
27
|
-
var ratio = PhotoCook.pixelRatio();
|
28
|
-
var command = '-' + (width ? Math.round(width * ratio) : '')
|
29
|
-
+ 'x'
|
30
|
-
+ (height ? Math.round(height * ratio) : '')
|
31
|
-
+ (crop ? 'crop' : '');
|
32
|
-
|
33
|
-
var directory = path.slice(0, Math.max(path.lastIndexOf('/'), 0));
|
34
|
-
|
35
|
-
var index = path.lastIndexOf('.');
|
36
|
-
var extension = index >= 0 ? path.slice(index) : '';
|
37
|
-
|
38
|
-
var basename = path.slice(
|
39
|
-
directory ? directory.length + 1 : 0,
|
40
|
-
extension ? 0 - extension.length : path.length
|
41
|
-
);
|
42
|
-
|
43
|
-
return (directory ? directory + '/' : directory)
|
44
|
-
+ PhotoCook.resizeDir()
|
45
|
-
+ '/'
|
46
|
-
+ basename
|
47
|
-
+ command
|
48
|
-
+ extension;
|
49
|
-
};
|