File: vips.rb

package info (click to toggle)
ruby-image-processing 1.10.3-3
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 1,312 kB
  • sloc: ruby: 1,504; sh: 14; makefile: 4
file content (206 lines) | stat: -rw-r--r-- 8,078 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
require "vips"
require "image_processing"

fail "image_processing/vips requires libvips 8.6+" unless Vips.at_least_libvips?(8, 6)

module ImageProcessing
  module Vips
    extend Chainable

    # Returns whether the given image file is processable.
    def self.valid_image?(file)
      ::Vips::Image.new_from_file(file.path, access: :sequential).avg
      true
    rescue ::Vips::Error
      false
    end

    class Processor < ImageProcessing::Processor
      accumulator :image, ::Vips::Image

      # Default sharpening mask that provides a fast and mild sharpen.
      SHARPEN_MASK = ::Vips::Image.new_from_array [[-1, -1, -1],
                                                   [-1, 32, -1],
                                                   [-1, -1, -1]], 24


      # Loads the image on disk into a Vips::Image object. Accepts additional
      # loader-specific options (e.g. interlacing). Afterwards auto-rotates the
      # image to be upright.
      def self.load_image(path_or_image, loader: nil, autorot: true, **options)
        if path_or_image.is_a?(::Vips::Image)
          image = path_or_image
        else
          path = path_or_image

          if loader
            image = ::Vips::Image.public_send(:"#{loader}load", path, **options)
          else
            options = Utils.select_valid_loader_options(path, options)
            image = ::Vips::Image.new_from_file(path, **options)
          end
        end

        image = image.autorot if autorot && !options.key?(:autorotate)
        image
      end

      # See #thumbnail.
      def self.supports_resize_on_load?
        true
      end

      # Writes the Vips::Image object to disk. This starts the processing
      # pipeline defined in the Vips::Image object. Accepts additional
      # saver-specific options (e.g. quality).
      def self.save_image(image, path, saver: nil, quality: nil, **options)
        options[:Q] = quality if quality

        if saver
          image.public_send(:"#{saver}save", path, **options)
        else
          options = Utils.select_valid_saver_options(path, options)
          image.write_to_file(path, **options)
        end
      end

      # Resizes the image to not be larger than the specified dimensions.
      def resize_to_limit(width, height, **options)
        width, height = default_dimensions(width, height)
        thumbnail(width, height, size: :down, **options)
      end

      # Resizes the image to fit within the specified dimensions.
      def resize_to_fit(width, height, **options)
        width, height = default_dimensions(width, height)
        thumbnail(width, height, **options)
      end

      # Resizes the image to fill the specified dimensions, applying any
      # necessary cropping.
      def resize_to_fill(width, height, **options)
        thumbnail(width, height, crop: :centre, **options)
      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, gravity: "centre", extend: nil, background: nil, alpha: nil, **options)
        image = thumbnail(width, height, **options)
        image = image.add_alpha if alpha && !image.has_alpha?
        image.gravity(gravity, width, height, extend: extend, background: background)
      end

      # Rotates the image by an arbitrary angle.
      def rotate(degrees, **options)
        image.similarity(angle: degrees, **options)
      end

      # Overlays the specified image over the current one. Supports specifying
      # composite mode, direction or offset of the overlay image.
      def composite(overlay, _mode = nil, mode: "over", gravity: "north-west", offset: nil, **options)
        # if the mode argument is given, call the original Vips::Image#composite
        if _mode
          overlay = [overlay] unless overlay.is_a?(Array)
          overlay = overlay.map { |object| convert_to_image(object, "overlay") }

          return image.composite(overlay, _mode, **options)
        end

        overlay = convert_to_image(overlay, "overlay")
        # add alpha channel so that #gravity can use a transparent background
        overlay = overlay.add_alpha unless overlay.has_alpha?

        # apply offset with correct gravity and make remainder transparent
        if offset
          opposite_gravity = gravity.to_s.gsub(/\w+/, "north"=>"south", "south"=>"north", "east"=>"west", "west"=>"east")
          overlay = overlay.gravity(opposite_gravity, overlay.width + offset.first, overlay.height + offset.last)
        end

        # create image-sized transparent background and apply specified gravity
        overlay = overlay.gravity(gravity, image.width, image.height)

        # apply the composition
        image.composite(overlay, mode, **options)
      end

      # make metadata setter methods chainable
      def set(*args)       image.tap { |img| img.set(*args) }       end
      def set_type(*args)  image.tap { |img| img.set_type(*args) }  end
      def set_value(*args) image.tap { |img| img.set_value(*args) } end
      def remove(*args)    image.tap { |img| img.remove(*args) }    end

      private

      # Resizes the image according to the specified parameters, and sharpens
      # the resulting thumbnail.
      def thumbnail(width, height, sharpen: SHARPEN_MASK, **options)
        if self.image.is_a?(String) # path
          # resize on load
          image = ::Vips::Image.thumbnail(self.image, width, height: height, **options)
        else
          # we're already calling Image#autorot when loading the image
          no_rotate = ::Vips.at_least_libvips?(8, 8) ? { no_rotate: true } : { auto_rotate: false }
          options   = no_rotate.merge(options)

          image = self.image.thumbnail_image(width, height: height, **options)
        end

        image = image.conv(sharpen, precision: :integer) if sharpen
        image
      end

      # Hack to allow omitting one dimension.
      def default_dimensions(width, height)
        raise Error, "either width or height must be specified" unless width || height

        [width || ::Vips::MAX_COORD, height || ::Vips::MAX_COORD]
      end

      # Converts the image on disk in various forms into a Vips::Image object.
      def convert_to_image(object, name)
        return object if object.is_a?(::Vips::Image)

        if object.is_a?(String)
          path = object
        elsif object.respond_to?(:to_path)
          path = object.to_path
        elsif object.respond_to?(:path)
          path = object.path
        else
          raise ArgumentError, "#{name} must be a Vips::Image, String, Pathname, or respond to #path"
        end

        ::Vips::Image.new_from_file(path)
      end

      module Utils
        module_function

        # libvips uses various loaders depending on the input format.
        def select_valid_loader_options(source_path, options)
          loader = ::Vips.vips_foreign_find_load(source_path)
          loader ? select_valid_options(loader, options) : options
        end

        # Filters out unknown options for saving images.
        def select_valid_saver_options(destination_path, options)
          saver = ::Vips.vips_foreign_find_save(destination_path)
          saver ? select_valid_options(saver, options) : options
        end

        # libvips uses various loaders and savers depending on the input and
        # output image format. Each of these loaders and savers accept slightly
        # different options, so to allow the user to be able to specify options
        # for a specific loader/saver and have it ignored for other
        # loaders/savers, we do some introspection and filter out options that
        # don't exist for a particular loader or saver.
        def select_valid_options(operation_name, options)
          introspect        = ::Vips::Introspect.get(operation_name)
          operation_options = introspect.optional_input.keys.map(&:to_sym)

          options.select { |name, value| operation_options.include?(name) }
        end
      end
    end
  end
end