image_vise 0.1.6 → 0.2.0

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.
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