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 +4 -4
- data/lib/imogen/auto_crop.rb +3 -13
- data/lib/imogen/iiif/region.rb +48 -4
- data/lib/imogen/iiif/rotation.rb +23 -20
- data/lib/imogen/iiif/size.rb +1 -1
- data/lib/imogen/iiif.rb +4 -9
- data/lib/imogen/zoomable.rb +1 -2
- data/lib/imogen.rb +7 -80
- data/spec/fixtures/files/sample.jpg +0 -0
- data/spec/integration/imogen_autocrop_spec.rb +17 -0
- data/spec/integration/imogen_iiif_spec.rb +17 -0
- data/spec/spec_helper.rb +111 -0
- data/spec/unit/imogen_iiif_rotate_spec.rb +58 -25
- metadata +20 -21
- data/ext/imogencv/extconf.rb +0 -99
- data/ext/imogencv/imogencv.cpp +0 -87
- data/lib/imogen/auto_crop/box.rb +0 -93
- data/lib/imogen/auto_crop/edges.rb +0 -67
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4b23125bf531a9a524f745c2ff9ffed4e3bd2e1aedf26dbdcd07434c0df9bbc0
|
4
|
+
data.tar.gz: 70b706da9db53e41b691eda24c70b8211cce3073cd99eae77a4045dad7dd0bab
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fa4d10b1e6aa9a00b72d73daa4796b2ffcd72bde1bfcb8dc36443bfc3f8d82d72a86abff0e1a4f8e6150d17b959c5670bc6868e83a6a40906b40b56fa1e24786
|
7
|
+
data.tar.gz: f5e43a5c25d37b0b99b0578216169d0cb0deabc5598c46bd21f50cbc546ac38250f3b658609d34751006696c10652399453a2537c12d72e710a515cafa7c5145
|
data/lib/imogen/auto_crop.rb
CHANGED
@@ -1,18 +1,8 @@
|
|
1
1
|
module Imogen
|
2
2
|
module AutoCrop
|
3
|
-
|
4
|
-
|
5
|
-
|
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
|
data/lib/imogen/iiif/region.rb
CHANGED
@@ -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
|
-
|
49
|
-
|
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
|
data/lib/imogen/iiif/rotation.rb
CHANGED
@@ -1,24 +1,27 @@
|
|
1
1
|
module Imogen
|
2
|
-
module Iiif
|
3
|
-
class Rotation < Transform
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
img
|
18
|
-
|
19
|
-
|
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
|
data/lib/imogen/iiif/size.rb
CHANGED
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.
|
33
|
+
yield img.copy(interpretation: :b_w)
|
34
34
|
elsif q == :bitonal
|
35
|
-
img.
|
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
|
-
|
57
|
-
|
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
|
data/lib/imogen/zoomable.rb
CHANGED
data/lib/imogen.rb
CHANGED
@@ -1,108 +1,35 @@
|
|
1
1
|
# encoding: UTF-8
|
2
|
-
require '
|
3
|
-
require 'rbconfig'
|
4
|
-
require 'free-image'
|
2
|
+
require 'vips'
|
5
3
|
module Imogen
|
6
4
|
|
7
5
|
def self.from(src_path)
|
8
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
11
|
-
expect(subject.get(0)).to
|
12
|
-
expect(subject.get(360)).to
|
13
|
-
expect(subject.get("360")).to
|
14
|
-
expect(subject.get("-360")).to
|
15
|
-
expect(subject.get("0")).to
|
16
|
-
expect(subject.get(nil)).to
|
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
|
-
|
19
|
-
|
20
|
-
expect(subject.get(90)).to eql(270)
|
21
|
-
expect(subject.get("
|
22
|
-
expect(subject.get("
|
23
|
-
expect(subject.get("
|
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
|
-
|
27
|
-
|
28
|
-
expect(subject.get(
|
29
|
-
expect(subject.get("
|
30
|
-
expect(subject.get("
|
31
|
-
expect(subject.get("
|
32
|
-
expect(subject.get("
|
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.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Armintor
|
8
|
-
|
8
|
+
- Eric O'Hanlon
|
9
|
+
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date:
|
12
|
+
date: 2022-05-09 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
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:
|
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:
|
43
|
+
name: rspec
|
43
44
|
requirement: !ruby/object:Gem::Requirement
|
44
45
|
requirements:
|
45
|
-
- - "
|
46
|
+
- - "~>"
|
46
47
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
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: '
|
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.
|
101
|
-
signing_key:
|
99
|
+
rubygems_version: 3.2.32
|
100
|
+
signing_key:
|
102
101
|
specification_version: 4
|
103
|
-
summary: derivative generation
|
102
|
+
summary: IIIF image derivative generation helpers for Vips
|
104
103
|
test_files: []
|
data/ext/imogencv/extconf.rb
DELETED
@@ -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')
|
data/ext/imogencv/imogencv.cpp
DELETED
@@ -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
|
-
}
|
data/lib/imogen/auto_crop/box.rb
DELETED
@@ -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
|