Skip to content

Commit

Permalink
feat: introduce vips image processor
Browse files Browse the repository at this point in the history
For simplicity of implementation, ruby-vips gem is required even when not using the new processor

At the beginning, only colour profile conversion and size-related restrictions are implemented,
the other operations will be introduced iteratively
  • Loading branch information
knarewski committed Nov 18, 2024
1 parent 37c20e6 commit 6578510
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 18 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## UNRELEASED
### Added
- Vips image processor

## [0.99.4] 18.11.2024
### Added
- Better test coverage for straighten operation
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ RUN apt-get update && apt-get install -yyq --no-install-recommends \
libgirepository1.0-dev \
# At the time of writing, "time" package is only required for benchmark
time \
libvips \
&& apt-get clean \
&& rm -rf /va/lib/apt/lists/*

Expand Down
5 changes: 5 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ PATH
gdk_pixbuf2 (~> 4.0)
pango (~> 4.0)
rake-compiler (~> 1.2)
ruby-vips

GEM
remote: https://rubygems.org/
Expand Down Expand Up @@ -61,6 +62,7 @@ GEM
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
logger (1.6.1)
lumberjack (1.2.10)
matrix (0.4.2)
method_source (1.1.0)
Expand Down Expand Up @@ -121,6 +123,9 @@ GEM
rubocop-ast (1.32.3)
parser (>= 3.3.1.0)
ruby-progressbar (1.13.0)
ruby-vips (2.2.2)
ffi (~> 1.12)
logger
shellany (0.0.1)
simplecov (0.22.0)
docile (~> 1.1)
Expand Down
18 changes: 15 additions & 3 deletions lib/morandi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
require 'morandi/pixbuf_ext'
require 'morandi/errors'
require 'morandi/image_processor'
require 'morandi/vips_image_processor'
require 'morandi/redeye'
require 'morandi/crop_utils'

Expand Down Expand Up @@ -42,9 +43,20 @@ module Morandi
# @param target_path [String] target location for image
# @param local_options [Hash] Hash of options other than desired transformations
# @option local_options [String] 'path.icc' A path to store the input after converting to sRGB colour space
# @options local_options [String] 'processor' ('pixbuf') Name of the image processing library ('pixbuf', 'vips')
def process(source, options, target_path, local_options = {})
pro = ImageProcessor.new(source, options, local_options)
pro.result
pro.write_to_jpeg(target_path)
case local_options['processor']
when 'vips'
# Cache saves time in expense of RAM when performing the same processing multiple times
# Cache is also created for files based on their names, which can lead to leaking files data, so in terms
# of security it feels prudent to disable it. Latest libvips supports "revalidate" option to prevent that risk
cache_max = 0
concurrency = 2 # Hardcoding to 2 for now to maintain some balance between resource usage and performance
VipsImageProcessor.with_global_options(cache_max: cache_max, concurrency: concurrency) do
VipsImageProcessor.new(source, options).write_to_jpeg(target_path)
end
else
ImageProcessor.new(source, options, local_options).tap(&:result).write_to_jpeg(target_path)
end
end
end
77 changes: 77 additions & 0 deletions lib/morandi/vips_image_processor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# frozen_string_literal: true

require 'vips'

require 'morandi/srgb_conversion'

module Morandi
# An alternative to ImageProcessor which is based on libvips for faster and less resource-intensive processing
class VipsImageProcessor
# 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
previous_concurrency = Vips.concurrency

Vips.cache_set_max(cache_max)
Vips.concurrency_set(concurrency)

yield
ensure
Vips.cache_set_max(previous_cache_max)
Vips.concurrency_set(previous_concurrency)
end

def initialize(path, user_options)
@path = path

@options = user_options

@size_limit_on_load_px = @options['output.max']
@output_width = @options['output.width']
@output_height = @options['output.height']
end

def process!
source_file_path = Morandi::SrgbConversion.perform(@path) || @path
begin
@img = Vips::Image.new_from_file(source_file_path)
rescue Vips::Error => e
# Match the known errors
raise UnknownTypeError if /is not a known file format/.match?(e.message)
raise CorruptImageError if /Premature end of JPEG file/.match?(e.message)

# Re-raise generic Error when unknown
raise Error, e.message
end
if @size_limit_on_load_px
@scale = @size_limit_on_load_px.to_f / [@img.width, @img.height].max
@img = @img.resize(@scale) if not_equal_to_one(@scale)
else
@scale = 1.0
end

return unless @options['output.limit'] && @output_width && @output_height

scale_factor = [@output_width, @output_height].max.to_f / [@img.width, @img.height].max
@img = @img.resize(scale_factor) if scale_factor < 1.0
end

def write_to_png(_write_to, _orientation = :any)
raise 'not implemented'
end

def write_to_jpeg(write_to, quality = nil)
process!

quality ||= @options.fetch('quality', 97)

@img.write_to_file(write_to, Q: quality)
end

private

def not_equal_to_one(float)
(float - 1.0).abs >= Float::EPSILON
end
end
end
1 change: 1 addition & 0 deletions morandi.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ Gem::Specification.new do |spec|
spec.add_dependency 'gdk_pixbuf2', '~> 4.0'
spec.add_dependency 'pango', '~> 4.0'
spec.add_dependency 'rake-compiler', '~> 1.2'
spec.add_dependency 'ruby-vips'
end
34 changes: 19 additions & 15 deletions spec/morandi_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
let(:reference_image_prefix) { processor_name == 'pixbuf' ? '' : processor_name }
subject(:process_image) { Morandi.process(file_arg, options, file_out, { 'processor' => processor_name }) }

describe 'when given an input without any options' do
describe 'when given an input without any options', vips_wip: processor_name == 'vips' do
it 'creates output' do
process_image
expect(File).to exist(file_out)
Expand Down Expand Up @@ -87,7 +87,7 @@
end
end

describe 'with a big image and a bigger cropped area to fill' do
describe 'with a big image and a bigger cropped area to fill', vips_wip: processor_name == 'vips' do
let(:options) do
{
'crop' => '0,477,15839,18804',
Expand All @@ -111,7 +111,7 @@
describe 'when given an angle of rotation' do
let(:options) { { 'angle' => angle } }

context '90 degress' do
context '90 degress', vips_wip: processor_name == 'vips' do
let(:angle) { 90 }

it 'rotates the image' do
Expand All @@ -120,7 +120,7 @@
end
end

context '180 degress' do
context '180 degress', vips_wip: processor_name == 'vips' do
let(:angle) { 180 }

it 'rotates the image' do
Expand All @@ -129,7 +129,7 @@
end
end

context '270 degress' do
context '270 degress', vips_wip: processor_name == 'vips' do
let(:angle) { 270 }

it 'rotates the image' do
Expand All @@ -138,7 +138,7 @@
end
end

context '360 degress' do
context '360 degress', vips_wip: processor_name == 'vips' do
let(:angle) { 360 }

it 'does not perform any rotation' do
Expand All @@ -148,7 +148,7 @@
end
end

context 'when give a "crop" option' do
context 'when give a "crop" option', vips_wip: processor_name == 'vips' do
let(:cropped_width) { 300 }
let(:cropped_height) { 300 }

Expand Down Expand Up @@ -223,7 +223,7 @@
end
end

describe 'when given an output.max option' do
describe 'when given an output.max option', vips_wip: processor_name == 'vips' do
let(:options) { { 'output.max' => max_size } }
let(:max_size) { 200 }

Expand All @@ -238,7 +238,7 @@
end
end

describe 'when given a straighten option' do
describe 'when given a straighten option', vips_wip: processor_name == 'vips' do
let(:options) { { 'straighten' => 5 } }

it 'straightens images' do
Expand Down Expand Up @@ -278,7 +278,7 @@
end
end

describe 'when given a gamma option' do
describe 'when given a gamma option', vips_wip: processor_name == 'vips' do
let(:options) { { 'gamma' => 2.0 } }

it 'should apply the gamma to the image' do
Expand All @@ -291,7 +291,7 @@
end
end

describe 'when given an fx option' do
describe 'when given an fx option', vips_wip: processor_name == 'vips' do
let(:options) { { 'fx' => filter_name } }

context 'with sepia' do
Expand Down Expand Up @@ -331,7 +331,7 @@
end
end

describe 'when changing the dimensions and auto-cropping' do
describe 'when changing the dimensions and auto-cropping', vips_wip: processor_name == 'vips' do
let(:max_width) { 300 }
let(:max_height) { 200 }

Expand Down Expand Up @@ -389,7 +389,7 @@
end
end

context 'with non-sRGB colour profile' do
context 'with non-sRGB colour profile', vips_wip: processor_name == 'vips' do
let(:file_in) { 'spec/fixtures/pumpkins-icc-adobe-rgb-1998.jpg' }

it 'converts the profile to sRGB' do
Expand Down Expand Up @@ -431,7 +431,7 @@
end
end

context 'with transparent png input' do
context 'with transparent png input', vips_wip: processor_name == 'vips' do
let(:file_in) { 'spec/fixtures/match-with-transparency.png' }
let(:options) do
{
Expand Down Expand Up @@ -463,7 +463,7 @@
end
end

context 'with a non-rgb image' do
context 'with a non-rgb image', vips_wip: processor_name == 'vips' do
let(:generate_image) do
generate_test_image_greyscale(file_in, width: original_image_width, height: original_image_height)
end
Expand Down Expand Up @@ -751,4 +751,8 @@
end
end
end

context 'vips processor' do
it_behaves_like 'an image processor', 'vips'
end
end
4 changes: 4 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@
puts "Visual report is available here: #{VisualReportHelper.visual_report_path}"
puts 'Coverage report is here: coverage/index.html'
end

config.before(:each, vips_wip: true) do
pending('Vips WIP')
end
end

RSpec::Matchers.define :be_redish do
Expand Down

0 comments on commit 6578510

Please sign in to comment.