image_processing 0.9.0 → 0.10.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.

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