* Add mini_magick and rufus-scheduler * Expose attributes and add sigs to AwsService * Get Party ready for preview state * Added new fields for preview state and generated_at timestamp * Add preview state enum to model * Add preview_relevant_changes? after_commit hook * Add jobs for generating and cleaning up party previews * Add new endpoints to PartiesController * `preview` shows the preview and queues it up for generation if it doesn't exist yet * `regenerate_preview` allows the party owner to force regeneration of previews * Schedule jobs * Stalled jobs are checked every 5 minutes * Failed jobs are retried every hour * Old preview jobs are cleaned up daily * Add the preview service This is where the bulk of the work is. This service renders out the preview images bit by bit. Currently we render the party name, creator, job icon, and weapon grid. This includes signatures and some fonts.
173 lines
5.1 KiB
Ruby
173 lines
5.1 KiB
Ruby
# app/services/canvas.rb
|
|
module PreviewService
|
|
class Canvas
|
|
PREVIEW_WIDTH = 1200
|
|
PREVIEW_HEIGHT = 630
|
|
DEFAULT_BACKGROUND_COLOR = '#1a1b1e'
|
|
|
|
# Padding and spacing constants
|
|
PADDING = 24
|
|
TITLE_IMAGE_GAP = 24
|
|
GRID_GAP = 4
|
|
|
|
def initialize(image_fetcher)
|
|
@image_fetcher = image_fetcher
|
|
end
|
|
|
|
def create_blank_canvas(width: PREVIEW_WIDTH, height: PREVIEW_HEIGHT, color: DEFAULT_BACKGROUND_COLOR)
|
|
temp_file = Tempfile.new(%w[canvas .png])
|
|
|
|
MiniMagick::Tool::Convert.new do |convert|
|
|
convert.size "#{width}x#{height}"
|
|
convert << "xc:#{color}"
|
|
convert << temp_file.path
|
|
end
|
|
|
|
temp_file
|
|
end
|
|
|
|
def add_text(image, party_name, job_icon: nil, user: nil, **options)
|
|
font_size = options.fetch(:size, '32')
|
|
font_color = options.fetch(:color, 'white')
|
|
|
|
# Load custom font for username, for later use
|
|
@font_path ||= Rails.root.join('app', 'assets', 'fonts', 'Gk-Bd.otf').to_s
|
|
|
|
# Measure party name text size
|
|
text_metrics = measure_text(party_name, font_size)
|
|
|
|
# Draw job icon if provided
|
|
image = draw_job_icon(image, job_icon) if job_icon
|
|
|
|
# Draw party name text
|
|
image = draw_party_name(image, party_name, text_metrics, job_icon, font_color, font_size)
|
|
|
|
# Compute vertical center of the party name text line
|
|
party_text_center_y = PADDING + (text_metrics[:height] / 2.0)
|
|
|
|
# Draw user info if provided
|
|
image = draw_user_info(image, user, party_text_center_y, font_color) if user
|
|
|
|
{
|
|
image: image,
|
|
text_bottom_y: PADDING + text_metrics[:height] + TITLE_IMAGE_GAP
|
|
}
|
|
end
|
|
|
|
private
|
|
|
|
def draw_job_icon(image, job_icon)
|
|
job_icon.format("png32")
|
|
job_icon.alpha('set')
|
|
job_icon.background('none')
|
|
job_icon.combine_options do |c|
|
|
c.filter "Lanczos" # High-quality filter
|
|
c.resize "64x64"
|
|
end
|
|
image = image.composite(job_icon) do |c|
|
|
c.compose "Over"
|
|
c.geometry "+#{PADDING}+#{PADDING}"
|
|
end
|
|
image
|
|
end
|
|
|
|
def draw_party_name(image, party_name, text_metrics, job_icon, font_color, font_size)
|
|
# Determine x position based on presence of job_icon
|
|
text_x = job_icon ? PADDING + 64 + 16 : PADDING
|
|
text_y = PADDING + text_metrics[:height]
|
|
|
|
image.combine_options do |c|
|
|
c.font @font_path
|
|
c.fill font_color
|
|
c.pointsize font_size
|
|
c.draw "text #{text_x},#{text_y} '#{party_name}'"
|
|
end
|
|
image
|
|
end
|
|
|
|
def draw_user_info(image, user, party_text_center_y, font_color)
|
|
username_font_size = 24
|
|
username_font_path = @font_path
|
|
|
|
# Fetch and prepare user picture
|
|
user_picture = @image_fetcher.fetch_user_picture(user.picture)
|
|
if user_picture
|
|
user_picture.format("png32")
|
|
user_picture.alpha('set')
|
|
user_picture.background('none')
|
|
user_picture.combine_options do |c|
|
|
c.filter "Lanczos" # Use a high-quality filter
|
|
c.resize "48x48"
|
|
end
|
|
end
|
|
|
|
# Measure username text size
|
|
username_metrics = measure_text(user.username, username_font_size, font: username_font_path)
|
|
|
|
right_padding = PADDING
|
|
total_user_width = 48 + 8 + username_metrics[:width]
|
|
user_x = image.width - right_padding - total_user_width
|
|
|
|
# Center user picture vertically relative to party text line
|
|
user_pic_y = (party_text_center_y - (48 / 2.0)).round
|
|
|
|
image = image.composite(user_picture) do |c|
|
|
c.compose "Over"
|
|
c.geometry "+#{user_x}+#{user_pic_y}"
|
|
end if user_picture
|
|
|
|
# Adjust text y-coordinate to better align vertically with the picture
|
|
# You may need to tweak the offset value based on visual inspection.
|
|
vertical_offset = 6 # Adjust this value as needed
|
|
user_text_y = (party_text_center_y + (username_metrics[:height] / 2.0) - vertical_offset).round
|
|
|
|
image.combine_options do |c|
|
|
c.font username_font_path
|
|
c.fill font_color
|
|
c.pointsize username_font_size
|
|
text_x = user_x + 48 + 12
|
|
c.draw "text #{text_x},#{user_text_y} '#{user.username}'"
|
|
end
|
|
|
|
image
|
|
end
|
|
|
|
def measure_text(text, font_size, font: 'Arial')
|
|
|
|
# Create a temporary file for the text measurement
|
|
temp_file = Tempfile.new(['text_measure', '.png'])
|
|
|
|
begin
|
|
# Use ImageMagick command to create an image with the text
|
|
command = [
|
|
'magick',
|
|
'-background', 'transparent',
|
|
'-fill', 'black',
|
|
'-font', font,
|
|
'-pointsize', font_size.to_s,
|
|
"label:#{text}",
|
|
temp_file.path
|
|
]
|
|
|
|
# Execute the command
|
|
system(*command)
|
|
|
|
# Use MiniMagick to read the image and get dimensions
|
|
image = MiniMagick::Image.open(temp_file.path)
|
|
|
|
{
|
|
height: image.height,
|
|
width: image.width
|
|
}
|
|
ensure
|
|
# Close and unlink the temporary file
|
|
temp_file.close
|
|
temp_file.unlink
|
|
end
|
|
rescue => e
|
|
Rails.logger.error "Text measurement error: #{e.message}"
|
|
# Fallback dimensions
|
|
{ height: 50, width: 200 }
|
|
end
|
|
end
|
|
end
|