diff --git a/CHANGELOG.md b/CHANGELOG.md index 53b6a30..6aa777a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added - Vips image processor (resizing) - Vips colour filters +- Vips crop ### Removed - [BREAKING] dropped support for a broken 'dominant' border colour diff --git a/lib/morandi/crop_utils.rb b/lib/morandi/crop_utils.rb index c30439c..e2bc2ac 100644 --- a/lib/morandi/crop_utils.rb +++ b/lib/morandi/crop_utils.rb @@ -91,5 +91,39 @@ def apply_crop(pixbuf, x_coord, y_coord, width, height, fill_col = 0xffffffff) end pixbuf end + + def apply_crop_vips(img, x_coord, y_coord, width, height) + if x_coord.negative? || + y_coord.negative? || + ((x_coord + width) > img.width) || + ((y_coord + height) > img.height) + + extract_area_x = [0, x_coord].max + extract_area_y = [0, y_coord].max + area_to_copy = img.extract_area(extract_area_x, extract_area_y, img.width - extract_area_x, + img.height - extract_area_y) + + fill_colour = [255, 255, 255] + pixel = (Vips::Image.black(1, 1).colourspace(:srgb) + fill_colour).cast(img.format) + canvas = pixel.embed 0, 0, width, height, extend: :copy + + cropped = canvas.composite(area_to_copy, :over, x: [-x_coord, 0].max, + y: [-y_coord, 0].max, + compositing_space: area_to_copy.interpretation) + + # Because image is drawn on an opaque white, alpha doesn't matter at this point anyway, so let's strip the + # alpha channel from the output. According to #composite docs, the resulting image always has alpha channel, + # but I added a guard to avoid regressions if that ever changes. + cropped = cropped.extract_band(0, n: cropped.bands - 1) if cropped.has_alpha? + cropped + else + x_coord = x_coord.clamp(0, img.width) + y_coord = y_coord.clamp(0, img.height) + width = width.clamp(1, img.width - x_coord) + height = height.clamp(1, img.height - y_coord) + + img.crop(x_coord, y_coord, width, height) + end + end end end diff --git a/lib/morandi/vips_image_processor.rb b/lib/morandi/vips_image_processor.rb index 684ca5a..fe7fbad 100644 --- a/lib/morandi/vips_image_processor.rb +++ b/lib/morandi/vips_image_processor.rb @@ -60,6 +60,7 @@ def process! @scale = 1.0 end + apply_crop! apply_filters! return unless @options['output.limit'] && @output_width && @output_height @@ -82,6 +83,24 @@ def write_to_jpeg(write_to, quality = nil) private + def apply_crop! + crop = @options['crop'] + + return if crop.nil? && @options['image.auto-crop'].eql?(false) + + crop = crop.split(',').map(&:to_i) if crop.is_a?(String) && crop =~ /^\d+,\d+,\d+,\d+/ + + crop = nil unless crop.is_a?(Array) && crop.size.eql?(4) && crop.all? do |i| + i.is_a?(Numeric) + end + # can't crop, won't crop + return if @output_width.nil? && @output_height.nil? && crop.nil? + + crop = crop.map { |s| (s.to_f * @scale).floor } if crop && not_equal_to_one(@scale) + crop ||= Morandi::CropUtils.autocrop_coords(@img.width, @img.height, @output_width, @output_height) + @img = Morandi::CropUtils.apply_crop_vips(@img, crop[0], crop[1], crop[2], crop[3]) + end + def apply_filters! filter_name = @options['fx'] return unless SUPPORTED_FILTERS.include?(filter_name) diff --git a/spec/fixtures/reference_images/vips/plasma-auto-cropped.jpg b/spec/fixtures/reference_images/vips/plasma-auto-cropped.jpg new file mode 100644 index 0000000..b74ed8f Binary files /dev/null and b/spec/fixtures/reference_images/vips/plasma-auto-cropped.jpg differ diff --git a/spec/fixtures/reference_images/vips/plasma-cropped-1x1.jpg b/spec/fixtures/reference_images/vips/plasma-cropped-1x1.jpg new file mode 100644 index 0000000..42e2ad0 Binary files /dev/null and b/spec/fixtures/reference_images/vips/plasma-cropped-1x1.jpg differ diff --git a/spec/fixtures/reference_images/vips/plasma-cropped-excessive-size.jpg b/spec/fixtures/reference_images/vips/plasma-cropped-excessive-size.jpg new file mode 100644 index 0000000..9404e65 Binary files /dev/null and b/spec/fixtures/reference_images/vips/plasma-cropped-excessive-size.jpg differ diff --git a/spec/fixtures/reference_images/vips/plasma-cropped-negative-initial-coords.jpg b/spec/fixtures/reference_images/vips/plasma-cropped-negative-initial-coords.jpg new file mode 100644 index 0000000..9fc8c72 Binary files /dev/null and b/spec/fixtures/reference_images/vips/plasma-cropped-negative-initial-coords.jpg differ diff --git a/spec/fixtures/reference_images/vips/plasma-cropped.jpg b/spec/fixtures/reference_images/vips/plasma-cropped.jpg new file mode 100644 index 0000000..72c4ecc Binary files /dev/null and b/spec/fixtures/reference_images/vips/plasma-cropped.jpg differ diff --git a/spec/morandi_spec.rb b/spec/morandi_spec.rb index 4b0d4ae..e0f88be 100644 --- a/spec/morandi_spec.rb +++ b/spec/morandi_spec.rb @@ -87,7 +87,7 @@ end end - describe 'with a big image and a bigger cropped area to fill', vips_wip: processor_name == 'vips' do + describe 'with a big image and a bigger cropped area to fill' do let(:options) do { 'crop' => '0,477,15839,18804', @@ -148,7 +148,7 @@ end end - context 'when give a "crop" option', vips_wip: processor_name == 'vips' do + context 'when give a "crop" option' do let(:cropped_width) { 300 } let(:cropped_height) { 300 } @@ -162,7 +162,7 @@ expect(processed_image_width).to eq(cropped_width) expect(processed_image_height).to eq(cropped_height) - expect(file_out).to match_reference_image('plasma-cropped') + expect(file_out).to match_reference_image(reference_image_prefix, 'plasma-cropped') end end @@ -176,7 +176,7 @@ expect(processed_image_width).to eq(cropped_width) expect(processed_image_height).to eq(cropped_height) - expect(file_out).to match_reference_image('plasma-cropped') + expect(file_out).to match_reference_image(reference_image_prefix, 'plasma-cropped') end end @@ -190,7 +190,7 @@ expect(processed_image_width).to eq(cropped_width) expect(processed_image_height).to eq(cropped_height) - expect(file_out).to match_reference_image('plasma-cropped-negative-initial-coords') + expect(file_out).to match_reference_image(reference_image_prefix, 'plasma-cropped-negative-initial-coords') end end @@ -204,7 +204,7 @@ expect(processed_image_width).to eq(original_image_width + 50) expect(processed_image_height).to eq(original_image_height + 50) - expect(file_out).to match_reference_image('plasma-cropped-excessive-size') + expect(file_out).to match_reference_image(reference_image_prefix, 'plasma-cropped-excessive-size') end end @@ -218,7 +218,7 @@ expect(processed_image_width).to eq(1) expect(processed_image_height).to eq(1) - expect(file_out).to match_reference_image('plasma-cropped-1x1') + expect(file_out).to match_reference_image(reference_image_prefix, 'plasma-cropped-1x1') end end end @@ -332,7 +332,7 @@ end end - describe 'when changing the dimensions and auto-cropping', vips_wip: processor_name == 'vips' do + describe 'when changing the dimensions and auto-cropping' do let(:max_width) { 300 } let(:max_height) { 200 } @@ -353,7 +353,7 @@ expect(processed_image_width).to be <= max_width expect(processed_image_height).to be <= max_height - expect(file_out).to match_reference_image('plasma-auto-cropped') + expect(file_out).to match_reference_image(reference_image_prefix, 'plasma-auto-cropped') end end