riiif 2.7.0 → 2.8.1
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 +4 -4
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ruby.yml +3 -3
- data/.rubocop_todo.yml +7 -1
- data/Gemfile +2 -5
- data/README.md +26 -4
- data/app/extractors/riiif/abstract_info_extractor.rb +12 -0
- data/app/{services → extractors}/riiif/image_magick_info_extractor.rb +1 -6
- data/app/extractors/riiif/vips_info_extractor.rb +22 -0
- data/app/models/riiif/file.rb +11 -2
- data/app/services/riiif/crop.rb +40 -0
- data/app/services/riiif/vips_resize.rb +47 -0
- data/app/transformers/riiif/vips_transformer.rb +103 -0
- data/docs/vips_comparison.md +257 -0
- data/lib/riiif/engine.rb +4 -0
- data/lib/riiif/version.rb +1 -1
- data/lib/riiif.rb +4 -0
- data/riiif.gemspec +1 -0
- data/spec/extractors/riiif/image_magick_info_extractor_spec.rb +31 -0
- data/spec/extractors/riiif/vips_info_extractor_spec.rb +76 -0
- data/spec/fixtures/test.jpg +0 -0
- data/spec/fixtures/test.png +0 -0
- data/spec/fixtures/test.tif +0 -0
- data/spec/models/riiif/file_spec.rb +35 -0
- data/spec/test_app_templates/lib/generators/test_app_generator.rb +6 -1
- data/spec/transformers/riiif/vips_transformer_spec.rb +271 -0
- metadata +38 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5c83cf037ced763741c5aa704db17564fd43ee2610866d27629b1de4700c518a
|
|
4
|
+
data.tar.gz: 447135b1b851c29969cd42a91a51b26facb1084b47dc246422b11a15de0dcbc5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 86952c0517dfd44217aee70ae4927209aaa7c4078c9142126a6f0a06fe831774371dffb0191c2719cb868249b98b324cbfbf7853c2518622bb67dc2d1afab182
|
|
7
|
+
data.tar.gz: f315b4762cf831614a80728ac072678ea4ad9f1fc38cbc836cd7f6e3eaf6c2009b870682e0024d4cebca02c9da919a33dde3b66bcd98ac8da5f2f4d5691b23fd
|
data/.github/workflows/ruby.yml
CHANGED
|
@@ -18,12 +18,12 @@ jobs:
|
|
|
18
18
|
runs-on: ubuntu-latest
|
|
19
19
|
strategy:
|
|
20
20
|
matrix:
|
|
21
|
-
ruby: ["3.2", "3.3"]
|
|
22
|
-
rails: ["8.
|
|
21
|
+
ruby: ["3.2", "3.3", "3.4"]
|
|
22
|
+
rails: ["8.1.1", "8.0.4", "7.2.3"]
|
|
23
23
|
steps:
|
|
24
24
|
- name: Install ImageMagick
|
|
25
25
|
run: sudo apt install imagemagick
|
|
26
|
-
- uses: actions/checkout@
|
|
26
|
+
- uses: actions/checkout@v6
|
|
27
27
|
- name: Set up Ruby
|
|
28
28
|
uses: ruby/setup-ruby@v1
|
|
29
29
|
with:
|
data/.rubocop_todo.yml
CHANGED
|
@@ -69,7 +69,13 @@ Lint/SuppressedException:
|
|
|
69
69
|
Metrics/BlockLength:
|
|
70
70
|
Max: 237
|
|
71
71
|
|
|
72
|
-
# Offense count:
|
|
72
|
+
# Offense count: 2
|
|
73
|
+
Metrics/ClassLength:
|
|
74
|
+
Exclude:
|
|
75
|
+
- 'app/services/riiif/crop.rb'
|
|
76
|
+
- 'app/services/riiif/resize.rb'
|
|
77
|
+
|
|
78
|
+
# Offense count: 1
|
|
73
79
|
# Configuration parameters: IgnoredMethods.
|
|
74
80
|
Metrics/CyclomaticComplexity:
|
|
75
81
|
Max: 7
|
data/Gemfile
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
source 'https://rubygems.org'
|
|
2
2
|
|
|
3
|
-
gem 'byebug'
|
|
4
3
|
# Specify your gem's dependencies in riiif.gemspec
|
|
5
4
|
gemspec
|
|
6
5
|
|
|
7
6
|
# BEGIN ENGINE_CART BLOCK
|
|
8
|
-
# engine_cart: 2.0
|
|
9
|
-
# engine_cart stanza:
|
|
7
|
+
# engine_cart: 2.6.0
|
|
8
|
+
# engine_cart stanza: 2.5.0
|
|
10
9
|
# the below comes from engine_cart, a gem used to test this Rails engine gem in the context of a Rails app.
|
|
11
10
|
file = File.expand_path('Gemfile', ENV['ENGINE_CART_DESTINATION'] || ENV['RAILS_ROOT'] || File.expand_path('.internal_test_app', File.dirname(__FILE__)))
|
|
12
11
|
if File.exist?(file)
|
|
@@ -27,7 +26,5 @@ else
|
|
|
27
26
|
gem 'rails', ENV['RAILS_VERSION']
|
|
28
27
|
end
|
|
29
28
|
end
|
|
30
|
-
|
|
31
|
-
gem 'sass-rails', '~> 5.0'
|
|
32
29
|
end
|
|
33
30
|
# END ENGINE_CART BLOCK
|
data/README.md
CHANGED
|
@@ -5,7 +5,9 @@ A Ruby IIIF image server as a rails engine.
|
|
|
5
5
|
|
|
6
6
|
## Installation
|
|
7
7
|
|
|
8
|
-
RIIIF
|
|
8
|
+
To use RIIIF, you need to install at least one of Imagemagick, Graphicsmagick, or Vips (libvips) (see [comparison](docs/vips_comparison.md)). By default, RIIIF will use Imagemagick.
|
|
9
|
+
|
|
10
|
+
To install Imagemagick on a Mac using Homebrew you can follow these instructions:
|
|
9
11
|
|
|
10
12
|
ImageMagick (7.0.4) may be installed with a few options:
|
|
11
13
|
* `--with-ghostscript` Compile with Ghostscript for Postscript/PDF support
|
|
@@ -31,6 +33,16 @@ Or install it yourself as:
|
|
|
31
33
|
|
|
32
34
|
## Configure
|
|
33
35
|
|
|
36
|
+
Any of the following code examples should be included in a `config/initializers/riiif.rb` file in your application (or somewhere else loaded in your app environment) like so:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
Rails.application.config.to_prepare do
|
|
40
|
+
# code goes here
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
For test applications generated by engine cart (see [Running the tests](#running-the-tests)), this would be `<cloned repo folder>/.internal_test_app/config/initializers/riiif.rb`.
|
|
45
|
+
|
|
34
46
|
### Images on the servers file system.
|
|
35
47
|
|
|
36
48
|
By default Riiif is set to load images from the filesystem using the Riiif::FileSystemFileResolver.
|
|
@@ -75,15 +87,25 @@ See [benchmark](docs/benchmark.md) for details
|
|
|
75
87
|
|
|
76
88
|
To use [GraphicsMagick](http://www.graphicsmagick.org/) instead of ImageMagick
|
|
77
89
|
|
|
78
|
-
|
|
79
|
-
|
|
90
|
+
```ruby
|
|
91
|
+
Riiif::ImagemagickCommandFactory.external_command = "gm convert"
|
|
92
|
+
Riiif::ImageMagickInfoExtractor.external_command = "gm identify"
|
|
93
|
+
```
|
|
80
94
|
|
|
81
95
|
You will of course need to install GraphicsMagick on your system.
|
|
82
96
|
|
|
97
|
+
### Libvips (aka Vips)
|
|
98
|
+
|
|
99
|
+
To use [libvips](https://www.libvips.org/) instead of ImageMagick
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
Riiif::Engine.config.use_vips = true
|
|
103
|
+
```
|
|
104
|
+
|
|
83
105
|
## Usage
|
|
84
106
|
|
|
85
107
|
Add the routes to your application by inserting the following line into `config/routes.rb`
|
|
86
|
-
```
|
|
108
|
+
```ruby
|
|
87
109
|
mount Riiif::Engine => '/image-service', as: 'riiif'
|
|
88
110
|
```
|
|
89
111
|
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
module Riiif
|
|
2
2
|
# Get information using imagemagick to interrogate the file
|
|
3
|
-
class ImageMagickInfoExtractor
|
|
3
|
+
class ImageMagickInfoExtractor < AbstractInfoExtractor
|
|
4
4
|
# perhaps you want to use GraphicsMagick instead, set to "gm identify"
|
|
5
|
-
class_attribute :external_command
|
|
6
5
|
self.external_command = 'identify'
|
|
7
6
|
|
|
8
|
-
def initialize(path)
|
|
9
|
-
@path = path
|
|
10
|
-
end
|
|
11
|
-
|
|
12
7
|
def extract
|
|
13
8
|
height, width, format, channels = Riiif::CommandRunner.execute(
|
|
14
9
|
"#{external_command} -format '%h %w %m %[channels]' '#{@path}[0]'"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'ruby-vips' if Riiif::Engine.config.use_vips
|
|
2
|
+
|
|
3
|
+
module Riiif
|
|
4
|
+
# Get information using (lib)vips to interrogate the file
|
|
5
|
+
class VipsInfoExtractor < AbstractInfoExtractor
|
|
6
|
+
self.external_command = 'vipsheader'
|
|
7
|
+
|
|
8
|
+
def extract
|
|
9
|
+
attributes = Riiif::CommandRunner.execute("#{external_command} '#{@path}' -a")
|
|
10
|
+
.split(/\n/)
|
|
11
|
+
.map { |str| str.strip.split(': ', 2) }.to_h
|
|
12
|
+
width, height = attributes.values_at("width", "height")
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
height: Integer(height),
|
|
16
|
+
width: Integer(width),
|
|
17
|
+
format: attributes["vips-loader"].match?("pngload") ? "PNG" : "JPEG",
|
|
18
|
+
channels: ::Vips::Image.new_from_file(@path.to_s).has_alpha? ? "srgba" : "srgb"
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/app/models/riiif/file.rb
CHANGED
|
@@ -4,7 +4,14 @@ module Riiif
|
|
|
4
4
|
|
|
5
5
|
class_attribute :info_extractor_class
|
|
6
6
|
# TODO: add alternative that uses kdu_jp2info
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
def self.info_extractor_class
|
|
9
|
+
if Riiif.use_vips?
|
|
10
|
+
VipsInfoExtractor
|
|
11
|
+
else
|
|
12
|
+
ImageMagickInfoExtractor
|
|
13
|
+
end
|
|
14
|
+
end
|
|
8
15
|
|
|
9
16
|
# @param input_path [String] The location of an image file
|
|
10
17
|
def initialize(input_path, tempfile = nil)
|
|
@@ -20,7 +27,9 @@ module Riiif
|
|
|
20
27
|
end
|
|
21
28
|
|
|
22
29
|
def transformer
|
|
23
|
-
if Riiif.
|
|
30
|
+
if Riiif.use_vips?
|
|
31
|
+
VipsTransformer
|
|
32
|
+
elsif Riiif.kakadu_enabled? && path.ends_with?('.jp2')
|
|
24
33
|
KakaduTransformer
|
|
25
34
|
else
|
|
26
35
|
ImagemagickTransformer
|
data/app/services/riiif/crop.rb
CHANGED
|
@@ -45,8 +45,35 @@ module Riiif
|
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
def to_vips
|
|
49
|
+
case region
|
|
50
|
+
when IIIF::Image::Region::Full
|
|
51
|
+
nil
|
|
52
|
+
when IIIF::Image::Region::Absolute
|
|
53
|
+
[region.offset_x, region.offset_y, region.width, region.height]
|
|
54
|
+
when IIIF::Image::Region::Square
|
|
55
|
+
vips_square
|
|
56
|
+
when IIIF::Image::Region::Percent
|
|
57
|
+
vips_percent
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
48
61
|
private
|
|
49
62
|
|
|
63
|
+
def vips_percent
|
|
64
|
+
# Calculate x values
|
|
65
|
+
offset_x, width = [region.x_pct, region.width_pct].map do |percent|
|
|
66
|
+
(image_info.width * percentage_to_fraction(percent)).round
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Calculate y values
|
|
70
|
+
offset_y, height = [region.y_pct, region.height_pct].map do |percent|
|
|
71
|
+
(image_info.height * percentage_to_fraction(percent)).round
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
[offset_x, offset_y, width, height]
|
|
75
|
+
end
|
|
76
|
+
|
|
50
77
|
def imagemagick_percent
|
|
51
78
|
offset_x = (image_info.width * percentage_to_fraction(region.x_pct)).round
|
|
52
79
|
offset_y = (image_info.height * percentage_to_fraction(region.y_pct)).round
|
|
@@ -60,6 +87,19 @@ module Riiif
|
|
|
60
87
|
"\{#{percentage_to_fraction(region.height_pct)},#{percentage_to_fraction(region.width_pct)}\}"
|
|
61
88
|
end
|
|
62
89
|
|
|
90
|
+
def vips_square
|
|
91
|
+
min, max = [image_info.width, image_info.height].minmax
|
|
92
|
+
offset = (max - min) / 2
|
|
93
|
+
|
|
94
|
+
if image_info.height >= image_info.width
|
|
95
|
+
# Portrait: left, offset, width, height
|
|
96
|
+
[0, offset, min, min]
|
|
97
|
+
else
|
|
98
|
+
# Landscape: left, offset, width, height
|
|
99
|
+
[offset, 0, min, min]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
63
103
|
def kakadu_square
|
|
64
104
|
min, max = [image_info.width, image_info.height].minmax
|
|
65
105
|
offset = (max - min) / 2
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Riiif
|
|
2
|
+
# Represents a resize operation
|
|
3
|
+
class VipsResize
|
|
4
|
+
def initialize(size, image)
|
|
5
|
+
@size = size
|
|
6
|
+
@image = image
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
attr_reader :size, :image
|
|
10
|
+
|
|
11
|
+
# @return the parameters that vips will use to resize the image. This can be
|
|
12
|
+
# 1. A [Float] representing the scale factor, passed to Vips::Image#resize
|
|
13
|
+
# 2. An [Array], where the 1st elem is an Integer and the 2nd is a
|
|
14
|
+
# Hash of options, passed to Vips::Image#thumbnail
|
|
15
|
+
# 3. [NilClass] when image should not be resized at all
|
|
16
|
+
def to_vips
|
|
17
|
+
case size
|
|
18
|
+
when IIIF::Image::Size::Percent
|
|
19
|
+
size.percentage
|
|
20
|
+
when IIIF::Image::Size::Width
|
|
21
|
+
resize_ratio(:width, image)
|
|
22
|
+
when IIIF::Image::Size::Height
|
|
23
|
+
resize_ratio(:height, image)
|
|
24
|
+
when IIIF::Image::Size::Absolute
|
|
25
|
+
[size.width, { height: size.height, size: :force }]
|
|
26
|
+
when IIIF::Image::Size::BestFit
|
|
27
|
+
[size.width, { height: size.height }]
|
|
28
|
+
when IIIF::Image::Size::Max, IIIF::Image::Size::Full
|
|
29
|
+
nil
|
|
30
|
+
else
|
|
31
|
+
raise "unknown size #{size.class}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param [Symbol] - which side of the image to calculate, either :width or :height
|
|
36
|
+
# @return [Float] - the scale or percentage to resize the image by; passed to Vips::Image#resize
|
|
37
|
+
def resize_ratio(side, image)
|
|
38
|
+
length = image.send(side)
|
|
39
|
+
target_length = size.send(side)
|
|
40
|
+
if target_length < length
|
|
41
|
+
target_length / length.to_f # Size down
|
|
42
|
+
else
|
|
43
|
+
length / target_length.to_f # Size up
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Use ruby-vips to execute image transformations (via ffi gem) instead of
|
|
2
|
+
# using the vips CLI. Since vips CLI commands can't be chained without creating
|
|
3
|
+
# temp files after each operation, using the CLI would decrease performance.
|
|
4
|
+
# See 'Chaining operations': https://www.libvips.org/API/current/using-cli.html
|
|
5
|
+
require 'ruby-vips' if Riiif::Engine.config.use_vips
|
|
6
|
+
|
|
7
|
+
module Riiif
|
|
8
|
+
class VipsTransformer < AbstractTransformer
|
|
9
|
+
include ActiveSupport::Benchmarkable
|
|
10
|
+
delegate :logger, to: :Rails
|
|
11
|
+
|
|
12
|
+
# @param path [String] The path of the source image file
|
|
13
|
+
# @param image_info [ImageInformation] information about the source
|
|
14
|
+
# @param [IIIF::Image::Transformation] transformation
|
|
15
|
+
def initialize(path, image_info, transformation, compression: 85, subsample: true, strip_metadata: true)
|
|
16
|
+
super(path, image_info, transformation)
|
|
17
|
+
@image = ::Vips::Image.new_from_file(path.to_s)
|
|
18
|
+
@compression = compression
|
|
19
|
+
@subsample = subsample
|
|
20
|
+
@strip_metadata = strip_metadata
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
attr_reader :image, :path, :compression, :subsample, :strip_metadata
|
|
24
|
+
|
|
25
|
+
# @return [String] all the image data
|
|
26
|
+
def transform
|
|
27
|
+
benchmark("Riiif transformed image using vips") do
|
|
28
|
+
transform_image.write_to_buffer(".#{format}#{format_options}")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Chain every method in the array together and apply it to the image
|
|
35
|
+
# @return [Vips::Image] - the image after all transformations
|
|
36
|
+
def transform_image
|
|
37
|
+
result = [crop, resize, rotate, colourspace].reduce(image) do |image, array|
|
|
38
|
+
method, options = array
|
|
39
|
+
# Options are blank when transformation is not required (e.g. when requesting full size)
|
|
40
|
+
next image if options.blank?
|
|
41
|
+
|
|
42
|
+
case method
|
|
43
|
+
when :resize
|
|
44
|
+
image.send(method, VipsResize.new(transformation.size, image).to_vips)
|
|
45
|
+
when :thumbnail_image
|
|
46
|
+
# .thumbnail_image needs a positional argument (width) and keyword args (options)
|
|
47
|
+
# https://www.rubydoc.info/gems/ruby-vips/Vips/Image#thumbnail_image-instance_method
|
|
48
|
+
image.send(method, options.first, **options.last)
|
|
49
|
+
when :crop
|
|
50
|
+
# .crop needs positional arguments
|
|
51
|
+
image.send(method, *options)
|
|
52
|
+
else # :rotate or :colourspace
|
|
53
|
+
image.send(method, options)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
# If result should be bitonal, set a value threshold
|
|
57
|
+
# https://github.com/libvips/libvips/issues/1840
|
|
58
|
+
transformation.quality == 'bitonal' ? (result > 200) : result
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def format
|
|
62
|
+
# In cases where the input file has an alpha_channel but the transformation
|
|
63
|
+
# format is 'jpg', change to 'png' as jpeg does not support alpha channels
|
|
64
|
+
image.has_alpha? && transformation.format == 'jpg' ? 'png' : transformation.format
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def format_options
|
|
68
|
+
format_string = [compression,
|
|
69
|
+
("optimize-coding" if format == 'jpg'),
|
|
70
|
+
("strip" if strip_metadata),
|
|
71
|
+
("no-subsample" unless subsample)].select(&:present?).join(',')
|
|
72
|
+
|
|
73
|
+
"[Q=#{format_string}]"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def resize
|
|
77
|
+
case transformation.size
|
|
78
|
+
when IIIF::Image::Size::Percent, IIIF::Image::Size::Width, IIIF::Image::Size::Height
|
|
79
|
+
[:resize, transformation.size]
|
|
80
|
+
else # IIIF::Image::Size::Absolute, IIIF::Image::Size::BestFit
|
|
81
|
+
[:thumbnail_image, VipsResize.new(transformation.size, image).to_vips]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def crop
|
|
86
|
+
[:crop, Crop.new(transformation.region, image_info).to_vips]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def rotate
|
|
90
|
+
angle = transformation.rotation.zero? ? nil : transformation.rotation
|
|
91
|
+
[:rotate, angle]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def colourspace
|
|
95
|
+
case transformation.quality
|
|
96
|
+
when 'gray', 'bitonal'
|
|
97
|
+
[:colourspace, :b_w]
|
|
98
|
+
else
|
|
99
|
+
[:colourspace, nil]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# Benchmarks comparing Vips, Imagemagick, and Graphicsmagick
|
|
2
|
+
|
|
3
|
+
The following benchmarks were run using ApacheBench v. 2.3 on Ubuntu / Windows Subsystem for Linux (WSL) on a 32 GB (RAM) laptop.
|
|
4
|
+
|
|
5
|
+
To generate the tables below, a simple resize `curl` command (more details below) was run 50 times in a row and the median response time was calculated.
|
|
6
|
+
|
|
7
|
+
## Testing with a JPEG
|
|
8
|
+
|
|
9
|
+
For a 4264 x 3282 jpeg image (26.8 MB)
|
|
10
|
+
|
|
11
|
+
| Software Used | Median processing time (ms) | Mean processing time (ms) |
|
|
12
|
+
| ---------------|-----------------------------|---------------------------|
|
|
13
|
+
| Imagemagick | 753 | 753 |
|
|
14
|
+
| Graphicsmagick | 662 | 660 |
|
|
15
|
+
| Vips | 79 | 78 |
|
|
16
|
+
|
|
17
|
+
## Testing with a TIFF
|
|
18
|
+
|
|
19
|
+
For a 7800 x 5865 tif image (7.11 MB)
|
|
20
|
+
|
|
21
|
+
| Software Used | Median processing time (ms) | Mean processing time (ms) |
|
|
22
|
+
| ---------------|-----------------------------|---------------------------|
|
|
23
|
+
| Imagemagick | 1091 | 1089 |
|
|
24
|
+
| Graphicsmagick | 800 | 796 |
|
|
25
|
+
| Vips | 130 | 139 |
|
|
26
|
+
|
|
27
|
+
## More Resources & Discussion
|
|
28
|
+
|
|
29
|
+
Those interested in more comprehensive benchmarking with Ruby may be interested in the [vips-benchmarks](https://github.com/jcupitt/vips-benchmarks?tab=readme-ov-file) code repository, which also tests memory usage.
|
|
30
|
+
|
|
31
|
+
Glen Robson, Stefano Cossu, Ruven Pillay, and Michael D. Smith have written an [excellent article comparing the speed of different image processing tools and formats in a IIIF context](https://journal.code4lib.org/articles/17596). They write, "The testing clearly shows that tiled multi-resolution pyramid TIFF is the fastest format for IIIF, but it comes at a cost of significantly more storage space compared to both HTJ2K [([High Throughput JPEG2000](https://jpeg.org/jpeg2000/htj2k.html))] and JP2." The latter two standards are used by [Kakadu](https://kakadusoftware.com/), a proprietary image toolkit that is commonly used for IIIF servers.
|
|
32
|
+
|
|
33
|
+
Based on their results, institutions/organizations that use large TIFFs as the base image for IIIF derivatives will likely see the best performance using vips. Conversely, institutions/organizations that use JP2 images will get the best performance using Kakadu HTJ2K.
|
|
34
|
+
|
|
35
|
+
## Command and Detailed Results for JPGs
|
|
36
|
+
|
|
37
|
+
Command: `ab -n 50 'http://localhost:3000/images/irises/full/!500,500/0/default.jpg'`
|
|
38
|
+
|
|
39
|
+
### Using imagemagick
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
Document Path: /images/irises/full/!500,500/0/default.jpg
|
|
43
|
+
Document Length: 79018 bytes
|
|
44
|
+
|
|
45
|
+
Concurrency Level: 1
|
|
46
|
+
Time taken for tests: 37.655 seconds
|
|
47
|
+
Complete requests: 50
|
|
48
|
+
Failed requests: 0
|
|
49
|
+
Total transferred: 3995500 bytes
|
|
50
|
+
HTML transferred: 3950900 bytes
|
|
51
|
+
Requests per second: 1.33 [#/sec] (mean)
|
|
52
|
+
Time per request: 753.097 [ms] (mean)
|
|
53
|
+
Time per request: 753.097 [ms] (mean, across all concurrent requests)
|
|
54
|
+
Transfer rate: 103.62 [Kbytes/sec] received
|
|
55
|
+
|
|
56
|
+
Connection Times (ms)
|
|
57
|
+
min mean[+/-sd] median max
|
|
58
|
+
Connect: 0 0 0.0 0 0
|
|
59
|
+
Processing: 701 753 28.6 753 813
|
|
60
|
+
Waiting: 701 753 28.6 753 813
|
|
61
|
+
Total: 701 753 28.6 753 813
|
|
62
|
+
|
|
63
|
+
Percentage of the requests served within a certain time (ms)
|
|
64
|
+
50% 753
|
|
65
|
+
66% 768
|
|
66
|
+
75% 774
|
|
67
|
+
80% 778
|
|
68
|
+
90% 794
|
|
69
|
+
95% 810
|
|
70
|
+
98% 813
|
|
71
|
+
99% 813
|
|
72
|
+
100% 813 (longest request)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Using graphicsmagick
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
Document Path: /images/irises/full/!500,500/0/default.jpg
|
|
79
|
+
Document Length: 78992 bytes
|
|
80
|
+
|
|
81
|
+
Concurrency Level: 1
|
|
82
|
+
Time taken for tests: 33.131 seconds
|
|
83
|
+
Complete requests: 50
|
|
84
|
+
Failed requests: 0
|
|
85
|
+
Total transferred: 3994173 bytes
|
|
86
|
+
HTML transferred: 3949600 bytes
|
|
87
|
+
Requests per second: 1.51 [#/sec] (mean)
|
|
88
|
+
Time per request: 662.629 [ms] (mean)
|
|
89
|
+
Time per request: 662.629 [ms] (mean, across all concurrent requests)
|
|
90
|
+
Transfer rate: 117.73 [Kbytes/sec] received
|
|
91
|
+
|
|
92
|
+
Connection Times (ms)
|
|
93
|
+
min mean[+/-sd] median max
|
|
94
|
+
Connect: 0 0 0.0 0 0
|
|
95
|
+
Processing: 539 662 64.9 660 847
|
|
96
|
+
Waiting: 539 662 64.9 660 847
|
|
97
|
+
Total: 539 663 65.0 660 848
|
|
98
|
+
|
|
99
|
+
Percentage of the requests served within a certain time (ms)
|
|
100
|
+
50% 660
|
|
101
|
+
66% 685
|
|
102
|
+
75% 703
|
|
103
|
+
80% 720
|
|
104
|
+
90% 739
|
|
105
|
+
95% 775
|
|
106
|
+
98% 848
|
|
107
|
+
99% 848
|
|
108
|
+
100% 848 (longest request)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Using libvips
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
Document Path: /images/irises/full/!500,500/0/default.jpg
|
|
115
|
+
Document Length: 77647 bytes
|
|
116
|
+
|
|
117
|
+
Concurrency Level: 1
|
|
118
|
+
Time taken for tests: 3.920 seconds
|
|
119
|
+
Complete requests: 50
|
|
120
|
+
Failed requests: 0
|
|
121
|
+
Total transferred: 3926860 bytes
|
|
122
|
+
HTML transferred: 3882350 bytes
|
|
123
|
+
Requests per second: 12.75 [#/sec] (mean)
|
|
124
|
+
Time per request: 78.409 [ms] (mean)
|
|
125
|
+
Time per request: 78.409 [ms] (mean, across all concurrent requests)
|
|
126
|
+
Transfer rate: 978.16 [Kbytes/sec] received
|
|
127
|
+
|
|
128
|
+
Connection Times (ms)
|
|
129
|
+
min mean[+/-sd] median max
|
|
130
|
+
Connect: 0 0 0.0 0 0
|
|
131
|
+
Processing: 67 78 5.5 79 90
|
|
132
|
+
Waiting: 67 78 5.5 79 90
|
|
133
|
+
Total: 67 78 5.5 79 90
|
|
134
|
+
|
|
135
|
+
Percentage of the requests served within a certain time (ms)
|
|
136
|
+
50% 79
|
|
137
|
+
66% 81
|
|
138
|
+
75% 81
|
|
139
|
+
80% 82
|
|
140
|
+
90% 86
|
|
141
|
+
95% 88
|
|
142
|
+
98% 90
|
|
143
|
+
99% 90
|
|
144
|
+
100% 90 (longest request)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Command and Detailed Results for TIFFs
|
|
148
|
+
|
|
149
|
+
Command: `ab -n 50 'http://localhost:3000/images/big/full/!500,500/0/default.jpg'`
|
|
150
|
+
|
|
151
|
+
### Using imagemagick
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
Document Path: /images/big/full/!500,500/0/default.jpg
|
|
155
|
+
Document Length: 82826 bytes
|
|
156
|
+
|
|
157
|
+
Concurrency Level: 1
|
|
158
|
+
Time taken for tests: 54.537 seconds
|
|
159
|
+
Complete requests: 50
|
|
160
|
+
Failed requests: 0
|
|
161
|
+
Total transferred: 4185950 bytes
|
|
162
|
+
HTML transferred: 4141300 bytes
|
|
163
|
+
Requests per second: 0.92 [#/sec] (mean)
|
|
164
|
+
Time per request: 1090.745 [ms] (mean)
|
|
165
|
+
Time per request: 1090.745 [ms] (mean, across all concurrent requests)
|
|
166
|
+
Transfer rate: 74.96 [Kbytes/sec] received
|
|
167
|
+
|
|
168
|
+
Connection Times (ms)
|
|
169
|
+
min mean[+/-sd] median max
|
|
170
|
+
Connect: 0 0 0.0 0 0
|
|
171
|
+
Processing: 1004 1091 39.8 1089 1163
|
|
172
|
+
Waiting: 1004 1091 39.8 1089 1163
|
|
173
|
+
Total: 1004 1091 39.8 1089 1163
|
|
174
|
+
|
|
175
|
+
Percentage of the requests served within a certain time (ms)
|
|
176
|
+
50% 1089
|
|
177
|
+
66% 1113
|
|
178
|
+
75% 1120
|
|
179
|
+
80% 1139
|
|
180
|
+
90% 1149
|
|
181
|
+
95% 1156
|
|
182
|
+
98% 1163
|
|
183
|
+
99% 1163
|
|
184
|
+
100% 1163 (longest request)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Using graphicsmagick
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
Document Path: /images/big/full/!500,500/0/default.jpg
|
|
191
|
+
Document Length: 82841 bytes
|
|
192
|
+
|
|
193
|
+
Concurrency Level: 1
|
|
194
|
+
Time taken for tests: 39.825 seconds
|
|
195
|
+
Complete requests: 50
|
|
196
|
+
Failed requests: 0
|
|
197
|
+
Total transferred: 4186581 bytes
|
|
198
|
+
HTML transferred: 4142050 bytes
|
|
199
|
+
Requests per second: 1.26 [#/sec] (mean)
|
|
200
|
+
Time per request: 796.495 [ms] (mean)
|
|
201
|
+
Time per request: 796.495 [ms] (mean, across all concurrent requests)
|
|
202
|
+
Transfer rate: 102.66 [Kbytes/sec] received
|
|
203
|
+
|
|
204
|
+
Connection Times (ms)
|
|
205
|
+
min mean[+/-sd] median max
|
|
206
|
+
Connect: 0 0 0.0 0 0
|
|
207
|
+
Processing: 642 796 66.2 800 924
|
|
208
|
+
Waiting: 642 796 66.2 800 924
|
|
209
|
+
Total: 642 796 66.2 800 924
|
|
210
|
+
|
|
211
|
+
Percentage of the requests served within a certain time (ms)
|
|
212
|
+
50% 800
|
|
213
|
+
66% 832
|
|
214
|
+
75% 845
|
|
215
|
+
80% 859
|
|
216
|
+
90% 880
|
|
217
|
+
95% 899
|
|
218
|
+
98% 924
|
|
219
|
+
99% 924
|
|
220
|
+
100% 924 (longest request)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Using libvips
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
Document Path: /images/big/full/!500,500/0/default.jpg
|
|
227
|
+
Document Length: 81878 bytes
|
|
228
|
+
|
|
229
|
+
Concurrency Level: 1
|
|
230
|
+
Time taken for tests: 6.661 seconds
|
|
231
|
+
Complete requests: 50
|
|
232
|
+
Failed requests: 0
|
|
233
|
+
Total transferred: 4138502 bytes
|
|
234
|
+
HTML transferred: 4093900 bytes
|
|
235
|
+
Requests per second: 7.51 [#/sec] (mean)
|
|
236
|
+
Time per request: 133.222 [ms] (mean)
|
|
237
|
+
Time per request: 133.222 [ms] (mean, across all concurrent requests)
|
|
238
|
+
Transfer rate: 606.73 [Kbytes/sec] received
|
|
239
|
+
|
|
240
|
+
Connection Times (ms)
|
|
241
|
+
min mean[+/-sd] median max
|
|
242
|
+
Connect: 0 0 0.0 0 0
|
|
243
|
+
Processing: 115 133 12.0 130 160
|
|
244
|
+
Waiting: 115 133 12.0 130 160
|
|
245
|
+
Total: 115 133 12.0 130 160
|
|
246
|
+
|
|
247
|
+
Percentage of the requests served within a certain time (ms)
|
|
248
|
+
50% 130
|
|
249
|
+
66% 141
|
|
250
|
+
75% 143
|
|
251
|
+
80% 146
|
|
252
|
+
90% 150
|
|
253
|
+
95% 154
|
|
254
|
+
98% 160
|
|
255
|
+
99% 160
|
|
256
|
+
100% 160 (longest request)
|
|
257
|
+
```
|
data/lib/riiif/engine.rb
CHANGED
|
@@ -11,6 +11,10 @@ module Riiif
|
|
|
11
11
|
# Set to true to use kdu for jp2000 source images
|
|
12
12
|
config.kakadu_enabled = false
|
|
13
13
|
|
|
14
|
+
# Set to true to use libvips to transform images
|
|
15
|
+
# https://www.libvips.org/
|
|
16
|
+
config.use_vips = false
|
|
17
|
+
|
|
14
18
|
config.before_configuration do
|
|
15
19
|
# see https://github.com/fxn/zeitwerk#for_gem
|
|
16
20
|
# We put a generator into LOCAL APP lib/generators, so tell
|
data/lib/riiif/version.rb
CHANGED
data/lib/riiif.rb
CHANGED
data/riiif.gemspec
CHANGED
|
@@ -21,6 +21,7 @@ Gem::Specification.new do |spec|
|
|
|
21
21
|
spec.add_dependency 'railties', '>= 4.2', '< 9'
|
|
22
22
|
spec.add_dependency 'deprecation', '>= 1.0.0'
|
|
23
23
|
spec.add_dependency 'iiif-image-api', '>= 0.1.0'
|
|
24
|
+
spec.add_dependency 'ruby-vips'
|
|
24
25
|
|
|
25
26
|
spec.add_development_dependency 'bundler'
|
|
26
27
|
spec.add_development_dependency 'rake'
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
RSpec.describe Riiif::ImageMagickInfoExtractor do
|
|
2
|
+
it 'uses identify as its external command' do
|
|
3
|
+
expect(described_class.external_command).to eq "identify"
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
context 'with a jpg' do
|
|
7
|
+
let(:image) { Rails.root.join("spec", "fixtures", "test.jpg") }
|
|
8
|
+
|
|
9
|
+
it 'returns the extracted attributes' do
|
|
10
|
+
expect(described_class.new(image).extract).to eq({
|
|
11
|
+
height: 397,
|
|
12
|
+
width: 300,
|
|
13
|
+
format: "JPEG",
|
|
14
|
+
channels: "srgb"
|
|
15
|
+
})
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
context 'with a png' do
|
|
20
|
+
let(:image) { Rails.root.join("spec", "fixtures", "test.png") }
|
|
21
|
+
|
|
22
|
+
it 'returns the extracted attributes' do
|
|
23
|
+
expect(described_class.new(image).extract).to eq({
|
|
24
|
+
height: 50,
|
|
25
|
+
width: 50,
|
|
26
|
+
format: "PNG",
|
|
27
|
+
channels: "srgba"
|
|
28
|
+
})
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe Riiif::VipsInfoExtractor do
|
|
4
|
+
before do
|
|
5
|
+
allow(Riiif::CommandRunner).to receive(:execute).and_return(fake_info)
|
|
6
|
+
allow(Vips::Image).to receive(:new_from_file).and_return(image)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
let(:image) { double(has_alpha?: false) }
|
|
10
|
+
|
|
11
|
+
let(:fake_info) do
|
|
12
|
+
"width: 500
|
|
13
|
+
height: 376
|
|
14
|
+
interpretation: srgb
|
|
15
|
+
filename: spec/fixtures/test.tif
|
|
16
|
+
vips-loader: tiffload"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'uses vipsheader as its external command' do
|
|
20
|
+
expect(described_class.external_command).to eq "vipsheader"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
context 'on a file without transparency' do
|
|
24
|
+
it 'returns the extracted attributes' do
|
|
25
|
+
expect(described_class.new("path/to/image.jpg").extract).to eq({
|
|
26
|
+
height: 376,
|
|
27
|
+
width: 500,
|
|
28
|
+
format: "JPEG",
|
|
29
|
+
channels: "srgb"
|
|
30
|
+
})
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
context 'on a file with transparency' do
|
|
35
|
+
let(:image) { double(has_alpha?: true) }
|
|
36
|
+
|
|
37
|
+
let(:fake_info) do
|
|
38
|
+
"width: 50
|
|
39
|
+
height: 50
|
|
40
|
+
interpretation: srgb
|
|
41
|
+
filename: spec/fixtures/test.tif
|
|
42
|
+
vips-loader: pngload"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'returns the extracted attributes' do
|
|
46
|
+
expect(described_class.new(image).extract).to eq({
|
|
47
|
+
height: 50,
|
|
48
|
+
width: 50,
|
|
49
|
+
format: "PNG",
|
|
50
|
+
channels: "srgba"
|
|
51
|
+
})
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
context 'on a JPEG file with extra EXIF metadata' do
|
|
56
|
+
let(:image) { double(has_alpha?: false) }
|
|
57
|
+
|
|
58
|
+
let(:fake_info) do
|
|
59
|
+
"width: 150
|
|
60
|
+
height: 150
|
|
61
|
+
interpretation: srgb
|
|
62
|
+
filename: spec/fixtures/test.jpeg
|
|
63
|
+
vips-loader: jpegload
|
|
64
|
+
exif-ifd0-Artist: University Library (Digital Object: Digital Media Group)"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'returns the extracted attributes' do
|
|
68
|
+
expect(described_class.new(image).extract).to eq({
|
|
69
|
+
height: 150,
|
|
70
|
+
width: 150,
|
|
71
|
+
format: "JPEG",
|
|
72
|
+
channels: "srgb"
|
|
73
|
+
})
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
RSpec.describe Riiif::File do
|
|
2
|
+
describe '#info_extractor_class' do
|
|
3
|
+
subject { described_class.info_extractor_class }
|
|
4
|
+
|
|
5
|
+
context 'when not using vips' do
|
|
6
|
+
it { is_expected.to eq Riiif::ImageMagickInfoExtractor }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
context 'when vips is configured' do
|
|
10
|
+
before { allow(Riiif).to receive(:use_vips?).and_return true }
|
|
11
|
+
|
|
12
|
+
it { is_expected.to eq Riiif::VipsInfoExtractor }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe '#transformer' do
|
|
17
|
+
subject { described_class.new('path.jp2', double).transformer }
|
|
18
|
+
|
|
19
|
+
context 'when vips is configured' do
|
|
20
|
+
before { allow(Riiif).to receive(:use_vips?).and_return true }
|
|
21
|
+
|
|
22
|
+
it { is_expected.to eq Riiif::VipsTransformer }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
context 'when Kakadu is enabled' do
|
|
26
|
+
before { allow(Riiif).to receive(:kakadu_enabled?).and_return true }
|
|
27
|
+
|
|
28
|
+
it { is_expected.to eq Riiif::KakaduTransformer }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
context 'when using image/graphicsmagick without Kakadu' do
|
|
32
|
+
it { is_expected.to eq Riiif::ImagemagickTransformer }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
require 'rails/generators'
|
|
2
2
|
|
|
3
3
|
class TestAppGenerator < Rails::Generators::Base
|
|
4
|
-
source_root 'spec/test_app_templates'
|
|
4
|
+
# source_root 'spec/test_app_templates'
|
|
5
|
+
source_root File.expand_path("../../../spec", __dir__)
|
|
5
6
|
|
|
6
7
|
def add_routes
|
|
7
8
|
route "mount Riiif::Engine => '/images', as: 'riiif'"
|
|
8
9
|
end
|
|
10
|
+
|
|
11
|
+
def copy_fixtures
|
|
12
|
+
directory 'fixtures', 'spec/fixtures'
|
|
13
|
+
end
|
|
9
14
|
end
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require 'ruby-vips'
|
|
5
|
+
rescue LoadError
|
|
6
|
+
module Vips
|
|
7
|
+
class Image
|
|
8
|
+
# Intentionally blank.
|
|
9
|
+
#
|
|
10
|
+
# This prevents uninitialized constant errors if vips
|
|
11
|
+
# is not installed.
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
RSpec.describe Riiif::VipsTransformer do
|
|
17
|
+
let(:channels) { 'rgb' }
|
|
18
|
+
|
|
19
|
+
let(:path) { "path/to/image.tif" }
|
|
20
|
+
|
|
21
|
+
let(:image) { double('Vips Image', has_alpha?: false) }
|
|
22
|
+
|
|
23
|
+
let(:image_info) do
|
|
24
|
+
double({ height: 376,
|
|
25
|
+
width: 500,
|
|
26
|
+
format: 'jpg',
|
|
27
|
+
channels: channels })
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
let(:target) { 'jpg' }
|
|
31
|
+
|
|
32
|
+
let(:transformation) do
|
|
33
|
+
IIIF::Image::Transformation.new(region: region,
|
|
34
|
+
size: size,
|
|
35
|
+
rotation: rotation,
|
|
36
|
+
format: target)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Default/Placeholder values that should be modified in tests
|
|
40
|
+
let(:size) { IIIF::Image::Size::Full.new }
|
|
41
|
+
let(:region) { IIIF::Image::Region::Full.new }
|
|
42
|
+
let(:rotation) { 0 }
|
|
43
|
+
|
|
44
|
+
before do
|
|
45
|
+
allow(Vips::Image).to receive(:new_from_file).and_return(image)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe '#initialize' do
|
|
49
|
+
let(:path) { Pathname.new("path/to/image.tif") }
|
|
50
|
+
|
|
51
|
+
it 'normalizes pathnames to strings' do
|
|
52
|
+
expect(Vips::Image).to receive(:new_from_file).with("path/to/image.tif")
|
|
53
|
+
described_class.new(path, image_info, transformation)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
describe '#transform' do
|
|
58
|
+
subject { described_class.new(path, image_info, transformation).transform }
|
|
59
|
+
before { allow(image).to receive(:write_to_buffer) }
|
|
60
|
+
after { subject }
|
|
61
|
+
|
|
62
|
+
context 'when requesting jpg format with default options' do
|
|
63
|
+
it 'writes to jpg format' do
|
|
64
|
+
expect(image).to receive(:write_to_buffer).with(".jpg[Q=85,optimize-coding,strip]")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
context 'when requesting png format with default options' do
|
|
69
|
+
let(:target) { 'png' }
|
|
70
|
+
|
|
71
|
+
it 'writes to png format' do
|
|
72
|
+
expect(image).to receive(:write_to_buffer).with(".png[Q=85,strip]")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
context 'when requesting jpeg format for a png' do
|
|
77
|
+
let(:image) { double('Vips Image', has_alpha?: true) }
|
|
78
|
+
|
|
79
|
+
it 'writes to png anyway to preserve transparency' do
|
|
80
|
+
expect(image).to receive(:write_to_buffer).with(".png[Q=85,strip]")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
context 'with subsampling turned off' do
|
|
85
|
+
subject { described_class.new(path, image_info, transformation, subsample: false).transform }
|
|
86
|
+
|
|
87
|
+
it 'does not subsample' do
|
|
88
|
+
expect(image).to receive(:write_to_buffer).with(".jpg[Q=85,optimize-coding,strip,no-subsample]")
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
context 'when specifying compression factor' do
|
|
93
|
+
subject { described_class.new(path, image_info, transformation, compression: 90).transform }
|
|
94
|
+
|
|
95
|
+
it 'compresses to the correct quality' do
|
|
96
|
+
expect(image).to receive(:write_to_buffer).with(".jpg[Q=90,optimize-coding,strip]")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
context 'when strip_metadata is false' do
|
|
101
|
+
subject { described_class.new(path, image_info, transformation, strip_metadata: false).transform }
|
|
102
|
+
|
|
103
|
+
it 'does not strip metadata' do
|
|
104
|
+
expect(image).to receive(:write_to_buffer).with(".jpg[Q=85,optimize-coding]")
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe '#transform_image' do
|
|
110
|
+
subject { described_class.new(path, image_info, transformation).send(:transform_image) }
|
|
111
|
+
|
|
112
|
+
before do
|
|
113
|
+
allow(image).to receive_messages(crop: image, resize: image, rotate: image, thumbnail_image: image, colourspace: image)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
describe 'resize' do
|
|
117
|
+
context 'when specifing full size' do
|
|
118
|
+
it 'does not resize' do
|
|
119
|
+
expect(image).not_to receive(:resize)
|
|
120
|
+
expect(image).not_to receive(:thumbnail_image)
|
|
121
|
+
subject
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
context 'when specifing percent size' do
|
|
126
|
+
let(:size) { IIIF::Image::Size::Percent.new(50) }
|
|
127
|
+
|
|
128
|
+
it 'resizes the image' do
|
|
129
|
+
expect(image).to receive(:resize).with(50.0)
|
|
130
|
+
expect(image).not_to receive(:thumbnail_image)
|
|
131
|
+
subject
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
context 'when specifing float percent size' do
|
|
136
|
+
let(:size) { IIIF::Image::Size::Percent.new(12.5) }
|
|
137
|
+
|
|
138
|
+
it 'resizes the image' do
|
|
139
|
+
expect(image).to receive(:resize).with(12.5)
|
|
140
|
+
expect(image).not_to receive(:thumbnail_image)
|
|
141
|
+
subject
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
context 'when specifying width and/or height' do
|
|
146
|
+
context 'when specifing w, size' do
|
|
147
|
+
let(:size) { IIIF::Image::Size::Width.new(300) }
|
|
148
|
+
|
|
149
|
+
before { allow(image).to receive(:width).and_return(600) }
|
|
150
|
+
|
|
151
|
+
it 'resizes the image to 300px wide, maintaining aspect ratio' do
|
|
152
|
+
expect(image).to receive(:resize).with(0.5)
|
|
153
|
+
subject
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
context 'when specifing ,h size' do
|
|
158
|
+
let(:size) { IIIF::Image::Size::Height.new(200) }
|
|
159
|
+
|
|
160
|
+
before { allow(image).to receive(:height).and_return(500) }
|
|
161
|
+
|
|
162
|
+
it 'resizes the image to 300px high, maintaining aspect ratio' do
|
|
163
|
+
expect(image).to receive(:resize).with(0.4)
|
|
164
|
+
subject
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
context 'when specifing absolute w,h size' do
|
|
169
|
+
let(:size) { IIIF::Image::Size::Absolute.new(200, 300) }
|
|
170
|
+
|
|
171
|
+
it 'resizes the image, ignoring aspect ratio' do
|
|
172
|
+
expect(image).to receive(:thumbnail_image).with(200, height: 300, size: :force)
|
|
173
|
+
subject
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
context 'when specifing bestfit (!w,h) size' do
|
|
178
|
+
let(:size) { IIIF::Image::Size::BestFit.new(200, 300) }
|
|
179
|
+
|
|
180
|
+
it 'resizes the image so that the width and height are equal or less than the requested value' do
|
|
181
|
+
expect(image).to receive(:thumbnail_image).with(200, height: 300)
|
|
182
|
+
subject
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
describe 'crop' do
|
|
189
|
+
after { subject }
|
|
190
|
+
|
|
191
|
+
context 'when specifing full size' do
|
|
192
|
+
let(:region) { IIIF::Image::Region::Full.new }
|
|
193
|
+
|
|
194
|
+
it 'does not crop' do
|
|
195
|
+
expect(image).not_to receive(:crop)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
context 'when specifing absolute geometry' do
|
|
200
|
+
let(:region) { IIIF::Image::Region::Absolute.new(80, 15, 60, 75) }
|
|
201
|
+
|
|
202
|
+
it 'crops to that region' do
|
|
203
|
+
expect(image).to receive(:crop).with(80, 15, 60, 75)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
context 'when specifing percent geometry' do
|
|
208
|
+
let(:region) { IIIF::Image::Region::Percent.new(10, 10, 80, 70) }
|
|
209
|
+
before { allow(image_info).to receive_messages(width: 100, height: 100, format: 'jpeg', channels: channels) }
|
|
210
|
+
|
|
211
|
+
it 'crops to that region' do
|
|
212
|
+
expect(image).to receive(:crop).with(10, 10, 80, 70)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
context 'when specifing square geometry' do
|
|
217
|
+
let(:region) { IIIF::Image::Region::Square.new }
|
|
218
|
+
|
|
219
|
+
it 'crops a square the size of the shortest edge' do
|
|
220
|
+
expect(image).to receive(:crop).with(62, 0, 376, 376)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
describe 'rotate' do
|
|
226
|
+
after { subject }
|
|
227
|
+
|
|
228
|
+
context 'when no rotation (0) is specified' do
|
|
229
|
+
it 'does not rotate' do
|
|
230
|
+
expect(image).not_to receive(:rotate)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
context 'when rotation is specified' do
|
|
235
|
+
let(:rotation) { 45 }
|
|
236
|
+
|
|
237
|
+
it 'rotates the image' do
|
|
238
|
+
expect(image).to receive(:rotate).with(45)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
describe 'colourspace' do
|
|
244
|
+
after { subject }
|
|
245
|
+
|
|
246
|
+
context 'when quality is default or color' do
|
|
247
|
+
it 'leaves the image in color' do
|
|
248
|
+
expect(image).not_to receive(:colourspace).with(:b_w)
|
|
249
|
+
expect(image).not_to receive(:>)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
context 'when quality is gray' do
|
|
254
|
+
let(:transformation) { IIIF::Image::Transformation.new(region: region, size: size, rotation: rotation, quality: 'gray') }
|
|
255
|
+
|
|
256
|
+
it 'makes the image grayscale' do
|
|
257
|
+
expect(image).to receive(:colourspace).with(:b_w)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
context 'when quality is bitonal' do
|
|
262
|
+
let(:transformation) { IIIF::Image::Transformation.new(region: region, size: size, rotation: rotation, quality: 'bitonal') }
|
|
263
|
+
|
|
264
|
+
it 'makes the image bitonal' do
|
|
265
|
+
expect(image).to receive(:colourspace).with(:b_w)
|
|
266
|
+
expect(image).to receive(:>).with(200)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: riiif
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.8.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Justin Coyne
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: railties
|
|
@@ -58,6 +57,20 @@ dependencies:
|
|
|
58
57
|
- - ">="
|
|
59
58
|
- !ruby/object:Gem::Version
|
|
60
59
|
version: 0.1.0
|
|
60
|
+
- !ruby/object:Gem::Dependency
|
|
61
|
+
name: ruby-vips
|
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '0'
|
|
67
|
+
type: :runtime
|
|
68
|
+
prerelease: false
|
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '0'
|
|
61
74
|
- !ruby/object:Gem::Dependency
|
|
62
75
|
name: bundler
|
|
63
76
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -163,6 +176,7 @@ executables: []
|
|
|
163
176
|
extensions: []
|
|
164
177
|
extra_rdoc_files: []
|
|
165
178
|
files:
|
|
179
|
+
- ".github/dependabot.yml"
|
|
166
180
|
- ".github/workflows/ruby.yml"
|
|
167
181
|
- ".gitignore"
|
|
168
182
|
- ".rspec"
|
|
@@ -174,6 +188,9 @@ files:
|
|
|
174
188
|
- README.md
|
|
175
189
|
- Rakefile
|
|
176
190
|
- app/controllers/riiif/images_controller.rb
|
|
191
|
+
- app/extractors/riiif/abstract_info_extractor.rb
|
|
192
|
+
- app/extractors/riiif/image_magick_info_extractor.rb
|
|
193
|
+
- app/extractors/riiif/vips_info_extractor.rb
|
|
177
194
|
- app/models/riiif/file.rb
|
|
178
195
|
- app/models/riiif/image.rb
|
|
179
196
|
- app/models/riiif/image_information.rb
|
|
@@ -183,17 +200,19 @@ files:
|
|
|
183
200
|
- app/resolvers/riiif/http_file_resolver.rb
|
|
184
201
|
- app/services/riiif/command_runner.rb
|
|
185
202
|
- app/services/riiif/crop.rb
|
|
186
|
-
- app/services/riiif/image_magick_info_extractor.rb
|
|
187
203
|
- app/services/riiif/imagemagick_command_factory.rb
|
|
188
204
|
- app/services/riiif/kakadu_command_factory.rb
|
|
189
205
|
- app/services/riiif/link_name_service.rb
|
|
190
206
|
- app/services/riiif/nil_authorization_service.rb
|
|
191
207
|
- app/services/riiif/resize.rb
|
|
208
|
+
- app/services/riiif/vips_resize.rb
|
|
192
209
|
- app/transformers/riiif/abstract_transformer.rb
|
|
193
210
|
- app/transformers/riiif/imagemagick_transformer.rb
|
|
194
211
|
- app/transformers/riiif/kakadu_transformer.rb
|
|
212
|
+
- app/transformers/riiif/vips_transformer.rb
|
|
195
213
|
- config/routes.rb
|
|
196
214
|
- docs/benchmark.md
|
|
215
|
+
- docs/vips_comparison.md
|
|
197
216
|
- lib/riiif.rb
|
|
198
217
|
- lib/riiif/engine.rb
|
|
199
218
|
- lib/riiif/rails/routes.rb
|
|
@@ -201,7 +220,13 @@ files:
|
|
|
201
220
|
- lib/riiif/version.rb
|
|
202
221
|
- riiif.gemspec
|
|
203
222
|
- spec/controllers/riiif/images_controller_spec.rb
|
|
223
|
+
- spec/extractors/riiif/image_magick_info_extractor_spec.rb
|
|
224
|
+
- spec/extractors/riiif/vips_info_extractor_spec.rb
|
|
225
|
+
- spec/fixtures/test.jpg
|
|
226
|
+
- spec/fixtures/test.png
|
|
227
|
+
- spec/fixtures/test.tif
|
|
204
228
|
- spec/models/riiif/akubra_system_file_resolver_spec.rb
|
|
229
|
+
- spec/models/riiif/file_spec.rb
|
|
205
230
|
- spec/models/riiif/file_system_file_resolver_spec.rb
|
|
206
231
|
- spec/models/riiif/http_file_resolver_spec.rb
|
|
207
232
|
- spec/models/riiif/image_information_spec.rb
|
|
@@ -214,11 +239,11 @@ files:
|
|
|
214
239
|
- spec/test_app_templates/Gemfile.extra
|
|
215
240
|
- spec/test_app_templates/lib/generators/test_app_generator.rb
|
|
216
241
|
- spec/transformers/riiif/kakadu_transformer_spec.rb
|
|
242
|
+
- spec/transformers/riiif/vips_transformer_spec.rb
|
|
217
243
|
homepage: https://github.com/sul-dlss/riiif
|
|
218
244
|
licenses:
|
|
219
245
|
- APACHE2
|
|
220
246
|
metadata: {}
|
|
221
|
-
post_install_message:
|
|
222
247
|
rdoc_options: []
|
|
223
248
|
require_paths:
|
|
224
249
|
- lib
|
|
@@ -233,13 +258,18 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
233
258
|
- !ruby/object:Gem::Version
|
|
234
259
|
version: '0'
|
|
235
260
|
requirements: []
|
|
236
|
-
rubygems_version:
|
|
237
|
-
signing_key:
|
|
261
|
+
rubygems_version: 4.0.7
|
|
238
262
|
specification_version: 4
|
|
239
263
|
summary: A Rails engine that support IIIF requests
|
|
240
264
|
test_files:
|
|
241
265
|
- spec/controllers/riiif/images_controller_spec.rb
|
|
266
|
+
- spec/extractors/riiif/image_magick_info_extractor_spec.rb
|
|
267
|
+
- spec/extractors/riiif/vips_info_extractor_spec.rb
|
|
268
|
+
- spec/fixtures/test.jpg
|
|
269
|
+
- spec/fixtures/test.png
|
|
270
|
+
- spec/fixtures/test.tif
|
|
242
271
|
- spec/models/riiif/akubra_system_file_resolver_spec.rb
|
|
272
|
+
- spec/models/riiif/file_spec.rb
|
|
243
273
|
- spec/models/riiif/file_system_file_resolver_spec.rb
|
|
244
274
|
- spec/models/riiif/http_file_resolver_spec.rb
|
|
245
275
|
- spec/models/riiif/image_information_spec.rb
|
|
@@ -252,3 +282,4 @@ test_files:
|
|
|
252
282
|
- spec/test_app_templates/Gemfile.extra
|
|
253
283
|
- spec/test_app_templates/lib/generators/test_app_generator.rb
|
|
254
284
|
- spec/transformers/riiif/kakadu_transformer_spec.rb
|
|
285
|
+
- spec/transformers/riiif/vips_transformer_spec.rb
|