mirror of
https://github.com/discourse/discourse.git
synced 2026-08-05 11:52:32 +08:00
Letter avatars, dominant-color extraction, and generated topic OG images use ImageMagick today. These straightforward operations keep ImageMagick in the request and background-job paths while the wider image-processing migration remains behind a site setting. This commit moves the three operations to the stock libvips CLI. It permits untrusted libvips access only for explicit SVG loads, converts each OG asset through its expected loader, rejects unsafe SVG features and media-type mismatches, and bounds SVG asset rendering. Dominant-color extraction verifies the stored format before decoding and caches unsupported pixel layouts as an empty result. Failures do not fall back to ImageMagick. The companion discourse_docker change removes the unsupported TIFF loader from the libvips build.
419 lines
9.5 KiB
Ruby
Vendored
419 lines
9.5 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
require "erb"
|
|
|
|
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 = 6
|
|
|
|
# CHANGE these values to support more pixel ratios
|
|
FULLSIZE = 120 * 3
|
|
POINTSIZE = 280
|
|
SVG_TEXT_BASELINE = 245
|
|
private_constant :SVG_TEXT_BASELINE
|
|
FONT_LIST_TIMEOUT_SECONDS = 10
|
|
private_constant :FONT_LIST_TIMEOUT_SECONDS
|
|
|
|
class << self
|
|
def version
|
|
"#{VERSION}_#{vips_version}"
|
|
end
|
|
|
|
def cache_path
|
|
"tmp/letter_avatars/#{version}"
|
|
end
|
|
|
|
# Run `script/letter_avatar_pixel_diff` to inspect rendering changes.
|
|
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)
|
|
|
|
resize(fullsize, filename, 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)
|
|
filename = fullsize_path(identity)
|
|
|
|
Tempfile.create(%w[letter-avatar .svg]) do |svg|
|
|
svg.write(svg_for(identity))
|
|
svg.flush
|
|
|
|
Vips.run(
|
|
"vips",
|
|
"svgload",
|
|
svg.path,
|
|
filename,
|
|
read: [svg.path],
|
|
write: [File.dirname(filename)],
|
|
allow_untrusted: true,
|
|
)
|
|
end
|
|
filename
|
|
end
|
|
|
|
def vips_version
|
|
return @vips_version if @vips_version
|
|
|
|
fonts =
|
|
Discourse::Utils
|
|
.execute_command(
|
|
"fc-list",
|
|
"--format",
|
|
"%{file}\t%{family}\t%{style}\t%{fontversion}\n",
|
|
timeout: FONT_LIST_TIMEOUT_SECONDS,
|
|
)
|
|
.lines
|
|
.sort
|
|
.join
|
|
@vips_version =
|
|
Digest::MD5.hexdigest([VERSION.to_s, Vips.run("vips", "--version"), fonts].join("\0"))
|
|
|
|
Thread.new do
|
|
sleep 2
|
|
cleanup_old
|
|
end
|
|
|
|
@vips_version
|
|
end
|
|
|
|
def cleanup_old
|
|
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
|
|
|
|
private
|
|
|
|
def resize(from, to, size)
|
|
profile = Rails.root.join("vendor/data/RT_sRGB.icm").to_s
|
|
output = "#{to}[palette,Q=100,compression=6,strip]"
|
|
|
|
Tempfile.create(%w[letter-avatar-resized .png], binmode: true) do |resized|
|
|
resized.close
|
|
Vips.run(
|
|
"vips",
|
|
"thumbnail",
|
|
from,
|
|
"#{resized.path}[compression=6,strip]",
|
|
size.to_s,
|
|
"--height",
|
|
size.to_s,
|
|
"--size",
|
|
"both",
|
|
"--crop",
|
|
"centre",
|
|
"--output-profile",
|
|
profile,
|
|
read: [from, profile],
|
|
write: [resized.path],
|
|
nice: 10,
|
|
)
|
|
Vips.run(
|
|
"vips",
|
|
"sharpen",
|
|
resized.path,
|
|
output,
|
|
"--sigma",
|
|
"0.5",
|
|
"--m1",
|
|
"0.7",
|
|
read: [resized.path],
|
|
write: [File.dirname(to)],
|
|
nice: 10,
|
|
)
|
|
end
|
|
end
|
|
|
|
def svg_for(identity)
|
|
color = identity.color.join(",")
|
|
letter = ERB::Util.html_escape(identity.letter)
|
|
baseline = SVG_TEXT_BASELINE + vertical_offset
|
|
|
|
<<~SVG
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="#{FULLSIZE}" height="#{FULLSIZE}" viewBox="0 0 #{FULLSIZE} #{FULLSIZE}">
|
|
<rect width="#{FULLSIZE}" height="#{FULLSIZE}" fill="rgb(#{color})"/>
|
|
<text x="#{FULLSIZE / 2}" y="#{baseline}" fill="white" fill-opacity="0.8" font-family="#{font_family}" font-size="#{POINTSIZE}" text-anchor="middle" dominant-baseline="central">#{letter}</text>
|
|
</svg>
|
|
SVG
|
|
end
|
|
|
|
# Use Helvetica on macOS because Nimbus Sans is unavailable there.
|
|
def font_family
|
|
RbConfig::CONFIG["host_os"].match?(/darwin/i) ? "Helvetica" : "Nimbus Sans"
|
|
end
|
|
|
|
# Helvetica needs a larger offset to keep the glyph vertically centered.
|
|
def vertical_offset
|
|
font_family == "Helvetica" ? 26 : 34
|
|
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
|