From 368e5d38f741cd6546f593134b1548dc3f83bde8 Mon Sep 17 00:00:00 2001 From: Matthias Grosser Date: Fri, 23 Apr 2021 09:46:56 +0200 Subject: [PATCH] Image orientation #10 --- .gitignore | 2 + .ruby-version | 2 +- Gemfile.lock | 4 +- ext/rszr/extconf.rb | 7 ++ ext/rszr/image.c | 71 ++++++++++++-- ext/rszr/rszr.h | 1 + lib/rszr.rb | 14 +++ lib/rszr/batch_transformation.rb | 24 +++++ lib/rszr/image.rb | 155 ++++++++++++++++++++++--------- lib/rszr/image_processing.rb | 2 +- rszr.gemspec | 4 +- spec/gc_threading_spec.rb | 52 +++++++++++ spec/rszr_spec.rb | 60 +++--------- 13 files changed, 293 insertions(+), 105 deletions(-) create mode 100644 lib/rszr/batch_transformation.rb create mode 100644 spec/gc_threading_spec.rb diff --git a/.gitignore b/.gitignore index f62fe40..d47318e 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ build/ /spec/examples.txt .byebug_history + +/lib/rszr/rszr.bundle diff --git a/.ruby-version b/.ruby-version index 2714f53..338a5b5 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.6.4 +2.6.6 diff --git a/Gemfile.lock b/Gemfile.lock index 95c5380..fc2ae59 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -19,7 +19,7 @@ GEM memory_profiler (0.9.12) mini_magick (4.9.5) minitest (5.11.3) - rake (10.5.0) + rake (13.0.1) rake-compiler (1.0.7) rake rspec (3.8.0) @@ -54,7 +54,7 @@ DEPENDENCIES memory_profiler mini_magick minitest - rake (~> 10.0) + rake (~> 13.0) rake-compiler rspec rszr! diff --git a/ext/rszr/extconf.rb b/ext/rszr/extconf.rb index 38a041b..52485b8 100644 --- a/ext/rszr/extconf.rb +++ b/ext/rszr/extconf.rb @@ -15,4 +15,11 @@ abort 'Imlib2 is missing' end +unless find_library('exif', 'exif_data_new_from_file') + abort 'libexif is missing' +end + +have_library('exif') +have_header('libexif/exif-data.h') + create_makefile 'rszr/rszr' diff --git a/ext/rszr/image.c b/ext/rszr/image.c index 089af70..f28f03a 100644 --- a/ext/rszr/image.c +++ b/ext/rszr/image.c @@ -28,6 +28,42 @@ static void rszr_image_deallocate(rszr_image_handle * handle) } +static void rszr_image_autorotate(Imlib_Image image, char * path) +{ + ExifData * exifData; + ExifByteOrder byteOrder; + ExifEntry * exifEntry; + int orientation = 0; + int turns = 0; + + exifData = exif_data_new_from_file(path); + + if (exifData) { + byteOrder = exif_data_get_byte_order(exifData); + exifEntry = exif_data_get_entry(exifData, EXIF_TAG_ORIENTATION); + if (exifEntry) { + orientation = exif_get_short(exifEntry->data, byteOrder); + } + } + + if (orientation < 2 || orientation > 8) return; + + imlib_context_set_image(image); + + if (orientation == 2 || orientation == 4 || orientation == 5 || orientation == 7) { + imlib_image_flip_horizontal(); + } + + if (orientation == 5 || orientation == 6) { + imlib_image_orientate(1); + } else if (orientation == 3 || orientation == 4) { + imlib_image_orientate(2); + } else if (orientation == 7 || orientation == 8) { + imlib_image_orientate(3); + } +} + + static VALUE rszr_image_s_allocate(VALUE klass) { rszr_image_handle * handle = calloc(1, sizeof(rszr_image_handle)); @@ -50,7 +86,7 @@ static VALUE rszr_image_initialize(VALUE self, VALUE rb_width, VALUE rb_height) } -static VALUE rszr_image_s__load(VALUE klass, VALUE rb_path) +static VALUE rszr_image_s__load(VALUE klass, VALUE rb_path, VALUE autorotate) { rszr_image_handle * handle; Imlib_Image image; @@ -68,6 +104,8 @@ static VALUE rszr_image_s__load(VALUE klass, VALUE rb_path) return Qnil; } + if (RTEST(autorotate)) rszr_image_autorotate(image, path); + oImage = rszr_image_s_allocate(cImage); Data_Get_Struct(oImage, rszr_image_handle, handle); handle->image = image; @@ -246,7 +284,24 @@ static VALUE rszr_image__rotate(VALUE self, VALUE bang, VALUE rb_angle) } } -/* + +static VALUE rszr_image_filter_bang(VALUE self, VALUE rb_filter_expr) +{ + rszr_image_handle * handle; + Imlib_Image image; + char * filter_expr; + + filter_expr = StringValueCStr(rb_filter_expr); + + Data_Get_Struct(self, rszr_image_handle, handle); + + imlib_context_set_image(handle->image); + imlib_apply_filter(filter_expr); + + return self; +} + + static VALUE rszr_image__brighten_bang(VALUE self, VALUE rb_brightness) { rszr_image_handle * handle; @@ -254,12 +309,14 @@ static VALUE rszr_image__brighten_bang(VALUE self, VALUE rb_brightness) brightness = NUM2DBL(rb_brightness); + Data_Get_Struct(self, rszr_image_handle, handle); + imlib_context_set_image(handle->image); - imlib_modify_color_modifier_brightness(brightness); + imlib_apply_filter("brightness(10);"); return self; } -*/ + static VALUE rszr_image__sharpen_bang(VALUE self, VALUE rb_radius) { @@ -419,7 +476,7 @@ void Init_rszr_image() rb_define_alloc_func(cImage, rszr_image_s_allocate); // Class methods - rb_define_private_method(rb_singleton_class(cImage), "_load", rszr_image_s__load, 1); + rb_define_private_method(rb_singleton_class(cImage), "_load", rszr_image_s__load, 2); // Instance methods rb_define_method(cImage, "initialize", rszr_image_initialize, 2); @@ -427,6 +484,8 @@ void Init_rszr_image() rb_define_method(cImage, "height", rszr_image_height, 0); rb_define_method(cImage, "format", rszr_image_format_get, 0); rb_define_method(cImage, "dup", rszr_image_dup, 0); + rb_define_method(cImage, "filter!", rszr_image_filter_bang, 1); + // rb_define_method(cImage, "quality", rszr_image_get_quality, 0); // rb_define_method(cImage, "quality=", rszr_image_set_quality, 1); @@ -437,7 +496,7 @@ void Init_rszr_image() rb_define_private_method(cImage, "_turn!", rszr_image__turn_bang, 1); rb_define_private_method(cImage, "_rotate", rszr_image__rotate, 2); rb_define_private_method(cImage, "_sharpen!", rszr_image__sharpen_bang, 1); - /* rb_define_private_method(cImage, "_brighten!", rszr_image__brighten_bang, 1); */ + rb_define_private_method(cImage, "_brighten!", rszr_image__brighten_bang, 1); rb_define_private_method(cImage, "_save", rszr_image__save, 3); } diff --git a/ext/rszr/rszr.h b/ext/rszr/rszr.h index 796945f..6fa190c 100644 --- a/ext/rszr/rszr.h +++ b/ext/rszr/rszr.h @@ -3,6 +3,7 @@ #include "ruby.h" #include +#include extern VALUE mRszr; void Init_rszr(); diff --git a/lib/rszr.rb b/lib/rszr.rb index a8afbfd..eeec219 100644 --- a/lib/rszr.rb +++ b/lib/rszr.rb @@ -4,3 +4,17 @@ require 'rszr/rszr' require 'rszr/version' require 'rszr/image' + +module Rszr + class << self + @@autorotate = nil + + def autorotate + @@autorotate + end + + def autorotate=(value) + @@autorotate = !!value + end + end +end diff --git a/lib/rszr/batch_transformation.rb b/lib/rszr/batch_transformation.rb new file mode 100644 index 0000000..9d600ca --- /dev/null +++ b/lib/rszr/batch_transformation.rb @@ -0,0 +1,24 @@ +module Rszr + class BatchTransformation + attr_reader :transformations, :image + + def initialize(path, **opts) + puts "INITIALIZED BATCH for #{path}" + @image = path.is_a?(Image) ? path : Image.load(path, **opts) + @transformations = [] + end + + Image::Transformations.instance_methods.grep(/\w\z/) do |method| + define_method method do |*args| + transformations << [method, args] + self + end + end + + def call + transformations.each { |method, args| image.public_send("#{method}!", *args) } + image + end + + end +end diff --git a/lib/rszr/image.rb b/lib/rszr/image.rb index 390f64a..307a104 100644 --- a/lib/rszr/image.rb +++ b/lib/rszr/image.rb @@ -1,12 +1,12 @@ module Rszr class Image - + class << self - def load(path, **opts) + def load(path, autorotate: Rszr.autorotate, **opts) path = path.to_s raise FileNotFound unless File.exist?(path) - _load(path) + _load(path, autorotate) end alias :open :load @@ -43,7 +43,7 @@ def crop(x, y, width, height) def crop!(x, y, width, height) _crop(true, x, y, width, height) end - + def turn(orientation) dup.turn!(orientation) end @@ -79,11 +79,36 @@ def blur!(radius) _sharpen!(-radius) end - # TODO - #def brighten!(brightness) - # raise ArgumentError, 'illegal brightness' if brightness > 1 || brightness < -1 - # _brighten!(brightness) - #end + def filter(filter_expr) + dup.filter!(filter_expr) + end + + def brighten!(value, r: nil, g: nil, b: nil, a: nil) + raise ArgumentError, 'illegal brightness' if value > 1 || value < -1 + filter!("colormod(brightness=#{value.to_f});") + end + + def brighten(*args) + dup.brighten!(*args) + end + + def contrast!(value, r: nil, g: nil, b: nil, a: nil) + raise ArgumentError, 'illegal contrast (must be > 0)' if value < 0 + filter!("colormod(contrast=#{value.to_f});") + end + + def contrast(*args) + dup.contrast!(*args) + end + + def gamma!(value, r: nil, g: nil, b: nil, a: nil) + #raise ArgumentError, 'illegal gamma (must be > 0)' if value < 0 + filter!("colormod(gamma=#{value.to_f});") + end + + def gamma(*args) + dup.gamma!(*args) + end end include Transformations @@ -96,7 +121,7 @@ def save(path, format: nil, quality: nil) private - # 0.5 0 < scale < 1 + # 0.5 scale > 0 # 400, 300 fit box # 400, :auto fit width, auto height # :auto, 300 auto width, fit height @@ -104,51 +129,91 @@ def save(path, format: nil, quality: nil) # 400, 300, background: rgba # 400, 300, skew: true - def calculate_size(*args) - options = args.last.is_a?(Hash) ? args.pop : {} - assert_valid_keys options, :crop, :background, :skew #:extend, :width, :height, :max_width, :max_height, :box - original_width, original_height = width, height - x, y, = 0, 0 - if args.size == 1 - scale = args.first - raise ArgumentError, "scale #{scale.inspect} out of range" unless scale > 0 && scale < 1 - new_width = original_width.to_f * scale - new_height = original_height.to_f * scale - elsif args.size == 2 + def calculate_size(*args, upsize: false, crop: nil, background: nil, skew: false) + case args.size + when 1 + scale_size(args.first) + when 2 box_width, box_height = args - if :auto == box_width && box_height.is_a?(Numeric) + if box_width.is_a?(Numeric) && box_height.is_a?(Numeric) + bounding_box_size(box_width, box_height, upsize: upsize, crop: crop, skew: skew) + else + auto_box_size(box_width, box_height, upsize: upsize) + end + else + raise ArgumentError, "wrong number of arguments (#{args.size} for 1..2)" + end + end + + def scale_size(factor) + raise ArgumentError, "scale factor #{factor.inspect} out of range" unless factor > 0 + [0, 0, width, height, (width.to_f * factor).round, (height.to_f * factor).round] + end + + def bounding_box_size(box_width, box_height, upsize: false, crop: nil, skew: false) + original_width, original_height = width, height + x = y = 0 + if skew + new_width, new_height = box_width, box_height + else + original_ratio = original_width.to_f / original_height.to_f + box_ratio = box_width.to_f / box_height.to_f + if original_ratio >= box_ratio # wider than box + if original_width < box_width and not upsize + new_width, new_height = original_width, original_height + elsif crop + new_width = box_width + new_height = original_height.to_f * box_ratio + # TODO x, y + else + new_width = box_width + new_height = box_height + x = (original_width - original_width * original_height / new_height) / 2.0 + end + else # narrower than box + if box_height > original_height and not upsize + new_width, new_height = original_width, original_height + elsif crop + # TODO + else + new_height = box_height + new_width = original_width.to_f * box_height.to_f / original_height.to_f + end + end + end + [0, 0, original_width, original_height, new_width.round, new_height.round] + end + + def crop_box_size(box_width, box_height, upsize: false) + original_width, original_height = width, height + x = y = 0 + # TODO + end + + def auto_box_size(box_width, box_height, upsize: false) + original_width, original_height = width, height + if :auto == box_width && box_height.is_a?(Numeric) + if box_height > original_height and not upsize + new_width, new_height = original_width, original_height + else new_height = box_height new_width = box_height.to_f / original_height.to_f * original_width.to_f - elsif box_width.is_a?(Numeric) && :auto == box_height + end + elsif box_width.is_a?(Numeric) && :auto == box_height + if box_width > original_width and not upsize + new_width, new_height = original_width, original_height + else new_width = box_width new_height = box_width.to_f / original_width.to_f * original_height.to_f - elsif box_width.is_a?(Numeric) && box_height.is_a?(Numeric) - if options[:skew] - new_width, new_height = box_width, box_height - elsif options[:crop] - # TODO: calculate x, y offset if crop - else - scale = original_width.to_f / original_height.to_f - box_scale = box_width.to_f / box_height.to_f - if scale >= box_scale # wider - new_width = box_width - new_height = original_height.to_f * box_width.to_f / original_width.to_f - else # narrower - new_height = box_height - new_width = original_width.to_f * box_height.to_f / original_height.to_f - end - end - else - raise ArgumentError, "unconclusive arguments #{args.inspect} #{options.inspect}" end else - raise ArgumentError, "wrong number of arguments (#{args.size} for 1..2)" + raise ArgumentError, "unconclusive arguments #{box_width} x #{box_height}" end - [x, y, original_width, original_height, new_width.round, new_height.round] + [0, 0, original_width, original_height, new_width.round, new_height.round] end - + def format_from_filename(path) - File.extname(path)[1..-1] + File.extname(path)[1..-1].to_s.downcase end def assert_valid_keys(hsh, *valid_keys) diff --git a/lib/rszr/image_processing.rb b/lib/rszr/image_processing.rb index 619a8b5..28de18b 100644 --- a/lib/rszr/image_processing.rb +++ b/lib/rszr/image_processing.rb @@ -47,7 +47,7 @@ def apply_operation(accumulator, (name, args, block)) end end - + # Resizes the image to not be larger than the specified dimensions. def resize_to_limit(width, height, **options) width, height = default_dimensions(width, height) diff --git a/rszr.gemspec b/rszr.gemspec index aa1ce73..854a82a 100644 --- a/rszr.gemspec +++ b/rszr.gemspec @@ -17,8 +17,10 @@ Gem::Specification.new do |s| s.require_paths = %w[lib ext] s.extensions = %w[ext/rszr/extconf.rb] + s.requirements = %w[imlib2 libexif] + s.add_development_dependency 'bundler', '~> 1.17' - s.add_development_dependency 'rake', '~> 10.0' + s.add_development_dependency 'rake', '~> 13.0' s.add_development_dependency 'rake-compiler' s.add_development_dependency 'byebug' s.add_development_dependency 'rspec' diff --git a/spec/gc_threading_spec.rb b/spec/gc_threading_spec.rb new file mode 100644 index 0000000..51f020b --- /dev/null +++ b/spec/gc_threading_spec.rb @@ -0,0 +1,52 @@ +RSpec.describe 'Rszr' do + + context 'Garbage collection' do + + it 'releases instances' do + 10.times { GC.start(full_mark: true, immediate_sweep: true); sleep 0.5; print '.' } + expect(ObjectSpace.each_object(Rszr::Image).count).to eq(0) + 20.times { Rszr::Image.load(RSpec.root.join('images/bacon.png')) } + expect(ObjectSpace.each_object(Rszr::Image).count).to be > 0 + 5.times { GC.start(full_mark: true, immediate_sweep: true); sleep 0.5; print '.' } + expect(ObjectSpace.each_object(Rszr::Image).count).to eq(0) + end + + end + + context 'Threading' do + + def data + @data ||= RSpec.root.join('images', 'bacon.png').binread.freeze + end + + def resize + Tempfile.open('src') do |src_file| + src_file.binmode + src_file.write data + Rszr::Image.open(src_file.path) do |image| + image.resize!(200, :auto) + Tempfile.open('dst') do |dst_file| + image.save(dst_file.path) + dst_file.close(true) + end + end + src_file.close(true) + end + end + + it 'synchronizes access to imlib2 context by GIL' do + threads = [] + 10.times do |t| + threads << Thread.new do + 1000.times do |i| + print '.' + resize + end + end + end + threads.each(&:join) + puts + end + + end +end diff --git a/spec/rszr_spec.rb b/spec/rszr_spec.rb index 7468034..471f1dd 100644 --- a/spec/rszr_spec.rb +++ b/spec/rszr_spec.rb @@ -1,4 +1,4 @@ -RSpec.describe 'Rszr image processing' do +RSpec.describe 'Rszr' do it 'has a version number' do expect(Rszr::VERSION).to_not be_nil @@ -80,7 +80,7 @@ end it 'raises on scale larger than one' do - expect { @image.resize(2) }.to raise_error(ArgumentError, 'scale 2 out of range') + expect { @image.resize(-0.5) }.to raise_error(ArgumentError, 'scale factor -0.5 out of range') end it 'raises on too many arguments' do @@ -170,55 +170,17 @@ end end - end - - context 'Garbage collection' do - - it 'releases instances' do - 10.times { GC.start(full_mark: true, immediate_sweep: true); sleep 0.5; print '.' } - expect(ObjectSpace.each_object(Rszr::Image).count).to eq(0) - 20.times { Rszr::Image.load(RSpec.root.join('images/bacon.png')) } - expect(ObjectSpace.each_object(Rszr::Image).count).to be > 0 - 5.times { GC.start(full_mark: true, immediate_sweep: true); sleep 0.5; print '.' } - expect(ObjectSpace.each_object(Rszr::Image).count).to eq(0) - end - - end - - context 'Threading' do - - def data - @data ||= RSpec.root.join('images', 'bacon.png').binread.freeze - end - - def resize - Tempfile.open('src') do |src_file| - src_file.binmode - src_file.write data - Rszr::Image.open(src_file.path) do |image| - image.resize!(200, :auto) - Tempfile.open('dst') do |dst_file| - image.save(dst_file.path) - dst_file.close(true) - end - end - src_file.close(true) - end - end - - it 'synchronizes access to imlib2 context by GIL' do - threads = [] - 10.times do |t| - threads << Thread.new do - 1000.times do |i| - print '.' - resize - end + it 'accepts uppercase extensions' do + Dir.mktmpdir do |dir| + %w[JPG JPEG PNG].each do |format| + resized_file = Pathname.new(File.join(dir, "resized.#{format}")) + expect(@image.save(resized_file.to_s)).to be(true) + expect(resized_file.exist?).to be(true) end end - threads.each(&:join) - puts end - + end + end +