Created
December 2, 2011 16:50
-
-
Save cxx/1423946 to your computer and use it in GitHub Desktop.
diet animated GIF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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