Skip to content

Instantly share code, notes, and snippets.

@cxx
Created December 2, 2011 16:50
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cxx/1423946 to your computer and use it in GitHub Desktop.
Save cxx/1423946 to your computer and use it in GitHub Desktop.
diet animated GIF
require 'RMagick'
module DietGIF
MAX_BYTESIZE = 1024 * 1024
MAX_SIZE = [500, 700]
MID_SIZE = [400, 600]
MIN_SIZE = [250, 400]
MIN_COLORS = 32
def diet(src)
orig = GIF.from_blob(src)
coa = orig.coalesce.scale
return "" if orig.length <= 1 || (!orig.should_reduce? && (!orig.should_scale? || within_limit?(coa.bytesize * 1.1)))
if coa.minimize.should_reduce?
coa_max = coa
coa = orig.coalesce.scale(*MIN_SIZE) if coa.should_scale?(*MIN_SIZE)
if coa.minimize.should_reduce?
ratio = ([coa_max, coa].map {|g| g.minimize.bytesize }.min.to_f / MAX_BYTESIZE).ceil
coa = [coa_max, coa].find {|g| within_limit?(g.minimize.bytesize / ratio) }.decimate(ratio)
elsif coa_max.should_scale?(*MID_SIZE)
coa_max = nil
coa_max = orig.coalesce.scale(*MID_SIZE)
coa = coa_max unless coa_max.minimize.should_reduce?
end
end
orig = coa_max = nil
opt = coa.optimize
if !coa.minimize.should_reduce? && !opt.should_reduce?
best = opt
l, u = 0, 4
begin
m = (l + u) / 2
opt = coa.optimize(m)
if opt.should_reduce?
l = m + 1
else
best = opt
u = m - 1
end
end while l <= u
return best.to_blob
end
best, nc = coa.minimize.should_reduce? ? [nil, 8] : [coa.minimize, 128]
coa = nil
qua = opt.quantize(nc)
if qua.should_reduce?
nc /= 2
else
best = qua
nc *= 2
end
qua = opt.quantize(nc)
best = qua unless qua.should_reduce?
best.to_blob if best
end
module_function :diet
def within_limit?(bytesize)
bytesize <= MAX_BYTESIZE
end
module_function :within_limit?
class GIF
include Magick
def initialize(image_list, blob=nil)
@image_list = image_list
@blob = blob
@optimized = nil
@quantized = nil
end
def coalesce
GIF.new(@image_list.coalesce)
end
def optimize(fuzz=5)
return @optimized if fuzz == 5 && @optimized
@image_list.each {|f| f.fuzz = "#{fuzz}%" }
list = @image_list.optimize_layers(OptimizeLayer)
list.each do |f|
page = f.page
page.width, page.height = f.columns, f.rows
f.page = page
end
optimized = GIF.new(list)
@optimized = optimized if fuzz == 5
optimized
end
def quantize(nc=MIN_COLORS)
return @quantized if nc == MIN_COLORS && @quantized
list = @image_list.quantize(nc, RGBColorspace, FloydSteinbergDitherMethod)
quantized = GIF.new(list, list.to_blob)
@quantized = quantized if nc == MIN_COLORS
quantized
end
def minimize
optimize.quantize
end
def scale(w=0, h=w)
w, h = MAX_SIZE if w.zero?
if should_scale?(w, h)
@image_list.each {|f| f.resize_to_fit!(w, h) }
clear_cache
end
self
end
def decimate(ratio)
new_frames = @image_list.each_slice(ratio).map do |a|
a.first.delay = a.inject(0) {|result,f| result + f.delay }
a.first
end
@image_list.clear.concat(new_frames)
clear_cache
self
end
def bytesize
to_blob.bytesize
end
def width
@image_list.first.columns
end
def height
@image_list.first.rows
end
def length
@image_list.length
end
def should_reduce?
!DietGIF.within_limit?(bytesize)
end
def should_scale?(w=0, h=w)
w, h = MAX_SIZE if w.zero?
width > w || height > h
end
def to_blob
@blob ||= @image_list.copy.to_blob
end
def self.from_blob(blob)
GIF.new(ImageList.new.from_blob(blob), blob)
end
private
def clear_cache
@blob = @optimized = @quantized = nil
end
end
end
module Kernel
def diet_gif(blob)
DietGIF.diet(blob)
end
module_function :diet_gif
end
if __FILE__ == $PROGRAM_NAME && ARGV.length == 2
require 'open-uri'
infile, outfile = ARGV
orig = open(infile, "rb:ASCII-8BIT") {|io| io.read }
dieted = diet_gif(orig)
IO.write(outfile, dieted.empty? ? orig : dieted)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment