discourse/lib/letter_avatar.rb
Sam Saffron 30990006a9 DEV: enable frozen string literal on all files
This reduces chances of errors where consumers of strings mutate inputs
and reduces memory usage of the app.

Test suite passes now, but there may be some stuff left, so we will run
a few sites on a branch prior to merging
2019-05-13 09:31:32 +08:00

347 lines
8.0 KiB
Ruby

# 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.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}
}
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|
unless ['.', '..'].include?(path) || path == skip
FileUtils.rm_rf(parent_path + "/" + path)
end
end
rescue Errno::ENOENT
# no worries, folder doesn't exists
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