imogen 0.1.9 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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