photo-cook 1.0.2 → 1.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 +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
|
-
};
|