image_vise 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,27 +0,0 @@
1
- class ImageVise::FetcherFile
2
- class AccessError < StandardError
3
- def http_status; 403; end
4
- end
5
- def self.fetch_uri_to_tempfile(uri)
6
- tf = Tempfile.new 'imagevise-localfs-copy'
7
- real_path_on_filesystem = File.expand_path(URI.decode(uri.path))
8
- verify_filesystem_access! real_path_on_filesystem
9
- # Do the checks
10
- File.open(real_path_on_filesystem, 'rb') do |f|
11
- IO.copy_stream(f, tf)
12
- end
13
- tf.rewind; tf
14
- rescue Exception => e
15
- ImageVise.close_and_unlink(tf)
16
- raise e
17
- end
18
-
19
- def self.verify_filesystem_access!(path_on_filesystem)
20
- patterns = ImageVise.allowed_filesystem_sources
21
- matches = patterns.any? { |glob_pattern| File.fnmatch?(glob_pattern, path_on_filesystem) }
22
- raise AccessError, "filesystem access is disabled" unless patterns.any?
23
- raise AccessError, "#{path_on_filesystem} is not on the path whitelist" unless matches
24
- end
25
-
26
- ImageVise.register_fetcher 'file', self
27
- end
@@ -1,42 +0,0 @@
1
- class ImageVise::FetcherHTTP
2
- EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS = 5
3
-
4
- class AccessError < StandardError; end
5
-
6
- class UpstreamError < StandardError
7
- attr_accessor :http_status
8
- def initialize(http_status, message)
9
- super(message)
10
- @http_status = http_status
11
- end
12
- end
13
-
14
- def self.fetch_uri_to_tempfile(uri)
15
- tf = Tempfile.new 'imagevise-http-download'
16
- verify_uri_access!(uri)
17
- s = Patron::Session.new
18
- s.automatic_content_encoding = true
19
- s.timeout = EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS
20
- s.connect_timeout = EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS
21
-
22
- response = s.get_file(uri.to_s, tf.path)
23
-
24
- if response.status != 200
25
- raise UpstreamError.new(response.status, "Unfortunate upstream response #{response.status} on #{uri}")
26
- end
27
-
28
- tf
29
- rescue Exception => e
30
- ImageVise.close_and_unlink(tf)
31
- raise e
32
- end
33
-
34
- def self.verify_uri_access!(uri)
35
- host = uri.host
36
- return if ImageVise.allowed_hosts.include?(uri.host)
37
- raise AccessError, "#{uri} is not permitted as source"
38
- end
39
-
40
- ImageVise.register_fetcher 'http', self
41
- ImageVise.register_fetcher 'https', self
42
- end
@@ -1,22 +0,0 @@
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
@@ -1,70 +0,0 @@
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
@@ -1,10 +0,0 @@
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
@@ -1,18 +0,0 @@
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
@@ -1,32 +0,0 @@
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
@@ -1,70 +0,0 @@
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
@@ -1,33 +0,0 @@
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
@@ -1,17 +0,0 @@
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
@@ -1,16 +0,0 @@
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
@@ -1,21 +0,0 @@
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
@@ -1,30 +0,0 @@
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
@@ -1,10 +0,0 @@
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
@@ -1,64 +0,0 @@
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