image_processing 0.9.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of image_processing might be problematic. Click here for more details.

@@ -0,0 +1,119 @@
1
+ require "tempfile"
2
+ require "fileutils"
3
+
4
+ module ImageProcessing
5
+ module MiniMagick
6
+ module DeprecatedApi
7
+ def self.included(base)
8
+ base.extend(self)
9
+ end
10
+
11
+ def self.deprecated_processing_method(name, &body)
12
+ define_method(name) do |*args, &block|
13
+ return ImageProcessing::MiniMagick.send(name, *args, &block) if self != ImageProcessing::MiniMagick
14
+ return super(*args, &block) unless args.first.respond_to?(:read)
15
+
16
+ warn "[IMAGE_PROCESSING DEPRECATION WARNING] This API is deprecated and will be removed in ImageProcessing 1.0. Please use the new chainable API."
17
+
18
+ file = args.shift
19
+
20
+ if file.respond_to?(:path)
21
+ instance_exec(file, *args, block, &body)
22
+ else
23
+ Utils.copy_to_tempfile(file) do |tempfile|
24
+ send(name, tempfile, *args, &block)
25
+ end
26
+ end
27
+ end
28
+
29
+ define_method("#{name}!") do |*args, &block|
30
+ return ImageProcessing::MiniMagick.send(name, *args, &block) if self != ImageProcessing::MiniMagick
31
+ return super(*args, &block) unless args.first.respond_to?(:read)
32
+
33
+ processed = send(name, *args, &block)
34
+ source = args.first
35
+
36
+ if name == :convert
37
+ File.delete(source.path)
38
+ else
39
+ processed.close
40
+ FileUtils.mv processed.path, source.path
41
+ source.open if source.is_a?(Tempfile)
42
+ end
43
+
44
+ source
45
+ end
46
+ end
47
+
48
+ deprecated_processing_method :resize_to_limit do |file, *args, block|
49
+ source(file)
50
+ .custom(&block)
51
+ .resize_to_limit!(*args)
52
+ end
53
+
54
+ deprecated_processing_method :resize_to_fit do |file, *args, block|
55
+ source(file)
56
+ .custom(&block)
57
+ .resize_to_fit!(*args)
58
+ end
59
+
60
+ deprecated_processing_method :resize_to_fill do |file, *args, block|
61
+ source(file)
62
+ .custom(&block)
63
+ .resize_to_fill!(*args)
64
+ end
65
+
66
+ deprecated_processing_method :resize_and_pad do |file, *args, block|
67
+ source(file)
68
+ .custom(&block)
69
+ .resize_and_pad!(*args)
70
+ end
71
+
72
+ deprecated_processing_method :convert do |file, format, page = nil, block|
73
+ source(file, page: page)
74
+ .custom(&block)
75
+ .convert!(format)
76
+ end
77
+
78
+ deprecated_processing_method :auto_orient do |file, *args, block|
79
+ source(file)
80
+ .custom(&block)
81
+ .auto_orient!(*args)
82
+ end
83
+
84
+ deprecated_processing_method :resample do |file, width, height, block|
85
+ source(file)
86
+ .custom(&block)
87
+ .resample!("#{width}x#{height}")
88
+ end
89
+
90
+ deprecated_processing_method :crop do |file, width, height, x_offset, y_offset, block|
91
+ source(file)
92
+ .custom(&block)
93
+ .crop!("#{width}x#{height}+#{x_offset}+#{y_offset}")
94
+ end
95
+
96
+ deprecated_processing_method :corrupted? do |file, block|
97
+ valid_image?(file)
98
+ end
99
+
100
+ module Utils
101
+ module_function
102
+
103
+ def copy_to_tempfile(io)
104
+ extension = File.extname(io.path) if io.respond_to?(:path)
105
+ tempfile = Tempfile.new(["image_processing", extension.to_s], binmode: true)
106
+
107
+ IO.copy_stream(io, tempfile)
108
+
109
+ io.rewind
110
+ tempfile.open # refresh content
111
+
112
+ yield tempfile
113
+ ensure
114
+ tempfile.close! if tempfile
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -1,236 +1,90 @@
1
- require "image_processing/version"
1
+ gem "mini_magick", "~> 4.0"
2
2
  require "mini_magick"
3
- require "tempfile"
4
3
 
5
- if !MiniMagick.respond_to?(:version) || MiniMagick.version < Gem::Version.new("4.3.5")
6
- raise "image_processing requires mini_magick version >= 4.3.5"
7
- end
4
+ require "image_processing"
5
+ require "image_processing/mini_magick/deprecated_api"
8
6
 
9
7
  module ImageProcessing
10
8
  module MiniMagick
11
- def self.nondestructive_alias(name, original)
12
- define_method(name) do |image, *args, &block|
13
- send(original, _copy_to_tempfile(image), *args, &block)
9
+ def self.valid_image?(file)
10
+ ::MiniMagick::Tool::Convert.new do |convert|
11
+ convert.regard_warnings
12
+ convert << file.path
13
+ convert << "null:"
14
14
  end
15
- module_function name
15
+ true
16
+ rescue ::MiniMagick::Error
17
+ false
16
18
  end
17
19
 
18
- module_function
19
-
20
- # Changes the image encoding format to the given format
21
- #
22
- # @see http://www.imagemagick.org/script/command-line-options.php#format
23
- # @param [MiniMagick::Image] image the image to convert
24
- # @param [String] format the format to convert to
25
- # @yield [MiniMagick::Tool::Mogrify, MiniMagick::Tool::Convert]
26
- # @return [File, Tempfile]
27
- def convert!(image, format, page = nil, &block)
28
- with_minimagick(image) do |img|
29
- img.format(format.downcase, page, &block)
30
- end
31
- end
32
- nondestructive_alias :convert, :convert!
33
-
34
- # Adjusts the image so that its orientation is suitable for viewing.
35
- #
36
- # @see http://www.imagemagick.org/script/command-line-options.php#auto-orient
37
- # @param [MiniMagick::Image] image the image to convert
38
- # @yield [MiniMagick::Tool::Mogrify, MiniMagick::Tool::Convert]
39
- # @return [File, Tempfile]
40
- def auto_orient!(image)
41
- with_minimagick(image) do |img|
42
- img.combine_options do |cmd|
43
- yield cmd if block_given?
44
- cmd.auto_orient
20
+ class Processor
21
+ IMAGE_CLASS = ::MiniMagick::Tool
22
+ TRANSPARENT = "rgba(255,255,255,0.0)"
23
+
24
+ def apply_operation(name, magick, *args)
25
+ if respond_to?(name)
26
+ public_send(name, magick, *args)
27
+ else
28
+ magick.send(name, *args)
45
29
  end
46
30
  end
47
- end
48
- nondestructive_alias :auto_orient, :auto_orient!
49
-
50
- # Resize the image to fit within the specified dimensions while retaining
51
- # the original aspect ratio. Will only resize the image if it is larger
52
- # than the specified dimensions. The resulting image may be shorter or
53
- # narrower than specified in either dimension but will not be larger than
54
- # the specified values.
55
- #
56
- # @param [MiniMagick::Image] image the image to convert
57
- # @param [#to_s] width the maximum width
58
- # @param [#to_s] height the maximum height
59
- # @yield [MiniMagick::Tool::Mogrify, MiniMagick::Tool::Convert]
60
- # @return [File, Tempfile]
61
- def resize_to_limit!(image, width, height)
62
- with_minimagick(image) do |img|
63
- img.combine_options do |cmd|
64
- yield cmd if block_given?
65
- cmd.resize "#{width}x#{height}>"
66
- end
31
+
32
+ def resize_to_limit(magick, width, height)
33
+ magick.resize "#{width}x#{height}>"
67
34
  end
68
- end
69
- nondestructive_alias :resize_to_limit, :resize_to_limit!
70
-
71
- # Resize the image to fit within the specified dimensions while retaining
72
- # the original aspect ratio. The image may be shorter or narrower than
73
- # specified in the smaller dimension but will not be larger than the
74
- # specified values.
75
- #
76
- # @param [MiniMagick::Image] image the image to convert
77
- # @param [#to_s] width the width to fit into
78
- # @param [#to_s] height the height to fit into
79
- # @yield [MiniMagick::Tool::Mogrify, MiniMagick::Tool::Convert]
80
- # @return [File, Tempfile]
81
- def resize_to_fit!(image, width, height)
82
- with_minimagick(image) do |img|
83
- img.combine_options do |cmd|
84
- yield cmd if block_given?
85
- cmd.resize "#{width}x#{height}"
86
- end
35
+
36
+ def resize_to_fit(magick, width, height)
37
+ magick.resize "#{width}x#{height}"
87
38
  end
88
- end
89
- nondestructive_alias :resize_to_fit, :resize_to_fit!
90
-
91
- # Resize the image so that it is at least as large in both dimensions as
92
- # specified, then crops any excess outside the specified dimensions.
93
- #
94
- # The resulting image will always be exactly as large as the specified
95
- # dimensions.
96
- #
97
- # By default, the center part of the image is kept, and the remainder
98
- # cropped off, but this can be changed via the `gravity` option.
99
- #
100
- # @param [MiniMagick::Image] image the image to convert
101
- # @param [#to_s] width the width to fill out
102
- # @param [#to_s] height the height to fill out
103
- # @param [String] gravity which part of the image to focus on
104
- # @yield [MiniMagick::Tool::Mogrify, MiniMagick::Tool::Convert]
105
- # @return [File, Tempfile]
106
- # @see http://www.imagemagick.org/script/command-line-options.php#gravity
107
- def resize_to_fill!(image, width, height, gravity: "Center")
108
- with_minimagick(image) do |img|
109
- img.combine_options do |cmd|
110
- yield cmd if block_given?
111
- cmd.resize "#{width}x#{height}^"
112
- cmd.gravity gravity
113
- cmd.background "rgba(255,255,255,0.0)" # transparent
114
- cmd.extent "#{width}x#{height}"
115
- end
39
+
40
+ def resize_to_fill(magick, width, height, gravity: "Center")
41
+ magick.resize "#{width}x#{height}^"
42
+ magick.gravity gravity
43
+ magick.background TRANSPARENT
44
+ magick.extent "#{width}x#{height}"
116
45
  end
117
- end
118
- nondestructive_alias :resize_to_fill, :resize_to_fill!
119
-
120
- # Resize the image to fit within the specified dimensions while retaining
121
- # the original aspect ratio in the same way as {#fill}. Unlike {#fill} it
122
- # will, if necessary, pad the remaining area with the given color, which
123
- # defaults to transparent where supported by the image format and white
124
- # otherwise.
125
- #
126
- # The resulting image will always be exactly as large as the specified
127
- # dimensions.
128
- #
129
- # By default, the image will be placed in the center but this can be
130
- # changed via the `gravity` option.
131
- #
132
- # @param [MiniMagick::image] image the image to convert
133
- # @param [#to_s] width the width to fill out
134
- # @param [#to_s] height the height to fill out
135
- # @param [string] background the color to use as a background
136
- # @param [string] gravity which part of the image to focus on
137
- # @yield [MiniMagick::Tool::Mogrify, MiniMagick::Tool::Convert]
138
- # @return [File, Tempfile]
139
- # @see http://www.imagemagick.org/script/color.php
140
- # @see http://www.imagemagick.org/script/command-line-options.php#gravity
141
- def resize_and_pad!(image, width, height, background: "transparent", gravity: "Center")
142
- with_minimagick(image) do |img|
143
- img.combine_options do |cmd|
144
- yield cmd if block_given?
145
- cmd.resize "#{width}x#{height}"
146
- if background == "transparent"
147
- cmd.background "rgba(255, 255, 255, 0.0)"
148
- else
149
- cmd.background background
150
- end
151
- cmd.gravity gravity
152
- cmd.extent "#{width}x#{height}"
153
- end
46
+
47
+ def resize_and_pad(magick, width, height, background: TRANSPARENT, gravity: "Center")
48
+ background = TRANSPARENT if background == "transparent"
49
+
50
+ magick.resize "#{width}x#{height}"
51
+ magick.background background
52
+ magick.gravity gravity
53
+ magick.extent "#{width}x#{height}"
154
54
  end
155
- end
156
- nondestructive_alias :resize_and_pad, :resize_and_pad!
157
-
158
- # Resample the image to fit within the specified resolution while retaining
159
- # the original image size.
160
- #
161
- # The resulting image will always be the same pixel size as the source with
162
- # an adjusted resolution dimensions.
163
- #
164
- # @param [MiniMagick::Image] image the image to convert
165
- # @param [#to_s] width the dpi width
166
- # @param [#to_s] height the dpi height
167
- # @yield [MiniMagick::Tool::Mogrify, MiniMagick::Tool::Convert]
168
- # @return [File, Tempfile]
169
- # @see http://www.imagemagick.org/script/command-line-options.php#resample
170
- def resample!(image, width, height)
171
- with_minimagick(image) do |img|
172
- img.combine_options do |cmd|
173
- yield cmd if block_given?
174
- cmd.resample "#{width}x#{height}"
175
- end
55
+
56
+ def append(magick, *args)
57
+ magick.merge! args
176
58
  end
177
- end
178
- nondestructive_alias :resample, :resample!
179
-
180
- # Crops the image to be the defined area.
181
- #
182
- # @param [#to_s] width the width of the cropped image
183
- # @param [#to_s] height the height of the cropped image
184
- # @param [#to_s] x_offset the x coordinate where to start cropping
185
- # @param [#to_s] y_offset the y coordinate where to start cropping
186
- # @param [string] gravity which part of the image to focus on
187
- # @yield [MiniMagick::Tool::Mogrify, MiniMagick::Tool::Convert]
188
- # @return [File, Tempfile]
189
- # @see http://www.imagemagick.org/script/command-line-options.php#crop
190
- def crop!(image, width, height, x_offset = 0, y_offset = 0, gravity: "NorthWest")
191
- with_minimagick(image) do |img|
192
- img.combine_options do |cmd|
193
- yield cmd if block_given?
194
- cmd.gravity gravity
195
- cmd.crop "#{width}x#{height}+#{x_offset}+#{y_offset}"
59
+
60
+ def load_image(path_or_magick, page: nil, geometry: nil, fail: true, auto_orient: true)
61
+ if path_or_magick.is_a?(::MiniMagick::Tool)
62
+ magick = path_or_magick
63
+ else
64
+ source_path = path_or_magick
65
+ magick = ::MiniMagick::Tool::Convert.new
66
+
67
+ input_path = source_path
68
+ input_path += "[#{page}]" if page
69
+ input_path += "[#{geometry}]" if geometry
70
+
71
+ magick << input_path
196
72
  end
73
+
74
+ magick.regard_warnings if fail
75
+ magick.auto_orient if auto_orient
76
+
77
+ magick
197
78
  end
198
- end
199
- nondestructive_alias :crop, :crop!
200
-
201
- # Returns whether the image is corrupt.
202
- #
203
- # @param [File] image
204
- # @return [Boolean]
205
- def corrupted?(image)
206
- ::MiniMagick::Tool::Identify.new do |identify|
207
- identify.verbose
208
- identify.regard_warnings
209
- identify << image.path
79
+
80
+ def save_image(magick, destination_path, **options)
81
+ magick << destination_path
82
+ magick.call
210
83
  end
211
- false
212
- rescue ::MiniMagick::Error
213
- true
214
84
  end
215
85
 
216
- # Convert an image into a MiniMagick::Image for the duration of the block,
217
- # and at the end return a File object.
218
- def with_minimagick(image)
219
- image = ::MiniMagick::Image.new(image.path, image)
220
- yield image
221
- tempfile = image.instance_variable_get("@tempfile")
222
- tempfile.open if tempfile.is_a?(Tempfile)
223
- tempfile
224
- end
86
+ extend Chainable
225
87
 
226
- # Creates a copy of the file and stores it into a Tempfile. Works for any
227
- # IO object that responds to `#read(length = nil, outbuf = nil)`.
228
- def _copy_to_tempfile(file)
229
- extension = File.extname(file.path) if file.respond_to?(:path)
230
- tempfile = Tempfile.new(["image_processing-mini_magick", extension.to_s], binmode: true)
231
- IO.copy_stream(file, tempfile.path)
232
- file.rewind
233
- tempfile
234
- end
88
+ include DeprecatedApi
235
89
  end
236
90
  end
@@ -0,0 +1,59 @@
1
+ require "tempfile"
2
+
3
+ module ImageProcessing
4
+ class Pipeline
5
+ include Chainable
6
+
7
+ def initialize(options)
8
+ @default_options = options
9
+ end
10
+
11
+ def call!(save: true, destination: nil)
12
+ fail Error, "source file is not provided" unless default_options[:source]
13
+
14
+ image_class = default_options[:processor]::IMAGE_CLASS
15
+
16
+ if default_options[:source].is_a?(image_class)
17
+ source = default_options[:source]
18
+ elsif default_options[:source].is_a?(String)
19
+ source = default_options[:source]
20
+ elsif default_options[:source].respond_to?(:path)
21
+ source = default_options[:source].path
22
+ elsif default_options[:source].respond_to?(:to_path)
23
+ source = default_options[:source].to_path
24
+ else
25
+ fail Error, "source file needs to respond to #path, or be a String, a Pathname, or a #{image_class} object"
26
+ end
27
+
28
+ processor = default_options[:processor].new
29
+ image = processor.load_image(source, default_options[:loader])
30
+
31
+ default_options[:operations].each do |name, args|
32
+ if name == :custom
33
+ image = args.first.call(image) || image
34
+ else
35
+ image = processor.apply_operation(name, image, *args)
36
+ end
37
+ end
38
+
39
+ return image unless save
40
+
41
+ return processor.save_image(image, destination, default_options[:saver]) if destination
42
+
43
+ source_path = source if source.is_a?(String)
44
+ format = default_options[:format] || File.extname(source_path.to_s)[1..-1] || "jpg"
45
+
46
+ result = Tempfile.new(["image_processing", ".#{format}"], binmode: true)
47
+
48
+ begin
49
+ processor.save_image(image, result.path, default_options[:saver])
50
+ rescue
51
+ result.close!
52
+ raise
53
+ end
54
+
55
+ result.open
56
+ result
57
+ end
58
+ end
59
+ end
@@ -1,3 +1,3 @@
1
1
  module ImageProcessing
2
- VERSION = "0.9.0"
2
+ VERSION = "0.10.0"
3
3
  end
@@ -1,12 +1,98 @@
1
- require "image_processing/vips/chainable"
2
- require "image_processing/vips/pipeline"
3
- require "image_processing/vips/processor"
1
+ gem "ruby-vips", "~> 2.0"
2
+ require "vips"
4
3
 
5
- require "image_processing/version"
4
+ require "image_processing"
5
+
6
+ fail "image_processing/vips requires libvips 8.6+" unless Vips.at_least_libvips?(8, 6)
6
7
 
7
8
  module ImageProcessing
8
9
  module Vips
9
- Error = Class.new(StandardError)
10
+ def self.valid_image?(file)
11
+ ::Vips::Image.new_from_file(file.path, access: :sequential, fail: true).avg
12
+ true
13
+ rescue ::Vips::Error
14
+ false
15
+ end
16
+
17
+ class Processor
18
+ IMAGE_CLASS = ::Vips::Image
19
+ # libvips has this arbitrary number as a sanity-check upper bound on image
20
+ # size.
21
+ MAX_COORD = 10_000_000
22
+
23
+ def apply_operation(name, image, *args)
24
+ if respond_to?(name)
25
+ public_send(name, image, *args)
26
+ else
27
+ result = image.send(name, *args)
28
+ result.is_a?(::Vips::Image) ? result : image
29
+ end
30
+ end
31
+
32
+ def resize_to_limit(image, width, height, **options)
33
+ width, height = default_dimensions(width, height)
34
+ image.thumbnail_image(width, height: height, size: :down, **options)
35
+ end
36
+
37
+ def resize_to_fit(image, width, height, **options)
38
+ width, height = default_dimensions(width, height)
39
+ image.thumbnail_image(width, height: height, **options)
40
+ end
41
+
42
+ def resize_to_fill(image, width, height, **options)
43
+ image.thumbnail_image(width, height: height, crop: :centre, **options)
44
+ end
45
+
46
+ def resize_and_pad(image, width, height, gravity: "centre", extend: nil, background: nil, alpha: nil, **options)
47
+ embed_options = { extend: extend, background: background }
48
+ embed_options.reject! { |name, value| value.nil? }
49
+
50
+ image = image.thumbnail_image(width, height: height, **options)
51
+ image = image.bandjoin(255) if alpha && image.bands == 3
52
+ image.gravity(gravity, width, height, **embed_options)
53
+ end
54
+
55
+ def load_image(path_or_image, autorot: true, **options)
56
+ if path_or_image.is_a?(::Vips::Image)
57
+ image = path_or_image
58
+ else
59
+ source_path = path_or_image
60
+ loader = ::Vips.vips_foreign_find_load(source_path)
61
+ options = select_valid_options(loader, options) if loader
62
+
63
+ image = ::Vips::Image.new_from_file(source_path, fail: true, **options)
64
+ end
65
+
66
+ image = image.autorot if autorot
67
+ image
68
+ end
69
+
70
+ def save_image(image, destination_path, **options)
71
+ saver = ::Vips.vips_foreign_find_save(destination_path)
72
+ options = select_valid_options(saver, options) if saver
73
+
74
+ image.write_to_file(destination_path, **options)
75
+ end
76
+
77
+ private
78
+
79
+ def default_dimensions(width, height)
80
+ raise Error, "either width or height must be specified" unless width || height
81
+
82
+ [width || MAX_COORD, height || MAX_COORD]
83
+ end
84
+
85
+ def select_valid_options(operation_name, options)
86
+ operation = ::Vips::Operation.new(operation_name)
87
+
88
+ operation_options = operation.get_construct_args
89
+ .select { |name, flags| (flags & ::Vips::ARGUMENT_INPUT) != 0 }
90
+ .select { |name, flags| (flags & ::Vips::ARGUMENT_REQUIRED) == 0 }
91
+ .map(&:first).map(&:to_sym)
92
+
93
+ options.select { |name, value| operation_options.include?(name) }
94
+ end
95
+ end
10
96
 
11
97
  extend Chainable
12
98
  end
@@ -1,4 +1,10 @@
1
+ require "image_processing/chainable"
2
+ require "image_processing/pipeline"
3
+ require "image_processing/version"
4
+
1
5
  module ImageProcessing
6
+ Error = Class.new(StandardError)
7
+
2
8
  autoload :MiniMagick, 'image_processing/mini_magick'
3
9
  autoload :Vips, 'image_processing/vips'
4
10
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: image_processing
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janko Marohnić
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-03-16 00:00:00.000000000 Z
11
+ date: 2018-03-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -106,13 +106,12 @@ files:
106
106
  - README.md
107
107
  - image_processing.gemspec
108
108
  - lib/image_processing.rb
109
+ - lib/image_processing/chainable.rb
109
110
  - lib/image_processing/mini_magick.rb
111
+ - lib/image_processing/mini_magick/deprecated_api.rb
112
+ - lib/image_processing/pipeline.rb
110
113
  - lib/image_processing/version.rb
111
114
  - lib/image_processing/vips.rb
112
- - lib/image_processing/vips/chainable.rb
113
- - lib/image_processing/vips/color.rb
114
- - lib/image_processing/vips/pipeline.rb
115
- - lib/image_processing/vips/processor.rb
116
115
  homepage: https://github.com/janko-m/image_processing
117
116
  licenses:
118
117
  - MIT