root/trunk/lib/image_builder.rb

Revision 1212, 8.4 kB (checked in by gaspard, 3 months ago)

commit cd6ceac2af29dc3fe3408a0879f44436ddaeb0a6
Author: Gaspard Bucher <gaspard@teti.ch>

There seemed to be a problem with the last favicon (was all orange).

Line 
1 require 'tempfile'
2 begin
3   # this works on the deb box
4   require 'RMagick'
5 rescue LoadError
6   begin
7     # this works on my Mac
8     require 'rmagick'
9   rescue LoadError
10     puts "ImageMagick not found. Using dummy."
11     # Create a dummy magick module
12     module Magick
13       CenterGravity = OverCompositeOp = MaxRGB = NorthGravity = SouthGravity = nil
14       class << self
15       end
16       class ZenaDummy
17         def initialize(*a)
18         end
19         def method_missing(meth, *args)
20           # do nothing
21         end
22       end
23       class Image < ZenaDummy
24       end
25       class ImageList < ZenaDummy
26       end
27     end
28   end
29 end
30
31 class ImageBuilder
32   DEFAULT_FORMATS = {
33     'tiny' =>   { :name=>'tiny', :size=>:force, :width=>16,  :height=>16 , :gravity=>Magick::CenterGravity   }.freeze,
34     'mini' =>   { :name=>'mini', :size=>:force, :width=>32,  :height=>32 , :gravity=>Magick::CenterGravity   }.freeze,
35     'square' => { :name=>'square', :size=>:limit, :width=>180, :height=>180, :gravity=>Magick::CenterGravity   }.freeze,
36     'med'  =>   { :name=>'med',  :size=>:limit, :width=>280, :height=>186, :gravity=>Magick::CenterGravity   }.freeze,
37     'top'  =>   { :name=>'top',  :size=>:force, :width=>280, :height=>186, :gravity => Magick::NorthGravity  }.freeze,
38     'low'  =>   { :name=>'low',  :size=>:force, :width=>280, :height=>186, :gravity => Magick::SouthGravity  }.freeze,
39     'side' =>   { :name=>'side', :size=>:force, :width=>220, :height=>500, :gravity=>Magick::CenterGravity   }.freeze,
40     'std'  =>   { :name=>'std',  :size=>:limit, :width=>600, :height=>400, :gravity=>Magick::CenterGravity   }.freeze,
41     'pv'   =>   { :name=>'pv',   :size=>:force, :width=>70,  :height=>70 , :gravity=>Magick::CenterGravity   }.freeze,
42     'edit' =>   { :name=>'edit', :size=>:limit, :width=>400, :height=>400, :gravity=>Magick::CenterGravity   }.freeze,
43     'full' =>   { :name=>'full', :size=>:keep                            , :gravity=>Magick::CenterGravity   }.freeze,
44     nil    =>   { :name=>'full', :size=>:keep                            , :gravity=>Magick::CenterGravity   }.freeze,
45   }.freeze
46
47   # 'sepia'=>   { :size=>:limit, :width=>280, :ratio=>2/3.0, :post=>Proc.new {|img| img.sepiatone(Magick::MaxRGB * 0.8)}},
48  
49   class << self
50     def image_content_type?(content_type)
51       content_type =~ /image/
52     end
53     def dummy?
54       Magick.const_defined?(:ZenaDummy)
55     end
56   end
57
58   def initialize(h)
59     params = {:height=>nil, :width=>nil, :path=>nil, :file=>nil, :actions=>[]}.merge(h)
60    
61     params.each do |k,v|
62       case k
63       when :height
64         @height = v if v
65       when :width
66         @width = v if v
67       when :path
68         @path = v if v
69       when :file
70         @file = v if v
71       when :actions
72         if v.kind_of?(Array)
73           @actions = v
74         else
75           raise StandardError, "Bad actions format"
76         end
77       else
78         raise StandardError, "Bad parameter (#{k})"
79       end
80     end
81     unless @width && @height || dummy?
82       if @file || @path
83         if @file.kind_of?(StringIO)
84           @img = Magick::ImageList.new
85           @file.rewind
86           @img.from_blob(@file.read)
87           @file.rewind
88         else
89           @img = Magick::ImageList.new(@file ? @file.path : @path)
90         end
91         #@img.from_blob(@file.read) # .rewind
92         @width  = @img.columns
93         @height = @img.rows
94       end
95     end
96   end
97  
98   def dummy?
99     ImageBuilder.dummy? || (!@path && !@img && !@file)
100   end
101
102   def read
103     return nil if dummy?
104     render_img
105     @img.to_blob
106   end
107
108   def write(path)
109     return false if dummy?
110     render_img
111     @img.write(path)
112   end
113
114   def rows
115     return nil unless @height || !dummy?
116     (@height ||= render_img.rows).round
117   end
118  
119   def columns
120     return nil unless @width || !dummy?
121     (@width ||= render_img.columns).round
122   end
123
124   alias height rows
125   alias width columns
126
127   def resize!(s)
128     # we do not zoom pixels
129     return unless s < 1.0
130     @img = nil # reset current rendered image
131     @width  *= s
132     @height *= s
133     @actions << Proc.new {|img| img.resize!(s) }
134   end
135  
136   def crop!(x,y,w,h)
137     @img = nil # reset current rendered image
138     @width  = [@width -x, w].min
139     @height = [@height-y, h].min
140     @actions << Proc.new {|img| img.crop!(x,y,[@img.columns-x, w].min,[@img.rows-y, h].min, true) }
141   end
142  
143   def format=(fmt)
144     return unless !dummy? && Magick.formats[fmt.upcase] =~ /w/
145     @actions << Proc.new {|img| img.format = fmt.upcase; img }
146   end
147  
148   def format
149     render_img.format
150   end
151  
152   def max_filesize=(size)
153     @actions << Proc.new {|img| do_limit!(size) }
154   end
155  
156   def do_limit!(size)
157     return @img if @img.filesize <= size
158    
159     # Check real size
160     tmp_path = Tempfile.new('tmp_img').path
161     @img.write('jpeg:' + tmp_path)
162    
163     return @img if File.stat(tmp_path).size <= size
164    
165     # Change type to JPG and quality to 80
166     if (@img.format == 'JPG' || @img.format == 'JPEG') && @img.quality > 80
167       @img.write('jpeg:' + tmp_path) { self.quality = 80 }
168     else
169       @img.format = 'JPG'
170       @img.write('jpeg:' + tmp_path) { self.quality = 80 }
171     end
172     ratio = File.stat(tmp_path).size.to_f / size
173     return @img = Magick::ImageList.new(tmp_path) if ratio <= 1.0
174    
175     # Not enough ? Resize.
176     ratio   = 1.0 / Math.sqrt(ratio)
177     @width  *= ratio
178     @height *= ratio
179     @img.resize!(ratio)
180     @img
181   end
182  
183   def crop_min!(w,h,gravity=Magick::CenterGravity)
184     @img = nil # reset current rendered image
185     @width  = [@width ,w].min
186     @height = [@height,h].min
187     @actions << Proc.new {|img| img.crop!(gravity,[@img.columns,w].min,[@img.rows,h].min, true) }
188   end
189
190   def set_background!(opacity,w,h)
191     @img = nil # reset current rendered image
192     @width  = [@width ,w].max
193     @height = [@height,h].max
194     @actions << Proc.new do |img|
195       bg = Magick::Image.new(w,h)
196       bg.opacity = opacity
197       bg.format = img.format
198       img = bg.composite(img, Magick::CenterGravity, Magick::OverCompositeOp)
199     end
200   end
201
202   # Transform into another format. If nil : do nothing.
203   def transform!(tformat=nil)
204     return self unless tformat
205     @img = nil
206     format = { :size=>:limit, :gravity=>Magick::CenterGravity }.merge(tformat)
207     @pre, @post = format[:pre], format[:post]
208
209     if format[:size] == :keep
210       h,w = @height, @width
211     else
212       h,w = format[:height], format[:width]
213     end
214     if format[:scale]
215       if h || w
216         # scale is a pre-zoom before crop
217         scale = format[:scale]
218       else
219         # we resize to scale
220         h,w = @height*format[:scale], @width*format[:scale]
221         # but we do not zoom
222         scale = 1.0
223         # ignore ':size' format if not height nor width was given
224         format[:size] = :force
225       end
226     else
227       scale = 1.0
228     end
229     if format[:ratio] && h && !w
230       w = h / format[:ratio]
231     elsif format[:ratio] && w && !h
232       h = w * format[:ratio]
233     end
234
235     pw,ph = @width, @height
236     raise StandardError, "image size or thumb size is null" if [w,h,pw,ph].include?(nil) || [w,h,pw,ph].min <= 0
237
238     case format[:size]
239     when :force
240       crop_scale = [w.to_f/pw, h.to_f/ph].max
241       if crop_scale > 1.0
242         # we do not zoom. Fill with transparent background.
243         crop_min!(w,h,format[:gravity])
244         set_background!(Magick::MaxRGB, w, h)
245       else
246         resize!(crop_scale * scale)
247         crop_min!(w, h,format[:gravity])
248       end
249     when :force_no_crop
250       crop_scale = [w.to_f/pw, h.to_f/ph].min
251       resize!(crop_scale * scale)
252       crop_min!(w, h,format[:gravity])
253       set_background!(Magick::MaxRGB, w, h)
254     when :limit
255       crop_scale = [w.to_f/pw, h.to_f/ph].min
256       resize!(crop_scale * scale)
257       crop_min!(w, h,format[:gravity])
258     when :keep
259     end
260     self
261   end
262
263   def render_img
264     raise IOError, 'MagickDummy cannot render image' if ImageBuilder.dummy?
265     unless @img
266       if @file
267         @img = Magick::ImageList.new
268         @file.rewind # we have read this file once when saving to disk
269         @img.from_blob(@file.read)
270         @file.rewind
271       elsif @path
272         @img = Magick::ImageList.new(@path)
273       else
274         raise IOError, 'Cannot render image without path or file'
275       end
276       if @pre
277         @pre = [@pre].flatten
278         @pre.each do |a|
279           @img = a.call(@img)
280         end
281       end
282
283       if @actions
284         @actions.each do |a|
285           @img = a.call(@img)
286         end
287       end
288
289       if @post
290         @post = [@post].flatten
291         @post.each do |a|
292           @img = a.call(@img)
293         end
294       end
295     end
296     @img
297   end
298 end
Note: See TracBrowser for help on using the browser.