imogen 0.1.9 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/imogen.rb +7 -80
- data/lib/imogen/auto_crop.rb +3 -13
- data/lib/imogen/iiif.rb +4 -9
- data/lib/imogen/iiif/region.rb +48 -4
- data/lib/imogen/iiif/rotation.rb +3 -4
- data/lib/imogen/iiif/size.rb +1 -1
- data/lib/imogen/zoomable.rb +1 -2
- 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 +8 -8
- metadata +14 -16
- 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: 0b9d709cf8f472296f8feafaf8d54a14201fd86b25fb2dfee1f20e0e0994fdaa
|
4
|
+
data.tar.gz: 8ec9471abc118d5d13e661a86ab403695285cb4996b4f2cd47849cd9830a5e19
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5f9a648001fda8a468914b787eb04218bbc2b6c96f4e0ef3bb5331b0e76e2b4285eb9df76c7c66d47151f38a4064676a84ceada530c445b48343d92701aab809
|
7
|
+
data.tar.gz: c35f17d9c6e90732c721fff635684103e5779c0b313f1e9186ecb0dd677b960de4aa7d03daa33b537453e0290788cb9bf6ca332166b792724e139633a91cb8e9
|
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'
|
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.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/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
@@ -5,16 +5,15 @@ class Rotation < Transform
|
|
5
5
|
def get(rotate)
|
6
6
|
return nil if [nil, 0, '0'].include?(rotate)
|
7
7
|
raise BadRequest.new("bad rotate #{rotate}") unless rotate.to_s =~ /^-?\d+$/
|
8
|
-
#
|
9
|
-
r =
|
10
|
-
r = r + 360 if r < 0
|
8
|
+
# libvips and IIIF spec counts clockwise
|
9
|
+
r = rotate.to_i % 360
|
11
10
|
raise BadRequest.new("bad rotate #{rotate}") unless RIGHT_ANGLES.include? r
|
12
11
|
return r > 0 ? r : nil
|
13
12
|
end
|
14
13
|
def self.convert(img, rotate)
|
15
14
|
rotation = Rotation.new(img).get(rotate)
|
16
15
|
if rotation
|
17
|
-
img.
|
16
|
+
yield img.rot("d#{rotation}")
|
18
17
|
else
|
19
18
|
yield img
|
20
19
|
end
|
data/lib/imogen/iiif/size.rb
CHANGED
data/lib/imogen/zoomable.rb
CHANGED
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
|
+
# This allows you to limit a spec run to individual examples or groups
|
51
|
+
# you care about by tagging them with `:focus` metadata. When nothing
|
52
|
+
# is tagged with `:focus`, all examples get run. RSpec also provides
|
53
|
+
# aliases for `it`, `describe`, and `context` that include `:focus`
|
54
|
+
# metadata: `fit`, `fdescribe` and `fcontext`, respectively.
|
55
|
+
config.filter_run_when_matching :focus
|
56
|
+
|
57
|
+
# Allows RSpec to persist some state between runs in order to support
|
58
|
+
# the `--only-failures` and `--next-failure` CLI options. We recommend
|
59
|
+
# you configure your source control system to ignore this file.
|
60
|
+
config.example_status_persistence_file_path = "spec/examples.txt"
|
61
|
+
|
62
|
+
# Limits the available syntax to the non-monkey patched syntax that is
|
63
|
+
# recommended. For more details, see:
|
64
|
+
# - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
|
65
|
+
# - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
66
|
+
# - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
|
67
|
+
config.disable_monkey_patching!
|
68
|
+
|
69
|
+
# This setting enables warnings. It's recommended, but in some cases may
|
70
|
+
# be too noisy due to issues in dependencies.
|
71
|
+
config.warnings = true
|
72
|
+
|
73
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
74
|
+
# file, and it's useful to allow more verbose output when running an
|
75
|
+
# individual spec file.
|
76
|
+
if config.files_to_run.one?
|
77
|
+
# Use the documentation formatter for detailed output,
|
78
|
+
# unless a formatter has already been configured
|
79
|
+
# (e.g. via a command-line flag).
|
80
|
+
config.default_formatter = "doc"
|
81
|
+
end
|
82
|
+
|
83
|
+
# Print the 10 slowest examples and example groups at the
|
84
|
+
# end of the spec run, to help surface which specs are running
|
85
|
+
# particularly slow.
|
86
|
+
config.profile_examples = 10
|
87
|
+
|
88
|
+
# Run specs in random order to surface order dependencies. If you find an
|
89
|
+
# order dependency and want to debug it, you can fix the order by providing
|
90
|
+
# the seed, which is printed after each run.
|
91
|
+
# --seed 1234
|
92
|
+
config.order = :random
|
93
|
+
|
94
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
95
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
96
|
+
# test failures related to randomization by passing the same `--seed` value
|
97
|
+
# as the one that triggered the failure.
|
98
|
+
Kernel.srand config.seed
|
99
|
+
=end
|
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
|
@@ -17,19 +17,19 @@ describe Imogen::Iiif::Rotation, type: :unit do
|
|
17
17
|
end
|
18
18
|
# IIIF rotation is opposite FreeImage
|
19
19
|
it "should calculate for positive values" do
|
20
|
-
expect(subject.get(90)).to eql(
|
21
|
-
expect(subject.get("90")).to eql(
|
20
|
+
expect(subject.get(90)).to eql(90)
|
21
|
+
expect(subject.get("90")).to eql(90)
|
22
22
|
expect(subject.get("180")).to eql(180)
|
23
|
-
expect(subject.get("270")).to eql(
|
24
|
-
expect(subject.get("450")).to eql(
|
23
|
+
expect(subject.get("270")).to eql(270)
|
24
|
+
expect(subject.get("450")).to eql(90)
|
25
25
|
end
|
26
26
|
# IIIF rotation is opposite FreeImage
|
27
27
|
it "should calculate for negative values" do
|
28
|
-
expect(subject.get(-90)).to eql(
|
29
|
-
expect(subject.get("-90")).to eql(
|
28
|
+
expect(subject.get(-90)).to eql(270)
|
29
|
+
expect(subject.get("-90")).to eql(270)
|
30
30
|
expect(subject.get("-180")).to eql(180)
|
31
|
-
expect(subject.get("-270")).to eql(
|
32
|
-
expect(subject.get("-450")).to eql(
|
31
|
+
expect(subject.get("-270")).to eql(90)
|
32
|
+
expect(subject.get("-450")).to eql(270)
|
33
33
|
end
|
34
34
|
end
|
35
35
|
it "should reject arbitrary integer and float values" do
|
metadata
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: imogen
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Armintor
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-11-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: ruby-vips
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
@@ -25,7 +25,7 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
@@ -39,32 +39,27 @@ dependencies:
|
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - "
|
45
|
+
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '3.9'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - "
|
52
|
+
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '3.9'
|
55
55
|
description:
|
56
56
|
email: armintor@gmail.com
|
57
57
|
executables: []
|
58
|
-
extensions:
|
59
|
-
- ext/imogencv/extconf.rb
|
58
|
+
extensions: []
|
60
59
|
extra_rdoc_files: []
|
61
60
|
files:
|
62
|
-
- ext/imogencv/extconf.rb
|
63
|
-
- ext/imogencv/imogencv.cpp
|
64
61
|
- lib/imogen.rb
|
65
62
|
- lib/imogen/auto_crop.rb
|
66
|
-
- lib/imogen/auto_crop/box.rb
|
67
|
-
- lib/imogen/auto_crop/edges.rb
|
68
63
|
- lib/imogen/dzi.rb
|
69
64
|
- lib/imogen/iiif.rb
|
70
65
|
- lib/imogen/iiif/region.rb
|
@@ -73,6 +68,9 @@ files:
|
|
73
68
|
- lib/imogen/iiif/tiles.rb
|
74
69
|
- lib/imogen/zoomable.rb
|
75
70
|
- lib/imogencv.bundle
|
71
|
+
- spec/fixtures/files/sample.jpg
|
72
|
+
- spec/integration/imogen_autocrop_spec.rb
|
73
|
+
- spec/integration/imogen_iiif_spec.rb
|
76
74
|
- spec/spec_helper.rb
|
77
75
|
- spec/unit/imogen_iiif_quality_spec.rb
|
78
76
|
- spec/unit/imogen_iiif_region_spec.rb
|
@@ -100,5 +98,5 @@ requirements: []
|
|
100
98
|
rubygems_version: 3.0.6
|
101
99
|
signing_key:
|
102
100
|
specification_version: 4
|
103
|
-
summary: derivative generation
|
101
|
+
summary: IIIF image derivative generation helpers for Vips
|
104
102
|
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
|