diff --git a/CHANGELOG.md b/CHANGELOG.md index d98d119..53b6a30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## UNRELEASED ### Added -- Vips image processor +- Vips image processor (resizing) +- Vips colour filters ### Removed - [BREAKING] dropped support for a broken 'dominant' border colour diff --git a/lib/morandi/vips_image_processor.rb b/lib/morandi/vips_image_processor.rb index 03b92e2..684ca5a 100644 --- a/lib/morandi/vips_image_processor.rb +++ b/lib/morandi/vips_image_processor.rb @@ -7,6 +7,16 @@ module Morandi # An alternative to ImageProcessor which is based on libvips for concurrent and less memory-intensive processing class VipsImageProcessor + # Colour filter related constants + RGB_LUMINANCE_EXTRACTION_FACTORS = [0.3086, 0.6094, 0.0820].freeze + SEPIA_MODIFIER = [25, 5, -25].freeze + BLUETONE_MODIFIER = [-10, 5, 25].freeze + COLOUR_FILTER_MODIFIERS = { + 'sepia' => SEPIA_MODIFIER, + 'bluetone' => BLUETONE_MODIFIER + }.freeze + SUPPORTED_FILTERS = COLOUR_FILTER_MODIFIERS.keys + ['greyscale'] + # Vips options are global, this method sets them for yielding, then restores to original def self.with_global_options(cache_max:, concurrency:) previous_cache_max = Vips.cache_max @@ -50,6 +60,8 @@ def process! @scale = 1.0 end + apply_filters! + return unless @options['output.limit'] && @output_width && @output_height scale_factor = [@output_width, @output_height].max.to_f / [@img.width, @img.height].max @@ -70,6 +82,32 @@ def write_to_jpeg(write_to, quality = nil) private + def apply_filters! + filter_name = @options['fx'] + return unless SUPPORTED_FILTERS.include?(filter_name) + + # The filter-related constants assume RGB colourspace, so it requires conversion + @img = @img.colourspace(:srgb) unless @img.interpretation == :srgb + + # Convert to greyscale using weights + rgb_factors = RGB_LUMINANCE_EXTRACTION_FACTORS + recombination_matrix = [rgb_factors, rgb_factors, rgb_factors] + if @img.has_alpha? + # Add "0" multiplier for alpha to ignore it for luminance calculation + recombination_matrix = recombination_matrix.map { |channel_multipliers| channel_multipliers + [0] } + # Add fourth row in the matrix to preserve unchanged alpha channel + recombination_matrix << [0, 0, 0, 1] + end + @img = @img.recomb(recombination_matrix) + + return unless COLOUR_FILTER_MODIFIERS[filter_name] + + # Apply colour adjustment based on the modifiers setup + colour_filter_modifier = COLOUR_FILTER_MODIFIERS[filter_name] + colour_filter_modifier += [0] if @img.has_alpha? + @img = @img.linear(1.0, colour_filter_modifier) + end + def not_equal_to_one(float) (float - 1.0).abs >= Float::EPSILON end diff --git a/spec/fixtures/reference_images/vips/greyscale-with-sepia.jpg b/spec/fixtures/reference_images/vips/greyscale-with-sepia.jpg new file mode 100644 index 0000000..e1a0d20 Binary files /dev/null and b/spec/fixtures/reference_images/vips/greyscale-with-sepia.jpg differ diff --git a/spec/fixtures/reference_images/vips/plasma-bluetone.jpg b/spec/fixtures/reference_images/vips/plasma-bluetone.jpg new file mode 100644 index 0000000..fd7b493 Binary files /dev/null and b/spec/fixtures/reference_images/vips/plasma-bluetone.jpg differ diff --git a/spec/fixtures/reference_images/vips/plasma-greyscale.jpg b/spec/fixtures/reference_images/vips/plasma-greyscale.jpg new file mode 100644 index 0000000..9fa8c77 Binary files /dev/null and b/spec/fixtures/reference_images/vips/plasma-greyscale.jpg differ diff --git a/spec/fixtures/reference_images/vips/plasma-sepia.jpg b/spec/fixtures/reference_images/vips/plasma-sepia.jpg new file mode 100644 index 0000000..68fdeaa Binary files /dev/null and b/spec/fixtures/reference_images/vips/plasma-sepia.jpg differ diff --git a/spec/morandi_spec.rb b/spec/morandi_spec.rb index b6b9667..4b0d4ae 100644 --- a/spec/morandi_spec.rb +++ b/spec/morandi_spec.rb @@ -292,7 +292,7 @@ end end - describe 'when given an fx option', vips_wip: processor_name == 'vips' do + describe 'when given an fx option' do let(:options) { { 'fx' => filter_name } } context 'with sepia' do @@ -303,7 +303,7 @@ expect(File).to exist(file_out) expect(processed_image_type).to eq('jpeg') - expect(file_out).to match_reference_image('plasma-sepia') + expect(file_out).to match_reference_image(reference_image_prefix, 'plasma-sepia') end end @@ -315,7 +315,7 @@ expect(File).to exist(file_out) expect(processed_image_type).to eq('jpeg') - expect(file_out).to match_reference_image('plasma-bluetone') + expect(file_out).to match_reference_image(reference_image_prefix, 'plasma-bluetone') end end @@ -327,7 +327,7 @@ expect(File).to exist(file_out) expect(processed_image_type).to eq('jpeg') - expect(file_out).to match_reference_image('plasma-greyscale') + expect(file_out).to match_reference_image(reference_image_prefix, 'plasma-greyscale') end end end @@ -465,12 +465,12 @@ end end - context 'with a non-rgb image', vips_wip: processor_name == 'vips' do + context 'with a non-rgb image' do let(:generate_image) do generate_test_image_greyscale(file_in, width: original_image_width, height: original_image_height) end - it 'changes greyscale image to srgb' do + it 'changes greyscale image to srgb', vips_wip: processor_name == 'vips' do expect(file_in).to match_colourspace('gray') # Testing a setup to protect from a hidden regression process_image @@ -484,7 +484,7 @@ it 'creates a valid, srgb image' do process_image - expect(file_out).to match_reference_image('greyscale-with-sepia') + expect(file_out).to match_reference_image(reference_image_prefix, 'greyscale-with-sepia') expect(file_out).to match_colourspace('srgb') end end