Updates to Canvas and Coordinator

This commit is contained in:
Justin Edmund 2025-01-20 03:53:08 -08:00
parent e9d9940d96
commit 8207b3a09a
2 changed files with 125 additions and 62 deletions

View file

@ -44,11 +44,25 @@ module PreviewService
end end
def add_text(image, party_name, job_icon: nil, user: nil, **options) def add_text(image, party_name, job_icon: nil, user: nil, **options)
party_name = party_name.to_s.strip
party_name = 'Untitled' if party_name.empty?
font_size = options.fetch(:size, '32') font_size = options.fetch(:size, '32')
font_color = options.fetch(:color, 'white') font_color = options.fetch(:color, 'white')
# Load custom font for username, for later use # Try multiple font locations
@font_path = Rails.root.join('app', 'assets', 'fonts', 'Gk-Bd.otf').to_s font_locations = [
Rails.root.join('app', 'assets', 'fonts', 'Gk-Bd.otf').to_s,
Rails.root.join('public', 'assets', 'fonts', 'Gk-Bd.otf').to_s
]
@font_path = font_locations.find { |path| File.exist?(path) }
unless @font_path
Rails.logger.error("Font file not found in any location: #{font_locations.join(', ')}")
raise "Font file not found"
end
Rails.logger.info("Using font path: #{@font_path}") Rails.logger.info("Using font path: #{@font_path}")
unless File.exist?(@font_path) unless File.exist?(@font_path)
Rails.logger.error("Font file not found at: #{@font_path}") Rails.logger.error("Font file not found at: #{@font_path}")
@ -94,16 +108,18 @@ module PreviewService
end end
def draw_party_name(image, party_name, text_metrics, job_icon, font_color, font_size) 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_x = job_icon ? PADDING + 64 + 16 : PADDING
text_y = PADDING + text_metrics[:height] text_y = PADDING + text_metrics[:height]
image.combine_options do |c| image.combine_options do |c|
c.font @font_path c.gravity 'NorthWest'
c.fill font_color c.fill font_color
c.font @font_path
c.pointsize font_size c.pointsize font_size
c.draw "text #{text_x},#{text_y} '#{party_name}'" # Escape quotes and use pango markup for better text handling
c.annotate "0x0+#{text_x}+#{text_y}", party_name.gsub('"', '\"')
end end
image image
end end
@ -154,7 +170,13 @@ module PreviewService
image image
end end
def measure_text(text, font_size, font: 'Arial') def measure_text(text, font_size, font: @font_path)
# Ensure text is not empty and is properly escaped
text = text.to_s.strip
text = 'Untitled' if text.empty?
# Escape text for shell command
escaped_text = text.gsub(/'/, "'\\\\''")
# Create a temporary file for the text measurement # Create a temporary file for the text measurement
temp_file = Tempfile.new(['text_measure', '.png']) temp_file = Tempfile.new(['text_measure', '.png'])
@ -167,7 +189,7 @@ module PreviewService
'-fill', 'black', '-fill', 'black',
'-font', font, '-font', font,
'-pointsize', font_size.to_s, '-pointsize', font_size.to_s,
"label:#{text}", "label:'#{escaped_text}'", # Quote the text
temp_file.path temp_file.path
] ]
@ -181,15 +203,15 @@ module PreviewService
height: image.height, height: image.height,
width: image.width width: image.width
} }
rescue => e
Rails.logger.error "Text measurement error: #{e.message}"
# Fallback dimensions
{ height: 50, width: 200 }
ensure ensure
# Close and unlink the temporary file # Close and unlink the temporary file
temp_file.close temp_file.close
temp_file.unlink temp_file.unlink
end end
rescue => e
Rails.logger.error "Text measurement error: #{e.message}"
# Fallback dimensions
{ height: 50, width: 200 }
end end
end end
end end

View file

@ -8,6 +8,8 @@ module PreviewService
GENERATION_TIMEOUT = 5.minutes GENERATION_TIMEOUT = 5.minutes
LOCAL_STORAGE_PATH = Rails.root.join('storage', 'party-previews') LOCAL_STORAGE_PATH = Rails.root.join('storage', 'party-previews')
# Public Interface - Core Operations
# Initialize the party preview service # Initialize the party preview service
# #
# @param party [Party] The party to generate a preview for # @param party [Party] The party to generate a preview for
@ -103,6 +105,8 @@ module PreviewService
end end
# Deletes the existing preview image for the party # Deletes the existing preview image for the party
#
# @return [void]
def delete_preview def delete_preview
if Rails.env.production? if Rails.env.production?
delete_s3_preview delete_s3_preview
@ -118,6 +122,8 @@ module PreviewService
Rails.logger.error("Failed to delete preview for party #{@party.id}: #{e.message}") Rails.logger.error("Failed to delete preview for party #{@party.id}: #{e.message}")
end end
# State Management - Public
# Determines if a new preview should be generated # Determines if a new preview should be generated
# #
# @return [Boolean] True if a new preview should be generated, false otherwise # @return [Boolean] True if a new preview should be generated, false otherwise
@ -150,15 +156,43 @@ module PreviewService
false false
end end
private # Checks if a preview generation is currently in progress
#
# @return [Boolean] True if a preview is being generated, false otherwise
def generation_in_progress?
in_progress = Rails.cache.exist?("party_preview_generating_#{@party.id}")
Rails.logger.info("Cache key check for generation_in_progress: #{in_progress}")
in_progress
end
# Sets up the appropriate storage system based on environment # Retrieves the S3 object for the party's preview image
def setup_storage #
# Always initialize AWS service for potential image fetching # @return [Aws::S3::Types::GetObjectOutput] S3 object containing the preview image
@aws_service = AwsService.new # @raise [Aws::S3::Errors::NoSuchKey] If the preview image doesn't exist in S3
# @raise [Aws::S3::Errors::NoSuchBucket] If the configured bucket doesn't exist
def get_s3_object
@aws_service.s3_client.get_object(
bucket: @aws_service.bucket,
key: preview_key
)
end
# Create local storage paths in development # Schedules a background job to generate the preview
FileUtils.mkdir_p(LOCAL_STORAGE_PATH) unless Dir.exist?(LOCAL_STORAGE_PATH.to_s) #
# @return [void]
def schedule_generation
GeneratePartyPreviewJob
.set(wait: 30.seconds)
.perform_later(@party.id)
@party.update!(preview_state: :queued)
end
# Returns the full path for storing preview images locally
#
# @return [Pathname] Full path where the preview image should be stored
def local_preview_path
LOCAL_STORAGE_PATH.join(preview_filename)
end end
# Creates the preview image for the party # Creates the preview image for the party
@ -225,6 +259,21 @@ module PreviewService
image image
end end
private
# Sets up the appropriate storage system based on environment
#
# @return [void]
def setup_storage
# Always initialize AWS service for potential image fetching
@aws_service = AwsService.new
# Create local storage paths in development
FileUtils.mkdir_p(LOCAL_STORAGE_PATH) unless Dir.exist?(LOCAL_STORAGE_PATH.to_s)
end
# Image Generation Pipeline
# Adds the job icon to the preview image # Adds the job icon to the preview image
# #
# @param image [MiniMagick::Image] The base image # @param image [MiniMagick::Image] The base image
@ -241,6 +290,7 @@ module PreviewService
# Organizes and draws weapons on the preview image # Organizes and draws weapons on the preview image
# #
# @param image [MiniMagick::Image] The base image # @param image [MiniMagick::Image] The base image
# @param grid_layout [Hash] The layout configuration for the grid
# @return [MiniMagick::Image] The updated image with weapons # @return [MiniMagick::Image] The updated image with weapons
def organize_and_draw_weapons(image, grid_layout) def organize_and_draw_weapons(image, grid_layout)
mainhand_weapon = @party.weapons.find(&:mainhand) mainhand_weapon = @party.weapons.find(&:mainhand)
@ -277,9 +327,12 @@ module PreviewService
end end
end end
# Storage Operations
# Saves the preview image to the appropriate storage system # Saves the preview image to the appropriate storage system
# #
# @param image [MiniMagick::Image] The image to save # @param image [MiniMagick::Image] The image to save
# @return [void]
def save_preview(image) def save_preview(image)
if Rails.env.production? if Rails.env.production?
upload_to_s3(image) upload_to_s3(image)
@ -291,14 +344,14 @@ module PreviewService
# Uploads the preview image to S3 # Uploads the preview image to S3
# #
# @param image [MiniMagick::Image] The image to upload # @param image [MiniMagick::Image] The image to upload
# @return [void]
def upload_to_s3(image) def upload_to_s3(image)
temp_file = Tempfile.new(['preview', '.png']) temp_file = Tempfile.new(%w[preview .png])
begin begin
image.write(temp_file.path) image.write(temp_file.path)
# Use timestamped filename similar to local storage # Use fixed key without timestamp
timestamp = Time.current.strftime('%Y%m%d%H%M%S') key = "#{PREVIEW_FOLDER}/#{@party.shortcode}.png"
key = "#{PREVIEW_FOLDER}/#{@party.shortcode}_#{timestamp}.png"
File.open(temp_file.path, 'rb') do |file| File.open(temp_file.path, 'rb') do |file|
@aws_service.s3_client.put_object( @aws_service.s3_client.put_object(
@ -310,7 +363,6 @@ module PreviewService
) )
end end
# Optionally, store this key on the party record if needed for retrieval
@party.update!(preview_s3_key: key) @party.update!(preview_s3_key: key)
ensure ensure
temp_file.close temp_file.close
@ -321,29 +373,18 @@ module PreviewService
# Saves the preview image to local storage # Saves the preview image to local storage
# #
# @param image [MiniMagick::Image] The image to save # @param image [MiniMagick::Image] The image to save
# @return [void]
def save_to_local_storage(image) def save_to_local_storage(image)
# Remove any existing previews for this party
Dir.glob(LOCAL_STORAGE_PATH.join("#{@party.shortcode}_*.png").to_s).each do |file|
File.delete(file)
end
# Save new version
image.write(local_preview_path) image.write(local_preview_path)
end end
# Generates a timestamped filename for the preview image # Path & URL Generation
#
# @return [String] Filename in format "shortcode_YYYYMMDDHHMMSS.png"
def preview_filename
timestamp = Time.current.strftime('%Y%m%d%H%M%S')
"#{@party.shortcode}_#{timestamp}.png"
end
# Returns the full path for storing preview images locally # Generates a filename for the preview image
# #
# @return [Pathname] Full path where the preview image should be stored # @return [String] Filename for the preview image
def local_preview_path def preview_filename
LOCAL_STORAGE_PATH.join(preview_filename) "#{@party.shortcode}.png"
end end
# Returns the URL for accessing locally stored preview images # Returns the URL for accessing locally stored preview images
@ -364,6 +405,8 @@ module PreviewService
"#{PREVIEW_FOLDER}/#{@party.shortcode}.png" "#{PREVIEW_FOLDER}/#{@party.shortcode}.png"
end end
# Preview State Management
# Checks if a preview image exists for the party # Checks if a preview image exists for the party
# #
# @return [Boolean] True if a preview exists, false otherwise # @return [Boolean] True if a preview exists, false otherwise
@ -371,10 +414,9 @@ module PreviewService
return false unless @party.preview_state == 'generated' return false unless @party.preview_state == 'generated'
if Rails.env.production? if Rails.env.production?
@aws_service.s3_client.head_object(bucket: S3_BUCKET, key: preview_key) @aws_service.file_exists?(preview_key)
true
else else
!Dir.glob(LOCAL_STORAGE_PATH.join("#{@party.shortcode}_*.png").to_s).empty? !Dir.glob(LOCAL_STORAGE_PATH.join("#{@party.shortcode}.png").to_s).empty?
end end
rescue Aws::S3::Errors::NotFound rescue Aws::S3::Errors::NotFound
false false
@ -387,22 +429,15 @@ module PreviewService
signer = Aws::S3::Presigner.new(client: @aws_service.s3_client) signer = Aws::S3::Presigner.new(client: @aws_service.s3_client)
signer.presigned_url( signer.presigned_url(
:get_object, :get_object,
bucket: S3_BUCKET, bucket: @aws_service.bucket,
key: preview_key, key: preview_key,
expires_in: 1.hour expires_in: 1.hour.to_i
) )
end end
# Checks if a preview generation is currently in progress
#
# @return [Boolean] True if a preview is being generated, false otherwise
def generation_in_progress?
in_progress = Rails.cache.exist?("party_preview_generating_#{@party.id}")
Rails.logger.info("Cache key check for generation_in_progress: #{in_progress}")
in_progress
end
# Marks the preview generation as in progress # Marks the preview generation as in progress
#
# @return [void]
def set_generation_in_progress def set_generation_in_progress
Rails.cache.write( Rails.cache.write(
"party_preview_generating_#{@party.id}", "party_preview_generating_#{@party.id}",
@ -412,18 +447,15 @@ module PreviewService
end end
# Clears the in-progress flag for preview generation # Clears the in-progress flag for preview generation
#
# @return [void]
def clear_generation_in_progress def clear_generation_in_progress
Rails.cache.delete("party_preview_generating_#{@party.id}") Rails.cache.delete("party_preview_generating_#{@party.id}")
end end
# Schedules a background job to generate the preview # Job Scheduling
def schedule_generation
GeneratePartyPreviewJob
.set(wait: 30.seconds)
.perform_later(@party.id)
@party.update!(preview_state: :queued) # URL Generation
end
# Provides a default preview URL based on party attributes # Provides a default preview URL based on party attributes
# #
@ -436,24 +468,33 @@ module PreviewService
end end
end end
# Cleanup Operations
# Deletes the preview from S3 # Deletes the preview from S3
#
# @return [void]
def delete_s3_preview def delete_s3_preview
@aws_service.s3_client.delete_object( @aws_service.s3_client.delete_object(
bucket: S3_BUCKET, bucket: @aws_service.bucket,
key: preview_key key: preview_key
) )
end end
# Deletes local preview files # Deletes local preview files
#
# @return [void]
def delete_local_previews def delete_local_previews
Dir.glob(LOCAL_STORAGE_PATH.join("#{@party.shortcode}_*.png").to_s).each do |file| Dir.glob(LOCAL_STORAGE_PATH.join("#{@party.shortcode}_*.png").to_s).each do |file|
File.delete(file) File.delete(file)
end end
end end
# Error Handling
# Handles errors during preview generation # Handles errors during preview generation
# #
# @param error [Exception] The error that occurred # @param error [Exception] The error that occurred
# @return [void]
def handle_preview_generation_error(error) def handle_preview_generation_error(error)
Rails.logger.error("Preview generation failed for party #{@party.id}") Rails.logger.error("Preview generation failed for party #{@party.id}")
Rails.logger.error("Error: #{error.class} - #{error.message}") Rails.logger.error("Error: #{error.class} - #{error.message}")