├── vendor ├── cmyk.icm └── sRGB_v4_ICC_preference.icc ├── samples ├── sample.gif ├── sample.jpg ├── sample.pdf ├── sample.png ├── sample.tif ├── sample_anim.gif ├── sample_cmyk.jpg ├── white pixel.png ├── landscape_sample.png └── sample.svg ├── lib ├── dragonfly_libvips │ ├── version.rb │ ├── analysers │ │ └── image_properties.rb │ ├── plugin.rb │ ├── processors │ │ ├── rotate.rb │ │ ├── extract_area.rb │ │ ├── encode.rb │ │ └── thumb.rb │ └── dimensions.rb └── dragonfly_libvips.rb ├── Gemfile ├── .gitignore ├── bin ├── setup └── console ├── Rakefile ├── Guardfile ├── test ├── support │ └── image_assertions.rb ├── dragonfly_libvips │ ├── analysers │ │ └── image_properties_test.rb │ ├── processors │ │ ├── extract_area_test.rb │ │ ├── rotate_test.rb │ │ ├── encode_test.rb │ │ └── thumb_test.rb │ ├── plugin_test.rb │ └── dimensions_test.rb └── test_helper.rb ├── .travis.yml ├── LICENSE.txt ├── dragonfly_libvips.gemspec ├── CHANGELOG.md └── README.md /vendor/cmyk.icm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/dragonfly_libvips/master/vendor/cmyk.icm -------------------------------------------------------------------------------- /samples/sample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/dragonfly_libvips/master/samples/sample.gif -------------------------------------------------------------------------------- /samples/sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/dragonfly_libvips/master/samples/sample.jpg -------------------------------------------------------------------------------- /samples/sample.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/dragonfly_libvips/master/samples/sample.pdf -------------------------------------------------------------------------------- /samples/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/dragonfly_libvips/master/samples/sample.png -------------------------------------------------------------------------------- /samples/sample.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/dragonfly_libvips/master/samples/sample.tif -------------------------------------------------------------------------------- /lib/dragonfly_libvips/version.rb: -------------------------------------------------------------------------------- 1 | module DragonflyLibvips 2 | VERSION = '2.3.3'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /samples/sample_anim.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/dragonfly_libvips/master/samples/sample_anim.gif -------------------------------------------------------------------------------- /samples/sample_cmyk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/dragonfly_libvips/master/samples/sample_cmyk.jpg -------------------------------------------------------------------------------- /samples/white pixel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/dragonfly_libvips/master/samples/white pixel.png -------------------------------------------------------------------------------- /samples/landscape_sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/dragonfly_libvips/master/samples/landscape_sample.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in dragonfly_libvips.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /vendor/sRGB_v4_ICC_preference.icc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/dragonfly_libvips/master/vendor/sRGB_v4_ICC_preference.icc -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.log -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /samples/sample.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList['test/**/*_test.rb'] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard :minitest do 5 | watch(%r{^lib/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" } 6 | watch(%r{^test/.+_test\.rb$}) 7 | watch(%r{^test/test_helper\.rb$}) { 'test' } 8 | end 9 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "dragonfly_libvips" 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 | -------------------------------------------------------------------------------- /test/support/image_assertions.rb: -------------------------------------------------------------------------------- 1 | require 'vips' 2 | 3 | def image_properties(content) 4 | 5 | img = Vips::Image.new_from_file(content.path, access: :sequential) 6 | 7 | { 8 | format: File.extname(img.filename)[1..-1], 9 | width: img.width, 10 | height: img.height, 11 | xres: img.xres, 12 | yres: img.yres, 13 | } 14 | end 15 | 16 | module MiniTest::Assertions 17 | [:width, :height, :format].each do |property| 18 | define_method "assert_#{property}" do |obj, value| 19 | assert_equal value, image_properties(obj)[property] 20 | end 21 | Object.infect_an_assertion "assert_#{property}", "must_have_#{property}", :reverse 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/dragonfly_libvips/analysers/image_properties_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe DragonflyLibvips::Analysers::ImageProperties do 4 | let(:app) { test_libvips_app } 5 | let(:analyser) { DragonflyLibvips::Analysers::ImageProperties.new } 6 | let(:png) { Dragonfly::Content.new(app, SAMPLES_DIR.join('sample.png')) } # 280x355 7 | let(:jpg) { Dragonfly::Content.new(app, SAMPLES_DIR.join('sample.jpg')) } # 280x355 8 | 9 | it { analyser.call(png).must_equal('format' => 'png', 'width' => 280, 'height' => 355, 'xres' => 72.0, 'yres' => 72.0, 'progressive' => false) } 10 | 11 | describe 'jpgs' do 12 | it { analyser.call(jpg)['progressive'].must_equal false } 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | 3 | require 'minitest' 4 | require 'minitest/autorun' 5 | require 'minitest/reporters' 6 | require 'minitest/spec' 7 | 8 | require 'dragonfly' 9 | require 'dragonfly_libvips' 10 | 11 | SAMPLES_DIR = Pathname.new(File.expand_path('../../samples', __FILE__)) 12 | 13 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 14 | 15 | Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new 16 | 17 | def test_app(name = nil) 18 | Dragonfly::App.instance(name).tap do |app| 19 | app.datastore = Dragonfly::MemoryDataStore.new 20 | app.secret = 'test secret' 21 | end 22 | end 23 | 24 | def test_libvips_app 25 | test_app.configure do 26 | plugin :libvips 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # libvips build instructions taken from https://github.com/marcbachmann/dockerfile-libvips 2 | 3 | language: ruby 4 | cache: bundler 5 | script: 'bundle exec rake' 6 | sudo: required 7 | dist: trusty 8 | rvm: 9 | - 2.6.1 10 | 11 | before_install: 12 | - gem update bundler 13 | - sudo apt-get update 14 | - sudo apt-get install -y gobject-introspection libgirepository1.0-dev libglib2.0-dev libpoppler-glib-dev libgif-dev 15 | - curl -OL https://github.com/libvips/libvips/releases/download/v8.7.4/vips-8.7.4.tar.gz 16 | - tar zxvf vips-8.7.4.tar.gz && cd vips-8.7.4 && ./configure $1 && sudo make && sudo make install 17 | - export GI_TYPELIB_PATH=/usr/local/lib/girepository-1.0/ 18 | - sudo ldconfig 19 | 20 | notifications: 21 | email: 22 | recipients: 23 | - tomas.celizna@gmail.com 24 | on_failure: change 25 | on_success: never 26 | -------------------------------------------------------------------------------- /lib/dragonfly_libvips.rb: -------------------------------------------------------------------------------- 1 | require 'dragonfly' 2 | require 'dragonfly_libvips/dimensions' 3 | require 'dragonfly_libvips/plugin' 4 | require 'dragonfly_libvips/version' 5 | require 'vips' 6 | 7 | module DragonflyLibvips 8 | class UnsupportedFormat < RuntimeError; end 9 | class UnsupportedOutputormat < RuntimeError; end 10 | 11 | CMYK_PROFILE_PATH = File.expand_path('../vendor/cmyk.icm', __dir__) 12 | EPROFILE_PATH = File.expand_path('../vendor/sRGB_v4_ICC_preference.icc', __dir__) 13 | 14 | SUPPORTED_FORMATS = begin 15 | output = `vips -l | grep -i ForeignLoad` 16 | output.scan(/\.(\w{1,4})/).flatten.compact.sort.map(&:downcase).uniq 17 | end 18 | 19 | SUPPORTED_OUTPUT_FORMATS = begin 20 | output = `vips -l | grep -i ForeignSave` 21 | output.scan(/\.(\w{1,4})/).flatten.compact.sort.map(&:downcase).uniq 22 | end - %w[ 23 | csv 24 | fit 25 | fits 26 | fts 27 | mat 28 | pbm 29 | pfm 30 | pgm 31 | ppm 32 | v 33 | vips 34 | webp 35 | ] 36 | 37 | FORMATS_WITHOUT_PROFILE_SUPPORT = %w[bmp dz gif hdr webp heic] 38 | end 39 | -------------------------------------------------------------------------------- /lib/dragonfly_libvips/analysers/image_properties.rb: -------------------------------------------------------------------------------- 1 | require 'vips' 2 | 3 | module DragonflyLibvips 4 | module Analysers 5 | class ImageProperties 6 | DPI = 300 7 | 8 | def call(content) 9 | return {} unless content.ext 10 | return {} unless SUPPORTED_FORMATS.include?(content.ext.downcase) 11 | 12 | input_options = {} 13 | input_options['access'] = 'sequential' 14 | input_options['autorotate'] = true if content.mime_type == 'image/jpeg' 15 | input_options['dpi'] = DPI if content.mime_type == 'application/pdf' 16 | 17 | img = ::Vips::Image.new_from_file(content.path, input_options) 18 | 19 | width = img.width 20 | height = img.height 21 | xres = img.xres 22 | yres = img.yres 23 | 24 | { 25 | 'format' => content.ext.to_s, 26 | 'width' => width, 27 | 'height' => height, 28 | 'xres' => xres, 29 | 'yres' => yres, 30 | 'progressive' => (content.mime_type == 'image/jpeg' && img.get('jpeg-multiscan') != 0) 31 | } 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tomas Celizna 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/dragonfly_libvips/processors/extract_area_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe DragonflyLibvips::Processors::ExtractArea do 4 | let(:app) { test_libvips_app } 5 | let(:content) { Dragonfly::Content.new(app, SAMPLES_DIR.join('sample.png')) } # 280x355 6 | let(:processor) { DragonflyLibvips::Processors::ExtractArea.new } 7 | 8 | let(:x) { 100 } 9 | let(:y) { 100 } 10 | let(:width) { 100 } 11 | let(:height) { 200 } 12 | 13 | describe 'keep format' do 14 | before { processor.call(content, x, y, width, height) } 15 | 16 | it { content.must_have_width width } 17 | it { content.must_have_height height } 18 | end 19 | 20 | describe 'convert to format' do 21 | before { processor.call(content, x, y, width, height, format: 'jpg') } 22 | 23 | it { content.must_have_width width } 24 | it { content.must_have_height height } 25 | it { content.ext.must_equal 'jpg' } 26 | end 27 | 28 | describe 'tempfile has extension' do 29 | let(:format) { 'jpg' } 30 | before { processor.call(content, x, y, width, height, format: format) } 31 | it { content.tempfile.path.must_match /\.#{format}\z/ } 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/dragonfly_libvips/processors/rotate_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe DragonflyLibvips::Processors::Rotate do 4 | let(:app) { test_libvips_app } 5 | let(:content) { Dragonfly::Content.new(app, SAMPLES_DIR.join('sample.png')) } # 280x355 6 | let(:processor) { DragonflyLibvips::Processors::Rotate.new } 7 | 8 | describe 'rotate 90' do 9 | before { processor.call(content, 90) } 10 | 11 | it { content.must_have_width 355 } 12 | it { content.must_have_height 280 } 13 | end 14 | 15 | describe 'rotate 180' do 16 | before { processor.call(content, 180) } 17 | 18 | it { content.must_have_width 280 } 19 | it { content.must_have_height 355 } 20 | end 21 | 22 | describe 'rotate 270' do 23 | before { processor.call(content, 270) } 24 | 25 | it { content.must_have_width 355 } 26 | it { content.must_have_height 280 } 27 | end 28 | 29 | describe 'rotate with format' do 30 | before { processor.call(content, 90, format: 'jpg') } 31 | 32 | it { content.must_have_width 355 } 33 | it { content.must_have_height 280 } 34 | it { content.ext.must_equal 'jpg' } 35 | end 36 | 37 | describe 'tempfile has extension' do 38 | let(:format) { 'jpg' } 39 | before { processor.call(content, 90, format: format) } 40 | it { content.tempfile.path.must_match /\.#{format}\z/ } 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /dragonfly_libvips.gemspec: -------------------------------------------------------------------------------- 1 | 2 | lib = File.expand_path('lib', __dir__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'dragonfly_libvips/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'dragonfly_libvips' 8 | spec.version = DragonflyLibvips::VERSION 9 | spec.authors = ['Tomas Celizna'] 10 | spec.email = ['tomas.celizna@gmail.com'] 11 | 12 | spec.summary = 'Dragonfly analysers and processors for libvips image processing library.' 13 | spec.homepage = 'https://github.com/tomasc/dragonfly_libvips' 14 | spec.license = 'MIT' 15 | 16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 17 | spec.bindir = 'exe' 18 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_dependency 'dragonfly', '~> 1.0' 22 | spec.add_dependency 'ruby-vips', '~> 2.0', '< 2.0.14' 23 | 24 | spec.add_development_dependency 'bundler', '~> 2.0' 25 | spec.add_development_dependency 'rb-readline' 26 | spec.add_development_dependency 'guard' 27 | spec.add_development_dependency 'guard-minitest' 28 | spec.add_development_dependency 'minitest', '~> 5.0' 29 | spec.add_development_dependency 'minitest-reporters' 30 | spec.add_development_dependency 'rake', '~> 10.0' 31 | end 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 2.3.3 4 | 5 | * locks `ruby-vips` to `< 2.0.14` 6 | * adds support for `libvips` `8.8.0` 7 | 8 | ## 2.3.2 9 | 10 | * FIX: support for .gif 11 | 12 | ## 2.3.1 13 | 14 | * improved `SUPPORTED_FORMATS` matching that ignores case 15 | 16 | ## 2.3.0 17 | 18 | * add support for progressive JPEGs 19 | 20 | ## 2.2.0 21 | 22 | * add `SUPPORTED_FORMATS` and `SUPPORTED_OUTPUT_FORMATS` and raise errors when formats are not matching 23 | * add more thorough tests for supported formats 24 | * skip unnecessary conversion from-to same format 25 | * add `extract_area` processor 26 | 27 | ## 2.1.3 28 | 29 | * make sure the downcase analyser survives nil 30 | 31 | ## 2.1.2 32 | 33 | * changed image properties analyser to downcase the image's format 34 | 35 | ## 2.1.1 36 | 37 | * add `CMYK` support using the `cmyk.icm` profile 38 | 39 | ## 2.1.0 40 | 41 | * `thumb` process refactored for `Vips::Image.thumbnail`, with faster performance 42 | 43 | ## 2.0.1 44 | 45 | * `ruby-vips` updated to `>= 2.0.1` 46 | * added `autorotate` support, based on `orientation` value from EXIF tags (JPEG only) 47 | 48 | ## 2.0.0 49 | 50 | * `ruby-vips` updated to `~> 2.0` 51 | 52 | ## 1.0.4 53 | 54 | * `vips` is required closer to when the classes are called, in hope of fixing [#107](https://github.com/jcupitt/ruby-vips/issues/107) 55 | 56 | ## 1.0.0 57 | 58 | * rewritten to use `ruby-vips` instead of CLI vips utils, which should result in better performance 59 | * processors `convert`, `vips` and `vipsthumbnail` have been removed 60 | -------------------------------------------------------------------------------- /test/dragonfly_libvips/processors/encode_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe DragonflyLibvips::Processors::Encode do 4 | let(:app) { test_libvips_app } 5 | let(:content_image) { Dragonfly::Content.new(app, SAMPLES_DIR.join('sample.png')) } # 280x355 6 | let(:processor) { DragonflyLibvips::Processors::Encode.new } 7 | 8 | describe 'SUPPORTED_FORMATS' do 9 | DragonflyLibvips::SUPPORTED_FORMATS.each do |format| 10 | unless File.exist?(SAMPLES_DIR.join("sample.#{format}")) 11 | it(format) { skip "sample.#{format} does not exist, skipping" } 12 | next 13 | end 14 | 15 | let(:content) { app.fetch_file SAMPLES_DIR.join("sample.#{format}") } 16 | 17 | DragonflyLibvips::SUPPORTED_OUTPUT_FORMATS.each do |output_format| 18 | it("#{format} to #{output_format}") do 19 | result = content.encode(output_format) 20 | content.encode(output_format).mime_type.must_equal Rack::Mime.mime_type(".#{output_format}") 21 | content.encode(output_format).size.must_be :>, 0 22 | content.encode(output_format).tempfile.path.must_match /\.#{output_format_short(output_format)}\z/ 23 | end 24 | end 25 | end 26 | end 27 | 28 | describe 'allows for options' do 29 | before { processor.call(content_image, 'jpg', output_options: { Q: 50 }) } 30 | it { content_image.ext.must_equal 'jpg' } 31 | end 32 | 33 | def output_format_short(format) 34 | case format 35 | when 'tiff' then 'tif' 36 | when 'jpeg' then 'jpg' 37 | else format 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/dragonfly_libvips/plugin.rb: -------------------------------------------------------------------------------- 1 | require 'dragonfly_libvips/analysers/image_properties' 2 | require 'dragonfly_libvips/processors/encode' 3 | require 'dragonfly_libvips/processors/extract_area' 4 | require 'dragonfly_libvips/processors/rotate' 5 | require 'dragonfly_libvips/processors/thumb' 6 | 7 | module DragonflyLibvips 8 | class Plugin 9 | def call(app, _opts = {}) 10 | # Analysers 11 | app.add_analyser :image_properties, DragonflyLibvips::Analysers::ImageProperties.new 12 | 13 | %w[ width 14 | height 15 | xres 16 | yres 17 | format 18 | ].each do |name| 19 | app.add_analyser(name) { |c| c.analyse(:image_properties)[name] } 20 | end 21 | 22 | app.add_analyser(:aspect_ratio) { |c| c.analyse(:width).to_f / c.analyse(:height).to_f } 23 | app.add_analyser(:portrait) { |c| c.analyse(:aspect_ratio) < 1.0 } 24 | app.add_analyser(:landscape) { |c| !c.analyse(:portrait) } 25 | 26 | app.add_analyser(:image) do |c| 27 | begin 28 | c.analyse(:image_properties).key?('format') 29 | rescue ::Vips::Error 30 | false 31 | end 32 | end 33 | 34 | # Aliases 35 | app.define(:portrait?) { portrait } 36 | app.define(:landscape?) { landscape } 37 | app.define(:image?) { image } 38 | 39 | # Processors 40 | app.add_processor :encode, Processors::Encode.new 41 | app.add_processor :extract_area, Processors::ExtractArea.new 42 | app.add_processor :thumb, Processors::Thumb.new 43 | app.add_processor :rotate, Processors::Rotate.new 44 | end 45 | end 46 | end 47 | 48 | Dragonfly::App.register_plugin(:libvips) { DragonflyLibvips::Plugin.new } 49 | -------------------------------------------------------------------------------- /lib/dragonfly_libvips/processors/rotate.rb: -------------------------------------------------------------------------------- 1 | require 'vips' 2 | 3 | module DragonflyLibvips 4 | module Processors 5 | class Rotate 6 | def call(content, rotate, options = {}) 7 | raise UnsupportedFormat unless content.ext 8 | raise UnsupportedFormat unless SUPPORTED_FORMATS.include?(content.ext.downcase) 9 | 10 | options = options.each_with_object({}) { |(k, v), memo| memo[k.to_s] = v } # stringify keys 11 | format = options.fetch('format', content.ext) 12 | 13 | input_options = options.fetch('input_options', {}) 14 | 15 | # input_options['access'] ||= 'sequential' 16 | if content.mime_type == 'image/jpeg' 17 | input_options['autorotate'] = true unless input_options.has_key?('autorotate') 18 | end 19 | 20 | output_options = options.fetch('output_options', {}) 21 | if FORMATS_WITHOUT_PROFILE_SUPPORT.include?(format) 22 | output_options.delete('profile') 23 | else 24 | output_options['profile'] ||= input_options.fetch('profile', EPROFILE_PATH) 25 | end 26 | output_options.delete('Q') unless format.to_s =~ /jpg|jpeg/i 27 | output_options['format'] ||= format.to_s if format.to_s =~ /gif|bmp/i 28 | 29 | img = ::Vips::Image.new_from_file(content.path, input_options) 30 | img = img.rot("d#{rotate}") 31 | 32 | content.update( 33 | img.write_to_buffer(".#{format}", output_options), 34 | 'name' => "temp.#{format}", 35 | 'format' => format 36 | ) 37 | content.ext = format 38 | end 39 | 40 | def update_url(url_attributes, _, options = {}) 41 | options = options.each_with_object({}) { |(k, v), memo| memo[k.to_s] = v } # stringify keys 42 | return unless format = options.fetch('format', nil) 43 | url_attributes.ext = format 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/dragonfly_libvips/processors/extract_area.rb: -------------------------------------------------------------------------------- 1 | require 'vips' 2 | 3 | module DragonflyLibvips 4 | module Processors 5 | class ExtractArea 6 | def call(content, x, y, width, height, options = {}) 7 | raise UnsupportedFormat unless content.ext 8 | raise UnsupportedFormat unless SUPPORTED_FORMATS.include?(content.ext.downcase) 9 | 10 | options = options.each_with_object({}) { |(k, v), memo| memo[k.to_s] = v } # stringify keys 11 | format = options.fetch('format', content.ext) 12 | 13 | input_options = options.fetch('input_options', {}) 14 | 15 | # input_options['access'] ||= 'sequential' 16 | if content.mime_type == 'image/jpeg' 17 | input_options['autorotate'] = true unless input_options.has_key?('autorotate') 18 | end 19 | 20 | output_options = options.fetch('output_options', {}) 21 | if FORMATS_WITHOUT_PROFILE_SUPPORT.include?(format) 22 | output_options.delete('profile') 23 | else 24 | output_options['profile'] ||= input_options.fetch('profile', EPROFILE_PATH) 25 | end 26 | output_options.delete('Q') unless format.to_s =~ /jpg|jpeg/i 27 | output_options['format'] ||= format.to_s if format.to_s =~ /gif|bmp/i 28 | 29 | img = ::Vips::Image.new_from_file(content.path, input_options) 30 | img = img.extract_area(x, y, width, height) 31 | 32 | content.update( 33 | img.write_to_buffer(".#{format}", output_options), 34 | 'name' => "temp.#{format}", 35 | 'format' => format 36 | ) 37 | content.ext = format 38 | end 39 | 40 | def update_url(url_attributes, _, _, _, _, options = {}) 41 | options = options.each_with_object({}) { |(k, v), memo| memo[k.to_s] = v } # stringify keys 42 | return unless format = options.fetch('format', nil) 43 | url_attributes.ext = format 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/dragonfly_libvips/dimensions.rb: -------------------------------------------------------------------------------- 1 | module DragonflyLibvips 2 | class Dimensions < Struct.new(:geometry, :orig_w, :orig_h) 3 | def self.call(*args) 4 | new(*args).call 5 | end 6 | 7 | def call 8 | return OpenStruct.new(width: orig_w, height: orig_h, scale: 1) if do_not_resize_if_image_smaller_than_requested? || do_not_resize_if_image_larger_than_requested? 9 | OpenStruct.new(width: width, height: height, scale: scale) 10 | end 11 | 12 | private 13 | 14 | def width 15 | if landscape? 16 | dimensions_specified_by_width? ? dimensions.width : dimensions.height / aspect_ratio 17 | else 18 | dimensions_specified_by_height? ? dimensions.height / aspect_ratio : dimensions.width 19 | end 20 | end 21 | 22 | def height 23 | if landscape? 24 | dimensions_specified_by_width? ? dimensions.width * aspect_ratio : dimensions.height 25 | else 26 | dimensions_specified_by_height? ? dimensions.height : dimensions.width * aspect_ratio 27 | end 28 | end 29 | 30 | def scale 31 | width.to_f / orig_w.to_f 32 | end 33 | 34 | def dimensions 35 | w, h = geometry.scan(/\A(\d*)x(\d*)/).flatten.map(&:to_f) 36 | OpenStruct.new(width: w, height: h) 37 | end 38 | 39 | def aspect_ratio 40 | orig_h.to_f / orig_w 41 | end 42 | 43 | def dimensions_specified_by_width? 44 | dimensions.width > 0 45 | end 46 | 47 | def dimensions_specified_by_height? 48 | dimensions.height > 0 49 | end 50 | 51 | def landscape? 52 | aspect_ratio <= 1.0 53 | end 54 | 55 | def portrait? 56 | !landscape? 57 | end 58 | 59 | def do_not_resize_if_image_smaller_than_requested? 60 | return false unless geometry.include? '>' 61 | orig_w < width && orig_h < height 62 | end 63 | 64 | def do_not_resize_if_image_larger_than_requested? 65 | return false unless geometry.include? '<' 66 | orig_w > width && orig_h > height 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/dragonfly_libvips/processors/encode.rb: -------------------------------------------------------------------------------- 1 | require 'vips' 2 | 3 | module DragonflyLibvips 4 | module Processors 5 | class Encode 6 | def call(content, format, options = {}) 7 | raise UnsupportedFormat unless content.ext 8 | raise UnsupportedFormat unless SUPPORTED_FORMATS.include?(content.ext.downcase) 9 | 10 | format = format.to_s 11 | format = 'tif' if format == 'tiff' 12 | format = 'jpg' if format == 'jpeg' 13 | 14 | raise UnsupportedOutputFormat unless SUPPORTED_OUTPUT_FORMATS.include?(format.downcase) 15 | 16 | if content.mime_type == Rack::Mime.mime_type(".#{format}") 17 | content.ext ||= format 18 | content.meta['format'] = format 19 | return 20 | end 21 | 22 | options = options.each_with_object({}) { |(k, v), memo| memo[k.to_s] = v } # stringify keys 23 | 24 | input_options = options.fetch('input_options', {}) 25 | input_options['access'] ||= 'sequential' 26 | if content.mime_type == 'image/jpeg' 27 | input_options['autorotate'] = true unless input_options.has_key?('autorotate') 28 | end 29 | 30 | output_options = options.fetch('output_options', {}) 31 | if FORMATS_WITHOUT_PROFILE_SUPPORT.include?(format) 32 | output_options.delete('profile') 33 | else 34 | output_options['profile'] ||= input_options.fetch('profile', EPROFILE_PATH) 35 | end 36 | output_options.delete('Q') unless format.to_s =~ /jpg|jpeg/i 37 | output_options['format'] ||= format.to_s if format.to_s =~ /gif|bmp/i 38 | 39 | img = ::Vips::Image.new_from_file(content.path, input_options) 40 | 41 | content.update( 42 | img.write_to_buffer(".#{format}", output_options), 43 | 'name' => "temp.#{format}", 44 | 'format' => format 45 | ) 46 | content.ext = format 47 | end 48 | 49 | def update_url(url_attributes, format, options = {}) 50 | url_attributes.ext = format.to_s 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/dragonfly_libvips/plugin_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'openssl' 3 | 4 | module DragonflyLibvips 5 | describe Plugin do 6 | let(:app) { test_app.configure_with(:libvips) } 7 | let(:content) { app.fetch_file(SAMPLES_DIR.join('sample.png')) } 8 | 9 | describe 'analysers' do 10 | it { content.width.must_equal 280 } 11 | it { content.height.must_equal 355 } 12 | it { content.aspect_ratio.must_equal (280.0 / 355.0) } 13 | it { content.xres.must_equal 72.0 } 14 | it { content.yres.must_equal 72.0 } 15 | 16 | it { content.must_be :portrait? } 17 | it { content.portrait.must_equal true } # for use with magic attributes 18 | it { content.wont_be :landscape? } 19 | it { content.landscape.must_equal false } # for use with magic attributes 20 | 21 | it { content.format.must_equal 'png' } 22 | 23 | it { content.must_be :image? } 24 | it { content.image.must_equal true } # for use with magic attributes 25 | it { app.create('blah').wont_be :image? } 26 | end 27 | 28 | describe 'processors that change the url' do 29 | before { app.configure { url_format '/:name' } } 30 | 31 | describe 'encode' do 32 | let(:thumb) { content.encode('png') } 33 | 34 | it { thumb.url.must_match(/^\/sample\.png\?.*job=\w+/) } 35 | it { thumb.format.must_equal 'png' } 36 | it { thumb.meta['format'].must_equal 'png' } 37 | end 38 | 39 | describe 'rotate' do 40 | let(:thumb) { content.rotate(90, format: 'png') } 41 | 42 | it { thumb.url.must_match(/^\/sample\.png\?.*job=\w+/) } 43 | it { thumb.format.must_equal 'png' } 44 | it { thumb.meta['format'].must_equal 'png' } 45 | end 46 | 47 | describe 'thumb' do 48 | let(:thumb) { content.thumb('100x', format: 'png') } 49 | 50 | it { thumb.url.must_match(/^\/sample\.png\?.*job=\w+/) } 51 | it { thumb.format.must_equal 'png' } 52 | it { thumb.meta['format'].must_equal 'png' } 53 | end 54 | end 55 | 56 | describe 'other processors' do 57 | describe 'encode' do 58 | it { content.encode('jpg').format.must_equal 'jpg' } 59 | it { content.encode('jpg', output_options: { Q: 1 }).format.must_equal 'jpg' } 60 | it { content.encode('jpg', output_options: { Q: 1 }).size.must_be :<, 65_000 } 61 | end 62 | 63 | describe 'extract_area' do 64 | it { content.extract_area(100, 100, 50, 200, format: 'jpg').format.must_equal 'jpg' } 65 | it { content.extract_area(100, 100, 50, 200, format: 'jpg').width.must_equal 50 } 66 | it { content.extract_area(100, 100, 50, 200, format: 'jpg').height.must_equal 200 } 67 | end 68 | 69 | describe 'rotate' do 70 | it { content.rotate(90).width.must_equal 355 } 71 | it { content.rotate(90).height.must_equal 280 } 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/dragonfly_libvips/processors/thumb.rb: -------------------------------------------------------------------------------- 1 | require 'dragonfly_libvips/dimensions' 2 | require 'vips' 3 | 4 | module DragonflyLibvips 5 | module Processors 6 | class Thumb 7 | OPERATORS = '><'.freeze 8 | RESIZE_GEOMETRY = /\A\d*x\d*[#{OPERATORS}]?\z/ # e.g. '300x200>' 9 | DPI = 300 10 | 11 | def call(content, geometry, options = {}) 12 | raise UnsupportedFormat unless content.ext 13 | raise UnsupportedFormat unless SUPPORTED_FORMATS.include?(content.ext.downcase) 14 | 15 | options = options.each_with_object({}) { |(k, v), memo| memo[k.to_s] = v } # stringify keys 16 | 17 | filename = content.path 18 | format = options.fetch('format', content.ext).to_s 19 | 20 | input_options = options.fetch('input_options', {}) 21 | input_options['access'] = input_options.fetch('access', 'sequential') 22 | input_options['autorotate'] = input_options.fetch('autorotate', true) if content.mime_type == 'image/jpeg' 23 | 24 | if content.mime_type == 'application/pdf' 25 | input_options['dpi'] = input_options.fetch('dpi', DPI) 26 | input_options['page'] = input_options.fetch('page', 0) 27 | else 28 | input_options.delete('page') 29 | input_options.delete('dpi') 30 | end 31 | 32 | output_options = options.fetch('output_options', {}) 33 | if FORMATS_WITHOUT_PROFILE_SUPPORT.include?(format) 34 | output_options.delete('profile') 35 | else 36 | output_options['profile'] ||= input_options.fetch('profile', EPROFILE_PATH) 37 | end 38 | output_options.delete('Q') unless format.to_s =~ /jpg|jpeg/i 39 | output_options['format'] ||= format.to_s if format.to_s =~ /gif|bmp/i 40 | 41 | input_options = input_options.each_with_object({}) { |(k, v), memo| memo[k.to_sym] = v } # symbolize 42 | img = ::Vips::Image.new_from_file(filename, input_options) 43 | 44 | dimensions = case geometry 45 | when RESIZE_GEOMETRY then Dimensions.call(geometry, img.width, img.height) 46 | else raise ArgumentError, "Didn't recognise the geometry string: #{geometry}" 47 | end 48 | 49 | thumbnail_options = options.fetch('thumbnail_options', {}) 50 | if Vips.at_least_libvips?(8, 8) 51 | thumbnail_options['no_rotate'] = input_options.fetch('no_rotate', false) if content.mime_type == 'image/jpeg' 52 | else 53 | thumbnail_options['auto_rotate'] = input_options.fetch('autorotate', true) if content.mime_type == 'image/jpeg' 54 | end 55 | thumbnail_options['height'] = thumbnail_options.fetch('height', dimensions.height.ceil) 56 | thumbnail_options['import_profile'] = CMYK_PROFILE_PATH if img.get('interpretation') == :cmyk 57 | thumbnail_options['size'] ||= case geometry 58 | when />\z/ then :down # do_not_resize_if_image_smaller_than_requested 59 | when /<\z/ then :up # do_not_resize_if_image_larger_than_requested 60 | else :both 61 | end 62 | 63 | filename += "[page=#{input_options[:page]}]" if content.mime_type == 'application/pdf' 64 | 65 | thumbnail_options = thumbnail_options.each_with_object({}) { |(k, v), memo| memo[k.to_sym] = v } # symbolize 66 | thumb = ::Vips::Image.thumbnail(filename, dimensions.width.ceil, thumbnail_options) 67 | 68 | content.update( 69 | thumb.write_to_buffer(".#{format}", output_options), 70 | 'name' => "temp.#{format}", 71 | 'format' => format 72 | ) 73 | content.ext = format 74 | end 75 | 76 | def update_url(url_attributes, _, options = {}) 77 | options = options.each_with_object({}) { |(k, v), memo| memo[k.to_s] = v } # stringify keys 78 | return unless format = options.fetch('format', nil) 79 | url_attributes.ext = format 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/dragonfly_libvips/dimensions_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe DragonflyLibvips::Dimensions do 4 | let(:geometry) { '' } 5 | let(:orig_w) { nil } 6 | let(:orig_h) { nil } 7 | let(:result) { DragonflyLibvips::Dimensions.call(geometry, orig_w, orig_h) } 8 | 9 | describe 'NNxNN' do 10 | let(:geometry) { '250x250' } 11 | 12 | describe 'when square' do 13 | let(:orig_w) { 1000 } 14 | let(:orig_h) { 1000 } 15 | 16 | it { result.width.must_equal 250 } 17 | it { result.height.must_equal 250 } 18 | it { result.scale.must_equal 250.0 / orig_w } 19 | 20 | describe '250x250>' do 21 | let(:geometry) { '250x250>' } 22 | 23 | describe 'when image larger than specified' do 24 | it 'resize' do 25 | result.width.must_equal 250 26 | result.height.must_equal 250 27 | result.scale.must_equal 250.0 / orig_w 28 | end 29 | end 30 | 31 | describe 'when image smaller than specified' do 32 | let(:orig_w) { 100 } 33 | let(:orig_h) { 100 } 34 | it 'do not resize' do 35 | result.width.must_equal 100 36 | result.height.must_equal 100 37 | result.scale.must_equal 100.0 / orig_w 38 | end 39 | end 40 | end 41 | 42 | describe '250x50<' do 43 | let(:geometry) { '250x250<' } 44 | 45 | describe 'when image larger than specified' do 46 | it 'do not resize' do 47 | result.width.must_equal 1000 48 | result.height.must_equal 1000 49 | result.scale.must_equal 1000.0 / orig_w 50 | end 51 | end 52 | 53 | describe 'when image smaller than specified' do 54 | let(:orig_w) { 100 } 55 | let(:orig_h) { 100 } 56 | 57 | it 'do resize' do 58 | result.width.must_equal 250 59 | result.height.must_equal 250 60 | result.scale.must_equal 250.0 / orig_w 61 | end 62 | end 63 | end 64 | end 65 | 66 | describe 'when landscape' do 67 | let(:orig_w) { 1000 } 68 | let(:orig_h) { 500 } 69 | 70 | it { result.width.must_equal 250 } 71 | it { result.height.must_equal 125 } 72 | it { result.scale.must_equal 250.0 / orig_w } 73 | end 74 | 75 | describe 'when portrait' do 76 | let(:orig_w) { 500 } 77 | let(:orig_h) { 1000 } 78 | 79 | it { result.width.must_equal 125 } 80 | it { result.height.must_equal 250 } 81 | it { result.scale.must_equal 125.0 / orig_w } 82 | end 83 | end 84 | 85 | describe 'NNx' do 86 | let(:geometry) { '250x' } 87 | 88 | describe 'when square' do 89 | let(:orig_w) { 1000 } 90 | let(:orig_h) { 1000 } 91 | 92 | it { result.width.must_equal 250 } 93 | it { result.height.must_equal 250 } 94 | it { result.scale.must_equal 250.0 / orig_w } 95 | end 96 | 97 | describe 'when landscape' do 98 | let(:orig_w) { 1000 } 99 | let(:orig_h) { 500 } 100 | 101 | it { result.width.must_equal 250 } 102 | it { result.height.must_equal 125 } 103 | it { result.scale.must_equal 250.0 / orig_w } 104 | end 105 | 106 | describe 'when portrait' do 107 | let(:orig_w) { 500 } 108 | let(:orig_h) { 1000 } 109 | 110 | it { result.width.must_equal 250 } 111 | it { result.height.must_equal 500 } 112 | it { result.scale.must_equal 250.0 / orig_w } 113 | end 114 | end 115 | 116 | describe 'xNN' do 117 | let(:geometry) { 'x250' } 118 | 119 | describe 'when square' do 120 | let(:orig_w) { 1000 } 121 | let(:orig_h) { 1000 } 122 | 123 | it { result.width.must_equal 250 } 124 | it { result.height.must_equal 250 } 125 | it { result.scale.must_equal 250.0 / orig_w } 126 | end 127 | 128 | describe 'when landscape' do 129 | let(:orig_w) { 1000 } 130 | let(:orig_h) { 500 } 131 | 132 | it { result.width.must_equal 500 } 133 | it { result.height.must_equal 250 } 134 | it { result.scale.must_equal 500.0 / orig_w } 135 | end 136 | 137 | describe 'when portrait' do 138 | let(:orig_w) { 500 } 139 | let(:orig_h) { 1000 } 140 | 141 | it { result.width.must_equal 125 } 142 | it { result.height.must_equal 250 } 143 | it { result.scale.must_equal 125.0 / orig_w } 144 | end 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dragonfly libvips 2 | 3 | [![Build Status](https://travis-ci.org/tomasc/dragonfly_libvips.svg)](https://travis-ci.org/tomasc/dragonfly_libvips) [![Gem Version](https://badge.fury.io/rb/dragonfly_libvips.svg)](http://badge.fury.io/rb/dragonfly_libvips) [![Coverage Status](https://img.shields.io/coveralls/tomasc/dragonfly_libvips.svg)](https://coveralls.io/r/tomasc/dragonfly_libvips) 4 | 5 | Dragonfly analysers and processors for [libvips](https://github.com/jcupitt/libvips) image processing library 6 | 7 | From the libvips README: 8 | 9 | > libvips is a 2D image processing library. Compared to similar libraries, [libvips runs quickly and uses little memory](http://www.vips.ecs.soton.ac.uk/index.php?title=Speed_and_Memory_Use). libvips is licensed under the LGPL 2.1+. 10 | 11 | ## Installation 12 | 13 | Add this line to your application's Gemfile: 14 | 15 | ```ruby 16 | gem 'dragonfly_libvips' 17 | ``` 18 | 19 | And then execute: 20 | 21 | ``` 22 | $ bundle 23 | ``` 24 | 25 | Or install it yourself as: 26 | 27 | ``` 28 | $ gem install dragonfly_libvips 29 | ``` 30 | 31 | ### libvips 32 | 33 | If you run into trouble installing `libvips` with Ruby introspection on Linux, follow the [build steps here](https://github.com/tomasc/dragonfly_libvips/blob/master/.travis.yml). Please note the importance of `gobject-introspection` and `libgirepository1.0-dev` plus the `export GI_TYPELIB_PATH=/usr/local/lib/girepository-1.0/` and `ldconfig`. 34 | 35 | ## Dependencies 36 | 37 | The [vips](http://www.vips.ecs.soton.ac.uk/index.php?title=Supported) library and its [dependencies](https://github.com/jcupitt/libvips#dependencies). 38 | 39 | ## Usage 40 | 41 | Configure your app the usual way: 42 | 43 | ```ruby 44 | Dragonfly.app.configure do 45 | plugin :libvips 46 | end 47 | ``` 48 | 49 | ## Supported Formats 50 | 51 | List of supported formats (based on your build and version of the `libvips` library) is available as: 52 | 53 | ```ruby 54 | DragonflyLibvips::SUPPORTED_FORMATS # => ["csv", "dz", "gif", …] 55 | DragonflyLibvips::SUPPORTED_OUTPUT_FORMATS # => ["csv", "dz", "gif", …] 56 | ``` 57 | 58 | ## Processors 59 | 60 | ### Thumb 61 | 62 | Create a thumbnail by resizing/cropping 63 | 64 | ```ruby 65 | image.thumb('40x30') 66 | ``` 67 | 68 | Below are some examples of geometry strings for `thumb`: 69 | 70 | ```ruby 71 | '400x300' # resize, maintain aspect ratio 72 | '400x' # resize width, maintain aspect ratio 73 | 'x300' # resize height, maintain aspect ratio 74 | '400x300<' # resize only if the image is smaller than this 75 | '400x300>' # resize only if the image is larger than this 76 | ``` 77 | 78 | ### Encode 79 | 80 | Change the encoding with 81 | 82 | ```ruby 83 | image.encode('jpg') 84 | ``` 85 | 86 | ### Extract Area 87 | 88 | Extract an area from an image. 89 | 90 | ```ruby 91 | image.extract_area(x, y, width, height) 92 | ``` 93 | 94 | ### Rotate 95 | 96 | Rotate a number of degrees with 97 | 98 | ```ruby 99 | image.rotate(90) 100 | ``` 101 | 102 | ### Options 103 | 104 | All processors support `input_options` and `output_options` for passing additional options to vips. For example: 105 | 106 | ```ruby 107 | image.encode('jpg', output_options: { Q: 50 }) 108 | image.encode('jpg', output_options: { interlace: true }) # use interlace to generate a progressive jpg 109 | pdf.encode('jpg', input_options: { page: 0, dpi: 600 }) 110 | ``` 111 | 112 | Defaults: 113 | 114 | ```ruby 115 | input_options: { access: :sequential } 116 | output_options: { profile: … } # embeds 'sRGB_v4_ICC_preference.icc' profile included with this gem 117 | ``` 118 | 119 | ## Analysers 120 | 121 | The following methods are provided 122 | 123 | ```ruby 124 | image.width # => 280 125 | image.height # => 355 126 | image.xres # => 72.0 127 | image.yres # => 72.0 128 | image.progressive # => true 129 | image.aspect_ratio # => 0.788732394366197 130 | image.portrait? # => true 131 | image.landscape? # => false 132 | image.format # => 'png' 133 | image.image? # => true 134 | ``` 135 | 136 | ## Development 137 | 138 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 139 | 140 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 141 | 142 | ## Contributing 143 | 144 | Bug reports and pull requests are welcome on GitHub at . 145 | 146 | ## License 147 | 148 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 149 | -------------------------------------------------------------------------------- /test/dragonfly_libvips/processors/thumb_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'ostruct' 3 | 4 | describe DragonflyLibvips::Processors::Thumb do 5 | let(:app) { test_libvips_app } 6 | let(:image) { Dragonfly::Content.new(app, SAMPLES_DIR.join('sample.png')) } # 280x355 7 | let(:pdf) { Dragonfly::Content.new(app, SAMPLES_DIR.join('sample.pdf')) } 8 | let(:jpg) { Dragonfly::Content.new(app, SAMPLES_DIR.join('sample.jpg')) } 9 | let(:cmyk) { Dragonfly::Content.new(app, SAMPLES_DIR.join('sample_cmyk.jpg')) } 10 | let(:gif) { Dragonfly::Content.new(app, SAMPLES_DIR.join('sample.gif')) } 11 | let(:anim_gif) { Dragonfly::Content.new(app, SAMPLES_DIR.join('sample_anim.gif')) } 12 | let(:landscape_image) { Dragonfly::Content.new(app, SAMPLES_DIR.join('landscape_sample.png')) } # 355x280 13 | let(:processor) { DragonflyLibvips::Processors::Thumb.new } 14 | 15 | it 'raises an error if an unrecognized string is given' do 16 | assert_raises(ArgumentError) do 17 | processor.call(image, '30x40#ne!') 18 | end 19 | end 20 | 21 | describe 'cmyk images' do 22 | before { processor.call(cmyk, '30x') } 23 | it { cmyk.must_have_width 30 } 24 | end 25 | 26 | describe 'resizing' do 27 | describe 'xNN' do 28 | before { processor.call(landscape_image, 'x30') } 29 | it { landscape_image.must_have_width 38 } 30 | it { landscape_image.must_have_height 30 } 31 | end 32 | 33 | describe 'NNx' do 34 | before { processor.call(image, '30x') } 35 | it { image.must_have_width 30 } 36 | it { image.must_have_height 38 } 37 | end 38 | 39 | describe 'NNxNN' do 40 | before { processor.call(image, '30x30') } 41 | it { image.must_have_width 24 } 42 | it { image.must_have_height 30 } 43 | end 44 | 45 | describe 'NNxNN>' do 46 | describe 'if the image is smaller than specified' do 47 | before { processor.call(image, '1000x1000>') } 48 | it { image.must_have_width 280 } 49 | it { image.must_have_height 355 } 50 | end 51 | 52 | describe 'if the image is larger than specified' do 53 | before { processor.call(image, '30x30>') } 54 | it { image.must_have_width 24 } 55 | it { image.must_have_height 30 } 56 | end 57 | end 58 | 59 | describe 'NNxNN<' do 60 | describe 'if the image is larger than specified' do 61 | before { processor.call(image, '10x10<') } 62 | it { image.must_have_width 280 } 63 | it { image.must_have_height 355 } 64 | end 65 | 66 | describe 'if the image is smaller than specified' do 67 | before { processor.call(image, '500x500<') } 68 | it { image.must_have_width 394 } 69 | it { image.must_have_height 500 } 70 | end 71 | end 72 | end 73 | 74 | describe 'pdf' do 75 | describe 'resize' do 76 | before { processor.call(pdf, '500x500', format: 'jpg') } 77 | # it { pdf.must_have_width 386 } 78 | it { pdf.must_have_height 500 } 79 | end 80 | 81 | describe 'page param' do 82 | before { processor.call(pdf, '500x500', format: 'jpg', input_options: { page: 0 }) } 83 | # it { pdf.must_have_width 386 } 84 | it { pdf.must_have_height 500 } 85 | end 86 | end 87 | 88 | describe 'jpg' do 89 | describe 'progressive' do 90 | before { processor.call(jpg, '300x', output_options: { interlace: true }) } 91 | it { (`vipsheader -f jpeg-multiscan #{jpg.file.path}`.to_i == 1).must_equal true } 92 | end 93 | end 94 | 95 | describe 'gif' do 96 | describe 'static' do 97 | before { processor.call(gif, '200x') } 98 | it { gif.must_have_width 200 } 99 | end 100 | 101 | describe 'animated' do 102 | before { processor.call(anim_gif, '200x') } 103 | it { 104 | skip 'waiting for full support' 105 | gif.must_have_width 200 106 | } 107 | end 108 | end 109 | 110 | describe 'format' do 111 | let(:url_attributes) { OpenStruct.new } 112 | 113 | describe 'when format passed in' do 114 | before { processor.call(image, '2x2', format: 'jpeg', output_options: { Q: 50 }) } 115 | it { image.ext.must_equal 'jpeg' } 116 | it { image.size.must_be :<, 65_000 } 117 | end 118 | 119 | describe 'when format not passed in' do 120 | before { processor.call(image, '2x2') } 121 | it { image.ext.must_equal 'png' } 122 | end 123 | 124 | describe 'when ext passed in' do 125 | before { processor.update_url(url_attributes, '2x2', 'format' => 'png') } 126 | it { url_attributes.ext.must_equal 'png' } 127 | end 128 | 129 | describe 'when ext not passed in' do 130 | before { processor.update_url(url_attributes, '2x2') } 131 | it { url_attributes.ext.must_be_nil } 132 | end 133 | end 134 | 135 | describe 'tempfile has extension' do 136 | let(:format) { 'jpg' } 137 | before { processor.call(image, '100x', format: 'jpg') } 138 | it { image.tempfile.path.must_match /\.#{format}\z/ } 139 | end 140 | end 141 | --------------------------------------------------------------------------------