attachment_saver 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/MIT-LICENSE +19 -0
- data/README +137 -0
- data/Rakefile +16 -0
- data/attachment_saver.gemspec +41 -0
- data/init.rb +1 -0
- data/lib/attachment_saver.rb +171 -0
- data/lib/attachment_saver/version.rb +3 -0
- data/lib/attachment_saver_errors.rb +3 -0
- data/lib/datastores/file_system.rb +189 -0
- data/lib/datastores/in_column.rb +49 -0
- data/lib/misc/extended_tempfile.rb +12 -0
- data/lib/misc/file_size.rb +5 -0
- data/lib/misc/image_science_extensions.rb +102 -0
- data/lib/misc/mini_magick_extensions.rb +89 -0
- data/lib/processors/image.rb +187 -0
- data/lib/processors/image_science.rb +94 -0
- data/lib/processors/mini_magick.rb +103 -0
- data/lib/processors/r_magick.rb +120 -0
- data/test/attachment_saver_test.rb +162 -0
- data/test/database.yml +3 -0
- data/test/file_system_datastore_test.rb +468 -0
- data/test/fixtures/broken.jpg +1 -0
- data/test/fixtures/emptyextension. +0 -0
- data/test/fixtures/noextension +0 -0
- data/test/fixtures/test.jpg +0 -0
- data/test/fixtures/test.js +1 -0
- data/test/fixtures/wrongextension.png +0 -0
- data/test/image_fixtures.rb +69 -0
- data/test/image_operations.rb +114 -0
- data/test/image_processor_test.rb +67 -0
- data/test/image_processor_test_common.rb +81 -0
- data/test/image_science_processor_test.rb +20 -0
- data/test/in_column_datastore_test.rb +115 -0
- data/test/mini_magick_processor_test.rb +20 -0
- data/test/model_test.rb +205 -0
- data/test/public/.empty +0 -0
- data/test/rmagick_processor_test.rb +20 -0
- data/test/schema.rb +41 -0
- data/test/test_helper.rb +49 -0
- metadata +223 -0
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'attachment_saver_errors'
|
2
|
+
|
3
|
+
class InColumnAttachmentDatastoreError < AttachmentDataStoreError; end
|
4
|
+
|
5
|
+
module AttachmentSaver
|
6
|
+
module DataStores
|
7
|
+
module InColumn
|
8
|
+
def self.included(base)
|
9
|
+
base.attachment_options[:column_name] ||= 'data'
|
10
|
+
base.attachment_options[:temp_directory] ||= Dir.tmpdir
|
11
|
+
end
|
12
|
+
|
13
|
+
def save_attachment
|
14
|
+
return unless @save_upload # this method is called every time the model is saved, not just when a new file has been uploaded
|
15
|
+
|
16
|
+
send("#{self.class.attachment_options[:column_name]}=", uploaded_data)
|
17
|
+
|
18
|
+
save_temporary_and_process_attachment if process_attachment?
|
19
|
+
|
20
|
+
@save_upload = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def tidy_attachment; end
|
24
|
+
def delete_attachment; end # delete_attachment is used when the record is deleted, so we don't need to do anything
|
25
|
+
|
26
|
+
def in_storage?
|
27
|
+
!send(self.class.attachment_options[:column_name]).nil?
|
28
|
+
end
|
29
|
+
|
30
|
+
# there is no public_path, since you need to make a controller to pull the blob from the database
|
31
|
+
|
32
|
+
def reprocess!
|
33
|
+
raise "this attachment already has a file open to process" unless uploaded_file.nil?
|
34
|
+
save_temporary_and_process_attachment
|
35
|
+
save!
|
36
|
+
end
|
37
|
+
|
38
|
+
def save_temporary_and_process_attachment
|
39
|
+
FileUtils.mkdir_p(self.class.attachment_options[:temp_directory])
|
40
|
+
Tempfile.open("asctemp", self.class.attachment_options[:temp_directory]) do |temp|
|
41
|
+
temp.binmode
|
42
|
+
temp.write(send(self.class.attachment_options[:column_name]))
|
43
|
+
temp.flush
|
44
|
+
process_attachment_with_wrapping(temp.path)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
class ExtendedTempfile < Tempfile
|
4
|
+
def initialize(basename, tmpdir = Dir.tmpdir, extension = '')
|
5
|
+
@extension = extension
|
6
|
+
super(basename, tmpdir)
|
7
|
+
end
|
8
|
+
|
9
|
+
def make_tmpname(basename, n)
|
10
|
+
sprintf('%s.%d.%d.%s', basename, $$, n || 0, @extension)
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'image_science'
|
2
|
+
|
3
|
+
class ImageScience
|
4
|
+
# TODO: submit a patch to image_science calling the built-in freeimage functions instead of using these lists
|
5
|
+
|
6
|
+
# from freeimage.h
|
7
|
+
FIF_UNKNOWN = -1;
|
8
|
+
FIF_BMP = 0;
|
9
|
+
FIF_ICO = 1;
|
10
|
+
FIF_JPEG = 2;
|
11
|
+
FIF_JNG = 3;
|
12
|
+
FIF_KOALA = 4;
|
13
|
+
FIF_LBM = 5;
|
14
|
+
FIF_IFF = FIF_LBM;
|
15
|
+
FIF_MNG = 6;
|
16
|
+
FIF_PBM = 7;
|
17
|
+
FIF_PBMRAW = 8;
|
18
|
+
FIF_PCD = 9;
|
19
|
+
FIF_PCX = 10;
|
20
|
+
FIF_PGM = 11;
|
21
|
+
FIF_PGMRAW = 12;
|
22
|
+
FIF_PNG = 13;
|
23
|
+
FIF_PPM = 14;
|
24
|
+
FIF_PPMRAW = 15;
|
25
|
+
FIF_RAS = 16;
|
26
|
+
FIF_TARGA = 17;
|
27
|
+
FIF_TIFF = 18;
|
28
|
+
FIF_WBMP = 19;
|
29
|
+
FIF_PSD = 20;
|
30
|
+
FIF_CUT = 21;
|
31
|
+
FIF_XBM = 22;
|
32
|
+
FIF_XPM = 23;
|
33
|
+
FIF_DDS = 24;
|
34
|
+
FIF_GIF = 25;
|
35
|
+
FIF_HDR = 26;
|
36
|
+
FIF_FAXG3 = 27;
|
37
|
+
FIF_SGI = 28;
|
38
|
+
|
39
|
+
FILE_TYPES = {
|
40
|
+
# from the documentation
|
41
|
+
FIF_BMP => 'BMP',
|
42
|
+
FIF_ICO => 'ICO',
|
43
|
+
FIF_JPEG => 'JPG',
|
44
|
+
FIF_JNG => 'JNG',
|
45
|
+
FIF_KOALA => 'KOA',
|
46
|
+
FIF_LBM => 'LBM',
|
47
|
+
FIF_MNG => 'MNG',
|
48
|
+
FIF_PBM => 'PBM',
|
49
|
+
FIF_PBMRAW => 'PBM',
|
50
|
+
FIF_PCD => 'PCD',
|
51
|
+
FIF_PCX => 'PCX',
|
52
|
+
FIF_PGM => 'PGM',
|
53
|
+
FIF_PGMRAW => 'PGM',
|
54
|
+
FIF_PNG => 'PNG',
|
55
|
+
FIF_PPM => 'PPM',
|
56
|
+
FIF_PPMRAW => 'PPM',
|
57
|
+
FIF_RAS => 'RAS',
|
58
|
+
FIF_TARGA => 'TGA',
|
59
|
+
FIF_TIFF => 'TIFF',
|
60
|
+
FIF_WBMP => 'WBMP',
|
61
|
+
FIF_PSD => 'PSD',
|
62
|
+
FIF_CUT => 'CUT',
|
63
|
+
FIF_XBM => 'XBM',
|
64
|
+
FIF_XPM => 'XPM',
|
65
|
+
FIF_DDS => 'DDS',
|
66
|
+
FIF_GIF => 'GIF',
|
67
|
+
FIF_HDR => 'HDR',
|
68
|
+
FIF_FAXG3 => 'G3',
|
69
|
+
FIF_SGI => 'SGI'
|
70
|
+
}
|
71
|
+
|
72
|
+
MIME_TYPES = {
|
73
|
+
# we only list the standardised MIME types here, leaving out any freeimage invented (ie. all the image/freeimage-* values)
|
74
|
+
FIF_BMP => 'image/bmp',
|
75
|
+
FIF_ICO => 'image/x-icon',
|
76
|
+
FIF_JPEG => 'image/jpeg',
|
77
|
+
FIF_MNG => 'video/x-mng',
|
78
|
+
FIF_PCD => 'image/x-photo-cd',
|
79
|
+
FIF_PCX => 'image/x-pcx',
|
80
|
+
FIF_PNG => 'image/png',
|
81
|
+
FIF_RAS => 'image/x-cmu-raster',
|
82
|
+
FIF_TIFF => 'image/tiff',
|
83
|
+
FIF_WBMP => 'image/vnd.wap.wbmp',
|
84
|
+
FIF_XBM => 'image/x-xbitmap',
|
85
|
+
FIF_XPM => 'image/xpm',
|
86
|
+
FIF_GIF => 'image/gif',
|
87
|
+
FIF_FAXG3 => 'image/fax-g3',
|
88
|
+
FIF_SGI => 'image/sgi'
|
89
|
+
}
|
90
|
+
|
91
|
+
# determines the file format of this image file (represented as an uppercase string).
|
92
|
+
# nil if the file type is not known, or if no image has been loaded.
|
93
|
+
def file_type
|
94
|
+
FILE_TYPES[@file_type]
|
95
|
+
end
|
96
|
+
|
97
|
+
# determines the MIME type of this image file.
|
98
|
+
# nil if the file type is not known, or if no image has been loaded.
|
99
|
+
def mime_type
|
100
|
+
MIME_TYPES[@file_type]
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'mini_magick'
|
2
|
+
|
3
|
+
class MiniMagick::Image
|
4
|
+
MIME_TYPES = {
|
5
|
+
'BMP' => 'image/bmp',
|
6
|
+
'CUR' => 'image/x-win-bitmap',
|
7
|
+
'DCX' => 'image/dcx',
|
8
|
+
'EPDF' => 'application/pdf',
|
9
|
+
'EPI' => 'application/postscript',
|
10
|
+
'EPS' => 'application/postscript',
|
11
|
+
'EPSF' => 'application/postscript',
|
12
|
+
'EPSI' => 'application/postscript',
|
13
|
+
'EPT' => 'application/postscript',
|
14
|
+
'EPT2' => 'application/postscript',
|
15
|
+
'EPT3' => 'application/postscript',
|
16
|
+
'FAX' => 'image/g3fax',
|
17
|
+
'FITS' => 'image/x-fits',
|
18
|
+
'G3' => 'image/g3fax',
|
19
|
+
'GIF' => 'image/gif',
|
20
|
+
'GIF87' => 'image/gif',
|
21
|
+
'ICB' => 'application/x-icb',
|
22
|
+
'ICO' => 'image/x-win-bitmap',
|
23
|
+
'ICON' => 'image/x-win-bitmap',
|
24
|
+
'JNG' => 'image/jng',
|
25
|
+
'JPEG' => 'image/jpeg',
|
26
|
+
'JPG' => 'image/jpeg',
|
27
|
+
'M2V' => 'video/mpeg2',
|
28
|
+
'MIFF' => 'application/x-mif',
|
29
|
+
'MNG' => 'video/mng',
|
30
|
+
'MPEG' => 'video/mpeg',
|
31
|
+
'MPG' => 'video/mpeg',
|
32
|
+
'OTB' => 'image/x-otb',
|
33
|
+
'PALM' => 'image/x-palm',
|
34
|
+
'PBM' => 'image/pbm',
|
35
|
+
'PCD' => 'image/pcd',
|
36
|
+
'PCDS' => 'image/pcd',
|
37
|
+
'PCL' => 'application/pcl',
|
38
|
+
'PCT' => 'image/pict',
|
39
|
+
'PCX' => 'image/x-pcx',
|
40
|
+
'PDB' => 'application/vnd.palm',
|
41
|
+
'PDF' => 'application/pdf',
|
42
|
+
'PGM' => 'image/x-pgm',
|
43
|
+
'PICON' => 'image/xpm',
|
44
|
+
'PICT' => 'image/pict',
|
45
|
+
'PJPEG' => 'image/pjpeg',
|
46
|
+
'PNG' => 'image/png',
|
47
|
+
'PNG24' => 'image/png',
|
48
|
+
'PNG32' => 'image/png',
|
49
|
+
'PNG8' => 'image/png',
|
50
|
+
'PNM' => 'image/pbm',
|
51
|
+
'PPM' => 'image/x-ppm',
|
52
|
+
'PS' => 'application/postscript',
|
53
|
+
'PSD' => 'image/x-photoshop',
|
54
|
+
'PTIF' => 'image/x-ptiff',
|
55
|
+
'RAS' => 'image/ras',
|
56
|
+
'SGI' => 'image/sgi',
|
57
|
+
'SUN' => 'image/ras',
|
58
|
+
'SVG' => 'image/svg+xml',
|
59
|
+
'SVGZ' => 'image/svg',
|
60
|
+
'TEXT' => 'text/plain',
|
61
|
+
'TGA' => 'image/tga',
|
62
|
+
'TIF' => 'image/tiff',
|
63
|
+
'TIFF' => 'image/tiff',
|
64
|
+
'TXT' => 'text/plain',
|
65
|
+
'VDA' => 'image/vda',
|
66
|
+
'VIFF' => 'image/x-viff',
|
67
|
+
'VST' => 'image/vst',
|
68
|
+
'WBMP' => 'image/vnd.wap.wbmp',
|
69
|
+
'XBM' => 'image/x-xbitmap',
|
70
|
+
'XPM' => 'image/x-xbitmap',
|
71
|
+
'XV' => 'image/x-viff',
|
72
|
+
'XWD' => 'image/xwd',
|
73
|
+
}
|
74
|
+
|
75
|
+
# determines the MIME type of this image file.
|
76
|
+
# nil if the file type is not known, or if no image has been loaded.
|
77
|
+
def mime_type
|
78
|
+
MIME_TYPES[format]
|
79
|
+
end
|
80
|
+
|
81
|
+
# creates an independent copy of the file.
|
82
|
+
def dup
|
83
|
+
if ::MiniMagick::Image.respond_to?(:read) # v3
|
84
|
+
self.class.read(to_blob)
|
85
|
+
else # v1
|
86
|
+
self.class.from_blob(to_blob, File.extname(path))
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'attachment_saver_errors'
|
2
|
+
|
3
|
+
class ImageProcessorError < AttachmentProcessorError; end
|
4
|
+
|
5
|
+
module AttachmentSaver
|
6
|
+
module Processors
|
7
|
+
# shared code for all image processors
|
8
|
+
module Image
|
9
|
+
def image?
|
10
|
+
return false if content_type.blank?
|
11
|
+
parts = content_type.split(/\//)
|
12
|
+
parts.size == 2 && parts.first.strip == 'image'
|
13
|
+
end
|
14
|
+
|
15
|
+
def before_validate_attachment
|
16
|
+
examine_attachment unless uploaded_file.nil? || derived_image?
|
17
|
+
rescue ImageProcessorError
|
18
|
+
# we examine all files, regardless of whether the client browser labelled them an
|
19
|
+
# image, because they may be an image with the wrong extension or content type.
|
20
|
+
# but this will raise for non-image files, so ignore such errors but make sure
|
21
|
+
# image? will return false, if it doesn't already.
|
22
|
+
self.content_type = "application/octet-stream" if image?
|
23
|
+
end
|
24
|
+
|
25
|
+
def process_attachment?
|
26
|
+
image? && !self.class.attachment_options[:formats].blank? && !derived_image?
|
27
|
+
end
|
28
|
+
|
29
|
+
# determines if this is a derived image. used to prevent infinite recursion when
|
30
|
+
# storing the derived images in the same model as the originals (and as a secondary
|
31
|
+
# benefit avoid unnecessary work examining images for derived images, for which the
|
32
|
+
# full metadata is already filled in by the resizing code).
|
33
|
+
def derived_image?
|
34
|
+
respond_to?(:format_name) && !format_name.blank?
|
35
|
+
end
|
36
|
+
|
37
|
+
# determines if a particular configured derived image should be created. this
|
38
|
+
# implementation, which always returns true, may be overridden by applications to make
|
39
|
+
# certain formats conditional (for example, only creating certain larger sizes if the
|
40
|
+
# original image was at least that large).
|
41
|
+
def want_format?(derived_name, derived_width, derived_height)
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
def process_attachment(filename)
|
46
|
+
with_image(filename) do |original_image|
|
47
|
+
unless self.class.attachment_options[:formats].blank?
|
48
|
+
old_children = new_record? ? {} : formats.group_by(&:format_name)
|
49
|
+
self.class.attachment_options[:formats].each do |derived_name, resize_format|
|
50
|
+
derived_attributes = process_image(original_image, derived_name, resize_format)
|
51
|
+
if derived_attributes
|
52
|
+
if old_children[derived_name.to_s]
|
53
|
+
update_derived(old_children[derived_name.to_s].pop, derived_attributes)
|
54
|
+
else
|
55
|
+
build_derived(derived_attributes)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
old_children = old_children.values.flatten
|
60
|
+
formats.destroy(old_children) unless old_children.blank? # remove any old derived images for formats for which want_format? now returned false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
rescue AttachmentSaverError
|
64
|
+
raise
|
65
|
+
rescue Exception => ex
|
66
|
+
raise ImageProcessorError, "#{ex.class}: #{ex.message}", ex.backtrace
|
67
|
+
end
|
68
|
+
|
69
|
+
# builds a new derived image model instance, but doesn't save it.
|
70
|
+
# provided so apps can easily override this step.
|
71
|
+
def build_derived(attributes)
|
72
|
+
formats.build(attributes)
|
73
|
+
end
|
74
|
+
|
75
|
+
# updates an existing derived image model instance, and queues it for save when this
|
76
|
+
# model is saved. provided so apps can easily override this step.
|
77
|
+
def update_derived(derived, attributes)
|
78
|
+
derived.attributes = attributes
|
79
|
+
@updated_derived_children ||= []
|
80
|
+
@updated_derived_children << derived # we don't want to save it just yet in case processing subsequent images fail; rails will automatically save it if we're a new record, but we have to do it ourselves in an after_save if not
|
81
|
+
derived
|
82
|
+
end
|
83
|
+
|
84
|
+
# unpacks a resize geometry string into an array contining the corresponding image
|
85
|
+
# operation method name (see Operations below, plus any from your chosen image processor)
|
86
|
+
# followed by the arguments to that method.
|
87
|
+
def self.from_geometry_string(geom)
|
88
|
+
match, w, cross, h, flag = geom.match(/^(\d+\.?\d*)?(?:([xX])(\d+\.?\d*)?)?([!%<>#*])?$/).to_a
|
89
|
+
raise "couldn't parse geometry string '#{geom}'" if match.nil? || (w.nil? && h.nil?)
|
90
|
+
h = w unless cross # there's <w>x<h>, there's <w>x, there's x<h>, and then there's just plain <n>, which means <w>=<h>=<n>
|
91
|
+
return [:scale_by, (w || h).to_f/100, (h || w).to_f/100] if flag == '%'
|
92
|
+
operation = case flag
|
93
|
+
when nil then :scale_to_fit
|
94
|
+
when '!' then w && h ? :squish : :scale_to_fit
|
95
|
+
when '>' then :shrink_to_fit
|
96
|
+
when '<' then :expand_to_fit
|
97
|
+
when '*' then :scale_to_cover
|
98
|
+
when '#' then :cover_and_crop
|
99
|
+
end
|
100
|
+
[operation, w ? w.to_i : nil, h ? h.to_i : nil]
|
101
|
+
end
|
102
|
+
|
103
|
+
module Operations
|
104
|
+
# if they choose to use this module to implement the resize operations, the processor
|
105
|
+
# module just needs to implement width, height, resize_to(new_width, new_height, &block),
|
106
|
+
# and crop_to(new_width, new_height, &block)
|
107
|
+
|
108
|
+
# squishes the image to the given width and height, without preserving the aspect ratio.
|
109
|
+
# yields this image itself if it is already the given size.
|
110
|
+
def squish(new_width, new_height, &block)
|
111
|
+
return block.call(self) if new_width == width && new_height == height
|
112
|
+
resize_to(new_width.to_i, new_height.to_i, &block)
|
113
|
+
end
|
114
|
+
|
115
|
+
# scales the image by the given factors.
|
116
|
+
def scale_by(width_factor, height_factor, &block)
|
117
|
+
squish(width*width_factor, height*height_factor, &block)
|
118
|
+
end
|
119
|
+
|
120
|
+
# calculates the appropriate dimensions for scale_to_fit.
|
121
|
+
def scale_dimensions_to_fit(new_width, new_height)
|
122
|
+
raise ArgumentError, "must supply the width and/or height" if new_width.nil? && new_height.nil?
|
123
|
+
if new_height.nil? || (!new_width.nil? && height*new_width < width*new_height)
|
124
|
+
return [new_width, height*new_width/width]
|
125
|
+
else
|
126
|
+
return [width*new_height/height, new_height]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# scales the image proportionately so that it fits within the given width and height
|
131
|
+
# (ie. one dimension will be equal to the given dimension, and the other dimension
|
132
|
+
# will be smaller than the given other dimension). either (but not both) of the new
|
133
|
+
# width & height may be nil, in which case the image will be scaled solely based on
|
134
|
+
# the other parameter. yields this image itself if it is already the appropriate size.
|
135
|
+
def scale_to_fit(new_width, new_height, &block)
|
136
|
+
squish(*scale_dimensions_to_fit(new_width, new_height), &block)
|
137
|
+
end
|
138
|
+
|
139
|
+
# keeps proportions, as for scale_to_fit, but only ever makes images smaller.
|
140
|
+
# yields this image itself if it is already within the given dimensions.
|
141
|
+
def shrink_to_fit(new_width, new_height, &block)
|
142
|
+
new_width, new_height = scale_dimensions_to_fit(new_width, new_height)
|
143
|
+
return block.call(self) if new_width >= width && new_height >= height
|
144
|
+
squish(new_width, new_height, &block)
|
145
|
+
end
|
146
|
+
|
147
|
+
# keeps proportions, as for scale_to_fit, but only ever makes images bigger.
|
148
|
+
# yields this image itself if it is already within the given dimensions or if the
|
149
|
+
# scaled dimensions would be smaller than the current dimensions.
|
150
|
+
# this is one of the operations specified by the *magick geometry strings, but
|
151
|
+
# IMHO it's not particularly useful as it doesn't establish any particularly helpful
|
152
|
+
# postconditions; consider whether scale_to_cover would be more appropriate.
|
153
|
+
def expand_to_fit(new_width, new_height, &block)
|
154
|
+
new_width, new_height = scale_dimensions_to_fit(new_width, new_height)
|
155
|
+
return block.call(self) if new_width <= width && new_height <= height
|
156
|
+
squish(new_width, new_height, &block)
|
157
|
+
end
|
158
|
+
|
159
|
+
# scales the image proportionately so that it fits over the given width and height (ie.
|
160
|
+
# one dimension will be equal to the given dimension, and the other dimension will be
|
161
|
+
# larger than the given other dimension). either (but not both) of the new width &
|
162
|
+
# height may be nil, in which case the image will be scaled solely based on the other
|
163
|
+
# parameter (in this case the result is the same as using scale_to_fit).
|
164
|
+
# yields this image itself if it is already the appropriate size.
|
165
|
+
def scale_to_cover(new_width, new_height, &block)
|
166
|
+
raise ArgumentError, "must supply the width and/or height" if new_width.nil? && new_height.nil?
|
167
|
+
if new_height.nil? || (!new_width.nil? && height*new_width > width*new_height)
|
168
|
+
squish(new_width, height*new_width/width, &block)
|
169
|
+
else
|
170
|
+
squish(width*new_height/height, new_height, &block)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
# haven't seen any reason to implement shrink_to_cover and expand_to_cover yet, but could.
|
174
|
+
|
175
|
+
# scales the image proportionately to fit over the given width and height (as for
|
176
|
+
# scale_to_cover), then crops the image to the given width & height.
|
177
|
+
# yields this image itself if it is already the appropriate size.
|
178
|
+
def cover_and_crop(new_width, new_height, &block)
|
179
|
+
scale_to_cover(new_width, new_height) do |scaled|
|
180
|
+
return block.call(scaled) if new_width == scaled.width && new_height == scaled.height
|
181
|
+
scaled.crop_to(new_width || width, new_height || height, &block)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|