image_vise 0.2.1 → 0.2.2
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.
- 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
|