# frozen_string_literal: true 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.exist?(filename) fullsize = fullsize_path(identity) generate_fullsize(identity) if !cache || !File.exist?(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 NimbusSans-Regular -gravity Center -annotate -0+34 #{letter} -depth 8 #{filename} ] Discourse::Utils.execute_command("convert", *instructions) ## 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 begin skip = File.basename(cache_path) parent_path = File.dirname(cache_path) Dir .entries(parent_path) .each do |path| FileUtils.rm_rf(parent_path + "/" + path) unless %w[. ..].include?(path) || path == skip end rescue Errno::ENOENT # no worries, folder doesn't exists end end end # palette of optimally distinct 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