├── .github └── workflows │ └── test.yml ├── .gitignore ├── .rubocop.yml ├── .ruby-version ├── .tool-versions ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── dragonfly_libvips.gemspec ├── lefthook.yml ├── lib ├── dragonfly_libvips.rb └── dragonfly_libvips │ ├── analysers │ └── image_properties.rb │ ├── dimensions.rb │ ├── plugin.rb │ ├── processors │ ├── encode.rb │ ├── extract_area.rb │ ├── rotate.rb │ └── thumb.rb │ └── version.rb ├── samples ├── landscape_sample.png ├── sample.gif ├── sample.jpg ├── sample.pdf ├── sample.png ├── sample.svg ├── sample.tif ├── sample_anim.gif ├── sample_cmyk.jpg └── white pixel.png ├── test ├── dragonfly_libvips │ ├── analysers │ │ └── image_properties_test.rb │ ├── dimensions_test.rb │ ├── plugin_test.rb │ └── processors │ │ ├── encode_test.rb │ │ ├── extract_area_test.rb │ │ ├── rotate_test.rb │ │ └── thumb_test.rb ├── support │ └── image_assertions.rb └── test_helper.rb └── vendor ├── cmyk.icm └── sRGB_v4_ICC_preference.icc /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | branches: ["master"] 5 | push: 6 | branches: ["master"] 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | ruby: ['2.7', '3.2'] 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Install dependencies 16 | run: | 17 | sudo apt-get clean 18 | sudo apt-get update 19 | sudo apt-get install -y gobject-introspection libgirepository1.0-dev libglib2.0-dev libpoppler-glib-dev libgif-dev 20 | curl -OL https://github.com/libvips/libvips/releases/download/v8.13.3/vips-8.13.3.tar.gz 21 | tar zxvf vips-8.13.3.tar.gz && cd vips-8.13.3 && ./configure $1 && sudo make && sudo make install 22 | export GI_TYPELIB_PATH=/usr/local/lib/girepository-1.0/ 23 | sudo ldconfig 24 | - uses: ruby/setup-ruby@v1 25 | with: 26 | ruby-version: ${{ matrix.ruby }} 27 | bundler-cache: true 28 | - name: Run tests 29 | run: bundle exec rake 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.log -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | rubocop-rails_config: 3 | - config/rails.yml 4 | 5 | AllCops: 6 | TargetRubyVersion: 3.1 7 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.1.2 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 2.6.3 2 | -------------------------------------------------------------------------------- /.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.3 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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 2.5.1 4 | 5 | * Update all DragonflyLibvips.symbolize_keys to new syntax by @Bartuz 6 | * Remove `raw` from supported output formats by @asgerb 7 | * Add `j2c`, `j2k`, `jp2`, `jpc`, `jpt`, `jxl`, and `szi` to formats without profile support by @asgerb 8 | * Don't pass `format` output option to `gif` by @asgerb 9 | * Fix missing parameters in `Dimensions` by @asgerb 10 | 11 | ## 2.5.0 12 | 13 | * Fix ruby 3.0+ compatibility by @Bartuz 14 | 15 | ## 2.4.2 16 | 17 | * Add `webp` as supported format, and `aviz` and `heif` to formats without profile support (#14) by @asgerb 18 | 19 | ## 2.4.1 20 | 21 | * Fix misspelled UnsupportedOutputFormat (#13) by @asgerb 22 | * Fix broken links in README (#12) by @NARKOZ 23 | 24 | ## 2.4.0 25 | 26 | * symbolize `input_options` and `output_options` to avoid argument errors coming from `ruby-vips` 27 | 28 | ## 2.3.3 29 | 30 | * locks `ruby-vips` to `< 2.0.14` 31 | * adds support for `libvips` `8.8.0` 32 | 33 | ## 2.3.2 34 | 35 | * FIX: support for .gif 36 | 37 | ## 2.3.1 38 | 39 | * improved `SUPPORTED_FORMATS` matching that ignores case 40 | 41 | ## 2.3.0 42 | 43 | * add support for progressive JPEGs 44 | 45 | ## 2.2.0 46 | 47 | * add `SUPPORTED_FORMATS` and `SUPPORTED_OUTPUT_FORMATS` and raise errors when formats are not matching 48 | * add more thorough tests for supported formats 49 | * skip unnecessary conversion from-to same format 50 | * add `extract_area` processor 51 | 52 | ## 2.1.3 53 | 54 | * make sure the downcase analyser survives nil 55 | 56 | ## 2.1.2 57 | 58 | * changed image properties analyser to downcase the image's format 59 | 60 | ## 2.1.1 61 | 62 | * add `CMYK` support using the `cmyk.icm` profile 63 | 64 | ## 2.1.0 65 | 66 | * `thumb` process refactored for `Vips::Image.thumbnail`, with faster performance 67 | 68 | ## 2.0.1 69 | 70 | * `ruby-vips` updated to `>= 2.0.1` 71 | * added `autorotate` support, based on `orientation` value from EXIF tags (JPEG only) 72 | 73 | ## 2.0.0 74 | 75 | * `ruby-vips` updated to `~> 2.0` 76 | 77 | ## 1.0.4 78 | 79 | * `vips` is required closer to when the classes are called, in hope of fixing [#107](https://github.com/jcupitt/ruby-vips/issues/107) 80 | 81 | ## 1.0.0 82 | 83 | * rewritten to use `ruby-vips` instead of CLI vips utils, which should result in better performance 84 | * processors `convert`, `vips` and `vipsthumbnail` have been removed 85 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in dragonfly_libvips.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # A sample Guardfile 4 | # More info at https://github.com/guard/guard#readme 5 | 6 | guard :minitest do 7 | watch(%r{^lib/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" } 8 | watch(%r{^test/.+_test\.rb$}) 9 | watch(%r{^test/test_helper\.rb$}) { "test" } 10 | end 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/libvips/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](https://github.com/libvips/libvips/wiki/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/libvips/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 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rake/testtask" 5 | 6 | Rake::TestTask.new(:test) do |t| 7 | t.libs << "test" 8 | t.libs << "lib" 9 | t.test_files = FileList["test/**/*_test.rb"] 10 | end 11 | 12 | task default: :test 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /dragonfly_libvips.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path("lib", __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require "dragonfly_libvips/version" 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "dragonfly_libvips" 9 | spec.version = DragonflyLibvips::VERSION 10 | spec.authors = ["Tomas Celizna"] 11 | spec.email = ["tomas.celizna@gmail.com"] 12 | 13 | spec.summary = "Dragonfly analysers and processors for libvips image processing library." 14 | spec.homepage = "https://github.com/tomasc/dragonfly_libvips" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = "exe" 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ["lib"] 21 | 22 | spec.add_dependency "dragonfly", "~> 1.0" 23 | spec.add_dependency "ruby-vips", "~> 2.0", ">= 2.0.16" 24 | 25 | spec.add_development_dependency "bundler" # , '~> 2.0' 26 | spec.add_development_dependency "rb-readline" 27 | spec.add_development_dependency "guard" 28 | spec.add_development_dependency "guard-minitest" 29 | spec.add_development_dependency "minitest", "~> 5.0" 30 | spec.add_development_dependency "minitest-reporters" 31 | spec.add_development_dependency "rake" 32 | 33 | spec.add_development_dependency "lefthook" 34 | spec.add_development_dependency "rubocop-rails_config" 35 | end 36 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | rubocop: 5 | run: bundle exec rubocop {staged_files} -A --display-cop-names --extra-details --force-exclusion 6 | -------------------------------------------------------------------------------- /lib/dragonfly_libvips.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dragonfly" 4 | require "dragonfly_libvips/dimensions" 5 | require "dragonfly_libvips/plugin" 6 | require "dragonfly_libvips/version" 7 | require "vips" 8 | 9 | module DragonflyLibvips 10 | class UnsupportedFormat < RuntimeError; end 11 | class UnsupportedOutputFormat < RuntimeError; end 12 | 13 | CMYK_PROFILE_PATH = File.expand_path("../vendor/cmyk.icm", __dir__) 14 | EPROFILE_PATH = File.expand_path("../vendor/sRGB_v4_ICC_preference.icc", __dir__) 15 | 16 | SUPPORTED_FORMATS = begin 17 | output = `vips -l | grep -i ForeignLoad` 18 | output.scan(/\.(\w{1,4})/).flatten.compact.sort.map(&:downcase).uniq 19 | end 20 | 21 | SUPPORTED_OUTPUT_FORMATS = begin 22 | output = `vips -l | grep -i ForeignSave` 23 | output.scan(/\.(\w{1,4})/).flatten.compact.sort.map(&:downcase).uniq 24 | end - %w[ 25 | csv 26 | fit 27 | fits 28 | fts 29 | mat 30 | pbm 31 | pfm 32 | pgm 33 | ppm 34 | raw 35 | v 36 | vips 37 | ] 38 | 39 | FORMATS_WITHOUT_PROFILE_SUPPORT = %w[ 40 | avif 41 | bmp 42 | dz 43 | gif 44 | hdr 45 | heic 46 | heif 47 | j2c 48 | j2k 49 | jp2 50 | jpc 51 | jpt 52 | jxl 53 | szi 54 | webp 55 | ] 56 | 57 | private 58 | def self.stringify_keys(hash = {}) 59 | hash.transform_keys { |k| k.to_s } 60 | end 61 | 62 | def self.symbolize_keys(hash = {}) 63 | hash.transform_keys { |k| k.to_sym } 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/dragonfly_libvips/analysers/image_properties.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "vips" 4 | 5 | module DragonflyLibvips 6 | module Analysers 7 | class ImageProperties 8 | DPI = 300 9 | 10 | def call(content) 11 | return {} unless content.ext 12 | return {} unless SUPPORTED_FORMATS.include?(content.ext.downcase) 13 | 14 | input_options = {} 15 | input_options["access"] = "sequential" 16 | input_options["autorotate"] = true if content.mime_type == "image/jpeg" 17 | input_options["dpi"] = DPI if content.mime_type == "application/pdf" 18 | 19 | img = ::Vips::Image.new_from_file(content.path, **DragonflyLibvips.symbolize_keys(input_options)) 20 | 21 | width = img.width 22 | height = img.height 23 | xres = img.xres 24 | yres = img.yres 25 | 26 | { 27 | "format" => content.ext.to_s, 28 | "width" => width, 29 | "height" => height, 30 | "xres" => xres, 31 | "yres" => yres, 32 | "progressive" => (content.mime_type == "image/jpeg" && img.get("jpeg-multiscan") != 0) 33 | } 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/dragonfly_libvips/dimensions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DragonflyLibvips 4 | class Dimensions < Struct.new(:geometry, :orig_w, :orig_h) 5 | def self.call(*args) 6 | new(*args).call 7 | end 8 | 9 | def call 10 | 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? 11 | OpenStruct.new(width: width, height: height, scale: scale) 12 | end 13 | 14 | private 15 | def width 16 | if landscape? 17 | dimensions_specified_by_width? ? dimensions.width : dimensions.height / aspect_ratio 18 | else 19 | dimensions_specified_by_height? ? dimensions.height / aspect_ratio : dimensions.width 20 | end 21 | end 22 | 23 | def height 24 | if landscape? 25 | dimensions_specified_by_width? ? dimensions.width * aspect_ratio : dimensions.height 26 | else 27 | dimensions_specified_by_height? ? dimensions.height : dimensions.width * aspect_ratio 28 | end 29 | end 30 | 31 | def scale 32 | width.to_f / orig_w.to_f 33 | end 34 | 35 | def dimensions 36 | w, h = geometry.scan(/\A(\d*)x(\d*)/).flatten.map(&:to_f) 37 | OpenStruct.new(width: w, height: h) 38 | end 39 | 40 | def aspect_ratio 41 | orig_h.to_f / orig_w 42 | end 43 | 44 | def dimensions_specified_by_width? 45 | dimensions.width > 0 46 | end 47 | 48 | def dimensions_specified_by_height? 49 | dimensions.height > 0 50 | end 51 | 52 | def landscape? 53 | aspect_ratio <= 1.0 54 | end 55 | 56 | def portrait? 57 | !landscape? 58 | end 59 | 60 | def do_not_resize_if_image_smaller_than_requested? 61 | return false unless geometry.include? ">" 62 | orig_w < width && orig_h < height 63 | end 64 | 65 | def do_not_resize_if_image_larger_than_requested? 66 | return false unless geometry.include? "<" 67 | orig_w > width && orig_h > height 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/dragonfly_libvips/plugin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dragonfly_libvips/analysers/image_properties" 4 | require "dragonfly_libvips/processors/encode" 5 | require "dragonfly_libvips/processors/extract_area" 6 | require "dragonfly_libvips/processors/rotate" 7 | require "dragonfly_libvips/processors/thumb" 8 | 9 | module DragonflyLibvips 10 | class Plugin 11 | def call(app, _opts = {}) 12 | # Analysers 13 | app.add_analyser :image_properties, DragonflyLibvips::Analysers::ImageProperties.new 14 | 15 | %w[ width 16 | height 17 | xres 18 | yres 19 | format 20 | ].each do |name| 21 | app.add_analyser(name) { |c| c.analyse(:image_properties)[name] } 22 | end 23 | 24 | app.add_analyser(:aspect_ratio) { |c| c.analyse(:width).to_f / c.analyse(:height).to_f } 25 | app.add_analyser(:portrait) { |c| c.analyse(:aspect_ratio) < 1.0 } 26 | app.add_analyser(:landscape) { |c| !c.analyse(:portrait) } 27 | 28 | app.add_analyser(:image) do |c| 29 | c.analyse(:image_properties).key?("format") 30 | rescue ::Vips::Error 31 | false 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/encode.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "vips" 4 | 5 | module DragonflyLibvips 6 | module Processors 7 | class Encode 8 | def call(content, format, options = {}) 9 | raise UnsupportedFormat unless content.ext 10 | raise UnsupportedFormat unless SUPPORTED_FORMATS.include?(content.ext.downcase) 11 | 12 | format = format.to_s 13 | format = "tif" if format == "tiff" 14 | format = "jpg" if format == "jpeg" 15 | 16 | raise UnsupportedOutputFormat unless SUPPORTED_OUTPUT_FORMATS.include?(format.downcase) 17 | 18 | if content.mime_type == Rack::Mime.mime_type(".#{format}") 19 | content.ext ||= format 20 | content.meta["format"] = format 21 | return 22 | end 23 | 24 | options = DragonflyLibvips.stringify_keys(options) 25 | 26 | input_options = options.fetch("input_options", {}) 27 | input_options["access"] ||= "sequential" 28 | if content.mime_type == "image/jpeg" 29 | input_options["autorotate"] = true unless input_options.key?("autorotate") 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 /jpg|jpeg/i.match?(format.to_s) 39 | output_options["format"] ||= format.to_s if /bmp/i.match?(format.to_s) 40 | 41 | img = ::Vips::Image.new_from_file(content.path, **DragonflyLibvips.symbolize_keys(input_options)) 42 | 43 | content.update( 44 | img.write_to_buffer(".#{format}", **DragonflyLibvips.symbolize_keys(output_options)), 45 | "name" => "temp.#{format}", 46 | "format" => format 47 | ) 48 | content.ext = format 49 | end 50 | 51 | def update_url(url_attributes, format, _options = {}) 52 | url_attributes.ext = format.to_s 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/dragonfly_libvips/processors/extract_area.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "vips" 4 | 5 | module DragonflyLibvips 6 | module Processors 7 | class ExtractArea 8 | def call(content, x, y, width, height, options = {}) 9 | raise UnsupportedFormat unless content.ext 10 | raise UnsupportedFormat unless SUPPORTED_FORMATS.include?(content.ext.downcase) 11 | 12 | options = DragonflyLibvips.stringify_keys(options) 13 | format = options.fetch("format", content.ext) 14 | 15 | input_options = options.fetch("input_options", {}) 16 | 17 | # input_options['access'] ||= 'sequential' 18 | if content.mime_type == "image/jpeg" 19 | input_options["autorotate"] = true unless input_options.has_key?("autorotate") 20 | end 21 | 22 | output_options = options.fetch("output_options", {}) 23 | if FORMATS_WITHOUT_PROFILE_SUPPORT.include?(format) 24 | output_options.delete("profile") 25 | else 26 | output_options["profile"] ||= input_options.fetch("profile", EPROFILE_PATH) 27 | end 28 | output_options.delete("Q") unless /jpg|jpeg/i.match?(format.to_s) 29 | output_options["format"] ||= format.to_s if /gif|bmp/i.match?(format.to_s) 30 | 31 | img = ::Vips::Image.new_from_file(content.path, **DragonflyLibvips.symbolize_keys(input_options)) 32 | img = img.extract_area(x, y, width, height) 33 | 34 | content.update( 35 | img.write_to_buffer(".#{format}", **DragonflyLibvips.symbolize_keys(output_options)), 36 | "name" => "temp.#{format}", 37 | "format" => format 38 | ) 39 | content.ext = format 40 | end 41 | 42 | def update_url(url_attributes, _, _, _, _, options = {}) 43 | options = options.transform_keys { |k| k.to_s } # stringify keys 44 | return unless format = options.fetch("format", nil) 45 | url_attributes.ext = format 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/dragonfly_libvips/processors/rotate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "vips" 4 | 5 | module DragonflyLibvips 6 | module Processors 7 | class Rotate 8 | def call(content, rotate, options = {}) 9 | raise UnsupportedFormat unless content.ext 10 | raise UnsupportedFormat unless SUPPORTED_FORMATS.include?(content.ext.downcase) 11 | 12 | options = DragonflyLibvips.stringify_keys(options) 13 | format = options.fetch("format", content.ext) 14 | 15 | input_options = options.fetch("input_options", {}) 16 | 17 | # input_options['access'] ||= 'sequential' 18 | if content.mime_type == "image/jpeg" 19 | input_options["autorotate"] = true unless input_options.has_key?("autorotate") 20 | end 21 | 22 | output_options = options.fetch("output_options", {}) 23 | if FORMATS_WITHOUT_PROFILE_SUPPORT.include?(format) 24 | output_options.delete("profile") 25 | else 26 | output_options["profile"] ||= input_options.fetch("profile", EPROFILE_PATH) 27 | end 28 | output_options.delete("Q") unless /jpg|jpeg/i.match?(format.to_s) 29 | output_options["format"] ||= format.to_s if /gif|bmp/i.match?(format.to_s) 30 | 31 | img = ::Vips::Image.new_from_file(content.path, **DragonflyLibvips.symbolize_keys(input_options)) 32 | img = img.rot("d#{rotate}") 33 | 34 | content.update( 35 | img.write_to_buffer(".#{format}", **DragonflyLibvips.symbolize_keys(output_options)), 36 | "name" => "temp.#{format}", 37 | "format" => format 38 | ) 39 | content.ext = format 40 | end 41 | 42 | def update_url(url_attributes, _, options = {}) 43 | options = options.transform_keys { |k| k.to_s } # stringify keys 44 | return unless format = options.fetch("format", nil) 45 | url_attributes.ext = format 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/dragonfly_libvips/processors/thumb.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dragonfly_libvips/dimensions" 4 | require "vips" 5 | 6 | module DragonflyLibvips 7 | module Processors 8 | class Thumb 9 | OPERATORS = "><" 10 | RESIZE_GEOMETRY = /\A\d*x\d*[#{OPERATORS}]?\z/ # e.g. '300x200>' 11 | DPI = 300 12 | 13 | def call(content, geometry, options = {}) 14 | raise UnsupportedFormat unless content.ext 15 | raise UnsupportedFormat unless SUPPORTED_FORMATS.include?(content.ext.downcase) 16 | 17 | options = DragonflyLibvips.stringify_keys(options) 18 | 19 | filename = content.path 20 | format = options.fetch("format", content.ext).to_s 21 | 22 | input_options = options.fetch("input_options", {}) 23 | input_options["access"] = input_options.fetch("access", "sequential") 24 | input_options["autorotate"] = input_options.fetch("autorotate", true) if content.mime_type == "image/jpeg" 25 | 26 | if content.mime_type == "application/pdf" 27 | input_options["dpi"] = input_options.fetch("dpi", DPI) 28 | input_options["page"] = input_options.fetch("page", 0) 29 | else 30 | input_options.delete("page") 31 | input_options.delete("dpi") 32 | end 33 | 34 | output_options = options.fetch("output_options", {}) 35 | if FORMATS_WITHOUT_PROFILE_SUPPORT.include?(format) 36 | output_options.delete("profile") 37 | else 38 | output_options["profile"] ||= input_options.fetch("profile", EPROFILE_PATH) 39 | end 40 | output_options.delete("Q") unless /jpg|jpeg/i.match?(format.to_s) 41 | output_options["format"] ||= format.to_s if /bmp/i.match?(format.to_s) 42 | 43 | input_options = input_options.transform_keys { |k| k.to_sym } # symbolize 44 | img = ::Vips::Image.new_from_file(filename, **DragonflyLibvips.symbolize_keys(input_options)) 45 | 46 | dimensions = case geometry 47 | when RESIZE_GEOMETRY then Dimensions.call(geometry, img.width, img.height) 48 | else raise ArgumentError, "Didn't recognise the geometry string: #{geometry}" 49 | end 50 | 51 | thumbnail_options = options.fetch("thumbnail_options", {}) 52 | if Vips.at_least_libvips?(8, 8) 53 | thumbnail_options["no_rotate"] = input_options.fetch("no_rotate", false) if content.mime_type == "image/jpeg" 54 | else 55 | thumbnail_options["auto_rotate"] = input_options.fetch("autorotate", true) if content.mime_type == "image/jpeg" 56 | end 57 | thumbnail_options["height"] = thumbnail_options.fetch("height", dimensions.height.ceil) 58 | thumbnail_options["import_profile"] = CMYK_PROFILE_PATH if img.get("interpretation") == :cmyk 59 | thumbnail_options["size"] ||= case geometry 60 | when />\z/ then :down # do_not_resize_if_image_smaller_than_requested 61 | when /<\z/ then :up # do_not_resize_if_image_larger_than_requested 62 | else :both 63 | end 64 | 65 | filename += "[page=#{input_options[:page]}]" if content.mime_type == "application/pdf" 66 | 67 | thumbnail_options = thumbnail_options.transform_keys { |k| k.to_sym } # symbolize 68 | thumb = ::Vips::Image.thumbnail(filename, dimensions.width.ceil, **DragonflyLibvips.symbolize_keys(thumbnail_options)) 69 | 70 | content.update( 71 | thumb.write_to_buffer(".#{format}", **DragonflyLibvips.symbolize_keys(output_options)), 72 | "name" => "temp.#{format}", 73 | "format" => format 74 | ) 75 | content.ext = format 76 | end 77 | 78 | def update_url(url_attributes, _, options = {}) 79 | options = options.transform_keys { |k| k.to_s } # stringify keys 80 | return unless format = options.fetch("format", nil) 81 | url_attributes.ext = format 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/dragonfly_libvips/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DragonflyLibvips 4 | VERSION = "2.5.1" 5 | end 6 | -------------------------------------------------------------------------------- /samples/landscape_sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomasc/dragonfly_libvips/a36cdb912a874ce74a67600fecbff79f2aa9c294/samples/landscape_sample.png -------------------------------------------------------------------------------- /samples/sample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomasc/dragonfly_libvips/a36cdb912a874ce74a67600fecbff79f2aa9c294/samples/sample.gif -------------------------------------------------------------------------------- /samples/sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomasc/dragonfly_libvips/a36cdb912a874ce74a67600fecbff79f2aa9c294/samples/sample.jpg -------------------------------------------------------------------------------- /samples/sample.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomasc/dragonfly_libvips/a36cdb912a874ce74a67600fecbff79f2aa9c294/samples/sample.pdf -------------------------------------------------------------------------------- /samples/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomasc/dragonfly_libvips/a36cdb912a874ce74a67600fecbff79f2aa9c294/samples/sample.png -------------------------------------------------------------------------------- /samples/sample.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /samples/sample.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomasc/dragonfly_libvips/a36cdb912a874ce74a67600fecbff79f2aa9c294/samples/sample.tif -------------------------------------------------------------------------------- /samples/sample_anim.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomasc/dragonfly_libvips/a36cdb912a874ce74a67600fecbff79f2aa9c294/samples/sample_anim.gif -------------------------------------------------------------------------------- /samples/sample_cmyk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomasc/dragonfly_libvips/a36cdb912a874ce74a67600fecbff79f2aa9c294/samples/sample_cmyk.jpg -------------------------------------------------------------------------------- /samples/white pixel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomasc/dragonfly_libvips/a36cdb912a874ce74a67600fecbff79f2aa9c294/samples/white pixel.png -------------------------------------------------------------------------------- /test/dragonfly_libvips/analysers/image_properties_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | describe DragonflyLibvips::Analysers::ImageProperties do 6 | let(:app) { test_libvips_app } 7 | let(:analyser) { DragonflyLibvips::Analysers::ImageProperties.new } 8 | let(:png) { Dragonfly::Content.new(app, SAMPLES_DIR.join("sample.png")) } # 280x355 9 | let(:jpg) { Dragonfly::Content.new(app, SAMPLES_DIR.join("sample.jpg")) } # 280x355 10 | 11 | it { _(analyser.call(png)).must_equal("format" => "png", "width" => 280, "height" => 355, "xres" => 72.0, "yres" => 72.0, "progressive" => false) } 12 | 13 | describe "jpgs" do 14 | it { _(analyser.call(jpg)["progressive"]).must_equal false } 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/dragonfly_libvips/dimensions_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | describe DragonflyLibvips::Dimensions do 6 | let(:geometry) { "" } 7 | let(:orig_w) { nil } 8 | let(:orig_h) { nil } 9 | let(:result) { DragonflyLibvips::Dimensions.call(geometry, orig_w, orig_h) } 10 | 11 | describe "NNxNN" do 12 | let(:geometry) { "250x250" } 13 | 14 | describe "when square" do 15 | let(:orig_w) { 1000 } 16 | let(:orig_h) { 1000 } 17 | 18 | it { _(result.width).must_equal 250 } 19 | it { _(result.height).must_equal 250 } 20 | it { _(result.scale).must_equal 250.0 / orig_w } 21 | 22 | describe "250x250>" do 23 | let(:geometry) { "250x250>" } 24 | 25 | describe "when image larger than specified" do 26 | it "resize" do 27 | _(result.width).must_equal 250 28 | _(result.height).must_equal 250 29 | _(result.scale).must_equal 250.0 / orig_w 30 | end 31 | end 32 | 33 | describe "when image smaller than specified" do 34 | let(:orig_w) { 100 } 35 | let(:orig_h) { 100 } 36 | it "do not resize" do 37 | _(result.width).must_equal 100 38 | _(result.height).must_equal 100 39 | _(result.scale).must_equal 100.0 / orig_w 40 | end 41 | end 42 | end 43 | 44 | describe "250x50<" do 45 | let(:geometry) { "250x250<" } 46 | 47 | describe "when image larger than specified" do 48 | it "do not resize" do 49 | _(result.width).must_equal 1000 50 | _(result.height).must_equal 1000 51 | _(result.scale).must_equal 1000.0 / orig_w 52 | end 53 | end 54 | 55 | describe "when image smaller than specified" do 56 | let(:orig_w) { 100 } 57 | let(:orig_h) { 100 } 58 | 59 | it "do resize" do 60 | _(result.width).must_equal 250 61 | _(result.height).must_equal 250 62 | _(result.scale).must_equal 250.0 / orig_w 63 | end 64 | end 65 | end 66 | end 67 | 68 | describe "when landscape" do 69 | let(:orig_w) { 1000 } 70 | let(:orig_h) { 500 } 71 | 72 | it { _(result.width).must_equal 250 } 73 | it { _(result.height).must_equal 125 } 74 | it { _(result.scale).must_equal 250.0 / orig_w } 75 | end 76 | 77 | describe "when portrait" do 78 | let(:orig_w) { 500 } 79 | let(:orig_h) { 1000 } 80 | 81 | it { _(result.width).must_equal 125 } 82 | it { _(result.height).must_equal 250 } 83 | it { _(result.scale).must_equal 125.0 / orig_w } 84 | end 85 | end 86 | 87 | describe "NNx" do 88 | let(:geometry) { "250x" } 89 | 90 | describe "when square" do 91 | let(:orig_w) { 1000 } 92 | let(:orig_h) { 1000 } 93 | 94 | it { _(result.width).must_equal 250 } 95 | it { _(result.height).must_equal 250 } 96 | it { _(result.scale).must_equal 250.0 / orig_w } 97 | end 98 | 99 | describe "when landscape" do 100 | let(:orig_w) { 1000 } 101 | let(:orig_h) { 500 } 102 | 103 | it { _(result.width).must_equal 250 } 104 | it { _(result.height).must_equal 125 } 105 | it { _(result.scale).must_equal 250.0 / orig_w } 106 | end 107 | 108 | describe "when portrait" do 109 | let(:orig_w) { 500 } 110 | let(:orig_h) { 1000 } 111 | 112 | it { _(result.width).must_equal 250 } 113 | it { _(result.height).must_equal 500 } 114 | it { _(result.scale).must_equal 250.0 / orig_w } 115 | end 116 | end 117 | 118 | describe "xNN" do 119 | let(:geometry) { "x250" } 120 | 121 | describe "when square" do 122 | let(:orig_w) { 1000 } 123 | let(:orig_h) { 1000 } 124 | 125 | it { _(result.width).must_equal 250 } 126 | it { _(result.height).must_equal 250 } 127 | it { _(result.scale).must_equal 250.0 / orig_w } 128 | end 129 | 130 | describe "when landscape" do 131 | let(:orig_w) { 1000 } 132 | let(:orig_h) { 500 } 133 | 134 | it { _(result.width).must_equal 500 } 135 | it { _(result.height).must_equal 250 } 136 | it { _(result.scale).must_equal 500.0 / orig_w } 137 | end 138 | 139 | describe "when portrait" do 140 | let(:orig_w) { 500 } 141 | let(:orig_h) { 1000 } 142 | 143 | it { _(result.width).must_equal 125 } 144 | it { _(result.height).must_equal 250 } 145 | it { _(result.scale).must_equal 125.0 / orig_w } 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /test/dragonfly_libvips/plugin_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "openssl" 5 | 6 | module DragonflyLibvips 7 | describe Plugin do 8 | let(:app) { test_app.configure_with(:libvips) } 9 | let(:content) { app.fetch_file(SAMPLES_DIR.join("sample.png")) } 10 | 11 | describe "analysers" do 12 | it { _(content.width).must_equal 280 } 13 | it { _(content.height).must_equal 355 } 14 | it { _(content.aspect_ratio).must_equal (280.0 / 355.0) } 15 | it { _(content.xres).must_equal 72.0 } 16 | it { _(content.yres).must_equal 72.0 } 17 | 18 | it { _(content).must_be :portrait? } 19 | it { _(content.portrait).must_equal true } # for use with magic attributes 20 | it { _(content).wont_be :landscape? } 21 | it { _(content.landscape).must_equal false } # for use with magic attributes 22 | 23 | it { _(content.format).must_equal "png" } 24 | 25 | it { _(content).must_be :image? } 26 | it { _(content.image).must_equal true } # for use with magic attributes 27 | it { _(app.create("blah")).wont_be :image? } 28 | end 29 | 30 | describe "processors that change the url" do 31 | before { app.configure { url_format "/:name" } } 32 | 33 | describe "encode" do 34 | let(:thumb) { content.encode("png") } 35 | 36 | it { _(thumb.url).must_match(/^\/sample\.png\?.*job=\w+/) } 37 | it { _(thumb.format).must_equal "png" } 38 | it { _(thumb.meta["format"]).must_equal "png" } 39 | end 40 | 41 | describe "rotate" do 42 | let(:thumb) { content.rotate(90, format: "png") } 43 | 44 | it { _(thumb.url).must_match(/^\/sample\.png\?.*job=\w+/) } 45 | it { _(thumb.format).must_equal "png" } 46 | it { _(thumb.meta["format"]).must_equal "png" } 47 | end 48 | 49 | describe "thumb" do 50 | let(:thumb) { content.thumb("100x", format: "png") } 51 | 52 | it { _(thumb.url).must_match(/^\/sample\.png\?.*job=\w+/) } 53 | it { _(thumb.format).must_equal "png" } 54 | it { _(thumb.meta["format"]).must_equal "png" } 55 | end 56 | end 57 | 58 | describe "other processors" do 59 | describe "encode" do 60 | it { _(content.encode("jpg").format).must_equal "jpg" } 61 | it { _(content.encode("jpg", output_options: { Q: 1 }).format).must_equal "jpg" } 62 | it { _(content.encode("jpg", output_options: { Q: 1 }).size).must_be :<, 65_000 } 63 | end 64 | 65 | describe "extract_area" do 66 | it { _(content.extract_area(100, 100, 50, 200, format: "jpg").format).must_equal "jpg" } 67 | it { _(content.extract_area(100, 100, 50, 200, format: "jpg").width).must_equal 50 } 68 | it { _(content.extract_area(100, 100, 50, 200, format: "jpg").height).must_equal 200 } 69 | end 70 | 71 | describe "rotate" do 72 | it { _(content.rotate(90).width).must_equal 355 } 73 | it { _(content.rotate(90).height).must_equal 280 } 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /test/dragonfly_libvips/processors/encode_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | describe DragonflyLibvips::Processors::Encode do 6 | let(:app) { test_libvips_app } 7 | let(:content_image) { Dragonfly::Content.new(app, SAMPLES_DIR.join("sample.png")) } # 280x355 8 | let(:processor) { DragonflyLibvips::Processors::Encode.new } 9 | 10 | describe "SUPPORTED_FORMATS" do 11 | DragonflyLibvips::SUPPORTED_FORMATS.each do |format| 12 | unless File.exist?(SAMPLES_DIR.join("sample.#{format}")) 13 | it(format) { skip "sample.#{format} does not exist, skipping" } 14 | next 15 | end 16 | 17 | let(:content) { app.fetch_file SAMPLES_DIR.join("sample.#{format}") } 18 | 19 | DragonflyLibvips::SUPPORTED_OUTPUT_FORMATS.each do |output_format| 20 | it("#{format} to #{output_format}") do 21 | _(content.encode(output_format).mime_type).must_equal Rack::Mime.mime_type(".#{output_format}") 22 | _(content.encode(output_format).size).must_be :>, 0 23 | _(content.encode(output_format).tempfile.path).must_match(/\.#{output_format_short(output_format)}\z/) 24 | end 25 | end 26 | end 27 | end 28 | 29 | describe "allows for options" do 30 | before { processor.call(content_image, "jpg", output_options: { Q: 50 }) } 31 | it { _(content_image.ext).must_equal "jpg" } 32 | end 33 | 34 | def output_format_short(format) 35 | case format 36 | when "tiff" then "tif" 37 | when "jpeg" then "jpg" 38 | else format 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/dragonfly_libvips/processors/extract_area_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | describe DragonflyLibvips::Processors::ExtractArea do 6 | let(:app) { test_libvips_app } 7 | let(:content) { Dragonfly::Content.new(app, SAMPLES_DIR.join("sample.png")) } # 280x355 8 | let(:processor) { DragonflyLibvips::Processors::ExtractArea.new } 9 | 10 | let(:x) { 100 } 11 | let(:y) { 100 } 12 | let(:width) { 100 } 13 | let(:height) { 200 } 14 | 15 | describe "keep format" do 16 | before { processor.call(content, x, y, width, height) } 17 | 18 | it { _(content).must_have_width width } 19 | it { _(content).must_have_height height } 20 | end 21 | 22 | describe "convert to format" do 23 | before { processor.call(content, x, y, width, height, format: "jpg") } 24 | 25 | it { _(content).must_have_width width } 26 | it { _(content).must_have_height height } 27 | it { _(content.ext).must_equal "jpg" } 28 | end 29 | 30 | describe "tempfile has extension" do 31 | before { processor.call(content, x, y, width, height, format: "jpg") } 32 | it { _(content.tempfile.path).must_match(/\.jpg\z/) } 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/dragonfly_libvips/processors/rotate_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | describe DragonflyLibvips::Processors::Rotate do 6 | let(:app) { test_libvips_app } 7 | let(:content) { Dragonfly::Content.new(app, SAMPLES_DIR.join("sample.png")) } # 280x355 8 | let(:processor) { DragonflyLibvips::Processors::Rotate.new } 9 | 10 | describe "rotate 90" do 11 | before { processor.call(content, 90) } 12 | 13 | it { _(content).must_have_width 355 } 14 | it { _(content).must_have_height 280 } 15 | end 16 | 17 | describe "rotate 180" do 18 | before { processor.call(content, 180) } 19 | 20 | it { _(content).must_have_width 280 } 21 | it { _(content).must_have_height 355 } 22 | end 23 | 24 | describe "rotate 270" do 25 | before { processor.call(content, 270) } 26 | 27 | it { _(content).must_have_width 355 } 28 | it { _(content).must_have_height 280 } 29 | end 30 | 31 | describe "rotate with format" do 32 | before { processor.call(content, 90, format: "jpg") } 33 | 34 | it { _(content).must_have_width 355 } 35 | it { _(content).must_have_height 280 } 36 | it { _(content.ext).must_equal "jpg" } 37 | end 38 | 39 | describe "tempfile has extension" do 40 | before { processor.call(content, 90, format: "jpg") } 41 | it { _(content.tempfile.path).must_match(/\.jpg\z/) } 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/dragonfly_libvips/processors/thumb_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "ostruct" 5 | 6 | describe DragonflyLibvips::Processors::Thumb do 7 | let(:app) { test_libvips_app } 8 | let(:image) { Dragonfly::Content.new(app, SAMPLES_DIR.join("sample.png")) } # 280x355 9 | let(:pdf) { Dragonfly::Content.new(app, SAMPLES_DIR.join("sample.pdf")) } 10 | let(:jpg) { Dragonfly::Content.new(app, SAMPLES_DIR.join("sample.jpg")) } 11 | let(:cmyk) { Dragonfly::Content.new(app, SAMPLES_DIR.join("sample_cmyk.jpg")) } 12 | let(:gif) { Dragonfly::Content.new(app, SAMPLES_DIR.join("sample.gif")) } 13 | let(:anim_gif) { Dragonfly::Content.new(app, SAMPLES_DIR.join("sample_anim.gif")) } 14 | let(:landscape_image) { Dragonfly::Content.new(app, SAMPLES_DIR.join("landscape_sample.png")) } # 355x280 15 | let(:processor) { DragonflyLibvips::Processors::Thumb.new } 16 | 17 | it "raises an error if an unrecognized string is given" do 18 | assert_raises(ArgumentError) do 19 | processor.call(image, "30x40#ne!") 20 | end 21 | end 22 | 23 | describe "cmyk images" do 24 | before { processor.call(cmyk, "30x") } 25 | it { _(cmyk).must_have_width 30 } 26 | end 27 | 28 | describe "resizing" do 29 | describe "xNN" do 30 | before { processor.call(landscape_image, "x30") } 31 | it { _(landscape_image).must_have_width 38 } 32 | it { _(landscape_image).must_have_height 30 } 33 | end 34 | 35 | describe "NNx" do 36 | before { processor.call(image, "30x") } 37 | it { _(image).must_have_width 30 } 38 | it { _(image).must_have_height 38 } 39 | end 40 | 41 | describe "NNxNN" do 42 | before { processor.call(image, "30x30") } 43 | it { _(image).must_have_width 24 } 44 | it { _(image).must_have_height 30 } 45 | end 46 | 47 | describe "NNxNN>" do 48 | describe "if the image is smaller than specified" do 49 | before { processor.call(image, "1000x1000>") } 50 | it { _(image).must_have_width 280 } 51 | it { _(image).must_have_height 355 } 52 | end 53 | 54 | describe "if the image is larger than specified" do 55 | before { processor.call(image, "30x30>") } 56 | it { _(image).must_have_width 24 } 57 | it { _(image).must_have_height 30 } 58 | end 59 | end 60 | 61 | describe "NNxNN<" do 62 | describe "if the image is larger than specified" do 63 | before { processor.call(image, "10x10<") } 64 | it { _(image).must_have_width 280 } 65 | it { _(image).must_have_height 355 } 66 | end 67 | 68 | describe "if the image is smaller than specified" do 69 | before { processor.call(image, "500x500<") } 70 | it { _(image).must_have_width 394 } 71 | it { _(image).must_have_height 500 } 72 | end 73 | end 74 | end 75 | 76 | describe "pdf" do 77 | describe "resize" do 78 | before { processor.call(pdf, "500x500", format: "jpg") } 79 | # it { _(pdf).must_have_width 386 } 80 | it { _(pdf).must_have_height 500 } 81 | end 82 | 83 | describe "page param" do 84 | before { processor.call(pdf, "500x500", format: "jpg", input_options: { page: 0 }) } 85 | # it { _(pdf).must_have_width 386 } 86 | it { _(pdf).must_have_height 500 } 87 | end 88 | end 89 | 90 | describe "jpg" do 91 | describe "progressive" do 92 | before { processor.call(jpg, "300x", output_options: { interlace: true }) } 93 | it { _(`vipsheader -f jpeg-multiscan #{jpg.file.path}`.to_i).must_equal 1 } 94 | end 95 | end 96 | 97 | describe "gif" do 98 | describe "static" do 99 | before { processor.call(gif, "200x") } 100 | it { _(gif).must_have_width 200 } 101 | end 102 | 103 | describe "animated" do 104 | before { processor.call(anim_gif, "200x") } 105 | it { 106 | skip "waiting for full support" 107 | _(gif).must_have_width 200 108 | } 109 | end 110 | end 111 | 112 | describe "format" do 113 | let(:url_attributes) { OpenStruct.new } 114 | 115 | describe "when format passed in" do 116 | before { processor.call(image, "2x2", format: "jpeg", output_options: { Q: 50 }) } 117 | it { _(image.ext).must_equal "jpeg" } 118 | it { _(image.size).must_be :<, 65_000 } 119 | end 120 | 121 | describe "when format not passed in" do 122 | before { processor.call(image, "2x2") } 123 | it { _(image.ext).must_equal "png" } 124 | end 125 | 126 | describe "when ext passed in" do 127 | before { processor.update_url(url_attributes, "2x2", "format" => "png") } 128 | it { _(url_attributes.ext).must_equal "png" } 129 | end 130 | 131 | describe "when ext not passed in" do 132 | before { processor.update_url(url_attributes, "2x2") } 133 | it { _(url_attributes.ext).must_be_nil } 134 | end 135 | end 136 | 137 | describe "tempfile has extension" do 138 | let(:format) { "jpg" } 139 | before { processor.call(image, "100x", format: "jpg") } 140 | it { _(image.tempfile.path).must_match(/\.#{format}\z/) } 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /test/support/image_assertions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "vips" 4 | 5 | def image_properties(content) 6 | img = Vips::Image.new_from_file(content.path, access: :sequential) 7 | 8 | { 9 | format: File.extname(img.filename)[1..-1], 10 | width: img.width, 11 | height: img.height, 12 | xres: img.xres, 13 | yres: img.yres, 14 | } 15 | end 16 | 17 | module MiniTest::Assertions 18 | [:width, :height, :format].each do |property| 19 | define_method "assert_#{property}" do |obj, value| 20 | assert_equal value, image_properties(obj)[property] 21 | end 22 | Object.infect_an_assertion "assert_#{property}", "must_have_#{property}", :reverse 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/setup" 4 | 5 | require "minitest" 6 | require "minitest/autorun" 7 | require "minitest/reporters" 8 | require "minitest/spec" 9 | 10 | require "dragonfly" 11 | require "dragonfly_libvips" 12 | 13 | SAMPLES_DIR = Pathname.new(File.expand_path("../../samples", __FILE__)) 14 | 15 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 16 | 17 | Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new 18 | 19 | def test_app(name = nil) 20 | Dragonfly::App.instance(name).tap do |app| 21 | app.datastore = Dragonfly::MemoryDataStore.new 22 | app.secret = "test secret" 23 | end 24 | end 25 | 26 | def test_libvips_app 27 | test_app.configure do 28 | plugin :libvips 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /vendor/cmyk.icm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomasc/dragonfly_libvips/a36cdb912a874ce74a67600fecbff79f2aa9c294/vendor/cmyk.icm -------------------------------------------------------------------------------- /vendor/sRGB_v4_ICC_preference.icc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomasc/dragonfly_libvips/a36cdb912a874ce74a67600fecbff79f2aa9c294/vendor/sRGB_v4_ICC_preference.icc --------------------------------------------------------------------------------