File: mini_magick.rb

package info (click to toggle)
ruby-image-processing 1.10.3-1%2Bdeb11u1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 1,280 kB
  • sloc: ruby: 1,504; sh: 14; makefile: 3
file content (233 lines) | stat: -rw-r--r-- 8,420 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
require "mini_magick"
require "image_processing"

module ImageProcessing
  module MiniMagick
    extend Chainable

    # Returns whether the given image file is processable.
    def self.valid_image?(file)
      ::MiniMagick::Tool::Convert.new do |convert|
        convert << file.path
        convert << "null:"
      end
      true
    rescue ::MiniMagick::Error
      false
    end

    class Processor < ImageProcessing::Processor
      accumulator :magick, ::MiniMagick::Tool

      # Default sharpening parameters used on generated thumbnails.
      SHARPEN_PARAMETERS = { radius: 0, sigma: 1 }

      # Initializes the image on disk into a MiniMagick::Tool object. Accepts
      # additional options related to loading the image (e.g. geometry).
      # Additionally auto-orients the image to be upright.
      def self.load_image(path_or_magick, loader: nil, page: nil, geometry: nil, auto_orient: true, **options)
        if path_or_magick.is_a?(::MiniMagick::Tool)
          magick = path_or_magick
        else
          source_path = path_or_magick
          magick = ::MiniMagick::Tool::Convert.new

          Utils.apply_options(magick, **options)

          input  = source_path
          input  = "#{loader}:#{input}" if loader
          input += "[#{page}]" if page
          input += "[#{geometry}]" if geometry

          magick << input
        end

        magick.auto_orient if auto_orient
        magick
      end

      # Calls the built ImageMagick command to perform processing and save
      # the result to disk. Accepts additional options related to saving the
      # image (e.g. quality).
      def self.save_image(magick, destination_path, allow_splitting: false, **options)
        Utils.apply_options(magick, **options)

        magick << destination_path
        magick.call

        Utils.disallow_split_layers!(destination_path) unless allow_splitting
      end

      # Resizes the image to not be larger than the specified dimensions.
      def resize_to_limit(width, height, **options)
        thumbnail("#{width}x#{height}>", **options)
      end

      # Resizes the image to fit within the specified dimensions.
      def resize_to_fit(width, height, **options)
        thumbnail("#{width}x#{height}", **options)
      end

      # Resizes the image to fill the specified dimensions, applying any
      # necessary cropping.
      def resize_to_fill(width, height, gravity: "Center", **options)
        thumbnail("#{width}x#{height}^", **options)
        magick.gravity gravity
        magick.background color(:transparent)
        magick.extent "#{width}x#{height}"
      end

      # Resizes the image to fit within the specified dimensions and fills
      # the remaining area with the specified background color.
      def resize_and_pad(width, height, background: :transparent, gravity: "Center", **options)
        thumbnail("#{width}x#{height}", **options)
        magick.background color(background)
        magick.gravity gravity
        magick.extent "#{width}x#{height}"
      end

      # Rotates the image by an arbitrary angle. For angles that are not
      # multiple of 90 degrees an optional background color can be specified to
      # fill in the gaps.
      def rotate(degrees, background: nil)
        magick.background color(background) if background
        magick.rotate(degrees)
      end

      # Overlays the specified image over the current one. Supports specifying
      # an additional mask, composite mode, direction or offset of the overlay
      # image.
      def composite(overlay = :none, mask: nil, mode: nil, gravity: nil, offset: nil, args: nil, **options, &block)
        return magick.composite if overlay == :none

        if options.key?(:compose)
          warn "[IMAGE_PROCESSING] The :compose parameter in #composite has been renamed to :mode, the :compose alias will be removed in ImageProcessing 2."
          mode = options[:compose]
        end

        if options.key?(:geometry)
          warn "[IMAGE_PROCESSING] The :geometry parameter in #composite has been deprecated and will be removed in ImageProcessing 2. Use :offset instead, e.g. `geometry: \"+10+15\"` should be replaced with `offset: [10, 15]`."
          geometry = options[:geometry]
        end
        geometry = "%+d%+d" % offset if offset

        overlay_path = convert_to_path(overlay, "overlay")
        mask_path    = convert_to_path(mask, "mask") if mask

        magick << overlay_path
        magick << mask_path if mask_path

        magick.compose(mode) if mode
        define(compose: { args: args }) if args

        magick.gravity(gravity) if gravity
        magick.geometry(geometry) if geometry

        yield magick if block_given?

        magick.composite
      end

      # Defines settings from the provided hash.
      def define(options)
        return magick.define(options) if options.is_a?(String)
        Utils.apply_define(magick, options)
      end

      # Specifies resource limits from the provided hash.
      def limits(options)
        options.each { |type, value| magick.args.unshift("-limit", type.to_s, value.to_s) }
        magick
      end

      # Appends a raw ImageMagick command-line argument to the command.
      def append(*args)
        magick.merge! args
      end

      private

      # Converts the given color value into an identifier ImageMagick understands.
      # This supports specifying RGB(A) values with arrays, which mainly exists
      # for compatibility with the libvips implementation.
      def color(value)
        return "rgba(255,255,255,0.0)" if value.to_s == "transparent"
        return "rgb(#{value.join(",")})" if value.is_a?(Array) && value.count == 3
        return "rgba(#{value.join(",")})" if value.is_a?(Array) && value.count == 4
        return value if value.is_a?(String)

        raise ArgumentError, "unrecognized color format: #{value.inspect} (must be one of: string, 3-element RGB array, 4-element RGBA array)"
      end

      # Resizes the image using the specified geometry, and sharpens the
      # resulting thumbnail.
      def thumbnail(geometry, sharpen: {})
        magick.resize(geometry)

        if sharpen
          sharpen = SHARPEN_PARAMETERS.merge(sharpen)
          magick.sharpen("#{sharpen[:radius]}x#{sharpen[:sigma]}")
        end

        magick
      end

      # Converts the image on disk in various forms into a path.
      def convert_to_path(file, name)
        if file.is_a?(String)
          file
        elsif file.respond_to?(:to_path)
          file.to_path
        elsif file.respond_to?(:path)
          file.path
        else
          raise ArgumentError, "#{name} must be a String, Pathname, or respond to #path"
        end
      end

      module Utils
        module_function

        # When a multi-layer format is being converted into a single-layer
        # format, ImageMagick will create multiple images, one for each layer.
        # We want to warn the user that this is probably not what they wanted.
        def disallow_split_layers!(destination_path)
          layers = Dir[destination_path.sub(/\.\w+$/, '-*\0')]

          if layers.any?
            layers.each { |path| File.delete(path) }
            raise Error, "Source format is multi-layer, but destination format is single-layer. If you care only about the first layer, add `.loader(page: 0)` to your pipeline. If you want to process each layer, see https://github.com/janko/image_processing/wiki/Splitting-a-PDF-into-multiple-images or use `.saver(allow_splitting: true)`."
          end
        end

        # Applies options from the provided hash.
        def apply_options(magick, define: {}, **options)
          options.each do |option, value|
            case value
            when true, nil then magick.send(option)
            when false     then magick.send(option).+
            else                magick.send(option, *value)
            end
          end

          apply_define(magick, define)
        end

        # Applies settings from the provided (nested) hash.
        def apply_define(magick, options)
          options.each do |namespace, settings|
            namespace = namespace.to_s.tr("_", "-")

            settings.each do |key, value|
              key = key.to_s.tr("_", "-")

              magick.define "#{namespace}:#{key}=#{value}"
            end
          end

          magick
        end
      end
    end
  end
end