assembly-image 1.7.7 → 1.7.8

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.
@@ -0,0 +1,182 @@
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
@@ -4,7 +4,7 @@
4
4
  module Assembly
5
5
  # Main Image class
6
6
  class Image
7
- # Project version number
8
- VERSION = '1.7.7'
7
+ # Gem version
8
+ VERSION = '1.7.8'
9
9
  end
10
10
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Assembly::Image::Jp2Creator do
6
+ subject(:result) { creator.create }
7
+
8
+ let(:ai) { Assembly::Image.new(input_path) }
9
+ let(:input_path) { TEST_TIF_INPUT_FILE }
10
+ let(:creator) { described_class.new(ai, output: TEST_JP2_OUTPUT_FILE) }
11
+
12
+ after do
13
+ # after each test, empty out the input and output test directories
14
+ remove_files(TEST_INPUT_DIR)
15
+ remove_files(TEST_OUTPUT_DIR)
16
+ end
17
+
18
+ context 'when given an LZW compressed RGB tif' do
19
+ before do
20
+ generate_test_image(TEST_TIF_INPUT_FILE, compress: 'lzw')
21
+ end
22
+
23
+ it 'creates the jp2 with a temp file' do
24
+ expect(File).to exist TEST_TIF_INPUT_FILE
25
+ expect(File).not_to exist TEST_JP2_OUTPUT_FILE
26
+ expect(result).to be_a_kind_of Assembly::Image
27
+ expect(result.path).to eq TEST_JP2_OUTPUT_FILE
28
+ expect(TEST_JP2_OUTPUT_FILE).to be_a_jp2
29
+
30
+ # Indicates a temp tiff was not created.
31
+ expect(creator.tmp_path).not_to be_nil
32
+ expect(result.exif.colorspace).to eq 'sRGB'
33
+ jp2 = Assembly::Image.new(TEST_JP2_OUTPUT_FILE)
34
+ expect(jp2.height).to eq 100
35
+ expect(jp2.width).to eq 100
36
+ end
37
+ end
38
+
39
+ context 'when the input file is a JPEG' do
40
+ before do
41
+ generate_test_image(TEST_JPEG_INPUT_FILE)
42
+ end
43
+
44
+ let(:input_path) { TEST_JPEG_INPUT_FILE }
45
+
46
+ it 'creates jp2 when given a JPEG' do
47
+ expect(File).to exist TEST_JPEG_INPUT_FILE
48
+ expect(File).not_to exist TEST_JP2_OUTPUT_FILE
49
+ expect(result).to be_a_kind_of Assembly::Image
50
+ expect(result.path).to eq TEST_JP2_OUTPUT_FILE
51
+ expect(TEST_JP2_OUTPUT_FILE).to be_a_jp2
52
+
53
+ # Indicates a temp tiff was created.
54
+ expect(creator.tmp_path).not_to be_nil
55
+ expect(File).not_to exist creator.tmp_path
56
+ end
57
+ end
58
+ end