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

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'