Updates to Canvas and Coordinator
This commit is contained in:
parent
e9d9940d96
commit
8207b3a09a
2 changed files with 125 additions and 62 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue