imogen 0.1.9 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f78baebd0825df57e7ddeea7a7ca02c95a9c2c064c264532a911b12e72d289ad
4
- data.tar.gz: c2d64e9187fa67bf65c6be9eb256cd430155609b4e5c8a0f2d5a48742fc87ceb
3
+ metadata.gz: 4b23125bf531a9a524f745c2ff9ffed4e3bd2e1aedf26dbdcd07434c0df9bbc0
4
+ data.tar.gz: 70b706da9db53e41b691eda24c70b8211cce3073cd99eae77a4045dad7dd0bab
5
5
  SHA512:
6
- metadata.gz: 72f13fe92f9dc14fb61131218d84eb6516dab1a3737839e7377f717d0badee2ffe0bd8b57bb0c6fb6ba056bf2ad151c910296d09f1943e20bcaf96621c1db20d
7
- data.tar.gz: 9eb6bd80a0dfa8d797a39267d805acde74e7ac33fe9395556db2d143396fe7aef0dd2603b9fd8df58b4b2d519adf24019b82113cf7506fca867a53eeb43404c4
6
+ metadata.gz: fa4d10b1e6aa9a00b72d73daa4796b2ffcd72bde1bfcb8dc36443bfc3f8d82d72a86abff0e1a4f8e6150d17b959c5670bc6868e83a6a40906b40b56fa1e24786
7
+ data.tar.gz: f5e43a5c25d37b0b99b0578216169d0cb0deabc5598c46bd21f50cbc546ac38250f3b658609d34751006696c10652399453a2537c12d72e710a515cafa7c5145
@@ -1,18 +1,8 @@
1
1
  module Imogen
2
2
  module AutoCrop
3
- autoload :Edges, 'imogen/auto_crop/edges'
4
- autoload :Box, 'imogen/auto_crop/box'
5
- def self.convert(img, dest_path, scale=768, format=:jpeg)
6
- frame = Edges.new(img)
7
- edges = frame.get(scale)
8
- img.copy(*edges) do |crop|
9
- crop.rescale(scale, scale) do |thumb|
10
- dst = FreeImage::File.new(dest_path)
11
- t24 = (crop.color_type == :rgb) ? thumb.convert_to_24bits : thumb.convert_to_8bits
12
- dst.save(t24, format)
13
- t24.free
14
- thumb.free
15
- end
3
+ def self.convert(img, dest_path, scale=768, opts = {})
4
+ Imogen::Iiif::Region::Featured.convert(img, scale, opts) do |smartcrop|
5
+ smartcrop.write_to_file(dest_path)
16
6
  end
17
7
  end
18
8
  end
@@ -1,5 +1,4 @@
1
1
  #!ruby
2
-
3
2
  module Imogen
4
3
  module Iiif
5
4
  class Region < Transform
@@ -45,10 +44,55 @@ class Region < Transform
45
44
  yield img
46
45
  else
47
46
  if edges == :featured
48
- frame = Imogen::AutoCrop::Edges.new(img)
49
- edges = frame.get([img.width, img.height,768].min)
47
+ side = [img.width, img.height,768].min
48
+ Featured.convert(img, side) { |x| yield x }
49
+ else
50
+ # edges are leftX, topY, rightX, bottomY
51
+ # Vips wants left, top, width, height
52
+ yield img.extract_area(edges[0], edges[1], edges[2] - edges[0], edges[3] - edges[1])
53
+ end
54
+ end
55
+ end
56
+ class Featured < Transform
57
+ SQUARISH = 5.to_f / 6
58
+ ONE_THIRD = 1.to_f / 3
59
+ def self.convert(img, scale = 768, opts = {})
60
+ middle_dims = [(img.width * 2 * ONE_THIRD).floor, (img.height * 2 * ONE_THIRD).floor]
61
+ x_offset = (img.width * ONE_THIRD/2).floor
62
+ y_offset = (img.height * ONE_THIRD/2).floor
63
+ crop_scale = middle_dims.min
64
+ smart_crop_opts = {interesting: (squarish?(img) ? :centre : :entropy)}.merge(opts)
65
+ window = img.extract_area(x_offset, y_offset, middle_dims[0], middle_dims[1])
66
+ smartcrop = window.smartcrop(crop_scale, crop_scale, **smart_crop_opts)
67
+ # Vips counts with negative offsets from left and top
68
+ yield smartcrop.thumbnail_image(scale, height: scale)
69
+ end
70
+
71
+ # returns leftX, topY, rightX, bottomY
72
+ def self.get(img, scale = 768, opts = {})
73
+ middle_dims = [(img.width * 2 * ONE_THIRD).floor, (img.height * 2 * ONE_THIRD).floor]
74
+ x_offset = (img.width * ONE_THIRD/2).floor
75
+ y_offset = (img.height * ONE_THIRD/2).floor
76
+ crop_scale = middle_dims.min
77
+ smart_crop_opts = {interesting: (squarish?(img) ? :centre : :entropy)}.merge(opts)
78
+ window = img.extract_area(x_offset, y_offset, middle_dims[0], middle_dims[1])
79
+ smartcrop = window.smartcrop(crop_scale, crop_scale, **smart_crop_opts)
80
+ # Vips counts with negative offsets from left and top
81
+ left = (window.xoffset + smartcrop.xoffset)*-1
82
+ top = (window.yoffset + smartcrop.yoffset)*-1
83
+ right = left + smartcrop.width
84
+ bottom = top + smartcrop.height
85
+ return [left, top, right, bottom]
86
+ end
87
+
88
+ def self.squarish?(img)
89
+ if img.is_a? Vips::Image
90
+ dims = [img.width, img.height]
91
+ ratio = dims.min.to_f / dims.max
92
+ return ratio >= Featured::SQUARISH
93
+ else
94
+ raise "#{img.class.name} is not a Vips::Image"
50
95
  end
51
- img.copy(*edges) {|crop| yield crop}
52
96
  end
53
97
  end
54
98
  end
@@ -1,24 +1,27 @@
1
1
  module Imogen
2
- module Iiif
3
- class Rotation < Transform
4
- RIGHT_ANGLES = [0,90,180,270]
5
- def get(rotate)
6
- return nil if [nil, 0, '0'].include?(rotate)
7
- raise BadRequest.new("bad rotate #{rotate}") unless rotate.to_s =~ /^-?\d+$/
8
- # negate offset because IIIF spec counts clockwise, FreeImage counterclockwise
9
- r = (rotate.to_i * -1) % 360
10
- r = r + 360 if r < 0
11
- raise BadRequest.new("bad rotate #{rotate}") unless RIGHT_ANGLES.include? r
12
- return r > 0 ? r : nil
13
- end
14
- def self.convert(img, rotate)
15
- rotation = Rotation.new(img).get(rotate)
16
- if rotation
17
- img.rotate(rotation) {|crop| yield crop}
18
- else
19
- yield img
2
+ module Iiif
3
+ class Rotation < Transform
4
+ RIGHT_ANGLES = [0,90,180,270]
5
+ def get(rotate)
6
+ return [0, false] if rotate.nil?
7
+ original_rotate_value = rotate
8
+ rotate = rotate.to_s
9
+ raise BadRequest.new("bad rotate #{original_rotate_value}") unless rotate =~ /^!?-?\d+$/
10
+ flip = rotate.to_s.start_with?('!')
11
+ # libvips and IIIF spec counts clockwise
12
+ angle = rotate.sub(/^!/, '').to_i % 360
13
+ raise BadRequest.new("bad rotate #{original_rotate_value}") unless RIGHT_ANGLES.include?(angle)
14
+ return angle, flip
15
+ end
16
+
17
+ def self.convert(img, rotate)
18
+ angle, flip = Rotation.new(img).get(rotate)
19
+ # IIIF spec applies horizontal flip ("mirrored by reflection on the vertical axis") before rotation
20
+ img = img.fliphor if flip
21
+ # No need to rotate if angle is zero
22
+ img = img.rot("d#{angle}") unless angle.zero?
23
+ yield img
24
+ end
20
25
  end
21
26
  end
22
27
  end
23
- end
24
- end
@@ -35,7 +35,7 @@ class Size < Transform
35
35
  def self.convert(img, size)
36
36
  dims = Size.new(img).get(size)
37
37
  if dims
38
- img.rescale(*dims) {|crop| yield crop}
38
+ yield img.thumbnail_image(dims[0], height: dims[1])
39
39
  else
40
40
  yield img
41
41
  end
data/lib/imogen/iiif.rb CHANGED
@@ -30,9 +30,9 @@ module Imogen
30
30
  def self.convert(img, quality)
31
31
  q = get(quality)
32
32
  if q == :grey
33
- img.convert_to_greyscale {|c| yield c}
33
+ yield img.copy(interpretation: :b_w)
34
34
  elsif q == :bitonal
35
- img.threshold(128) {|c| yield c}
35
+ yield img.copy(interpretation: :b_w) > 128
36
36
  else
37
37
  yield img
38
38
  end
@@ -53,13 +53,8 @@ module Imogen
53
53
  Size.convert(region, opts[:size]) do |size|
54
54
  Rotation.convert(size, opts[:rotation]) do |rotation|
55
55
  Quality.convert(rotation, opts[:quality]) do |quality|
56
- dst = FreeImage::File.new(dest_path)
57
- format = :jpeg if format == :jpg
58
- if (img.color_type == :rgb)
59
- quality.convert_to_24bits {|result| dst.save(result, format, (format == :jp2 ? 8 : 0)); yield result if block_given?}
60
- else
61
- quality.convert_to_8bits {|result| dst.save(result, format, (format == :jp2 ? 8 : 0)); yield result if block_given?}
62
- end
56
+ quality.write_to_file(dest_path)
57
+ yield quality if block_given?
63
58
  end
64
59
  end
65
60
  end
@@ -12,8 +12,7 @@ module Imogen
12
12
  return Math.log2(dims[0..1].max.to_f / tile_size).ceil
13
13
  end
14
14
  def self.convert(img, dest_path)
15
- dst = FreeImage::File.new(dest_path)
16
- dst.save(img, :jp2, 8)
15
+ raise "jp2 output conversion not implemented"
17
16
  end
18
17
  end
19
18
  end
data/lib/imogen.rb CHANGED
@@ -1,108 +1,35 @@
1
1
  # encoding: UTF-8
2
- require 'ffi'
3
- require 'rbconfig'
4
- require 'free-image'
2
+ require 'vips'
5
3
  module Imogen
6
4
 
7
5
  def self.from(src_path)
8
- FreeImage::Bitmap.open(src_path) do |img|
9
- yield img
10
- end
6
+ yield Vips::Image.matload(src_path)
11
7
  end
12
8
  module Scaled
13
9
  def self.convert(img, dest_path, scale=1500, format = :jpeg)
14
10
  w = img.width
15
11
  h = img.height
16
12
  dims = (w > h) ? [scale, scale*h/w] : [scale*w/h, scale]
17
- img.rescale(dims[0], dims[1]) do |scaled|
18
- scaled = (scaled.color_type == :rgb) ? scaled.convert_to_24bits : scaled.convert_to_8bits
19
- dst = FreeImage::File.new(dest_path)
20
- dst.save(scaled, format)
21
- scaled.free
22
- end
13
+ img.thumbnail_image(dims[0], height: dims[1]).write_to_file(dest_path)
23
14
  end
24
15
  end
25
16
  module Cropped
26
17
  def self.convert(img, dest_path, edges, scale=nil, format=:jpeg)
18
+ img.crop(*edges).write_to_file(dest_path)
27
19
  end
28
20
  end
29
21
  require 'imogen/auto_crop'
30
22
  require 'imogen/zoomable'
31
23
  require 'imogen/iiif'
32
24
 
33
- def self.search_paths
34
- @search_paths ||= begin
35
- if ENV['FREE_IMAGE_LIBRARY_PATH']
36
- [ ENV['FREE_IMAGE_LIBRARY_PATH'] ]
37
- elsif FFI::Platform::IS_WINDOWS
38
- ENV['PATH'].split(File::PATH_SEPARATOR)
39
- else
40
- [ '/usr/local/{lib64,lib32,lib}', '/opt/local/{lib64,lib32,lib}', '/usr/{lib64,lib32,lib}' ]
41
- end
42
- end
43
- end
44
-
45
- def self.find_lib(lib)
46
- files = search_paths.inject(Array.new) do |array, path|
47
- file_name = File.expand_path(File.join(path, "#{lib}.#{FFI::Platform::LIBSUFFIX}"))
48
- array << Dir.glob(file_name)
49
- array
50
- end
51
- files.flatten.compact.first
52
- end
53
-
54
- def self.free_image_library_paths
55
- @free_image_library_paths ||= begin
56
- libs = %w{libfreeimage libfreeimage.3 FreeImage}
57
-
58
- libs.map do |lib|
59
- find_lib(lib)
60
- end.compact
61
- end
62
- end
63
-
64
- extend ::FFI::Library
65
-
66
- if free_image_library_paths.any?
67
- ffi_lib(*free_image_library_paths)
68
- elsif FFI::Platform.windows?
69
- ffi_lib("FreeImaged")
70
- else
71
- ffi_lib("freeimage")
72
- end
73
-
74
- ffi_convention :stdcall if FFI::Platform.windows?
75
-
76
25
  def self.format_from(image_path)
77
- result = FreeImage.FreeImage_GetFileType(image_path, 0)
78
- FreeImage.check_last_error
79
-
80
- if result == :unknown
81
- # Try to guess the file format from the file extension
82
- result = FreeImage.FreeImage_GetFIFFromFilename(image_path)
83
- FreeImage.check_last_error
84
- end
85
- result
26
+ raise "format from path not implemented"
86
27
  end
87
28
 
88
29
  def self.image(src_path, flags=0)
89
-
90
- fif = format_from(src_path)
91
- if ((fif != :unknown) and FreeImage.FreeImage_FIFSupportsReading(fif))
92
- ptr = FreeImage.FreeImage_Load(fif, src_path, flags)
93
- FreeImage.check_last_error(ptr)
94
- return FreeImage::Bitmap.new(ptr, nil)
95
- end
96
- return nil
30
+ Vips::Image.new_from_file(src_path)
97
31
  end
98
32
  def self.with_image(src_path, flags = 0, &block)
99
-
100
- fif = format_from(src_path)
101
- if ((fif != :unknown) and FreeImage.FreeImage_FIFSupportsReading(fif))
102
- ptr = FreeImage.FreeImage_Load(fif, src_path, flags)
103
- FreeImage.check_last_error(ptr)
104
- FreeImage::Bitmap.new(ptr, nil, &block)
105
- end
33
+ block.yield(image(src_path, flags))
106
34
  end
107
35
  end
108
- require 'imogencv'
Binary file
@@ -0,0 +1,17 @@
1
+ require 'imogen'
2
+ require 'tmpdir'
3
+
4
+ describe Imogen::AutoCrop, vips: true do
5
+ describe "#convert" do
6
+ let(:output_file) { Dir.tmpdir + '/test-imogen-crop.jpg' }
7
+ it "should successfully convert the image" do
8
+ Imogen.with_image(fixture('sample.jpg').path) do |img|
9
+ Imogen::AutoCrop.convert(img, output_file, 150)
10
+ end
11
+ expect(File.exist?(output_file)).to be true
12
+ expect(File.size?(output_file)).to be > 0
13
+ ensure
14
+ File.delete(output_file) if File.exist?(output_file)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ require 'imogen'
2
+ require 'tmpdir'
3
+
4
+ describe Imogen::AutoCrop, vips: true do
5
+ describe "#convert" do
6
+ let(:output_file) { Dir.tmpdir + '/test-imogen-convert.jpg' }
7
+ it "should successfully convert the image" do
8
+ Imogen.with_image(fixture('sample.jpg').path) do |img|
9
+ Imogen::Iiif.convert(img, output_file, 'jpg', region: '50,60,500,800', size: '!100,100', quality: 'color', rotation: '!90')
10
+ end
11
+ expect(File.exist?(output_file)).to be true
12
+ expect(File.size?(output_file)).to be > 0
13
+ ensure
14
+ File.delete(output_file) if File.exist?(output_file)
15
+ end
16
+ end
17
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,3 +1,104 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
4
+ # this file to always be loaded, without a need to explicitly require it in any
5
+ # files.
6
+ #
7
+ # Given that it is always loaded, you are encouraged to keep this file as
8
+ # light-weight as possible. Requiring heavyweight dependencies from this file
9
+ # will add to the boot time of your test suite on EVERY test run, even for an
10
+ # individual file that may not need all of that loaded. Instead, consider making
11
+ # a separate helper file that requires the additional dependencies and performs
12
+ # the additional setup, and require it from the spec files that actually need
13
+ # it.
14
+ #
15
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
16
+ RSpec.configure do |config|
17
+ # rspec-expectations config goes here. You can use an alternate
18
+ # assertion/expectation library such as wrong or the stdlib/minitest
19
+ # assertions if you prefer.
20
+ config.expect_with :rspec do |expectations|
21
+ # This option will default to `true` in RSpec 4. It makes the `description`
22
+ # and `failure_message` of custom matchers include text for helper methods
23
+ # defined using `chain`, e.g.:
24
+ # be_bigger_than(2).and_smaller_than(4).description
25
+ # # => "be bigger than 2 and smaller than 4"
26
+ # ...rather than:
27
+ # # => "be bigger than 2"
28
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
29
+ end
30
+
31
+ # rspec-mocks config goes here. You can use an alternate test double
32
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
33
+ config.mock_with :rspec do |mocks|
34
+ # Prevents you from mocking or stubbing a method that does not exist on
35
+ # a real object. This is generally recommended, and will default to
36
+ # `true` in RSpec 4.
37
+ mocks.verify_partial_doubles = true
38
+ end
39
+
40
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
41
+ # have no way to turn it off -- the option exists only for backwards
42
+ # compatibility in RSpec 3). It causes shared context metadata to be
43
+ # inherited by the metadata hash of host groups and examples, rather than
44
+ # triggering implicit auto-inclusion in groups with matching metadata.
45
+ config.shared_context_metadata_behavior = :apply_to_host_groups
46
+
47
+ # The settings below are suggested to provide a good initial experience
48
+ # with RSpec, but feel free to customize to your heart's content.
49
+ =begin
50
+ # Allows RSpec to persist some state between runs in order to support
51
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
52
+ # you configure your source control system to ignore this file.
53
+ config.example_status_persistence_file_path = "spec/examples.txt"
54
+
55
+ # Limits the available syntax to the non-monkey patched syntax that is
56
+ # recommended. For more details, see:
57
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
58
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
59
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
60
+ config.disable_monkey_patching!
61
+
62
+ # This setting enables warnings. It's recommended, but in some cases may
63
+ # be too noisy due to issues in dependencies.
64
+ config.warnings = true
65
+
66
+ # Many RSpec users commonly either run the entire suite or an individual
67
+ # file, and it's useful to allow more verbose output when running an
68
+ # individual spec file.
69
+ if config.files_to_run.one?
70
+ # Use the documentation formatter for detailed output,
71
+ # unless a formatter has already been configured
72
+ # (e.g. via a command-line flag).
73
+ config.default_formatter = "doc"
74
+ end
75
+
76
+ # Print the 10 slowest examples and example groups at the
77
+ # end of the spec run, to help surface which specs are running
78
+ # particularly slow.
79
+ config.profile_examples = 10
80
+
81
+ # Run specs in random order to surface order dependencies. If you find an
82
+ # order dependency and want to debug it, you can fix the order by providing
83
+ # the seed, which is printed after each run.
84
+ # --seed 1234
85
+ config.order = :random
86
+
87
+ # Seed global randomization in this process using the `--seed` CLI option.
88
+ # Setting this allows you to use `--seed` to deterministically reproduce
89
+ # test failures related to randomization by passing the same `--seed` value
90
+ # as the one that triggered the failure.
91
+ Kernel.srand config.seed
92
+ =end
93
+
94
+ # This allows you to limit a spec run to individual examples or groups
95
+ # you care about by tagging them with `:focus` metadata. When nothing
96
+ # is tagged with `:focus`, all examples get run. RSpec also provides
97
+ # aliases for `it`, `describe`, and `context` that include `:focus`
98
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
99
+ config.filter_run_when_matching :focus
100
+ end
101
+
1
102
  class ImageStub
2
103
  attr_reader :width, :height
3
104
  def initialize(width,height)
@@ -5,3 +106,13 @@ class ImageStub
5
106
  @height = height
6
107
  end
7
108
  end
109
+
110
+ def absolute_fixture_path(file)
111
+ File.realpath(File.join(File.dirname(__FILE__), '..','spec','fixtures', 'files', file))
112
+ end
113
+
114
+ def fixture(file)
115
+ path = absolute_fixture_path(file)
116
+ raise "No fixture file at #{path}" unless File.exist? path
117
+ File.new(path)
118
+ end
@@ -1,35 +1,41 @@
1
1
  require 'imogen/iiif'
2
2
  require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
3
3
  describe Imogen::Iiif::Rotation, type: :unit do
4
- before(:all) do
5
- @test_image = ImageStub.new(175,131)
6
- end
7
- subject {Imogen::Iiif::Rotation.new(@test_image)}
4
+ let(:image) { double("image", width: 175, height: 131) }
8
5
  describe "#get" do
6
+ subject { Imogen::Iiif::Rotation.new(image) }
9
7
  describe "with values mod 360 in 90 degree rotations" do
10
- it "should nil for 0 or '0' or nil" do
11
- expect(subject.get(0)).to be_nil
12
- expect(subject.get(360)).to be_nil
13
- expect(subject.get("360")).to be_nil
14
- expect(subject.get("-360")).to be_nil
15
- expect(subject.get("0")).to be_nil
16
- expect(subject.get(nil)).to be_nil
8
+ it "should return [0, false] angle and flip for 0 or '0' or nil" do
9
+ expect(subject.get(0)).to eq([0, false])
10
+ expect(subject.get(360)).to eq([0, false])
11
+ expect(subject.get("360")).to eq([0, false])
12
+ expect(subject.get("-360")).to eq([0, false])
13
+ expect(subject.get("0")).to eq([0, false])
14
+ expect(subject.get(nil)).to eq([0, false])
15
+ end
16
+ it "should return the expected angle and flip for positive values" do
17
+ expect(subject.get(90)).to eql([90, false])
18
+ expect(subject.get("90")).to eql([90, false])
19
+ expect(subject.get("180")).to eql([180, false])
20
+ expect(subject.get("270")).to eql([270, false])
21
+ expect(subject.get("450")).to eql([90, false])
17
22
  end
18
- # IIIF rotation is opposite FreeImage
19
- it "should calculate for positive values" do
20
- expect(subject.get(90)).to eql(270)
21
- expect(subject.get("90")).to eql(270)
22
- expect(subject.get("180")).to eql(180)
23
- expect(subject.get("270")).to eql(90)
24
- expect(subject.get("450")).to eql(270)
23
+ it "should return the expected angle and flip for negative values" do
24
+ expect(subject.get(-90)).to eql([270, false])
25
+ expect(subject.get("-90")).to eql([270, false])
26
+ expect(subject.get("-180")).to eql([180, false])
27
+ expect(subject.get("-270")).to eql([90, false])
28
+ expect(subject.get("-450")).to eql([270, false])
25
29
  end
26
- # IIIF rotation is opposite FreeImage
27
- it "should calculate for negative values" do
28
- expect(subject.get(-90)).to eql(90)
29
- expect(subject.get("-90")).to eql(90)
30
- expect(subject.get("-180")).to eql(180)
31
- expect(subject.get("-270")).to eql(270)
32
- expect(subject.get("-450")).to eql(90)
30
+ it "should return the expected angle and flip for string values that start with an exclamation point" do
31
+ expect(subject.get("!0")).to eql([0, true])
32
+ expect(subject.get("!90")).to eql([90, true])
33
+ expect(subject.get("!180")).to eql([180, true])
34
+ expect(subject.get("!270")).to eql([270, true])
35
+ expect(subject.get("!-90")).to eql([270, true])
36
+ expect(subject.get("!-180")).to eql([180, true])
37
+ expect(subject.get("!-270")).to eql([90, true])
38
+ expect(subject.get("!-450")).to eql([270, true])
33
39
  end
34
40
  end
35
41
  it "should reject arbitrary integer and float values" do
@@ -41,4 +47,31 @@ describe Imogen::Iiif::Rotation, type: :unit do
41
47
  expect{subject.get("-2,")}.to raise_error Imogen::Iiif::BadRequest
42
48
  end
43
49
  end
50
+ describe '.convert' do
51
+ let(:no_op) { Proc.new {|x| x} }
52
+ context 'at multiple of 360' do
53
+ it "does not rotate" do
54
+ expect(image).not_to receive(:rot)
55
+ expect(image).not_to receive(:fliphor)
56
+ (-2..2).each { |x| described_class.convert(image, (x*360).to_s, &no_op) }
57
+ end
58
+ end
59
+ context 'at right angle rotations not multiple of 360' do
60
+ it "does rotate" do
61
+ expect(image).not_to receive(:fliphor)
62
+ [-3, -2, -1, 1, 2, 3].each do |x|
63
+ tuple = Imogen::Iiif::Rotation.new(image).get((90*x).to_s)
64
+ param = "d#{tuple[0]}"
65
+ expect(image).to receive(:rot).with(param)
66
+ described_class.convert(image, (x*90).to_s, &no_op)
67
+ end
68
+ end
69
+ end
70
+ context 'with a bang param' do
71
+ it "flips horizontal" do
72
+ expect(image).to receive(:fliphor)
73
+ described_class.convert(image, "!0", &no_op)
74
+ end
75
+ end
76
+ end
44
77
  end
metadata CHANGED
@@ -1,17 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: imogen
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Armintor
8
- autorequire:
8
+ - Eric O'Hanlon
9
+ autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2019-10-30 00:00:00.000000000 Z
12
+ date: 2022-05-09 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
- name: rice
15
+ name: ruby-vips
15
16
  requirement: !ruby/object:Gem::Requirement
16
17
  requirements:
17
18
  - - ">="
@@ -25,7 +26,7 @@ dependencies:
25
26
  - !ruby/object:Gem::Version
26
27
  version: '0'
27
28
  - !ruby/object:Gem::Dependency
28
- name: rspec
29
+ name: rake
29
30
  requirement: !ruby/object:Gem::Requirement
30
31
  requirements:
31
32
  - - ">="
@@ -39,32 +40,27 @@ dependencies:
39
40
  - !ruby/object:Gem::Version
40
41
  version: '0'
41
42
  - !ruby/object:Gem::Dependency
42
- name: rake-compiler
43
+ name: rspec
43
44
  requirement: !ruby/object:Gem::Requirement
44
45
  requirements:
45
- - - ">="
46
+ - - "~>"
46
47
  - !ruby/object:Gem::Version
47
- version: '0'
48
+ version: '3.9'
48
49
  type: :development
49
50
  prerelease: false
50
51
  version_requirements: !ruby/object:Gem::Requirement
51
52
  requirements:
52
- - - ">="
53
+ - - "~>"
53
54
  - !ruby/object:Gem::Version
54
- version: '0'
55
- description:
55
+ version: '3.9'
56
+ description:
56
57
  email: armintor@gmail.com
57
58
  executables: []
58
- extensions:
59
- - ext/imogencv/extconf.rb
59
+ extensions: []
60
60
  extra_rdoc_files: []
61
61
  files:
62
- - ext/imogencv/extconf.rb
63
- - ext/imogencv/imogencv.cpp
64
62
  - lib/imogen.rb
65
63
  - lib/imogen/auto_crop.rb
66
- - lib/imogen/auto_crop/box.rb
67
- - lib/imogen/auto_crop/edges.rb
68
64
  - lib/imogen/dzi.rb
69
65
  - lib/imogen/iiif.rb
70
66
  - lib/imogen/iiif/region.rb
@@ -73,6 +69,9 @@ files:
73
69
  - lib/imogen/iiif/tiles.rb
74
70
  - lib/imogen/zoomable.rb
75
71
  - lib/imogencv.bundle
72
+ - spec/fixtures/files/sample.jpg
73
+ - spec/integration/imogen_autocrop_spec.rb
74
+ - spec/integration/imogen_iiif_spec.rb
76
75
  - spec/spec_helper.rb
77
76
  - spec/unit/imogen_iiif_quality_spec.rb
78
77
  - spec/unit/imogen_iiif_region_spec.rb
@@ -82,7 +81,7 @@ files:
82
81
  homepage: https://github.com/cul/imogen
83
82
  licenses: []
84
83
  metadata: {}
85
- post_install_message:
84
+ post_install_message:
86
85
  rdoc_options: []
87
86
  require_paths:
88
87
  - lib
@@ -97,8 +96,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
97
96
  - !ruby/object:Gem::Version
98
97
  version: '0'
99
98
  requirements: []
100
- rubygems_version: 3.0.6
101
- signing_key:
99
+ rubygems_version: 3.2.32
100
+ signing_key:
102
101
  specification_version: 4
103
- summary: derivative generation via FreeImage and smart square thumbnail via OpenCV
102
+ summary: IIIF image derivative generation helpers for Vips
104
103
  test_files: []
@@ -1,99 +0,0 @@
1
- require 'mkmf-rice'
2
-
3
- osx = RbConfig::CONFIG['target_os'] =~ /darwin/
4
-
5
- if osx
6
- $CFLAGS << " -x c++ -std=c++14"# damn the torpedoes!
7
- else
8
- $CFLAGS << " -x c++"
9
- end
10
-
11
- def real_inc_dir(src)
12
- File.symlink?(src) ? File.realdirpath(src) : src
13
- end
14
-
15
- def add_flags_if_header(header, header_dir, lib_dir)
16
- a_file = File.join(header_dir, header)
17
- exists = File.exist?(a_file)
18
- puts "#{a_file} exists ... #{exists}"
19
- if exists
20
- inc_opt = "-I#{header_dir}".quote
21
- lib_opt = "-L#{lib_dir}".quote
22
- puts "adding compiler flags:\n#{inc_opt}\n#{lib_opt}"
23
- $INCFLAGS << " " << inc_opt
24
- $LIBPATH = $LIBPATH | [lib_dir]
25
- end
26
- exists
27
- end
28
-
29
- incdir_default = "/usr/local/include"
30
- libdir_default = "/usr/local/lib"
31
-
32
- have_library('stdc++')
33
- # MakeMakefile::CONFTEST_C = "#{CONFTEST}.cc"
34
-
35
- required_headers = {}
36
- required_libs = {}
37
-
38
- required_headers['opencv4'] = [ 'opencv2/features2d.hpp' ]
39
- required_libs['opencv4'] = [
40
- 'opencv_core',
41
- 'opencv_imgcodecs',
42
- 'opencv_imgproc',
43
- 'opencv_features2d'
44
- ]
45
-
46
- required_libs['zlib'] = ['z']
47
- required_libs['libwebp'] = ['webp']
48
- required_libs['libjpeg'] = ['jpeg']
49
- required_libs['libtiff-4'] = ['tiff']
50
- required_libs['libpng'] = ['png16']
51
- required_libs['jasper'] = [] # just run pkg-config if you can
52
- required_libs['OpenEXR'] = ['IlmImf']
53
-
54
- all_deps = (required_libs.keys | required_headers.keys).sort.uniq
55
- all_deps.each do |dep_key|
56
- has_pkg_config = (pkg_config(dep_key) || []).detect { |c| c =~ /\-L\/\w+/ }
57
-
58
- # expect to call with --with-opencv4-include=DIR and --with-opencv4-lib=DIR or --withopencv4-dir=DIR
59
- incdir, libdir = dir_config(dep_key, incdir_default, libdir_default) unless has_pkg_config
60
-
61
- unless !has_pkg_config && incdir && incdir != incdir_default
62
- puts "using default #{dep_key} include path: #{incdir_default}"
63
- end
64
-
65
- unless !has_pkg_config && libdir && libdir != libdir_default
66
- puts "using default #{dep_key} library path: #{libdir_default}"
67
- end
68
-
69
- include_paths = [incdir_default, "/usr/local"]
70
- include_paths = ([incdir, File.join(incdir, dep_key)] | include_paths) if incdir
71
-
72
- required_headers.fetch(dep_key, []).each do |hdr|
73
- unless find_header(hdr, *include_paths.compact.uniq)
74
- open(MakeMakefile::Logging.instance_variable_get(:@logfile), 'r') do |logblob|
75
- logblob.each { |logline| puts logline.strip }
76
- end
77
- puts "Cannot find required header: #{hdr}"
78
- puts "if this output is from rake compile, consider adding:"
79
- puts "rake compile -- --with#{dep_key}-include=DIR"
80
- exit 1
81
- end
82
- end
83
-
84
- lib_paths = [libdir_default, "/usr/local"]
85
- lib_paths.unshift(libdir) if libdir
86
-
87
- required_libs.fetch(dep_key, []).each do |lib|
88
- unless find_library(lib, nil, *lib_paths.compact.uniq)
89
- puts "Cannot find required lib: #{lib}"
90
- exit 1
91
- end
92
- end
93
- append_cflags(lib_paths.compact.uniq.map {|x| "-L#{x}"}.join(' '))
94
- end
95
-
96
- append_cflags('-stdlib=libc++')
97
- @libdir_basename ||= 'lib'
98
- $LIBRUBYARG.prepend(' ') # there's some weird spacing issue in rice's lib linking routine
99
- create_makefile('imogencv')
@@ -1,87 +0,0 @@
1
- #include "rice/Class.hpp"
2
- #include "rice/Constructor.hpp"
3
- #include "rice/Enum.hpp"
4
- #include "opencv2/features2d.hpp"
5
- #include "opencv2/imgproc.hpp"
6
- #include "opencv2/imgcodecs.hpp"
7
-
8
- using namespace Rice;
9
-
10
- Object process_kaze_features(Object r_image)
11
- {
12
- cv::Mat image = from_ruby<cv::Mat>(r_image);
13
- std::vector<cv::KeyPoint> keyPoints;
14
- cv::Ptr<cv::KAZE> alg = cv::KAZE::create();
15
- cv::Mat features;
16
- alg->detectAndCompute(image, cv::noArray(), keyPoints, features);
17
- Array a;
18
- for(cv::KeyPoint keyPoint : keyPoints)
19
- {
20
- a.push(to_ruby<cv::Point2f>(keyPoint.pt));
21
- }
22
- keyPoints.clear();
23
- return a;
24
- }
25
-
26
- Object load_grayscale(Object filename)
27
- {
28
- Check_Type(filename, T_STRING);
29
- cv::String const c_path = cv::String(String(filename).str());
30
- cv::Mat image = imread(c_path, cv::IMREAD_GRAYSCALE);
31
- return to_ruby<cv::Mat>(image);
32
- }
33
-
34
- Object point2f_x(Object self)
35
- {
36
- cv::Point2f point = from_ruby<cv::Point2f>(self);
37
- return to_ruby<int>(point.x);
38
- }
39
-
40
- Object point2f_y(Object self)
41
- {
42
- cv::Point2f point = from_ruby<cv::Point2f>(self);
43
- return to_ruby<int>(point.y);
44
- }
45
-
46
- Object mat_cols(Object self)
47
- {
48
- cv::Mat image = from_ruby<cv::Mat>(self);
49
- return to_ruby<int>(image.cols);
50
- }
51
-
52
- Object mat_rows(Object self)
53
- {
54
- cv::Mat image = from_ruby<cv::Mat>(self);
55
- return to_ruby<int>(image.rows);
56
- }
57
-
58
- Object mat_good_features_to_track(Object self, int maxCorners, double qualityLevel, double minDistance, int blockSize, bool useHarrisDetector, double k)
59
- {
60
- cv::Mat image = from_ruby<cv::Mat>(self);
61
- cv::Mat mask;
62
- std::vector<cv::Point2f> corners;
63
- cv::goodFeaturesToTrack(image, corners, maxCorners, qualityLevel, minDistance, mask, blockSize, useHarrisDetector, k);
64
- Array a;
65
- for(cv::Point2f corner : corners)
66
- {
67
- a.push(to_ruby<cv::Point2f>(corner));
68
- }
69
- corners.clear();
70
- return a;
71
- }
72
-
73
- extern "C"
74
- void Init_imogencv()
75
- {
76
- Module rb_mOpenCV = define_module("ImogenCV");
77
- Class rb_cKazeFeatures = rb_mOpenCV.define_class("KazeFeatures");
78
- rb_cKazeFeatures.define_singleton_method("process", &process_kaze_features);
79
- Data_Type<cv::Mat> rb_cMat = rb_mOpenCV.define_class<cv::Mat>("Mat")
80
- .define_method(Identifier("cols"), &mat_cols)
81
- .define_method(Identifier("rows"), &mat_rows)
82
- .define_method("good_features_to_track", &mat_good_features_to_track)
83
- .define_singleton_method("load_grayscale", &load_grayscale);
84
- Data_Type<cv::Point2f> rb_cPoint2f = rb_mOpenCV.define_class<cv::Point2f>("Point2f")
85
- .define_method(Identifier("x"), &point2f_x)
86
- .define_method(Identifier("y"), &point2f_y);
87
- }
@@ -1,93 +0,0 @@
1
- #!ruby
2
- require 'imogencv'
3
- module Imogen::AutoCrop::Box
4
- include ImogenCV
5
- class Best
6
- def initialize(grayscale)
7
- # mat_good_features_to_track(std::int maxCorners, std::double qualityLevel, std:double minDistance, std::int blockSize, std::bool useHarrisDetector, std::double k)
8
- @corners = grayscale.good_features_to_track(20, 0.3, 1.0, 3, false, 0.04)
9
- if @corners.nil? or @corners.length == 0
10
- @center = Center.new(grayscale)
11
- else
12
- @center = nil
13
- end
14
- end
15
-
16
- def self.distance(p1, p2)
17
- dx = p1.x.to_i - p2.x.to_i
18
- dy = p1.y.to_i - p2.y.to_i
19
- return Math.sqrt((dx * dx) + (dy * dy))
20
- end
21
-
22
- def box()
23
- return @center.box unless @center.nil?
24
- c = median()
25
- cp = BoxInfo.new(c[0], c[1],0)
26
- total_distance = 0;
27
- features = @corners.collect {|corner| d = Best.distance(corner, cp); total_distance += d; {x: corner.x, y: corner.y, d: d}}
28
- mean_distance = total_distance/features.length
29
- sigma = features.inject(0) {|memo, feature| v = feature[:d] - mean_distance; memo += (v*v)}
30
- sigma = Math.sqrt(sigma.to_f/features.length)
31
- # 2 sigmas would capture > 95% of normally distributed features
32
- cp.radius = 2*sigma
33
- cp
34
- end
35
-
36
- def median()
37
- @median ||= begin
38
- xs = []
39
- ys = []
40
- @corners.each {|c| xs << c.x.to_i; ys << c.y.to_i}
41
- xs.sort!
42
- ys.sort!
43
- ix = 0
44
- if (@corners.length % 2 == 0)
45
- l = (@corners.length == 2) ? 0 : (@corners.length/2)
46
- x = ((xs[l] + xs[l+1]) /2).floor
47
- y = ((ys[l] + ys[l+1]) /2).floor
48
- [x,y]
49
- else
50
- r = (@corners.length/2).ceil
51
- [xs[r], ys[r]]
52
- end
53
- end
54
- end
55
- end
56
- class Center
57
- def initialize(grayscale)
58
- @center ||= [(grayscale.cols/2).floor, (grayscale.rows/2).floor]
59
- @radius = @center.min
60
- @ratio = @radius / @center.max
61
- end
62
- def box
63
- return BoxInfo.new(@center[0],@center[1],@radius)
64
- end
65
- end
66
- class BoxInfo
67
- attr_reader :x, :y
68
- attr_accessor :radius
69
- SQUARISH = 5.to_f / 6
70
- def initialize(x,y,r)
71
- @x = x
72
- @y = y
73
- @radius = r
74
- end
75
- end
76
- def self.squarish?(img)
77
- if img.is_a? FreeImage::Bitmap
78
- dims = [img.width, img.height]
79
- ratio = dims.min.to_f / dims.max
80
- return ratio >= BoxInfo::SQUARISH
81
- elsif img.is_a? ImogenCV::Mat
82
- dims = [img.cols, img.rows]
83
- ratio = dims.min.to_f / dims.max
84
- return ratio >= BoxInfo::SQUARISH
85
- else
86
- raise "#{img.class.name} is not a FreeImage::Bitmap"
87
- end
88
- end
89
- def self.info(grayscale)
90
- dims = [grayscale.cols, grayscale.rows]
91
- squarish?(grayscale) ? Center.new(grayscale).box() : Best.new(grayscale).box()
92
- end
93
- end
@@ -1,67 +0,0 @@
1
- #!ruby
2
- require 'imogencv'
3
- require 'free-image'
4
- require 'tempfile'
5
-
6
- module Imogen::AutoCrop
7
- class Edges
8
- include ImogenCV
9
- def initialize(src)
10
- @xoffset = 0
11
- @yoffset = 0
12
- tempfile = nil
13
- if src.is_a? FreeImage::Bitmap
14
- img = src
15
- @xoffset = img.width.to_f/6
16
- @yoffset = img.height.to_f/6
17
- if Imogen::AutoCrop::Box.squarish? img
18
- @xoffset = @xoffset/2
19
- @yoffset = @yoffset/2
20
- end
21
- tempfile = Tempfile.new(['crop','.png'])
22
-
23
- img.copy(@xoffset,@yoffset,img.width-@xoffset,img.height-@yoffset) do |crop|
24
- crop.save(tempfile.path, :png)
25
- crop.free
26
- end
27
- else
28
- raise src.class.name
29
- end
30
- # use bigger features on bigger images
31
- @grayscale = ImogenCV::Mat.load_grayscale(tempfile.path)
32
- @xrange = (0..@grayscale.cols)
33
- @yrange = (0..@grayscale.rows)
34
- ensure
35
- tempfile.unlink if tempfile
36
- end
37
-
38
- def bound_min(center)
39
- [center.x - @xrange.min, @xrange.max - center.x, center.y - @yrange.min, @yrange.max - center.y].min
40
- end
41
-
42
- # returns leftX, topY, rightX, bottomY
43
- def get(*args)
44
- c = Imogen::AutoCrop::Box.info(@grayscale)
45
- r = c.radius.floor
46
- # adjust the box
47
- coords = [c.x, c.y]
48
- min_rad = args.max/2
49
- unless r >= min_rad && r <= bound_min(c)
50
- # first adjust to the lesser of max (half short dimension) and min (half requested length) radius
51
- # this might require upscaling in rare situations to preserve bound safety
52
- r = min_rad if r < min_rad
53
- max_rad = [@xrange.max - @xrange.min, @yrange.max - @yrange.min].min / 2
54
- r = max_rad if r > max_rad
55
- # now move the center point minimally to accomodate the necessary radius
56
- coords[0] = @xrange.max - r if (coords[0] + r) > @xrange.max
57
- coords[0] = @xrange.min + r if (coords[0] - r) < @xrange.min
58
- coords[1] = @yrange.max - r if (coords[1] + r) > @yrange.max
59
- coords[1] = @yrange.min + r if (coords[1] - r) < @yrange.min
60
- end
61
- coords = [coords[0] + @xoffset, coords[1] + @yoffset].collect {|i| i.floor}
62
- c = coords
63
-
64
- return [c[0]-r, c[1]-r, c[0]+r, c[1] + r]
65
- end
66
- end
67
- end