Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kakadu support #87

Merged
merged 10 commits into from
Sep 8, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
source 'https://rubygems.org'

gem 'byebug'
# Specify your gem's dependencies in riiif.gemspec
gemspec
31 changes: 19 additions & 12 deletions app/models/riiif/file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
23 changes: 12 additions & 11 deletions app/models/riiif/image.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions app/models/riiif/image_information.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions app/models/riiif/transformation.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions app/services/riiif/command_runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions app/services/riiif/crop.rb
Original file line number Diff line number Diff line change
@@ -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
39 changes: 20 additions & 19 deletions app/services/riiif/imagemagick_command_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 8 additions & 0 deletions app/services/riiif/imagemagick_transformer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module Riiif
# Transforms an image using Imagemagick
class ImagemagickTransformer < AbstractTransformer
def command_factory
ImagemagickCommandFactory
end
end
end
63 changes: 63 additions & 0 deletions app/services/riiif/kakadu_command_factory.rb
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions app/services/riiif/link_name_service.rb
Original file line number Diff line number Diff line change
@@ -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
Loading