in src/Relecloud.TicketRenderer/Services/TicketRenderer.cs [12:118]
internal class TicketRenderer(ILogger<TicketRenderer> logger, IImageStorage imageStorage, IBarcodeGenerator barcodeGenerator) : ITicketRenderer
{
// Default ticket image name format string (in case no path is specified).
private const string TicketNameFormatString = "ticket-{0}.png";
private static readonly Dictionary<string, SKTypeface> Typefaces = GetFonts();
public async Task<string?> RenderTicketAsync(TicketRenderRequestMessage request, CancellationToken cancellationToken)
{
logger.LogInformation("Rendering ticket {ticket} for message {message}", request.Ticket?.Id.ToString() ?? "<null>", request.MessageId);
// Error checking to ensure that we have all the necessary information to render the ticket.
if (request.Ticket == null)
{
logger.LogWarning("Nothing to render for null ticket");
return null;
}
if (request.Ticket.Concert == null)
{
logger.LogWarning("Cannot find the concert related to this ticket");
return null;
}
if (request.Ticket.User == null)
{
logger.LogWarning("Cannot find the user related to this ticket");
return null;
}
if (request.Ticket.Customer == null)
{
logger.LogWarning("Cannot find the customer related to this ticket");
return null;
}
// Generate Skia assets for creating the image.
// SkiaSharp is a recommended cross-platform third-party open source alternative to System.Drawing which works.
// See https://learn.microsoft.com/dotnet/core/compatibility/core-libraries/6.0/system-drawing-common-windows-only#recommended-action
using var headerFont = new SKFont(Typefaces["OpenSans-Bold"], 18);
using var textFont = new SKFont(Typefaces["OpenSans-Regular"], 12);
using var bluePaint = new SKPaint { Color = SKColors.DarkSlateBlue, Style = SKPaintStyle.StrokeAndFill, IsAntialias = true };
using var grayPaint = new SKPaint { Color = SKColors.Gray, Style = SKPaintStyle.StrokeAndFill, IsAntialias = true };
using var blackPaint = new SKPaint { Color = SKColors.Black, Style = SKPaintStyle.StrokeAndFill, IsAntialias = true };
using var surface = SKSurface.Create(new SKImageInfo(640, 200, SKColorType.Rgb888x));
// Initialize and clear the canvas.
var canvas = surface.Canvas;
canvas.Clear(SKColors.White);
// Print concert details.
canvas.DrawText(SKTextBlob.Create(request.Ticket.Concert.Artist, headerFont), 10, 30, bluePaint);
canvas.DrawText(SKTextBlob.Create($"{request.Ticket.Concert.Location} | {request.Ticket.Concert.StartTime.UtcDateTime:yyyy-MM-dd hh:mm tt}", textFont), 10, 50, grayPaint);
canvas.DrawText(SKTextBlob.Create($"{request.Ticket.Customer.Email} | ${request.Ticket.Concert.Price:F2}", textFont), 10, 70, grayPaint);
// Print a fake barcode.
var barcode = barcodeGenerator.GenerateBarcode(request.Ticket).GetEnumerator();
var offset = 15;
while (barcode.MoveNext())
{
var width = barcode.Current;
canvas.DrawRect(offset, 95, width, 90, blackPaint);
if (barcode.MoveNext())
{
offset += width + barcode.Current;
}
}
using var image = surface.Snapshot();
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
// Generate an output path for the image if none is specified.
var outputPath = string.IsNullOrEmpty(request.OutputPath)
? string.Format(TicketNameFormatString, request.Ticket.Id)
: request.OutputPath;
if (await imageStorage.StoreImageAsync(data.AsStream(), outputPath, cancellationToken))
{
return outputPath;
}
else
{
logger.LogError("Failed to store image for ticket {TicketId}", request.Ticket.Id);
return null;
}
}
// Helper method to load fonts from embedded resources.
// Small Linux images (like the chiseled ones used with this project)
// don't have fonts installed by default, so we need to load them from resources.
private static Dictionary<string, SKTypeface> GetFonts()
{
static string GetFontName(string resourceName)
{
// The resource names are in the format "Relecloud.TicketRenderer.Fonts.<font name>.ttf".
// This finds the last period in the name prior to the .ttf extension and takes the substring
// between that index and the extension.
var index = resourceName.LastIndexOf('.', resourceName.Length - 5);
return resourceName.Substring(index + 1, resourceName.Length - (index + 1) - 4);
}
var assembly = typeof(TicketRenderer).Assembly;
var fontResourceNames = assembly.GetManifestResourceNames().Where(s => s.Contains("Fonts"));
return fontResourceNames.ToDictionary(
GetFontName,
name => SKTypeface.FromStream(assembly.GetManifestResourceStream(name)));
}
}