1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
|
# frozen_string_literal: true
module Paperclip
class LazyThumbnail < Paperclip::Processor
ALLOWED_FIELDS = %w(
width
height
bands
format
coding
interpretation
icc-profile-data
page-height
n-pages
loop
delay
).freeze
class PixelGeometryParser
def self.parse(current_geometry, pixels)
width = Math.sqrt(pixels * (current_geometry.width.to_f / current_geometry.height)).round.to_i
height = Math.sqrt(pixels * (current_geometry.height.to_f / current_geometry.width)).round.to_i
Paperclip::Geometry.new(width, height)
end
end
def initialize(file, options = {}, attachment = nil)
super
@crop = options[:geometry].to_s[-1, 1] == '#'
@current_geometry = options.fetch(:file_geometry_parser, Geometry).from_file(@file)
@target_geometry = options[:pixels] ? PixelGeometryParser.parse(@current_geometry, options[:pixels]) : options.fetch(:string_geometry_parser, Geometry).parse(options[:geometry].to_s)
@format = options[:format]
@current_format = File.extname(@file.path)
@basename = File.basename(@file.path, @current_format)
correct_current_format!
correct_target_geometry!
end
def make
return File.open(@file.path) unless needs_convert?
dst = TempfileFactory.new.generate([@basename, @format ? ".#{@format}" : @current_format].join)
transformed_image.write_to_file(dst.path, **save_options)
dst
end
private
def correct_target_geometry!
min_side = [@current_geometry.width, @current_geometry.height].min.to_i
@target_geometry = Paperclip::Geometry.new(min_side, min_side) if @target_geometry&.square? && min_side < @target_geometry.width
end
def correct_current_format!
# If the attachment was uploaded through a base64 payload, the tempfile
# will not have a file extension. It could also have the wrong file extension,
# depending on what the uploaded file was named. We correct for this in the final
# file name, which is however not yet physically in place on the temp file, so we
# need to use it here. Mind that this only reliably works if this processor is
# the first in line and we're working with the original, unmodified file.
@current_format = File.extname(attachment.instance_read(:file_name))
end
def transformed_image
# libvips has some optimizations for resizing an image on load. If we don't need to
# resize the image, we have to load it a different way.
if @target_geometry.nil?
return Vips::Image.new_from_file(preserve_animation? ? "#{@file.path}[n=-1]" : @file.path, access: :sequential).copy.mutate do |mutable|
(mutable.get_fields - ALLOWED_FIELDS).each do |field|
mutable.remove!(field)
end
end
end
# libvips thumbnail operation does not work correctly on animated GIFs. If we need to
# preserve the animation, we have to load all the frames and then manually crop
# them to then reassemble.
if preserve_animation?
original_image = Vips::Image.new_from_file("#{@file.path}[n=-1]", access: :sequential)
n_pages = original_image.get('n-pages')
# The loaded image has each frame stacked on top of each other, therefore we must
# account for this when giving the resizing constraint, otherwise the width will
# always end up smaller than we want.
resized_image = original_image.thumbnail_image(@target_geometry.width, height: @target_geometry.height * n_pages, size: :down).mutate do |mutable|
(mutable.get_fields - ALLOWED_FIELDS).each do |field|
mutable.remove!(field)
end
end
# If we don't need to crop the image, then we're already done. Otherwise,
# we need to manually crop each frame of the animation and reassemble them.
return resized_image unless @crop
page_height = resized_image.get('page-height')
frames = (0...n_pages).map do |i|
resized_image.crop(0, i * page_height, @target_geometry.width, @target_geometry.height)
end
Vips::Image.arrayjoin(frames, across: 1).copy.mutate do |mutable|
mutable.set!('page-height', @target_geometry.height)
end
else
Vips::Image.thumbnail(@file.path, @target_geometry.width, height: @target_geometry.height, **thumbnail_options).mutate do |mutable|
(mutable.get_fields - ALLOWED_FIELDS).each do |field|
mutable.remove!(field)
end
end
end
end
def thumbnail_options
@crop ? { crop: :centre } : { size: :down }
end
def save_options
case @format
when 'jpg'
{ Q: 90, interlace: true }
else
{}
end
end
def preserve_animation?
@format == 'gif' || (@format.blank? && @current_format == '.gif')
end
def needs_convert?
needs_different_geometry? || needs_different_format? || needs_metadata_stripping?
end
def needs_different_geometry?
(options[:geometry] && @current_geometry.width != @target_geometry.width && @current_geometry.height != @target_geometry.height) ||
(options[:pixels] && @current_geometry.width * @current_geometry.height > options[:pixels])
end
def needs_different_format?
@format.present? && @current_format != ".#{@format}"
end
def needs_metadata_stripping?
@attachment.instance.respond_to?(:local?) && @attachment.instance.local?
end
end
end
|