assembly-image 1.8.0 → 2.1.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 +4 -4
- data/.circleci/config.yml +4 -1
- data/.gitignore +2 -1
- data/.rubocop.yml +23 -5
- data/.rubocop_todo.yml +11 -51
- data/.rvmrc.example +1 -1
- data/Gemfile.lock +105 -0
- data/README.md +15 -58
- data/assembly-image.gemspec +4 -5
- data/bin/console +1 -1
- data/config/boot.rb +2 -2
- data/lib/assembly/image/jp2_creator.rb +127 -0
- data/lib/assembly/image.rb +58 -0
- data/lib/assembly-image.rb +5 -3
- data/profiles/cmyk.icc +0 -0
- data/spec/assembly/image/jp2_creator_spec.rb +327 -38
- data/spec/assembly/image_spec.rb +25 -0
- data/spec/spec_helper.rb +115 -29
- data/spec/test_data/color_rgb_srgb_rot90cw.tif +0 -0
- metadata +39 -38
- data/bin/run_all_tests +0 -3
- data/lib/assembly-image/image.rb +0 -162
- data/lib/assembly-image/images.rb +0 -107
- data/lib/assembly-image/jp2_creator.rb +0 -182
- data/lib/assembly-image/version.rb +0 -10
- data/spec/image_spec.rb +0 -279
- data/spec/images_spec.rb +0 -47
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: assembly-image
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Mangiafico
|
@@ -11,8 +11,22 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: exe
|
13
13
|
cert_chain: []
|
14
|
-
date: 2022-
|
14
|
+
date: 2022-07-19 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: activesupport
|
18
|
+
requirement: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '6.1'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '6.1'
|
16
30
|
- !ruby/object:Gem::Dependency
|
17
31
|
name: assembly-objectfile
|
18
32
|
requirement: !ruby/object:Gem::Requirement
|
@@ -28,25 +42,33 @@ dependencies:
|
|
28
42
|
- !ruby/object:Gem::Version
|
29
43
|
version: 1.6.4
|
30
44
|
- !ruby/object:Gem::Dependency
|
31
|
-
name:
|
45
|
+
name: ruby-vips
|
32
46
|
requirement: !ruby/object:Gem::Requirement
|
33
47
|
requirements:
|
34
48
|
- - ">="
|
35
49
|
- !ruby/object:Gem::Version
|
36
|
-
version: '
|
37
|
-
- - "<"
|
38
|
-
- !ruby/object:Gem::Version
|
39
|
-
version: '3'
|
50
|
+
version: '2.0'
|
40
51
|
type: :runtime
|
41
52
|
prerelease: false
|
42
53
|
version_requirements: !ruby/object:Gem::Requirement
|
43
54
|
requirements:
|
44
55
|
- - ">="
|
45
56
|
- !ruby/object:Gem::Version
|
46
|
-
version: '
|
47
|
-
|
57
|
+
version: '2.0'
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: pry-byebug
|
60
|
+
requirement: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
48
63
|
- !ruby/object:Gem::Version
|
49
|
-
version: '
|
64
|
+
version: '0'
|
65
|
+
type: :development
|
66
|
+
prerelease: false
|
67
|
+
version_requirements: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
50
72
|
- !ruby/object:Gem::Dependency
|
51
73
|
name: rake
|
52
74
|
requirement: !ruby/object:Gem::Requirement
|
@@ -117,20 +139,6 @@ dependencies:
|
|
117
139
|
- - ">="
|
118
140
|
- !ruby/object:Gem::Version
|
119
141
|
version: '0'
|
120
|
-
- !ruby/object:Gem::Dependency
|
121
|
-
name: yard
|
122
|
-
requirement: !ruby/object:Gem::Requirement
|
123
|
-
requirements:
|
124
|
-
- - ">="
|
125
|
-
- !ruby/object:Gem::Version
|
126
|
-
version: '0'
|
127
|
-
type: :development
|
128
|
-
prerelease: false
|
129
|
-
version_requirements: !ruby/object:Gem::Requirement
|
130
|
-
requirements:
|
131
|
-
- - ">="
|
132
|
-
- !ruby/object:Gem::Version
|
133
|
-
version: '0'
|
134
142
|
description: Contains classes to create derivative image files and perform other image
|
135
143
|
operations
|
136
144
|
email:
|
@@ -147,25 +155,24 @@ files:
|
|
147
155
|
- ".rubocop_todo.yml"
|
148
156
|
- ".rvmrc.example"
|
149
157
|
- Gemfile
|
158
|
+
- Gemfile.lock
|
150
159
|
- LICENSE
|
151
160
|
- README.md
|
152
161
|
- Rakefile
|
153
162
|
- assembly-image.gemspec
|
154
163
|
- bin/console
|
155
|
-
- bin/run_all_tests
|
156
164
|
- config/boot.rb
|
157
165
|
- lib/assembly-image.rb
|
158
|
-
- lib/assembly
|
159
|
-
- lib/assembly
|
160
|
-
- lib/assembly-image/jp2_creator.rb
|
161
|
-
- lib/assembly-image/version.rb
|
166
|
+
- lib/assembly/image.rb
|
167
|
+
- lib/assembly/image/jp2_creator.rb
|
162
168
|
- profiles/AdobeRGB1998.icc
|
163
169
|
- profiles/DotGain20.icc
|
170
|
+
- profiles/cmyk.icc
|
164
171
|
- profiles/sRGBIEC6196621.icc
|
165
172
|
- spec/assembly/image/jp2_creator_spec.rb
|
166
|
-
- spec/image_spec.rb
|
167
|
-
- spec/images_spec.rb
|
173
|
+
- spec/assembly/image_spec.rb
|
168
174
|
- spec/spec_helper.rb
|
175
|
+
- spec/test_data/color_rgb_srgb_rot90cw.tif
|
169
176
|
- spec/test_data/input/.empty
|
170
177
|
- spec/test_data/output/.empty
|
171
178
|
homepage: ''
|
@@ -192,10 +199,4 @@ signing_key:
|
|
192
199
|
specification_version: 4
|
193
200
|
summary: Ruby immplementation of image services needed to prepare objects to be accessioned
|
194
201
|
in SULAIR digital library
|
195
|
-
test_files:
|
196
|
-
- spec/assembly/image/jp2_creator_spec.rb
|
197
|
-
- spec/image_spec.rb
|
198
|
-
- spec/images_spec.rb
|
199
|
-
- spec/spec_helper.rb
|
200
|
-
- spec/test_data/input/.empty
|
201
|
-
- spec/test_data/output/.empty
|
202
|
+
test_files: []
|
data/bin/run_all_tests
DELETED
data/lib/assembly-image/image.rb
DELETED
@@ -1,162 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'assembly-objectfile'
|
4
|
-
require_relative 'jp2_creator'
|
5
|
-
|
6
|
-
module Assembly
|
7
|
-
# The Image class contains methods to operate on an image.
|
8
|
-
class Image
|
9
|
-
# include common behaviors from assembly-objectfile gem
|
10
|
-
include Assembly::ObjectFileable
|
11
|
-
|
12
|
-
# Examines the input image for validity. Used to determine if image is correct and if JP2 generation is likely to succeed.
|
13
|
-
# This method is automatically called before you create a jp2 but it can be called separately earlier as a sanity check.
|
14
|
-
#
|
15
|
-
# @return [boolean] true if image is valid, false if not.
|
16
|
-
#
|
17
|
-
# Example:
|
18
|
-
# source_img=Assembly::ObjectFile.new('/input/path_to_file.tif')
|
19
|
-
# puts source_img.valid? # gives true
|
20
|
-
def valid?
|
21
|
-
valid_image? # behavior is defined in assembly-objectfile gem
|
22
|
-
end
|
23
|
-
|
24
|
-
# Get the image color profile
|
25
|
-
#
|
26
|
-
# @return [string] image color profile
|
27
|
-
# Example:
|
28
|
-
# source_img=Assembly::Image.new('/input/path_to_file.tif')
|
29
|
-
# puts source_img.profile # gives 'Adobe RGB 1998'
|
30
|
-
def profile
|
31
|
-
exif.nil? ? nil : exif['profiledescription']
|
32
|
-
end
|
33
|
-
|
34
|
-
# Get the image height from exif data
|
35
|
-
#
|
36
|
-
# @return [integer] image height in pixels
|
37
|
-
# Example:
|
38
|
-
# source_img=Assembly::Image.new('/input/path_to_file.tif')
|
39
|
-
# puts source_img.height # gives 100
|
40
|
-
def height
|
41
|
-
exif.imageheight
|
42
|
-
end
|
43
|
-
|
44
|
-
# Get the image width from exif data
|
45
|
-
# @return [integer] image height in pixels
|
46
|
-
# Example:
|
47
|
-
# source_img=Assembly::Image.new('/input/path_to_file.tif')
|
48
|
-
# puts source_img.width # gives 100
|
49
|
-
def width
|
50
|
-
exif.imagewidth
|
51
|
-
end
|
52
|
-
|
53
|
-
# Examines the input image to determine if it is compressed.
|
54
|
-
#
|
55
|
-
# @return [boolean] true if image is compressed, false if not.
|
56
|
-
#
|
57
|
-
# Example:
|
58
|
-
# source_img=Assembly::ObjectFile.new('/input/path_to_file.tif')
|
59
|
-
# puts source_img.compressed? # gives true
|
60
|
-
# def compressed?
|
61
|
-
# exif.compression != 'Uncompressed'
|
62
|
-
# end
|
63
|
-
|
64
|
-
# Add an exif color profile descriptions to the image.
|
65
|
-
# This is useful if your source TIFFs do not have color profile descriptions in the EXIF data, but you know what it should be.
|
66
|
-
# This will allow the images to pass the validaty check and have JP2s created successfully.
|
67
|
-
#
|
68
|
-
# Note you will need full read/write access to the source path so that new EXIF data can be saved.
|
69
|
-
#
|
70
|
-
# @param [String] profile_name profile name to be added, current options are 'Adobe RBG 1998','Dot Gain 20%','sRGB IEC61966-2.1'
|
71
|
-
#
|
72
|
-
# @param [String] force if set to true, force overwrite a color profile description even if it already exists (default: false)
|
73
|
-
#
|
74
|
-
# Example:
|
75
|
-
# source_img=Assembly::Image.new('/input/path_to_file.tif')
|
76
|
-
# source_img.add_exif_profile_description('Adobe RGB 1998')
|
77
|
-
def add_exif_profile_description(profile_name, force = false)
|
78
|
-
if profile.nil? || force
|
79
|
-
input_profile = profile_name.gsub(/[^[:alnum:]]/, '') # remove all non alpha-numeric characters, so we can get to a filename
|
80
|
-
path_to_profiles = File.join(Assembly::PATH_TO_IMAGE_GEM, 'profiles')
|
81
|
-
input_profile_file = File.join(path_to_profiles, "#{input_profile}.icc")
|
82
|
-
command = "exiftool '-icc_profile<=#{input_profile_file}' #{path}"
|
83
|
-
result = `#{command} 2>&1`
|
84
|
-
raise "profile addition command failed: #{command} with result #{result}" unless $CHILD_STATUS.success?
|
85
|
-
end
|
86
|
-
rescue StandardError => e
|
87
|
-
puts "** Error for #{filename}: #{e.message}"
|
88
|
-
end
|
89
|
-
|
90
|
-
# Returns the full default jp2 path and filename that will be created from the given image
|
91
|
-
#
|
92
|
-
# @return [string] full default jp2 path and filename that will be created from the given image
|
93
|
-
# Example:
|
94
|
-
# source_img=Assembly::Image.new('/input/path_to_file.tif')
|
95
|
-
# puts source_img.jp2_filename # gives /input/path_to_file.jp2
|
96
|
-
def jp2_filename
|
97
|
-
File.extname(path).empty? ? "#{path}.jp2" : path.gsub(File.extname(path), '.jp2')
|
98
|
-
end
|
99
|
-
|
100
|
-
# Returns the full DPG equivalent jp2 path and filename that would match with the given image
|
101
|
-
#
|
102
|
-
# @return [string] full DPG equivalent jp2 path and filename
|
103
|
-
# Example:
|
104
|
-
# source_img=Assembly::Image.new('/input/path_to_file.tif')
|
105
|
-
# puts source_img.jp2_filename # gives /input/path_to_file.jp2
|
106
|
-
def dpg_jp2_filename
|
107
|
-
jp2_filename.gsub('_00_', '_05_')
|
108
|
-
end
|
109
|
-
|
110
|
-
# Create a JP2 file for the current image.
|
111
|
-
# Important note: this will not work for multipage TIFFs.
|
112
|
-
#
|
113
|
-
# @return [Assembly::Image] object containing the generated JP2 file
|
114
|
-
#
|
115
|
-
# @param [Hash] params Optional parameters specified as a hash, using symbols for options:
|
116
|
-
# * :output => path to the output JP2 file (default: mirrors the source file name and path, but with a .jp2 extension)
|
117
|
-
# * :overwrite => if set to false, an existing JP2 file with the same name won't be overwritten (default: false)
|
118
|
-
# * :tmp_folder => the temporary folder to use when creating the jp2 (default: '/tmp'); also used by imagemagick
|
119
|
-
#
|
120
|
-
# Example:
|
121
|
-
# source_img=Assembly::Image.new('/input/path_to_file.tif')
|
122
|
-
# derivative_img=source_img.create_jp2(:overwrite=>true)
|
123
|
-
# puts derivative_img.mimetype # 'image/jp2'
|
124
|
-
# puts derivative_image.path # '/input/path_to_file.jp2'
|
125
|
-
# rubocop:disable Metrics/CyclomaticComplexity:
|
126
|
-
def create_jp2(params = {})
|
127
|
-
Jp2Creator.create(self, params)
|
128
|
-
end
|
129
|
-
|
130
|
-
def samples_per_pixel
|
131
|
-
if exif['samplesperpixel']
|
132
|
-
exif['samplesperpixel'].to_i
|
133
|
-
else
|
134
|
-
case mimetype
|
135
|
-
when 'image/tiff'
|
136
|
-
1
|
137
|
-
when 'image/jpeg'
|
138
|
-
3
|
139
|
-
end
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
# Get size of image data in bytes
|
144
|
-
def image_data_size
|
145
|
-
(samples_per_pixel * height * width * bits_per_sample) / 8
|
146
|
-
end
|
147
|
-
|
148
|
-
private
|
149
|
-
|
150
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
151
|
-
def bits_per_sample
|
152
|
-
if exif['bitspersample']
|
153
|
-
exif['bitspersample'].to_i
|
154
|
-
else
|
155
|
-
case mimetype
|
156
|
-
when 'image/tiff'
|
157
|
-
1
|
158
|
-
end
|
159
|
-
end
|
160
|
-
end
|
161
|
-
end
|
162
|
-
end
|
@@ -1,107 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'logger'
|
4
|
-
module Assembly
|
5
|
-
# The Images class contains methods to operate on multiple images in batch.
|
6
|
-
class Images
|
7
|
-
def self.logger
|
8
|
-
@logger ||= Logger.new(STDERR)
|
9
|
-
end
|
10
|
-
|
11
|
-
class << self
|
12
|
-
attr_writer :logger
|
13
|
-
end
|
14
|
-
|
15
|
-
# Pass in a source path and have exif color profile descriptions added to all images contained.
|
16
|
-
# This is useful if your source TIFFs do not have color profile descriptions in the EXIF data, but you know what it should be.
|
17
|
-
# This will allow the images to pass the validty check and have JP2s created successfully.
|
18
|
-
#
|
19
|
-
# Note you will need full read/write access to the source path so that new EXIF data can be saved.
|
20
|
-
#
|
21
|
-
# @param [String] source path full path to the directory containing TIFFs
|
22
|
-
# @param [String] profile_name profile name to be added, current options are 'Adobe RBG 1998','Dot Gain 20%','sRGB IEC61966-2.1'
|
23
|
-
#
|
24
|
-
# @param [Hash] params Optional parameters specified as a hash, using symbols for options:
|
25
|
-
# * :force => if set to true, force overwrite a color profile description even if it already exists (default: false)
|
26
|
-
# * :recusrive => if set to true, directories will be searched recursively for TIFFs from the source specified, false searches the top level only (default: false)
|
27
|
-
# * :extension => defines the types of files that will be processed (default '.tif')
|
28
|
-
#
|
29
|
-
# Example:
|
30
|
-
# Assembly::Images.batch_add_exif_profile_description('/full_path_to_tifs','Adobe RGB 1998')
|
31
|
-
# rubocop:disable Metrics/MethodLength
|
32
|
-
# rubocop:disable Metrics/AbcSize
|
33
|
-
def self.batch_add_exif_profile_descr(source, profile_name, params = {})
|
34
|
-
extension = params[:extension] || 'tif'
|
35
|
-
recursive = params[:recursive] || false
|
36
|
-
force = params[:force] || false
|
37
|
-
|
38
|
-
raise 'Input path does not exist' unless File.directory?(source)
|
39
|
-
|
40
|
-
logger.debug "Source: #{source}"
|
41
|
-
|
42
|
-
# iterate over input directory looking for tifs
|
43
|
-
pattern = recursive ? "**/*.#{extension}" : "*.#{extension}*"
|
44
|
-
Dir.glob(File.join(source, pattern)).each do |file|
|
45
|
-
img = Assembly::Image.new(file)
|
46
|
-
logger.debug "Processing #{file}"
|
47
|
-
img.add_exif_profile_description(profile_name, force)
|
48
|
-
end
|
49
|
-
'Complete'
|
50
|
-
end
|
51
|
-
# rubocop:enable Metrics/MethodLength
|
52
|
-
# rubocop:enable Metrics/AbcSize
|
53
|
-
|
54
|
-
# Pass in a source path and get JP2s generate for each tiff that is in the source path
|
55
|
-
#
|
56
|
-
# If not passed in, the destination will be a "jp2" subfolder within the source folder.
|
57
|
-
# Note you will need read access to the source path, and write access to the destination path.
|
58
|
-
#
|
59
|
-
# @param [String] source path full path to the directory containing TIFFs to be converted to JP2
|
60
|
-
#
|
61
|
-
# @param [Hash] params Optional parameters specified as a hash, using symbols for options:
|
62
|
-
# * :output=>'/full/path_to_jp2' # specifies full path to folder where jp2s will be created (default: jp2 subdirectory from source path)
|
63
|
-
# * :overwrite => if set to false, an existing JP2 file with the same name won't be overwritten (default: false)
|
64
|
-
# * :recursive => if set to true, directories will be searched recursively for TIFFs from the source specified, false searches the top level only (default: false)
|
65
|
-
# * :extension => defines the types of files that will be processed (default '.tif')
|
66
|
-
#
|
67
|
-
# Example:
|
68
|
-
# Assembly::Images.batch_generate_jp2('/full_path_to_tifs')
|
69
|
-
# rubocop:disable Metrics/MethodLength
|
70
|
-
# rubocop:disable Metrics/AbcSize
|
71
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
72
|
-
# rubocop:disable Metrics/PerceivedComplexity
|
73
|
-
def self.batch_generate_jp2(source, params = {})
|
74
|
-
raise 'Input path does not exist' unless File.directory?(source)
|
75
|
-
|
76
|
-
output = params[:output] || File.join(source, 'jp2') # default output directgory is jp2 sub-directory from source
|
77
|
-
extension = params[:extension] || 'tif'
|
78
|
-
overwrite = params[:overwrite] || false
|
79
|
-
recursive = params[:recursive] || false
|
80
|
-
|
81
|
-
Dir.mkdir(output) unless File.directory?(output) # attemp to make output directory
|
82
|
-
raise 'Output path does not exist or could not be created' unless File.directory?(output)
|
83
|
-
|
84
|
-
logger.debug "Source: #{source}"
|
85
|
-
logger.debug "Destination: #{output}"
|
86
|
-
|
87
|
-
pattern = recursive ? "**/*.#{extension}" : "*.#{extension}*"
|
88
|
-
|
89
|
-
# iterate over input directory looking for tifs
|
90
|
-
Dir.glob(File.join(source, pattern)).each do |file|
|
91
|
-
source_img = Assembly::Image.new(file)
|
92
|
-
output_img = File.join(output, File.basename(file, File.extname(file)) + '.jp2') # output image gets same file name as source, but with a jp2 extension and in the correct output directory
|
93
|
-
begin
|
94
|
-
source_img.create_jp2(overwrite: overwrite, output: output_img)
|
95
|
-
logger.debug "Generated jp2 for #{File.basename(file)}"
|
96
|
-
rescue StandardError => e
|
97
|
-
logger.debug "** Error for #{File.basename(file)}: #{e.message}"
|
98
|
-
end
|
99
|
-
end
|
100
|
-
'Complete'
|
101
|
-
end
|
102
|
-
# rubocop:enable Metrics/MethodLength
|
103
|
-
# rubocop:enable Metrics/AbcSize
|
104
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
105
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
106
|
-
end
|
107
|
-
end
|
@@ -1,182 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'tempfile'
|
4
|
-
require 'English' # see https://github.com/rubocop-hq/rubocop/issues/1747 (not #MAGA related)
|
5
|
-
|
6
|
-
module Assembly
|
7
|
-
class Image
|
8
|
-
# Creates jp2 derivatives
|
9
|
-
class Jp2Creator # rubocop:disable Metrics/ClassLength
|
10
|
-
# Create a JP2 file for the current image.
|
11
|
-
# Important note: this will not work for multipage TIFFs.
|
12
|
-
#
|
13
|
-
# @return [Assembly::Image] object containing the generated JP2 file
|
14
|
-
#
|
15
|
-
# @param [Assembly::Image] the image file
|
16
|
-
# @param [Hash] params Optional parameters specified as a hash, using symbols for options:
|
17
|
-
# * :output => path to the output JP2 file (default: mirrors the source file name and path, but with a .jp2 extension)
|
18
|
-
# * :overwrite => if set to false, an existing JP2 file with the same name won't be overwritten (default: false)
|
19
|
-
# * :tmp_folder => the temporary folder to use when creating the jp2 (default: '/tmp'); also used by imagemagick
|
20
|
-
#
|
21
|
-
# Example:
|
22
|
-
# source_img = Assembly::Image.new('/input/path_to_file.tif')
|
23
|
-
# derivative_img = source_img.create_jp2(:overwrite=>true)
|
24
|
-
# puts derivative_img.mimetype # 'image/jp2'
|
25
|
-
# puts derivative_image.path # '/input/path_to_file.jp2'
|
26
|
-
def self.create(image, params = {})
|
27
|
-
new(image, params).create
|
28
|
-
end
|
29
|
-
|
30
|
-
def initialize(image, params)
|
31
|
-
@image = image
|
32
|
-
@output_path = params.fetch(:output, image.jp2_filename)
|
33
|
-
@tmp_folder = params[:tmp_folder]
|
34
|
-
@overwrite = params[:overwrite]
|
35
|
-
@params = params
|
36
|
-
end
|
37
|
-
|
38
|
-
attr_reader :image, :output_path, :tmp_folder, :tmp_path
|
39
|
-
|
40
|
-
# @return [Assembly::Image] object containing the generated JP2 file
|
41
|
-
def create
|
42
|
-
create_jp2_checks
|
43
|
-
|
44
|
-
# Using instance variable so that can check in tests.
|
45
|
-
@tmp_path = make_tmp_tiff(tmp_folder: tmp_folder)
|
46
|
-
|
47
|
-
jp2_command = jp2_create_command(source_path: @tmp_path, output: output_path)
|
48
|
-
result = `#{jp2_command}`
|
49
|
-
unless $CHILD_STATUS.success?
|
50
|
-
# Clean up any partial result
|
51
|
-
File.delete(output_path) if File.exist?(output_path)
|
52
|
-
raise "JP2 creation command failed: #{jp2_command} with result #{result}"
|
53
|
-
end
|
54
|
-
|
55
|
-
File.delete(@tmp_path) unless @tmp_path.nil?
|
56
|
-
|
57
|
-
# create output response object, which is an Assembly::Image type object
|
58
|
-
Image.new(output_path)
|
59
|
-
end
|
60
|
-
|
61
|
-
private
|
62
|
-
|
63
|
-
def overwrite?
|
64
|
-
@overwrite
|
65
|
-
end
|
66
|
-
|
67
|
-
def jp2_create_command(source_path:, output:)
|
68
|
-
options = []
|
69
|
-
options << '-jp2_space sRGB' if image.samples_per_pixel == 3
|
70
|
-
options += KDU_COMPRESS_DEFAULT_OPTIONS
|
71
|
-
options << "Clayers=#{layers}"
|
72
|
-
"kdu_compress #{options.join(' ')} -i '#{source_path}' -o '#{output}' 2>&1"
|
73
|
-
end
|
74
|
-
|
75
|
-
# Get the number of JP2 layers to generate
|
76
|
-
def layers
|
77
|
-
pixdem = [image.width, image.height].max
|
78
|
-
((Math.log(pixdem) / Math.log(2)) - (Math.log(96) / Math.log(2))).ceil + 1
|
79
|
-
end
|
80
|
-
|
81
|
-
KDU_COMPRESS_DEFAULT_OPTIONS = [
|
82
|
-
'-num_threads 2', # forces Kakadu to only use 2 threads
|
83
|
-
'-precise', # forces the use of 32-bit representations
|
84
|
-
'-no_weights', # minimization of the MSE over all reconstructed colour components
|
85
|
-
'-quiet', # suppress informative messages.
|
86
|
-
'Creversible=no', # Disable reversible compression
|
87
|
-
'Cmodes=BYPASS', #
|
88
|
-
'Corder=RPCL', # R=resolution P=position C=component L=layer
|
89
|
-
'Cblk=\\{64,64\\}', # code-block dimensions; 64x64 happens to also be the default
|
90
|
-
'Cprecincts=\\{256,256\\},\\{256,256\\},\\{128,128\\}', # Precinct dimensions; 256x256 for the 2 highest resolution levels, defaults to 128x128 for the rest
|
91
|
-
'ORGgen_plt=yes', # Insert packet length information
|
92
|
-
'-rate 1.5', # Ratio of compressed bits to the image size
|
93
|
-
'Clevels=5' # Number of wavelet decomposition levels, or stages
|
94
|
-
].freeze
|
95
|
-
|
96
|
-
# rubocop:disable Metrics/AbcSize
|
97
|
-
def create_jp2_checks
|
98
|
-
image.send(:check_for_file)
|
99
|
-
raise 'input file is not a valid image, or is the wrong mimetype' unless image.jp2able?
|
100
|
-
|
101
|
-
raise SecurityError, "output #{output_path} exists, cannot overwrite" if !overwrite? && File.exist?(output_path)
|
102
|
-
raise SecurityError, 'cannot recreate jp2 over itself' if overwrite? && image.mimetype == 'image/jp2' && output_path == image.path
|
103
|
-
end
|
104
|
-
|
105
|
-
# rubocop:disable Metrics/MethodLength
|
106
|
-
def profile_conversion_switch(profile, tmp_folder:)
|
107
|
-
path_to_profiles = File.join(Assembly::PATH_TO_IMAGE_GEM, 'profiles')
|
108
|
-
# eventually we may allow the user to specify the output_profile...when we do, you can just uncomment this code
|
109
|
-
# and update the tests that check for this
|
110
|
-
output_profile = 'sRGBIEC6196621' # params[:output_profile] || 'sRGBIEC6196621'
|
111
|
-
output_profile_file = File.join(path_to_profiles, "#{output_profile}.icc")
|
112
|
-
|
113
|
-
raise "output profile #{output_profile} invalid" unless File.exist?(output_profile_file)
|
114
|
-
|
115
|
-
return '' if image.profile.nil?
|
116
|
-
|
117
|
-
# if the input color profile exists, contract paths to the profile and setup the command
|
118
|
-
|
119
|
-
input_profile = profile.gsub(/[^[:alnum:]]/, '') # remove all non alpha-numeric characters, so we can get to a filename
|
120
|
-
|
121
|
-
# construct a path to the input profile, which might exist either in the gem itself or in the tmp folder
|
122
|
-
input_profile_file_gem = File.join(path_to_profiles, "#{input_profile}.icc")
|
123
|
-
input_profile_file_tmp = File.join(tmp_folder, "#{input_profile}.icc")
|
124
|
-
input_profile_file = File.exist?(input_profile_file_gem) ? input_profile_file_gem : input_profile_file_tmp
|
125
|
-
|
126
|
-
# if input profile was extracted and does not matches an existing known profile either in the gem or in the tmp folder,
|
127
|
-
# we'll issue an imagicmagick command to extract the profile to the tmp folder
|
128
|
-
unless File.exist?(input_profile_file)
|
129
|
-
input_profile_extract_command = "MAGICK_TEMPORARY_PATH=#{tmp_folder} convert '#{image.path}'[0] #{input_profile_file}" # extract profile from input image
|
130
|
-
result = `#{input_profile_extract_command} 2>&1`
|
131
|
-
raise "input profile extraction command failed: #{input_profile_extract_command} with result #{result}" unless $CHILD_STATUS.success?
|
132
|
-
# if extraction failed or we cannot write the file, throw exception
|
133
|
-
raise 'input profile is not a known profile and could not be extracted from input file' unless File.exist?(input_profile_file)
|
134
|
-
end
|
135
|
-
|
136
|
-
"-profile #{input_profile_file} -profile #{output_profile_file}"
|
137
|
-
end
|
138
|
-
|
139
|
-
# Bigtiff needs to be used if size of image exceeds 2^32 bytes.
|
140
|
-
def need_bigtiff?
|
141
|
-
image.image_data_size >= 2**32
|
142
|
-
end
|
143
|
-
|
144
|
-
def make_tmp_tiff(tmp_folder: nil)
|
145
|
-
tmp_folder ||= Dir.tmpdir
|
146
|
-
raise "tmp_folder #{tmp_folder} does not exists" unless File.exist?(tmp_folder)
|
147
|
-
|
148
|
-
# make temp tiff filename
|
149
|
-
tmp_tiff_file = Tempfile.new(['assembly-image', '.tif'], tmp_folder)
|
150
|
-
tmp_path = tmp_tiff_file.path
|
151
|
-
|
152
|
-
options = []
|
153
|
-
|
154
|
-
# Limit the amount of memory ImageMagick is able to use.
|
155
|
-
options << '-limit memory 1GiB -limit map 1GiB'
|
156
|
-
|
157
|
-
case image.samples_per_pixel
|
158
|
-
when 3
|
159
|
-
options << '-type TrueColor'
|
160
|
-
when 1
|
161
|
-
options << '-depth 8' # force the production of a grayscale access derivative
|
162
|
-
options << '-type Grayscale'
|
163
|
-
end
|
164
|
-
|
165
|
-
options << profile_conversion_switch(image.profile, tmp_folder: tmp_folder)
|
166
|
-
|
167
|
-
# The output in the covnert command needs to be prefixed by the image type. By default ImageMagick
|
168
|
-
# will assume TIFF: when the file extension is .tif/.tiff. TIFF64: Needs to be forced when image will
|
169
|
-
# exceed 2^32 bytes in size
|
170
|
-
tiff_type = need_bigtiff? ? 'TIFF64:' : ''
|
171
|
-
|
172
|
-
tiff_command = "MAGICK_TEMPORARY_PATH=#{tmp_folder} convert -quiet -compress none #{options.join(' ')} '#{image.path}[0]' #{tiff_type}'#{tmp_path}'"
|
173
|
-
result = `#{tiff_command} 2>&1`
|
174
|
-
raise "tiff convert command failed: #{tiff_command} with result #{result}" unless $CHILD_STATUS.success?
|
175
|
-
|
176
|
-
tmp_path
|
177
|
-
end
|
178
|
-
# rubocop:enable Metrics/MethodLength
|
179
|
-
# rubocop:enable Metrics/AbcSize
|
180
|
-
end
|
181
|
-
end
|
182
|
-
end
|