riiif 2.1.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 +39 -0
- data/.rubocop_todo.yml +115 -16
- data/Gemfile +2 -12
- data/README.md +29 -9
- data/app/controllers/riiif/images_controller.rb +1 -1
- data/app/extractors/riiif/abstract_info_extractor.rb +12 -0
- data/app/{services → extractors}/riiif/image_magick_info_extractor.rb +2 -7
- data/app/extractors/riiif/vips_info_extractor.rb +22 -0
- data/app/models/riiif/file.rb +11 -24
- data/app/models/riiif/image.rb +0 -3
- data/app/resolvers/riiif/abstract_file_system_resolver.rb +1 -11
- data/app/resolvers/riiif/akubra_system_file_resolver.rb +1 -0
- data/app/resolvers/riiif/file_system_file_resolver.rb +1 -1
- data/app/resolvers/riiif/http_file_resolver.rb +11 -3
- data/app/services/riiif/crop.rb +40 -0
- data/app/services/riiif/imagemagick_command_factory.rb +1 -1
- 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 +14 -0
- data/lib/riiif/version.rb +1 -1
- data/lib/riiif.rb +14 -0
- data/riiif.gemspec +9 -7
- data/spec/controllers/riiif/images_controller_spec.rb +8 -8
- 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/models/riiif/file_system_file_resolver_spec.rb +7 -0
- data/spec/models/riiif/http_file_resolver_spec.rb +1 -1
- data/spec/models/riiif/image_spec.rb +27 -21
- data/spec/spec_helper.rb +0 -6
- data/spec/test_app_templates/lib/generators/test_app_generator.rb +6 -1
- data/spec/transformers/riiif/kakadu_transformer_spec.rb +8 -8
- data/spec/transformers/riiif/vips_transformer_spec.rb +271 -0
- metadata +49 -19
- data/.travis.yml +0 -11
|
@@ -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
|
@@ -10,5 +10,19 @@ module Riiif
|
|
|
10
10
|
|
|
11
11
|
# Set to true to use kdu for jp2000 source images
|
|
12
12
|
config.kakadu_enabled = false
|
|
13
|
+
|
|
14
|
+
# Set to true to use libvips to transform images
|
|
15
|
+
# https://www.libvips.org/
|
|
16
|
+
config.use_vips = false
|
|
17
|
+
|
|
18
|
+
config.before_configuration do
|
|
19
|
+
# see https://github.com/fxn/zeitwerk#for_gem
|
|
20
|
+
# We put a generator into LOCAL APP lib/generators, so tell
|
|
21
|
+
# zeitwerk to ignore the whole directory? If we're using a recent
|
|
22
|
+
# enough version of Rails to have zeitwerk config
|
|
23
|
+
#
|
|
24
|
+
# See: https://github.com/cbeer/engine_cart/issues/117
|
|
25
|
+
Rails.autoloaders.main.ignore(Rails.root.join('lib', 'generators')) if Rails.try(:autoloaders).try(:main).respond_to?(:ignore)
|
|
26
|
+
end
|
|
13
27
|
end
|
|
14
28
|
end
|
data/lib/riiif/version.rb
CHANGED
data/lib/riiif.rb
CHANGED
|
@@ -12,10 +12,24 @@ module Riiif
|
|
|
12
12
|
# This error is raised when Riiif can't convert an image
|
|
13
13
|
class ConversionError < Error; end
|
|
14
14
|
|
|
15
|
+
def self.deprecation
|
|
16
|
+
@deprecation ||= ActiveSupport::Deprecation.new('3.0', 'Riiif')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
HTTPFileResolver = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
|
|
20
|
+
'Riiif::HTTPFileResolver',
|
|
21
|
+
'Riiif::HttpFileResolver',
|
|
22
|
+
Riiif.deprecation
|
|
23
|
+
)
|
|
24
|
+
|
|
15
25
|
mattr_accessor :not_found_image # the image to use when a lookup fails
|
|
16
26
|
mattr_accessor :unauthorized_image # the image to use when a user doesn't have access
|
|
17
27
|
|
|
18
28
|
def self.kakadu_enabled?
|
|
19
29
|
Engine.config.kakadu_enabled
|
|
20
30
|
end
|
|
31
|
+
|
|
32
|
+
def self.use_vips?
|
|
33
|
+
Engine.config.use_vips
|
|
34
|
+
end
|
|
21
35
|
end
|
data/riiif.gemspec
CHANGED
|
@@ -7,10 +7,10 @@ Gem::Specification.new do |spec|
|
|
|
7
7
|
spec.name = 'riiif'
|
|
8
8
|
spec.version = Riiif::VERSION
|
|
9
9
|
spec.authors = ['Justin Coyne']
|
|
10
|
-
spec.email = ['
|
|
10
|
+
spec.email = ['jcoyne85@stanford.edu']
|
|
11
11
|
spec.description = 'A IIIF image server'
|
|
12
|
-
spec.summary = 'A
|
|
13
|
-
spec.homepage = 'https://github.com/
|
|
12
|
+
spec.summary = 'A Rails engine that support IIIF requests'
|
|
13
|
+
spec.homepage = 'https://github.com/sul-dlss/riiif'
|
|
14
14
|
spec.license = 'APACHE2'
|
|
15
15
|
|
|
16
16
|
spec.files = `git ls-files|grep -v spec/samples`.split($INPUT_RECORD_SEPARATOR)
|
|
@@ -18,14 +18,16 @@ Gem::Specification.new do |spec|
|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
19
19
|
spec.require_paths = ['lib']
|
|
20
20
|
|
|
21
|
-
spec.add_dependency 'railties', '>= 4.2', '<
|
|
21
|
+
spec.add_dependency 'railties', '>= 4.2', '< 9'
|
|
22
22
|
spec.add_dependency 'deprecation', '>= 1.0.0'
|
|
23
|
-
spec.add_dependency 'iiif-image-api', '
|
|
23
|
+
spec.add_dependency 'iiif-image-api', '>= 0.1.0'
|
|
24
|
+
spec.add_dependency 'ruby-vips'
|
|
25
|
+
|
|
24
26
|
spec.add_development_dependency 'bundler'
|
|
25
27
|
spec.add_development_dependency 'rake'
|
|
26
28
|
spec.add_development_dependency 'engine_cart', '~> 2.0'
|
|
27
29
|
spec.add_development_dependency 'rspec-rails'
|
|
28
30
|
spec.add_development_dependency 'sqlite3'
|
|
29
|
-
spec.add_development_dependency 'bixby', '~>
|
|
30
|
-
spec.add_development_dependency '
|
|
31
|
+
spec.add_development_dependency 'bixby', '~> 5.0'
|
|
32
|
+
spec.add_development_dependency 'simplecov'
|
|
31
33
|
end
|
|
@@ -24,9 +24,9 @@ RSpec.describe Riiif::ImagesController do
|
|
|
24
24
|
it 'sends images to the service' do
|
|
25
25
|
image = double
|
|
26
26
|
expect(Riiif::Image).to receive(:new).with('abcd1234').and_return(image)
|
|
27
|
-
expect(image).to receive(:render).with('region' => 'full', 'size' => 'full',
|
|
27
|
+
expect(image).to receive(:render).with({ 'region' => 'full', 'size' => 'full',
|
|
28
28
|
'rotation' => '0', 'quality' => 'default',
|
|
29
|
-
'format' => 'jpg').and_return('IMAGEDATA')
|
|
29
|
+
'format' => 'jpg' }).and_return('IMAGEDATA')
|
|
30
30
|
get :show, params: { id: 'abcd1234', action: 'show', region: 'full', size: 'full',
|
|
31
31
|
rotation: '0', quality: 'default', format: 'jpg' }
|
|
32
32
|
expect(response).to be_successful
|
|
@@ -102,9 +102,9 @@ RSpec.describe Riiif::ImagesController do
|
|
|
102
102
|
raise Riiif::ImageNotFoundError unless file.present?
|
|
103
103
|
not_found_image
|
|
104
104
|
end.twice
|
|
105
|
-
expect(not_found_image).to receive(:render).with('region' => 'full', 'size' => 'full',
|
|
105
|
+
expect(not_found_image).to receive(:render).with({ 'region' => 'full', 'size' => 'full',
|
|
106
106
|
'rotation' => '0', 'quality' => 'default',
|
|
107
|
-
'format' => 'jpg').and_return('default-image-data')
|
|
107
|
+
'format' => 'jpg' }).and_return('default-image-data')
|
|
108
108
|
|
|
109
109
|
get :show, params: { id: 'bad_id', action: 'show', region: 'full', size: 'full',
|
|
110
110
|
rotation: '0', quality: 'default', format: 'jpg' }
|
|
@@ -118,9 +118,9 @@ RSpec.describe Riiif::ImagesController do
|
|
|
118
118
|
raise Riiif::ImageNotFoundError unless file.present?
|
|
119
119
|
not_found_image
|
|
120
120
|
end.twice
|
|
121
|
-
expect(not_found_image).to receive(:render).with('region' => 'full', 'size' => 'full',
|
|
121
|
+
expect(not_found_image).to receive(:render).with({ 'region' => 'full', 'size' => 'full',
|
|
122
122
|
'rotation' => '0', 'quality' => 'default',
|
|
123
|
-
'format' => 'jpg').and_return('default-image-data')
|
|
123
|
+
'format' => 'jpg' }).and_return('default-image-data')
|
|
124
124
|
|
|
125
125
|
get :show, params: { id: 'bad_id', action: 'show', region: 'full', size: 'full',
|
|
126
126
|
rotation: '0', quality: 'default', format: 'jpg' }
|
|
@@ -155,12 +155,12 @@ RSpec.describe Riiif::ImagesController do
|
|
|
155
155
|
get :info, params: { id: 'abcd1234', format: 'json' }
|
|
156
156
|
expect(response).to be_successful
|
|
157
157
|
expect(json).to eq '@context' => 'http://iiif.io/api/image/2/context.json',
|
|
158
|
-
'@id' => 'http://test.host/abcd1234',
|
|
158
|
+
'@id' => Rails.version > '6.1' ? 'http://test.host/images/abcd1234' : 'http://test.host/abcd1234',
|
|
159
159
|
'width' => 6000,
|
|
160
160
|
'height' => 4000,
|
|
161
161
|
'format' => 'JPEG',
|
|
162
162
|
'channels' => 'rgb',
|
|
163
|
-
'profile' => ['http://iiif.io/api/image/2/level1.json', 'formats' => %w(jpg png)],
|
|
163
|
+
'profile' => ['http://iiif.io/api/image/2/level1.json', 'formats' => %w(webp jpg png)],
|
|
164
164
|
'protocol' => 'http://iiif.io/api/image'
|
|
165
165
|
expect(response.headers['Link']).to eq '<http://iiif.io/api/image/2/level1.json>;rel="profile"'
|
|
166
166
|
expect(response.headers['Content-Type']).to eq 'application/ld+json; charset=utf-8'
|
|
@@ -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
|