class LetterAvatar class Identity attr_accessor :color, :letter def self.from_username(username) identity = new identity.color = LetterAvatar::COLORS[ Digest::MD5.hexdigest(username)[0...15].to_i(16) % LetterAvatar::COLORS.length ] identity.letter = username[0].upcase identity end end # BUMP UP if avatar algorithm changes VERSION = 5 # CHANGE these values to support more pixel ratios FULLSIZE = 120 * 3 POINTSIZE = 280 class << self def version "#{VERSION}_#{image_magick_version}" end def cache_path "tmp/letter_avatars/#{version}" end def generate(username, size, opts = nil) DistributedMutex.synchronize("letter_avatar_#{version}_#{username}") do identity = (opts && opts[:identity]) || LetterAvatar::Identity.from_username(username) cache = true cache = false if opts && opts[:cache] == false size = FULLSIZE if size > FULLSIZE filename = cached_path(identity, size) return filename if cache && File.exists?(filename) fullsize = fullsize_path(identity) generate_fullsize(identity) if !cache || !File.exists?(fullsize) # Optimizing here is dubious, it can save up to 2x for large images (eg 359px) # BUT... we are talking 2400 bytes down to 1200 bytes, both fit in one packet # The cost of this is huge, its a 40% perf hit OptimizedImage.resize(fullsize, filename, size, size) filename end end def cached_path(identity, size) dir = "#{cache_path}/#{identity.letter}/#{identity.color.join("_")}" FileUtils.mkdir_p(dir) File.expand_path "#{dir}/#{size}.png" end def fullsize_path(identity) File.expand_path cached_path(identity, FULLSIZE) end def generate_fullsize(identity) color = identity.color letter = identity.letter filename = fullsize_path(identity) instructions = %W{ -size #{FULLSIZE}x#{FULLSIZE} xc:#{to_rgb(color)} -pointsize #{POINTSIZE} -fill '#FFFFFFCC' -font 'Helvetica' -gravity Center -annotate -0+26 '#{letter}' -depth 8 '#{filename}' } `convert #{instructions.join(" ")}` ## do not optimize image, it will end up larger than original filename end def to_rgb(color) r,g,b = color "'rgb(#{r},#{g},#{b})'" end def image_magick_version @image_magick_version ||= begin Thread.new do sleep 2 cleanup_old end Digest::MD5.hexdigest(`convert --version` << `convert -list font`) end end def cleanup_old skip = File.basename(cache_path) parent_path = File.dirname(cache_path) Dir.entries(parent_path).each do |path| unless ['.','..'].include?(path) || path == skip FileUtils.rm_rf(parent_path + "/" + path) end end end end # palette of optimally disctinct colors # cf. http://tools.medialab.sciences-po.fr/iwanthue/index.php # parameters used: # - H: 0 - 360 # - C: 0 - 2 # - L: 0.75 - 1.5 COLORS = [[198,125,40], [61,155,243], [74,243,75], [238,89,166], [52,240,224], [177,156,155], [240,120,145], [111,154,78], [237,179,245], [237,101,95], [89,239,155], [43,254,70], [163,212,245], [65,152,142], [165,135,246], [181,166,38], [187,229,206], [77,164,25], [179,246,101], [234,93,37], [225,155,115], [142,140,188], [223,120,140], [249,174,27], [244,117,225], [137,141,102], [75,191,146], [188,239,142], [164,199,145], [173,120,149], [59,195,89], [222,198,220], [68,145,187], [236,204,179], [159,195,72], [188,121,189], [166,160,85], [181,233,37], [236,177,85], [121,147,160], [234,218,110], [241,157,191], [62,200,234], [133,243,34], [88,149,110], [59,228,248], [183,119,118], [251,195,45], [113,196,122], [197,115,70], [80,175,187], [103,231,238], [240,72,133], [228,149,241], [180,188,159], [172,132,85], [180,135,251], [236,194,58], [217,176,109], [88,244,199], [186,157,239], [113,230,96], [206,115,165], [244,178,163], [230,139,26], [241,125,89], [83,160,66], [107,190,166], [197,161,210], [198,203,245], [238,117,19], [228,119,116], [131,156,41], [145,178,168], [139,170,220], [233,95,125], [87,178,230], [157,200,119], [237,140,76], [229,185,186], [144,206,212], [236,209,158], [185,189,79], [34,208,66], [84,238,129], [133,140,134], [67,157,94], [168,179,25], [140,145,240], [151,241,125], [67,162,107], [200,156,21], [169,173,189], [226,116,189], [133,231,191], [194,161,63], [241,77,99], [241,217,53], [123,204,105], [210,201,119], [229,108,155], [240,91,72], [187,115,210], [240,163,100], [178,217,57], [179,135,116], [204,211,24], [186,135,57], [223,176,135], [204,148,151], [116,223,50], [95,195,46], [123,160,236], [181,172,131], [142,220,202], [240,140,112], [172,145,164], [228,124,45], [135,151,243], [42,205,125], [192,233,116], [119,170,114], [158,138,26], [73,190,183], [185,229,243], [227,107,55], [196,205,202], [132,143,60], [233,192,237], [62,150,220], [205,201,141], [106,140,190], [161,131,205], [135,134,158], [198,139,81], [115,171,32], [101,181,67], [149,137,119], [37,142,183], [183,130,175], [168,125,133], [124,142,87], [236,156,171], [232,194,91], [219,200,69], [144,219,34], [219,95,187], [145,154,217], [165,185,100], [127,238,163], [224,178,198], [119,153,120], [124,212,92], [172,161,105], [231,155,135], [157,132,101], [122,185,146], [53,166,51], [70,163,90], [150,190,213], [210,107,60], [166,152,185], [159,194,159], [39,141,222], [202,176,161], [95,140,229], [168,142,87], [93,170,203], [159,142,54], [14,168,39], [94,150,149], [187,206,136], [157,224,166], [235,158,208], [109,232,216], [141,201,87], [208,124,118], [142,125,214], [19,237,174], [72,219,41], [234,102,111], [168,142,79], [188,135,35], [95,155,143], [148,173,116], [223,112,95], [228,128,236], [206,114,54], [195,119,88], [235,140,94], [235,202,125], [233,155,153], [214,214,238], [246,200,35], [151,125,171], [132,145,172], [131,142,118], [199,126,150], [61,162,123], [58,176,151], [215,141,69], [225,154,220], [220,77,167], [233,161,64], [130,221,137], [81,191,129], [169,162,140], [174,177,222], [236,174,47], [233,188,180], [69,222,172], [71,232,93], [118,211,238], [157,224,83], [218,105,73], [126,169,36]] end