attachment_saver 1.0.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.
- 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
|