image_vise 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +8 -0
- data/.travis.yml +13 -0
- data/DEVELOPMENT.md +111 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +29 -0
- data/README.md +213 -0
- data/Rakefile +6 -0
- data/SECURITY.md +57 -0
- data/examples/config.ru +17 -0
- data/examples/custom_image_operator.rb +27 -0
- data/examples/error_handline_appsignal.rb +23 -0
- data/examples/error_handling_sentry.rb +25 -0
- data/image_vise.gemspec +43 -0
- data/lib/image_vise/fetchers/fetcher_file.rb +27 -0
- data/lib/image_vise/fetchers/fetcher_http.rb +42 -0
- data/lib/image_vise/file_response.rb +22 -0
- data/lib/image_vise/image_request.rb +70 -0
- data/lib/image_vise/operators/auto_orient.rb +10 -0
- data/lib/image_vise/operators/background_fill.rb +18 -0
- data/lib/image_vise/operators/crop.rb +32 -0
- data/lib/image_vise/operators/ellipse_stencil.rb +70 -0
- data/lib/image_vise/operators/fit_crop.rb +33 -0
- data/lib/image_vise/operators/force_jpg_out.rb +17 -0
- data/lib/image_vise/operators/geom.rb +16 -0
- data/lib/image_vise/operators/sRGB_v4_ICC_preference_displayclass.icc +0 -0
- data/lib/image_vise/operators/sharpen.rb +21 -0
- data/lib/image_vise/operators/srgb.rb +30 -0
- data/lib/image_vise/operators/strip_metadata.rb +10 -0
- data/lib/image_vise/pipeline.rb +64 -0
- data/lib/image_vise/render_engine.rb +298 -0
- data/lib/image_vise/version.rb +3 -0
- data/lib/image_vise/writers/auto_writer.rb +23 -0
- data/lib/image_vise/writers/jpeg_writer.rb +9 -0
- data/lib/image_vise.rb +177 -0
- data/spec/image_vise/auto_orient_spec.rb +10 -0
- data/spec/image_vise/background_fill_spec.rb +39 -0
- data/spec/image_vise/crop_spec.rb +20 -0
- data/spec/image_vise/ellipse_stencil_spec.rb +18 -0
- data/spec/image_vise/fetcher_file_spec.rb +48 -0
- data/spec/image_vise/fetcher_http_spec.rb +44 -0
- data/spec/image_vise/file_response_spec.rb +45 -0
- data/spec/image_vise/fit_crop_spec.rb +20 -0
- data/spec/image_vise/force_jpg_out_spec.rb +36 -0
- data/spec/image_vise/geom_spec.rb +33 -0
- data/spec/image_vise/image_request_spec.rb +62 -0
- data/spec/image_vise/pipeline_spec.rb +72 -0
- data/spec/image_vise/render_engine_spec.rb +336 -0
- data/spec/image_vise/sharpen_spec.rb +17 -0
- data/spec/image_vise/srgb_spec.rb +23 -0
- data/spec/image_vise/strip_metadata_spec.rb +14 -0
- data/spec/image_vise/writers/auto_writer_spec.rb +25 -0
- data/spec/image_vise/writers/jpeg_writer_spec.rb +32 -0
- data/spec/image_vise_spec.rb +110 -0
- data/spec/layers-with-blending.psd +0 -0
- data/spec/spec_helper.rb +112 -0
- data/spec/test_server.rb +61 -0
- data/spec/waterside_magic_hour.jpg +0 -0
- data/spec/waterside_magic_hour.psd +0 -0
- data/spec/waterside_magic_hour_adobergb.jpg +0 -0
- data/spec/waterside_magic_hour_gray.tif +0 -0
- data/spec/waterside_magic_hour_transp.png +0 -0
- metadata +63 -2
@@ -0,0 +1,22 @@
|
|
1
|
+
# Wrappers a given Tempfile for a Rack response.
|
2
|
+
# Will close _and_ unlink the Tempfile it contains.
|
3
|
+
class ImageVise::FileResponse
|
4
|
+
ONE_CHUNK_BYTES = 1024 * 1024 * 2
|
5
|
+
def initialize(file)
|
6
|
+
@file = file
|
7
|
+
end
|
8
|
+
|
9
|
+
def each
|
10
|
+
@file.flush # Make sure all the writes have been synchronized
|
11
|
+
# We can easily open another file descriptor
|
12
|
+
File.open(@file.path, 'rb') do |my_file_descriptor|
|
13
|
+
while data = my_file_descriptor.read(ONE_CHUNK_BYTES)
|
14
|
+
yield(data)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def close
|
20
|
+
ImageVise.close_and_unlink(@file)
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
class ImageVise::ImageRequest < Ks.strict(:src_url, :pipeline)
|
4
|
+
class InvalidRequest < ArgumentError; end
|
5
|
+
class SignatureError < InvalidRequest; end
|
6
|
+
class URLError < InvalidRequest; end
|
7
|
+
class MissingParameter < InvalidRequest; end
|
8
|
+
|
9
|
+
# Initializes a new ParamsChecker from given HTTP server framework
|
10
|
+
# params. The params can be symbol- or string-keyed, does not matter.
|
11
|
+
def self.from_params(qs_params:, secrets:)
|
12
|
+
base64_encoded_params = qs_params.fetch(:q) rescue qs_params.fetch('q')
|
13
|
+
given_signature = qs_params.fetch(:sig) rescue qs_params.fetch('sig')
|
14
|
+
|
15
|
+
# Unmask slashes and equals signs (if they are present)
|
16
|
+
base64_encoded_params = base64_encoded_params.tr('-', '/').tr('_', '+')
|
17
|
+
|
18
|
+
# Check the signature before decoding JSON (since we will be creating symbols)
|
19
|
+
unless valid_signature?(base64_encoded_params, given_signature, secrets)
|
20
|
+
raise SignatureError, "Invalid or missing signature"
|
21
|
+
end
|
22
|
+
|
23
|
+
# Decode the JSON
|
24
|
+
# (only AFTER the signature has been validated, so we can use symbol keys)
|
25
|
+
decoded_json = Base64.decode64(base64_encoded_params)
|
26
|
+
params = JSON.parse(decoded_json, symbolize_names: true)
|
27
|
+
|
28
|
+
# Pick up the URL and validate it
|
29
|
+
source_url_str = params.fetch(:src_url).to_s
|
30
|
+
raise URLError, "the :src_url parameter must be non-empty" if source_url_str.empty?
|
31
|
+
pipeline_definition = params.fetch(:pipeline)
|
32
|
+
new(src_url: URI(source_url_str), pipeline: ImageVise::Pipeline.from_param(pipeline_definition))
|
33
|
+
rescue KeyError => e
|
34
|
+
raise InvalidRequest.new(e.message)
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_path_params(signed_with_secret)
|
38
|
+
qs = to_query_string_params(signed_with_secret)
|
39
|
+
q_masked = qs.fetch(:q).tr('/', '-').tr('+', '_')
|
40
|
+
'/%s/%s' % [q_masked, qs[:sig]]
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_query_string_params(signed_with_secret)
|
44
|
+
payload = JSON.dump(to_h)
|
45
|
+
base64_enc = Base64.strict_encode64(payload).gsub(/\=+$/, '')
|
46
|
+
{q: base64_enc, sig: OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, signed_with_secret, base64_enc)}
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_h
|
50
|
+
{pipeline: pipeline.to_params, src_url: src_url.to_s}
|
51
|
+
end
|
52
|
+
|
53
|
+
def cache_etag
|
54
|
+
Digest::SHA1.hexdigest(JSON.dump(to_h))
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def self.valid_signature?(for_payload, given_signature, secrets)
|
60
|
+
# Check the signature against every key that we have,
|
61
|
+
# since different apps might be using different keys
|
62
|
+
seen_valid_signature = false
|
63
|
+
secrets.each do | stored_secret |
|
64
|
+
expected_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, stored_secret, for_payload)
|
65
|
+
result_for_this_key = Rack::Utils.secure_compare(expected_signature, given_signature)
|
66
|
+
seen_valid_signature ||= result_for_this_key
|
67
|
+
end
|
68
|
+
seen_valid_signature
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# Applies ImageMagick auto_orient to the image, so that i.e. mobile photos
|
2
|
+
# can be oriented correctly. The operation is applied destructively (changes actual pixel data)
|
3
|
+
#
|
4
|
+
# The corresponding Pipeline method is `auto_orient`.
|
5
|
+
class ImageVise::AutoOrient
|
6
|
+
def apply!(magick_image)
|
7
|
+
magick_image.auto_orient!
|
8
|
+
end
|
9
|
+
ImageVise.add_operator 'auto_orient', self
|
10
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Applies a background fill color.
|
2
|
+
# Can handle most 'word' colors and hex color codes but not RGB values.
|
3
|
+
#
|
4
|
+
# The corresponding Pipeline method is `background_fill`.
|
5
|
+
class ImageVise::BackgroundFill < Ks.strict(:color)
|
6
|
+
def initialize(*)
|
7
|
+
super
|
8
|
+
self.color = color.to_s
|
9
|
+
raise ArgumentError, "the :color parameter must be present and not empty" if self.color.empty?
|
10
|
+
end
|
11
|
+
|
12
|
+
def apply!(image)
|
13
|
+
image.border!(0, 0, color)
|
14
|
+
image.alpha(Magick::DeactivateAlphaChannel)
|
15
|
+
end
|
16
|
+
|
17
|
+
ImageVise.add_operator 'background_fill', self
|
18
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# Crops the image to the given dimensions with a given gravity. Gravities are shorthand versions
|
2
|
+
# of ImageMagick gravity parameters (see GRAVITY_PARAMS)
|
3
|
+
#
|
4
|
+
# The corresponding Pipeline method is `crop`.
|
5
|
+
class ImageVise::Crop < Ks.strict(:width, :height, :gravity)
|
6
|
+
GRAVITY_PARAMS = {
|
7
|
+
'nw' => Magick::NorthWestGravity,
|
8
|
+
'n' => Magick::NorthGravity,
|
9
|
+
'ne' => Magick::NorthEastGravity,
|
10
|
+
'w' => Magick::WestGravity,
|
11
|
+
'c' => Magick::CenterGravity,
|
12
|
+
'e' => Magick::EastGravity,
|
13
|
+
'sw' => Magick::SouthWestGravity,
|
14
|
+
's' => Magick::SouthGravity,
|
15
|
+
'se' => Magick::SouthEastGravity,
|
16
|
+
}
|
17
|
+
|
18
|
+
def initialize(*)
|
19
|
+
super
|
20
|
+
self.width = width.to_i
|
21
|
+
self.height = height.to_i
|
22
|
+
raise ArgumentError, ":width must positive" unless width > 0
|
23
|
+
raise ArgumentError, ":height must positive" unless height > 0
|
24
|
+
raise ArgumentError, ":gravity must be within the permitted values" unless GRAVITY_PARAMS.key? gravity
|
25
|
+
end
|
26
|
+
|
27
|
+
def apply!(image)
|
28
|
+
image.crop!(GRAVITY_PARAMS.fetch(gravity), width, height, remove_padding_data_outside_window = true)
|
29
|
+
end
|
30
|
+
|
31
|
+
ImageVise.add_operator 'crop', self
|
32
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# Applies an elliptic stencil around the entire image. The stencil will fit inside the image boundaries,
|
2
|
+
# with about 1 pixel cushion on each side to provide smooth anti-aliased edges. If the input image to be
|
3
|
+
# provessed is square, the ellipse will turn into a neat circle.
|
4
|
+
#
|
5
|
+
# This adds an alpha channel to the image being processed (and premultiplies the RGB channels by it). This
|
6
|
+
# will force the RenderEngine to return the processed image as a PNG in all cases, instead of keeping it
|
7
|
+
# in the original format.
|
8
|
+
#
|
9
|
+
# The corresponding Pipeline method is `ellipse_stencil`.
|
10
|
+
class ImageVise::EllipseStencil
|
11
|
+
C_black = 'black'.freeze
|
12
|
+
C_white = 'white'.freeze
|
13
|
+
private_constant :C_white, :C_black
|
14
|
+
|
15
|
+
def apply!(magick_image)
|
16
|
+
width, height = magick_image.columns, magick_image.rows
|
17
|
+
|
18
|
+
# This is a bit involved. We need to do a manual composite. Here is what it entails.
|
19
|
+
#
|
20
|
+
# Given a premultiplied RGB image B, and a grayscale mask A, we need to do the following
|
21
|
+
# operation:
|
22
|
+
#
|
23
|
+
# BrBgBb / Ba * (Ba * A)
|
24
|
+
#
|
25
|
+
# Since ImageMagick works with unpremultiplied alphas, it is doable - but special care
|
26
|
+
# must be taken not to overmult or overdivide.
|
27
|
+
#
|
28
|
+
# To begin,generate a black and white image for the stencil
|
29
|
+
mask = Magick::Image.new(width, height)
|
30
|
+
draw_circle(mask, width, height)
|
31
|
+
|
32
|
+
# At this stage the mask contains a B/W image of the circle, black outside, white inside.
|
33
|
+
# Retain the alpha of the original in a separate image
|
34
|
+
only_alpha = magick_image.copy
|
35
|
+
only_alpha.alpha(Magick::ExtractAlphaChannel)
|
36
|
+
mask.composite!(only_alpha, Magick::CenterGravity, Magick::MultiplyCompositeOp)
|
37
|
+
|
38
|
+
# With this composite op, enabling alpha on the destination image is
|
39
|
+
# not required - it will be enabled automatically.
|
40
|
+
# The CopyOpacityCompositeOp implies that we copy the grayscale version
|
41
|
+
# of the RGB channels as the alpha channel, so for some weird reason we need
|
42
|
+
# to disable the alpha on our mask image
|
43
|
+
mask.alpha(Magick::DeactivateAlphaChannel)
|
44
|
+
# And perform the operation (set gray(RGB) of mask as the A of magick_image)
|
45
|
+
magick_image.composite!(mask, Magick::CenterGravity, Magick::CopyOpacityCompositeOp)
|
46
|
+
ensure
|
47
|
+
[mask, only_alpha].each do |maybe_image|
|
48
|
+
ImageVise.destroy(maybe_image)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def draw_circle(into_image, width, height)
|
53
|
+
center_x = (width / 2.0)
|
54
|
+
center_y = (height / 2.0)
|
55
|
+
# Make sure all the edges are anti-aliased
|
56
|
+
radius_width = center_x - 1.5
|
57
|
+
radius_height = center_y - 1.5
|
58
|
+
|
59
|
+
gc = Magick::Draw.new
|
60
|
+
gc.fill C_black
|
61
|
+
gc.rectangle(0, 0, width, height)
|
62
|
+
gc.fill C_white
|
63
|
+
gc.ellipse(center_x, center_y, radius_width, radius_height, deg_start=0, deg_end=360)
|
64
|
+
gc.draw(into_image)
|
65
|
+
ensure
|
66
|
+
ImageVise.destroy(gc)
|
67
|
+
end
|
68
|
+
|
69
|
+
ImageVise.add_operator 'ellipse_stencil', self
|
70
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Fits the image based on the smaller-side fit. This means that the image is going to be fit
|
2
|
+
# into the requested rectangle so that all of the pixels of the rectangle are filled. The
|
3
|
+
# gravity parameter defines the crop gravity (on corners, sides, or in the middle).
|
4
|
+
#
|
5
|
+
# The corresponding Pipeline method is `fit_crop`.
|
6
|
+
class ImageVise::FitCrop < Ks.strict(:width, :height, :gravity)
|
7
|
+
GRAVITY_PARAMS = {
|
8
|
+
'nw' => Magick::NorthWestGravity,
|
9
|
+
'n' => Magick::NorthGravity,
|
10
|
+
'ne' => Magick::NorthEastGravity,
|
11
|
+
'w' => Magick::WestGravity,
|
12
|
+
'c' => Magick::CenterGravity,
|
13
|
+
'e' => Magick::EastGravity,
|
14
|
+
'sw' => Magick::SouthWestGravity,
|
15
|
+
's' => Magick::SouthGravity,
|
16
|
+
'se' => Magick::SouthEastGravity,
|
17
|
+
}
|
18
|
+
|
19
|
+
def initialize(*)
|
20
|
+
super
|
21
|
+
self.width = width.to_i
|
22
|
+
self.height = height.to_i
|
23
|
+
raise ArgumentError, ":width must positive" unless width > 0
|
24
|
+
raise ArgumentError, ":height must positive" unless height > 0
|
25
|
+
raise ArgumentError, ":gravity must be within the permitted values" unless GRAVITY_PARAMS.key? gravity
|
26
|
+
end
|
27
|
+
|
28
|
+
def apply!(magick_image)
|
29
|
+
magick_image.resize_to_fill! width, height, GRAVITY_PARAMS.fetch(gravity)
|
30
|
+
end
|
31
|
+
|
32
|
+
ImageVise.add_operator 'fit_crop', self
|
33
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# Forces the output format to be JPEG and specifies the quality factor to use when saving
|
2
|
+
#
|
3
|
+
# The corresponding Pipeline method is `force_jpg_out`.
|
4
|
+
class ImageVise::ForceJPGOut < Ks.strict(:quality)
|
5
|
+
def initialize(quality:)
|
6
|
+
unless (0..100).cover?(quality)
|
7
|
+
raise ArgumentError, "the :quality setting must be within 0..100, but was %d" % quality
|
8
|
+
end
|
9
|
+
self.quality = quality
|
10
|
+
end
|
11
|
+
|
12
|
+
def apply!(_, metadata)
|
13
|
+
metadata[:writer] = ImageVise::JPGWriter.new(quality: quality)
|
14
|
+
end
|
15
|
+
|
16
|
+
ImageVise.add_operator 'force_jpg_out', self
|
17
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# Applies a transformation using an ImageMagick geometry string
|
2
|
+
#
|
3
|
+
# The corresponding Pipeline method is `geom`.
|
4
|
+
class ImageVise::Geom < Ks.strict(:geometry_string)
|
5
|
+
def initialize(*)
|
6
|
+
super
|
7
|
+
self.geometry_string = geometry_string.to_s
|
8
|
+
raise ArgumentError, "the :geom parameter must be present and not empty" if self.geometry_string.empty?
|
9
|
+
end
|
10
|
+
|
11
|
+
def apply!(image)
|
12
|
+
image.change_geometry(geometry_string) { |cols, rows, _| image.resize!(cols,rows) }
|
13
|
+
end
|
14
|
+
|
15
|
+
ImageVise.add_operator 'geom', self
|
16
|
+
end
|
Binary file
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Applies a sharpening filter to the image.
|
2
|
+
#
|
3
|
+
# The corresponding Pipeline method is `sharpen`.
|
4
|
+
class ImageVise::Sharpen < Ks.strict(:radius, :sigma)
|
5
|
+
def initialize(*)
|
6
|
+
super
|
7
|
+
self.radius = radius.to_f
|
8
|
+
self.sigma = sigma.to_f
|
9
|
+
raise ArgumentError, ":radius must positive" unless sigma > 0
|
10
|
+
raise ArgumentError, ":sigma must positive" unless sigma > 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def apply!(magick_image)
|
14
|
+
sharpened_image = magick_image.sharpen(radius, sigma)
|
15
|
+
magick_image.composite!(sharpened_image, Magick::CenterGravity, Magick::CopyCompositeOp)
|
16
|
+
ensure
|
17
|
+
ImageVise.destroy(sharpened_image)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
ImageVise.add_operator 'sharpen', ImageVise::Sharpen
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# Applies the sRGB profile to the image.
|
2
|
+
# For this to work, your ImageMagick must be built
|
3
|
+
# witl LCMS support. On OSX, you need to use the brew install
|
4
|
+
# command with the following options:
|
5
|
+
#
|
6
|
+
# $brew install imagemagick --with-little-cms --with-little-cms2
|
7
|
+
#
|
8
|
+
# You can verify if you do have LittleCMS support by checking the
|
9
|
+
# delegates list that `$convert --version` outputs:
|
10
|
+
#
|
11
|
+
# For instance, if you do not have it, the list will look like this:
|
12
|
+
#
|
13
|
+
# $ convert --version
|
14
|
+
# ...
|
15
|
+
# Delegates (built-in): bzlib freetype jng jpeg ltdl lzma png tiff xml zlib
|
16
|
+
#
|
17
|
+
# whereas if you do, the list will include the "lcms" delegate:
|
18
|
+
#
|
19
|
+
# $ convert --version
|
20
|
+
# ...
|
21
|
+
# Delegates (built-in): bzlib freetype jng jpeg lcms ltdl lzma png tiff xml zlib
|
22
|
+
#
|
23
|
+
# The corresponding Pipeline method is `srgb`.
|
24
|
+
class ImageVise::SRGB
|
25
|
+
PROFILE_PATH = File.expand_path(__dir__ + '/sRGB_v4_ICC_preference_displayclass.icc')
|
26
|
+
def apply!(magick_image)
|
27
|
+
magick_image.add_profile(PROFILE_PATH)
|
28
|
+
end
|
29
|
+
ImageVise.add_operator 'srgb', self
|
30
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# Strips metadata from the image (EXIF, IPTC etc.) using the
|
2
|
+
# RMagick `strip!` method
|
3
|
+
#
|
4
|
+
# The corresponding Pipeline method is `strip_metadata`.
|
5
|
+
class ImageVise::StripMetadata
|
6
|
+
def apply!(magick_image)
|
7
|
+
magick_image.strip!
|
8
|
+
end
|
9
|
+
ImageVise.add_operator 'strip_metadata', self
|
10
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
class ImageVise::Pipeline
|
2
|
+
def self.operator_by_name(name)
|
3
|
+
operator = ImageVise.operator_from(name) or raise "Unknown operator #{name}"
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.from_param(array_of_operator_names_to_operator_params)
|
7
|
+
operators = array_of_operator_names_to_operator_params.map do |(operator_name, operator_params)|
|
8
|
+
operator_class = operator_by_name(operator_name)
|
9
|
+
if operator_params && operator_params.any? && operator_class.method(:new).arity.nonzero?
|
10
|
+
operator_class.new(**operator_params)
|
11
|
+
else
|
12
|
+
operator_class.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
new(operators)
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(operators = [])
|
19
|
+
@ops = operators.to_a
|
20
|
+
end
|
21
|
+
|
22
|
+
def <<(image_operator)
|
23
|
+
@ops << image_operator; self
|
24
|
+
end
|
25
|
+
|
26
|
+
def empty?
|
27
|
+
@ops.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
def method_missing(method_name, *a, &blk)
|
31
|
+
operator_builder = ImageVise.operator_from(method_name)
|
32
|
+
self << operator_builder.new(*a)
|
33
|
+
end
|
34
|
+
|
35
|
+
def respond_to_missing?(method_name, *a)
|
36
|
+
ImageVise.defined_operators.include?(method_name.to_s)
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_params
|
40
|
+
@ops.map do |operator|
|
41
|
+
operator_name = ImageVise.operator_name_for(operator)
|
42
|
+
operator_params = operator.respond_to?(:to_h) ? operator.to_h : {}
|
43
|
+
[operator_name, operator_params]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def apply!(magick_image, image_metadata)
|
48
|
+
@ops.each do |operator|
|
49
|
+
apply_operator_passing_metadata(magick_image, operator, image_metadata)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def apply_operator_passing_metadata(magick_image, operator, image_metadata)
|
54
|
+
if operator.method(:apply!).arity == 1
|
55
|
+
operator.apply!(magick_image)
|
56
|
+
else
|
57
|
+
operator.apply!(magick_image, image_metadata)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def each(&b)
|
62
|
+
@ops.each(&b)
|
63
|
+
end
|
64
|
+
end
|