diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 97bcc23..5f68262 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -43,6 +43,10 @@ Metrics/LineLength: Metrics/MethodLength: Max: 18 +Metrics/ParameterLists: + Exclude: + - 'app/services/riiif/imagemagick_command_factory.rb' + # Offense count: 1 # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: nested, compact diff --git a/Gemfile b/Gemfile index 0c4cc1f..3d18d76 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,5 @@ source 'https://rubygems.org' +gem 'byebug' # Specify your gem's dependencies in riiif.gemspec gemspec diff --git a/app/models/riiif/file.rb b/app/models/riiif/file.rb index 2fa1699..3c9879f 100644 --- a/app/models/riiif/file.rb +++ b/app/models/riiif/file.rb @@ -3,6 +3,7 @@ class File attr_reader :path class_attribute :info_extractor_class + # TODO: add alternative that uses kdu_jp2info self.info_extractor_class = ImageMagickInfoExtractor # @param input_path [String] The location of an image file @@ -20,6 +21,8 @@ def self.read(stream, ext) end deprecation_deprecate read: 'Riiif::File.read is deprecated and will be removed in version 2.0' + # Yields a tempfile to the provided block + # @return [Riiif::File] a file backed by the Tempfile def self.create(ext = nil, _validate = true, &block) tempfile = Tempfile.new(['mini_magick', ext.to_s.downcase]) tempfile.binmode @@ -32,22 +35,26 @@ def self.create(ext = nil, _validate = true, &block) deprecation_deprecate create: 'Riiif::File.create is deprecated and will be removed in version 2.0' # @param [Transformation] transformation - def extract(transformation) - command = command_factory.build(path, transformation) - execute(command) + # @param [ImageInformation] image_info + # @return [String] the processed image data + def extract(transformation, image_info = info) + transformer.transform(path, image_info, transformation) end - def info - @info ||= info_extractor_class.new(path).extract + def transformer + if Riiif.kakadu_enabled? && path.ends_with?('.jp2') + KakaduTransformer + else + ImagemagickTransformer + end end - delegate :execute, to: Riiif::CommandRunner - private :execute - - private + def info + @info ||= info_extractor.extract + end - def command_factory - ImagemagickCommandFactory - end + def info_extractor + @info_extractor ||= info_extractor_class.new(path) + end end end diff --git a/app/models/riiif/image.rb b/app/models/riiif/image.rb index 52107fe..3216327 100644 --- a/app/models/riiif/image.rb +++ b/app/models/riiif/image.rb @@ -4,17 +4,17 @@ # These explict requires are needed because in some contexts the Rails # autoloader can either: unload already loaded classes, or cause a lock while # trying to load a needed class. -require_dependency 'riiif/region/imagemagick/absolute_decoder' -require_dependency 'riiif/region/imagemagick/full_decoder' -require_dependency 'riiif/region/imagemagick/percentage_decoder' -require_dependency 'riiif/region/imagemagick/square_decoder' +require_dependency 'riiif/region/absolute' +require_dependency 'riiif/region/full' +require_dependency 'riiif/region/percentage' +require_dependency 'riiif/region/square' -require_dependency 'riiif/size/imagemagick/absolute_decoder' -require_dependency 'riiif/size/imagemagick/best_fit_decoder' -require_dependency 'riiif/size/imagemagick/full_decoder' -require_dependency 'riiif/size/imagemagick/height_decoder' -require_dependency 'riiif/size/imagemagick/percent_decoder' -require_dependency 'riiif/size/imagemagick/width_decoder' +require_dependency 'riiif/size/absolute' +require_dependency 'riiif/size/best_fit' +require_dependency 'riiif/size/full' +require_dependency 'riiif/size/height' +require_dependency 'riiif/size/percent' +require_dependency 'riiif/size/width' module Riiif class Image @@ -54,12 +54,13 @@ def file ## # @param [ActiveSupport::HashWithIndifferentAccess] args + # @return [String] the image data def render(args) cache_opts = args.select { |a| %w(region size quality rotation format).include? a.to_s } key = Image.cache_key(id, cache_opts) cache.fetch(key, compress: true, expires_in: Image.expires_in) do - file.extract(OptionDecoder.decode(args, info)) + file.extract(OptionDecoder.decode(args, info), info) end end diff --git a/app/models/riiif/image_information.rb b/app/models/riiif/image_information.rb index 1a948cc..2d38e08 100644 --- a/app/models/riiif/image_information.rb +++ b/app/models/riiif/image_information.rb @@ -14,6 +14,10 @@ def to_h { width: width, height: height } end + def aspect_ratio + width.to_f / height + end + def [](key) to_h[key] end diff --git a/app/models/riiif/transformation.rb b/app/models/riiif/transformation.rb new file mode 100644 index 0000000..a516378 --- /dev/null +++ b/app/models/riiif/transformation.rb @@ -0,0 +1,35 @@ +module Riiif + # Represents a IIIF request + class Transformation + attr_reader :crop, :size, :quality, :rotation, :format + def initialize(crop, size, quality, rotation, format) + @crop = crop + @size = size + @quality = quality + @rotation = rotation + @format = format + end + + # Create a clone of this Transformation, scaled by the factor + # @param [Integer] factor the scale for the new transformation + # @return [Transformation] a new transformation, scaled by factor + def reduce(factor) + Transformation.new(crop.dup, + size.reduce(factor), + quality, + rotation, + format) + end + + # Create a clone of this Transformation, without the crop + # @return [Transformation] a new transformation + # TODO: it would be nice if we didn't need image_info + def without_crop(image_info) + Transformation.new(Region::Full.new(image_info), + size.dup, + quality, + rotation, + format) + end + end +end diff --git a/app/services/riiif/command_runner.rb b/app/services/riiif/command_runner.rb index ea89f14..37e895f 100644 --- a/app/services/riiif/command_runner.rb +++ b/app/services/riiif/command_runner.rb @@ -6,6 +6,8 @@ class CommandRunner include ActiveSupport::Benchmarkable delegate :logger, to: :Rails + # TODO: this is being loaded into memory. We could make this a stream. + # @return [String] all the image data def execute(command) out = nil benchmark("Riiif executed #{command}") do diff --git a/app/services/riiif/crop.rb b/app/services/riiif/crop.rb new file mode 100644 index 0000000..214b543 --- /dev/null +++ b/app/services/riiif/crop.rb @@ -0,0 +1,54 @@ +module Riiif + # Represents a cropping operation + class Crop + attr_reader :image_info + + # @return [String] a region for imagemagick to decode + # (appropriate for passing to the -crop parameter) + def to_imagemagick + "#{width}x#{height}+#{offset_x}+#{offset_y}" + end + + # @return [String] a region for kakadu to decode + # (appropriate for passing to the -region parameter) + def to_kakadu + "\{#{decimal_offset_y},#{decimal_offset_x}\},\{#{decimal_height},#{decimal_width}\}" + end + + attr_reader :offset_x + + attr_reader :offset_y + + # @return [Integer] the height in pixels + def height + image_info.height + end + + # @return [Integer] the width in pixels + def width + image_info.width + end + + # @return [Float] the fractional height with respect to the original size + def decimal_height(n = height) + n.to_f / image_info.height + end + + # @return [Float] the fractional width with respect to the original size + def decimal_width(n = width) + n.to_f / image_info.width + end + + def decimal_offset_x + offset_x.to_f / image_info.width + end + + def decimal_offset_y + offset_y.to_f / image_info.height + end + + def maintain_aspect_ratio? + (height / width) == (image_info.height / image_info.width) + end + end +end diff --git a/app/services/riiif/imagemagick_command_factory.rb b/app/services/riiif/imagemagick_command_factory.rb index 722cbe1..8a98ba9 100644 --- a/app/services/riiif/imagemagick_command_factory.rb +++ b/app/services/riiif/imagemagick_command_factory.rb @@ -9,35 +9,29 @@ class ImagemagickCommandFactory # A helper method to instantiate and invoke build # @param [String] path the location of the file + # @param info [ImageInformation] information about the source # @param [Transformation] transformation # @param [Integer] compression (85) the compression level to use (set 0 for no compression) # @param [String] sampling_factor ("4:2:0") the chroma sample factor (set 0 for no compression) # @param [Boolean] strip_metadata (true) do we want to strip EXIF tags? - # @return [String] a command for running imagemagick to produce the requested output - def self.build(path, transformation, compression: 85, sampling_factor: '4:2:0', strip_metadata: true) - new(path, transformation, - compression: compression, - sampling_factor: sampling_factor, - strip_metadata: strip_metadata).build - end - - # A helper method to instantiate and invoke build - # @param [String] path the location of the file - # @param [Transformation] transformation - # @param [Integer] compression the compression level to use (set 0 for no compression) - def initialize(path, transformation, compression:, sampling_factor:, strip_metadata:) + def initialize(path, info, transformation, compression: 85, sampling_factor: '4:2:0', strip_metadata: true) @path = path + @info = info @transformation = transformation @compression = compression @sampling_factor = sampling_factor @strip_metadata = strip_metadata end - attr_reader :path, :transformation, :compression, :sampling_factor, :strip_metadata + attr_reader :path, :info, :transformation, :compression, :sampling_factor, :strip_metadata # @return [String] a command for running imagemagick to produce the requested output - def build - [external_command, crop, size, rotation, colorspace, quality, sampling, metadata, output].join + def command + [external_command, crop, size, rotation, colorspace, quality, sampling, metadata, input, output].join + end + + def reduction_factor + nil end private @@ -50,16 +44,23 @@ def jpeg? transformation.format == 'jpg'.freeze end + def input + " #{path}" + end + + # pipe the output to STDOUT def output - " #{path} #{transformation.format}:-" + " #{transformation.format}:-" end def crop - " -crop #{transformation.crop}" if transformation.crop + directive = transformation.crop.to_imagemagick + " -crop #{directive}" if directive end def size - " -resize #{transformation.size}" if transformation.size + directive = transformation.size.to_imagemagick + " -resize #{directive}" if directive end def rotation diff --git a/app/services/riiif/imagemagick_transformer.rb b/app/services/riiif/imagemagick_transformer.rb new file mode 100644 index 0000000..5bebe8c --- /dev/null +++ b/app/services/riiif/imagemagick_transformer.rb @@ -0,0 +1,8 @@ +module Riiif + # Transforms an image using Imagemagick + class ImagemagickTransformer < AbstractTransformer + def command_factory + ImagemagickCommandFactory + end + end +end diff --git a/app/services/riiif/kakadu_command_factory.rb b/app/services/riiif/kakadu_command_factory.rb new file mode 100644 index 0000000..55cdee9 --- /dev/null +++ b/app/services/riiif/kakadu_command_factory.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Riiif + # Builds a command to run a transformation using Kakadu + class KakaduCommandFactory + class_attribute :external_command + self.external_command = 'kdu_expand' + + # A helper method to instantiate and invoke build + # @param [String] path the location of the file + # @param info [ImageInformation] information about the source + # @param [Transformation] transformation + def initialize(path, info, transformation) + @path = path + @info = info + @transformation = transformation + end + + attr_reader :path, :info, :transformation + + # @param tmp_file [String] the path to the temporary file + # @return [String] a command for running kdu_expand to produce the requested output + def command(tmp_file) + [external_command, quiet, input, threads, region, reduce, output(tmp_file)].join + end + + def reduction_factor + @reduction_factor ||= transformation.size.reduction_factor + end + + private + + def input + " -i #{path}" + end + + def output(output_filename) + " -o #{output_filename}" + end + + def threads + ' -num_threads 4' + end + + def quiet + ' -quiet' + end + + def region + region_arg = transformation.crop.to_kakadu + " -region \"#{region_arg}\"" if region_arg + end + + # kdu_expand is not capable of arbitrary scaling, but it does + # offer a -reduce argument which is capable of downscaling by + # factors of 2, significantly speeding decompression. We can + # use it if either the percent is <=50, or the height/width + # are <=50% of full size. + def reduce + " -reduce #{reduction_factor}" if reduction_factor && reduction_factor != 0 + end + end +end diff --git a/app/services/riiif/link_name_service.rb b/app/services/riiif/link_name_service.rb new file mode 100644 index 0000000..3787275 --- /dev/null +++ b/app/services/riiif/link_name_service.rb @@ -0,0 +1,8 @@ +module Riiif + # Creates names for a temporary file + class LinkNameService + def self.create + ::File.join(Dir.tmpdir, SecureRandom.uuid) + '.bmp' + end + end +end diff --git a/app/services/riiif/option_decoder.rb b/app/services/riiif/option_decoder.rb index 63f475a..e6a92b5 100644 --- a/app/services/riiif/option_decoder.rb +++ b/app/services/riiif/option_decoder.rb @@ -52,14 +52,14 @@ def validate_format!(format) def decode_region(region) if region.nil? || region == 'full' - Riiif::Region::Imagemagick::FullDecoder.new.decode + Riiif::Region::Full.new(image_info) elsif md = /^pct:(\d+),(\d+),(\d+),(\d+)$/.match(region) - Riiif::Region::Imagemagick::PercentageDecoder - .new(image_info, md[1], md[2], md[3], md[4]).decode + Riiif::Region::Percentage + .new(image_info, md[1], md[2], md[3], md[4]) elsif md = /^(\d+),(\d+),(\d+),(\d+)$/.match(region) - Riiif::Region::Imagemagick::AbsoluteDecoder.new(md[1], md[2], md[3], md[4]).decode + Riiif::Region::Absolute.new(image_info, md[1], md[2], md[3], md[4]) elsif region == 'square' - Riiif::Region::Imagemagick::SquareDecoder.new(image_info).decode + Riiif::Region::Square.new(image_info) else raise InvalidAttributeError, "Invalid region: #{region}" end @@ -68,17 +68,17 @@ def decode_region(region) # rubocop:disable Metrics/PerceivedComplexity def decode_size(size) if size.nil? || size == 'full' - Riiif::Size::Imagemagick::FullDecoder.new.decode + Riiif::Size::Full.new elsif md = /^,(\d+)$/.match(size) - Riiif::Size::Imagemagick::HeightDecoder.new(md[1]).decode + Riiif::Size::Height.new(image_info, md[1]) elsif md = /^(\d+),$/.match(size) - Riiif::Size::Imagemagick::WidthDecoder.new(md[1]).decode + Riiif::Size::Width.new(image_info, md[1]) elsif md = /^pct:(\d+(.\d+)?)$/.match(size) - Riiif::Size::Imagemagick::PercentDecoder.new(md[1]).decode + Riiif::Size::Percent.new(image_info, md[1]) elsif md = /^(\d+),(\d+)$/.match(size) - Riiif::Size::Imagemagick::AbsoluteDecoder.new(md[1], md[2]).decode + Riiif::Size::Absolute.new(image_info, md[1], md[2]) elsif md = /^!(\d+),(\d+)$/.match(size) - Riiif::Size::Imagemagick::BestFitDecoder.new(md[1], md[2]).decode + Riiif::Size::BestFit.new(image_info, md[1], md[2]) else raise InvalidAttributeError, "Invalid size: #{size}" end diff --git a/app/services/riiif/region/absolute.rb b/app/services/riiif/region/absolute.rb new file mode 100644 index 0000000..dc49cc8 --- /dev/null +++ b/app/services/riiif/region/absolute.rb @@ -0,0 +1,23 @@ +module Riiif + module Region + # Represents an absolute specified region + class Absolute < Crop + # TODO: only kakadu needs image_info. So there's potenial to optimize by + # making image_info a proxy that fetches the info lazily when needed. + # @param [ImageInformation] image_info + # @param [String] x + # @param [String] y + # @param [String] width + # @param [String] height + def initialize(image_info, x, y, width, height) + @image_info = image_info + @offset_x = x.to_i + @offset_y = y.to_i + @width = width.to_i + @height = height.to_i + end + + attr_reader :width, :height + end + end +end diff --git a/app/services/riiif/region/full.rb b/app/services/riiif/region/full.rb new file mode 100644 index 0000000..554e196 --- /dev/null +++ b/app/services/riiif/region/full.rb @@ -0,0 +1,23 @@ +module Riiif + module Region + # Represents the image or region requested at its full size. + # This is a nil crop operation. + class Full < Crop + def initialize(image_info) + @image_info = image_info + end + + # @return [NilClass] a region for imagemagick to decode + # the nil implies no cropping needed + def to_imagemagick + nil + end + + # @return [NilClass] a region for kakadu to decode + # the nil implies no cropping needed + def to_kakadu + nil + end + end + end +end diff --git a/app/services/riiif/region/imagemagick/absolute_decoder.rb b/app/services/riiif/region/imagemagick/absolute_decoder.rb deleted file mode 100644 index 5a797c9..0000000 --- a/app/services/riiif/region/imagemagick/absolute_decoder.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Riiif - module Region - module Imagemagick - # decodes requested cooridnates into an imagemagick crop directive - class AbsoluteDecoder - def initialize(x, y, width, height) - @x = x - @y = y - @width = width - @height = height - end - - # @return [String] a region for imagemagick to decode - # (appropriate for passing to the -crop parameter) - def decode - "#{@width}x#{@height}+#{@x}+#{@y}" - end - end - end - end -end diff --git a/app/services/riiif/region/imagemagick/full_decoder.rb b/app/services/riiif/region/imagemagick/full_decoder.rb deleted file mode 100644 index 65b25e2..0000000 --- a/app/services/riiif/region/imagemagick/full_decoder.rb +++ /dev/null @@ -1,14 +0,0 @@ -module Riiif - module Region - module Imagemagick - # The image or region is not scaled, and is returned at its full size. - class FullDecoder - # @return [NilClass] a region for imagemagick to decode - # the nil implies no cropping needed - def decode - nil - end - end - end - end -end diff --git a/app/services/riiif/region/imagemagick/percentage_decoder.rb b/app/services/riiif/region/imagemagick/percentage_decoder.rb deleted file mode 100644 index 9eeb3b6..0000000 --- a/app/services/riiif/region/imagemagick/percentage_decoder.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Riiif - module Region - module Imagemagick - # decodes requested cooridnates into an imagemagick crop directive - class PercentageDecoder - def initialize(image_info, x, y, width, height) - @image_info = image_info - @x = x - @y = y - @width = width - @height = height - end - - # Imagemagick can't do percentage offsets, so we have to calculate it - # @return [String] a region for imagemagick to decode - # (appropriate for passing to the -crop parameter) - def decode - "#{@width}%x#{@height}+#{offset_x}+#{offset_y}" - end - - private - - def offset_x - (@image_info.width * Integer(@x).to_f / 100).round - end - - def offset_y - (@image_info.height * Integer(@y).to_f / 100).round - end - end - end - end -end diff --git a/app/services/riiif/region/imagemagick/square_decoder.rb b/app/services/riiif/region/imagemagick/square_decoder.rb deleted file mode 100644 index 1e7c43f..0000000 --- a/app/services/riiif/region/imagemagick/square_decoder.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Riiif - module Region - module Imagemagick - # decodes requested cooridnates into an imagemagick crop directive - class SquareDecoder - def initialize(image_info) - @image_info = image_info - end - - # @return [String] a square region for imagemagick to decode - # (appropriate for passing to the -crop parameter) - def decode - min, max = [@image_info.width, @image_info.height].minmax - - offset = (max - min) / 2 - if @image_info.height >= @image_info.width - "#{min}x#{min}+0+#{offset}" - else - "#{min}x#{min}+#{offset}+0" - end - end - end - end - end -end diff --git a/app/services/riiif/region/percentage.rb b/app/services/riiif/region/percentage.rb new file mode 100644 index 0000000..4b69a10 --- /dev/null +++ b/app/services/riiif/region/percentage.rb @@ -0,0 +1,68 @@ +module Riiif + module Region + # represents request cooridnates specified as percentage + class Percentage < Crop + def initialize(image_info, x, y, width, height) + @image_info = image_info + @x_pct = x + @y_pct = y + @width_pct = width + @height_pct = height + end + + # From the Imagemagick docs: + # The percentage symbol '%' can appear anywhere in a argument, and if + # given will refer to both width and height numbers. It is a flag that + # just declares that the 'image size' parts are a percentage fraction + # of the images virtual canvas or page size. Offsets are always given + # in pixels. + # @return [String] a region for imagemagick to decode + # (appropriate for passing to the -crop parameter) + def to_imagemagick + "#{@width_pct}%x#{@height_pct}+#{offset_x}+#{offset_y}" + end + + def maintain_aspect_ratio? + @width_pct == @height_pct + end + + private + + # @param [String] n a percentage to convert + # @return [Float] + def percentage_to_fraction(n) + Integer(n).to_f / 100 + end + + # @return [Integer] + def offset_x + (@image_info.width * percentage_to_fraction(@x_pct)).round + end + + # @return [Integer] + def offset_y + (@image_info.height * percentage_to_fraction(@y_pct)).round + end + + # @return [Float] + def decimal_height + percentage_to_fraction(@height_pct) + end + + # @return [Float] + def decimal_width + percentage_to_fraction(@width_pct) + end + + # @return [Float] + def decimal_offset_y + percentage_to_fraction(@y_pct) + end + + # @return [Float] + def decimal_offset_x + percentage_to_fraction(@x_pct) + end + end + end +end diff --git a/app/services/riiif/region/square.rb b/app/services/riiif/region/square.rb new file mode 100644 index 0000000..cbed015 --- /dev/null +++ b/app/services/riiif/region/square.rb @@ -0,0 +1,45 @@ +module Riiif + module Region + # Represents requested square cooridnates + class Square < Crop + def initialize(image_info) + @image_info = image_info + @min, @max = [@image_info.width, @image_info.height].minmax + @offset = (@max - @min) / 2 + end + + # @return [String] a square region for imagemagick to decode + # (appropriate for passing to the -crop parameter) + def to_imagemagick + if @image_info.height >= @image_info.width + "#{height}x#{width}+0+#{@offset}" + else + "#{height}x#{width}+#{@offset}+0" + end + end + + # @return [String] a region for kakadu to decode + # (appropriate for passing to the -region parameter) + def to_kakadu + # (top, left, height, width) + if @image_info.height >= @image_info.width + # Portrait + "\{#{decimal_height(@offset)},0\}," \ + "\{#{decimal_height(height)},#{decimal_width(height)}\}" + else + # Landscape + "\{0,#{decimal_width(@offset)}\}," \ + "\{#{decimal_height(width)},#{decimal_width(width)}\}" + end + end + + def height + @min + end + + def width + @min + end + end + end +end diff --git a/app/services/riiif/resize.rb b/app/services/riiif/resize.rb new file mode 100644 index 0000000..ad08d4c --- /dev/null +++ b/app/services/riiif/resize.rb @@ -0,0 +1,45 @@ +module Riiif + # Represents a resize operation + class Resize + attr_reader :image_info + + # @return [Integer] the height in pixels + def height + image_info.height + end + + # @return [Integer] the width in pixels + def width + image_info.width + end + + # Should we reduce this image with KDU? + def reduce? + true + end + + # This is used for a second resize by imagemagick after resizing + # by kdu. + # No need to scale most resize operations (only percent) + # @param [Integer] factor to scale by + # @return [Absolute] a copy of self if factor is zero. + def reduce(_factor) + dup + end + + # @return [Integer] the reduction factor for this operation + def reduction_factor(max_factor = 5) + return nil unless reduce? + scale = [width.to_f / image_info.width, + height.to_f / image_info.height].min + factor = 0 + raise "I don't know how to scale to #{scale}" if scale > 1 + next_pct = 0.5 + while scale <= next_pct && factor < max_factor + next_pct /= 2.0 + factor += 1 + end + factor + end + end +end diff --git a/app/services/riiif/size/absolute.rb b/app/services/riiif/size/absolute.rb new file mode 100644 index 0000000..b1ff4b2 --- /dev/null +++ b/app/services/riiif/size/absolute.rb @@ -0,0 +1,39 @@ +module Riiif + module Size + # The width and height of the returned image are exactly w and h. + # The aspect ratio of the returned image may be different than the extracted + # region, resulting in a distorted image. + class Absolute < Resize + # @param [ImageInformation] info + # @param [String] width + # @param [String] height + def initialize(info, width, height) + @image_info = info + @width = width.to_i + @height = height.to_i + end + + # @return [String] a resize directive for imagemagick to use + def to_imagemagick + "#{@width}x#{@height}!" + end + + attr_reader :height, :width + + # Reduce this if the aspect ratio of the image is maintained. + def reduce? + in_delta?(image_info.aspect_ratio, aspect_ratio, 0.001) + end + + private + + def aspect_ratio + width.to_f / height + end + + def in_delta?(x1, x2, delta) + (x1 - x2).abs <= delta + end + end + end +end diff --git a/app/services/riiif/size/best_fit.rb b/app/services/riiif/size/best_fit.rb new file mode 100644 index 0000000..b043c20 --- /dev/null +++ b/app/services/riiif/size/best_fit.rb @@ -0,0 +1,18 @@ +module Riiif + module Size + # The image content is scaled for the best fit such that the resulting width and + # height are less than or equal to the requested width and height. + class BestFit < Resize + def initialize(info, width, height) + @image_info = info + @width = width + @height = height + end + + # @return [String] a resize directive for imagemagick to use + def to_imagemagick + "#{@width}x#{@height}" + end + end + end +end diff --git a/app/services/riiif/size/full.rb b/app/services/riiif/size/full.rb new file mode 100644 index 0000000..5a4aa59 --- /dev/null +++ b/app/services/riiif/size/full.rb @@ -0,0 +1,17 @@ +module Riiif + module Size + # represents requested full size + class Full < Resize + # @return [NilClass] a size for imagemagick to decode + # the nil implies no resizing needed + def to_imagemagick + nil + end + + # Should we reduce this image? + def reduce? + false + end + end + end +end diff --git a/app/services/riiif/size/height.rb b/app/services/riiif/size/height.rb new file mode 100644 index 0000000..a71dfd6 --- /dev/null +++ b/app/services/riiif/size/height.rb @@ -0,0 +1,24 @@ +module Riiif + module Size + # The image or region should be scaled so that its height is exactly equal + # to the provided parameter, and the width will be a calculated value that + # maintains the aspect ratio of the extracted region + class Height < Resize + def initialize(info, height) + @image_info = info + @height = height.to_i + end + + # @return [String] a resize directive for imagemagick to use + def to_imagemagick + "x#{@height}" + end + + def width + height * image_info.width / image_info.height + end + + attr_reader :height + end + end +end diff --git a/app/services/riiif/size/imagemagick/absolute_decoder.rb b/app/services/riiif/size/imagemagick/absolute_decoder.rb deleted file mode 100644 index eb10a52..0000000 --- a/app/services/riiif/size/imagemagick/absolute_decoder.rb +++ /dev/null @@ -1,20 +0,0 @@ -module Riiif - module Size - module Imagemagick - # The width and height of the returned image are exactly w and h. - # The aspect ratio of the returned image may be different than the extracted - # region, resulting in a distorted image. - class AbsoluteDecoder - def initialize(width, height) - @width = width - @height = height - end - - # @return [String] a resize directive for imagemagick to use - def decode - "#{@width}x#{@height}!" - end - end - end - end -end diff --git a/app/services/riiif/size/imagemagick/best_fit_decoder.rb b/app/services/riiif/size/imagemagick/best_fit_decoder.rb deleted file mode 100644 index 86c9b6d..0000000 --- a/app/services/riiif/size/imagemagick/best_fit_decoder.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Riiif - module Size - module Imagemagick - # The image content is scaled for the best fit such that the resulting width and - # height are less than or equal to the requested width and height. - class BestFitDecoder - def initialize(width, height) - @width = width - @height = height - end - - # @return [String] a resize directive for imagemagick to use - def decode - "#{@width}x#{@height}" - end - end - end - end -end diff --git a/app/services/riiif/size/imagemagick/full_decoder.rb b/app/services/riiif/size/imagemagick/full_decoder.rb deleted file mode 100644 index d9e79ee..0000000 --- a/app/services/riiif/size/imagemagick/full_decoder.rb +++ /dev/null @@ -1,14 +0,0 @@ -module Riiif - module Size - module Imagemagick - # decodes requested size into an imagemagick resize directive - class FullDecoder - # @return [NilClass] a size for imagemagick to decode - # the nil implies no resizing needed - def decode - nil - end - end - end - end -end diff --git a/app/services/riiif/size/imagemagick/height_decoder.rb b/app/services/riiif/size/imagemagick/height_decoder.rb deleted file mode 100644 index aa5a2f4..0000000 --- a/app/services/riiif/size/imagemagick/height_decoder.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Riiif - module Size - module Imagemagick - # The image or region should be scaled so that its height is exactly equal - # to the provided parameter, and the width will be a calculated value that - # maintains the aspect ratio of the extracted region - class HeightDecoder - def initialize(height) - @height = height - end - - # @return [String] a resize directive for imagemagick to use - def decode - "x#{@height}" - end - end - end - end -end diff --git a/app/services/riiif/size/imagemagick/percent_decoder.rb b/app/services/riiif/size/imagemagick/percent_decoder.rb deleted file mode 100644 index 115a7c1..0000000 --- a/app/services/riiif/size/imagemagick/percent_decoder.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Riiif - module Size - module Imagemagick - # The width and height of the returned image is scaled to n% of the width and height - # of the extracted region. The aspect ratio of the returned image is the same as that - # of the extracted region. - class PercentDecoder - def initialize(n) - @n = n - end - - # @return [String] a resize directive for imagemagick to use - def decode - "#{@n}%" - end - end - end - end -end diff --git a/app/services/riiif/size/imagemagick/width_decoder.rb b/app/services/riiif/size/imagemagick/width_decoder.rb deleted file mode 100644 index 4250c1a..0000000 --- a/app/services/riiif/size/imagemagick/width_decoder.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Riiif - module Size - module Imagemagick - # The image or region should be scaled so that its width is exactly equal - # to the provided parameter, and the height will be a calculated value that - # maintains the aspect ratio of the extracted region - class WidthDecoder - def initialize(width) - @width = width - end - - # @return [String] a resize directive for imagemagick to use - def decode - @width.to_s - end - end - end - end -end diff --git a/app/services/riiif/size/percent.rb b/app/services/riiif/size/percent.rb new file mode 100644 index 0000000..0e82be2 --- /dev/null +++ b/app/services/riiif/size/percent.rb @@ -0,0 +1,44 @@ +module Riiif + module Size + # The width and height of the returned image is scaled to n% of the width and height + # of the extracted region. The aspect ratio of the returned image is the same as that + # of the extracted region. + class Percent < Resize + def initialize(info, percentage) + @image_info = info + @percentage = percentage + end + + attr_reader :percentage + + # @return [String] a resize directive for imagemagick to use + def to_imagemagick + "#{percentage}%" + end + + # @return [Integer] the height in pixels + def height + percent_of(image_info.height) + end + + # @return [Integer] the width in pixels + def width + percent_of(image_info.width) + end + + # @param [Integer] factor number of times to reduce by 1/2 + def reduce(factor) + pct = percentage.to_f * 2**factor + Percent.new(image_info, pct) + end + + private + + # @param [Integer] value a value to convert to the percentage + # @return [Float] + def percent_of(value) + value * Integer(percentage).to_f / 100 + end + end + end +end diff --git a/app/services/riiif/size/width.rb b/app/services/riiif/size/width.rb new file mode 100644 index 0000000..ac3307f --- /dev/null +++ b/app/services/riiif/size/width.rb @@ -0,0 +1,24 @@ +module Riiif + module Size + # The image or region should be scaled so that its width is exactly equal + # to the provided parameter, and the height will be a calculated value that + # maintains the aspect ratio of the extracted region + class Width < Resize + def initialize(info, width) + @image_info = info + @width = width.to_i + end + + # @return [String] a resize directive for imagemagick to use + def to_imagemagick + @width.to_s + end + + attr_reader :width + + def height + width * image_info.height / image_info.width + end + end + end +end diff --git a/app/transformers/riiif/abstract_transformer.rb b/app/transformers/riiif/abstract_transformer.rb new file mode 100644 index 0000000..a096b90 --- /dev/null +++ b/app/transformers/riiif/abstract_transformer.rb @@ -0,0 +1,30 @@ +module Riiif + # Transforms an image using a backend + class AbstractTransformer + # @param path [String] The path of the source image file + # @param image_info [ImageInformation] information about the source + # @param [Transformation] transformation + def self.transform(path, image_info, transformation) + new(path, image_info, transformation).transform + end + + def initialize(path, image_info, transformation) + @path = path + @image_info = image_info + @transformation = transformation + end + + attr_reader :path, :image_info, :transformation + + def transform + execute(command_builder.command) + end + + def command_builder + @command_builder ||= command_factory.new(path, image_info, transformation) + end + + delegate :execute, to: Riiif::CommandRunner + private :execute + end +end diff --git a/app/transformers/riiif/imagemagick_transformer.rb b/app/transformers/riiif/imagemagick_transformer.rb new file mode 100644 index 0000000..5bebe8c --- /dev/null +++ b/app/transformers/riiif/imagemagick_transformer.rb @@ -0,0 +1,8 @@ +module Riiif + # Transforms an image using Imagemagick + class ImagemagickTransformer < AbstractTransformer + def command_factory + ImagemagickCommandFactory + end + end +end diff --git a/app/transformers/riiif/kakadu_transformer.rb b/app/transformers/riiif/kakadu_transformer.rb new file mode 100644 index 0000000..fde9d7e --- /dev/null +++ b/app/transformers/riiif/kakadu_transformer.rb @@ -0,0 +1,39 @@ +module Riiif + # Transforms an image using Kakadu + class KakaduTransformer < AbstractTransformer + def command_factory + KakaduCommandFactory + end + + def transform + with_tempfile do |file_name| + execute(command_builder.command(file_name)) + post_process(file_name, command_builder.reduction_factor) + end + end + + def with_tempfile + Tempfile.open(['riiif-intermediate', '.bmp']) do |f| + yield f.path + end + end + + # The data we get back from kdu_expand is a bmp and we need to change it + # to the requested format by calling Imagemagick. + def post_process(intermediate_file, reduction_factor) + # Calculate a new set of transforms with respect to reduction_factor + transformation = if reduction_factor + self.transformation.without_crop(image_info).reduce(reduction_factor) + else + self.transformation.without_crop(image_info) + end + Riiif::File.new(intermediate_file).extract(transformation, image_info) + end + + private + + def tmp_path + @link_path ||= LinkNameService.create + end + end +end diff --git a/lib/riiif.rb b/lib/riiif.rb index bd74c12..467e199 100644 --- a/lib/riiif.rb +++ b/lib/riiif.rb @@ -18,7 +18,10 @@ class ImageNotFoundError < Error; end # This error is raised when Riiif can't convert an image class ConversionError < Error; end - Transformation = Struct.new(:crop, :size, :quality, :rotation, :format) mattr_accessor :not_found_image # the image to use when a lookup fails mattr_accessor :unauthorized_image # the image to use when a user doesn't have access + + def self.kakadu_enabled? + Engine.config.kakadu_enabled + end end diff --git a/lib/riiif/engine.rb b/lib/riiif/engine.rb index c08ff99..d163c8d 100644 --- a/lib/riiif/engine.rb +++ b/lib/riiif/engine.rb @@ -5,8 +5,9 @@ class Engine < ::Rails::Engine # How long to cache the tiles for. config.cache_duration_in_days = 3 - config.action_dispatch.rescue_responses.merge!( - 'Riiif::ImageNotFoundError' => :not_found - ) + config.action_dispatch.rescue_responses['Riiif::ImageNotFoundError'] = :not_found + + # Set to true to use kdu for jp2000 source images + config.kakadu_enabled = false end end diff --git a/spec/models/riiif/image_spec.rb b/spec/models/riiif/image_spec.rb index ff914a6..33e2cd3 100644 --- a/spec/models/riiif/image_spec.rb +++ b/spec/models/riiif/image_spec.rb @@ -15,11 +15,14 @@ describe 'happy path' do before do - expect(subject.file).to receive(:execute) - .with("convert -quality 85 -sampling-factor 4:2:0 -strip #{filename} jpg:-") - .and_return('imagedata') + allow(image.info_service).to receive(:call).and_return({}) end + it 'renders' do + expect(Riiif::CommandRunner).to receive(:execute) + .with("convert -quality 85 -sampling-factor 4:2:0 -strip #{filename} jpg:-") + .and_return('imagedata') + expect(subject.render('size' => 'full', format: 'jpg')).to eq 'imagedata' end end diff --git a/spec/models/riiif/transformation_spec.rb b/spec/models/riiif/transformation_spec.rb new file mode 100644 index 0000000..c462459 --- /dev/null +++ b/spec/models/riiif/transformation_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Riiif::Transformation do + subject(:transformation) do + Riiif::Transformation.new(region, + size, + quality, + rotation, + fmt) + end + + let(:region) { Riiif::Region::Full.new(image_info) } + let(:size) { Riiif::Size::Percent.new(image_info, 20) } + let(:quality) { nil } + let(:rotation) { nil } + let(:fmt) { nil } + let(:image_info) { double('Image info', height: 4381, width: 6501) } + + describe 'reduce' do + subject { transformation.reduce(factor) } + context 'when reduced by 2' do + let(:factor) { 2 } + let(:size) { Riiif::Size::Percent.new(image_info, 20) } + + it 'downsamples the size' do + expect(subject.size).to be_kind_of Riiif::Size::Percent + expect(subject.size.percentage).to eq 80.0 + end + end + end + + describe 'without_crop' do + let(:region) { Riiif::Region::Absolute.new(image_info, 5, 6, 7, 8) } + + subject { transformation.without_crop(image_info) } + it 'nullifies the crop' do + expect(subject.crop).to be_kind_of Riiif::Region::Full + end + end +end diff --git a/spec/services/riiif/imagemagick_command_factory_spec.rb b/spec/services/riiif/imagemagick_command_factory_spec.rb index 6e29030..02c8879 100644 --- a/spec/services/riiif/imagemagick_command_factory_spec.rb +++ b/spec/services/riiif/imagemagick_command_factory_spec.rb @@ -2,13 +2,15 @@ RSpec.describe Riiif::ImagemagickCommandFactory do let(:path) { 'foo.tiff' } + let(:info) { double('foo') } - describe '.build' do - subject { described_class.build(path, transformation) } + describe '.command' do + subject { instance.command } + let(:instance) { described_class.new(path, info, transformation) } let(:transformation) do - Riiif::Transformation.new('region', - 'size', + Riiif::Transformation.new(Riiif::Region::Full.new(info), + Riiif::Size::Full.new, 'quality', 'rotation', format) diff --git a/spec/services/riiif/kakadu_command_factory_spec.rb b/spec/services/riiif/kakadu_command_factory_spec.rb new file mode 100644 index 0000000..851fecf --- /dev/null +++ b/spec/services/riiif/kakadu_command_factory_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Riiif::KakaduCommandFactory do + subject(:instance) { described_class.new(path, info, transformation) } + + let(:info) { double(:info) } + let(:path) { 'foo.jp2' } + let(:region) { Riiif::Region::Full.new(info) } + let(:size) { Riiif::Size::Full.new } + let(:quality) { nil } + let(:rotation) { nil } + let(:fmt) { nil } + + let(:transformation) do + Riiif::Transformation.new(region, + size, + quality, + rotation, + fmt) + end + + describe '#command' do + subject { instance.command '/tmp/bar.bmp' } + + context 'with a full size image' do + it { is_expected.to eq 'kdu_expand -quiet -i foo.jp2 -num_threads 4 -o /tmp/bar.bmp' } + end + end + + describe '#region' do + subject { instance.send(:region) } + let(:info) { double(height: 300, width: 300) } + + context 'with a full' do + it { is_expected.to be nil } + end + + context 'with absolute' do + let(:region) { Riiif::Region::Absolute.new(info, 25, 75, 150, 100) } + it { is_expected.to eq ' -region "{0.25,0.08333333333333333},{0.3333333333333333,0.5}"' } + end + + context 'with a square' do + let(:region) { Riiif::Region::Square.new(info) } + it { is_expected.to eq ' -region "{0.0,0},{1.0,1.0}"' } + end + + context 'with a percentage' do + let(:region) { Riiif::Region::Percentage.new(info, 20, 30, 40, 50) } + it { is_expected.to eq ' -region "{0.3,0.2},{0.5,0.4}"' } + end + end + + describe '#reduction_factor' do + subject { instance.send(:reduction_factor) } + + let(:info) { Riiif::ImageInformation.new(300, 300) } + + context 'for a full size image' do + it { is_expected.to eq nil } + end + + context 'when the aspect ratio is maintined for absolute' do + let(:size) { Riiif::Size::Absolute.new(info, 145, 145) } + it { is_expected.to eq 1 } + end + + context 'when the aspect ratio is not-maintined' do + let(:size) { Riiif::Size::Absolute.new(info, 100, 145) } + it { is_expected.to eq nil } + end + + context 'when aspect ratio is maintained for 45 pct' do + let(:size) { Riiif::Size::Percent.new(info, 45) } + it { is_expected.to eq 1 } + end + + context 'when aspect ratio is maintained for 20 pct' do + let(:size) { Riiif::Size::Percent.new(info, 20) } + it { is_expected.to eq 2 } + end + end +end diff --git a/spec/services/riiif/region/absolute_spec.rb b/spec/services/riiif/region/absolute_spec.rb new file mode 100644 index 0000000..19e3b9d --- /dev/null +++ b/spec/services/riiif/region/absolute_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +RSpec.describe Riiif::Region::Absolute do + let(:image_info) { double } + + context 'when initialized with strings' do + let(:instance) { described_class.new(image_info, '5', '15', '50', '100') } + + it 'casts height to an integer' do + expect(instance.height).to eq 100 + end + + it 'casts width to an integer' do + expect(instance.width).to eq 50 + end + end +end diff --git a/spec/services/riiif/size/absolute_spec.rb b/spec/services/riiif/size/absolute_spec.rb new file mode 100644 index 0000000..4c5ef03 --- /dev/null +++ b/spec/services/riiif/size/absolute_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +RSpec.describe Riiif::Size::Absolute do + let(:image_info) { double } + + context 'when initialized with strings' do + let(:instance) { described_class.new(image_info, '50', '100') } + + it 'casts height to an integer' do + expect(instance.height).to eq 100 + end + + it 'casts width to an integer' do + expect(instance.width).to eq 50 + end + end +end diff --git a/spec/services/riiif/size/height_spec.rb b/spec/services/riiif/size/height_spec.rb new file mode 100644 index 0000000..71b167f --- /dev/null +++ b/spec/services/riiif/size/height_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +RSpec.describe Riiif::Size::Height do + let(:image_info) { double } + + context 'when initialized with strings' do + let(:instance) { described_class.new(image_info, '50') } + + it 'casts height to an integer' do + expect(instance.height).to eq 50 + end + end +end diff --git a/spec/services/riiif/size/width_spec.rb b/spec/services/riiif/size/width_spec.rb new file mode 100644 index 0000000..6c7562b --- /dev/null +++ b/spec/services/riiif/size/width_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +RSpec.describe Riiif::Size::Width do + let(:image_info) { double } + + context 'when initialized with strings' do + let(:instance) { described_class.new(image_info, '50') } + + it 'casts height to an integer' do + expect(instance.width).to eq 50 + end + end +end diff --git a/spec/transformers/riiif/kakadu_transformer_spec.rb b/spec/transformers/riiif/kakadu_transformer_spec.rb new file mode 100644 index 0000000..9fcd43e --- /dev/null +++ b/spec/transformers/riiif/kakadu_transformer_spec.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Riiif::KakaduTransformer do + subject(:instance) { described_class.new(path, image_info, transformation) } + + let(:image_info) { Riiif::ImageInformation.new(6501, 4381) } + let(:path) { 'baseball.jp2' } + let(:region) { Riiif::Region::Full.new(image_info) } + let(:size) { Riiif::Size::Full.new } + let(:quality) { nil } + let(:rotation) { nil } + let(:fmt) { 'jpg' } + + let(:transformation) do + Riiif::Transformation.new(region, + size, + quality, + rotation, + fmt) + end + + describe '#transform' do + let(:image_data) { double } + + subject(:transform) { instance.transform } + + before do + allow(instance).to receive(:with_tempfile).and_yield('/tmp/foo.bmp') + end + + context 'resize and region' do + # This is the validator test for size_region + let(:size) { Riiif::Size::Absolute.new(image_info, 38, 38) } + let(:region) { Riiif::Region::Absolute.new(image_info, 200, 100, 100, 100) } + + let(:image_info) { Riiif::ImageInformation.new(1000, 1000) } + + it 'calls the Imagemagick transform' do + expect(Riiif::CommandRunner).to receive(:execute) + .with('kdu_expand -quiet -i baseball.jp2 -num_threads 4 ' \ + '-region "{0.1,0.2},{0.1,0.1}" -reduce 4 -o /tmp/foo.bmp') + expect(Riiif::CommandRunner).to receive(:execute) + .with('convert -resize 38x38! -quality 85 -sampling-factor 4:2:0 -strip /tmp/foo.bmp jpg:-') + transform + end + end + + context 'when reduction_factor is 0' do + let(:reduction_factor) { 0 } + context 'and the size is full' do + it 'calls the Imagemagick transform' do + expect(Riiif::CommandRunner).to receive(:execute) + .with('kdu_expand -quiet -i baseball.jp2 -num_threads 4 -o /tmp/foo.bmp') + expect(Riiif::CommandRunner).to receive(:execute) + .with('convert -quality 85 -sampling-factor 4:2:0 -strip /tmp/foo.bmp jpg:-') + transform + end + end + + context 'and size is a width' do + let(:size) { Riiif::Size::Width.new(image_info, 651) } + let(:image_info) { Riiif::ImageInformation.new(1000, 1000) } + + it 'calls the Imagemagick transform' do + expect(Riiif::CommandRunner).to receive(:execute) + .with('kdu_expand -quiet -i baseball.jp2 -num_threads 4 -o /tmp/foo.bmp') + expect(Riiif::CommandRunner).to receive(:execute) + .with('convert -resize 651 -quality 85 -sampling-factor 4:2:0 -strip /tmp/foo.bmp jpg:-') + transform + end + end + + context 'and size is a height' do + let(:size) { Riiif::Size::Height.new(image_info, 581) } + let(:image_info) { Riiif::ImageInformation.new(1000, 1000) } + + it 'calls the Imagemagick transform' do + expect(Riiif::CommandRunner).to receive(:execute) + .with('kdu_expand -quiet -i baseball.jp2 -num_threads 4 -o /tmp/foo.bmp') + expect(Riiif::CommandRunner).to receive(:execute) + .with('convert -resize x581 -quality 85 -sampling-factor 4:2:0 -strip /tmp/foo.bmp jpg:-') + transform + end + end + end + + context 'when reduction_factor is 1' do + let(:reduction_factor) { 1 } + + context 'and size is a Percent' do + let(:size) { Riiif::Size::Percent.new(image_info, 30) } + + it 'calls the Imagemagick transform' do + expect(Riiif::CommandRunner).to receive(:execute) + .with('kdu_expand -quiet -i baseball.jp2 -num_threads 4 -reduce 1 -o /tmp/foo.bmp') + expect(Riiif::CommandRunner).to receive(:execute) + .with('convert -resize 60.0% -quality 85 -sampling-factor 4:2:0 -strip /tmp/foo.bmp jpg:-') + transform + end + end + + context 'and size is a width' do + let(:size) { Riiif::Size::Width.new(image_info, 408) } + let(:image_info) { Riiif::ImageInformation.new(1000, 1000) } + + it 'calls the Imagemagick transform' do + expect(Riiif::CommandRunner).to receive(:execute) + .with('kdu_expand -quiet -i baseball.jp2 -num_threads 4 -reduce 1 -o /tmp/foo.bmp') + expect(Riiif::CommandRunner).to receive(:execute) + .with('convert -resize 408 -quality 85 -sampling-factor 4:2:0 -strip /tmp/foo.bmp jpg:-') + transform + end + end + + context 'and size is a height' do + let(:size) { Riiif::Size::Height.new(image_info, 481) } + let(:image_info) { Riiif::ImageInformation.new(1000, 1000) } + + it 'calls the Imagemagick transform' do + expect(Riiif::CommandRunner).to receive(:execute) + .with('kdu_expand -quiet -i baseball.jp2 -num_threads 4 -reduce 1 -o /tmp/foo.bmp') + expect(Riiif::CommandRunner).to receive(:execute) + .with('convert -resize x481 -quality 85 -sampling-factor 4:2:0 -strip /tmp/foo.bmp jpg:-') + transform + end + end + end + + context 'when reduction_factor is 2' do + let(:size) { Riiif::Size::Percent.new(image_info, 20) } + let(:reduction_factor) { 2 } + it 'calls the Imagemagick transform' do + expect(Riiif::CommandRunner).to receive(:execute) + .with('kdu_expand -quiet -i baseball.jp2 -num_threads 4 -reduce 2 -o /tmp/foo.bmp') + expect(Riiif::CommandRunner).to receive(:execute) + .with('convert -resize 80.0% -quality 85 -sampling-factor 4:2:0 -strip /tmp/foo.bmp jpg:-') + transform + end + end + end +end