hensei-api/app/services/preview_service/canvas.rb
Justin Edmund 00890eda10 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.
2025-01-18 08:55:38 -08:00

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