image_processing 1.6.0 → 1.7.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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c73cfe4047169c0385afc59779c47cc43385b8c02a82e540d8ad53adbd32aa41
4
- data.tar.gz: 6ae50e569a9bad8b408c7f17a2eaded27e939be43c8b72f956d19875e2de5116
3
+ metadata.gz: 8ebf3b3c8fc4a4729473e346f5caddb1cc56fd971436ead2842185aa900c33b5
4
+ data.tar.gz: 9aaa8ae0ffbecbb0d3badd05835ca1d04c800bd0da4dfdec0beb11ea895fe003
5
5
  SHA512:
6
- metadata.gz: deae9e4f3c13765f296aaf3801d02f55756518e077c38e1e6df89fc6c478fda48de41866292326684be06aeac3dedcd4fd1bd4688b09cc2b0ba858d6034e6416
7
- data.tar.gz: 152da0d976a408e8c2dbf66e5054df86e22ae8006381833e4d0c845f7c643acb55355efeb30f910ca32c0b19fb3171503f92fcef240d26728151a0a8de68ec54
6
+ metadata.gz: e223f370b545ff3181877d416565e3e5e67ce9bc4478b0b94a26434a2086cb76f1f07f9099e27b01d7b4c46a90c457e39409b146b5839fd953b1d08f694acae6
7
+ data.tar.gz: a0e9c3db88a40607128d90a4f37e8c5b5ba5e56064a7cc8c38f6ee13bb585f6de0d13b40177f5af20434e6b1ab37b5dbe22ca96d22cb4b98266878673d3c9e4b
@@ -1,4 +1,8 @@
1
- ## HEAD
1
+ ## 1.7.0 (2018-09-20)
2
+
3
+ * [vips] `#rotate` now always calls `vips_similarity()` and forwards all options to it (@janko-m)
4
+
5
+ ## 1.6.0 (2018-07-13)
2
6
 
3
7
  * [vips] In `#composite` accept `:offset` option for the position of the overlay image (@janko-m)
4
8
 
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.require_paths = ["lib"]
18
18
 
19
19
  spec.add_dependency "mini_magick", "~> 4.0"
20
- spec.add_dependency "ruby-vips", ">= 2.0.11", "< 3"
20
+ spec.add_dependency "ruby-vips", ">= 2.0.13", "< 3"
21
21
 
22
22
  spec.add_development_dependency "rake"
23
23
  spec.add_development_dependency "minitest", "~> 5.8"
@@ -8,6 +8,7 @@ module ImageProcessing
8
8
  @options = options
9
9
  end
10
10
 
11
+ # Calls the pipeline to perform the processing from built options.
11
12
  def call!(**options)
12
13
  Pipeline.new(self.options).call(**options)
13
14
  end
@@ -1,21 +1,31 @@
1
1
  module ImageProcessing
2
+ # Implements a chainable interface for building processing options.
2
3
  module Chainable
4
+ # Specify the source image file.
3
5
  def source(file)
4
6
  branch source: file
5
7
  end
6
8
 
9
+ # Specify the output format.
7
10
  def convert(format)
8
11
  branch format: format
9
12
  end
10
13
 
14
+ # Specify processor options applied when loading the image.
11
15
  def loader(**options)
12
16
  branch loader: options
13
17
  end
14
18
 
19
+ # Specify processor options applied when saving the image.
15
20
  def saver(**options)
16
21
  branch saver: options
17
22
  end
18
23
 
24
+ # Add multiple operations as a hash or an array.
25
+ #
26
+ # .apply(resize_to_limit: [400, 400], strip: true)
27
+ # # or
28
+ # .apply([[:resize_to_limit, [400, 400]], [:strip, true])
19
29
  def apply(operations)
20
30
  operations.inject(self) do |builder, (name, argument)|
21
31
  if argument == true || argument == nil
@@ -28,6 +38,8 @@ module ImageProcessing
28
38
  end
29
39
  end
30
40
 
41
+ # Assume that any unknown method names an operation supported by the
42
+ # processor. Add a bang ("!") if you want processing to be performed.
31
43
  def method_missing(name, *args, &block)
32
44
  return super if name.to_s.end_with?("?")
33
45
  return send(name.to_s.chomp("!"), *args, &block).call if name.to_s.end_with?("!")
@@ -35,10 +47,13 @@ module ImageProcessing
35
47
  operation(name, *args, &block)
36
48
  end
37
49
 
50
+ # Add an operation defined by the processor.
38
51
  def operation(name, *args, &block)
39
52
  branch operations: [[name, args, *block]]
40
53
  end
41
54
 
55
+ # Call the defined processing and get the result. Allows specifying
56
+ # the source file and destination.
42
57
  def call(file = nil, destination: nil, **call_options)
43
58
  options = {}
44
59
  options = options.merge(source: file) if file
@@ -47,6 +62,7 @@ module ImageProcessing
47
62
  branch(options).call!(**call_options)
48
63
  end
49
64
 
65
+ # Creates a new builder object, merging current options with new options.
50
66
  def branch(loader: nil, saver: nil, operations: nil, **other_options)
51
67
  options = respond_to?(:options) ? self.options : DEFAULT_OPTIONS
52
68
 
@@ -61,6 +77,7 @@ module ImageProcessing
61
77
  Builder.new(options)
62
78
  end
63
79
 
80
+ # Empty options which the builder starts with.
64
81
  DEFAULT_OPTIONS = {
65
82
  source: nil,
66
83
  loader: {},
@@ -5,6 +5,7 @@ module ImageProcessing
5
5
  module MiniMagick
6
6
  extend Chainable
7
7
 
8
+ # Returns whether the given image file is processable.
8
9
  def self.valid_image?(file)
9
10
  ::MiniMagick::Tool::Convert.new do |convert|
10
11
  convert << file.path
@@ -18,8 +19,12 @@ module ImageProcessing
18
19
  class Processor < ImageProcessing::Processor
19
20
  accumulator :magick, ::MiniMagick::Tool
20
21
 
22
+ # Default sharpening parameters used on generated thumbnails.
21
23
  SHARPEN_PARAMETERS = { radius: 0, sigma: 1 }
22
24
 
25
+ # Initializes the image on disk into a MiniMagick::Tool object. Accepts
26
+ # additional options related to loading the image (e.g. geometry).
27
+ # Additionally auto-orients the image to be upright.
23
28
  def self.load_image(path_or_magick, page: nil, geometry: nil, auto_orient: true, **options)
24
29
  if path_or_magick.is_a?(::MiniMagick::Tool)
25
30
  magick = path_or_magick
@@ -40,6 +45,9 @@ module ImageProcessing
40
45
  magick
41
46
  end
42
47
 
48
+ # Calls the built ImageMagick command to perform processing and save
49
+ # the result to disk. Accepts additional options related to saving the
50
+ # image (e.g. quality).
43
51
  def self.save_image(magick, destination_path, allow_splitting: false, **options)
44
52
  Utils.apply_options(magick, options)
45
53
 
@@ -49,14 +57,18 @@ module ImageProcessing
49
57
  Utils.disallow_split_layers!(destination_path) unless allow_splitting
50
58
  end
51
59
 
60
+ # Resizes the image to not be larger than the specified dimensions.
52
61
  def resize_to_limit(width, height, **options)
53
62
  thumbnail("#{width}x#{height}>", **options)
54
63
  end
55
64
 
65
+ # Resizes the image to fit within the specified dimensions.
56
66
  def resize_to_fit(width, height, **options)
57
67
  thumbnail("#{width}x#{height}", **options)
58
68
  end
59
69
 
70
+ # Resizes the image to fill the specified dimensions, applying any
71
+ # necessary cropping.
60
72
  def resize_to_fill(width, height, gravity: "Center", **options)
61
73
  thumbnail("#{width}x#{height}^", **options)
62
74
  magick.gravity gravity
@@ -64,6 +76,8 @@ module ImageProcessing
64
76
  magick.extent "#{width}x#{height}"
65
77
  end
66
78
 
79
+ # Resizes the image to fit within the specified dimensions and fills
80
+ # the remaining area with the specified background color.
67
81
  def resize_and_pad(width, height, background: :transparent, gravity: "Center", **options)
68
82
  thumbnail("#{width}x#{height}", **options)
69
83
  magick.background color(background)
@@ -71,11 +85,17 @@ module ImageProcessing
71
85
  magick.extent "#{width}x#{height}"
72
86
  end
73
87
 
88
+ # Rotates the image by an arbitrary angle. For angles that are not
89
+ # multiple of 90 degrees an optional background color can be specified to
90
+ # fill in the gaps.
74
91
  def rotate(degrees, background: nil)
75
92
  magick.background color(background) if background
76
93
  magick.rotate(degrees)
77
94
  end
78
95
 
96
+ # Overlays the specified image over the current one. Supports specifying
97
+ # an additional mask, composite mode, direction or offset of the overlay
98
+ # image.
79
99
  def composite(overlay = :none, mask: nil, mode: nil, gravity: nil, offset: nil, args: nil, **options, &block)
80
100
  return magick.composite if overlay == :none
81
101
 
@@ -107,22 +127,28 @@ module ImageProcessing
107
127
  magick.composite
108
128
  end
109
129
 
130
+ # Defines settings from the provided hash.
110
131
  def define(options)
111
132
  return magick.define(options) if options.is_a?(String)
112
133
  Utils.apply_define(magick, options)
113
134
  end
114
135
 
136
+ # Specifies resource limits from the provided hash.
115
137
  def limits(options)
116
138
  options.each { |type, value| magick.args.unshift("-limit", type.to_s, value.to_s) }
117
139
  magick
118
140
  end
119
141
 
142
+ # Appends a raw ImageMagick command-line argument to the command.
120
143
  def append(*args)
121
144
  magick.merge! args
122
145
  end
123
146
 
124
147
  private
125
148
 
149
+ # Converts the given color value into an identifier ImageMagick understands.
150
+ # This supports specifying RGB(A) values with arrays, which mainly exists
151
+ # for compatibility with the libvips implementation.
126
152
  def color(value)
127
153
  return "rgba(255,255,255,0.0)" if value.to_s == "transparent"
128
154
  return "rgb(#{value.join(",")})" if value.is_a?(Array) && value.count == 3
@@ -132,6 +158,8 @@ module ImageProcessing
132
158
  raise ArgumentError, "unrecognized color format: #{value.inspect} (must be one of: string, 3-element RGB array, 4-element RGBA array)"
133
159
  end
134
160
 
161
+ # Resizes the image using the specified geometry, and sharpens the
162
+ # resulting thumbnail.
135
163
  def thumbnail(geometry, sharpen: {})
136
164
  magick.resize(geometry)
137
165
 
@@ -143,6 +171,7 @@ module ImageProcessing
143
171
  magick
144
172
  end
145
173
 
174
+ # Converts the image on disk in various forms into a path.
146
175
  def convert_to_path(file, name)
147
176
  if file.is_a?(String)
148
177
  file
@@ -158,6 +187,9 @@ module ImageProcessing
158
187
  module Utils
159
188
  module_function
160
189
 
190
+ # When a multi-layer format is being converted into a single-layer
191
+ # format, ImageMagick will create multiple images, one for each layer.
192
+ # We want to warn the user that this is probably not what they wanted.
161
193
  def disallow_split_layers!(destination_path)
162
194
  layers = Dir[destination_path.sub(/\.\w+$/, '-*\0')]
163
195
 
@@ -167,6 +199,7 @@ module ImageProcessing
167
199
  end
168
200
  end
169
201
 
202
+ # Applies options from the provided hash.
170
203
  def apply_options(magick, define: {}, **options)
171
204
  options.each do |option, value|
172
205
  case value
@@ -179,12 +212,13 @@ module ImageProcessing
179
212
  apply_define(magick, define)
180
213
  end
181
214
 
215
+ # Applies settings from the provided (nested) hash.
182
216
  def apply_define(magick, options)
183
217
  options.each do |namespace, settings|
184
- namespace = namespace.to_s.gsub("_", "-")
218
+ namespace = namespace.to_s.tr("_", "-")
185
219
 
186
220
  settings.each do |key, value|
187
- key = key.to_s.gsub("_", "-")
221
+ key = key.to_s.tr("_", "-")
188
222
 
189
223
  magick.define "#{namespace}:#{key}=#{value}"
190
224
  end
@@ -6,6 +6,7 @@ module ImageProcessing
6
6
 
7
7
  attr_reader :source, :loader, :saver, :format, :operations, :processor, :destination
8
8
 
9
+ # Initializes the pipeline with all the processing options.
9
10
  def initialize(options)
10
11
  options.each do |name, value|
11
12
  value = normalize_source(value, options) if name == :source
@@ -13,6 +14,10 @@ module ImageProcessing
13
14
  end
14
15
  end
15
16
 
17
+ # Performs the defined series of operations, and saves the result in a new
18
+ # tempfile or a specified path on disk, or if `save: false` was passed in
19
+ # returns the unsaved accumulator object that can be used for further
20
+ # processing.
16
21
  def call(save: true)
17
22
  accumulator = processor.load_image(source, **loader)
18
23
 
@@ -33,10 +38,12 @@ module ImageProcessing
33
38
  end
34
39
  end
35
40
 
41
+ # Retrieves the source path on disk.
36
42
  def source_path
37
43
  source if source.is_a?(String)
38
44
  end
39
45
 
46
+ # Determines the appropriate destination image format.
40
47
  def destination_format
41
48
  format = File.extname(destination)[1..-1] if destination
42
49
  format ||= self.format
@@ -47,6 +54,8 @@ module ImageProcessing
47
54
 
48
55
  private
49
56
 
57
+ # Creates a new tempfile for the destination file, yields it, and refreshes
58
+ # the file descriptor to get the updated file.
50
59
  def create_tempfile
51
60
  tempfile = Tempfile.new(["image_processing", ".#{destination_format}"], binmode: true)
52
61
 
@@ -70,6 +79,7 @@ module ImageProcessing
70
79
  raise
71
80
  end
72
81
 
82
+ # Converts the source image object into a path or the accumulator object.
73
83
  def normalize_source(source, options)
74
84
  fail Error, "source file is not provided" unless source
75
85
 
@@ -1,11 +1,18 @@
1
1
  module ImageProcessing
2
+ # Abstract class inherited by individual processors.
2
3
  class Processor
4
+ # Use for processor subclasses to specify the name and the class of their
5
+ # accumulator object (e.g. MiniMagic::Tool or Vips::Image).
3
6
  def self.accumulator(name, klass)
4
7
  define_method(name) { @accumulator }
5
8
  protected(name)
6
9
  const_set(:ACCUMULATOR_CLASS, klass)
7
10
  end
8
11
 
12
+ # Calls the operation to perform the processing. If the operation is
13
+ # defined on the processor (macro), calls it. Otherwise calls the
14
+ # operation directly on the accumulator object. This provides a common
15
+ # umbrella above defined macros and direct operations.
9
16
  def self.apply_operation(accumulator, name, *args, &block)
10
17
  if (instance_methods - Object.instance_methods).include?(name)
11
18
  instance = new(accumulator)
@@ -19,6 +26,8 @@ module ImageProcessing
19
26
  @accumulator = accumulator
20
27
  end
21
28
 
29
+ # Calls the given block with the accumulator object. Useful for when you
30
+ # want to access the accumulator object directly.
22
31
  def custom(&block)
23
32
  (block && block.call(@accumulator)) || @accumulator
24
33
  end
@@ -1,3 +1,3 @@
1
1
  module ImageProcessing
2
- VERSION = "1.6.0"
2
+ VERSION = "1.7.0"
3
3
  end
@@ -7,6 +7,7 @@ module ImageProcessing
7
7
  module Vips
8
8
  extend Chainable
9
9
 
10
+ # Returns whether the given image file is processable.
10
11
  def self.valid_image?(file)
11
12
  ::Vips::Image.new_from_file(file.path, access: :sequential).avg
12
13
  true
@@ -17,12 +18,15 @@ module ImageProcessing
17
18
  class Processor < ImageProcessing::Processor
18
19
  accumulator :image, ::Vips::Image
19
20
 
20
- # default sharpening mask that provides a fast and mild sharpen
21
+ # Default sharpening mask that provides a fast and mild sharpen.
21
22
  SHARPEN_MASK = ::Vips::Image.new_from_array [[-1, -1, -1],
22
23
  [-1, 32, -1],
23
24
  [-1, -1, -1]], 24
24
25
 
25
26
 
27
+ # Loads the image on disk into a Vips::Image object. Accepts additional
28
+ # loader-specific options (e.g. interlacing). Afterwards auto-rotates the
29
+ # image to be upright.
26
30
  def self.load_image(path_or_image, autorot: true, **options)
27
31
  if path_or_image.is_a?(::Vips::Image)
28
32
  image = path_or_image
@@ -37,6 +41,9 @@ module ImageProcessing
37
41
  image
38
42
  end
39
43
 
44
+ # Writes the Vips::Image object to disk. This starts the processing
45
+ # pipeline defined in the Vips::Image object. Accepts additional
46
+ # saver-specific options (e.g. quality).
40
47
  def self.save_image(image, destination_path, quality: nil, **options)
41
48
  options = options.merge(Q: quality) if quality
42
49
  options = Utils.select_valid_saver_options(destination_path, options)
@@ -44,41 +51,43 @@ module ImageProcessing
44
51
  image.write_to_file(destination_path, **options)
45
52
  end
46
53
 
54
+ # Resizes the image to not be larger than the specified dimensions.
47
55
  def resize_to_limit(width, height, **options)
48
56
  width, height = default_dimensions(width, height)
49
57
  thumbnail(width, height, size: :down, **options)
50
58
  end
51
59
 
60
+ # Resizes the image to fit within the specified dimensions.
52
61
  def resize_to_fit(width, height, **options)
53
62
  width, height = default_dimensions(width, height)
54
63
  thumbnail(width, height, **options)
55
64
  end
56
65
 
66
+ # Resizes the image to fill the specified dimensions, applying any
67
+ # necessary cropping.
57
68
  def resize_to_fill(width, height, **options)
58
69
  thumbnail(width, height, crop: :centre, **options)
59
70
  end
60
71
 
72
+ # Resizes the image to fit within the specified dimensions and fills
73
+ # the remaining area with the specified background color.
61
74
  def resize_and_pad(width, height, gravity: "centre", extend: nil, background: nil, alpha: nil, **options)
62
- embed_options = { extend: extend, background: background }
63
- embed_options.reject! { |name, value| value.nil? }
64
-
65
75
  image = thumbnail(width, height, **options)
66
76
  image = image.add_alpha if alpha && !image.has_alpha?
67
- image.gravity(gravity, width, height, **embed_options)
77
+ image.gravity(gravity, width, height, extend: extend, background: background)
68
78
  end
69
79
 
70
- def rotate(degrees, background: nil)
71
- if degrees % 90 == 0
72
- image.rot(:"d#{degrees % 360}")
73
- else
74
- options = { angle: degrees }
75
- options[:background] = background if background
76
-
77
- image.similarity(**options)
78
- end
80
+ # Rotates the image by an arbitrary angle. Additional options can be
81
+ # specified, such as background colors to fill in the gaps when rotating
82
+ # with an angle which is not a multiple of 90 degrees.
83
+ def rotate(degrees, **options)
84
+ image.similarity(angle: degrees, **options)
79
85
  end
80
86
 
81
- def composite(overlay, _mode = nil, mode: :over, gravity: :"north-west", offset: nil, **options)
87
+ # Overlays the specified image over the current one. Supports specifying
88
+ # composite mode, direction or offset of the overlay image.
89
+ def composite(overlay, _mode = nil, mode: "over", gravity: "north-west", offset: nil, **options)
90
+ # if the mode argument is given, call the original Vips::Image#composite
82
91
  if _mode
83
92
  overlay = [overlay] unless overlay.is_a?(Array)
84
93
  overlay = overlay.map { |object| convert_to_image(object, "overlay") }
@@ -87,15 +96,19 @@ module ImageProcessing
87
96
  end
88
97
 
89
98
  overlay = convert_to_image(overlay, "overlay")
90
- overlay = overlay.add_alpha unless overlay.has_alpha? # so that #gravity can use transparent background
99
+ # add alpha channel so that #gravity can use a transparent background
100
+ overlay = overlay.add_alpha unless overlay.has_alpha?
91
101
 
102
+ # apply offset with correct gravity and make remainder transparent
92
103
  if offset
93
104
  opposite_gravity = gravity.to_s.gsub(/\w+/, "north"=>"south", "south"=>"north", "east"=>"west", "west"=>"east")
94
105
  overlay = overlay.gravity(opposite_gravity, overlay.width + offset.first, overlay.height + offset.last)
95
106
  end
96
107
 
108
+ # create image-sized transparent background and apply specified gravity
97
109
  overlay = overlay.gravity(gravity, image.width, image.height)
98
110
 
111
+ # apply the composition
99
112
  image.composite(overlay, mode, **options)
100
113
  end
101
114
 
@@ -106,6 +119,8 @@ module ImageProcessing
106
119
 
107
120
  private
108
121
 
122
+ # Resizes the image according to the specified parameters, and sharpens
123
+ # the resulting thumbnail.
109
124
  def thumbnail(width, height, sharpen: SHARPEN_MASK, **options)
110
125
  image = self.image
111
126
  image = image.thumbnail_image(width, height: height, **options)
@@ -113,12 +128,14 @@ module ImageProcessing
113
128
  image
114
129
  end
115
130
 
131
+ # Hack to allow omitting one dimension.
116
132
  def default_dimensions(width, height)
117
133
  raise Error, "either width or height must be specified" unless width || height
118
134
 
119
135
  [width || ::Vips::MAX_COORD, height || ::Vips::MAX_COORD]
120
136
  end
121
137
 
138
+ # Converts the image on disk in various forms into a Vips::Image object.
122
139
  def convert_to_image(object, name)
123
140
  return object if object.is_a?(::Vips::Image)
124
141
 
@@ -138,16 +155,24 @@ module ImageProcessing
138
155
  module Utils
139
156
  module_function
140
157
 
158
+ # libvips uses various loaders depending on the input format.
141
159
  def select_valid_loader_options(source_path, options)
142
160
  loader = ::Vips.vips_foreign_find_load(source_path)
143
161
  loader ? select_valid_options(loader, options) : options
144
162
  end
145
163
 
164
+ # Filters out unknown options for saving images.
146
165
  def select_valid_saver_options(destination_path, options)
147
166
  saver = ::Vips.vips_foreign_find_save(destination_path)
148
167
  saver ? select_valid_options(saver, options) : options
149
168
  end
150
169
 
170
+ # libvips uses various loaders and savers depending on the input and
171
+ # output image format. Each of these loaders and savers accept slightly
172
+ # different options, so to allow the user to be able to specify options
173
+ # for a specific loader/saver and have it ignored for other
174
+ # loaders/savers, we do a little bit of introspection and filter out
175
+ # options that don't exist for a particular loader or saver.
151
176
  def select_valid_options(operation_name, options)
152
177
  operation = ::Vips::Operation.new(operation_name)
153
178
 
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: 1.6.0
4
+ version: 1.7.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-07-13 00:00:00.000000000 Z
11
+ date: 2018-09-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mini_magick
@@ -30,7 +30,7 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 2.0.11
33
+ version: 2.0.13
34
34
  - - "<"
35
35
  - !ruby/object:Gem::Version
36
36
  version: '3'
@@ -40,7 +40,7 @@ dependencies:
40
40
  requirements:
41
41
  - - ">="
42
42
  - !ruby/object:Gem::Version
43
- version: 2.0.11
43
+ version: 2.0.13
44
44
  - - "<"
45
45
  - !ruby/object:Gem::Version
46
46
  version: '3'