riiif 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea4afe42b1178e584376a461d2a093dce410e4e7d6ffe7b77b03a65c2e90c787
4
- data.tar.gz: 74cff5d88b9cb6e0bb5077b67e81cf104b04c400419728090dcb423051b7382d
3
+ metadata.gz: 5ac6417bc393f31ac0789724dda5bfbacb41223dc22dc9b1f0979d18b66ec9fe
4
+ data.tar.gz: c33036fd69be61f884277e37a907e51817c35a1a7a213fd9808ad1207f67d6a6
5
5
  SHA512:
6
- metadata.gz: e3b19485e9b29e6383995a27540acd3356981e49ee2c62e27c069de234e87cd075b685c98818d947a9e03c073478ff55b4e2d49f8c377b9ea9bf9f563def5daf
7
- data.tar.gz: a7f044747b0d275e60e7ef3aac3184d7ab033e88c02b1b0bbf7337e212c428d9c985ed604ab1ef95e8b0deb3101dd608ec9b08a66fad70c99531caac5a2a1879
6
+ metadata.gz: af382985fd204ea8ed65a499f2c70f1e47e861bff2dfeb60ba292ccd34544e44ce7e1d222edd2d40cc1fbf53e9cea7dc3fccd11350b54d3746627ca68179f876
7
+ data.tar.gz: 80e1d79683e44cd0fd177ea8a444981643f0caae86dc41df24430fc04d0a8b82146f97cbfd0e5456084b73a55099e2d80040d96288aecffb2d3dbcd9a5f356bd
data/.rubocop_todo.yml CHANGED
@@ -69,7 +69,13 @@ Lint/SuppressedException:
69
69
  Metrics/BlockLength:
70
70
  Max: 237
71
71
 
72
- # Offense count: 1
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/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 depends on Imagemagick so you must install that first. On a mac using Homebrew you can follow these instructions:
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
- Riiif::ImagemagickCommandFactory.external_command = "gm convert"
79
- Riiif::ImageMagickInfoExtractor.external_command = "gm identify"
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
 
@@ -0,0 +1,12 @@
1
+ module Riiif
2
+ class AbstractInfoExtractor
3
+ # A basic image info extractor class from which ImageMagickInfoExtractor and
4
+ # VipsInfoExtractor inherit
5
+
6
+ class_attribute :external_command
7
+
8
+ def initialize(path)
9
+ @path = path
10
+ end
11
+ end
12
+ end
@@ -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(': ') }.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
@@ -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
- self.info_extractor_class = ImageMagickInfoExtractor
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.kakadu_enabled? && path.ends_with?('.jp2')
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Riiif
2
- VERSION = '2.7.0'.freeze
2
+ VERSION = '2.8.0'.freeze
3
3
  end
data/lib/riiif.rb CHANGED
@@ -28,4 +28,8 @@ module Riiif
28
28
  def self.kakadu_enabled?
29
29
  Engine.config.kakadu_enabled
30
30
  end
31
+
32
+ def self.use_vips?
33
+ Engine.config.use_vips
34
+ end
31
35
  end
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,54 @@
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
+ 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.7.0
4
+ version: 2.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Coyne
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-12-10 00:00:00.000000000 Z
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
@@ -174,6 +187,9 @@ files:
174
187
  - README.md
175
188
  - Rakefile
176
189
  - app/controllers/riiif/images_controller.rb
190
+ - app/extractors/riiif/abstract_info_extractor.rb
191
+ - app/extractors/riiif/image_magick_info_extractor.rb
192
+ - app/extractors/riiif/vips_info_extractor.rb
177
193
  - app/models/riiif/file.rb
178
194
  - app/models/riiif/image.rb
179
195
  - app/models/riiif/image_information.rb
@@ -183,17 +199,19 @@ files:
183
199
  - app/resolvers/riiif/http_file_resolver.rb
184
200
  - app/services/riiif/command_runner.rb
185
201
  - app/services/riiif/crop.rb
186
- - app/services/riiif/image_magick_info_extractor.rb
187
202
  - app/services/riiif/imagemagick_command_factory.rb
188
203
  - app/services/riiif/kakadu_command_factory.rb
189
204
  - app/services/riiif/link_name_service.rb
190
205
  - app/services/riiif/nil_authorization_service.rb
191
206
  - app/services/riiif/resize.rb
207
+ - app/services/riiif/vips_resize.rb
192
208
  - app/transformers/riiif/abstract_transformer.rb
193
209
  - app/transformers/riiif/imagemagick_transformer.rb
194
210
  - app/transformers/riiif/kakadu_transformer.rb
211
+ - app/transformers/riiif/vips_transformer.rb
195
212
  - config/routes.rb
196
213
  - docs/benchmark.md
214
+ - docs/vips_comparison.md
197
215
  - lib/riiif.rb
198
216
  - lib/riiif/engine.rb
199
217
  - lib/riiif/rails/routes.rb
@@ -201,7 +219,13 @@ files:
201
219
  - lib/riiif/version.rb
202
220
  - riiif.gemspec
203
221
  - spec/controllers/riiif/images_controller_spec.rb
222
+ - spec/extractors/riiif/image_magick_info_extractor_spec.rb
223
+ - spec/extractors/riiif/vips_info_extractor_spec.rb
224
+ - spec/fixtures/test.jpg
225
+ - spec/fixtures/test.png
226
+ - spec/fixtures/test.tif
204
227
  - spec/models/riiif/akubra_system_file_resolver_spec.rb
228
+ - spec/models/riiif/file_spec.rb
205
229
  - spec/models/riiif/file_system_file_resolver_spec.rb
206
230
  - spec/models/riiif/http_file_resolver_spec.rb
207
231
  - spec/models/riiif/image_information_spec.rb
@@ -214,11 +238,11 @@ files:
214
238
  - spec/test_app_templates/Gemfile.extra
215
239
  - spec/test_app_templates/lib/generators/test_app_generator.rb
216
240
  - spec/transformers/riiif/kakadu_transformer_spec.rb
241
+ - spec/transformers/riiif/vips_transformer_spec.rb
217
242
  homepage: https://github.com/sul-dlss/riiif
218
243
  licenses:
219
244
  - APACHE2
220
245
  metadata: {}
221
- post_install_message:
222
246
  rdoc_options: []
223
247
  require_paths:
224
248
  - lib
@@ -233,13 +257,18 @@ required_rubygems_version: !ruby/object:Gem::Requirement
233
257
  - !ruby/object:Gem::Version
234
258
  version: '0'
235
259
  requirements: []
236
- rubygems_version: 3.5.23
237
- signing_key:
260
+ rubygems_version: 3.6.9
238
261
  specification_version: 4
239
262
  summary: A Rails engine that support IIIF requests
240
263
  test_files:
241
264
  - spec/controllers/riiif/images_controller_spec.rb
265
+ - spec/extractors/riiif/image_magick_info_extractor_spec.rb
266
+ - spec/extractors/riiif/vips_info_extractor_spec.rb
267
+ - spec/fixtures/test.jpg
268
+ - spec/fixtures/test.png
269
+ - spec/fixtures/test.tif
242
270
  - spec/models/riiif/akubra_system_file_resolver_spec.rb
271
+ - spec/models/riiif/file_spec.rb
243
272
  - spec/models/riiif/file_system_file_resolver_spec.rb
244
273
  - spec/models/riiif/http_file_resolver_spec.rb
245
274
  - spec/models/riiif/image_information_spec.rb
@@ -252,3 +281,4 @@ test_files:
252
281
  - spec/test_app_templates/Gemfile.extra
253
282
  - spec/test_app_templates/lib/generators/test_app_generator.rb
254
283
  - spec/transformers/riiif/kakadu_transformer_spec.rb
284
+ - spec/transformers/riiif/vips_transformer_spec.rb