├── .ruby-version ├── .rspec ├── lib ├── rszr │ ├── version.rb │ ├── color.rb │ ├── color │ │ ├── base.rb │ │ ├── cmya.rb │ │ ├── point.rb │ │ ├── gradient.rb │ │ └── rgba.rb │ ├── fill.rb │ ├── buffered.rb │ ├── batch_transformation.rb │ ├── stream.rb │ ├── identification.rb │ ├── image_processing.rb │ ├── orientation.rb │ └── image.rb └── rszr.rb ├── benchmark ├── speed.png ├── memory.rb └── speed.rb ├── demo ├── landscape.jpg ├── portrait.jpg └── demo.rb ├── spec ├── images │ ├── bacon.png │ ├── test.bmp │ ├── test.gif │ ├── test.jpeg │ ├── test.jpg │ ├── test.png │ ├── test.tiff │ ├── test.webp │ ├── CHUNKY.PNG │ ├── broken.jpg │ └── orientation │ │ ├── 1.jpg │ │ ├── 2.jpg │ │ ├── 3.jpg │ │ ├── 4.jpg │ │ ├── 5.jpg │ │ ├── 6.jpg │ │ ├── 7.jpg │ │ ├── 8.jpg │ │ ├── 1.tiff │ │ ├── 2.tiff │ │ ├── 3.tiff │ │ ├── 4.tiff │ │ ├── 5.tiff │ │ ├── 6.tiff │ │ ├── 7.tiff │ │ ├── 8.tiff │ │ ├── multi.jpg │ │ ├── multi.tiff │ │ ├── none.jpg │ │ ├── none.tiff │ │ ├── invalid.jpg │ │ └── invalid.tiff ├── image_processing_spec.rb ├── gc_threading_spec.rb ├── spec_helper.rb └── rszr_spec.rb ├── bin ├── setup └── console ├── ext └── rszr │ ├── rszr.h │ ├── rszr.c │ ├── image.h │ ├── extconf.rb │ ├── errors.h │ ├── errors.c │ └── image.c ├── Gemfile ├── Rakefile ├── .github └── workflows │ └── build.yml ├── rszr.gemspec ├── .gitignore ├── LICENSE ├── CHANGELOG.md └── README.md /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.2 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /lib/rszr/version.rb: -------------------------------------------------------------------------------- 1 | module Rszr 2 | VERSION = '1.5.0' 3 | end 4 | -------------------------------------------------------------------------------- /benchmark/speed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/benchmark/speed.png -------------------------------------------------------------------------------- /demo/landscape.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/demo/landscape.jpg -------------------------------------------------------------------------------- /demo/portrait.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/demo/portrait.jpg -------------------------------------------------------------------------------- /spec/images/bacon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/bacon.png -------------------------------------------------------------------------------- /spec/images/test.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/test.bmp -------------------------------------------------------------------------------- /spec/images/test.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/test.gif -------------------------------------------------------------------------------- /spec/images/test.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/test.jpeg -------------------------------------------------------------------------------- /spec/images/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/test.jpg -------------------------------------------------------------------------------- /spec/images/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/test.png -------------------------------------------------------------------------------- /spec/images/test.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/test.tiff -------------------------------------------------------------------------------- /spec/images/test.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/test.webp -------------------------------------------------------------------------------- /spec/images/CHUNKY.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/CHUNKY.PNG -------------------------------------------------------------------------------- /spec/images/broken.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/broken.jpg -------------------------------------------------------------------------------- /spec/images/orientation/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/1.jpg -------------------------------------------------------------------------------- /spec/images/orientation/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/2.jpg -------------------------------------------------------------------------------- /spec/images/orientation/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/3.jpg -------------------------------------------------------------------------------- /spec/images/orientation/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/4.jpg -------------------------------------------------------------------------------- /spec/images/orientation/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/5.jpg -------------------------------------------------------------------------------- /spec/images/orientation/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/6.jpg -------------------------------------------------------------------------------- /spec/images/orientation/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/7.jpg -------------------------------------------------------------------------------- /spec/images/orientation/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/8.jpg -------------------------------------------------------------------------------- /spec/images/orientation/1.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/1.tiff -------------------------------------------------------------------------------- /spec/images/orientation/2.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/2.tiff -------------------------------------------------------------------------------- /spec/images/orientation/3.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/3.tiff -------------------------------------------------------------------------------- /spec/images/orientation/4.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/4.tiff -------------------------------------------------------------------------------- /spec/images/orientation/5.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/5.tiff -------------------------------------------------------------------------------- /spec/images/orientation/6.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/6.tiff -------------------------------------------------------------------------------- /spec/images/orientation/7.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/7.tiff -------------------------------------------------------------------------------- /spec/images/orientation/8.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/8.tiff -------------------------------------------------------------------------------- /spec/images/orientation/multi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/multi.jpg -------------------------------------------------------------------------------- /spec/images/orientation/multi.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/multi.tiff -------------------------------------------------------------------------------- /spec/images/orientation/none.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/none.jpg -------------------------------------------------------------------------------- /spec/images/orientation/none.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/none.tiff -------------------------------------------------------------------------------- /spec/images/orientation/invalid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/invalid.jpg -------------------------------------------------------------------------------- /spec/images/orientation/invalid.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgrosser/rszr/HEAD/spec/images/orientation/invalid.tiff -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /ext/rszr/rszr.h: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_RSZR_H 2 | #define RUBY_RSZR_H 3 | 4 | #include "ruby.h" 5 | #include 6 | 7 | extern VALUE mRszr; 8 | void Init_rszr(); 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'bundler' 6 | gem 'rake', '~> 13.0' 7 | gem 'rake-compiler' 8 | gem 'byebug' 9 | gem 'rspec' 10 | gem 'minitest' 11 | gem 'simplecov' 12 | gem 'image_processing' 13 | gem 'gd2-ffij' 14 | gem 'mini_magick' 15 | gem 'ruby-vips' 16 | gem 'memory_profiler' -------------------------------------------------------------------------------- /ext/rszr/rszr.c: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_RSZR 2 | #define RUBY_RSZR 3 | 4 | #include "rszr.h" 5 | #include "image.h" 6 | #include "errors.h" 7 | 8 | VALUE mRszr = Qnil; 9 | 10 | void Init_rszr() 11 | { 12 | mRszr = rb_define_module("Rszr"); 13 | Init_rszr_errors(); 14 | Init_rszr_image(); 15 | } 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /ext/rszr/image.h: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_RSZR_IMAGE_H 2 | #define RUBY_RSZR_IMAGE_H 3 | 4 | typedef struct { 5 | Imlib_Image image; 6 | } rszr_image_handle; 7 | 8 | typedef struct { 9 | uint8_t blue, green, red, alpha; //alpha, red, green, blue; 10 | } rszr_raw_pixel; 11 | 12 | extern VALUE cImage; 13 | 14 | void Init_rszr_image(); 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | begin 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task :default => :spec 9 | rescue LoadError 10 | # no rspec available 11 | end 12 | 13 | require 'rake/extensiontask' 14 | 15 | Rake::ExtensionTask.new('rszr') do |ext| 16 | ext.lib_dir = 'lib/rszr' 17 | end 18 | -------------------------------------------------------------------------------- /lib/rszr/color.rb: -------------------------------------------------------------------------------- 1 | require_relative 'color/base' 2 | require_relative 'color/rgba' 3 | require_relative 'color/cmya' 4 | require_relative 'color/point' 5 | require_relative 'color/gradient' 6 | 7 | module Rszr 8 | module Color 9 | 10 | Transparent = RGBA.new(0, 0, 0, 0) 11 | White = RGBA.new(255,255,255) 12 | Black = RGBA.new(0, 0, 0) 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "rszr" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /ext/rszr/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | require 'rbconfig' 3 | 4 | pkg_config('imlib2') 5 | 6 | $CFLAGS << ' -DX_DISPLAY_MISSING' 7 | $LDFLAGS.gsub!(/\ -lX11\ -lXext/, '') if RUBY_PLATFORM =~ /darwin/ 8 | 9 | unless find_header('Imlib2.h') 10 | abort 'imlib2 development headers are missing' 11 | end 12 | 13 | unless find_library('Imlib2', 'imlib_set_cache_size') 14 | abort 'Imlib2 is missing' 15 | end 16 | 17 | create_makefile 'rszr/rszr' 18 | -------------------------------------------------------------------------------- /ext/rszr/errors.h: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_RSZR_ERRORS_H 2 | #define RUBY_RSZR_ERRORS_H 3 | 4 | void Init_rszr_errors(); 5 | void rszr_raise_load_error(Imlib_Load_Error error); 6 | void rszr_raise_save_error(Imlib_Load_Error error); 7 | 8 | extern VALUE eRszrError; 9 | extern VALUE eRszrFileNotFound; 10 | extern VALUE eRszrTransformationError; 11 | extern VALUE eRszrErrorWithMessage; 12 | extern VALUE eRszrLoadError; 13 | extern VALUE eRszrSaveError; 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /lib/rszr/color/base.rb: -------------------------------------------------------------------------------- 1 | module Rszr 2 | module Color 3 | 4 | class Base 5 | attr_reader :alpha 6 | 7 | def rgba 8 | [red, green, blue, alpha] 9 | end 10 | 11 | def cmya 12 | [cyan, magenta, yellow, alpha] 13 | end 14 | 15 | def ==(other) 16 | other.is_a?(Base) && rgba == other.rgba 17 | end 18 | 19 | def to_fill(*) 20 | Fill.new(color: self) 21 | end 22 | end 23 | 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/rszr/fill.rb: -------------------------------------------------------------------------------- 1 | module Rszr 2 | class Fill 3 | attr_reader :color, :gradient, :angle 4 | 5 | def initialize(color: nil, gradient: nil, angle: 0) 6 | if gradient 7 | @gradient = gradient 8 | @angle = angle || 0 9 | elsif color 10 | @color = color 11 | else 12 | raise ArgumentError, 'incomplete fill definition' 13 | end 14 | end 15 | 16 | def to_fill(*) 17 | self 18 | end 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/rszr.rb: -------------------------------------------------------------------------------- 1 | require 'rbconfig' 2 | require 'pathname' 3 | require 'tempfile' 4 | require 'stringio' 5 | 6 | require 'rszr/version' 7 | require 'rszr/stream' 8 | require 'rszr/identification' 9 | require 'rszr/orientation' 10 | require 'rszr/buffered' 11 | require 'rszr/color' 12 | require 'rszr/fill' 13 | require 'rszr/rszr' 14 | require 'rszr/image' 15 | 16 | module Rszr 17 | class << self 18 | @@autorotate = nil 19 | 20 | def autorotate 21 | @@autorotate 22 | end 23 | 24 | def autorotate=(value) 25 | @@autorotate = !!value 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/rszr/buffered.rb: -------------------------------------------------------------------------------- 1 | module Rszr 2 | module Buffered 3 | def self.included(base) 4 | base.extend Buffered 5 | end 6 | 7 | private 8 | 9 | def with_tempfile(format, data = nil) 10 | raise ArgumentError, 'format is required' unless format 11 | result = nil 12 | Tempfile.create(['rszr-buffer', ".#{format}"], encoding: 'BINARY') do |file| 13 | if data 14 | file.binmode 15 | file << data 16 | file.fsync 17 | file.rewind 18 | end 19 | result = yield(file) 20 | end 21 | result 22 | end 23 | 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/rszr/color/cmya.rb: -------------------------------------------------------------------------------- 1 | module Rszr 2 | module Color 3 | 4 | class CMYA < Base 5 | attr_reader :cyan, :magenta, :yellow 6 | 7 | def initialize(cyan, magenta, yellow, alpha = 255) 8 | if cyan < 0 || cyan > 255 || magenta < 0 || magenta > 255 || yellow < 0 || yellow > 255 || alpha < 0 || alpha > 255 9 | raise ArgumentError, 'color out of range' 10 | end 11 | @cyan, @magenta, @yellow = cyan, magenta, yellow 12 | end 13 | 14 | def red 15 | 255 - cyan 16 | end 17 | 18 | def green 19 | 255 - magenta 20 | end 21 | 22 | def blue 23 | 255 - yellow 24 | end 25 | 26 | end 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/rszr/batch_transformation.rb: -------------------------------------------------------------------------------- 1 | module Rszr 2 | class BatchTransformation 3 | attr_reader :transformations, :image 4 | 5 | def initialize(path, **opts) 6 | puts "INITIALIZED BATCH for #{path}" 7 | @image = path.is_a?(Image) ? path : Image.load(path, **opts) 8 | @transformations = [] 9 | end 10 | 11 | Image::Transformations.instance_methods.grep(/\w\z/) do |method| 12 | define_method method do |*args| 13 | transformations << [method, args] 14 | self 15 | end 16 | end 17 | 18 | def call 19 | transformations.each { |method, args| image.public_send("#{method}!", *args) } 20 | image 21 | end 22 | 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 5 14 | strategy: 15 | matrix: 16 | ruby-version: ['2.7', '3.0', '3.1', '3.2', '3.3'] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up Ruby 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: ${{ matrix.ruby-version }} 24 | bundler-cache: true 25 | - name: Set up libs 26 | run: sudo apt-get install -y libimlib2 libimlib2-dev 27 | - name: Compile extension 28 | run: bundle exec rake compile 29 | - run: bundle exec rake 30 | -------------------------------------------------------------------------------- /rszr.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'rszr/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'rszr' 7 | s.version = Rszr::VERSION 8 | s.authors = ['Matthias Grosser'] 9 | s.email = ['mtgrosser@gmx.net'] 10 | 11 | s.summary = %q{Fast image resizer} 12 | s.description = %q{Fast image resizer - do one thing and do it fast.} 13 | s.licenses = %w[MIT] 14 | s.homepage = 'https://github.com/mtgrosser/rszr' 15 | 16 | s.files = Dir['{lib,ext}/**/*.{rb,h,c}', 'LICENSE', 'README.md', 'Rakefile'] 17 | s.require_paths = %w[lib ext] 18 | s.extensions = %w[ext/rszr/extconf.rb] 19 | 20 | s.requirements = %w[imlib2] 21 | end 22 | -------------------------------------------------------------------------------- /lib/rszr/color/point.rb: -------------------------------------------------------------------------------- 1 | module Rszr 2 | module Color 3 | 4 | class Point 5 | attr_reader :position, :color 6 | 7 | class << self 8 | def prgba(position, red, green, blue, alpha = 255) 9 | new(position, RGBA.new(red, green, blue, alpha)) 10 | end 11 | end 12 | 13 | def initialize(position, color) 14 | raise ArgumentError, 'position must be within 0..1' unless (0..1).cover?(position) 15 | raise ArgumentError, 'color must be a Rszr::Color::Base' unless color.is_a?(Rszr::Color::Base) 16 | @position, @color = position, color 17 | end 18 | 19 | def <=>(other) 20 | position <=> other.position 21 | end 22 | 23 | def prgba 24 | [position, *color.rgba] 25 | end 26 | end 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /test/tmp/ 9 | /test/version_tmp/ 10 | /tmp/ 11 | 12 | ## Specific to RubyMotion: 13 | .dat* 14 | .repl_history 15 | build/ 16 | 17 | ## Documentation cache and generated files: 18 | /.yardoc/ 19 | /_yardoc/ 20 | /doc/ 21 | /rdoc/ 22 | 23 | ## Environment normalisation: 24 | /.bundle/ 25 | /vendor/bundle 26 | /lib/bundler/man/ 27 | 28 | # for a library or gem, you might want to ignore these files since the code is 29 | # intended to run in multiple environments; otherwise, check them in: 30 | Gemfile.lock 31 | # .ruby-version 32 | # .ruby-gemset 33 | 34 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 35 | .rvmrc 36 | 37 | .DS_Store 38 | 39 | /spec/examples.txt 40 | 41 | .byebug_history 42 | 43 | /lib/rszr/rszr.bundle 44 | -------------------------------------------------------------------------------- /benchmark/memory.rb: -------------------------------------------------------------------------------- 1 | require 'memory_profiler' 2 | require 'rszr' 3 | require 'mini_magick' 4 | require 'gd2-ffij' 5 | 6 | ITERATIONS = 100 7 | 8 | original = Pathname.new(__FILE__).dirname.join('../spec/images/test.jpg') 9 | resized = Pathname.new(__FILE__).dirname.join('output.jpg') 10 | 11 | mini_magick = MemoryProfiler.report do 12 | ITERATIONS.times do 13 | image = MiniMagick::Image.open(original.to_s) 14 | image.resize '800x532' 15 | image.write resized.to_s 16 | image = nil 17 | end 18 | end 19 | 20 | mini_magick.pretty_print(scale_bytes: true) 21 | 22 | gd2 = MemoryProfiler.report do 23 | image = GD2::Image.import(original.to_s) 24 | image.resize! 800, 532 25 | image.export resized.to_s 26 | image = nil 27 | end 28 | 29 | gd2.pretty_print(scale_bytes: true) 30 | 31 | rszr = MemoryProfiler.report do 32 | ITERATIONS.times do 33 | image = Rszr::Image.load(original.to_s) 34 | image.resize! 800, 532 35 | image.save resized.to_s 36 | image = nil 37 | end 38 | end 39 | 40 | rszr.pretty_print(scale_bytes: true) 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Matthias Grosser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /spec/image_processing_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rszr/image_processing' 2 | 3 | RSpec.describe 'Rszr image processing' do 4 | 5 | it 'resizes image' do 6 | pipeline = ImageProcessing::Rszr.source(fixture_image('bacon.png')) 7 | resized = pipeline.resize(50, :auto) 8 | expect(resized.call(save: false)).to have_dimensions(50, 42) 9 | end 10 | 11 | it 'accepts convert option' do 12 | pipeline = ImageProcessing::Rszr.source(fixture_image('test.jpg')) 13 | converted = pipeline.convert(:png) 14 | expect(converted.options[:format]).to eq(:png) 15 | end 16 | 17 | it 'applies format' do 18 | result = ImageProcessing::Rszr.convert('png').call(fixture_image('test.jpg')) 19 | expect(File.extname(result.path)).to eq('.png') 20 | expect(result.path).to have_format('png') 21 | end 22 | 23 | describe 'image validation' do 24 | it 'returns true for correct images' do 25 | expect(ImageProcessing::Rszr.valid_image?(fixture_image('test.jpg'))).to be(true) 26 | end 27 | 28 | it 'returns false for incorrect images' do 29 | expect(ImageProcessing::Rszr.valid_image?(fixture_image('broken.jpg'))).to be(false) 30 | end 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /lib/rszr/color/gradient.rb: -------------------------------------------------------------------------------- 1 | module Rszr 2 | module Color 3 | 4 | class Gradient 5 | attr_reader :points 6 | 7 | def initialize(*args) 8 | @points = [] 9 | points = args.last.is_a?(Hash) ? args.pop.dup : {} 10 | args.each { |point| self << point } 11 | points.each { |pos, color| point(pos, color) } 12 | yield self if block_given? 13 | end 14 | 15 | def initialize_dup(other) # :nodoc: 16 | @points = other.points.map(&:dup) 17 | end 18 | 19 | def <<(position, red = nil, green = nil, blue= nil, alpha = 255) 20 | point = if red.is_a?(Point) 21 | red 22 | elsif red.is_a?(Color::Base) 23 | Point.new(position, red) 24 | elsif red.is_a?(String) && red.start_with?('#') 25 | Point.new(position, Color.hex(red)) 26 | else 27 | Point.new(position, RGBA.new(red, green, blue, alpha)) 28 | end 29 | points << point 30 | points.sort! 31 | end 32 | 33 | alias_method :point, :<< 34 | 35 | def to_fill(angle = 0) 36 | Fill.new(gradient: self, angle: angle) 37 | end 38 | 39 | end 40 | 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/rszr/stream.rb: -------------------------------------------------------------------------------- 1 | module Rszr 2 | class Stream 3 | attr_reader :pos, :data 4 | protected :data 5 | 6 | def initialize(data, start: 0) 7 | raise ArgumentError, 'start must be > 0' if start < 0 8 | @data = case data 9 | when IO then data 10 | when String then StringIO.new(data) 11 | when Stream then data.data 12 | else 13 | raise ArgumentError, "data must be File or String, got #{data.class}" 14 | end 15 | @data.binmode 16 | @data.seek(start) 17 | @pos = 0 18 | end 19 | 20 | def read(n) 21 | @data.read(n).tap { @pos += n } 22 | end 23 | 24 | def peek(n) 25 | old_pos = @data.pos 26 | @data.read(n) 27 | ensure 28 | @data.pos = old_pos 29 | end 30 | 31 | def skip(n) 32 | @data.seek(n, IO::SEEK_CUR).tap { @pos += n } 33 | end 34 | 35 | def substream 36 | self.class.new(self, @data.pos) 37 | end 38 | 39 | def fast_forward 40 | @pos = 0 41 | self 42 | end 43 | 44 | def read_byte 45 | read(1)[0].ord 46 | end 47 | 48 | def read_int 49 | read(2).unpack('n')[0] 50 | end 51 | 52 | def read_string_int 53 | value = [] 54 | while read(1) =~ /(\d)/ 55 | value << $1 56 | end 57 | value.join.to_i 58 | end 59 | 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/rszr/color/rgba.rb: -------------------------------------------------------------------------------- 1 | module Rszr 2 | module Color 3 | 4 | class << self 5 | def rgba(red, green, blue, alpha = 255) 6 | RGBA.new(red, green, blue, alpha) 7 | end 8 | 9 | def hex(str) 10 | str = str[1..-1] if str.start_with?('#') 11 | case str.size 12 | when 3, 4 then hex(str.chars.map { |c| c * 2 }.join) 13 | when 6 then hex("#{str}ff") 14 | when 8 15 | rgba(*str.scan(/../).map(&:hex)) 16 | else 17 | raise ArgumentError, 'invalid color code' 18 | end 19 | end 20 | end 21 | 22 | class RGBA < Base 23 | attr_reader :red, :green, :blue 24 | 25 | def initialize(red, green, blue, alpha = 255) 26 | if red < 0 || red > 255 || green < 0 || green > 255 || blue < 0 || blue > 255 || alpha < 0 || alpha > 255 27 | raise ArgumentError, 'color out of range' 28 | end 29 | @red, @green, @blue, @alpha = red, green, blue, alpha 30 | end 31 | 32 | def cyan 33 | 255 - red 34 | end 35 | 36 | def magenta 37 | 255 - green 38 | end 39 | 40 | def yellow 41 | 255 - blue 42 | end 43 | 44 | def to_i(alpha: true) 45 | i = red.to_i << 24 | green.to_i << 16 | blue.to_i << 8 | self.alpha.to_i 46 | alpha ? i : i >> 8 47 | end 48 | 49 | def to_hex(alpha: true) 50 | "#%0#{alpha ? 8 : 6}x" % to_i(alpha: alpha) 51 | end 52 | 53 | end 54 | 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /demo/demo.rb: -------------------------------------------------------------------------------- 1 | require 'rszr' 2 | require 'base64' 3 | 4 | def root 5 | Pathname.new(__FILE__).dirname 6 | end 7 | 8 | gravities = [:center, :n, :nw, :w, :sw, :s, :se, :e, :ne] 9 | 10 | template = <<~HTML 11 | 12 | 13 | 14 | 15 | 16 | Rszr Demo 17 | 18 | 19 | 20 |
21 |
22 |

23 | Rszr Demo 24 |

25 | %IMAGES% 26 |
27 |
28 | 29 | 30 | HTML 31 | 32 | def data_uri(data) 33 | "data:image/jpeg;base64,#{Base64.strict_encode64(data)}" 34 | end 35 | 36 | def image_tag(args, data) 37 | formatted_args = args.inspect[1..-2] 38 | %{resize(#{formatted_args})
} 39 | end 40 | 41 | html = '' 42 | 43 | %i[landscape portrait].each do |aspect| 44 | image = Rszr::Image.load(root.join("#{aspect}.jpg")) 45 | modes = [[0.25], [200, :auto], [:auto, 150], [200, 150], [150, 200]] 46 | modes += gravities.map { |g| [150, 200, { crop: g }] } 47 | modes += gravities.map { |g| [200, 150, { crop: g }] } 48 | modes.each do |args| 49 | html << image_tag(args, image.resize(*args).save_data) 50 | end 51 | end 52 | 53 | root.join('resizing.html').write(template.sub('%IMAGES%', html)) 54 | -------------------------------------------------------------------------------- /spec/gc_threading_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe 'Rszr' do 2 | 3 | context 'Garbage collection' do 4 | 5 | it 'releases instances' do 6 | 10.times { GC.start(full_mark: true, immediate_sweep: true); sleep 0.5; print '.' } 7 | expect(ObjectSpace.each_object(Rszr::Image).count).to eq(0) 8 | 20.times { Rszr::Image.load(RSpec.root.join('images/bacon.png')) } 9 | expect(ObjectSpace.each_object(Rszr::Image).count).to be > 0 10 | 5.times { GC.start(full_mark: true, immediate_sweep: true); sleep 0.5; print '.' } 11 | expect(ObjectSpace.each_object(Rszr::Image).count).to eq(0) 12 | end 13 | 14 | end 15 | 16 | context 'Threading' do 17 | 18 | def data 19 | @data ||= RSpec.root.join('images', 'bacon.png').binread.freeze 20 | end 21 | 22 | def resize 23 | Tempfile.open('src') do |src_file| 24 | src_file.binmode 25 | src_file.write data 26 | Rszr::Image.open(src_file.path) do |image| 27 | image.resize!(200, :auto) 28 | Tempfile.open('dst') do |dst_file| 29 | image.save(dst_file.path) 30 | dst_file.close(true) 31 | end 32 | end 33 | src_file.close(true) 34 | end 35 | end 36 | 37 | it 'synchronizes access to imlib2 context by GIL' do 38 | threads = [] 39 | 10.times do |t| 40 | threads << Thread.new do 41 | 1000.times do |i| 42 | print '.' 43 | resize 44 | end 45 | end 46 | end 47 | threads.each(&:join) 48 | puts 49 | end 50 | 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Rszr 1.5.0 (Feb 6, 2024) 2 | 3 | * Image desaturation (grayscale) 4 | 5 | ## Rszr 1.4.0 (Jan 11, 2024) 6 | 7 | * Fix `load_data` (@mantas) 8 | 9 | ## Rszr 1.3.0 (Aug 25, 2022) 10 | 11 | * Alpha channel control 12 | * Background initialization 13 | * Image blending / watermarking 14 | * Rectangle and image fills 15 | * Color gradients 16 | * Hex color codes always prefixed by "#" 17 | 18 | ## Rszr 1.2.1 (Mar 22, 2022) 19 | 20 | * Fix saving without extension (@mantas) 21 | 22 | ## Rszr 1.2.0 (Mar 11, 2022) 23 | 24 | * Saving interlaced PNG and progressive JPEG 25 | 26 | ## Rszr 1.1.0 (Feb 9, 2022) 27 | 28 | * Use pkg_config as imlib2 dropped imlib2-config 29 | 30 | ## Rszr 1.0.1 (Nov 10, 2021) 31 | 32 | * Remove libexif.h header check 33 | 34 | ## Rszr 1.0.0 (Nov 9, 2021) 35 | 36 | * Fix blur method 37 | * Gravity cropping (resize to fill) 38 | * Query pixel 39 | * Add demo generator 40 | 41 | ## Rszr 0.8.0 (Oct 8, 2021) 42 | 43 | * Allow loading of binary image data from buffer 44 | * Drop libexif dependency, use Ruby implementation for image orientation 45 | * Support Ruby 3 46 | 47 | ## Rszr 0.7.1 (May 10, 2021) 48 | 49 | * EXIF autorotation support 50 | * GFX filter support 51 | * Fix error on uppercase file extensions 52 | * image_processing integration 53 | 54 | 55 | ## Rszr 0.5.3 (March 13, 2021) 56 | 57 | * Fix rake dependency 58 | * Open-end bundler dev dependency 59 | 60 | 61 | ## Rszr 0.5.0 (March 7, 2019) 62 | 63 | * Full reimplementation in C to avoid Fiddle GC issues in Ruby 2.4+ (issue:3) 64 | 65 | 66 | ## Rszr 0.4.0 (March 5, 2019) 67 | 68 | * Synchronize imlib2 context access for thread safety (issue:2) 69 | 70 | -------------------------------------------------------------------------------- /benchmark/speed.rb: -------------------------------------------------------------------------------- 1 | require 'benchmark' 2 | require 'fileutils' 3 | 4 | require 'rszr' 5 | require 'mini_magick' 6 | require 'gd2-ffij' 7 | require 'vips' 8 | 9 | def root 10 | Pathname.new(__FILE__).dirname 11 | end 12 | 13 | def work_path(name) 14 | root.join('work', name) 15 | end 16 | 17 | ITERATIONS = 500 18 | ORIGINAL = root.join('..', 'spec', 'images', 'test.jpg').to_s 19 | WIDTH = 800 20 | HEIGHT = 532 21 | 22 | puts 'Preparing ...' 23 | FileUtils.mkdir root.join('work') unless root.join('work').directory? 24 | (1..ITERATIONS).each { |i| FileUtils.cp ORIGINAL, work_path("#{i - 1}.jpg") } 25 | 26 | Benchmark.bm(100) do |x| 27 | 28 | resized = Pathname.new(__FILE__).dirname.join('output.jpg') 29 | 30 | x.report 'MiniMagick' do 31 | ITERATIONS.times do |i| 32 | image = MiniMagick::Image.open(work_path("#{i}.jpg").to_s) 33 | image.resize "#{WIDTH}x#{HEIGHT}" 34 | image.write resized.to_s 35 | image = nil 36 | end 37 | end 38 | 39 | x.report 'GD2' do 40 | ITERATIONS.times do |i| 41 | image = GD2::Image.import(work_path("#{i}.jpg").to_s) 42 | image.resize! WIDTH, HEIGHT 43 | image.export resized.to_s 44 | image = nil 45 | end 46 | end 47 | 48 | x.report 'Vips' do 49 | ITERATIONS.times do |i| 50 | image = Vips::Image.new_from_file(work_path("#{i}.jpg").to_s) 51 | image = image.thumbnail_image(WIDTH, height: HEIGHT) 52 | image.jpegsave(resized.to_s) 53 | image = nil 54 | end 55 | end 56 | 57 | x.report 'Rszr' do 58 | ITERATIONS.times do |i| 59 | image = Rszr::Image.load(work_path("#{i}.jpg").to_s) 60 | image.resize! WIDTH, HEIGHT 61 | image.save resized.to_s 62 | image = nil 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/rszr/identification.rb: -------------------------------------------------------------------------------- 1 | # Type reader adapted from fastimage 2 | # https://github.com/sdsykes/fastimage/ 3 | 4 | module Rszr 5 | module Identification 6 | 7 | private 8 | 9 | def identify(data) 10 | case data[0, 2] 11 | when 'BM' 12 | :bmp 13 | when 'GI' 14 | :gif 15 | when 0xff.chr + 0xd8.chr 16 | :jpeg 17 | when 0x89.chr + 'P' 18 | :png 19 | when 'II', 'MM' 20 | case data[0, 11][8..10] # @stream.peek(11)[8..10] 21 | when 'APC', "CR\002" 22 | nil # do not recognise CRW or CR2 as tiff 23 | else 24 | :tiff 25 | end 26 | when '8B' 27 | :psd 28 | when "\0\0" 29 | case data[0, 3].bytes.last #@stream.peek(3).bytes.to_a.last 30 | when 0 31 | # http://www.ftyps.com/what.html 32 | # HEIC is composed of nested "boxes". Each box has a header composed of 33 | # - Size (32 bit integer) 34 | # - Box type (4 chars) 35 | # - Extended size: only if size === 1, the type field is followed by 64 bit integer of extended size 36 | # - Payload: Type-dependent 37 | case data[0, 12][4..-1] #@stream.peek(12)[4..-1] 38 | when 'ftypheic' 39 | :heic 40 | when 'ftypmif1' 41 | :heif 42 | end 43 | # ico has either a 1 (for ico format) or 2 (for cursor) at offset 3 44 | when 1 then :ico 45 | when 2 then :cur 46 | end 47 | when 'RI' 48 | :webp if data[0, 12][8..11] == 'WEBP' #@stream.peek(12)[8..11] == "WEBP" 49 | when " RSZR_MAX_ERROR_INDEX) 53 | error_index = RSZR_MAX_ERROR_INDEX; 54 | 55 | rb_error = rb_exc_new2(rb_error_class, sRszrErrorMessages[error_index]); 56 | rb_exc_raise(rb_error); 57 | } 58 | 59 | void rszr_raise_load_error(Imlib_Load_Error error) 60 | { 61 | rszr_raise_error_with_message(eRszrLoadError, error); 62 | } 63 | 64 | void rszr_raise_save_error(Imlib_Load_Error error) 65 | { 66 | rszr_raise_error_with_message(eRszrSaveError, error); 67 | } 68 | 69 | 70 | #endif -------------------------------------------------------------------------------- /lib/rszr/image_processing.rb: -------------------------------------------------------------------------------- 1 | require 'rszr' 2 | require 'image_processing' 3 | 4 | module ImageProcessing 5 | module Rszr 6 | extend Chainable 7 | 8 | class << self 9 | 10 | # Returns whether the given image file is processable. 11 | def valid_image?(file) 12 | ::Rszr::Image.load(file).width 13 | true 14 | rescue ::Rszr::Error 15 | false 16 | end 17 | 18 | end 19 | 20 | class Processor < ImageProcessing::Processor 21 | accumulator :image, ::Rszr::Image 22 | 23 | class << self 24 | 25 | # Loads the image on disk into a Rszr::Image object 26 | def load_image(path_or_image, **options) 27 | if path_or_image.is_a?(::Rszr::Image) 28 | path_or_image 29 | else 30 | ::Rszr::Image.load(path_or_image) 31 | end 32 | # TODO: image = image.autorot if autorot && !options.key?(:autorotate) 33 | end 34 | 35 | # Writes the image object to disk. 36 | # Accepts additional options (quality, format). 37 | def save_image(image, destination_path, **options) 38 | image.save(destination_path, **options) 39 | end 40 | 41 | # Calls the operation to perform the processing. If the operation is 42 | # defined on the processor (macro), calls it. Otherwise calls the 43 | # bang variant of the method directly on the Rszr image object. 44 | def apply_operation(accumulator, (name, args, block)) 45 | return super if method_defined?(name) 46 | accumulator.send("#{name}!", *args, &block) 47 | end 48 | 49 | end 50 | 51 | # Resizes the image to not be larger than the specified dimensions. 52 | def resize_to_limit(width, height, **options) 53 | width, height = default_dimensions(width, height) 54 | thumbnail(width, height, inflate: false, **options) 55 | end 56 | 57 | # Resizes the image to fit within the specified dimensions. 58 | def resize_to_fit(width, height, **options) 59 | width, height = default_dimensions(width, height) 60 | thumbnail(width, height, **options) 61 | end 62 | 63 | # Resizes the image to fill the specified dimensions, applying any 64 | # necessary cropping. 65 | def resize_to_fill(width, height, gravity: :center, **options) 66 | thumbnail(width, height, crop: gravity, **options) 67 | end 68 | 69 | private 70 | 71 | def thumbnail(width, height, **options) 72 | image.resize!(width, height, **options) 73 | end 74 | 75 | def default_dimensions(width, height) 76 | raise Error, 'either width or height must be specified' unless width || height 77 | [width || :auto, height || :auto] 78 | end 79 | 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/rszr/orientation.rb: -------------------------------------------------------------------------------- 1 | module Rszr 2 | module Orientation 3 | ROTATIONS = { 5 => 1, 6 => 1, 3 => 2, 4 => 2, 7 => 3, 8 => 3 } 4 | 5 | def self.included(base) 6 | base.extend(ClassMethods) 7 | base.attr_reader :original_orientation 8 | end 9 | 10 | module ClassMethods 11 | 12 | private 13 | 14 | def autorotate(image, path) 15 | return unless %w[jpeg tiff].include?(image.format) 16 | File.open(path) do |file| 17 | if orientation = send("parse_#{image.format}_orientation", file) and (1..8).member?(orientation) 18 | image.instance_variable_set :@original_orientation, orientation 19 | image.flop! if [2, 4, 5, 7].include?(orientation) 20 | image.turn!(ROTATIONS[orientation]) if ROTATIONS.key?(orientation) 21 | end 22 | end 23 | end 24 | 25 | def parse_tiff_orientation(data) 26 | exif_parse_orientation(Stream.new(data)) 27 | end 28 | 29 | def parse_jpeg_orientation(data) 30 | stream = Stream.new(data) 31 | exif = nil 32 | state = nil 33 | loop do 34 | state = case state 35 | when nil 36 | stream.skip(2) 37 | :started 38 | when :started 39 | stream.read_byte == 0xFF ? :sof : :started 40 | when :sof 41 | case stream.read_byte 42 | when 0xe1 # APP1 43 | skip_chars = stream.read_int - 2 44 | app1 = Stream.new(stream.read(skip_chars)) 45 | if app1.read(4) == 'Exif' 46 | app1.skip(2) 47 | orientation = exif_parse_orientation(app1.fast_forward)# rescue nil 48 | return orientation 49 | end 50 | :started 51 | when 0xe0..0xef 52 | :skipframe 53 | when 0xC0..0xC3, 0xC5..0xC7, 0xC9..0xCB, 0xCD..0xCF 54 | :readsize 55 | when 0xFF 56 | :sof 57 | else 58 | :skipframe 59 | end 60 | when :skipframe 61 | skip_chars = stream.read_int - 2 62 | stream.skip(skip_chars) 63 | :started 64 | when :readsize 65 | # stream.skip(3) 66 | # height = stream.read_int 67 | # width = stream.read_int 68 | return exif&.orientation 69 | end 70 | end 71 | end 72 | 73 | def exif_byte_order(stream) 74 | byte_order = stream.read(2) 75 | case byte_order 76 | when 'II' 77 | %w[v V] 78 | when 'MM' 79 | %w[n N] 80 | else 81 | raise LoadError 82 | end 83 | end 84 | 85 | def exif_parse_ifd(stream, short) 86 | tag_count = stream.read(2).unpack(short)[0] 87 | tag_count.downto(1) do 88 | type = stream.read(2).unpack(short)[0] 89 | stream.read(6) 90 | data = stream.read(2).unpack(short)[0] 91 | return data if 0x0112 == type 92 | stream.read(2) 93 | end 94 | nil 95 | end 96 | 97 | def exif_parse_orientation(stream) 98 | short, long = exif_byte_order(stream) 99 | stream.read(2) # 42 100 | offset = stream.read(4).unpack(long)[0] 101 | stream.skip(offset - 8) 102 | exif_parse_ifd(stream, short) 103 | end 104 | end 105 | 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start 3 | 4 | module HelperMethods 5 | def fixture_image(name) 6 | RSpec.root.join('images', name) 7 | end 8 | end 9 | 10 | RSpec.configure do |config| 11 | # rspec-expectations config goes here. You can use an alternate 12 | # assertion/expectation library such as wrong or the stdlib/minitest 13 | # assertions if you prefer. 14 | config.expect_with :rspec do |expectations| 15 | # This option will default to `true` in RSpec 4. It makes the `description` 16 | # and `failure_message` of custom matchers include text for helper methods 17 | # defined using `chain`, e.g.: 18 | # be_bigger_than(2).and_smaller_than(4).description 19 | # # => "be bigger than 2 and smaller than 4" 20 | # ...rather than: 21 | # # => "be bigger than 2" 22 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 23 | end 24 | 25 | # rspec-mocks config goes here. You can use an alternate test double 26 | # library (such as bogus or mocha) by changing the `mock_with` option here. 27 | config.mock_with :rspec do |mocks| 28 | # Prevents you from mocking or stubbing a method that does not exist on 29 | # a real object. This is generally recommended, and will default to 30 | # `true` in RSpec 4. 31 | mocks.verify_partial_doubles = true 32 | end 33 | 34 | config.include HelperMethods 35 | 36 | # The settings below are suggested to provide a good initial experience 37 | # with RSpec, but feel free to customize to your heart's content. 38 | 39 | # These two settings work together to allow you to limit a spec run 40 | # to individual examples or groups you care about by tagging them with 41 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 42 | # get run. 43 | config.filter_run :focus 44 | config.run_all_when_everything_filtered = true 45 | 46 | # Allows RSpec to persist some state between runs in order to support 47 | # the `--only-failures` and `--next-failure` CLI options. We recommend 48 | # you configure your source control system to ignore this file. 49 | config.example_status_persistence_file_path = "spec/examples.txt" 50 | 51 | # Limits the available syntax to the non-monkey patched syntax that is 52 | # recommended. For more details, see: 53 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 54 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 55 | # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching 56 | config.disable_monkey_patching! 57 | 58 | # This setting enables warnings. It's recommended, but in some cases may 59 | # be too noisy due to issues in dependencies. 60 | config.warnings = true 61 | 62 | # Many RSpec users commonly either run the entire suite or an individual 63 | # file, and it's useful to allow more verbose output when running an 64 | # individual spec file. 65 | if config.files_to_run.one? 66 | # Use the documentation formatter for detailed output, 67 | # unless a formatter has already been configured 68 | # (e.g. via a command-line flag). 69 | config.default_formatter = 'doc' 70 | end 71 | 72 | # Print the 10 slowest examples and example groups at the 73 | # end of the spec run, to help surface which specs are running 74 | # particularly slow. 75 | config.profile_examples = 10 76 | 77 | # Run specs in random order to surface order dependencies. If you find an 78 | # order dependency and want to debug it, you can fix the order by providing 79 | # the seed, which is printed after each run. 80 | # --seed 1234 81 | config.order = :random 82 | 83 | # Seed global randomization in this process using the `--seed` CLI option. 84 | # Setting this allows you to use `--seed` to deterministically reproduce 85 | # test failures related to randomization by passing the same `--seed` value 86 | # as the one that triggered the failure. 87 | Kernel.srand config.seed 88 | end 89 | 90 | require 'rszr' 91 | require 'byebug' 92 | require 'tempfile' 93 | require 'tmpdir' 94 | 95 | RSpec::Matchers.define :have_format do |expected| 96 | match do |actual| 97 | Rszr::Image.load(actual).format == expected 98 | end 99 | end 100 | 101 | RSpec::Matchers.define :have_dimensions do |expected_width, expected_height| 102 | match do |actual| 103 | actual.dimensions == [expected_width, expected_height] 104 | end 105 | end 106 | 107 | RSpec.class_eval do 108 | def self.root 109 | @spec_root ||= Pathname.new(__FILE__).dirname 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/rszr.svg)](http://badge.fury.io/rb/rszr) [![build](https://github.com/mtgrosser/rszr/actions/workflows/build.yml/badge.svg)](https://github.com/mtgrosser/rszr/actions/workflows/build.yml) 2 | # Rszr - fast image resizer for Ruby 3 | 4 | Rszr is an image resizer for Ruby based on the Imlib2 library. 5 | It is faster and consumes less memory than MiniMagick, GD2 and VIPS, and comes with an optional drop-in interface for Rails ActiveStorage image processing. 6 | 7 | ## Installation 8 | 9 | In your Gemfile: 10 | 11 | ```ruby 12 | gem 'rszr' 13 | ``` 14 | 15 | ### Imlib2 16 | 17 | Rszr requires the `Imlib2` library to do the heavy lifting. 18 | 19 | #### OS X 20 | 21 | Using homebrew: 22 | 23 | ```bash 24 | brew install imlib2 25 | ``` 26 | 27 | #### Linux 28 | 29 | Using your favourite package manager: 30 | 31 | ##### RedHat-based 32 | 33 | ```bash 34 | yum install imlib2 imlib2-devel 35 | ``` 36 | 37 | ##### Debian-based 38 | 39 | ```bash 40 | apt-get install libimlib2 libimlib2-dev 41 | ``` 42 | 43 | ## Usage 44 | 45 | ```ruby 46 | # bounding box 400x300 47 | image = Rszr::Image.load('image.jpg') 48 | image.resize!(400, 300) 49 | 50 | # save it 51 | image.save('resized.jpg') 52 | 53 | # save it as PNG 54 | image.save('resized.png') 55 | 56 | # save without extension in given format 57 | image.save('resized', format: 'png') 58 | ``` 59 | 60 | ### Image info 61 | ```ruby 62 | image.width => 400 63 | image.height => 300 64 | image.dimensions => [400, 300] 65 | image.format => "jpeg" 66 | image.alpha? => false 67 | image[0, 0] => 68 | image[0, 0].to_hex => "#26738dff" 69 | image[0, 0].to_hex(alpha: false) => "#26738d" 70 | ``` 71 | 72 | ### Transformations 73 | 74 | For each transformation, there is a bang! and non-bang method. 75 | The bang method changes the image in place, while the non-bang method 76 | creates a copy of the image in memory. 77 | 78 | #### Resizing 79 | 80 | ```ruby 81 | # auto height 82 | image.resize(400, :auto) 83 | 84 | # auto width 85 | image.resize(:auto, 300) 86 | 87 | # scale factor 88 | image.resize(0.5) 89 | 90 | # resize to fill 91 | image.resize(400, 300, crop: true) 92 | 93 | # resize to fill with gravity 94 | # where gravity in [:n, :nw, :w, :sw, :w, :se, :e, :ne, :center] 95 | image.resize(400, 300, crop: gravity) 96 | 97 | # save memory, do not duplicate instance 98 | image.resize!(400, :auto) 99 | ``` 100 | 101 | Check out the [full list and demo of resize options](https://mtgrosser.github.io/rszr/resizing.html)! 102 | 103 | #### Other transformations 104 | 105 | ```ruby 106 | # crop 107 | image.crop(200, 200, 100, 100) 108 | 109 | # rotate three times 90 deg clockwise 110 | image.turn!(3) 111 | 112 | # rotate one time 90 deg counterclockwise 113 | image.turn!(-1) 114 | 115 | # rotate by arbitrary angle 116 | image.rotate(45) 117 | 118 | # flip vertically 119 | image.flip 120 | 121 | # flop horizontally 122 | image.flop 123 | 124 | # initialize copy 125 | image.dup 126 | ``` 127 | 128 | ### Image generation 129 | 130 | ```ruby 131 | # generate new image with transparent background 132 | image = Rszr::Image.new(500, 500, alpha: true, background: Rszr::Color::Transparent) 133 | 134 | # fill image with 50% opacity 135 | image.fill!(Rszr::Color::RGBA.new(0, 206, 209, 50)) 136 | 137 | # define a color gradient 138 | gradient = Rszr::Color::Gradient.new do |g| 139 | g.point 0, 255, 250, 205, 50 140 | g.point 0.5, 135, 206, 250 141 | g.point 1, Rszr::Color::White 142 | end 143 | 144 | # draw a rectangle and fill it using the gradient with 45° 145 | image.rectangle!(gradient.to_fill(45), 100, 100, 300, 300) 146 | ``` 147 | 148 | ### Colors 149 | 150 | ```ruby 151 | # pre-defined colors 152 | Rszr::Color::White 153 | Rszr::Color::Black 154 | Rszr::Color::Transparent 155 | 156 | # RGB 157 | color = Rszr::Color.rgba(255, 250, 50) 158 | color.red => 255 159 | color.green => 250 160 | color.blue => 50 161 | color.alpha => 255 162 | color.cyan => 0 163 | color.magenta => 5 164 | color.yellow => 205 165 | 166 | # RGBA 167 | Rszr::Color.rgba(255, 250, 50, 255) 168 | 169 | # CMY 170 | Rszr::Color.cmya(0, 5, 205) 171 | 172 | # CMYA 173 | Rszr::Color.cmya(0, 5, 205, 255) 174 | ``` 175 | 176 | ### Color gradients 177 | 178 | ```ruby 179 | # three-color linear gradient with changing opacity 180 | gradient = Rszr::Color::Gradient.new do |g| 181 | g.point 0, 255, 250, 205, 50 182 | g.point 0.5, 135, 206, 250 183 | g.point 1, Rszr::Color::White 184 | end 185 | 186 | # alternative syntax 187 | gradient = Rszr::Color::Gradient.new(0 => "#fffacd32", 0.5 => "#87cefa", 1 => "#fff") 188 | 189 | # generate fill with 45° angle 190 | fill = gradient.to_fill(45) 191 | 192 | # use as image background 193 | image = Rszr::Image.new(500, 500, background: fill) 194 | ``` 195 | 196 | ### Watermarking and image blending 197 | 198 | ```ruby 199 | # load logo 200 | logo = Rszr::Image.load('logo.png') 201 | 202 | # load image 203 | image = Rszr::Image.load('image.jpg') 204 | 205 | # enable alpha channel 206 | image.alpha = true 207 | 208 | # blend it onto the image at position (10, 10) 209 | image.blend!(logo, 10, 10) 210 | 211 | # blending modes: 212 | # - copy (default) 213 | # - add 214 | # - subtract 215 | # - reshade 216 | image.blend(logo, 10, 10, mode: :subtract) 217 | ``` 218 | 219 | ### Filters 220 | 221 | Filters also support bang! and non-bang methods. 222 | 223 | ```ruby 224 | # sharpen image by pixel radius 225 | image.sharpen!(1) 226 | 227 | # blur image by pixel radius 228 | image.blur!(1) 229 | 230 | # brighten 231 | image.brighten(0.1) 232 | 233 | # darken 234 | image.brighten(-0.1) 235 | 236 | # contrast 237 | image.contrast(0.5) 238 | 239 | # gamma 240 | image.gamma(1.1) 241 | 242 | # convert to grayscale (automatic mode) 243 | image.desaturate 244 | 245 | # convert to grayscale with mode 246 | image.desaturate(:lightness) 247 | image.desaturate(:luminosity) 248 | image.desaturate(:average) 249 | ``` 250 | 251 | ### Image auto orientation 252 | 253 | Auto-rotation is supported for JPEG and TIFF files that include the necessary 254 | EXIF metadata. 255 | 256 | ```ruby 257 | # load and autorotate 258 | image = Rszr::Image.load('image.jpg', autorotate: true) 259 | ``` 260 | 261 | To enable autorotation by default: 262 | 263 | ```ruby 264 | # auto-rotate by default, for Rails apps put this into an initializer 265 | Rszr.autorotate = true 266 | ``` 267 | 268 | ### Creating interlaced PNG and progressive JPEG images 269 | 270 | In order to save interlaced PNGs and progressive JPEGs, set the `interlace` option to `true`: 271 | 272 | ```ruby 273 | image.save('interlaced.png', interlace: true) 274 | ``` 275 | 276 | Saving progressive JPEG images requires `imlib2` >= 1.8.1. 277 | 278 | For EL8, there are pre-built RPMs provided by the [onrooby repo](http://downloads.onrooby.com/repo/el/8/x86_64/). 279 | 280 | ## Rails / ActiveStorage interface 281 | 282 | Rszr provides a drop-in interface to the [image_processing](https://github.com/janko/image_processing) gem. 283 | It is faster than both `mini_magick` and `vips` and way easier to install than the latter. 284 | 285 | ```ruby 286 | # Gemfile 287 | gem 'image_processing' 288 | gem 'rszr' 289 | 290 | # config/initializers/rszr.rb 291 | require 'rszr/image_processing' 292 | 293 | # config/application.rb 294 | config.active_storage.variant_processor = :rszr 295 | ``` 296 | 297 | When creating image variants, you can use all of Rszr's transformation methods: 298 | 299 | ```erb 300 | <%= image_tag user.avatar.variant(resize_to_fit: [300, 200]) %> 301 | ``` 302 | 303 | ## Loading from and saving to memory 304 | 305 | The `Imlib2` library is mainly file-oriented and doesn't provide a way of loading 306 | the undecoded image from a memory buffer. Therefore, the functionality is 307 | implemented on the Ruby side of the gem, writing the memory buffer to a Tempfile. 308 | Currently, this local write cannot be avoided. 309 | 310 | ```ruby 311 | image = Rszr::Image.load_data(binary_data) 312 | 313 | data = image.save_data(format: :jpeg) 314 | ``` 315 | 316 | ## Thread safety 317 | 318 | As of version 0.5.0, Rszr is thread safe through Ruby's global VM lock. 319 | Use of any previous versions in a threaded environment is discouraged. 320 | 321 | ## Speed 322 | 323 | Resizing a 1500x997 JPEG image to 800x532, 500 times: 324 | ![Speed](https://github.com/mtgrosser/rszr/blob/master/benchmark/speed.png) 325 | 326 | 327 | Library | Time 328 | ----------------|----------- 329 | MiniMagick | 27.0 s 330 | GD2 | 28.2 s 331 | VIPS | 13.6 s 332 | Rszr | 7.9 s 333 | -------------------------------------------------------------------------------- /spec/rszr_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe 'Rszr' do 2 | 3 | it 'has a version number' do 4 | expect(Rszr::VERSION).to_not be_nil 5 | end 6 | 7 | it 'instantiates new images' do 8 | expect(Rszr::Image.new(300, 400)).to be_kind_of(Rszr::Image) 9 | end 10 | 11 | context 'Loading' do 12 | 13 | it 'loads images from disk' do 14 | expect(Rszr::Image.load(RSpec.root.join('images/bacon.png'))).to be_kind_of(Rszr::Image) 15 | end 16 | 17 | it 'loads image from memory' do 18 | image = Rszr::Image.load_data(RSpec.root.join('images/test.jpg').binread) 19 | expect(image.format).to eq('jpeg') 20 | expect(image[1,1]).to have_attributes(green: 93, blue: 112, red: 78) 21 | end 22 | 23 | it 'loads images with uppercase extension' do 24 | expect(Rszr::Image.load(RSpec.root.join('images/CHUNKY.PNG'))).to be_kind_of(Rszr::Image) 25 | end 26 | 27 | it 'raises an error when trying to load a non-existing image' do 28 | expect { Rszr::Image.load(RSpec.root.join('images/foo.jpg')) }.to raise_error(Rszr::FileNotFound) 29 | end 30 | 31 | it 'raises an error when trying to load a non-supported image' do 32 | expect { Rszr::Image.load(RSpec.root.join('images/broken.jpg')) }.to raise_error(Rszr::LoadError) 33 | end 34 | end 35 | 36 | context 'Images' do 37 | 38 | before(:each) do 39 | # 1500 997 40 | @image = Rszr::Image.load(RSpec.root.join('images/test.jpg')) 41 | end 42 | 43 | it 'provide the image format as lowercase' do 44 | expect(Rszr::Image.load(RSpec.root.join('images/CHUNKY.PNG')).format).to eq('png') 45 | end 46 | 47 | it 'always return jpeg for JPEG format' do 48 | expect(Rszr::Image.load(RSpec.root.join('images/test.jpeg')).format).to eq('jpeg') 49 | expect(Rszr::Image.load(RSpec.root.join('images/test.jpg')).format).to eq('jpeg') 50 | end 51 | 52 | it 'provide width and height' do 53 | expect(@image.width).to eq(1500) 54 | expect(@image.height).to eq(997) 55 | end 56 | 57 | it 'provide pixel RGBA value' do 58 | expect(@image[0, 0].to_hex).to eq('#4c5c6cff') 59 | end 60 | 61 | it 'provide pixel RGB value' do 62 | expect(@image[0, 0].to_hex(alpha: false)).to eq('#4c5c6c') 63 | end 64 | 65 | it 'return nil if pixel out of bounds' do 66 | expect(@image[1000, 1000]).to be_nil 67 | end 68 | 69 | end 70 | 71 | context 'Resizing' do 72 | 73 | before(:each) do 74 | # 1500 997 75 | @image = Rszr::Image.load(RSpec.root.join('images/test.jpg')) 76 | end 77 | 78 | it 'creates a new instance' do 79 | expect(@image.resize(0.5)).not_to be(@image) 80 | end 81 | 82 | it 'modifies instance in place' do 83 | expect(@image.resize!(0.5)).to be(@image) 84 | end 85 | 86 | it 'by scale' do 87 | expect(@image.resize(0.5).dimensions).to eq([750, 499]) 88 | expect(@image.resize(Rational(1, 3)).dimensions).to eq([500, 332]) 89 | expect(@image.resize(0.75).dimensions).to eq([1125, 748]) 90 | end 91 | 92 | it 'by max width' do 93 | expect(@image.resize(500, :auto).dimensions).to eq([500, 332]) 94 | end 95 | 96 | it 'by max height' do 97 | expect(@image.resize(:auto, 100).dimensions).to eq([150, 100]) 98 | end 99 | 100 | it 'by narrower bounding box' do 101 | expect(@image.resize(100, 400).dimensions).to eq([100, 66]) 102 | end 103 | 104 | it 'by wider bounding box' do 105 | expect(@image.resize(400, 100).dimensions).to eq([150, 100]) 106 | end 107 | 108 | it 'skew to dimensions' do 109 | expect(@image.resize(100, 100, skew: true).dimensions).to eq([100, 100]) 110 | end 111 | 112 | it 'raises on scale larger than one' do 113 | expect { @image.resize(-0.5) }.to raise_error(ArgumentError, 'scale factor -0.5 out of range') 114 | end 115 | 116 | it 'raises on too many arguments' do 117 | expect { @image.resize(20, 30, 40) }.to raise_error(ArgumentError, 'wrong number of arguments (3 for 1..2)') 118 | end 119 | 120 | it 'raises on too few arguments' do 121 | expect { @image.resize }.to raise_error(ArgumentError, 'wrong number of arguments (0 for 1..2)') 122 | end 123 | 124 | it 'raises on nonsense arguments' do 125 | expect { @image.resize('foo', 'bar') }.to raise_error(ArgumentError) 126 | end 127 | 128 | end 129 | 130 | context 'Cropping' do 131 | 132 | before(:each) do 133 | @image = Rszr::Image.load(RSpec.root.join('images/test.jpg')).resize(0.1) 134 | end 135 | 136 | it 'crops images' do 137 | expect(@image.crop(40, 50, 60, 70).dimensions).to eq([60, 70]) 138 | end 139 | 140 | it 'crops images in place' do 141 | expect(@image.crop!(40, 50, 60, 70)).to be(@image) 142 | end 143 | 144 | end 145 | 146 | context 'Turning' do 147 | 148 | before(:each) do 149 | @image = Rszr::Image.load(RSpec.root.join('images/test.jpg')) 150 | end 151 | 152 | it 'turns images clockwise' do 153 | expect(@image.turn!(3).dimensions).to eq([997, 1500]) 154 | end 155 | 156 | it 'turns images counterclockwise' do 157 | expect(@image.turn!(-3).dimensions).to eq([997, 1500]) 158 | end 159 | 160 | it 'turns images in place' do 161 | expect(@image.turn!(3)).to be(@image) 162 | end 163 | 164 | end 165 | 166 | context 'Duplicating' do 167 | 168 | before(:each) do 169 | @image = Rszr::Image.load(RSpec.root.join('images/test.jpg')) 170 | end 171 | 172 | it 'duplicates images' do 173 | expect(@image.dup).not_to be(@image) 174 | expect(@image.dup.dimensions).to eq(@image.dimensions) 175 | end 176 | 177 | end 178 | 179 | context 'Saving' do 180 | 181 | before(:each) do 182 | @image = Rszr::Image.load(RSpec.root.join('images/test.jpg')).resize(0.1) 183 | end 184 | 185 | it 'saves images' do 186 | Dir.mktmpdir do |dir| 187 | resized_file = Pathname.new(File.join(dir, 'resized.jpg')) 188 | resized_file.unlink if resized_file.exist? 189 | expect(resized_file.exist?).to be(false) 190 | expect(@image.save(resized_file.to_s)).to be(true) 191 | expect(resized_file.exist?).to be(true) 192 | end 193 | end 194 | 195 | it 'saves images when given path does not have extension' do 196 | Dir.mktmpdir do |dir| 197 | resized_file = Pathname.new(File.join(dir, 'resized_jpg')) 198 | resized_file.unlink if resized_file.exist? 199 | expect(resized_file.exist?).to be(false) 200 | expect(@image.save(resized_file.to_s)).to be(true) 201 | expect(resized_file.exist?).to be(true) 202 | end 203 | end 204 | 205 | it 'saves with given format when saving without extension' do 206 | Dir.mktmpdir do |dir| 207 | resized_file = Pathname.new(File.join(dir, 'resized')) 208 | resized_file.unlink if resized_file.exist? 209 | expect(resized_file.exist?).to be(false) 210 | expect(@image.save(resized_file.to_s, format: :png)).to be(true) 211 | expect(resized_file).to have_format('png') 212 | end 213 | end 214 | 215 | it 'saves using specified format, overriding extension' do 216 | Dir.mktmpdir do |dir| 217 | resized_file = Pathname.new(File.join(dir, 'resized.jpg')) 218 | resized_file.unlink if resized_file.exist? 219 | expect(resized_file.exist?).to be(false) 220 | expect(@image.save(resized_file.to_s, format: :png)).to be(true) 221 | expect(resized_file).to have_format('png') 222 | end 223 | end 224 | 225 | it 'raises save errors' do 226 | Dir.mktmpdir do |dir| 227 | resized_file = Pathname.new(File.join(dir, 'foo', 'bar', 'resized.jpg')) 228 | expect(resized_file.exist?).to be(false) 229 | expect { @image.save(resized_file.to_s) }.to raise_error(Rszr::SaveError, 'Non-existant path component') 230 | end 231 | end 232 | 233 | it 'accepts uppercase extensions' do 234 | Dir.mktmpdir do |dir| 235 | %w[JPG JPEG PNG].each do |format| 236 | resized_file = Pathname.new(File.join(dir, "resized.#{format}")) 237 | expect(@image.save(resized_file.to_s)).to be(true) 238 | expect(resized_file.exist?).to be(true) 239 | end 240 | end 241 | end 242 | 243 | it 'saves image to memory' do 244 | expect(Rszr::Image.load(RSpec.root.join('images/test.jpg')).save_data(format: 'png')).to start_with("\x89PNG".force_encoding('BINARY')) 245 | end 246 | 247 | it 'saves interlaced PNGs but clears interlacing flag' do 248 | Dir.mktmpdir do |dir| 249 | resized_file = Pathname.new(File.join(dir, 'interlace.png')) 250 | resized_file.unlink if resized_file.exist? 251 | @image.save(resized_file.to_s, interlace: true) 252 | expect(`file #{resized_file}`).to include('interlaced') 253 | resized_file.unlink 254 | @image.save(resized_file.to_s, interlace: false) 255 | expect(`file #{resized_file}`).to include('non-interlaced') 256 | end 257 | end 258 | 259 | end 260 | 261 | context 'Autorotation' do 262 | 263 | %w[jpg tiff].each do |format| 264 | it "autorotates #{format.upcase} images" do 265 | 1.upto(8) do |orientation| 266 | expect(Rszr::Image.load(RSpec.root.join('images', 'orientation', "#{orientation}.#{format}"), autorotate: true).original_orientation).to eq(orientation) 267 | end 268 | end 269 | 270 | it "ignores #{format.upcase} images without EXIF orientation" do 271 | expect(Rszr::Image.load(RSpec.root.join('images', 'orientation', "none.#{format}"), autorotate: true).original_orientation).to be_nil 272 | end 273 | 274 | it "ignores #{format.upcase} images with invalid EXIF orientation" do 275 | expect(Rszr::Image.load(RSpec.root.join('images', 'orientation', "invalid.#{format}"), autorotate: true).original_orientation).to be_nil 276 | end 277 | end 278 | 279 | end 280 | 281 | context 'Transformations' do 282 | before(:each) do 283 | @image = Rszr::Image.load(RSpec.root.join('images/test.jpg')) 284 | end 285 | 286 | it 'flips' do 287 | expect(@image.flip.dimensions).to eq(@image.dimensions) 288 | end 289 | 290 | it 'flops' do 291 | expect(@image.flop.dimensions).to eq(@image.dimensions) 292 | end 293 | 294 | it 'sharpens' do 295 | expect(@image.sharpen(2).dimensions).to eq(@image.dimensions) 296 | end 297 | 298 | it 'blurs' do 299 | expect(@image.blur(3).dimensions).to eq(@image.dimensions) 300 | end 301 | 302 | it 'brightens' do 303 | expect(@image.brighten(0.1).dimensions).to eq(@image.dimensions) 304 | end 305 | 306 | it 'darkens' do 307 | expect(@image.brighten(-0.1).dimensions).to eq(@image.dimensions) 308 | end 309 | 310 | it 'contrasts' do 311 | expect(@image.contrast(0.1).dimensions).to eq(@image.dimensions) 312 | end 313 | 314 | it 'gammas' do 315 | expect(@image.gamma(1.1).dimensions).to eq(@image.dimensions) 316 | end 317 | 318 | it 'filters' do 319 | expect(@image.filter('bump_map( map=tint(red=50,tint=200), blue=10 );').dimensions).to eq(@image.dimensions) 320 | end 321 | 322 | %i[dynamic luminosity lightness average].each do |mode| 323 | it "desaturates in mode #{mode}" do 324 | @image.desaturate!(mode) 325 | pixel = @image[500, 500] 326 | expect(pixel.red).to eq(pixel.green) 327 | expect(pixel.blue).to eq(pixel.red) 328 | end 329 | end 330 | 331 | end 332 | 333 | end 334 | -------------------------------------------------------------------------------- /lib/rszr/image.rb: -------------------------------------------------------------------------------- 1 | module Rszr 2 | class Image 3 | GRAVITIES = [true, :center, :n, :nw, :w, :sw, :s, :se, :e, :ne].freeze 4 | BLENDING_MODES = %i[copy add subtract reshade].freeze 5 | DESATURATION_MODES = %i[dynamic luminosity lightness average].freeze 6 | 7 | extend Identification 8 | include Buffered 9 | include Orientation 10 | 11 | class << self 12 | 13 | def load(path, autorotate: Rszr.autorotate, **opts) 14 | path = path.to_s 15 | raise FileNotFound unless File.exist?(path) 16 | image = _load(path, opts[:immediately]) 17 | autorotate(image, path) if autorotate 18 | image 19 | end 20 | alias :open :load 21 | 22 | def load_data(data, autorotate: Rszr.autorotate, **opts) 23 | raise LoadError, 'Unknown format' unless format = identify(data) 24 | with_tempfile(format, data) do |file| 25 | load(file.path, autorotate: autorotate, **opts.merge(immediately: true)) 26 | end 27 | end 28 | 29 | end 30 | 31 | def dimensions 32 | [width, height] 33 | end 34 | 35 | def format 36 | fmt = _format 37 | fmt == 'jpg' ? 'jpeg' : fmt 38 | end 39 | 40 | def format=(fmt) 41 | fmt = fmt.to_s if fmt.is_a?(Symbol) 42 | self._format = fmt 43 | end 44 | 45 | def alpha? 46 | !!alpha 47 | end 48 | 49 | def [](x, y) 50 | if x >= 0 && x <= width - 1 && y >= 0 && y <= height - 1 51 | Color::RGBA.new(*_pixel(x, y)) 52 | end 53 | end 54 | 55 | def inspect 56 | fmt = format 57 | fmt = " #{fmt.upcase}" if fmt 58 | "#<#{self.class.name}:0x#{object_id.to_s(16)} #{width}x#{height}x#{alpha? ? 32 : 24}#{fmt}>" 59 | end 60 | 61 | module Transformations 62 | def resize(*args, **opts) 63 | _resize(false, *calculate_size(*args, **opts)) 64 | end 65 | 66 | def resize!(*args, **opts) 67 | _resize(true, *calculate_size(*args, **opts)) 68 | end 69 | 70 | def crop(x, y, width, height) 71 | _crop(false, x, y, width, height) 72 | end 73 | 74 | def crop!(x, y, width, height) 75 | _crop(true, x, y, width, height) 76 | end 77 | 78 | def turn(orientation) 79 | dup.turn!(orientation) 80 | end 81 | 82 | def turn!(orientation) 83 | orientation = orientation.abs + 2 if orientation.negative? 84 | _turn!(orientation % 4) 85 | end 86 | 87 | def rotate(deg) 88 | _rotate(false, deg.to_f * Math::PI / 180.0) 89 | end 90 | 91 | def rotate!(deg) 92 | _rotate(true, deg.to_f * Math::PI / 180.0) 93 | end 94 | 95 | # horizontal 96 | def flop 97 | dup.flop! 98 | end 99 | 100 | # vertical 101 | def flip 102 | dup.flip! 103 | end 104 | 105 | def sharpen(radius) 106 | dup.sharpen!(radius) 107 | end 108 | 109 | def sharpen!(radius) 110 | raise ArgumentError, 'illegal radius' if radius < 0 111 | _sharpen!(radius) 112 | end 113 | 114 | def blur(radius) 115 | dup.blur!(radius) 116 | end 117 | 118 | def blur!(radius) 119 | raise ArgumentError, 'illegal radius' if radius < 0 120 | _sharpen!(-radius) 121 | end 122 | 123 | def desaturate!(mode = :dynamic) 124 | _mode = DESATURATION_MODES.index(mode) 125 | raise ArgumentError, 'illegal mode' unless _mode 126 | _desaturate!(_mode); 127 | end 128 | 129 | def desaturate(*args, **opts) 130 | dup.desaturate!(*args, **opts) 131 | end 132 | 133 | def filter(filter_expr) 134 | dup.filter!(filter_expr) 135 | end 136 | 137 | def brighten!(value, r: nil, g: nil, b: nil, a: nil) 138 | raise ArgumentError, 'illegal brightness' if value > 1 || value < -1 139 | filter!("colormod(brightness=#{value.to_f});") 140 | end 141 | 142 | def brighten(*args, **opts) 143 | dup.brighten!(*args, **opts) 144 | end 145 | 146 | def contrast!(value, r: nil, g: nil, b: nil, a: nil) 147 | raise ArgumentError, 'illegal contrast (must be > 0)' if value < 0 148 | filter!("colormod(contrast=#{value.to_f});") 149 | end 150 | 151 | def contrast(*args, **opts) 152 | dup.contrast!(*args, **opts) 153 | end 154 | 155 | def gamma!(value, r: nil, g: nil, b: nil, a: nil) 156 | #raise ArgumentError, 'illegal gamma (must be > 0)' if value < 0 157 | filter!("colormod(gamma=#{value.to_f});") 158 | end 159 | 160 | def gamma(*args, **opts) 161 | dup.gamma!(*args, **opts) 162 | end 163 | 164 | def blend!(image, x, y, mode: :copy) 165 | raise ArgumentError, "mode must be one of #{BLENDING_MODES.map(&:to_s).join(', ')}" unless BLENDING_MODES.include?(mode) 166 | _blend(image, true, BLENDING_MODES.index(mode), 0, 0, image.width, image.height, x, y, image.width, image.height) 167 | end 168 | 169 | def blend(*args, **opts) 170 | dup.blend!(*args, **opts) 171 | end 172 | 173 | def rectangle!(coloring, x, y, w, h) 174 | raise ArgumentError, "coloring must respond to to_fill" unless coloring.respond_to?(:to_fill) 175 | _rectangle!(coloring.to_fill, x, y, w, h) 176 | end 177 | 178 | def rectangle(*args, **opts) 179 | dup.rectangle!(*args, **opts) 180 | end 181 | 182 | def fill!(coloring) 183 | raise ArgumentError, "coloring must respond to to_fill" unless coloring.respond_to?(:to_fill) 184 | rectangle!(coloring, 0, 0, width, height) 185 | end 186 | 187 | def fill(*args, **opts) 188 | dup.fill(*args, **opts) 189 | end 190 | end 191 | 192 | include Transformations 193 | 194 | def initialize(width, height, alpha: false, background: nil) 195 | raise ArgumentError, 'illegal image dimensions' if width < 1 || width > 32766 || height < 1 || height > 32766 196 | raise ArgumentError, 'background must respond to to_fill' if background && !(background.respond_to?(:to_fill)) 197 | _initialize(width, height).tap do |image| 198 | image.alpha = alpha 199 | image.fill!(background) if background 200 | end 201 | end 202 | 203 | def save(path, format: nil, quality: nil, interlace: false) 204 | format ||= format_from_filename(path) || self.format || 'jpg' 205 | raise ArgumentError, "invalid quality #{quality.inspect}" if quality && !(0..100).cover?(quality) 206 | ensure_path_is_writable(path) 207 | _save(path.to_s, format.to_s, quality, interlace) 208 | end 209 | 210 | def save_data(format: nil, quality: nil) 211 | format ||= self.format || 'jpg' 212 | with_tempfile(format) do |file| 213 | save(file.path, format: format, quality: quality) 214 | file.rewind 215 | file.read 216 | end 217 | end 218 | 219 | private 220 | 221 | # 0.5 0 < scale < 1 222 | # 400, 300 fit box 223 | # 400, :auto fit width, auto height 224 | # :auto, 300 auto width, fit height 225 | # 400, 300, crop: :center_middle 226 | # 400, 300, background: rgba 227 | # 400, 300, skew: true 228 | 229 | def calculate_size(*args, crop: nil, skew: nil, inflate: true) 230 | #options = args.last.is_a?(Hash) ? args.pop : {} 231 | #assert_valid_keys options, :crop, :background, :skew #:extend, :width, :height, :max_width, :max_height, :box 232 | if args.size == 1 233 | calculate_size_for_scale(args.first) 234 | elsif args.size == 2 235 | box_width, box_height = args 236 | if args.include?(:auto) 237 | calculate_size_for_auto(box_width, box_height) 238 | elsif box_width.is_a?(Numeric) && box_height.is_a?(Numeric) 239 | if not inflate and width <= box_width and height <= box_height 240 | [0, 0, width, height, width, height] 241 | elsif skew 242 | calculate_size_for_skew(box_width, box_height) 243 | elsif crop 244 | calculate_size_for_crop(box_width, box_height, crop) 245 | else 246 | calculate_size_for_limit(box_width, box_height) 247 | end 248 | end 249 | else 250 | raise ArgumentError, "wrong number of arguments (#{args.size} for 1..2)" 251 | end 252 | end 253 | 254 | def calculate_size_for_scale(factor) 255 | raise ArgumentError, "scale factor #{factor.inspect} out of range" unless factor > 0 && factor < 1 256 | [0, 0, width, height, (width.to_f * factor).round, (height.to_f * factor).round] 257 | end 258 | 259 | def calculate_size_for_skew(box_width, box_height) 260 | [0, 0, width, height, box_width, box_height] 261 | end 262 | 263 | def calculate_size_for_auto(box_width, box_height) 264 | if :auto == box_width && box_height.is_a?(Numeric) 265 | new_height = box_height 266 | new_width = (box_height.to_f / height.to_f * width.to_f).round 267 | elsif box_width.is_a?(Numeric) && :auto == box_height 268 | new_width = box_width 269 | new_height = (box_width.to_f / width.to_f * height.to_f).round 270 | else 271 | raise ArgumentError, "unconclusive arguments #{box_width.inspect}, #{box_height.inspect}" 272 | end 273 | [0, 0, width, height, new_width, new_height] 274 | end 275 | 276 | def calculate_size_for_crop(box_width, box_height, crop) 277 | raise ArgumentError, "invalid crop gravity" unless GRAVITIES.include?(crop) 278 | aspect = width.to_f / height.to_f 279 | box_aspect = box_width.to_f / box_height.to_f 280 | if aspect >= box_aspect # wider than box 281 | src_width = (box_width.to_f * height.to_f / box_height.to_f).round 282 | src_height = height 283 | x = crop_horizontally(src_width, crop) 284 | y = 0 285 | else # narrower than box 286 | src_width = width 287 | src_height = (box_height.to_f * width.to_f / box_width.to_f).round 288 | x = 0 289 | y = crop_vertically(src_height, crop) 290 | end 291 | [x, y, src_width, src_height, box_width, box_height] 292 | end 293 | 294 | def crop_horizontally(src_width, crop) 295 | case crop 296 | when :nw, :w, :sw then 0 297 | when :ne, :e, :se then width - src_width 298 | else 299 | ((width - src_width).to_f / 2.to_f).round 300 | end 301 | end 302 | 303 | def crop_vertically(src_height, crop) 304 | case crop 305 | when :nw, :n, :ne then 0 306 | when :sw, :s, :se then height - src_height 307 | else 308 | ((height - src_height).to_f / 2.to_f).round 309 | end 310 | end 311 | 312 | def calculate_size_for_limit(box_width, box_height) 313 | scale = width.to_f / height.to_f 314 | box_scale = box_width.to_f / box_height.to_f 315 | if scale >= box_scale # wider 316 | new_width = box_width 317 | new_height = (height.to_f * box_width.to_f / width.to_f).round 318 | else # narrower 319 | new_height = box_height 320 | new_width = (width.to_f * box_height.to_f / height.to_f).round 321 | end 322 | [0, 0, width, height, new_width, new_height] 323 | end 324 | 325 | def format_from_filename(path) 326 | if extension = File.extname(path)[1..-1] 327 | extension.downcase 328 | end 329 | end 330 | 331 | def ensure_path_is_writable(path) 332 | path = Pathname.new(path) 333 | path.dirname.realpath.writable? 334 | rescue Errno::ENOENT => e 335 | raise SaveError, 'Non-existant path component' 336 | rescue SystemCallError => e 337 | raise SaveError, e.message 338 | end 339 | 340 | def assert_valid_keys(hsh, *valid_keys) 341 | if unknown_key = (hsh.keys - valid_keys).first 342 | raise ArgumentError.new("Unknown key: #{unknown_key.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}") 343 | end 344 | end 345 | 346 | end 347 | end 348 | -------------------------------------------------------------------------------- /ext/rszr/image.c: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_RSZR_IMAGE 2 | #define RUBY_RSZR_IMAGE 3 | 4 | #include "rszr.h" 5 | #include "image.h" 6 | #include "errors.h" 7 | 8 | VALUE cImage = Qnil; 9 | VALUE cColorBase = Qnil; 10 | VALUE cColorGradient = Qnil; 11 | VALUE cColorPoint = Qnil; 12 | VALUE cFill = Qnil; 13 | 14 | 15 | static void rszr_free_image(Imlib_Image image) 16 | { 17 | imlib_context_set_image(image); 18 | imlib_free_image(); 19 | } 20 | 21 | 22 | static void rszr_image_deallocate(rszr_image_handle * handle) 23 | { 24 | // fprintf(stderr, "rszr_image_deallocate"); 25 | if (handle->image) { 26 | // fprintf(stderr, ": freeing"); 27 | rszr_free_image(handle->image); 28 | handle->image = NULL; 29 | } 30 | free(handle); 31 | // fprintf(stderr, "\n"); 32 | } 33 | 34 | static VALUE rszr_image_s_allocate(VALUE klass) 35 | { 36 | rszr_image_handle * handle = calloc(1, sizeof(rszr_image_handle)); 37 | return Data_Wrap_Struct(klass, NULL, rszr_image_deallocate, handle); 38 | } 39 | 40 | 41 | static VALUE rszr_image__initialize(VALUE self, VALUE rb_width, VALUE rb_height) 42 | { 43 | rszr_image_handle * handle; 44 | 45 | Check_Type(rb_width, T_FIXNUM); 46 | Check_Type(rb_height, T_FIXNUM); 47 | 48 | Data_Get_Struct(self, rszr_image_handle, handle); 49 | 50 | handle->image = imlib_create_image(FIX2INT(rb_width), FIX2INT(rb_height)); 51 | 52 | return self; 53 | } 54 | 55 | 56 | static VALUE rszr_image_s__load(VALUE klass, VALUE rb_path, VALUE rb_immediately) 57 | { 58 | rszr_image_handle * handle; 59 | Imlib_Image image; 60 | char * path; 61 | Imlib_Load_Error error; 62 | VALUE oImage; 63 | 64 | path = StringValueCStr(rb_path); 65 | 66 | imlib_set_cache_size(0); 67 | if (RTEST(rb_immediately)) { 68 | image = imlib_load_image_immediately_without_cache(path); 69 | } else { 70 | image = imlib_load_image_without_cache(path); 71 | } 72 | 73 | if (!image) { 74 | image = imlib_load_image_with_error_return(path, &error); 75 | 76 | if (!image) { 77 | rszr_raise_load_error(error); 78 | return Qnil; 79 | } 80 | } 81 | 82 | imlib_context_set_image(image); 83 | imlib_image_set_irrelevant_format(0); 84 | 85 | oImage = rszr_image_s_allocate(cImage); 86 | Data_Get_Struct(oImage, rszr_image_handle, handle); 87 | handle->image = image; 88 | return oImage; 89 | } 90 | 91 | 92 | static VALUE rszr_image__format_get(VALUE self) 93 | { 94 | rszr_image_handle * handle; 95 | char * format; 96 | 97 | Data_Get_Struct(self, rszr_image_handle, handle); 98 | 99 | imlib_context_set_image(handle->image); 100 | format = imlib_image_format(); 101 | 102 | if (format) { 103 | return rb_str_new2(format); 104 | } else { 105 | return Qnil; 106 | } 107 | } 108 | 109 | static VALUE rszr_image__format_set(VALUE self, VALUE rb_format) 110 | { 111 | rszr_image_handle * handle; 112 | char * format = StringValueCStr(rb_format); 113 | 114 | Data_Get_Struct(self, rszr_image_handle, handle); 115 | 116 | imlib_context_set_image(handle->image); 117 | imlib_image_set_format(format); 118 | 119 | return self; 120 | } 121 | 122 | 123 | static void rszr_image_color_set(VALUE rb_color) 124 | { 125 | int r, g, b, a; 126 | 127 | if(!rb_obj_is_kind_of(rb_color, cColorBase) || RBASIC_CLASS(rb_color) == cColorBase) { 128 | rb_raise(rb_eArgError, "color must descend from Rszr::Color::Base"); 129 | } 130 | 131 | r = FIX2INT(rb_funcall(rb_color, rb_intern("red"), 0)); 132 | g = FIX2INT(rb_funcall(rb_color, rb_intern("green"), 0)); 133 | b = FIX2INT(rb_funcall(rb_color, rb_intern("blue"), 0)); 134 | a = FIX2INT(rb_funcall(rb_color, rb_intern("alpha"), 0)); 135 | 136 | // TODO: use color model specific setter function 137 | imlib_context_set_color(r, g, b, a); 138 | } 139 | 140 | 141 | static VALUE rszr_image_alpha_get(VALUE self) 142 | { 143 | rszr_image_handle * handle; 144 | 145 | Data_Get_Struct(self, rszr_image_handle, handle); 146 | 147 | imlib_context_set_image(handle->image); 148 | if (imlib_image_has_alpha()) { 149 | return Qtrue; 150 | } 151 | 152 | return Qfalse; 153 | } 154 | 155 | static VALUE rszr_image_alpha_set(VALUE self, VALUE rb_alpha) 156 | { 157 | rszr_image_handle * handle; 158 | 159 | Data_Get_Struct(self, rszr_image_handle, handle); 160 | 161 | imlib_context_set_image(handle->image); 162 | imlib_image_set_has_alpha(RTEST(rb_alpha) ? 1 : 0); 163 | 164 | return Qnil; 165 | } 166 | 167 | 168 | static VALUE rszr_image_width(VALUE self) 169 | { 170 | rszr_image_handle * handle; 171 | int width; 172 | 173 | Data_Get_Struct(self, rszr_image_handle, handle); 174 | 175 | imlib_context_set_image(handle->image); 176 | width = imlib_image_get_width(); 177 | 178 | return INT2NUM(width); 179 | } 180 | 181 | 182 | static VALUE rszr_image_height(VALUE self) 183 | { 184 | rszr_image_handle * handle; 185 | int height; 186 | 187 | Data_Get_Struct(self, rszr_image_handle, handle); 188 | 189 | imlib_context_set_image(handle->image); 190 | height = imlib_image_get_height(); 191 | 192 | return INT2NUM(height); 193 | } 194 | 195 | 196 | static VALUE rszr_image__pixel_get(VALUE self, VALUE rb_x, VALUE rb_y) 197 | { 198 | rszr_image_handle * handle; 199 | Imlib_Color color_return; 200 | VALUE rb_rgba; 201 | int x, y; 202 | 203 | Check_Type(rb_x, T_FIXNUM); 204 | x = FIX2INT(rb_x); 205 | Check_Type(rb_y, T_FIXNUM); 206 | y = FIX2INT(rb_y); 207 | 208 | Data_Get_Struct(self, rszr_image_handle, handle); 209 | 210 | imlib_context_set_image(handle->image); 211 | imlib_image_query_pixel(x, y, &color_return); 212 | 213 | rb_rgba = rb_ary_new3(4, INT2NUM(color_return.red), 214 | INT2NUM(color_return.green), 215 | INT2NUM(color_return.blue), 216 | INT2NUM(color_return.alpha)); 217 | return rb_rgba; 218 | } 219 | 220 | /* 221 | static VALUE rszr_image_get_quality(VALUE self) 222 | { 223 | rszr_image_handle * handle; 224 | int quality; 225 | 226 | Data_Get_Struct(self, rszr_image_handle, handle); 227 | 228 | imlib_context_set_image(handle->image); 229 | quality = imlib_image_get_attached_value("quality"); 230 | 231 | if (quality) { 232 | return INT2NUM(quality); 233 | } else { 234 | return Qnil; 235 | } 236 | } 237 | 238 | static VALUE rszr_image_set_quality(VALUE self, VALUE rb_quality) 239 | { 240 | rszr_image_handle * handle; 241 | int quality; 242 | 243 | Check_Type(rb_quality, T_FIXNUM); 244 | quality = FIX2INT(rb_quality); 245 | if (quality <= 0) { 246 | rb_raise(rb_eArgError, "quality must be >= 0"); 247 | return Qnil; 248 | } 249 | 250 | Data_Get_Struct(self, rszr_image_handle, handle); 251 | 252 | imlib_context_set_image(handle->image); 253 | imlib_image_attach_data_value("quality", NULL, quality, NULL); 254 | 255 | return INT2NUM(quality); 256 | } 257 | */ 258 | 259 | 260 | static VALUE rszr_image_dup(VALUE self) 261 | { 262 | rszr_image_handle * handle; 263 | rszr_image_handle * cloned_handle; 264 | Imlib_Image cloned_image; 265 | VALUE oClonedImage; 266 | 267 | Data_Get_Struct(self, rszr_image_handle, handle); 268 | 269 | imlib_context_set_image(handle->image); 270 | cloned_image = imlib_clone_image(); 271 | 272 | if (!cloned_image) { 273 | rb_raise(eRszrTransformationError, "error cloning image"); 274 | return Qnil; 275 | } 276 | 277 | oClonedImage = rszr_image_s_allocate(cImage); 278 | Data_Get_Struct(oClonedImage, rszr_image_handle, cloned_handle); 279 | cloned_handle->image = cloned_image; 280 | 281 | return oClonedImage; 282 | } 283 | 284 | 285 | static VALUE rszr_image__turn_bang(VALUE self, VALUE orientation) 286 | { 287 | rszr_image_handle * handle; 288 | 289 | Data_Get_Struct(self, rszr_image_handle, handle); 290 | 291 | imlib_context_set_image(handle->image); 292 | imlib_image_orientate(NUM2INT(orientation)); 293 | 294 | return self; 295 | } 296 | 297 | 298 | static VALUE rszr_image_flop_bang(VALUE self) 299 | { 300 | rszr_image_handle * handle; 301 | 302 | Data_Get_Struct(self, rszr_image_handle, handle); 303 | 304 | imlib_context_set_image(handle->image); 305 | imlib_image_flip_horizontal(); 306 | 307 | return self; 308 | } 309 | 310 | 311 | static VALUE rszr_image_flip_bang(VALUE self) 312 | { 313 | rszr_image_handle * handle; 314 | 315 | Data_Get_Struct(self, rszr_image_handle, handle); 316 | 317 | imlib_context_set_image(handle->image); 318 | imlib_image_flip_vertical(); 319 | 320 | return self; 321 | } 322 | 323 | 324 | static VALUE rszr_image__rotate(VALUE self, VALUE bang, VALUE rb_angle) 325 | { 326 | rszr_image_handle * handle; 327 | rszr_image_handle * rotated_handle; 328 | Imlib_Image rotated_image; 329 | VALUE oRotatedImage; 330 | double angle; 331 | 332 | angle = NUM2DBL(rb_angle); 333 | 334 | Data_Get_Struct(self, rszr_image_handle, handle); 335 | 336 | imlib_context_set_image(handle->image); 337 | rotated_image = imlib_create_rotated_image(angle); 338 | 339 | if (!rotated_image) { 340 | rb_raise(eRszrTransformationError, "error rotating image"); 341 | return Qnil; 342 | } 343 | 344 | if (RTEST(bang)) { 345 | rszr_free_image(handle->image); 346 | handle->image = rotated_image; 347 | 348 | return self; 349 | } 350 | else { 351 | oRotatedImage = rszr_image_s_allocate(cImage); 352 | Data_Get_Struct(oRotatedImage, rszr_image_handle, rotated_handle); 353 | rotated_handle->image = rotated_image; 354 | 355 | return oRotatedImage; 356 | } 357 | } 358 | 359 | 360 | static VALUE rszr_image_filter_bang(VALUE self, VALUE rb_filter_expr) 361 | { 362 | rszr_image_handle * handle; 363 | char * filter_expr; 364 | 365 | filter_expr = StringValueCStr(rb_filter_expr); 366 | 367 | Data_Get_Struct(self, rszr_image_handle, handle); 368 | 369 | imlib_context_set_image(handle->image); 370 | imlib_apply_filter(filter_expr); 371 | 372 | return self; 373 | } 374 | 375 | 376 | static VALUE rszr_image__sharpen_bang(VALUE self, VALUE rb_radius) 377 | { 378 | rszr_image_handle * handle; 379 | int radius; 380 | 381 | radius = NUM2INT(rb_radius); 382 | 383 | Data_Get_Struct(self, rszr_image_handle, handle); 384 | 385 | imlib_context_set_image(handle->image); 386 | 387 | if (radius >= 0) { 388 | imlib_image_sharpen(radius); 389 | } else { 390 | imlib_image_blur(-radius); 391 | } 392 | 393 | return self; 394 | } 395 | 396 | 397 | static void rszr_desaturate_pixel(rszr_raw_pixel * pixel, int mode) 398 | { 399 | uint8_t grey; 400 | if (mode == 2 || (mode == 0 && (pixel->blue > pixel->red && pixel->blue > pixel->green))) { 401 | // lightness 402 | grey = (pixel->blue + (pixel->red > pixel->green ? pixel->green : pixel->red)) / 2; 403 | } else if (mode == 1 || mode == 0) { 404 | // luminosity 405 | grey = 0.21 * pixel->red + 0.72 * pixel->green + 0.07 * pixel->blue; 406 | } else { 407 | // average 408 | grey = (pixel->red + pixel->green + pixel->blue) / 3; 409 | } 410 | pixel->red = grey; 411 | pixel->green = grey; 412 | pixel->blue = grey; 413 | } 414 | 415 | static VALUE rszr_image__desaturate_bang(VALUE self, VALUE rb_mode) 416 | { 417 | rszr_image_handle * handle; 418 | rszr_raw_pixel * pixels; 419 | uint64_t size; 420 | int mode; 421 | 422 | mode = NUM2INT(rb_mode); 423 | 424 | Data_Get_Struct(self, rszr_image_handle, handle); 425 | 426 | imlib_context_set_image(handle->image); 427 | 428 | pixels = (rszr_raw_pixel *) imlib_image_get_data(); 429 | if (pixels == NULL) { 430 | rb_raise(eRszrTransformationError, "error desaturating image"); 431 | return Qnil; 432 | } 433 | 434 | size = imlib_image_get_width() * imlib_image_get_height(); 435 | for (uint64_t i = 0; i < size; i++) { 436 | rszr_desaturate_pixel(&pixels[i], mode); 437 | } 438 | 439 | imlib_image_put_back_data((uint32_t *) pixels); 440 | 441 | return self; 442 | } 443 | 444 | 445 | static Imlib_Image rszr_create_cropped_scaled_image(const Imlib_Image image, VALUE rb_src_x, VALUE rb_src_y, VALUE rb_src_w, VALUE rb_src_h, VALUE rb_dst_w, VALUE rb_dst_h) 446 | { 447 | Imlib_Image resized_image; 448 | 449 | int src_x = NUM2INT(rb_src_x); 450 | int src_y = NUM2INT(rb_src_y); 451 | int src_w = NUM2INT(rb_src_w); 452 | int src_h = NUM2INT(rb_src_h); 453 | int dst_w = NUM2INT(rb_dst_w); 454 | int dst_h = NUM2INT(rb_dst_h); 455 | 456 | // TODO: raise if <= 0 457 | 458 | imlib_context_set_image(image); 459 | imlib_context_set_anti_alias(1); 460 | imlib_context_set_dither(1); 461 | resized_image = imlib_create_cropped_scaled_image(src_x, src_y, src_w, src_h, dst_w, dst_h); 462 | 463 | if (!resized_image) { 464 | rb_raise(eRszrTransformationError, "error resizing image"); 465 | return NULL; 466 | } 467 | 468 | return resized_image; 469 | } 470 | 471 | 472 | static VALUE rszr_image__resize(VALUE self, VALUE bang, VALUE rb_src_x, VALUE rb_src_y, VALUE rb_src_w, VALUE rb_src_h, VALUE rb_dst_w, VALUE rb_dst_h) 473 | { 474 | rszr_image_handle * handle; 475 | Imlib_Image resized_image; 476 | rszr_image_handle * resized_handle; 477 | VALUE oResizedImage; 478 | 479 | Data_Get_Struct(self, rszr_image_handle, handle); 480 | 481 | resized_image = rszr_create_cropped_scaled_image(handle->image, rb_src_x, rb_src_y, rb_src_w, rb_src_h, rb_dst_w, rb_dst_h); 482 | if (!resized_image) return Qfalse; 483 | 484 | if (RTEST(bang)) { 485 | rszr_free_image(handle->image); 486 | handle->image = resized_image; 487 | 488 | return self; 489 | } 490 | else { 491 | oResizedImage = rszr_image_s_allocate(cImage); 492 | Data_Get_Struct(oResizedImage, rszr_image_handle, resized_handle); 493 | resized_handle->image = resized_image; 494 | 495 | return oResizedImage; 496 | } 497 | } 498 | 499 | 500 | static Imlib_Image rszr_create_cropped_image(const Imlib_Image image, VALUE rb_x, VALUE rb_y, VALUE rb_w, VALUE rb_h) 501 | { 502 | Imlib_Image cropped_image; 503 | 504 | Check_Type(rb_x, T_FIXNUM); 505 | Check_Type(rb_y, T_FIXNUM); 506 | Check_Type(rb_w, T_FIXNUM); 507 | Check_Type(rb_h, T_FIXNUM); 508 | 509 | int x = NUM2INT(rb_x); 510 | int y = NUM2INT(rb_y); 511 | int w = NUM2INT(rb_w); 512 | int h = NUM2INT(rb_h); 513 | 514 | imlib_context_set_image(image); 515 | cropped_image = imlib_create_cropped_image(x, y, w, h); 516 | 517 | if (!cropped_image) { 518 | rb_raise(eRszrTransformationError, "error cropping image"); 519 | return NULL; 520 | } 521 | 522 | return cropped_image; 523 | } 524 | 525 | 526 | static VALUE rszr_image__crop(VALUE self, VALUE bang, VALUE rb_x, VALUE rb_y, VALUE rb_w, VALUE rb_h) 527 | { 528 | rszr_image_handle * handle; 529 | Imlib_Image cropped_image; 530 | rszr_image_handle * cropped_handle; 531 | VALUE oCroppedImage; 532 | 533 | Data_Get_Struct(self, rszr_image_handle, handle); 534 | 535 | cropped_image = rszr_create_cropped_image(handle->image, rb_x, rb_y, rb_w, rb_h); 536 | if (!cropped_image) return Qfalse; 537 | 538 | if (RTEST(bang)) { 539 | rszr_free_image(handle->image); 540 | handle->image = cropped_image; 541 | 542 | return self; 543 | } 544 | else { 545 | oCroppedImage = rszr_image_s_allocate(cImage); 546 | Data_Get_Struct(oCroppedImage, rszr_image_handle, cropped_handle); 547 | cropped_handle->image = cropped_image; 548 | 549 | return oCroppedImage; 550 | } 551 | } 552 | 553 | 554 | static VALUE rszr_image__blend(VALUE self, VALUE other, VALUE rb_merge_alpha, VALUE rb_mode, 555 | VALUE rb_src_x, VALUE rb_src_y, VALUE rb_src_w, VALUE rb_src_h, 556 | VALUE rb_dst_x, VALUE rb_dst_y, VALUE rb_dst_w, VALUE rb_dst_h) 557 | { 558 | rszr_image_handle * handle; 559 | rszr_image_handle * other_handle; 560 | Imlib_Operation operation; 561 | 562 | Check_Type(rb_mode, T_FIXNUM); 563 | Check_Type(rb_src_x, T_FIXNUM); 564 | Check_Type(rb_src_y, T_FIXNUM); 565 | Check_Type(rb_src_w, T_FIXNUM); 566 | Check_Type(rb_src_h, T_FIXNUM); 567 | Check_Type(rb_dst_x, T_FIXNUM); 568 | Check_Type(rb_dst_y, T_FIXNUM); 569 | Check_Type(rb_dst_w, T_FIXNUM); 570 | Check_Type(rb_dst_h, T_FIXNUM); 571 | 572 | operation = (Imlib_Operation) NUM2INT(rb_mode); 573 | int src_x = NUM2INT(rb_src_x); 574 | int src_y = NUM2INT(rb_src_y); 575 | int src_w = NUM2INT(rb_src_w); 576 | int src_h = NUM2INT(rb_src_h); 577 | int dst_x = NUM2INT(rb_dst_x); 578 | int dst_y = NUM2INT(rb_dst_y); 579 | int dst_w = NUM2INT(rb_dst_w); 580 | int dst_h = NUM2INT(rb_dst_h); 581 | 582 | char merge_alpha = RTEST(rb_merge_alpha) ? 1 : 0; 583 | 584 | Data_Get_Struct(self, rszr_image_handle, handle); 585 | Data_Get_Struct(other, rszr_image_handle, other_handle); 586 | 587 | imlib_context_set_image(handle->image); 588 | imlib_context_set_operation(operation); 589 | imlib_blend_image_onto_image(other_handle->image, merge_alpha, src_x, src_y, src_w, src_h, dst_x, dst_y, dst_w, dst_h); 590 | 591 | return self; 592 | } 593 | 594 | 595 | static Imlib_Color_Range rszr_image_init_color_range(VALUE rb_gradient) 596 | { 597 | Imlib_Color_Range range; 598 | VALUE rb_points; 599 | VALUE rb_point; 600 | VALUE rb_color; 601 | int size, i; 602 | double position; 603 | int red, green, blue, alpha; 604 | 605 | if(!rb_obj_is_kind_of(rb_gradient, cColorGradient)) { 606 | rb_raise(rb_eArgError, "color must be a Rszr::Color::Gradient"); 607 | } 608 | 609 | rb_points = rb_funcall(rb_gradient, rb_intern("points"), 0); 610 | Check_Type(rb_points, T_ARRAY); 611 | 612 | imlib_context_get_color(&red, &green, &blue, &alpha); 613 | 614 | range = imlib_create_color_range(); 615 | imlib_context_set_color_range(range); 616 | 617 | size = RARRAY_LEN(rb_points); 618 | for (i = 0; i < size; i++) { 619 | rb_point = rb_ary_entry(rb_points, i); 620 | if(!rb_obj_is_kind_of(rb_point, cColorPoint)) 621 | rb_raise(rb_eArgError, "point must be a Rszr::Color::Point"); 622 | 623 | rb_color = rb_funcall(rb_point, rb_intern("color"), 0); 624 | if(!rb_obj_is_kind_of(rb_color, cColorBase) || RBASIC_CLASS(rb_color) == cColorBase) 625 | rb_raise(rb_eArgError, "color must descend from Rszr::Color::Base"); 626 | 627 | position = NUM2DBL(rb_funcall(rb_point, rb_intern("position"), 0)); 628 | 629 | rszr_image_color_set(rb_color); 630 | imlib_add_color_to_color_range(position * 255); 631 | } 632 | 633 | imlib_context_set_color(red, green, blue, alpha); 634 | 635 | return range; 636 | } 637 | 638 | 639 | static VALUE rszr_image__rectangle_bang(VALUE self, VALUE rb_fill, VALUE rb_x, VALUE rb_y, VALUE rb_w, VALUE rb_h) 640 | { 641 | rszr_image_handle * handle; 642 | VALUE rb_gradient; 643 | VALUE rb_color; 644 | Imlib_Color_Range range; 645 | double angle; 646 | 647 | Check_Type(rb_x, T_FIXNUM); 648 | Check_Type(rb_y, T_FIXNUM); 649 | Check_Type(rb_w, T_FIXNUM); 650 | Check_Type(rb_h, T_FIXNUM); 651 | 652 | int x = NUM2INT(rb_x); 653 | int y = NUM2INT(rb_y); 654 | int w = NUM2INT(rb_w); 655 | int h = NUM2INT(rb_h); 656 | 657 | rb_gradient = rb_funcall(rb_fill, rb_intern("gradient"), 0); 658 | rb_color = rb_funcall(rb_fill, rb_intern("color"), 0); 659 | 660 | Data_Get_Struct(self, rszr_image_handle, handle); 661 | imlib_context_set_image(handle->image); 662 | 663 | if (!NIL_P(rb_gradient)) { 664 | angle = NUM2DBL(rb_funcall(rb_fill, rb_intern("angle"), 0)); 665 | range = rszr_image_init_color_range(rb_gradient); 666 | imlib_image_fill_color_range_rectangle(x, y, w, h, angle); 667 | imlib_free_color_range(); 668 | } else if (!NIL_P(rb_color)) { 669 | rszr_image_color_set(rb_color); 670 | imlib_image_fill_rectangle(x, y, w, h); 671 | } 672 | 673 | return self; 674 | } 675 | 676 | 677 | static VALUE rszr_image__save(VALUE self, VALUE rb_path, VALUE rb_format, VALUE rb_quality, VALUE rb_interlace) 678 | { 679 | rszr_image_handle * handle; 680 | char * path; 681 | char * format; 682 | int quality; 683 | Imlib_Load_Error save_error; 684 | 685 | path = StringValueCStr(rb_path); 686 | format = StringValueCStr(rb_format); 687 | quality = (NIL_P(rb_quality)) ? 0 : FIX2INT(rb_quality); 688 | 689 | Data_Get_Struct(self, rszr_image_handle, handle); 690 | 691 | imlib_context_set_image(handle->image); 692 | imlib_image_set_format(format); 693 | 694 | if (quality) 695 | imlib_image_attach_data_value("quality", NULL, quality, NULL); 696 | 697 | imlib_image_remove_attached_data_value("interlacing"); 698 | if (RTEST(rb_interlace)) 699 | imlib_image_attach_data_value("interlacing", NULL, 1, NULL); 700 | 701 | imlib_save_image_with_error_return(path, &save_error); 702 | 703 | if (save_error) { 704 | rszr_raise_save_error(save_error); 705 | return Qfalse; 706 | } 707 | 708 | return Qtrue; 709 | } 710 | 711 | void Init_rszr_image() 712 | { 713 | cImage = rb_define_class_under(mRszr, "Image", rb_cObject); 714 | 715 | cColorBase = rb_path2class("Rszr::Color::Base"); 716 | cColorGradient = rb_path2class("Rszr::Color::Gradient"); 717 | cColorPoint = rb_path2class("Rszr::Color::Point"); 718 | cFill = rb_path2class("Rszr::Fill"); 719 | 720 | rb_define_alloc_func(cImage, rszr_image_s_allocate); 721 | 722 | // Class methods 723 | rb_define_private_method(rb_singleton_class(cImage), "_load", rszr_image_s__load, 2); 724 | 725 | // Instance methods 726 | rb_define_method(cImage, "width", rszr_image_width, 0); 727 | rb_define_method(cImage, "height", rszr_image_height, 0); 728 | rb_define_method(cImage, "dup", rszr_image_dup, 0); 729 | rb_define_method(cImage, "filter!", rszr_image_filter_bang, 1); 730 | rb_define_method(cImage, "flop!", rszr_image_flop_bang, 0); 731 | rb_define_method(cImage, "flip!", rszr_image_flip_bang, 0); 732 | 733 | rb_define_method(cImage, "alpha", rszr_image_alpha_get, 0); 734 | rb_define_method(cImage, "alpha=", rszr_image_alpha_set, 1); 735 | 736 | rb_define_protected_method(cImage, "_format", rszr_image__format_get, 0); 737 | rb_define_protected_method(cImage, "_format=", rszr_image__format_set, 1); 738 | 739 | rb_define_private_method(cImage, "_initialize", rszr_image__initialize, 2); 740 | rb_define_private_method(cImage, "_resize", rszr_image__resize, 7); 741 | rb_define_private_method(cImage, "_crop", rszr_image__crop, 5); 742 | rb_define_private_method(cImage, "_turn!", rszr_image__turn_bang, 1); 743 | rb_define_private_method(cImage, "_rotate", rszr_image__rotate, 2); 744 | rb_define_private_method(cImage, "_sharpen!", rszr_image__sharpen_bang, 1); 745 | rb_define_private_method(cImage, "_desaturate!", rszr_image__desaturate_bang, 1); 746 | rb_define_private_method(cImage, "_pixel", rszr_image__pixel_get, 2); 747 | rb_define_private_method(cImage, "_blend", rszr_image__blend, 11); 748 | rb_define_private_method(cImage, "_rectangle!", rszr_image__rectangle_bang, 5); 749 | 750 | rb_define_private_method(cImage, "_save", rszr_image__save, 4); 751 | } 752 | 753 | #endif 754 | --------------------------------------------------------------------------------