Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for (animated) GIFs #36

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions Gemfile.lock
Expand Up @@ -33,7 +33,7 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
minitest (5.11.3)
rmagick (2.16.0)
rmagick (4.2.1)
rspec (3.5.0)
rspec-core (~> 3.5.0)
rspec-expectations (~> 3.5.0)
Expand Down Expand Up @@ -67,4 +67,4 @@ DEPENDENCIES
sqlite3

BUNDLED WITH
1.16.1
1.17.3
3 changes: 0 additions & 3 deletions README.md
Expand Up @@ -20,9 +20,6 @@ If you are using bundler, add this to your Gemfile:

gem 'carrierwave-vips'

You will need to install the `rmagick` gem if you want to load GIF files. Writing GIFs is not supported by ruby-vips or this library.


A quick overview
---------------------

Expand Down
104 changes: 79 additions & 25 deletions lib/carrierwave/vips.rb
Expand Up @@ -8,7 +8,7 @@ def self.configure
c = Struct.new(:sharpen_mask, :sharpen_scale, :allowed_formats).new
c.sharpen_mask = [ [ -1, -1, -1 ], [ -1, 24, -1 ], [ -1, -1, -1 ] ]
c.sharpen_scale = 16
c.allowed_formats = %w(jpeg jpg png)
c.allowed_formats = %w(jpeg jpg png gif)
c
end
@config
Expand Down Expand Up @@ -129,7 +129,7 @@ def convert(f, opts = {})
#
def resize_to_fit(new_width, new_height)
manipulate! do |image|
resize_image(image,new_width,new_height)
resize_image(image, new_width, new_height)
end
end

Expand All @@ -146,27 +146,20 @@ def resize_to_fit(new_width, new_height)
#
def resize_to_fill(new_width, new_height)
manipulate! do |image|

image = resize_image image, new_width, new_height, :max

if image.width > new_width
top = 0
left = (image.width - new_width) / 2
elsif image.height > new_height
left = 0
top = (image.height - new_height) / 2
if gif?
page_height = image.get 'page-height'
frame_count = image.height / page_height
frames = frame_count.times.map do |i|
frame = image.crop(0, i * page_height, image.width, page_height)
_resize_to_fill(frame, new_width, new_height)
end
new_image = ::Vips::Image.arrayjoin(frames, across: 1)
new_image.set('page-height', new_height)

new_image
else
left = 0
top = 0
_resize_to_fill(image, new_width, new_height)
end

# Floating point errors can sometimes chop off an extra pixel
# TODO: fix all the universe so that floating point errors never happen again
new_height = image.height if image.height < new_height
new_width = image.width if image.width < new_width

image.extract_area(left, top, new_width, new_height)

end
end

Expand All @@ -183,7 +176,8 @@ def resize_to_fill(new_width, new_height)
#
def resize_to_limit(new_width, new_height)
manipulate! do |image|
image = resize_image(image,new_width,new_height) if new_width < image.width || new_height < image.height
ratio = get_ratio image, new_width, new_height, :min
image = resize_image(image, new_width, new_height) if ratio < 1
image
end
end
Expand Down Expand Up @@ -244,6 +238,8 @@ def get_image
cache_stored_file! unless cached?
@_vimage ||= if jpeg? || png?
::Vips::Image.new_from_file(current_path, access: :sequential)
elsif gif?
::Vips::Image.new_from_file(current_path, n: -1)
else
::Vips::Image.new_from_file(current_path)
end
Expand All @@ -254,20 +250,74 @@ def write_opts
end

def resize_image(image, width, height, min_or_max = :min)
if gif?
page_height = image.get 'page-height'
frame_count = image.height / page_height
frames = frame_count.times.map do |i|
frame = image.crop(0, i * page_height, image.width, page_height)
_resize_image(frame, width, height, min_or_max)
end

new_image = ::Vips::Image.arrayjoin(frames, across: 1)
ratio = get_ratio image, width, height, min_or_max
new_image.set('page-height', (page_height * ratio).round)

new_image
else
_resize_image(image, width, height, min_or_max)
end
end

def _resize_image(image, width, height, min_or_max = :min)
ratio = get_ratio image, width, height, min_or_max
return image if ratio == 1
if ratio > 1
image = image.resize(ratio, kernel: :nearest)
else
image = image.resize(ratio, kernel: :cubic)
image = image.conv(cwv_sharpen_mask) if cwv_config.sharpen_mask
image = image.conv(cwv_sharpen_mask) if cwv_config.sharpen_mask && !gif?
end
image
end

def get_ratio(image, width,height, min_or_max = :min)
def _resize_to_fill(image, new_width, new_height)
image = _resize_image image, new_width, new_height, :max

if new_width && image.width > new_width
top = 0
left = (image.width - new_width) / 2
elsif new_height && image.height > new_height
left = 0
top = (image.height - new_height) / 2
else
left = 0
top = 0
end

# Floating point errors can sometimes chop off an extra pixel
# TODO: fix all the universe so that floating point errors never happen again
new_height = image.height if new_height.nil? || image.height < new_height
new_width = image.width if new_width.nil? || image.width < new_width

image.extract_area(left, top, new_width, new_height)
end

def get_image_height(image)
if gif?
image.get 'page-height'
else
image.height
end
end

def get_ratio(image, width, height, min_or_max = :min)
image_height = get_image_height image

return width.to_f / image.width if height.nil?
return height.to_f / image_height if width.nil?

width_ratio = width.to_f / image.width
height_ratio = height.to_f / image.height
height_ratio = height.to_f / image_height
[width_ratio, height_ratio].send(min_or_max)
end

Expand All @@ -279,6 +329,10 @@ def png?(path = current_path)
ext(path) == 'png'
end

def gif?(path = current_path)
ext(path) == 'gif'
end

def write_jpeg?(path)
format_override == 'jpg' || jpeg?(path)
end
Expand Down
102 changes: 82 additions & 20 deletions spec/carrierwave/vips_spec.rb
Expand Up @@ -19,7 +19,7 @@ class ImageUploader < CarrierWave::Uploader::Base
include CarrierWave::Vips

version :big_thumb do
process :resize_to_fill => [800,800]
process :resize_to_fill => [800, 800]
end

version :thumb do
Expand All @@ -38,30 +38,26 @@ class Dummy < ActiveRecord::Base


describe CarrierWave::Vips do

let(:instance) { create_instance }

after do
Dir[file_path('*.copy.jpg')].each do |file|
Dir[file_path('*.copy.{jpg,gif}')].each do |file|
FileUtils.rm(file)
end
end

# Gotta figure out how to test this properly.
it 'performs multiple operations properly'

describe "#convert" do
describe '#convert' do

it 'converts from one format to another' do
instance.convert('png')
instance.process!
expect(instance.filename).to match(/png$/)
end

it 'throws an error on gif' do
expect { instance.convert('gif') }.to raise_error(ArgumentError)
end

context 'when allowed formats are configured' do
around do |example|
original_formats = CarrierWave::Vips.configure.allowed_formats
Expand Down Expand Up @@ -90,51 +86,95 @@ class Dummy < ActiveRecord::Base
end

describe '#resize_to_fill' do

it 'resizes the image to exactly the given dimensions' do
instance.resize_to_fill(200,200)
instance.resize_to_fill(200, 200)
instance.process!
expect(instance).to have_dimensions(200, 200)
end

it 'scales up the image if it smaller than the given dimensions' do
instance.resize_to_fill(1000,1000)
it 'scales up the image if it is smaller than the given dimensions' do
instance.resize_to_fill(1000, 1000)
instance.process!
expect(instance).to have_dimensions(1000, 1000)
end

it 'does not throw error on exact dimensions' do
instance.resize_to_fill(640,480)
instance.resize_to_fill(640, 480)
instance.process!
expect(instance).to have_dimensions(640,480)
expect(instance).to have_dimensions(640, 480)
end

it 'recovers on floating point errors leading to overcrops' do
instance = create_instance('wonky-resize.jpg')
instance.resize_to_fill(200,200)
instance.resize_to_fill(200, 200)
instance.process!
end

it 'accepts a nil value for width or height' do
instance.resize_to_fill(200, nil)
instance.process!
expect(instance).to have_dimensions(200, 150)
end

describe 'gif' do
let(:instance) { create_instance('ani.gif') }

it 'resizes the frames' do
instance.resize_to_fill(200, 200)
instance.process!
expect(instance).to have_dimensions(200, 200)
end

it 'scales up the image if it is smaller than the given dimensions' do
instance.resize_to_fill(960, 960)
instance.process!
expect(instance).to have_dimensions(960, 960)
end
end

end

describe '#resize_to_fit' do

it 'resizes the image to fit within the given dimensions' do
instance.resize_to_fit(200, 200)
instance.process!
expect(instance).to have_dimensions(200, 150)
end

it 'scales up the image if it smaller than the given dimensions' do
it 'scales up the image if it is smaller than the given dimensions' do
instance.resize_to_fit(1000, 1000)
instance.process!
expect(instance).to have_dimensions(1000, 750)
end


it 'accepts a nil value for width or height' do
instance.resize_to_fit(nil, 150)
instance.process!
expect(instance).to have_dimensions(200, 150)
end

describe 'gif' do
let(:instance) { create_instance('ani.gif') }

it 'resizes the frames' do
instance.resize_to_fit(240, 240)
instance.process!
expect(instance).to have_dimensions(240, 135)
end

it 'scales up the image if it is smaller than the given dimensions' do
instance.resize_to_fit(960, 960)
instance.process!
expect(instance).to have_dimensions(960, 540)
end
end

end

describe '#resize_to_limit' do

it 'resizes the image to fit within the given dimensions' do
instance.resize_to_limit(200, 200)
instance.process!
Expand All @@ -147,6 +187,28 @@ class Dummy < ActiveRecord::Base
expect(instance).to have_dimensions(640, 480)
end

it 'accepts a nil value for width or height' do
instance.resize_to_limit(200, nil)
instance.process!
expect(instance).to have_dimensions(200, 150)
end

describe 'gif' do
let(:instance) { create_instance('ani.gif') }

it 'resizes the frames' do
instance.resize_to_limit(200, 200)
instance.process!
expect(instance).to have_dimensions(200, 113)
end

it 'does not scale up the image if it is smaller than the given dimensions' do
instance.resize_to_limit(1000, 1000)
instance.process!
expect(instance).to have_dimensions(480, 270)
end
end

end

describe '#strip' do
Expand Down
Binary file added spec/fixtures/ani.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.