image_vise 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +8 -0
  3. data/.travis.yml +13 -0
  4. data/DEVELOPMENT.md +0 -11
  5. data/Gemfile +2 -20
  6. data/Rakefile +3 -26
  7. data/image_vise.gemspec +37 -132
  8. data/lib/image_vise/file_response.rb +2 -2
  9. data/lib/image_vise/image_request.rb +2 -0
  10. data/lib/image_vise/operators/background_fill.rb +18 -0
  11. data/lib/image_vise/operators/ellipse_stencil.rb +7 -5
  12. data/lib/image_vise/operators/force_jpg_out.rb +17 -0
  13. data/lib/image_vise/pipeline.rb +13 -3
  14. data/lib/image_vise/render_engine.rb +36 -64
  15. data/lib/image_vise/version.rb +3 -0
  16. data/lib/image_vise/writers/auto_writer.rb +23 -0
  17. data/lib/image_vise/writers/jpeg_writer.rb +9 -0
  18. data/lib/image_vise.rb +19 -19
  19. metadata +43 -135
  20. data/spec/image_vise/auto_orient_spec.rb +0 -10
  21. data/spec/image_vise/crop_spec.rb +0 -20
  22. data/spec/image_vise/ellipse_stencil_spec.rb +0 -19
  23. data/spec/image_vise/fetcher_file_spec.rb +0 -48
  24. data/spec/image_vise/fetcher_http_spec.rb +0 -44
  25. data/spec/image_vise/file_response_spec.rb +0 -45
  26. data/spec/image_vise/fit_crop_spec.rb +0 -20
  27. data/spec/image_vise/geom_spec.rb +0 -33
  28. data/spec/image_vise/image_request_spec.rb +0 -62
  29. data/spec/image_vise/pipeline_spec.rb +0 -72
  30. data/spec/image_vise/render_engine_spec.rb +0 -325
  31. data/spec/image_vise/sharpen_spec.rb +0 -17
  32. data/spec/image_vise/srgb_spec.rb +0 -28
  33. data/spec/image_vise/strip_metadata_spec.rb +0 -14
  34. data/spec/image_vise_spec.rb +0 -110
  35. data/spec/layers-with-blending.psd +0 -0
  36. data/spec/spec_helper.rb +0 -103
  37. data/spec/test_server.rb +0 -61
  38. data/spec/waterside_magic_hour.jpg +0 -0
  39. data/spec/waterside_magic_hour.psd +0 -0
  40. data/spec/waterside_magic_hour_adobergb.jpg +0 -0
  41. data/spec/waterside_magic_hour_gray.tif +0 -0
  42. data/spec/waterside_magic_hour_transp.png +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2176fbaf15e4bc2d36a698796bfd934f84100070
4
- data.tar.gz: 56396a529ce373d32b7a2a27522c8ad82ac65bf9
3
+ metadata.gz: fe3aff4ba407443d636d9b23b4d7a2a55d74fc8d
4
+ data.tar.gz: 34a54619e0a29cb45594975dcf8b74b43742bb49
5
5
  SHA512:
6
- metadata.gz: cb597e00191e3f4428c35f295041ba0b93181f7fb309f3bb2790d93641d1487a3d9f374acc260a20b93b3acc2987decbf0960266cd2518d2b006243e7dc46e4b
7
- data.tar.gz: ac54cb79ad6d908e27fb2b0e26dd538a84db39fea929bdf72928587dcc4378764ed4a02fac458e6e0ef28fbb0ca6e3264cecfd740e4bd1d37b0744ba83675d76
6
+ metadata.gz: 8db53ad9d51d8a367cc316472a54fcb9eaa54e6d0c666fbe16de26ad00c336a8796c390bc343677fee635cf455352414bee43782899a162bb30089d79bc82f72
7
+ data.tar.gz: 22bc8553e556428e459dc794c123a001dce16f4df527336ecaec8cafa7f9c8f6fd91cad80d20414c0bbc2e03afb28e4c08a507decfcf2d9d4c22348cd91d863d
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ scratchpad
2
+ Gemfile.lock
3
+ .bundle
4
+ .env
5
+ *.sqlite3
6
+ *.log
7
+ pkg
8
+ coverage
data/.travis.yml ADDED
@@ -0,0 +1,13 @@
1
+ rvm:
2
+ - 2.1
3
+ - 2.2
4
+ - 2.3.0
5
+ - 2.4.1
6
+ sudo: false
7
+ cache: bundler
8
+
9
+ env:
10
+ global:
11
+ - SKIP_INTERACTIVE=yes
12
+
13
+ script: bundle exec rspec
data/DEVELOPMENT.md CHANGED
@@ -109,14 +109,3 @@ actually using them. If you are using a library it is a one-time cost, with very
109
109
  Also note that image operators are not per definition Imagemagick-specific - it's completely possible to not only use
110
110
  a different library for processing them, but even to use a different image processing server complying to the
111
111
  same protocol (a signed JSON-encodded waybill of HTTP(S) source-URL + pipeline instructions).
112
-
113
- ## Using forked child processes for RMagick tasks
114
-
115
- You can optionally set the `IMAGE_VISE_ENABLE_FORK` environment variable to `yes` to enable forking. When this
116
- variable is set, ImageVise will fork a child process and perform the image processing task within that process,
117
- killing it afterwards and deallocating all the memory. This can be extremely efficient for dealing with potential
118
- memory bloat issues in ImageMagick/RMagick. However, loading images into RMagick may hang in a forked child. This
119
- will lead to the child being timeout-terminated, and no image is going to be rendered. This issue is known and
120
- also platform-dependent (it does not happen on OSX but does happen on Docker within Ubuntu Trusty for instance).
121
-
122
- So, this feature _does_ exist but your mileage may vary with regards to it's use.
data/Gemfile CHANGED
@@ -1,22 +1,4 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem 'patron', '~> 0.6'
4
- gem 'rmagick', '~> 2.15', :require => 'rmagick'
5
- gem 'exceptional_fork', '~> 1.2'
6
- gem 'ks'
7
- gem 'magic_bytes'
8
-
9
- group :development do
10
- gem 'bundler'
11
- gem 'yard'
12
- gem 'simplecov'
13
- gem 'rack-cache'
14
- gem 'strenv'
15
- gem 'addressable', :require => %w( addressable/uri )
16
- gem 'rack', '~> 1'
17
- gem 'rack-test'
18
- gem 'foreman'
19
- gem 'rspec', '~> 3.2', '< 3.3'
20
- gem 'rake', '~> 10'
21
- gem "jeweler"
22
- end
3
+ # Specify your gem's dependencies in image_vise.gemspec
4
+ gemspec
data/Rakefile CHANGED
@@ -1,29 +1,6 @@
1
- require 'rspec/core/rake_task'
2
- require 'jeweler'
3
- require_relative 'lib/image_vise'
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
4
3
 
5
- Jeweler::Tasks.new do |gem|
6
- gem.version = ImageVise::VERSION
7
- gem.name = "image_vise"
8
- gem.summary = "Runtime thumbnailing proxy"
9
- gem.description = "Image processing via URLs"
10
- gem.email = "me@julik.nl"
11
- gem.homepage = "https://github.com/WeTransfer/image_vise"
12
- gem.authors = ["Julik Tarkhanov"]
13
- gem.license = 'MIT'
4
+ RSpec::Core::RakeTask.new(:spec)
14
5
 
15
- # Do not package invisibles
16
- gem.files.exclude ".*"
17
-
18
- # When running as a gem, do not lock all of our versions
19
- # even though the lockfile is in the repo for running standalone
20
- gem.files.exclude "Gemfile.lock"
21
- end
22
-
23
- Jeweler::RubygemsDotOrgTasks.new
24
-
25
- RSpec::Core::RakeTask.new(:spec) do |t|
26
- t.rspec_opts = ["-c", "-f progress", "-r ./spec/spec_helper.rb"]
27
- t.pattern = 'spec/**/*_spec.rb'
28
- end
29
6
  task :default => :spec
data/image_vise.gemspec CHANGED
@@ -1,138 +1,43 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
- # -*- encoding: utf-8 -*-
5
- # stub: image_vise 0.1.6 ruby lib
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'image_vise/version'
6
5
 
7
- Gem::Specification.new do |s|
8
- s.name = "image_vise"
9
- s.version = "0.1.6"
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "image_vise"
8
+ spec.version = ImageVise::VERSION
9
+ spec.authors = ["Julik Tarkhanov"]
10
+ spec.email = ["me@julik.nl"]
10
11
 
11
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
- s.require_paths = ["lib"]
13
- s.authors = ["Julik Tarkhanov"]
14
- s.date = "2016-12-07"
15
- s.description = "Image processing via URLs"
16
- s.email = "me@julik.nl"
17
- s.extra_rdoc_files = [
18
- "LICENSE.txt",
19
- "README.md"
20
- ]
21
- s.files = [
22
- "DEVELOPMENT.md",
23
- "Gemfile",
24
- "LICENSE.txt",
25
- "README.md",
26
- "Rakefile",
27
- "SECURITY.md",
28
- "examples/config.ru",
29
- "examples/custom_image_operator.rb",
30
- "examples/error_handline_appsignal.rb",
31
- "examples/error_handling_sentry.rb",
32
- "image_vise.gemspec",
33
- "lib/image_vise.rb",
34
- "lib/image_vise/fetchers/fetcher_file.rb",
35
- "lib/image_vise/fetchers/fetcher_http.rb",
36
- "lib/image_vise/file_response.rb",
37
- "lib/image_vise/image_request.rb",
38
- "lib/image_vise/operators/auto_orient.rb",
39
- "lib/image_vise/operators/crop.rb",
40
- "lib/image_vise/operators/ellipse_stencil.rb",
41
- "lib/image_vise/operators/fit_crop.rb",
42
- "lib/image_vise/operators/geom.rb",
43
- "lib/image_vise/operators/sRGB_v4_ICC_preference_displayclass.icc",
44
- "lib/image_vise/operators/sharpen.rb",
45
- "lib/image_vise/operators/srgb.rb",
46
- "lib/image_vise/operators/strip_metadata.rb",
47
- "lib/image_vise/pipeline.rb",
48
- "lib/image_vise/render_engine.rb",
49
- "spec/image_vise/auto_orient_spec.rb",
50
- "spec/image_vise/crop_spec.rb",
51
- "spec/image_vise/ellipse_stencil_spec.rb",
52
- "spec/image_vise/fetcher_file_spec.rb",
53
- "spec/image_vise/fetcher_http_spec.rb",
54
- "spec/image_vise/file_response_spec.rb",
55
- "spec/image_vise/fit_crop_spec.rb",
56
- "spec/image_vise/geom_spec.rb",
57
- "spec/image_vise/image_request_spec.rb",
58
- "spec/image_vise/pipeline_spec.rb",
59
- "spec/image_vise/render_engine_spec.rb",
60
- "spec/image_vise/sharpen_spec.rb",
61
- "spec/image_vise/srgb_spec.rb",
62
- "spec/image_vise/strip_metadata_spec.rb",
63
- "spec/image_vise_spec.rb",
64
- "spec/layers-with-blending.psd",
65
- "spec/spec_helper.rb",
66
- "spec/test_server.rb",
67
- "spec/waterside_magic_hour.jpg",
68
- "spec/waterside_magic_hour.psd",
69
- "spec/waterside_magic_hour_adobergb.jpg",
70
- "spec/waterside_magic_hour_gray.tif",
71
- "spec/waterside_magic_hour_transp.png"
72
- ]
73
- s.homepage = "https://github.com/WeTransfer/image_vise"
74
- s.licenses = ["MIT"]
75
- s.rubygems_version = "2.4.5.1"
76
- s.summary = "Runtime thumbnailing proxy"
12
+ spec.summary = "Runtime thumbnailing proxy"
13
+ spec.description = "Image processing via URLs"
14
+ spec.homepage = "https://github.com/WeTransfer/image_vise"
15
+ spec.license = "MIT"
77
16
 
78
- if s.respond_to? :specification_version then
79
- s.specification_version = 4
80
-
81
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
82
- s.add_runtime_dependency(%q<patron>, ["~> 0.6"])
83
- s.add_runtime_dependency(%q<rmagick>, ["~> 2.15"])
84
- s.add_runtime_dependency(%q<exceptional_fork>, ["~> 1.2"])
85
- s.add_runtime_dependency(%q<ks>, [">= 0"])
86
- s.add_runtime_dependency(%q<magic_bytes>, [">= 0"])
87
- s.add_development_dependency(%q<bundler>, [">= 0"])
88
- s.add_development_dependency(%q<yard>, [">= 0"])
89
- s.add_development_dependency(%q<simplecov>, [">= 0"])
90
- s.add_development_dependency(%q<rack-cache>, [">= 0"])
91
- s.add_development_dependency(%q<strenv>, [">= 0"])
92
- s.add_development_dependency(%q<addressable>, [">= 0"])
93
- s.add_development_dependency(%q<rack>, ["~> 1"])
94
- s.add_development_dependency(%q<rack-test>, [">= 0"])
95
- s.add_development_dependency(%q<foreman>, [">= 0"])
96
- s.add_development_dependency(%q<rspec>, ["< 3.3", "~> 3.2"])
97
- s.add_development_dependency(%q<rake>, ["~> 10"])
98
- s.add_development_dependency(%q<jeweler>, [">= 0"])
99
- else
100
- s.add_dependency(%q<patron>, ["~> 0.6"])
101
- s.add_dependency(%q<rmagick>, ["~> 2.15"])
102
- s.add_dependency(%q<exceptional_fork>, ["~> 1.2"])
103
- s.add_dependency(%q<ks>, [">= 0"])
104
- s.add_dependency(%q<magic_bytes>, [">= 0"])
105
- s.add_dependency(%q<bundler>, [">= 0"])
106
- s.add_dependency(%q<yard>, [">= 0"])
107
- s.add_dependency(%q<simplecov>, [">= 0"])
108
- s.add_dependency(%q<rack-cache>, [">= 0"])
109
- s.add_dependency(%q<strenv>, [">= 0"])
110
- s.add_dependency(%q<addressable>, [">= 0"])
111
- s.add_dependency(%q<rack>, ["~> 1"])
112
- s.add_dependency(%q<rack-test>, [">= 0"])
113
- s.add_dependency(%q<foreman>, [">= 0"])
114
- s.add_dependency(%q<rspec>, ["< 3.3", "~> 3.2"])
115
- s.add_dependency(%q<rake>, ["~> 10"])
116
- s.add_dependency(%q<jeweler>, [">= 0"])
117
- end
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
118
21
  else
119
- s.add_dependency(%q<patron>, ["~> 0.6"])
120
- s.add_dependency(%q<rmagick>, ["~> 2.15"])
121
- s.add_dependency(%q<exceptional_fork>, ["~> 1.2"])
122
- s.add_dependency(%q<ks>, [">= 0"])
123
- s.add_dependency(%q<magic_bytes>, [">= 0"])
124
- s.add_dependency(%q<bundler>, [">= 0"])
125
- s.add_dependency(%q<yard>, [">= 0"])
126
- s.add_dependency(%q<simplecov>, [">= 0"])
127
- s.add_dependency(%q<rack-cache>, [">= 0"])
128
- s.add_dependency(%q<strenv>, [">= 0"])
129
- s.add_dependency(%q<addressable>, [">= 0"])
130
- s.add_dependency(%q<rack>, ["~> 1"])
131
- s.add_dependency(%q<rack-test>, [">= 0"])
132
- s.add_dependency(%q<foreman>, [">= 0"])
133
- s.add_dependency(%q<rspec>, ["< 3.3", "~> 3.2"])
134
- s.add_dependency(%q<rake>, ["~> 10"])
135
- s.add_dependency(%q<jeweler>, [">= 0"])
22
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
136
23
  end
137
- end
138
24
 
25
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_dependency 'patron', '~> 0.6'
31
+ spec.add_dependency 'rmagick', '~> 2.15'
32
+ spec.add_dependency 'ks'
33
+ spec.add_dependency 'magic_bytes', '~> 1'
34
+ spec.add_dependency 'rack', '~> 1'
35
+
36
+ spec.add_development_dependency "bundler", "~> 1.7"
37
+ spec.add_development_dependency "rake", "~> 10.0"
38
+ spec.add_development_dependency "rack-test"
39
+ spec.add_development_dependency "rspec", "~> 3.0"
40
+ spec.add_development_dependency "addressable"
41
+ spec.add_development_dependency "strenv"
42
+ spec.add_development_dependency "simplecov"
43
+ end
@@ -5,7 +5,7 @@ class ImageVise::FileResponse
5
5
  def initialize(file)
6
6
  @file = file
7
7
  end
8
-
8
+
9
9
  def each
10
10
  @file.flush # Make sure all the writes have been synchronized
11
11
  # We can easily open another file descriptor
@@ -15,7 +15,7 @@ class ImageVise::FileResponse
15
15
  end
16
16
  end
17
17
  end
18
-
18
+
19
19
  def close
20
20
  ImageVise.close_and_unlink(@file)
21
21
  end
@@ -1,3 +1,5 @@
1
+ require 'openssl'
2
+
1
3
  class ImageVise::ImageRequest < Ks.strict(:src_url, :pipeline)
2
4
  class InvalidRequest < ArgumentError; end
3
5
  class SignatureError < InvalidRequest; 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
@@ -9,7 +9,8 @@
9
9
  # The corresponding Pipeline method is `ellipse_stencil`.
10
10
  class ImageVise::EllipseStencil
11
11
  C_black = 'black'.freeze
12
- private_constant :C_black
12
+ C_white = 'white'.freeze
13
+ private_constant :C_white, :C_black
13
14
 
14
15
  def apply!(magick_image)
15
16
  width, height = magick_image.columns, magick_image.rows
@@ -25,9 +26,8 @@ class ImageVise::EllipseStencil
25
26
  # must be taken not to overmult or overdivide.
26
27
  #
27
28
  # To begin,generate a black and white image for the stencil
28
- circle_img = Magick::Image.new(width, height)
29
- draw_circle(circle_img, width, height)
30
- mask = circle_img.negate
29
+ mask = Magick::Image.new(width, height)
30
+ draw_circle(mask, width, height)
31
31
 
32
32
  # At this stage the mask contains a B/W image of the circle, black outside, white inside.
33
33
  # Retain the alpha of the original in a separate image
@@ -44,7 +44,7 @@ class ImageVise::EllipseStencil
44
44
  # And perform the operation (set gray(RGB) of mask as the A of magick_image)
45
45
  magick_image.composite!(mask, Magick::CenterGravity, Magick::CopyOpacityCompositeOp)
46
46
  ensure
47
- [mask, only_alpha, circle_img].each do |maybe_image|
47
+ [mask, only_alpha].each do |maybe_image|
48
48
  ImageVise.destroy(maybe_image)
49
49
  end
50
50
  end
@@ -58,6 +58,8 @@ class ImageVise::EllipseStencil
58
58
 
59
59
  gc = Magick::Draw.new
60
60
  gc.fill C_black
61
+ gc.rectangle(0, 0, width, height)
62
+ gc.fill C_white
61
63
  gc.ellipse(center_x, center_y, radius_width, radius_height, deg_start=0, deg_end=360)
62
64
  gc.draw(into_image)
63
65
  ensure
@@ -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
@@ -44,10 +44,20 @@ class ImageVise::Pipeline
44
44
  end
45
45
  end
46
46
 
47
- def apply!(magick_image)
48
- @ops.each{|e| e.apply!(magick_image) }
47
+ def apply!(magick_image, image_metadata)
48
+ @ops.each do |operator|
49
+ apply_operator_passing_metadata(magick_image, operator, image_metadata)
50
+ end
49
51
  end
50
-
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
+
51
61
  def each(&b)
52
62
  @ops.each(&b)
53
63
  end
@@ -1,11 +1,11 @@
1
- class ImageVise::RenderEngine
1
+ class ImageVise::RenderEngine
2
2
  class UnsupportedInputFormat < StandardError; end
3
3
  class EmptyRender < StandardError; end
4
4
 
5
5
  DEFAULT_HEADERS = {
6
6
  'Allow' => "GET"
7
7
  }.freeze
8
-
8
+
9
9
  # Headers for error responses that denote an invalid or
10
10
  # an unsatisfiable request
11
11
  JSON_ERROR_HEADERS_REQUEST = DEFAULT_HEADERS.merge({
@@ -19,7 +19,7 @@ class ImageVise::RenderEngine
19
19
  'Content-Type' => 'application/json',
20
20
  'Cache-Control' => 'public, max-age=5'
21
21
  }).freeze
22
-
22
+
23
23
  # "public" of course. Add max-age so that there is _some_
24
24
  # revalidation after a time (otherwise some proxies treat it
25
25
  # as "must-revalidate" always), and "no-transform" so that
@@ -27,7 +27,7 @@ class ImageVise::RenderEngine
27
27
  # with Rack::Cache and leads Chrome to throw up on content
28
28
  # decoding for example).
29
29
  IMAGE_CACHE_CONTROL = 'public, no-transform, max-age=2592000'
30
-
30
+
31
31
  # How long is a render (the ImageMagick/write part) is allowed to
32
32
  # take before we kill it
33
33
  RENDER_TIMEOUT_SECONDS = 10
@@ -35,16 +35,9 @@ class ImageVise::RenderEngine
35
35
  # Which input files we permit (based on extensions stored in MagicBytes)
36
36
  PERMITTED_SOURCE_FILE_EXTENSIONS = %w( gif png jpg psd tif)
37
37
 
38
- # Which output files are permitted (regardless of the input format
39
- # the processed images will be converted to one of these types)
40
- PERMITTED_OUTPUT_FILE_EXTENSIONS = %W( gif png jpg)
41
-
42
38
  # How long should we wait when fetching the image from the external host
43
39
  EXTERNAL_IMAGE_FETCH_TIMEOUT_SECONDS = 4
44
-
45
- # The default file type for images with alpha
46
- PNG_FILE_TYPE = MagicBytes::FileType.new('png','image/png').freeze
47
-
40
+
48
41
  def bail(status, *errors_array)
49
42
  headers = if (300...500).cover?(status)
50
43
  JSON_ERROR_HEADERS_REQUEST.dup
@@ -54,7 +47,7 @@ class ImageVise::RenderEngine
54
47
  response = [status.to_i, headers, [JSON.pretty_generate({errors: errors_array})]]
55
48
  throw :__bail, response
56
49
  end
57
-
50
+
58
51
  # The main entry point for the Rack app. Wraps a call to {#handle_request} in a `catch{}` block
59
52
  # so that any method can abort the request by calling {#bail}
60
53
  #
@@ -63,7 +56,7 @@ class ImageVise::RenderEngine
63
56
  def call(env)
64
57
  catch(:__bail) { handle_request(env) }
65
58
  end
66
-
59
+
67
60
  # Hadles the Rack request. If one of the steps calls {#bail} the `:__bail` symbol will be
68
61
  # thrown and the execution will abort. Any errors will cause either an error response in
69
62
  # JSON format or an Exception will be raised (depending on the return value of `raise_exceptions?`)
@@ -80,9 +73,9 @@ class ImageVise::RenderEngine
80
73
  req = parse_env_into_request(env)
81
74
  bail(405, 'Only GET supported') unless req.get?
82
75
  params = extract_params_from_request(req)
83
-
76
+
84
77
  image_request = ImageVise::ImageRequest.from_params(qs_params: params, secrets: ImageVise.secret_keys)
85
- render_destination_file, render_file_type, etag = process_image_request(image_request)
78
+ render_destination_file, render_file_type, etag = process_image_request(image_request)
86
79
  image_rack_response(render_destination_file, render_file_type, etag)
87
80
  rescue *permanent_failures => e
88
81
  handle_request_error(e)
@@ -97,7 +90,7 @@ class ImageVise::RenderEngine
97
90
  raise_exception_or_error_response(e, 500)
98
91
  end
99
92
  end
100
-
93
+
101
94
  # Parses the Rack environment into a Rack::Reqest. The following methods
102
95
  # are going to be called on it: `#get?` and `#params`. You can use this
103
96
  # method to override path-to-parameter translation for example.
@@ -115,7 +108,7 @@ class ImageVise::RenderEngine
115
108
  def extract_params_from_request(rack_request)
116
109
  # Prevent cache bypass DOS attacks by only permitting :sig and :q
117
110
  bail(400, 'Query strings are not supported') if rack_request.params.any?
118
-
111
+
119
112
  # Extract the tail (signature) and the front (the Base64-encoded request).
120
113
  # Slashes within :q are masked by ImageRequest already, so we don't have
121
114
  # to worry about them.
@@ -143,11 +136,11 @@ class ImageVise::RenderEngine
143
136
  # Compute an ETag which describes this image transform + image source location.
144
137
  # Assume the image URL contents does _never_ change.
145
138
  etag = image_request.cache_etag
146
-
139
+
147
140
  # Download/copy the original into a Tempfile
148
141
  fetcher = ImageVise.fetcher_for(source_image_uri.scheme)
149
142
  source_file = fetcher.fetch_uri_to_tempfile(source_image_uri)
150
-
143
+
151
144
  # Make sure we do not try to process something...questionable
152
145
  source_file_type = detect_file_type(source_file)
153
146
  unless source_file_type_permitted?(source_file_type)
@@ -156,15 +149,8 @@ class ImageVise::RenderEngine
156
149
 
157
150
  render_destination_file = Tempfile.new('imagevise-render').tap{|f| f.binmode }
158
151
 
159
- # Perform the processing
160
- if enable_forking?
161
- require 'exceptional_fork'
162
- ExceptionalFork.fork_and_wait do
163
- apply_pipeline(source_file.path, pipeline, source_file_type, render_destination_file.path)
164
- end
165
- else
166
- apply_pipeline(source_file.path, pipeline, source_file_type, render_destination_file.path)
167
- end
152
+ # Do the actual imaging stuff
153
+ apply_pipeline(source_file.path, pipeline, source_file_type, render_destination_file.path)
168
154
 
169
155
  # Catch this one early
170
156
  render_destination_file.rewind
@@ -175,7 +161,7 @@ class ImageVise::RenderEngine
175
161
  ensure
176
162
  ImageVise.close_and_unlink(source_file)
177
163
  end
178
-
164
+
179
165
  # Returns a Rack response triplet. Accepts the return value of
180
166
  # `process_image_request` unsplatted, and returns a triplet that
181
167
  # can be returned as a Rack response. The Rack response will contain
@@ -204,13 +190,13 @@ class ImageVise::RenderEngine
204
190
  # @param exception[Exception] the error that has to be captured
205
191
  # @param status_code[Fixnum] the HTTP status code
206
192
  def raise_exception_or_error_response(exception, status_code)
207
- if raise_exceptions?
193
+ if raise_exceptions?
208
194
  raise exception
209
195
  else
210
196
  bail status_code, exception.message
211
197
  end
212
198
  end
213
-
199
+
214
200
  # Detects the file type of the given File and returns
215
201
  # a MagicBytes::FileType object that contains the extension and
216
202
  # the MIME type.
@@ -230,15 +216,6 @@ class ImageVise::RenderEngine
230
216
  PERMITTED_SOURCE_FILE_EXTENSIONS.include?(magic_bytes_file_info.ext)
231
217
  end
232
218
 
233
- # Tells whether the given file type may be returned
234
- # as the result of the render
235
- #
236
- # @param magic_bytes_file_info[MagicBytes::FileType] the filetype
237
- # @return [Boolean]
238
- def output_file_type_permitted?(magic_bytes_file_info)
239
- PERMITTED_OUTPUT_FILE_EXTENSIONS.include?(magic_bytes_file_info.ext)
240
- end
241
-
242
219
  # Lists exceptions that should lead to the request being flagged
243
220
  # as invalid (4xx as opposed to 5xx for a generic server error).
244
221
  # Decent clients should _not_ retry those requests.
@@ -249,7 +226,7 @@ class ImageVise::RenderEngine
249
226
  ImageVise::ImageRequest::InvalidRequest
250
227
  ]
251
228
  end
252
-
229
+
253
230
  # Is meant to be overridden by subclasses,
254
231
  # will be called at the start of each request to set up the error handling
255
232
  # library (Appsignal, Honeybadger, Sentry...)
@@ -278,7 +255,7 @@ class ImageVise::RenderEngine
278
255
  # @return [void]
279
256
  def handle_generic_error(exception)
280
257
  end
281
-
258
+
282
259
  # Tells whether the engine must raise the exceptions further up the Rack stack,
283
260
  # or they should be suppressed and a JSON response must be returned.
284
261
  #
@@ -286,14 +263,7 @@ class ImageVise::RenderEngine
286
263
  def raise_exceptions?
287
264
  false
288
265
  end
289
-
290
- # Tells whether image processing in a forked subproces should be turned on
291
- #
292
- # @return [Boolean]
293
- def enable_forking?
294
- ENV['IMAGE_VISE_ENABLE_FORK'] == 'yes'
295
- end
296
-
266
+
297
267
  # Applies the given {ImageVise::Pipeline} to the image, and writes the render to
298
268
  # the given path.
299
269
  #
@@ -303,21 +273,23 @@ class ImageVise::RenderEngine
303
273
  # @return [void]
304
274
  def apply_pipeline(source_file_path, pipeline, source_file_type, render_to_path)
305
275
  render_file_type = source_file_type
306
-
276
+
307
277
  # Load the first frame of the animated GIF _or_ the blended compatibility layer from Photoshop
308
278
  image_list = Magick::Image.read(source_file_path)
309
- magick_image = image_list.first
310
-
311
- # Apply the pipeline
312
- pipeline.apply!(magick_image)
313
-
314
- # If processing the image has created an alpha channel, use PNG always.
315
- # Otherwise, keep the original format for as far as the supported formats list goes.
316
- render_file_type = PNG_FILE_TYPE if magick_image.alpha?
317
- render_file_type = PNG_FILE_TYPE unless output_file_type_permitted?(render_file_type)
318
-
319
- magick_image.format = render_file_type.ext
320
- magick_image.write(render_to_path)
279
+ magick_image = image_list.first # Picks up the "precomp" PSD layer in compatibility mode, or the first frame of a GIF
280
+
281
+ # If any operators want to stash some data for downstream use we use this Hash
282
+ metadata = {}
283
+
284
+ # Apply the pipeline (all the image operators)
285
+ pipeline.apply!(magick_image, metadata)
286
+
287
+ # Write out the file honoring the possible injected metadata. One of the metadata
288
+ # elements (that an operator might want to alter) is the :writer, we forcibly #fetch
289
+ # it so that we get a KeyError if some operator has deleted it without providing a replacement.
290
+ # If no operators touched the writer we are going to use the automatic format selection
291
+ writer = metadata.fetch(:writer, ImageVise::AutoWriter.new)
292
+ writer.write_image!(magick_image, metadata, render_to_path)
321
293
  ensure
322
294
  # destroy all the loaded images explicitly
323
295
  (image_list || []).map {|img| ImageVise.destroy(img) }
@@ -0,0 +1,3 @@
1
+ class ImageVise
2
+ VERSION = '0.2.0'
3
+ end