Skip to content

Commit

Permalink
Merge pull request #87 from curationexperts/kakadu
Browse files Browse the repository at this point in the history
Kakadu support
  • Loading branch information
aaron-collier authored Sep 8, 2017
2 parents 437b3d5 + 1150ea9 commit c4073ca
Show file tree
Hide file tree
Showing 48 changed files with 1,038 additions and 267 deletions.
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

0 comments on commit c4073ca

Please sign in to comment.